在从 shell 脚本中的变量创建的命令中保留双引号

t0m*_*ppa 3 bash shell-script

以为我会写一个简单的脚本来同步一些数据,但结果比我想象的要困难。

基本布局是有一个配置文件夹,其中有子文件夹引用需要同步的文件夹,每个文件夹包含[0..2]个文件(includes.txt & excepts.txt)。然后脚本将读取这些内容并运行同步命令。

我想要运行的是:

me@my_machine:~/scripts$ aws s3 sync /home/me/Pictures s3://my_bucket/home/me/Pictures --exclude "*" --include "*.gif" --include "*.jpg" --profile=personal --dryrun
(dryrun) upload: ../Pictures/sample_picture.jpg to s3://my_bucket/home/me/Pictures/sample_picture.jpg
Run Code Online (Sandbox Code Playgroud)

所以,我可以忽略某些文件。我无法从脚本中获取排除项和包含项,因为 AWS CLI 要求将模式加双引号。

我读到的其他问题指示使用数组和函数,因此这是我的脚本:

#!/bin/bash

set -x

DRYRUN=true

s3_bucket_uri='s3://my_bucket'
aws_profile='--profile=personal'
config_folder='../config/*'
include_file='includes.txt'
exclude_file='excludes.txt'

includes=()
excludes=()

sync () {
    local params=()
    local local_folder="$HOME/$1"
    local bucket_folder="$s3_bucket_uri""$local_folder"

    params+=("$local_folder" "$bucket_folder")

    if [[ ${excludes[@]} ]]; then
        params+=("${excludes[@]/#/--exclude }")
    fi
    
    if [[ ${includes[@]} ]]; then
        params+=("${includes[@]/#/--include }")
    fi
    
    params+=("$aws_profile")

    if [[ "$DRYRUN" = true ]]; then
        params+=(--dryrun)
    fi

    aws s3 sync ${params[@]}
}


read_parameters () {
    if [[ -f "$1" ]]; then
        while read line; do
            if [[ $2 == "include" ]]; then
                includes+=("$line")
            elif [[ $2 == "exclude" ]]; then
                excludes+=("$line")
            fi
        done < $1
    fi
}

reset () {
    includes=()
    excludes=()
}

for folder in $config_folder; do
    if [[ -d "$folder" && ! -L "$folder" ]]; then
        read_parameters $folder/$exclude_file exclude
        read_parameters $folder/$include_file include
        sync "${folder##*/}"
        reset
    fi
done

Run Code Online (Sandbox Code Playgroud)

输入示例为:

"*.jpg"
"*.gif"
Run Code Online (Sandbox Code Playgroud)

在includes.txt文件中。

问题在于如何正确获取 AWS CLI 的引号,因为它需要双引号来表示包含和排除模式,而这似乎很难正确获取。

使用 时aws s3 sync ${params[@]},shell 在模式周围添加额外的单引号,这不会导致命令崩溃,但它只是忽略所有模式:

+ aws s3 sync /home/me/Pictures s3://mybucket/home/me/Pictures --exclude '"*"' --include '"*.gif"' --include '"*.jpg"' --profile=personal --dryrun
(dryrun) upload: ../../../Pictures/Bender_Rodriguez.png to s3://mybucket/home/me/Pictures/Bender_Rodriguez.png
Run Code Online (Sandbox Code Playgroud)

正如我们所看到的,它正在尝试上传应该排除的内容,因为我试图告诉它排除除 .gif 和 .jpg 文件之外的所有内容。


shell在aws s3 sync "${params[@]}"整个包含或排除语句周围添加单引号,导致命令崩溃:

+ aws s3 sync /home/me/Pictures s3://mybucket/home/me/Pictures '--exclude "*"' '--include "*.gif"' '--include "*.jpg"' --profile=personal --dryrun
Unknown options: --exclude "*",--include "*.gif",--include "*.jpg"
Run Code Online (Sandbox Code Playgroud)

还尝试简单地添加一个手动创建的值params+=(--testing "foobar"),因为这是在另一个问题中给出的方法。但这会丢失所有引号,最终结果是:

+ aws s3 sync /home/me/Pictures s3://mybucket/home/me/Pictures --testing foobar --profile=personal --dryrun
Run Code Online (Sandbox Code Playgroud)

我确实检查了这个问题,但即使得到了答案:

bar=( --bar a="b" )
cmd=(foo "${bar[@]}" )
printf '%q ' "${cmd[@]}" && echo  # print code equivalent to the command we're about to run
"${cmd[@]}"                       # actually run this code
Run Code Online (Sandbox Code Playgroud)
+ bar=(--bar a="b")
+ cmd=(foo "${bar[@]}")
+ printf '%q ' foo --bar a=b
foo --bar a=b + echo

