【问题标题】:Variables as commands in Bash scripts变量作为 Bash 脚本中的命令
【发布时间】:2010-10-22 03:18:41
【问题描述】:

我正在编写一个非常简单的 Bash 脚本,它 tars 给定目录,加密该目录的输出,然后将生成的文件拆分为多个较小的文件,因为备份媒体不支持大文件。

我在 Bash 脚本方面没有太多经验。我相信我在正确引用我的变量以允许参数中有空格时遇到问题。脚本如下:

#! /bin/bash

# This script tars the given directory, encrypts it, and transfers
# it to the given directory (likely a USB key).

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

DIRECTORY=$1
BACKUP_DIRECTORY=$2
BACKUP_FILE="$BACKUP_DIRECTORY/`date +%Y-%m-%dT%H-%M-%S.backup`"

TAR_CMD="tar cv $DIRECTORY"
SPLIT_CMD="split -b 1024m - \"$BACKUP_FILE\""

ENCRYPT_CMD='openssl des3 -salt'

echo "$TAR_CMD | $ENCRYPT_CMD | $SPLIT_CMD"

$TAR_CMD | $ENCRYPT_CMD | $SPLIT_CMD

say "Done backing up"

运行此命令失败:

split: "foo/2009-04-27T14-32-04.backup"aa: 没有这样的文件或目录

我可以通过删除我设置 $SPLIT_CMD$BACKUP_FILE 周围的引号来修复它。但是,如果我的备份目录名称中有空格,则它不起作用。此外,如果我将“echo”命令的输出直接复制并粘贴到终端中,它工作正常。显然,我不明白 Bash 是如何逃避事情的。

【问题讨论】:

  • 既然可以将 $BACKUP_FILE 放在管道中的 $SPLIT_CMD 之后,为什么还要将它嵌入到 SPLIT_CMD 中?
  • 好吧,我可以这样做,但在这一点上,让变量包含我的命令并没有多大意义,我不妨像下面 Juliano 的回答那样将其全部展开。

标签: bash unix shell


【解决方案1】:

不要将整个命令放在变量中。尝试恢复引用的参数会遇到很多麻烦。

还有:

  1. 避免在脚本中使用全大写的变量名。这是一个简单的方法让自己在脚上开枪。
  2. 不要使用反引号。改用 $(...) ;它嵌套得更好。

#! /bin/bash

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"

【讨论】:

  • 是的,我想我可能会这样做。对我来说似乎没有变量中的命令那么优雅,但我想这就是 bash 脚本的本质。
  • @wxs:将命令放在变量中一点也不优雅。您不会获得任何类型的灵活性;相反,您只是由于分词而导致错误。您可能打算做的是将命令放入函数中。你执行函数。您永远不应该执行可变内容。永远。
  • 我不明白 1. 愿意解释吗?如何/为什么更容易射中自己的脚?
  • ata:因为您可能会无意中破坏环境变量(始终为大写)?
  • @ata:请参阅pubs.opengroup.org/onlinepubs/009695399/basedefs/… 的环境变量名称的 POSIX 约定——明确地,全大写名称由对操作系统或 shell 有意义的变量使用,并且名称至少小写一个-case 字符保证在应用程序使用时是安全的,不会无意中修改 shell 行为。
【解决方案2】:

eval 是不可接受的做法,如果您的目录名称可以由不受信任的来源生成。有关为什么不应使用 eval 的更多信息,请参阅 BashFAQ #48,有关此问题的根本原因及其正确解决方案的更多信息,请参阅 BashFAQ #50,其中一些在下面有所提及:

如果您需要随着时间的推移构建命令,请使用数组:

tar_cmd=( tar cv "$directory" )
split_cmd=( split -b 1024m - "$backup_file" )
encrypt_cmd=( openssl des3 -salt )
"${tar_cmd[@]}" | "${encrypt_cmd[@]}" | "${split_cmd[@]}"

或者,如果这只是在一个中心位置定义您的命令,请使用函数:

tar_cmd() { tar cv "$directory"; }
split_cmd() { split -b 1024m - "$backup_file"; }
encrypt_cmd() { openssl des3 -salt; }
tar_cmd | split_cmd | encrypt_cmd

