使用 shell 变量作为命令选项

使用 shell 变量作为命令选项

在 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

一般来说,将单独项目的列表降级为单个字符串是一个坏主意,无论是命令行选项列表还是路径名列表。原因是,在接下来的某个地方,您将需要再次将该字符串拆分为单独的内容,并且当字符串包含对语义解释重要或不重要的引号和空格时,正确执行此操作并不容易。字符串中的数据。

使用数组代替:

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 对选项执行文件名生成(通配)。


¹ 前提是$IFS未修改且--exclude=.当前目录中不存在名称以 开头的文件,并且未设置nullglob或shell 选项。failglob

答案2

@Kusalananda已经解释过了基本问题及其解决方法,以及Bash 常见问题解答条目@glenn jackmann 链接还提供了很多有用的信息。以下是根据这些资源对我的问题所发生情况的详细解释。

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

#!/bin/bash

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

“手动”传递选项:

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

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

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

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

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

事实证明,巴什执行模式扩展,但该模式与--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=.*

答案3

当我们编写函数和shell脚本时,传入参数进行处理时,参数将被传递为int数字命名的变量,例如$1、$2、$3

例如:

bash my_script.sh Hello 42 World

在 内部my_script.sh,命令将用来$1引用 Hello、$2to42$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"

相关内容