为什么 bash 脚本中的字符串变量在回显时的行为与传递给可执行文件时的行为不同?

为什么 bash 脚本中的字符串变量在回显时的行为与传递给可执行文件时的行为不同?

我有以下脚本:

#!/bin/bash

KID3=$(command -v kid3-cli)
ARG1="-c 'get'"
file="'1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'"

echo Command:
echo "$KID3" "$ARG1" "$file"

echo The kid3-cli output:
"$KID3" "$ARG1" "$file"

这将产生以下内容:

Command:
/usr/bin/kid3-cli -c 'get' '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'
The kid3-cli output:
-c 'get', '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac' does not exist

请注意 kid3-cli 输出中的逗号。我尝试过使用 accolades 而不是引号(即${KID3} ${ARG1} ${file}),但这似乎没有任何区别。如果我使用多个命令变量 ($ARG1、$ARG2 等),kid3-cli 输出中的每个变量后面都会有一个逗号。

当我将命令(/usr/bin/kid3-cli -c 'get' '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac')复制并粘贴到交互式 bash shell(即在命令提示符下)中时,它可以起作用。

kid3-cli 输出中的这些逗号从何而来,我遗漏了什么,我该怎么做才能解决这个问题?

答案1

工作命令

当我将命令(/usr/bin/kid3-cli -c 'get' '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac')复制并粘贴到交互式 bash shell(即在命令提示符下)中时,它可以起作用。

一个好的起点是了解这里发生了什么;然后更容易看出在其他情况下会发生其他事情。

你的命令是:

/usr/bin/kid3-cli -c 'get' '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'

shell 将命令拆分为单词,使用未转义和未加引号的空格和制表符作为分隔符。单引号告知 shell 这get是一个单词(即使没有引号,它也是一个单词),这1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac是一个单词(即使没有引号,它也是一个单词)不是是一个不带引号的单词)。实际上,这些词是:

  • /usr/bin/kid3-cli
  • -c
  • get
  • 1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac

请注意,引号已完成其作用并且已被删除。

然后 shell 运行/usr/bin/kid3-cli并给出三个参数:-cget1-01 - Johann Strauss - __Waldmeister___ Ouverture.flackid3-cli无法知道是否以及在何处使用了引号。显然,三参数是一个元组,该工具可以理解而不会出错。


麻烦的命令

现在这个:

"$KID3" "$ARG1" "$file"

双引号中的非数组变量始终扩展为一个单词(除非出现错误,例如由于set -u变量未设置)。这三个变量将扩展为三个单词:

  • /usr/bin/kid3-cli从扩展"$KID3"
  • -c 'get'从扩展"$ARG1"
  • '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'从扩展"$file"

从扩展变量中出现的引号并不特殊。它们可能稍后会变得特殊,当(如果!)你告诉 shell(或另一个 shell,或其他)解析结果时再次(例子:eval)。这里你不必也不需要告诉 shell 再次解析结果;而且,这样做是一种不好的做法。

shell 运行/usr/bin/kid3-cli并为其提供两个参数:-c 'get''1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'。不仅-cget在单个参数内;而且单引号也到达kid3-cli。显然,这不是该工具可以以您想要的方式理解的东西。


关于什么echo

echo通过逐字打印其所有参数(除了被解释为选项的参数)来工作,每个非最后一个参数与下一个参数之间有一个空格字符;加上默认情况下一个尾随换行符。

echo "$KID3" "$ARG1" "$file"结果是以下单词:

  • echo
  • /usr/bin/kid3-cli从扩展"$KID3"
  • -c 'get'从扩展"$ARG1"
  • '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'从扩展"$file"

shell 运行echo并给出三个参数:/usr/bin/kid3-cli-c 'get''1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'。你会看到:

/usr/bin/kid3-cli -c 'get' '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'

其中一些空格来自变量,其他空格来自。请注意,如果获得、、、、、、和(8 个参数)或将所有内容作为一个长参数,则输出将echo相同。echo/usr/bin/kid3-cli-c'get''1-01-Johann Strauss-__Waldmeister___ Ouverture.flac'echo

如果您echo在工作命令前添加参数,即如果您为其提供工作命令的单词参数,即如果您运行:

echo /usr/bin/kid3-cli -c 'get' '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'

那么输出将是:

/usr/bin/kid3-cli -c get 1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac

再次没有任何迹象表明哪些空格是由 添加的echo

echo并不是“可视化”参数的好工具。我个人更喜欢printf '<%s>\n'。在你的情况下printf '<%s>\n' "$KID3" "$ARG1" "$file"会打印:

</usr/bin/kid3-cli>
<-c 'get'>
<'1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'>

并且它会非常清楚-c 'get'是一个单一的参数并且单引号得到了printf

仍然存在歧义:如果一个参数是/usr/bin/kid3-cli>\n<-c 'get'\n这里表示换行符,但我的意思是文字参数内部有换行符),那么我的printf会将其显示为单独的参数:/usr/bin/kid3-cli-c 'get'。不过,带有>\n<内部的参数很少见(至少在我的工作流程中如此;请<%s>\n根据您的需要进行调整)。


未引用$ARG1

如果您设置:

ARG1="-c get"
file="1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac"

如果你运行:

"$KID3" $ARG1 "$file"

那么这将有效(使用默认的$IFS)。不带引号的$ARG1将被拆分为两个单词:-cget。请注意,如果您想kid3-cli查看get(不是'get'),则不能存储-c 'get'在里面ARG1。同样file="'…'"在这里也是错误的。

$ARG1如果变量的值是静态的,完全由您控制,没有触发文件名生成(通配符)的字符,那么不加引号并不是那么糟糕,这样可以$IFS将值准确地拆分到您想要的位置。一般来说,您应该总是引用

最重要的是 shell 变量用于存储数据,而不是用于存储 shell 代码。在/usr/bin/kid3-cli -c 'get' …您输入的内容中,单引号对 shell 具有语法意义,后面的空格也是如此-c。这些部分是 shell 代码,将它们存储在变量中是错误的。有时(例如此处)您可以存储有意义的空格而不引用变量,但这仍然是一种不好的做法。

其他想法在这里:我们如何运行存储在变量中的命令?


解决方案

(不是唯一的解决方案,参见已经链接的问题。一个可靠、安全、足够优雅的解决方案,适合您的代码结构。)

在 Bash 中,你可以使用数组来可靠地存储多个单词:

KID3=$(command -v kid3-cli)
ARG1=( -c 'get' )
file="1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac"
"$KID3" "${ARG1[@]}" "$file"

单引号包围get是不需要的,我只是想向你展示它们可能be there."${ARG1[@]}"会扩展为两个词:-cget

通过这种方法,您也可以只使用一个逐步构建的数组:

cmnd=( "$(command -v kid3-cli)" )
cmnd+=( -c get )
cmnd+=( '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac' )
"${cmnd[@]}"

(引号很1-01 … ….flac重要)。或者使用一步构建的一个数组:

cmnd=( "$(command -v kid3-cli)" -c get '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac' )
"${cmnd[@]}"

在每种情况下,结果命令都等同于:

command kid3-cli -c get "1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac"

除非您有充分的理由使用变量,否则您应该只运行上述操作,甚至可能不带单词command

相关内容