【讨论】:

    【解决方案3】:

    我不确定,但可能值得先在命令上运行 eval(靠近“args 被读取并连接在一起”)。

    这将让 Bash 将变量 $TAR_CMD 等扩展到它们的全部宽度(就像 echo 命令对控制台所做的那样,你说它有效)。

    Bash 将在扩展变量的情况下第二次读取该行。

    eval $TAR_CMD | $ENCRYPT_CMD | $SPLIT_CMD
    

    页面 Bash: Why use eval with variable expansion? 看起来在解释为什么需要这样做方面做得不错。

    【讨论】:

    • 这似乎也是如此,但我觉得这让一切都变得有点太复杂了。哦,好吧,我明白为什么人们发明了其他脚本语言。
    • 这是一个重大的安全风险。请参阅 BashFAQ #50 了解最佳实践替代方案:mywiki.wooledge.org/BashFAQ/050 -- 和 BashFAQ #48 了解为什么 eval 会带来风险:mywiki.wooledge.org/BashFAQ/048
    • 很好@CharlesDuffy,如果在共享系统上使用它,其他用户被授予访问脚本以使用提升的权限运行,这是一个风险。
    • @Eddie,不仅在共享系统上,还针对您无法控制的数据。如果你有一个脚本从远程服务器镜像文件并解压它们,并且这些文件名可以被恶意修改,eval 会带来不合理的风险。有时,“恶意”甚至不是必需的——我曾经看到一个由缓冲区溢出导致的重大数据丢失事件,该事件将 随机 内容转储到文件名中,这些文件名后来由一个草率编写的脚本处理。跨度>
    【解决方案4】:

    只有将命令和选项放在变量中是有道理的。

    #! /bin/bash
    
    if [ $# -ne 2 ]
    then
        echo "Usage: `basename $0` DIRECTORY BACKUP_DIRECTORY"
        exit 1
    fi
    
    . standard_tools    
    
    directory=$1
    backup_directory=$2
    current_date=$(date +%Y-%m-%dT%H-%M-%S)
    backup_file="${backup_directory}/${current_date}.backup"
    
    ${tar_create} "${directory}" | ${openssl} | ${split_1024} "$backup_file"
    

    您可以将命令重新定位到您源的另一个文件,这样您就可以在许多脚本中重复使用相同的命令和选项。当您有很多脚本并且想要控制它们如何使用工具时,这非常方便。所以standard_tools 将包含:

    export tar_create="tar cv"
    export openssl="openssl des3 -salt"
    export split_1024="split -b 1024m -"
    

    【讨论】:

    • tar_create 不见了,但仍然有帮助
    • 这实际上并不能解决复杂参数的问题。如果你的tar_createtar cv --exclude="* *",它的失败方式与原来的大致相同。而exports 在这里没有任何用处——这些变量在同一个 shell 中使用,因此污染子进程的环境空间简直是浪费。
    【解决方案5】:

    在变量中引用空格以使 shell 正确地重新解释事物是困难。正是这种类型的东西促使我寻求一种更强大的语言。无论是PerlPythonRuby 还是其他(我选择 Perl,但这并不总是适合所有人),它只是 一些东西 可以让您绕过 shell 进行引用。

    并不是说我从来没有通过大量的eval 来解决它,而是那个 eval 给了我 heebie-jeebies (当你想接受用户输入并对其进行评估时,这会变成一个全新的头痛,虽然在这种情况下,您将使用您编写的内容并对其进行评估),并且我在调试时感到头疼。

    以 Perl 为例,我可以执行以下操作:

    @tar_cmd = ( qw(tar cv), $directory );
    @encrypt_cmd = ( qw(openssl des3 -salt) );
    @split_cmd = ( qw(split -b 1024m -), $backup_file );
    

    这里最困难的部分是做管道——但有点IO::Pipe、fork 和重新打开standard outputstandard error,这还不错。有人会说这比正确引用 shell 更糟糕,我理解它们的来源,但是,对我来说,这更容易阅读、维护和编写。哎呀,有人可以从中完成艰苦的工作并创建一个 IO::Pipeline 模块并使整个事情变得微不足道;-)

    【讨论】:

    • 只有使用eval 使成为难题,这才是难题。没有充分的理由这样做。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-28
    • 2011-10-16
    • 2011-06-04
    • 2015-04-24
    • 2018-04-02
    相关资源
    最近更新 更多