在 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)
As you can see, passing --exclude='.*'
to rsync
"manually" works fine ( .bar
isn't copied), it doesn't work when the options are stored in a variable first.
我猜这与引号或通配符(或两者)有关,但我无法弄清楚到底是什么问题。
通常,将单独的项目列表降级为单个字符串是一个坏主意,无论它是命令行选项列表还是路径名列表。
改用数组:
或者
然后...
这样,单个选项的引用就被保持(只要你双引号 的扩展
${rsync_options[@]}
)。它还允许您在调用rsync
.在任何 POSIX shell 中,可以为此使用位置参数列表:
同样,在这里双引号的扩展
$@
是至关重要的。切线相关:
问题是当您将两组选项放入一个字符串时,
--exclude
选项值的单引号将成为该值的一部分。因此,本来可以工作¹...但最好(更安全)使用带有单独引用条目的数组或位置参数。如果需要,这样做还允许您使用其中带有空格的内容,并避免让 shell 对选项执行文件名生成(通配符)。
¹ 前提
$IFS
是未修改且--exclude=.
当前目录中没有名称以开头的文件,并且未设置nullglob
or shell 选项。failglob
@Kusalananda已经解释了基本问题以及如何解决它,@glenn jackmann 链接到的Bash FAQ 条目也提供了很多有用的信息。以下是根据这些资源对我的问题中发生的事情的详细解释。
我们将使用一个小脚本将其每个参数打印在单独的行上来说明事情(
argtest.bash
):“手动”传递选项:
正如预期的那样,部分
-rnv
和--exclude='.*'
被分成两个参数,因为它们由不带引号的空格分隔(这称为分词)。另请注意,周围的引号
.*
已被删除:单引号告诉 shell 传递其内容而无需特殊解释,但引号本身并未传递给 command。如果我们现在将选项作为字符串存储在变量中(而不是使用数组),那么引号不会被删除:
这是因为两个原因:定义时使用的双引号
$OPTS
防止对单引号进行特殊处理,因此后者是值的一部分:当我们现在将
$OPTS
其用作命令的参数时,引号在参数扩展之前被处理,因此引号中的$OPTS
出现“为时已晚”。这意味着(在我原来的问题中)
rsync
使用排除模式'.*'
(带引号!)而不是模式.*
- 它排除名称以单引号后跟一个点并以单引号结尾的文件。显然这不是本意。一种解决方法是在定义时省略双引号
$OPTS
:但是,由于在更复杂的情况下存在细微差别,因此始终引用变量赋值是一种好习惯。
正如@Kusalananda 指出的那样,不引用
.*
也可以。我添加了引号以防止模式扩展,但在这种特殊情况下这并不是绝对必要的:事实证明,Bash确实执行了模式扩展,但模式
--exclude=.*
与任何文件都不匹配,因此模式被传递给命令。相比:但是,不引用模式是危险的,因为如果(无论出于何种原因)有一个文件匹配
--exclude=.*
,那么模式就会被扩展:最后,让我们看看为什么使用数组可以防止我的引用问题(除了使用数组存储命令参数的其他优点之外)。
定义数组时,分词和引号处理按预期进行:
将选项传递给命令时,我们使用语法
"${ARRAY[@]}"
,它将数组的每个元素扩展为一个单独的单词:当我们编写函数和 shell 脚本时,传入参数进行处理,参数将传递给 int 数字命名的变量,例如 $1, $2, $3
例如:
bash my_script.sh Hello 42 World
在内部
my_script.sh
,命令将用于$1
引用 Hello、$2
to42
和$3
forWorld
变量引用 ,
$0
将扩展为当前脚本的名称,例如my_script.sh
不要使用命令作为变量来播放整个代码。
请记住:
1 避免在脚本中使用全大写的变量名。
2 不要使用反引号,使用 $(...) 代替,它嵌套更好。