![变量扩展中缺少引号删除和转义背后的想法是什么?](https://linux22.com/image/211160/%E5%8F%98%E9%87%8F%E6%89%A9%E5%B1%95%E4%B8%AD%E7%BC%BA%E5%B0%91%E5%BC%95%E5%8F%B7%E5%88%A0%E9%99%A4%E5%92%8C%E8%BD%AC%E4%B9%89%E8%83%8C%E5%90%8E%E7%9A%84%E6%83%B3%E6%B3%95%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.png)
命令可以存储在变量中并在 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?
你看,它很快就会变得荒谬。