Bash - 扩展命令参数的变量会导致引用地狱

Bash - Expanding variables for command arguments leads to quoting hell

提问人:Aaron 提问时间:9/20/2022 更新时间:9/21/2022 访问量:117

问:

所以,我正在编写一个命令的脚本。我一整天都在摆弄。是时候寻求帮助了。rsync

我的问题似乎是引用,但对我来说并不明显到底出了什么问题。此代码片段:

#!/bin/bash
sudo -v
COMMAND=`basename ${0}`

SRC='/etc'
SRC=`realpath ${SRC}` # make relative paths absolute
SRC=${SRC}/ # force rsync to disallow symlinks to the parent path

if [[ ! -d ${SRC} ]];
then
    echo Use ${COMMAND} to backup directories only
    exit 1
fi

BACKUP_DIR='/tmp/backup prep'
TMP_DIR=${BACKUP_DIR}/tmp
DEST=${BACKUP_DIR}/`basename ${SRC}`
LOG_DIR=${TMP_DIR}
LOG_FILE=${LOG_DIR}/${COMMAND}-`date +%Y-%b-%d-%H-%M-%S-%N`.log
for DIR in "${BACKUP_DIR}" "${TMP_DIR}" "${LOG_DIR}"
do
    if [[ ! -d "'${DIR}'" ]];
    then
        echo Creating "'${DIR}'"
        sudo mkdir "${DIR}"
    fi
done

RSYNC_OPTS=""

#--dry-run, -n
RSYNC_OPTS=${RSYNC_OPTS}" --dry-run"
# --recursive, -r          recurse into directories
RSYNC_OPTS=${RSYNC_OPTS}" --recursive"
#--filter=RULE, -f        add a file-filtering RULE
RSYNC_OPTS=${RSYNC_OPTS}" --filter='dir-merge,p- .gitignore'"

# --checksum, -c           skip based on checksum, not mod-time & size
RSYNC_OPTS=${RSYNC_OPTS}" --checksum"

echo "rsync ${RSYNC_OPTS} '${SRC}' '${DEST}'" | sudo tee "${LOG_FILE}"
echo --------
echo --------
echo
echo
set -x
sudo rsync "${RSYNC_OPTS} '${SRC}' '${DEST}'"

生成以下内容:

Creating '/tmp/backup prep'
mkdir: cannot create directory ‘/tmp/backup prep’: File exists
Creating '/tmp/backup prep/tmp'
mkdir: cannot create directory ‘/tmp/backup prep/tmp’: File exists
Creating '/tmp/backup prep/tmp'
mkdir: cannot create directory ‘/tmp/backup prep/tmp’: File exists
rsync  --dry-run --recursive --filter='dir-merge,p- .gitignore' --checksum '/etc/' '/tmp/backup prep/etc'
--------


+ sudo rsync ' --dry-run --recursive --filter='\''dir-merge,p- .gitignore'\'' --checksum '\''/etc/'\'' '\''/tmp/backup prep/etc'\'''
rsync: [sender] change_dir "/home/aaron/bin/ --dry-run --recursive --filter='dir-merge,p- .gitignore' --checksum '/etc/' '/tmp/backup prep" failed: No such file or directory (2)
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1333) [sender=3.2.3]

并且似乎什么也没做。

问题是,如果我执行回显的命令行,一切似乎都按预期工作。sudo rsync --dry-run --recursive --filter='dir-merge,p- .gitignore' --checksum '/etc/' '/tmp/backup prep/etc'

炮弹扩张让我每一次血腥。如果有一个命令可以告诉你到底发生了什么,那就太好了。

bash rsync 引用变量 扩展

评论

6赞 chepner 9/20/2022
数组是你的朋友。将参数存储在数组中,而不是常规参数中。
4赞 chepner 9/20/2022
您可能还想在代码上使用 shellcheck.net。
6赞 chepner 9/20/2022
此外,测试由单引号构造的名称和值 of 是否为目录。去掉单引号。-d "'${DIR}'"$DIR
8赞 erik258 9/20/2022
“Shell 扩展让我每一次血腥的时间都让我兴奋”有趣的是,当人们想要一种简单、直接的脚本语言时,人们通常会使用 bash,但 shell 脚本既不简单也不直接。你总是可以使用一种更舒适的语言 - 比如说,Python,查看你的问题历史
3赞 M. Nejat Aydin 9/20/2022
双引号之间的字符串将扩展为单个单词(即,将其视为单个参数)。您可能想阅读 BashFAQ/050rsync "${RSYNC_OPTS} '${SRC}' '${DEST}'"rsync

答:

3赞 Aaron 9/21/2022 #1

这是我完成的剧本。非常感谢对我问题的评论者。在你的提示下,我能够取得进步。

就其价值而言,目标是定期备份正在使用 .git 存储库的 /etc 并保留 .git 存储库,但忽略 .gitignore 中的内容etckeeper

一些观察:

  • 在为命令构建参数列表时,数组完全是要走的路。
  • 使用数组时,shell 会漂亮地用空格引用值。Yo 可以通过在下面的脚本中使用来看到这一点。set -x
  • 使用真的很有帮助。set -x
  • 当您努力获得某些东西时,shellcheck 是一个了不起的诊断工具。
#!/bin/bash
COMMAND=$(basename "${0}")
SRC="/etc"
BACKUP_DIR="/root/backup prep"
TMP_DIR="${BACKUP_DIR}/tmp"
LOG_DIR="${TMP_DIR}"

