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

T.J*_*der 3 linux bash

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

通过正确地说,我的意思是处理带有空格或奇数字符的文件名,而不会无意中导致其他程序将它们作为单独的参数处理(或者,<或者> - 或者- 毕竟,如果正确的文件名字符被正确转义则有效- 执行某些操作更糟).

这是我的意思的一个组成例子,在一个不能正确处理文件名的形式中:让我们假设这个脚本()通过采取所有的' foo为命令构建命令行(bar假设在路径中)foo输入参数并将任何看起来像标志的东西移到前面,然后调用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
Run Code Online (Sandbox Code Playgroud)

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

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

one
two
three and a half
four < five

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

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. 我正在寻找一个通用的习语,因此我编写了bar程序和上面的人为例子,而不是使用真实的场景,人们可能很容易(并且合理地)沿着尝试使用目标程序中的功能的路线走下去.
  3. 我想坚持使用Bash脚本,我不想调用Perl,Python等.
  4. 我很好依赖于(其他)标准的*nix实用工具,如xargs,sedtr只要我们不要太钝(见上面的#1).(对Perl,Python等程序员抱歉,他们认为#3和#4相结合以形成任意区别.)
  5. 如果重要,目标程序也可能是Bash脚本,也可能不是.我不指望这很重要......
  6. 我不只是想处理空格,我也想正确处理奇怪的字符.
  7. 如果它不处理带有嵌入的nul字符的文件名(字面意思是字符代码0),我就不会感到烦恼.如果有人设法在他们的文件系统中创建一个,我并不担心处理它,他们已经非常努力地搞砸了.

伙计们,提前谢谢.


编辑:Ignacio Vazquez-Abrams向我指出Bash FAQ 条目#50,经过一些阅读和实验似乎表明一种方法是使用Bash 数组:

#!/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[@]}"
Run Code Online (Sandbox Code Playgroud)

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

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

Pau*_*ce. 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[@]}"
Run Code Online (Sandbox Code Playgroud)

有关以这种方式使用数组的快速演示:

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

输出:

..aaa..
..bbb ccc..
..ddd..
Run Code Online (Sandbox Code Playgroud)

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

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

一些额外的说明:

  • 使用小写(或大小写混合)变量名称可以减少它们与shell的内置变量发生冲突的可能性.
  • 如果你在任何现代外壳使用单方括号条件句古老的"X"的成语不再是必要的,如果你引用变量(见我的答案在这里).但是,在Bash中,请使用双括号.他们提供额外的功能(请参阅我的答案在这里).
  • 使用getopts如Let_Me_Be建议.你的脚本虽然我知道它只是一个例子,但它无法处理带参数的开关.
  • for ARG in "$@"可以缩短到这一点for ARG(但我更喜欢更明确版本的可读性).