使用Shell变量获取命令选项


19

在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)

如您所见,传递--exclude='.*'rsync“手动”可以很好地工作(.bar不被复制),当选项首先存储在变量中时,它就不起作用。

我猜想这与引号或通配符(或两者都有关)有关,但我一直无法弄清楚到底是什么错误。



1
或在我们自己的网站上查看此答案
斯科特,

Answers:


38

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

改用数组:

rsync_options=( -rnv --exclude='.*' )

要么

rsync_options=( -r -n -v --exclude='.*' )

然后...

rsync "${rsync_options[@]}" source/ target

这样,单个选项的引用得以保留(只要您对的扩展名加双引号${rsync_options[@]})。它还允许您在调用之前轻松操作数组的各个条目rsync

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

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

rsync "$@" source/ target

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

切线相关:


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

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

本来可以使用¹...,但最好使用数组或位置参数(带有单独引用的条目)(以确保安全)。这样做还可以使您在需要的地方使用带有空格的东西,并且避免让shell在选项上执行文件名的生成(globbing)。


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


使用数组很好用,谢谢您的详细回答!
弗洛里安·布鲁克

3

@Kusalananda 已经解释了基本问题及其解决方法,@ glenn jackmann链接到的Bash FAQ条目也提供了许多有用的信息。根据这些资源,这是我问题中正在发生的事情的详细说明。

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

#!/bin/bash

for var in "$@"
do
    echo "$var"
done

“手动”传递选项:

$ ./argtest.bash -rnv --exclude='.*'
-rnv
--exclude=.*

不出所料,部分-rnv--exclude='.*'被拆分为两个参数,因为它们之间用未加引号的空格隔开(这称为单词拆分)。

还要注意,周围的引号.*已被删除:单引号告诉shell传递其内容而无需特殊解释但是引号本身并未传递给command

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

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

$ ./argtest.bash $OPTS
--exclude='.*'

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

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

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

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

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

$ OPTS2=--exclude='.*'

$ ./argtest.bash $OPTS2
--exclude=.*

但是,由于在更复杂的情况下存在细微的差异,因此始终引用变量分配是一个好习惯。

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

$ ./argtest.bash --exclude=.*
--exclude=.*

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

$ touch some_file

$ ./argtest.bash some_*
some_file

$ ./argtest.bash does_not_exit_*
does_not_exit_*

但是,不引用模式是很危险的,因为如果(由于某种原因)存在文件匹配,--exclude=.*则模式将被扩展:

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

$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen

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

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

$ 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=.*

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

$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*

这些东西使我困惑了很长时间,因此像这样的详细说明会有所帮助。

0

当我们编写函数和shell脚本(其中传递参数进行处理)时,参数将传递给以数字命名的变量,例如$ 1,$ 2,$ 3

例如

bash my_script.sh Hello 42 World

在内my_script.sh,命令将用于$1引用Hello,$2to 42$3forWorld

变量引用$0将会扩展为当前脚本的名称,例如my_script.sh

不要以命令作为变量来播放整个代码。

注意事项

1避免在脚本中使用全大写字母的变量名。

2不要使用反引号,而是使用$(...),它嵌套更好。

if [ $# -ne 2 ]
then
    echo "Usage: $(basename $0) DIRECTORY BACKUP_DIRECTORY"
    exit 1
fi

directory=$1
backup_directory=$2
current_date=$(date +%Y-%m-%dT%H-%M-%S)
backup_file="${backup_directory}/${current_date}.backup"

tar cv "$directory" | openssl des3 -salt | split -b 1024m - "$backup_file"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.