if ! sudo -v
then
    echo ${COMMAND} quitting...
    exit 1
fi

SRC=$(realpath "${SRC}") # make relative paths absolute
SRC="${SRC}"/ # force rsync to disallow symlinks to the parent path

# https://www.shellcheck.net/wiki/SC2086
# Note that $( ) starts a new context, and variables in it have to
# be quoted independently
LOG_FILE="${LOG_DIR}/${COMMAND}-$(date +%Y-%b-%d-%H-%M-%S-%N).log"
for DIR in "${BACKUP_DIR}" "${TMP_DIR}" "${LOG_DIR}"
do
    if sudo test ! -d "${DIR}";
    then
        echo Creating "${DIR}"
        sudo mkdir "${DIR}"
    fi
done

RSYNC_OPTS=()

# ------------------------------------
# -------------Testing----------------
# ------------------------------------
#
#--dry-run, -n
#RSYNC_OPTS+=("--dry-run")

# ------------------------------------
# -------------Verbosity--------------
# ------------------------------------
#
# --verbose, -v            increase verbosity
#RSYNC_OPTS+=("--verbose")
# --progress               show progress during transfer
#RSYNC_OPTS+=("--progress")
# --itemize-changes, -i    output a change-summary for all updates
#RSYNC_OPTS+=("--itemize-changes")
# --stats                  give some file-transfer stats
#RSYNC_OPTS+=("--stats")
# --human-readable, -h     output numbers in a human-readable format
RSYNC_OPTS+=("--human-readable")
# --log-file=FILE          log what we're doing to the specified FILE
RSYNC_OPTS+=("--log-file=${LOG_FILE}")
# --log-file-format=FMT    log updates using the specified FMT


# ------------------------------------
# -------------Permissions------------
# ------------------------------------
#
# --owner, -o              preserve owner (super-user only)
RSYNC_OPTS+=("--owner")
# --group, -g              preserve group
RSYNC_OPTS+=("--group")
# --perms, -p              preserve permissions
RSYNC_OPTS+=("--perms")
# --xattrs, -X             preserve extended attributes
RSYNC_OPTS+=("--xattrs")
# --acls, -A               preserve ACLs (implies --perms)
RSYNC_OPTS+=("--acls")

# ------------------------------------
# -------------Times------------------
# ------------------------------------
#
# --times, -t              preserve modification times
RSYNC_OPTS+=("--times")
# --crtimes, -N            preserve create times (newness)
# rsync: This rsync does not support --crtimes (-N)
# RSYNC_OPTS+=("--crtimes")
# --atimes, -U             preserve access (use) times
RSYNC_OPTS+=("--atimes")
# --open-noatime           avoid changing the atime on opened files
RSYNC_OPTS+=("--open-noatime")

# ------------------------------------
# -------------Where------------------
# ------------------------------------
#
# --mkpath                 create the destination's path component
RSYNC_OPTS+=("--mkpath")
# --recursive, -r          recurse into directories
RSYNC_OPTS+=("--recursive")
# --links, -l              copy symlinks as symlinks
RSYNC_OPTS+=("--links")
# Rsync can also distinguish "safe" and "unsafe" symbolic links.  An
# example where this might be used is a web site mirror that wishes to
# ensure that the rsync module that is copied does not include symbolic
# links to /etc/passwd in  the public section of the site.  Using
# --copy-unsafe-links will cause any links to be copied as the file they
# point to on the destination.  Using --safe-links will cause unsafe
# links to be omitted altogether. (Note that you  must  specify --links
# for --safe-links to have any effect.)
# --copy-unsafe-links      where the link would point outside of the new tree, copy the file
RSYNC_OPTS+=("--copy-unsafe-links")
# --hard-links, -H         preserve hard links
RSYNC_OPTS+=("--hard-links")
# --one-file-system, -x    don't cross filesystem boundaries
RSYNC_OPTS+=("--one-file-system")

# ------------------------------------
# -------------Exclusions-------------
# ------------------------------------
#
#--filter=RULE, -f        add a file-filtering RULE
RSYNC_OPTS+=("--filter=dir-merge,p- .gitignore")
# --delete-excluded        also delete excluded files from dest dirs
RSYNC_OPTS+=("--delete-excluded")
#--delete-after           receiver deletes after transfer, not during
RSYNC_OPTS+=("--delete-after")
# --force                  force deletion of dirs even if not empty
RSYNC_OPTS+=("--force")
# --update, -u             skip files that are newer on the receiver
#RSYNC_OPTS=${RSYNC_OPTS}" --update")

# ------------------------------------
# -------------Misc-------------------
# ------------------------------------
#
# --protect-args, -s       no space-splitting; wildcard chars only
# This option sends all filenames and most options to the remote rsync
# without allowing the remote shell  to  interpret  them.
RSYNC_OPTS+=("--protect-args")
# --checksum, -c           skip based on checksum, not mod-time & size
RSYNC_OPTS+=("--checksum")
# --temp-dir=DIR, -T       create temporary files in directory DIR
RSYNC_OPTS+=("--temp-dir=${TMP_DIR}")

#echo sudo rsync "${RSYNC_OPTS[@]}" "'${SRC}'" "'${DEST}'"
#echo
#set -x
sudo rsync "${RSYNC_OPTS[@]}" "${SRC}" "${DEST}"