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

时间:2010-11-11 14:55:24

标签: linux bash

在Bash脚本(没有Perl,Python等等)中应该使用什么成语来为脚本参数中的另一个程序构建命令行,同时正确处理文件名 < / em>的

通过正确,我的意思是处理带有空格或奇数字符的文件名而不会无意中导致其他程序将它们作为单独的参数处理(或者,在<或{{1的情况下)毕竟,如果正确转义 不幸的 文件名字符,那么它们是有效的 - 做一些更糟糕的事情。)

以下是正确处理文件名的形式的含义示例:让我们假设这个脚本(>)构建了一个命令行命令(foo,假设在路径中)通过获取所有bar的输入参数并将看起来像标志的任何东西移到前面,然后调用foo:< / p>

bar

(请注意,此只是一个示例;还有很多其他时间需要执行此操作以及一堆args然后将它们传递到其他程序。)

在简单文件名的天真场景中,效果很好。但是如果我们假设一个包含文件的目录

one
two
three and a half
four < five

然后当然命令#!/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: Handling four < five
foo: Handling one
foo: Handling three and a half
foo: Handling two
Issuing: bar   four < five one three and a half two

如果我们实际允许foo *发出该命令,那么结果将不会是我们所期望的。

以前我试过通过确保每个文件名周围都有引号的简单方法来解决这个问题,但我(非常)很快就知道那是不是正确的方法。 : - )

那是什么?约束:

  1. 我想让成语尽可能简单(尤其是我记得它)。
  2. 我正在寻找一个通用的习语,因此我编写了foo程序和上面的人为例子,而不是使用一个真实的场景,人们可能很容易(并且合理地)沿着尝试的路线前进使用目标程序中的功能。
  3. 我想坚持Bash脚本,我不想打电话给Perl,Python等。
  4. 我很依赖(其他)标准* nix实用程序,例如barxargssed,前提是我们不会太迟钝(请参阅上面的#1) )。 (向Perl,Python等程序员抱歉,他们认为#3和#4相结合以形成任意区别。)
  5. 如果重要,目标程序也可能是Bash脚本,也可能不是。我不指望这很重要......
  6. 我不只是想处理空格,我也想正确处理奇怪的字符。
  7. 如果它不处理带有嵌入的nul字符的文件名(字面意思是字符代码0),我就不会感到烦恼。如果有人设法在他们的文件系统中创建一个,我不担心处理它,他们已经尝试非常难以搞砸了。
  8. 提前谢谢,伙计们。


    修改Ignacio Vazquez-Abrams向我指出了Bash常见问题解答entry #50,经过一些阅读和实验后似乎表明一种方法是使用Bash arrays:< / p>

    tr

    这是正确和合理的吗?或者我依靠上面的环境,以后会咬我。 似乎工作,它为我勾选所有其他框(简单,易记,等等)。它似乎依赖于相对最近的Bash功能(FAQ条目#50提及v3.1,但我不确定这是否是他们使用它的一些语法的一般数组),但我认为我可能只会处理拥有它的版本。

    (如果以上是正确的并且您想要取消删除您的答案,Ignacio,我会接受它,前提是我还没有接受任何其他人,尽管我支持我关于仅链接答案的陈述。)

3 个答案:

答案 0 :(得分:5)

你为什么要“建立”一个命令?使用适当的方法将文件和标志添加到数组 引用并直接使用带引号的数组作为参数发出命令。

脚本中的选定行(省略未更改的行):

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有关将命令放入变量的信息。您的脚本不起作用的原因是因为无法在带引号的字符串中引用参数。如果你在那里放置引号,它们将被视为字符串本身的一部分而不是分隔符。如果参数不加引号,则完成单词拆分,包含空格的参数被视为多个参数。带有“&lt;”,“&gt;”的参数或“|”在任何情况下都不是问题,因为在变量扩展之前执行重定向和管道,因此它们被视为字符串中的字符。

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

一些补充说明:

  • 使用小写(或大小写混合大小写)变量名来减少它们与shell内置变量冲突的可能性。
  • 如果在任何现代shell中使用单个方括号作为条件,如果引用变量,则不再需要古老的“x”习语(请参阅我的回答here)。但是,在Bash中,请使用双括号。它们提供了其他功能(请参阅我的回答here)。
  • 使用getopts作为Let_Me_Be建议。你的脚本虽然我知道它只是一个例子,但它无法处理带参数的开关。
  • for ARG in "$@"可以缩短为此for ARG(但我更喜欢更明确版本的可读性)。

答案 1 :(得分:1)

请参阅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重定向。如果您需要以编程方式构建这些,请为头痛做好准备。

答案 2 :(得分:0)

getopts应该能够正确处理参数中的空格("file name.txt")。假设它们被正确转义(ls -b)。奇怪的字符应该也可以正常工作。