将 shell 变量用于命令选项

Flo*_*ker 33 bash variable

在 Bash 脚本中,我试图将我使用的选项存储rsync在一个单独的变量中。这适用于简单的选项(如--recursive),但我遇到了--exclude='.*'以下问题:

$ find source
source
source/.bar
source/foo

$ rsync -rnv --exclude='.*' source/ dest
sending incremental file list
foo

sent 57 bytes  received 19 bytes  152.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

$ RSYNC_OPTIONS="-rnv --exclude='.*'"

$ rsync $RSYNC_OPTIONS source/ dest
sending incremental file list
.bar
foo

sent 78 bytes  received 22 bytes  200.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)
Run Code Online (Sandbox Code Playgroud)

As you can see, passing --exclude='.*'to rsync"manually" works fine ( .barisn't copied), it doesn't work when the options are stored in a variable first.

我猜这与引号或通配符(或两者)有关,但我一直无法弄清楚到底出了什么问题。

Kus*_*nda 54

通常,将单独的项目列表降级为单个字符串是一个坏主意,无论是命令行选项列表还是路径名列表。

改用数组:

rsync_options=( -rnv --exclude='.*' )
Run Code Online (Sandbox Code Playgroud)

或者

rsync_options=( -r -n -v --exclude='.*' )
Run Code Online (Sandbox Code Playgroud)

然后...

rsync "${rsync_options[@]}" source/ target
Run Code Online (Sandbox Code Playgroud)

这样,单个选项的引用得以保留(只要您将 的扩展双引号${rsync_options[@]})。它还允许您在调用rsync.

在任何 POSIX shell 中,可以为此使用位置参数列表:

set -- -rnv --exclude='.*'

rsync "$@" source/ target
Run Code Online (Sandbox Code Playgroud)

同样,双引号的扩展在$@这里很重要。

切线相关:


问题是,当您将两组选项放入字符串中时,--exclude选项值的单引号成为该值的一部分。因此,

RSYNC_OPTIONS='-rnv --exclude=.*'
Run Code Online (Sandbox Code Playgroud)

本来可以工作¹...但最好(因为更安全)使用数组或带有单独引用的条目的位置参数。这样做还允许您在需要时使用其中包含空格的内容,并避免让 shell 对选项执行文件名生成(通配)。


¹ 前提$IFS是未修改且--exclude=.当前目录中没有名称以开头的文件,并且未设置nullgloborfailglob外壳选项。


Flo*_*ker 6

@Kusalananda已经解释了基本问题以及如何解决它,@glenn jackmann 链接的Bash FAQ 条目也提供了很多有用的信息。这是基于这些资源对我的问题中发生的事情的详细说明。

我们将使用一个小脚本在单独的行上打印每个参数来说明事情 ( argtest.bash):

#!/bin/bash

for var in "$@"
do
    echo "$var"
done
Run Code Online (Sandbox Code Playgroud)

“手动”传递选项:

$ ./argtest.bash -rnv --exclude='.*'
-rnv
--exclude=.*
Run Code Online (Sandbox Code Playgroud)

正如预期的那样,部分-rnv--exclude='.*'被拆分为两个参数,因为它们由不带引号的空格分隔(这称为分)。

另请注意,周围的引号.*已被删除:单引号告诉 shell 在没有特殊解释的情况下传递其内容,但引号本身不会传递给命令

如果我们现在将选项作为字符串存储在变量中(而不是使用数组),则不会删除引号:

$ OPTS="--exclude='.*'"

$ ./argtest.bash $OPTS
--exclude='.*'
Run Code Online (Sandbox Code Playgroud)

这是因为两个原因:定义时使用的双引号$OPTS防止对单引号进行特殊处理,因此后者是值的一部分:

$ echo $OPTS
--exclude='.*'
Run Code Online (Sandbox Code Playgroud)

当我们现在$OPTS用作命令的参数时,引号会在参数扩展之前处理,因此引号$OPTS“太晚了”。

这意味着(在我的原始问题中)rsync使用排除模式'.*'(带引号!)而不是模式.*——它排除名称以单引号开头、后跟一个点并以单引号结尾的文件。显然这不是本意。

一种解决方法是在定义时省略双引号$OPTS

$ OPTS2=--exclude='.*'

$ ./argtest.bash $OPTS2
--exclude=.*
Run Code Online (Sandbox Code Playgroud)

但是,由于在更复杂的情况下存在细微差别,因此始终引用变量赋值是一种很好的做法。

正如@Kusalananda 指出的那样,不引用.*也可以。我已经添加了引号以防止模式扩展,但在这种特殊情况下这不是绝对必要的:

$ ./argtest.bash --exclude=.*
--exclude=.*
Run Code Online (Sandbox Code Playgroud)

事实证明,Bash确实执行了模式扩展,但该模式--exclude=.*不匹配任何文件,因此该模式被传递给命令。相比:

$ touch some_file

$ ./argtest.bash some_*
some_file

$ ./argtest.bash does_not_exit_*
does_not_exit_*
Run Code Online (Sandbox Code Playgroud)

但是,不引用模式是危险的,因为如果(无论出于何种原因)有文件匹配,--exclude=.*则模式会被扩展:

$ touch -- --exclude=.special-filenames-happen

$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen
Run Code Online (Sandbox Code Playgroud)

最后,让我们看看为什么使用数组可以防止我的引用问题(除了使用数组存储命令参数的其他优点之外)。

定义数组时,分词和引号处理按预期进行:

$ ARRAY_OPTS=( -rnv --exclude='.*' )

$ echo length of the array: "${#ARRAY_OPTS[@]}"
length of the array: 2

$ echo first element: "${ARRAY_OPTS[0]}"
first element: -rnv

$ echo second element: "${ARRAY_OPTS[1]}"
second element: --exclude=.*
Run Code Online (Sandbox Code Playgroud)

将选项传递给命令时,我们使用语法"${ARRAY[@]}",它将数组的每个元素扩展为一个单独的单词:

$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

20810 次

最近记录:

6 年,1 月 前