当前位置 博文首页 > astrotycoon:bash之波浪号扩展(tilde expansion)

    astrotycoon:bash之波浪号扩展(tilde expansion)

    作者:[db:作者] 时间:2021-08-06 13:09

    写在前面

    对于使用过linux的人来说,对于波浪号扩展应该不会陌生,都知道~号代表的是当前用户的宿主目录。

    但是呢,我想说的是,对于波浪号扩展的认知了解到这个程度可能还不够,波浪号扩展还有其它的意义。此外我们应该还需要进一步了解什么情况下bash会进行波浪号扩展,即进行波浪号扩展需要满足哪些条件,了解了这些之后,你就会觉得:哦,原来进行波浪号扩展的条件还是相当苛刻的啊!

    这里需要提一下,大家有没有觉得奇怪,为什么在linux和unix的世界里会用波浪号(~)来代表用户宿主目录呢? 其实这是有历史渊源的,原来在早期的计算机键盘上,HOME键(功能是移动到左上角或者是移动到页面的最开始位置)和波浪号(~)共用同一个物理按键,详情参考文章《Linux系统中用波浪号~表示用户的根目录即$HOME,以及为何用波浪号表示用户根目录》。

    什么是波浪号扩展

    如果你之前大致了解过bash的所有扩展,就会知道在这所有的扩展中有一类扩展类型是跟文件名或者目录名相关的,也就是说扩展后的结果是系统中存在的文件名或者目录名。好,bash的所有扩展中就有两个扩展是出于这样的目的,第一个就是本篇博文将要说明的波浪号扩展,第二个是路径扩展(pathname expansion),关于路径扩展详细参见博文《bash之通配符》。

    讲白了,波浪号扩展就是一种跟文件名或者目录名相关的一种的shell扩展,更确切地说波浪号扩展的结果是目录名 -- 扩展的结果可以归纳为以下三种特殊的目录:

    (1)宿主目录(home directories)

    (2)当前工作目录(current working directory)和上一次工作目录(previous working directory)

    (3)目录栈(?directory stack)里的目录

    ?

    bash何时会进行波浪号扩展

    仔细阅读bash的man手册,我归纳有两种情况bash会进行波浪号扩展。

    在这之前,让我们先了解一个概念,即“波浪号前缀tilde-prefix)”,对波浪号前缀的定义,手册中有这么一段话:

    If a word begins with an unquoted tilde character (`~'), all of the characters preceding the first unquoted slash? (or? all? characters,? if there? is no unquoted slash) are considered a tilde-prefix.

    意思就是说:一个单词(word)如果以未被转义的波浪号打头,那么从这个未被转义的波浪号(~)开始到第一个未被转义的斜杠(/)结束的一段字符串,被叫做波浪号前缀(tilde-prefix)。注意,这个波浪号前缀包括波浪号在内,但是不包括斜杠。如果后续没有未被转义的斜杠(/),那么后续的字符串都是波浪号前缀的一部分。例如,"~root/test"中"~root"是波浪号前缀,不包括后面的"/test"。

    ?

    好了,知道了波浪号前缀的概念后,让我们列出哪些条件下bash会进行波浪号扩展吧!

    (1)波浪号(~)在行首的位置,并且波浪号前缀中不存在转义的字符。

    (2)波浪号(~)不在行首,但是却是字符“=”或者":"之后的第一个字符,并且波浪号前缀中不存在转义字符 -- 也就是在变量赋值时。

    好了,接下来结合实际的例子对波浪号扩展做详细的介绍。

    ?

    用户宿主目录

    在shell中波浪号(~)代表当前用户的宿主目录,我想这个应该是大家最为熟知的。

    先给出结论:当波浪号前缀中不存在转义字符时,shell会认为~后的字符串是一个登陆名(login name)。

    (1)波浪号(~)后的字符串如果为空的话,shell默认会将波浪号(~)扩展为环境变量HOME的值,如果变量HOME没有设置,则默认扩展为当前用户的主目录。

    [19:58:39@astrol:/tmp]$ whoami
    astrol
    [19:58:45@astrol:/tmp]$ pwd
    /tmp
    [19:58:46@astrol:/tmp]$ echo $HOME
    /home/astrol
    [19:58:49@astrol:/tmp]$ echo ~
    /home/astrol
    [19:58:51@astrol:/tmp]$ cd ~
    [19:58:53@astrol:~]$ pwd
    /home/astrol
    [19:58:54@astrol:~]$ cd /tmp/
    [19:58:57@astrol:/tmp]$ unset HOME
    [19:59:02@astrol:/tmp]$ echo $HOME
    
    [19:59:08@astrol:/tmp]$ cd ~
    [19:59:15@astrol:/home/astrol]$ pwd
    /home/astrol

    说明:当前用户是astrol,当前工作目录在目录tmp下,当前环境变量HOME的值是astrol的根目录,使用echo ~输出,结果显示~代表的正是astrol的根目录路径。然后cd ~,切换到了astrol的根目录。再回到tmp目录下,取消HOME的值,再cd ~,默认还是回到了astrol用户的根目录下。

    在HOME值为空时,shell是怎么找到当前用户的根目录的呢? 当然是通过getpwnam读取/etc/passwd这个文件获取的啦~

    (2)波浪号(~)后的字符串如果不为空,shell会去读取/etc/passwd文件获取该用户根目录,如果用户不存在,则不进行任何扩展,原样输出。

    [20:12:14@astrol:~]$ whoami
    astrol
    [20:12:19@astrol:~]$ echo ~
    /home/astrol
    [20:12:21@astrol:~]$ echo ~astrol
    /home/astrol
    [20:12:23@astrol:~]$ echo ~root
    /root
    [20:12:26@astrol:~]$ echo ~aaaa
    ~aaaa

    当前用户是astrol,因此~和~astrol都被扩展为astrol用户的根目录,但是注意,两者扩展的过程是否一样是根据HOME值是否为空来决定的。如果HOME值不为空,则shell直接使用HOME值来扩展~,否则扩展的过程和~astrol的过程是一样的,都是去读取/etc/passwd文件。

    ~root表示扩展为root用户的根目录,但是~aaaa就有问题了,因为系统中并不存在aaaa这个用户,所以原样输出。

    ?

    当前工作目录和上一次工作目录

    波浪号扩展的这个功能就很少有人知晓了。

    (1)如果波浪号前缀是“~+”,那么shell会用环境变量PWD的值来扩展。

    (2)如果波浪号前缀是“~-”,那么shell会用环境变量OLDPWD的值来扩展,如果OLDPWD的值为空,则原样输出“~-”。

    来一起看一个例子:

    [09:43:25@astrol:/tmp]$ pwd
    /tmp

    [09:43:27@astrol:/tmp]$ echo ~+
    /tmp

    [09:43:31@astrol:/tmp]$ cd /etc/
    [09:43:40@astrol:/etc]$ pwd
    /etc

    [09:43:41@astrol:/etc]$ echo ~+
    /etc

    [09:43:45@astrol:/etc]$ echo ~-
    /tmp

    [09:43:47@astrol:/etc]$ unset OLDPWD
    [09:50:56@astrol:/etc]$ echo ~-
    ~-

    可以看到从tmp切换到etc目录下后,~-的值正确的表示出上一次目录。设置OLDPWD为空后,的确原样输出了。

    ?

    目录栈里的目录

    通过配合bash的pushd和popd功能可以轻松实现多目录之间的切换。通过dirs -v可以看到当前目录栈中有哪些目录。

    波浪号扩展提供了快捷的方式让我们引用当前目录栈中的目录名。

    (1)~+N扩展为目录栈中第N个目录名。

    (2)~-N扩展为目录栈中逆序第N个目录名。

    说明:检索目录栈中的目录是从标号0开始的。另外,如果N大于目录栈中的目录数,则shell原样输出,不做任何扩展。可以省略中间的+和-,那么默认是+。

    来看一个例子:

    [10:28:07@astrol:/tmp]$ dirs -v
    ?0? /tmp
    ?1? /boot
    ?2? /usr
    ?3? /etc
    ?4? ~

    [10:28:29@astrol:/tmp]$ echo ~+0
    /tmp

    [10:28:37@astrol:/tmp]$ echo ~+1
    /boot

    [10:28:42@astrol:/tmp]$ echo ~+2
    /usr

    [10:28:43@astrol:/tmp]$ echo ~+3
    /etc

    [10:28:45@astrol:/tmp]$ echo ~+4
    /home/astrol

    [10:28:46@astrol:/tmp]$ echo ~+5
    ~+5

    [10:28:48@astrol:/tmp]$ echo ~-0
    /home/astrol

    [10:28:50@astrol:/tmp]$ echo ~-1
    /etc

    [10:28:52@astrol:/tmp]$ echo ~-2
    /usr

    [10:28:53@astrol:/tmp]$ echo ~-3
    /boot

    [10:28:55@astrol:/tmp]$ echo ~-4
    /tmp

    [10:28:56@astrol:/tmp]$ echo ~-5
    ~-5

    [10:28:59@astrol:/tmp]$ echo ~0
    /tmp

    [10:29:04@astrol:/tmp]$ echo ~1
    /boot

    [10:29:06@astrol:/tmp]$ echo ~2
    /usr

    [10:29:08@astrol:/tmp]$ echo ~3
    /etc

    [10:29:09@astrol:/tmp]$ echo ~4
    /home/astrol

    [10:29:11@astrol:/tmp]$ echo ~5
    ~5

    可以看到当前目录栈中存在5个目录,“~+0” ~ “~+4”分别对应这个5个目录,“~-0” ~ “~-4”分别逆序对应这5个目录。大于4时,则原样输出。省略+时,默认正序输出。

    ?

    变量赋值时的波浪号扩展

    前面说的3点都是波浪号(~)在单词一开始部分的情况,其实在变量赋值时,shell也是支持波浪号扩展的,不过要满足以下两个条件之一才行。

    (1)波浪号(~)是第一个“=”符号之后的第一个字符。注意是第一个“=”符号

    (2)波浪号(~)是每一个“:”符号之后的第一个字符。注意是每一个“:”符号

    因此我们可以通过如下方式来为环境变量PATH增加路径:

    PATH=~/mybins:~peter/mybins:$PATH

    ?

    波浪号扩展的优点

    波浪号扩展的优势显而易见,那就是简洁。其次就是避免了在shell脚本中写死目录路径,增加了灵活性。假设脚本中有如下片段:

    printf "Enter username: "

    read user

    vi /home/$user/.profile

    这段脚本假设所有的用户宿主目录都在/home目录下,这就很武断了。其实在系统中是存在很多用户的宿主目录不是在/home目录下的,比如一个公司根据不同的部门而创建不同的子目录添加用户。因此最好的写法是这样的:

    printf "Enter username: "

    read user

    vi ~$user/.profle

    这样shell就会去/etc/passwd文件中去正确获取用户user的宿主目录了。

    ?

    Over~~~

    ?

    参考链接:

    《Bash Reference Manual -- Tilde Expansion》

    《Tilde expansion》

    《shell string程序参数中的特殊字符~问题?》

    《The Magic ~: Bash Tilde Expansion with 5 Examples》

    cs