如何在bash脚本中正确地将文件名传递给其他程序?

在Bash脚本中使用什么成语(不要使用Perl,Python等)为脚本参数中的另一个程序构build命令行,同时正确处理文件名?

正确地说 ,我的意思是使用空格或奇数字符来处理文件名,而不会不经意间导致其他程序将它们作为单独的参数来处理(或者在<>的情况下 – 毕竟如果不幸的文件名字符正确地转义,做更糟糕的事情)。

下面是一个我的意思是,在一个正确处理文件名的forms中的一个例子:让我们假设这个脚本( foo )为一个命令( bar ,假设在path中)build立一个命令行foo的input参数,并将任何看起来像标志的东西移动到前面,然后调用bar

 #!/bin/bash # This is clearly wrong FILES= FLAGS= for ARG in "$@"; do echo "foo: Handling $ARG" if [ x${ARG:0:1} = "x-" ]; then # Looks like a flag, add it to the flags string FLAGS="$FLAGS $ARG" else # Looks like a file, add it to the files string FILES="$FILES $ARG" fi done # Call bar with the flags and files (we don't care that they'll # have an extra space or two) CMD="bar $FLAGS $FILES" echo "Issuing: $CMD" $CMD 

(请注意,这仅仅是一个例子 ,还有很多其他的时候需要这样做,而且还有一些参数需要传递给其他程序。)

在简单的文件名天真的情况下,这很好。 但是,如果我们假设一个包含文件的目录

 一
二
三个半
四<五 

那么当然命令foo *在其任务中失败了:

  foo:处理四<五
 foo:处理一个
 foo:处理三个半
 foo:处理两个
发行:酒吧四<五一三二 

如果我们真的允许foo发出这个命令,那么结果就不会是我们所期待的。

以前我试图通过确保每个文件名都有引号的简单方法来处理这个问题,但是我很快就知道这不是正确的方法。 🙂

