我在Node.js spawn
参数中使用双引号,因为它们可能包含空格:
const excludes = ['/foo/bar', '/foo/baz', '/foo/bar baz'];
const tar = spawn('tar', [
'--create', '--gzip',
// '--exclude="/foo/bar"', '--exclude="/foo/baz"', '--exclude="/foo/bar baz"'
...excludes.map(exclude => `--exclude="${exclude}"`),
'/foo'
], { stdio: ['ignore', 'pipe', 'inherit'] });
出于某种原因,tar
忽略了以这种方式提供的--exclude
参数。结果与spawn
是require('child_process').spawn
和require('cross-spawn')
相同。
当不需要它们的路径没有双引号时,--exclude
按预期工作。
即使使用双引号,同样的东西也可以像shell一样工作:
tar --create --gzip --exclude="/foo/bar" --exclude="/foo/baz" /foo > ./foo.tgz
我不知道那里发生了什么,以及如何调试spawn
来检查它是否为双引号做了一些奇怪的转义。
这是引用类型优先级中的问题。双引号优先于单引号,因此产生的调用会中断。
系统shell将剥离参数周围的引号,因此程序在结尾处获得不带引号的值。在绕过shell时,产生一个进程会绕过这一步,因此程序会将这些文字引号作为参数的一部分,并且不知道如何正确处理它们。
我知道有两个解决这个问题的实际选择:
const tar = spawn("tar", [
"--create", "--gzip",
"--exclude='/foo/bar'", "--exclude='/foo/baz'", "/foo"
], { stdio: ["ignore", "pipe", "inherit"] });
{ shell: true }
并使用当前格式。这将通过shell传递spawn请求,因此将发生当前正在跳过的解析步骤。查看有关此here的更多信息。
const tar = spawn('tar', [
'--create', '--gzip',
'--exclude="/foo/bar"', '--exclude="/foo/baz"', '/foo'
], { stdio: ['ignore', 'pipe', 'inherit'], shell: true });
您应该了解shell如何处理空格和引号。我说“贝壳” - 有不同的贝壳,我不知道它们之间的区别,所以我写的东西可能不适用于你。有人可以自由编辑,以便更精确。
您可以在shell命令中包含各种语法复杂性:管道命令,输入和输出文件,插值变量,插值命令,环境变量以及至少4种(是,四种)不同的引用字符串的方式。但是出于这个问题的目的,我们只是说shell命令是一个命令名,后跟一个(可能是空的)字符串参数列表。命令名称可以是内置命令(cd
,ls
,sudo
等),也可以是可执行文件。或者,换句话说,shell命令是一个或多个字符串的列表(包括第一个字符串,它告诉shell它是什么类型的命令)。
由于上面提到的复杂性,几个字符是特殊字符。这意味着您可能需要使用引号来转义它们。但是,引号会在语言中引入大量冗余。例如,以下命令是等效的:
tar --create --exclude=/foo/bar /foo
tar --create --exclude='/foo/bar' /foo
tar --create --exclude="/foo/bar" /foo
tar --create '--exclude=/foo/bar' /foo
tar --create "--exclude=/foo/bar" /foo
在每种情况下,命令是使用参数列表tar
,--create
,--exclude=/foo/bar
运行可执行文件/foo
。
请注意引号的行为,它与我所知道的所有其他语言不同。在大多数语言中,字符串文字完全被一对引号括起来 - 这就是编译器/解释器知道它们开始和结束的位置。但是在shell命令中,空格是告诉shell一个参数结束而下一个参数开始的地方。 (引用/转义的空格不计算。)引号的唯一目的是更改某些字符的处理方式。 Shell命令对此非常灵活,因此以下命令也与上述命令相同:
tar -"-"create --exc'lude=/fo'o/bar /foo
tar --cr'eate' --exclude"="/foo"/bar" /foo
当我说这些命令是等价的时,我的意思是tar
可执行文件无法知道哪一个被调用。也就是说,不可能编写可执行文件mycommand
,使命令mycommand foo
和mycommand "foo"
将不同的输出写入STDOUT或STDERR,或返回不同的退出代码,或以其他方式表现不同。
但是,当从nodejs运行shell命令时,您不需要使用shell功能进行管道连接,流式传输到/来自文件,插入变量等,因为如果您愿意,javascript可以处理所有这些内容。因此,当您向spawn
提供参数时,它会绕过这些shell功能;它对shell特殊字符没有任何作用。你只是直接提供参数。因此在下面的示例中,其中一个参数将是--exclude=/foo/bar baz
,这将导致tar
忽略bar baz
目录中名为/foo
的文件/目录:
const tar = spawn('tar', [
'--create', '--gzip',
'--exclude=/foo/bar', '--exclude=/foo/baz', '--exclude=/foo/bar baz',
'/foo'
], { stdio: ['ignore', 'pipe', 'inherit'] });
(虽然很明显,如果你使用javascript字符串文字,你可能需要在javascript级别转义一些字符。)
我不喜欢joshuhn的答案。 (1)甚至没有为我工作,我很惊讶它为他工作 - 如果它确实那么我将其视为nodejs中的错误(或可能在tar
中)。 (我在Ubuntu 16.04.3 LTS中运行nodejs v6.9.5,使用GNU tar v1.28。)对于(2),它意味着不必要地将shell字符串处理的所有复杂性引入到您的javascript代码中。正如the documentation所说:
注意:如果启用了
shell
选项,请不要将未经过授权的用户输入传递给此函数。包含shell元字符的任何输入都可用于触发任意命令执行。
我一个人不知道shell逃逸的所有错综复杂,所以我不会冒险使用spawn
选项和不受信任的输入运行shell
。