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

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

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

基本布局是有一个配置文件夹,其中有子文件夹引用需要同步的文件夹,每个文件夹包含[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

所以,我可以忽略某些文件。我无法从脚本中获取排除项和包含项,因为 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

输入示例为:

"*.jpg"
"*.gif"

在里面包含.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

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


shellaws 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"

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

+ aws s3 sync /home/me/Pictures s3://mybucket/home/me/Pictures --testing foobar --profile=personal --dryrun

我确实检查过这个问题,但即使有它的答案我得到:

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
+ bar=(--bar a="b")
+ cmd=(foo "${bar[@]}")
+ printf '%q ' foo --bar a=b
foo --bar a=b + echo

+ foo --bar a=b

所以,它失去了双引号。


这是我的 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.

因此,有没有办法解决这个问题,或者我应该用编程语言重写脚本并使用 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

如果双引号位于单引号内,即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

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

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

.jpg
.gif

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

    aws s3 sync ${params[@]}

结果是单引号:

+ 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

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

并在脚本中加上引号:

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

它引用了整个参数:

+ 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

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

#!/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

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

+ 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

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

答案1

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

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

$ foo a\ b
+ foo 'a b'

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

foo 'a"b"'
foo a\"b\"
foo 'a"b'\"

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

$ foo a\"b\"
+ foo 'a"b"'

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

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


这两个代码块过于复杂:

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

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

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

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

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

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

相关内容