变量扩展中缺少引号删除和转义背后的想法是什么?

变量扩展中缺少引号删除和转义背后的想法是什么?

命令可以存储在变量中并在 shell 中执行(尽管不是一个好的做法),例如:

command='ls -l A* "B\" type"'
$command

它列出了以A"B\"和开头的文件type"。执行参数分隔和通配符,但不删除引号和转义。这种行为使得在不支持数组的 shell 中使用一个变量传递任意参数非常困难,并且不可能安全地组合find其他命令for(这经常被讨论)。它甚至限制了通配符的使用,因为许多字符在未加引号的变量扩展中不受控制(您无法存储包含`'"*?\n文字的通配符序列并正确重用它们)。

情况会是非常可以处理变量中不同的 if 引号和转义序列。但为什么大多数 shell 实际上不这样做呢?它是专门设计的,有一些我没有注意到的晦涩的考虑,还是只是简单地传承下来以保持兼容性?我知道还有类似的问题为什么 bash 变量扩展保留引号?“变量中的命令”中的引用/转义/扩展问题讨论了这种行为,但那里的答案没有谈到原因。

答案1

这种行为使得使用一个变量传递任意参数变得非常困难[...]

也许。但是,如果让扩展的结果经过所有通常的命令行处理,就不可能完整地传递一个任意参数。

例如,考虑一个从某处获取文件名并尝试将其传递给命令的脚本。假设我们通过以下方式获取文件名read

echo -n "please enter filename: "
read -r filename
some command "$filename"

现在,如果用户输入类似 的文件名don't stop me now.txt,运行some command将因单引号而崩溃并出现语法错误。

类似地,如果脚本运行为例如myscript don*.txt并从命令行参数获取文件名:

filename=$1
some command "$filename"

再次$filename(或$1已经)将包含该单引号。

更糟糕的是,文件名或用户输入字符串可能包含命令替换,从而可能仅使用变量运行任意命令。脚本编写者必须费力地向从脚本外部读取的每个字符串添加转义符,并且某些这样做的方法可能已经触发了扩展处理。另外,人们不会这样做,而且 shell 作为工具使用起来更加不安全。

(对于您想要的内容,不需要处理扩展,只需处理引号和反斜杠,但不成对引号的问题仍然存在。)

当然,您也可以说read应该只添加必要的转义符,但是是否也需要为所有其他类型的输入添加它们?字符串操作如何工作,它们是否也需要处理引号?即使是像${#var}变量长度这样简单的事情,实施起来也会变得更加昂贵。对于包含多个不同的带引号字符串的变量来说,长度意味着什么?

最后,最好考虑一下代码脚本的区别于数据脚本处理并组织它,以便它们不会混淆,以便数据仅以代码中明确设置的方式进行处理。如果您记得引用变量扩展,那么 shell 几乎就是这样做的。

按原样使用变量中的数据也是所有其他编程语言所做的。例如,在这个 C 代码片段中,打印的字符串是"foo bar",带有引号,它们不会被运行时环境解析:

char *s = "\"foo bar\"";
printf("%s\n", s);

类似地,如果是s = "foo()"相反,该printf()调用将不会调用该函数foo(),而只会打印字符串foo()。 (如果您想反对解释型语言与编译型语言,我们可以将示例更改为 Perl 或 Python。)


现在,这只是一个论点,说明为什么你的建议在 2022 年对我来说似乎不是一个好主意。但实际上,你问的是“为什么”和设计原理。这些并不是发生在 2022 年,而是发生在 1970 年代和 1980 年代左右。维基百科提到Bourne Shell 的首次发布发生在 1979 年。那是很久以前的事了,当时计算的现有历史比现在要短得多。我们现在有了事后诸葛亮的一些好处,这可能有助于创建其他工具,例如那些 shell 数组。更快的计算机和更多的内存。

我不会拒绝这样的想法,即设计背后的实际解释可能类似于“这就是他们在第一次弄清楚这一切时想到的,并且由于某种原因它卡住了”。向后兼容性有两种方式。至少现在你有了那些带有数组的 shell,以及完全不同的 shell。

答案2

因为引号删除仅适用于 shell 语法中的引号,所以原则上可编译功能。也就是说,实际上不必在替换后的 shell 中进行任何运行时引号删除。这是一个以这种方式解释的抽象,但实际上在解析语法时可以删除引号。

像这样的命令行组件"foo $bar"可以变成带引号的单元。 shell 的解析器会记住这是被引用的,但不记得实际的引号;只有foo $bar内部。当该项目在运行时处理时,$bar将逐字插值,仅此而已。而像这样的命令行项abc$bar可以成为不带引号的单元,其运行时语义是插入$bar,然后拆分为字段,并执行路径名扩展。

在这种模型下,如果对变量的内容进行引号处理,则意味着 shell 也必须在运行时进行词法扫描和解析活动。

这基本上与语法关键字不能来自变量的原因相同,例如:

thenvar=then
fivar=fi

# nonsense
if command; $thenvar
  echo command succeeded
$fivar

为什么 shell 无法识别关键字的$thenvar内容then

出于完全相同的原因,它不会将存储在变量中的引号识别为语法引号。

现在,shell 在某种程度上在运行时和解析时间之间进行了“混合级别”。如果扩展未加引号,则会将其拆分为多个字段。此外,如果扩展包含通配字符,那么这些字符是活动的。

可以说,这些功能也是语法,但也可以动态地来自数据。

唉,正是这些功能及其关卡混合导致了 shell 编程中的混乱和错误。忘记引用,本来应该是数据逐字部分的*,或 空格就会被破坏。?

处理来自数据的报价会导致更多混乱和错误。例如,如果一个变量包含不平衡的引号,就会出现语法错误,对吗?等等,什么?运行时数据引起的语法错误?或者,您是否允许变量中包含引号来平衡语法中的引号?这是否有效:

quote='"'
echo "foo bar$quote   # does $quote close the open quote?

你看,它很快就会变得荒谬。

相关内容