+ foo --bar a=b
Run Code Online (Sandbox Code Playgroud)

所以,它失去了双引号。


这是我的 Bash 版本,以防有所不同:

me@my_machine:~/scripts$ bash --version
GNU bash, version 5.0.17(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Run Code Online (Sandbox Code Playgroud)

因此,有没有办法解决这个问题,或者我应该用编程语言重写脚本并使用 AWS SDK,而不是使用 shell 脚本和 AWS CLI?


@muru:如果我不加任何引号,则不使用包含和排除模式:

me@my_machine:~/scripts$ aws s3 sync /home/me/Pictures s3://my_bucket/home/me/Pictures --exclude * --include *.gif --include *.jpg --profile=personal --dryrun
(dryrun) upload: ../Pictures/Bender_Rodriguez.png to s3://my_bucket/home/me/Pictures/Bender_Rodriguez.png
(dryrun) upload: ../Pictures/Panttaus/sormus_paalta.png to s3://my_bucket/home/me/Pictures/Panttaus/sormus_paalta.png
(dryrun) upload: ../Pictures/Panttaus/sormus_sivusta.png to s3://my_bucket/home/me/Pictures/Panttaus/sormus_sivusta.png
(dryrun) upload: ../Pictures/Screenshot from 2021-03-13 22-30-26.png to s3://my_bucket/home/me/Pictures/Screenshot from 2021-03-13 22-30-26.png
(dryrun) upload: ../Pictures/willow_7_months.jpg to s3://my_bucket/home/me/Pictures/willow_7_months.jpg
Run Code Online (Sandbox Code Playgroud)

如果双引号位于单引号内,即输入set -x显示的内容,如果我运行:

me@my_machine:~/scripts$ aws s3 sync /home/me/Pictures s3://my_bucket/home/me/Pictures --exclude '"*"' --include '"*.gif"' --include '"*.jpg"' --profile=personal --dryrun
(dryrun) upload: ../Pictures/Bender_Rodriguez.png to s3://my_bucket/home/me/Pictures/Bender_Rodriguez.png
(dryrun) upload: ../Pictures/Panttaus/sormus_paalta.png to s3://my_bucket/home/me/Pictures/Panttaus/sormus_paalta.png
(dryrun) upload: ../Pictures/Panttaus/sormus_sivusta.png to s3://my_bucket/home/me/Pictures/Panttaus/sormus_sivusta.png
(dryrun) upload: ../Pictures/Screenshot from 2021-03-13 22-30-26.png to s3://my_bucket/home/me/Pictures/Screenshot from 2021-03-13 22-30-26.png
(dryrun) upload: ../Pictures/willow_7_months.jpg to s3://my_bucket/home/me/Pictures/willow_7_months.jpg
Run Code Online (Sandbox Code Playgroud)

只有正确保留双引号,排除和包含模式才会起作用,如问题中上述。

如果我从输入中完全删除引号:

.jpg
.gif
Run Code Online (Sandbox Code Playgroud)

并且也不要尝试在脚本中添加任何内容:

    aws s3 sync ${params[@]}

Run Code Online (Sandbox Code Playgroud)

结果是单引号:

+ aws s3 sync /home/me/Pictures s3://my_bucket/home/me/Pictures --exclude '*' --include '*.gif' --include '*.jpg' --profile=personal --dryrun
(dryrun) upload: ../Pictures/Bender_Rodriguez.png to s3://my_bucket/home/me/Pictures/Bender_Rodriguez.png
(dryrun) upload: ../Pictures/Panttaus/sormus_paalta.png to s3://my_bucket/home/me/Pictures/Panttaus/sormus_paalta.png
(dryrun) upload: ../Pictures/Panttaus/sormus_sivusta.png to s3://my_bucket/home/me/Pictures/Panttaus/sormus_sivusta.png
(dryrun) upload: ../Pictures/Screenshot from 2021-03-13 22-30-26.png to s3://my_bucket/home/me/Pictures/Screenshot from 2021-03-13 22-30-26.png
(dryrun) upload: ../Pictures/willow_7_months.jpg to s3://my_bucket/home/me/Pictures/willow_7_months.jpg
Run Code Online (Sandbox Code Playgroud)

再次强调,.png 文件不会被忽略。

并在脚本中加上引号:

    aws s3 sync "${params[@]}"
Run Code Online (Sandbox Code Playgroud)

它引用了整个参数:

+ aws s3 sync /home/me/Pictures s3://my_bucket/home/me/Pictures '--exclude *' '--include *.gif' '--include *.jpg' --profile=personal --dryrun

Unknown options: --exclude sync.sh,--include *.png,--include *.jpg
Run Code Online (Sandbox Code Playgroud)

另外,只是简化脚本,即:

#!/bin/bash

set -x

DRYRUN=true

s3_bucket_uri='s3://my_bucket'
aws_profile='--profile=personal'
backup_config_folder='../config/*'
include_file='includes.txt'
exclude_file='excludes.txt'

includes=()
excludes=()

sync () {
    local params=()
    local local_folder="$HOME/$1"
    local bucket_folder="$s3_bucket_uri""$local_folder"

    params+=("$local_folder" "$bucket_folder")

    if [[ ${excludes[@]} ]]; then
        params+=("${excludes[@]}")
    fi
    
    if [[ ${includes[@]} ]]; then
        params+=("${includes[@]}")
    fi

    params+=("$aws_profile")

    if [[ "$DRYRUN" = true ]]; then
        params+=(--dryrun)
    fi

    aws s3 sync "${params[@]}"
}

read_parameters () {
    if [[ -f "$1" ]]; then
        while read line; do
            if [[ $2 == "include" ]]; then
                includes+=(--include "$line")
            elif [[ $2 == "exclude" ]]; then
                excludes+=(--exclude "$line")
            fi
        done < $1
    fi
}

reset () {
    includes=()
    excludes=()
}

for folder in $backup_config_folder; do
    if [[ -d "$folder" && ! -L "$folder" ]]; then
        read_parameters $folder/$exclude_file exclude
        read_parameters $folder/$include_file include
        sync "${folder##*/}"
        reset
    fi
done
Run Code Online (Sandbox Code Playgroud)

在输出中给出单引号,它终于可以工作了。

+ aws s3 sync /home/me/Pictures s3://my_bucket/home/me/Pictures --exclude '*' --include '*.gif' --include '*.jpg' --profile=personal --dryrun
(dryrun) upload: ../../../Pictures/willow_7_months.jpg to s3://my_bucket/home/me/Pictures/willow_7_months.jpg
Run Code Online (Sandbox Code Playgroud)

因此,我想教训是:一开始就不要尝试使用双引号。

mur*_*uru 5

您误解了 的调试输出set -x。Bash 在记录由于 运行的命令时set -x,会显示规范的引用,您可以从中导出通过应用引用删除而使用的实际命令。

假设你有一个类似foo "a b"- 一个带有空格的参数的命令。当 bash 需要将其记录为 时set -x,它需要一种方法来表明它a b是单个参数。因此,它在输出中显示了引用的版本 - 如果在命令行中使用该版本,则会得到以下输出:

$ foo a\ b
+ foo 'a b'
Run Code Online (Sandbox Code Playgroud)

假设您必须运行foo带有参数的 command ,a"b"即该命令应该接收这些引号,那么在 bash 中,您通常会运行其中之一的某种变体:

foo 'a"b"'
foo a\"b\"
foo 'a"b'\"
Run Code Online (Sandbox Code Playgroud)

现在,当需要记录此命令时,bash 现在必须显示引号也被引用,否则人们可能会认为这些引号是由于第一个代码块中的问题造成的。所以,我们得到:

$ foo a\"b\"
+ foo 'a"b"'
Run Code Online (Sandbox Code Playgroud)

Bash 不会添加或删除任何内容 - 它只是向您展示它将运行什么,并通过引用进行澄清。

因此,如果您'"*"'在调试输出中看到,您不会看到 bash 添加单引号。您看到 bash 试图向您显示它是"*"在删除引号后得到的,因此它必须将双引号传递给命令。您应该想知道这些双引号来自哪里,我猜那是来自您的输入文件。


这两个代码块过于复杂:

if [[ ${excludes[@]} ]]; then
    params+=("${excludes[@]/#/--exclude }")
fi

if [[ ${includes[@]} ]]; then
    params+=("${includes[@]/#/--include }")
fi   
Run Code Online (Sandbox Code Playgroud)
if [[ $2 == "include" ]]; then
    includes+=($line)
elif [[ $2 == "exclude" ]]; then
    excludes+=($line)
fi
Run Code Online (Sandbox Code Playgroud)

只需在阅读模式时添加选项即可:

if [[ $2 = include ]]; then
    includes+=(--include "$line")
elif [[ $2 = exclude ]]; then
    excludes+=(--exclude "$line")
fi
Run Code Online (Sandbox Code Playgroud)

然后你就可以直接使用这些数组而无需进一步操作。

当然,请记住在变量周围使用引号,除非您希望它们进行字段分割、文件名扩展等。