那么是什么? 约束:

  1. 我想保持这个成语尽可能简单 (尤其是我可以记住它)。
  2. 我正在寻找一个通用的习惯用语,因此我编写了上面的bar程序和人为的示例,而不是使用一个真实的场景,人们可以轻松地(合理地)走下试图在目标程序中使用function的路线。
  3. 我想坚持Bash脚本,我不想叫Perl,Python等。
  4. 如果我们不会太钝(参见上面的#1),那么依靠(其他)标准* nix实用程序(如xargssedtr ,我很好。 (对Perl,Python等的道歉,认为#3和#4结合在一起的程序员将任意区分)。
  5. 如果重要的话,目标程序可能也是Bash脚本,或者可能不是。 我不会期望它是重要的…
  6. 我不只是想处理空格,我也想正确处理怪异的字符。
  7. 如果它不处理带有embedded的空字符的文件名(字面代码为0),我不会感到困扰。 如果有人在他们的文件系统中创build了一个文件系统,我并不担心处理它,他们已经很努力地搞砸了。

在此先感谢,伙计们。


编辑 : 伊格纳西奥·巴斯克斯 – 艾布拉姆斯指出我Bash FAQ 条目#50 ,经过一些阅读和实验后,似乎表明,一种方法是使用Bash arrays

 #!/bin/bash # This appears to work, using Bash arrays # Start with blank arrays FILES=() FLAGS=() for ARG in "$@"; do echo "foo: Handling $ARG" if [ x${ARG:0:1} = "x-" ]; then # Looks like a flag, add it to the flags array FLAGS+=("$ARG") else # Looks like a file, add it to the files array FILES+=("$ARG") fi done # Call bar with the flags and files echo "Issuing (but properly delimited, not exactly as this appears): bar ${FLAGS[@]} ${FILES[@]}" bar "${FLAGS[@]}" "${FILES[@]}" 

这是正确和合理的吗? 或者我依赖于以上的环境,以后会咬我的东西。 它似乎工作,它为我所有的其他盒子(简单,易于记忆等)。 它似乎依赖于一个相对较新的Bash特性(FAQ条目#50提到了v3.1,但是我不确定这是否是它们使用的一些语法的一般数组),但是我认为这很可能我只会处理有版本的版本。

(如果上述内容是正确的,并且想要删除您的答案,Ignacio,我会接受它,但是我还没有接受任何其他答案,尽pipe我支持我的关于仅链接答案的声明。)

你为什么要“建立”一个命令? 使用正确的引号将文件和标志添加到数组,然后直接使用带引号的数组作为参数发出命令。

从脚本中选择的行(省略不变的行):

 if [[ ${ARG:0:1} == - ]]; then # using a Bash idiom FLAGS+=("$ARG") # add an element to an array FILES+=("$ARG") echo "Issuing: bar \"${FLAGS[@]}\" \"${FILES[@]}\"" bar "${FLAGS[@]}" "${FILES[@]}" 

以这种方式快速演示如何使用数组:

 $ a=(aaa 'bbb ccc' ddd); for arg in "${a[@]}"; do echo "..${arg}.."; done 

输出:

 ..aaa.. ..bbb ccc.. ..ddd.. 

请参阅BashFAQ / 050关于将命令放入变量。 你的脚本不起作用的原因是因为没有办法引用带引号的字符串中的参数。 如果你要在那里加上引号,它们将被视为字符串本身的一部分,而不是作为分隔符。 在没有引用参数的情况下,分词就完成了,包含空格的参数被视为不止一个参数。 带“<”,“>”或“|”的参数 在任何情况下都不是问题,因为重定向和管道是在变量扩展之前执行的,因此它们被视为字符串中的字符。

通过将参数(文件名)放在一个数组中,空格,换行符等被保存。 通过引用数组变量作为参数传递时,它们被保存在消费程序的路上。

一些附加说明:

  • 使用小写(或混合大小写)变量名称来减少它们与shell的内建变量发生冲突的机会。
  • 如果在任何现代shell中对条件使用单个方括号,那么如果引用变量(请参阅我的答案),则不再需要陈旧的“x”成语。 但是,在Bash中,使用双括号。 他们提供了额外的功能(请参阅我的答案)。
  • 使用getopts作为Let_Me_Be建议。 你的脚本,尽管我知道这只是一个例子,将不能够处理带参数的交换机。
  • for ARG in "$@"可以缩写for ARG (但是我更喜欢更明确的版本的可读性)。

见BashFAQ#50 (也可能#35选项解析)。 对于你描述的场景,你动态创建一个命令的地方,最好的选择是使用数组而不是简单的字符串,因为它们不会失去字边界的位置。 一般规则是:创建一个数组,而不是VAR="foo bar baz" ,使用VAR=("foo" "bar" "baz" ); 要使用数组而不是$VAR ,请使用"${VAR[@]}" 。 以下是使用此方法的示例脚本的工作版本:

 #!/bin/bash # This is clearly wrong FILES=() FLAGS=() for ARG in "$@"; do echo "foo: Handling $ARG" if [ x${ARG:0:1} = "x-" ]; then # Looks like a flag, add it to the flags array FLAGS=("${FLAGS[@]}" "$ARG") # FLAGS+=("$ARG") would also work in bash 3.1+, as Dennis pointed out else # Looks like a file, add it to the files string FILES=("${FILES[@]}" "$ARG") fi done # Call bar with the flags and files (we don't care that they'll # have an extra space or two) CMD=("bar" "${FLAGS[@]}" "${FILES[@]}") echo "Issuing: ${CMD[*]}" "${CMD[@]}" 

请注意,在echo命令中,我使用"${VAR[*]}"而不是[@]形式,因为这里没有必要/保留单词分隔符。 如果你想以明确的形式打印/记录命令,这将会变得更加混乱。

另外,这样就无法在构建的命令中建立重定向或其他特殊的shell选项 – 如果将>outfile添加到FILES数组,它将被视为另一个命令参数,而不是shell重定向。 如果你需要以编程方式建立这些,准备头痛。

getopts应该能够正确处理参数中的空格( "file name.txt" )。 奇怪的角色应该也能工作,假设他们正确地逃脱了( ls -b )。