如何扩展波浪号 ~ 作为变量的一部分?

And*_*rew 16 shell bash shell-script quoting variable

当我打开一个 bash 提示符并输入:

$ set -o xtrace
$ x='~/someDirectory'
+ x='~/someDirectory'
$ echo $x
+ echo '~/someDirectory'
~/someDirectory
Run Code Online (Sandbox Code Playgroud)

我希望上面的第 5 行会出现+ echo /home/myUsername/someDirectory。有没有办法做到这一点?在我的原始 Bash 脚本中,变量 x 实际上是从输入文件中的数据填充的,通过这样的循环:

while IFS= read line
do
    params=($line)
    echo ${params[0]}
done <"./someInputFile.txt"
Run Code Online (Sandbox Code Playgroud)

尽管如此,我得到了类似的结果,echo '~/someDirectory'而不是echo /home/myUsername/someDirectory.

Whi*_*olf 17

POSIX标准按照下面的顺序来进行强加字膨胀(强调的是矿):

  1. 波浪号扩展(参见波浪号扩展)、参数扩展(参见参数扩展)、命令替换(参见命令替换)和算术扩展(参见算术扩展)应从头到尾执行。请参阅令牌识别中的第 5 项。

  2. 除非 IFS 为空,否则应对步骤 1 生成的字段部分执行字段拆分(参见字段拆分)。

  3. 除非 set -f 有效,否则应执行路径名扩展(请参阅路径名扩展)。

  4. 报价删除(参见报价删除)应始终最后执行。

这里唯一让我们感兴趣的是第一点:正如你所看到的,波浪号展开是在参数展开之前处理的:

  1. shell 尝试在 上进行波浪号扩展echo $x,但找不到波浪号,因此它继续进行。
  2. shell 尝试对 进行参数扩展echo $x$x找到并扩展,命令行变为echo ~/someDirectory
  3. 处理继续,波浪号扩展已经处理,~字符保持原样。

通过在分配 时使用引号$x,您明确要求不要扩展波浪号并将其视为普通字符。经常忽略的一件事是,在 shell 命令中,您不必引用整个字符串,因此您可以在变量赋值期间进行扩展:

user@host:~$ set -o xtrace
user@host:~$ x=~/'someDirectory'
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$
Run Code Online (Sandbox Code Playgroud)

并且您也可以在echo命令行上进行扩展,只要它可以参数扩展之前发生:

user@host:~$ x='someDirectory'
+ x=someDirectory
user@host:~$ echo ~/$x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$
Run Code Online (Sandbox Code Playgroud)

如果由于某种原因您确实需要在$x不扩展的情况下影响变量的波浪号,并且能够在echo命令中扩展它,则必须进行两次以强制$x发生变量的两次扩展:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ echo "$( eval echo $x )"
++ eval echo '~/someDirectory'
+++ echo /home/user/someDirectory
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 
Run Code Online (Sandbox Code Playgroud)

但是,请注意,根据您使用此类结构的上下文,它可能会产生不需要的副作用。根据经验,eval当您有其他方法时,最好避免使用任何需要的东西。

如果你想专门解决波浪号问题而不是任何其他类型的扩展,这样的结构会更安全和便携:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ case "$x" in "~/"*)
>     x="${HOME}/${x#"~/"}"
> esac
+ case "$x" in
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 
Run Code Online (Sandbox Code Playgroud)

此结构明确检查前导的存在,~如果找到,则将其替换为用户主目录。

根据您的评论,x="${HOME}/${x#"~/"}"对于未在 shell 编程中使用过的人来说,这可能确实令人惊讶,但实际上与我上面引用的相同 POSIX 规则相关联。

正如 POSIX 标准所强加的,引用删除发生在最后,参数扩展发生在很早的时候。因此,${#"~"}在评估外部引号之前很远地评估和扩展。反过来,如参数扩展规则中所定义:

在需要 word 值的每种情况下(基于参数状态,如下所述),word 应进行波浪号扩展、参数扩展、命令替换和算术扩展。

因此,#必须正确引用或转义运算符的右侧以避免波浪号扩展。

因此,换一种说法,当 shell 解释器查看 时x="${HOME}/${x#"~/"}",他会看到:

  1. ${HOME}并且${x#"~/"}必须扩大。
  2. ${HOME}扩展到$HOME变量的内容。
  3. ${x#"~/"}触发嵌套扩展:"~/"被解析,但被引用,被视为文字1。您可以在此处使用单引号获得相同的结果。
  4. ${x#"~/"}表达式本身现在被扩展,导致前缀~/从 的值中删除$x
  5. 上面的结果现在连接起来: 的扩展${HOME},文字/,扩展${x#"~/"}
  6. 最终结果用双引号括起来,在功能上防止分词。我在这里功能说,因为这些双引号不是技术上需要(见这里那里的实例),但作为个人风格,只要一个任务得到任何超出a=$b我通常发现有更清晰的加双引号。

顺便说一句,如果更仔细地观察case语法,您将看到"~/"*依赖于x=~/'someDirectory'我上面解释的相同概念的构造(这里再次说明,双引号和单引号可以互换使用)。

如果这些东西乍一看似乎很模糊,请不要担心(甚至可能在第二次或以后看到!)。在我看来,参数扩展和子 shell 是使用 shell 语言编程时需要掌握的最复杂的概念之一。

我知道有些人可能强烈反对,但如果你想更深入地学习 shell 编程,我鼓励你阅读高级 Bash 脚本指南:它教 Bash 脚本,所以有很多扩展和花里胡哨的-与 POSIX shell 脚本相比,口哨,但我发现它用大量实际示例编写得很好。一旦你管理好这一点,很容易在需要时限制自己使用 POSIX 功能,我个人认为直接进入 POSIX 领域对于初学者来说是一个不必要的陡峭学习曲线(将我的 POSIX 波浪号替换与@m0dular 的类似正则表达式的 Bash 进行比较相当于了解我的意思;) !)。


1:这导致我在 Dash 中发现了一个错误,它没有在这里正确实现波浪号扩展(可使用 验证x='~/foo'; echo "${x#~/}")。参数扩展对于用户和 shell 开发人员来说都是一个复杂的领域!

  • 谢谢,这是一个很好的答案。我从阅读中学到了很多东西。希望我能多次投票:) (2认同)

m0d*_*lar 7

一种可能的答案:

eval echo "$x"
Run Code Online (Sandbox Code Playgroud)

由于您正在从文件中读取输入,因此我不会这样做。

您可以搜索并替换 ~ 为 $HOME 的值,如下所示:

x='~/.config'
x="${x//\~/$HOME}"
echo "$x"
Run Code Online (Sandbox Code Playgroud)

给我:

/home/adrian/.config
Run Code Online (Sandbox Code Playgroud)