跳过正文

Bash - 个人参考手册

·41540 字
Bash Shell
目录

这是我个人的 GNU Bash 参考手册。

1 Shell 历史
#

shell 的名字和概念是从 Unix 的前身 Multics 发展过来的。应用程序通过 shell 来进行调用并被系统执行。

图1  shell history
  1. Thompson shell

    Thompson shell(即 V6 Shell)是历史上第一个 Unix shell,1971 年由 Ken Thompson 写作出第一版并加入 UNIX 之中。它是一个简单的命令行解释器,但不能被用来运行 Shell script 。它的许多特征影响了以后命令行界面的发展。至 Unix V7 之后,被 Bourne shell 取代。当时关键字还只有 if and goto 两种, 管道和重定向就是那个时候做出来的。

  2. PWB shell

    PWB shell 是 Thompson shell 的一个改进版本,完全兼容 Thompson shell。

        VAR=World
        echo "Hello $VAR"
        # hello world
    
        # 或者使用这样消除歧义的写法
        echo "Hello ${VAR}"
    
  3. Bourne shell

    Bourne shell 于 1979 年作为 Unix V7 的一部分首次公开发布。(几乎所有的 Unix 和 类 Unix 系统都是 Unix V7 的后代)

        cat > file1 << EOF
        233
        EOF
    
        echo "两数之和为 : `expr 2 + 2`"
        echo "Hello `echo World`"
    
        go run `ls *.go | grep -v _test.go`
    
  4. Csh

    1978 年,Bill Joy 还在加州大学伯克利分校读书的时候,就为 BSD UNIX(Berkeley Software Distribution UNIX)开发了 C Shell。

    C shell 的 C 是来自于 C 语言,语法更接近 C 语言,但并不和 Bourne shell 兼容。

    csh 对交互使用做出了巨大的改进:

    • 命令历史
    • ~ 出现了作为 $HOME 的另一种写法
    • alias 命令
    • 访问目录栈
    • 任务控制
        alias gc='git commit'
        alias ls='ls -la'
    
        [user@server /etc] $ dirs
        /usr/bin
        [user@server /usr/bin] $ pushd /etc
        /etc /usr/bin
        [user@server /etc] $ popd
        /usr/bin
    
        sheep 10
        # Ctrl-z 暂停这个任务
        # bg 命令把他放到后台运行
        bg
        # 也可以直接放到后台运行
        sleep 10 &
        # fg 把后台最上面的放到前台运行
        fg
    
  5. Tcsh

    Csh 出现的五年之后 卡内基-梅隆大学 的 Ken Greer 引入了 Tenex 系统中的一些功能,如命令行编辑功能和文件名和命令自动补全功能。开发了 Tenex C shell(tcsh)

    在 csh 的基础上增加了:

    • 行编辑
    • 命令补全
    • !! 执行之前的命令
    • !n 执行之前执行的 n 条命令

    Mac OS X 在 10.1 puma 的时候默认 shell 是 tcsh。

  6. KornShell

    KornShell(ksh)是由 David Korn 在 1980 年代初期由贝尔实验室(Bell Labs)开发,并于 1983 年 7 月 14 日在 USENIX 上发布(差不多跟 Tenex C Shell 同时发布)。

    最初的开发基于 Bourne shell 源代码。早期贝尔实验室的贡献者 Mike Veach 和 Pat Sullivan,他们分别开发了 emacs 和vi 风格的行编辑模式。

    KornShell 与 Bourne Shell 向后兼容,并受贝尔实验室用户的要求和启发,包括 C Shell 的许多功能。 最引人瞩目的特性就是支持脚本编程。

    现在的 zsh/bash 也有这两种行编辑模式:默认是 emacs 风格

    Ctrl-b 左移光标
    Ctrl-f 右移光标
    Ctrl-p 查看上一条命令(或上移光标)
    Ctrl-n 查看下一条命令(或下移光标)
    Ctrl-a 移动光标至行首
    Ctrl-e 移动光标至行尾
    Ctrl-w 删除前一个词
    Ctrl-u 删除从光标至行首的内容
    Ctrl-k 删除从光标至行尾的内容
    Ctrl-y 粘贴已删除的文本(例如粘贴 Ctrl-u 所删除的内容)
    Alt-right, Alt-f, Ctrl-right 向右移动一个词
    Alt-lift, Alt-b, Ctrl-lift 向左移动一个词

    支持关联数组(Associative Array)。

        declare -A filetypes=([txt]=text [sh]=shell [mk]=makefile)
        filetypes[c]="c source file"
    
        # 获取关联数组的长度
        echo ${#filetypes[*]}
    
        # 输出键为 txt 的值
        echo ${filetypes[txt]}
    
  7. POSIX shell

    POSIX shell 是基于 1988 年版本的 KornShell(ksh88),而 KornShell 又是为了取代 AT&T Unix 上的 Bourne shell,在功能上超过了 BSD 的 C shell。

    就 ksh 是 POSIX shell 的祖先而言,大多数 Unix 和类似 Unix 的系统今天都包括 Korn shell 的一些变体。

  8. Ash

    Almquist shell (也称为 A shell、ash 和 sh) 是一种轻量级 Unix shell(最大的特点就是轻量化)。 最初由 Kenneth Almquist 在20 世纪 80 年代末编写。它在 BSD 版本的 Unix 取代了 Bourne shell

    BSD 世界之外有两个重要的分支:

    dash (Debian Almquist shell) 在 2006 年被 Debian 和 Ubuntu 作为默认的 /bin/sh 实现。(Bash 仍然是 Debian 衍生产品中的默认交互式命令 shell。)

    BusyBox 中的 ash 命令,在嵌入式 Linux 中经常使用,可用于实现 /bin/sh。由于它是在 dash 之前发布的,并且它是从 Debian 的旧 ash 软件包衍生而来的,所以我选择将其视为 dash 的派生形式,而不是 ash,尽管它在 BusyBox 中是命令名称也是 ash

    (BusyBox 还包括一个叫 hush 的 ash 替代品,功能较弱。通常,在给定的 BusyBox 二进制文件中将仅内置这两者之一:默认情况下为 ash,但当空间非常狭窄时则采用 hush。因此,基于 BusyBox 的 /bin/sh 系统并不总是像 dash 一样。)

    嵌入式 Linux 系统,或以 Linux 为基础的路由系统:OpenWRT 之类的都 BusyBox ash

  9. Rc shell

    rc(Run Commond)是 Bell Labs 操作系统的 Unix V10 和 Plan 9 的命令行解释器。 它类似于 Bourne shell,但是其语法稍微简单一些。 由 Tom Duff 开发,这个并不是 POSIX shell 兼容

  10. Bash

    Bash(Bourne Again Shell)是 Brian Fox 为 GNU 项目编写的 Unix Shell 和命令语言,是 Bourne Shell 的免费软件替代品。 它于 1989 年首次发布,被用作大多数 Linux 发行版的默认登录 shell。

    Bash 也是个双关词。也有 “Born Again(重生)shell” 的意思

    Korn Shell 原本是专有软件,直到 2000 年,它才(遵照通用公共许可协议)作为开源软件发布。

    BSD Unix 避开了它,而选择了 C shell,而且在 Linux 起步时,它的源代码也不能免费提供给 Linux 使用。 因此,当早期的 Linux 发行版去寻找一个 shell 来搭配他们的 Linux 内核时,他们通常会选择 GNU Bash。

    自 2002 年发布 Mac OS X 10.2 Jaguar 以来,Mac OS X 也换成了 Bash。

        # 捕获 INT 信号来实现安全的退出
        # trap ctrl-c and call ctrl_c()
    
        trap ctrl_c INT
        function ctrl_c() {
          echo "** Trapped CTRL-C"
          exit
        }
    
  11. Zsh

    Paul Falstad 于 1990 年在普林斯顿大学读书时编写了 Zsh 的第一版。 zsh 的名称源自耶鲁大学教授 Zhong Shao(邵中,没错是华人,当时是普林斯顿大学的助教)的名字 Paul Falstad 认为 Shao 的登录 ID“ zsh” 是 shell 的好名字。

    还有一种网上流传的说法是:z 是字母表的最后一个字母。zsh 的意思就是最后的 shell。或者从现在来看,zsh 大概是POSIX shell 里最后一代的 shell 了

    MacOS X 15 Catalina 将默认 shell 程序更改为 Zsh。在 2009 年 2 月,Bash 4.0 将其许可证切换到 GPLv3。 一些用户怀疑这种许可变更是 MacOS 继续使用旧版本的原因随着 2019 年 MacOS Catalina 的发布,苹果改变了默认 shell

    Zsh 的模拟模式(emulation mode)emulate sh

    Zsh 是与 Bash 兼容的。这种说法既对,也不对,由于 Zsh 自己做为一种脚本语言,是与 Bash 不兼容的。 符合 Bash 规范的脚本没法保证被 Zsh 解释器正确执行。因此,Zsh 实现中包含了一个模拟模式(emulation mode),支持对两种主流的Bourne 衍生版 shell(bash、ksh)和 C shell 的模拟 (csh 的支持并不完整)。

    在 Bash 的 emulation mode 下,可使用与 Bash 相同的语法和命令集合,从而达到近乎彻底兼容的目的。使用对 Bash 的模拟,需要显式执行:emulate bash

2 Definitions
#

‘blank’
A space or tab character.
‘metacharacter’
A character that, when unquoted, separates words. A metacharacter is a 'space', 'tab', 'newline', or one of the following characters: '|', '&', ';', '(', ')', '<', or '>'.
‘word’
A sequence of characters treated as a unit by the shell. Words may not include unquoted ‘metacharacters’.
‘control operator’
A ’token’ that performs a control function. It is a 'newline' or one of the following: '||', '&&', '&', ';', ';;', ';&', ';;&', '|', '|&', '(', or ')'.
‘operator’
A 'control operator' or a 'redirection operator'. *Note Redirections::, for a list of redirection operators. Operators contain at least one unquoted ‘metacharacter’.
’token'
A sequence of characters considered a single unit by the shell. It is either a ‘word’ or an ‘operator’.
‘field’
A unit of text that is the result of one of the shell expansions. After expansion, when executing a command, the resulting fields are used as the command name and arguments.

3 Shell Operation
#

The following is a brief description of the shell’s operation when it reads and executes a command. Basically, the shell does the following:

  1. Reads its input from a file (*note Shell Scripts::), from a string supplied as an argument to the '-c' invocation option (*note Invoking Bash::), or from the user’s terminal.

  2. Breaks the input into words and operators, obeying the quoting rules described in *note Quoting::. These tokens are separated by ‘metacharacters’. Alias expansion is performed by this step (*note Aliases::).

  3. Parses the tokens into simple and compound commands (*note Shell Commands::).

  4. Performs the various shell expansions (*note Shell Expansions::), breaking the expanded tokens into lists of filenames (*note Filename Expansion::) and commands and arguments.

  5. Performs any necessary redirections (*note Redirections::) and removes the redirection operators and their operands from the argument list. 进程不感知重定向操作符。

  6. Executes the command.

  7. Optionally waits for the command to complete and collects its exit status.

4 Quoting
#

  • Escape Character
  • Single Quotes
  • Double Quotes
  • ANSI-C Quoting

转义字符 \ 用来消除下一个字符的特殊含义,然后自身从结果中删除。用在没有使用双引号包围的 meta char 前,消除这些 meta char 的特殊含义:

‘metacharacter’
A character that, when unquoted, separates words.

A metacharacter is a 'space', 'tab', 'newline', or one of the following characters: '|', '&', ';', '(', ')', '<', or '>'.

$ echo \\ \* \. \? \& \|
\ * . ? & |

# m 和 a 不是特殊字符, \ 转义没有效果,但也从结果中删除。
$ echo \msd \asd
msd asd
$

# \ 必须紧接着换行,这时换行会被删除(不是替换为一个空格)
$ echo abc\
> def
abcdef

相邻紧挨着的字符串会被连接为一个字符串, 可用于实现长字符串的换行分割:

$ echo afb cdf 'dfd''dfd'
afb cdf dfddfd

# \ 必须紧接着换行,中间不能有空格
$ echo abf cdf 'asdf'\
> 'dfd'
abf cdf asdfdfd

# \换行 只是起着下一行连接作用,前后的空格会被整合为一个
$ echo abc  \
>    asdfa\
>   asdf  \
>     asdf
abc asdfa asdf asdf
$ echo abc \
> "    asdfa"\
>   asdf \
>     "asfa  "
abc     asdfa asdf asfa

# 有空格的字符串需要转义
$ echo abc\ bcd |{ read var; echo "#$var#"; }
#abc bcd#

$ var="asdaf""afsdafs"
$ echo $var
asdafafsdafs

$ var="addf"\
> "afasdf"
$ echo $var
addfafasdf

# 字符串间不能有空格
$ var=asdfa\
>   asdfas
bash: asdfas: 未找到命令

$ var=asdfa\
> "asdf"
$ echo $var
asdfaasdf

$ var="asdaf"\ "afsdafs"
$ echo $var
asdaf afsdafs

$ var=asdf\ asdfasdfa
$ echo $var
asdf asdfasdfa

bash 字符串可以是单引号或双引号,支持多行,shell 最终在给命令传参时会删除未转义的单引号或双引号:

$ echo 'asdf
> asdfa
> asdfas
> '
asdf
asdfa
asdfas

$ echo "asdfa
> asdfa
> "
asdfa
asdfa

shell 换行前的 \ 对单引号无效, 对于双引号有效,而且 \ 后面必须紧跟换行(中间有空格则无效),用于将较长的字符串分割为多行。

$ echo "asdfa\
asdfasdf\
asdfasdf\
asdfasdf"
asdfaasdfasdfasdfasdfasdfasdf

$ echo a \
> b
A b

$ echo 'asdfasd\
> asdfasd\
> asdfas\
> asdfa'
asdfasd\
asdfasd\
asdfas\
asdfa
$

# echo 支持 -e 选项,这时会将字符串中的转义字符按照 printf 的语义来解释。
$ echo -e "asdfasdf \ asdfasdf\nasdfasdf"
asdfasdf \ asdfasdf
asdfasdf
$

# \ 后是空格+换行, shell 不会连行
$ echo "asdfasdfa \
> asdfasd
> asdfasdf"
asdfasdfa \
asdfasd
asdfasdf

# \ 后必须紧跟换行, shell 才会连行
$ echo "asdfasdf \
> asfasdfa
> asdfadsfasdf"
asdfasdf asfasdfa
asdfadsfasdf
$

单引号: 消除其中任何特殊含义的字符,同时单引号中的 \ 不具有特殊含义,原样输出;

# 直接转义单引号
$ echo fa\'
fa'

# 单引号中 \ 是普通字符,不具有转义功能
$ echo 'asda
> asdfas \
> sdfa'
asda
asdfas \
sdfa

$ echo 'asdfa\basdfa'
asdfa\basdfa

# 由于单引号中 \ 不具有特殊含义,所以第一个 \' 会原样打印 \,同时与第一个 ' 匹配,
# 表示单引号字符串结束。由于第三个 ' 没有结束匹配单引号,所以提示继续输入。
$ echo 'asdfa\b\'asdfa'
> cd'
asdfa\b\asdfa
cd

# 第一个单引号字符串到 \'f 前结束,然后 a\' 表示对 ' 转义。
$ echo 'asd\a\'fa\'asdfa'asda'
asd\a\fa'asdfaasda

双引号:

  1. 除了 $ ` \ 和 ! 外,其它字符 都是字面量含义 , 也就是 \ 只有位于 $ ` " \ newline 前 才具有转义的含义 ,从结果字符串中删除,否则就是普通字符原样输出。
  2. 字符串中的 !也具有特殊含义,用于命令历史记录,可以用 \ 转义,但是 \ 并不会从结果中删除。
# 多行字符串,\newline 会删除\和换行符, 实现字符串连接
$ echo "asdfa
> aaa\
> bbb\n" # \n 不具有特殊含义,原样输出
asdfa
aaabbb\n

# 转义空格, 结果为一个 field.
$ set -- space\ space2
$ echo $#
1

# 双引号中,非 $ ` " \ newline 字符前,\ 原样输出。
$ echo "space\ space2"
SPACE\ space2

# 双引号中,!word 表示命令历史记录
$ echo "aszdafsd !. abc"
echo "aszdafsd ./target/debug/apsarabot  abc"
aszdafsd ./target/debug/apsarabot  abc
$ echo "asdfa !asdf"
bash: !asdf": event not found
$ echo "asdfas !_sdfa"
bash: !_sdfa: event not found
$ echo "asdfa !.abc"
bash: !.abc: event not found
$ echo "adfa !{sdf}"
bash: !{sdf}: event not found

# 如果 !后是空格,则不具有命令历史 event 的含义,原样输出
$ echo 'asdfa ! asdf'
asdfa ! asdf

# \ 转义 !, 结果中 \ 不被删除(特殊情况)。
$ echo "asfa \!asdf"
asfa \!asdf

echo 的 -e 选项支持转义字符串(单双引号均可),类似的还有 printf 命令的格式化字符串:

zj@a:~/docs$ echo -e 'ab\tcd\nd'
ab      cd
d
zj@a:~/docs$ echo -e "ab\tcd\nd"
ab      cd
d
zj@a:~$ echo -e 'asdfa\
>  asdfa\t\nasdf'
asdfa\
 asdfa
asdf

# 字符串被 shell 处理后再传给 echo
zj@a:~$ var=vvvv
zj@a:~$ echo -e "${var} \!.abc"
vvvv \!.abc

# 双引号中原样输出
$ echo  "abcd\t\r\nd"
abcd\t\r\nd
# 按照转义字符来解释
$ echo -e  "abcd\t\r\nd"
abcd
d
# 非有效的转义字符被原样输出
$ echo -e  "abcd\t\r\nd\x"
abcd
d\x

bash 转义字符串,只能是 单引号前加 $ 的格式,可以用于 转义单引号自身

  • 可以使用的转义字符包括:\a:响铃, \b:退格,\n:换行,\r:回车,\t:制表符,其它都不需要转义含义,原样输出。
$ echo $'asdfa\tasdfa\nasda'
asdfa   asdfa
asda

# 只能使用单引号。
$ echo $"asdfa\tasdfa\nasda"
asdfa\tasdfa\nasda

# $var 变量并不会被替换
$ var=var
$ echo $'$var\tasda'
$var    asda

# 单引号中的 \ 是普通字符,不具有转义含义
$ echo 'asdfa\'asdfa'asda'
asdfa\asdfaasda

# 普通单引号中不能使用 \' 来转义单引号本身。
$ echo 'asdfa\'asdfa'
> bash: unexpected EOF while looking for matching `''
bash: syntax error: unexpected end of file

# 但是转义字符串中的 \ 具有特殊含义,可以用来转义单引号
$ echo $'asda\'asda'
asda'asda

对于各种 shell 扩展的结果,如变量扩展,命令替换,使用引号可以:

  1. 避免结果被分词,如字符串中换行被保留,结果作为一个整体;
  2. 避免结果被文件名扩展;
$ cat yy
echo $#
for i in "$@"; do
  echo $i
done

$ ./yy $date
0

$ ./yy $(date)
6
10
17
10:48:15
CST
2022

$ ./yy "$(date)"
1
10 17 10:48:21 CST 2022
$

# 变量替换后再进行 filename 扩展。使用双引号可以避免文件名扩展。
$ a=$(echo  "/*")
$ echo "$a"
/*

$ echo $a
/Applications /Library /System /Users /Volumes /bin /cores /dev /etc /home /opt /private /sbin /tmp /usr /var
$

$ msg=$(printf "a: %s\nb: %s" "abc" "def")
# 未被分词,字符串中的换行还保留
$ echo -e "${msg}"
a: abc
b: def
# 被分词后,换行被转换为空格
$ echo -e ${msg}
a: abc b: def

变量赋值的 VALUE 不会被 word splitting, 所以不需要加双引号:

$ aa=$(echo zhang jun)
$ echo $aa
zhang jun

# 变量赋值的 VALUE 也不会被 filename expansion, 所以也不需要加双引号:
$ bb=/*
$ echo "$bb"
/*
# 变量替换后,由于没有双引号包围,所以会进行 filename expansion 和 word splitting
$ echo $bb
/Applications /Library /System /Users /Volumes /bin /cores /dev /etc /home /opt /private /sbin /tmp /usr /var
$

当使用命令替换时 $(COMMAND) 中的 COMMAND 内容 都不会特殊对待, 它位于一个新的 quote context, 不受外围引号的影响, 也不需要转义 , calling shell 也不会替换/执行中的任何内容, 而是 原样的 传给 dep sub shell 来执行,例如可以直接写:

foo "$(bar "$(baz "$(ban "bla")")")"
$ v1=value1
# 在 dup subshell 中执行  echo a" bc "d $v1 命令。
# $(xx) 中 xx 的双引号不需要转义,会被无差别的传给 sub shell 来解释和执行。
$ res="$(echo a" bc "d $v1)"  # subshell 中可以使用 v1 变量
$ echo $res
a bc d value1

# 在 dup subshell 中执行 echo a" bc "d "$v1"
$ res="$(echo a" bc "d "$v1")"
$ echo $res
a bc d value1

# 在 dup subshell 中执行 echo a" bc "d '$v1'
# 这也说明了对于 $(xx) 中的 xx 的处理和解释是完全在 sub shell 中进行的。
$ res="$(echo a" bc "d '$v1')"
$ echo $res
a bc d $v1

# $() 中的内容被原样传给 dup sub shell
$ echo "$(echo \"ab"c")"
"abc
$ pwd
/var
$ echo "$(realpath $(dirname "./spool"))"
/var

$ read name tag <<< $(echo "zhang4 jun4")
$ echo $name
zhang4
$ echo $tag
jun4

# shell 不对 here string 的内容进行分词, 所以外围的双引号是可选的。
$ read name tag <<< "$(echo "zhang5 jun5")"
$ echo $name
zhang5
$ echo $tag
jun5

对于 (xxx)$(xxx) , shell 会创建一个 dup sub shell 来执行 xxx 命令,所以即使没有被 export 的变量和函数,也可以在 xxx 内访问。而 {xxx} 是在当前 shell 中执行 xxx 命令。

5 Simple Commands
#

最常见的命令类型,是 blank 字符( space 或 tab) 分割的 words, 直到遇到 control operator: A ’token’ that performs a control function. It is a ’newline’ or one of the following:

  • ‘||’
  • ‘&&’
  • ‘&’
  • ‘;’
  • ‘;;’
  • ‘;&’
  • ‘;;&’
  • ‘|’
  • ‘|&’
  • ‘(’
  • ‘)’

重定向操作符(<, >, >|, <<, >>) 不属于 control operator, 属于 simple command 的一部分:

$ echo adfasdf & >/dev/null # >/dev/null 重定向的是第二个空命令
[32528] 1
adfasdf
[1]+  Done                    echo adfasdf

# 重定向只是针对最后一个 echo 命令
$ echo abc; echo bcd; echo cde >out
abc
bcd
$

# 对命令列表整体做重定向。
$ { echo abc; echo bcd; echo cde; } > out

shell 在执行命令前执行重定向操作,然后将它从命令行参数删除,应用不感知,重定向操作可以位于 simple command 的任意位置,甚至单独使用:

# 重定向操作符位于 simple command 范围内即可,位置任意。
$ echo ab ; > /dev/null echo cd
ab
$

# 输入重定向被 shell 执行后删除,应用不感知;
$ bash -c 'echo "$#" "$@"' a b c d </dev/null 3</dev/null e f
5 b c d e f
$

$ >/dev/null  # 单独使用

control operator 用于分割 simple command, 如果连用会报错:

# 错误
$ ;;
bash: syntax error near unexpected token `;;'
$ ; ;
bash: syntax error near unexpected token `;'
$ echo ;;
bash: syntax error near unexpected token `;;'
$ echo ; | echo
bash: syntax error near unexpected token `|'
$ echo ; &
bash: syntax error near unexpected token `&'
$ echo fff  &| cat -
bash: syntax error near unexpected token `|'

# 正确
$ echo ; echo;

simple command 的 return status(exit status) 分 3 种:

  1. shell 不具有执行命令的权限权限(126) 或未找到命令 (127) 等出错;
  2. 命令自身的退出码;
  3. 被 signal 中止时退出码是 128+N(N 为 signal 编号, 从 0 开始);

为了区分自身退出和信号退出,命令自身的退出码应该位于 0-125。

6 Pipeline
#

|, |& 属于 control operator, 不能在 | 中间混用其它 control operator。

$ echo abc | echo bcd ; | echo def & | echo fff
bash: syntax error near unexpected token `|'
$

shell 优先对 pipeline 命令的 stdout 重定向 ,然后再进行其它重定向,所以可以通过中间 fd 来实现 stdout 和 stderr 的交换:

  • 3>&1: 由于 shell 优先将 cmd1 的 stdout 重定向到 pipeline, 所以 &1 指向 pipeline;
  • |& 同时重定向 stdout 和 stderr, 等效为 2>&1 |
$ echo abcd |& cat
# 等效于, 因为 shell 优先对 pipeline 重定向,所以 2>&1 中的 &1 是 pipeline
$ echo abcd 2>&1 | cat

$ cmd1 3>&1 1>&2 2>&3- | echo -

重定向是 simple command 的一部分,所以在 pipeline 尾部添加的重定向运算符是对 最后一个命令 而言的(中间命令的标准输出被重定向到下一个命令):

  • 可以使用 (…) 或 {…} 或 func 来打包一组命令,这时可以对它们整体进行重定向。
$ echo test test >test
$ echo abc </dev/null | cat <&3 | echo bcd >/dev/null | echo cde |cat - 3<./test
bash: 3: Bad file descriptor
cde
$ echo abc </dev/null | cat 3<./test  <&3 | echo bcd >/dev/null | echo cde |cat -
cde
$

shell 使用 sub shell 来执行 pipeline 中各子命令,而且是一次性启动所有子命令 ,当 所有命令都执行结束后 , pipeline 才返回,返回码为 pipeline 定义的最右侧进程的退出码(不管中间命令是否执行成功):

$ date; echo abc </dev/null | mybad | exit 4 | sleep 5 | echo bcd >/dev/null | echo cde | exit 6; echo $?; date
Sat 20 Aug 2022 05:04:52 PM CST
mybad: command not found
6
Sat 20 Aug 2022 05:04:57 PM CST
$

# 在子命令中 cd 的目录及设置的环境变量,calling shell 不感知:
$ echo a | (cd /tmp; pwd) |cat - && pwd
/tmp
/root

设置 set -o pipefail 后, pipeline 返回 最右侧非正常结束命令 的退出码:

  1. shell 还是会执行 pipeline 中 所有命令, 并等待所有命令执行结束后才返回;
  2. 形式上最右侧最后一个执行失败的命令退出码,非最后执行返回的命令;
# 默认,pipeline 形式上最后一个命令的退出码作为 pipeline 的退出码
$ echo a | grep ff | cat -
$ echo $?
0

# pipeline 最右边执行失败的命令的退出码为 pipeline 的退出码
$ set -o pipefail
$ echo a | grep ff | cat -
$ echo $?
1

$ set -o pipefail
$ echo aa | cat - | echo bb
bb
# echo 不读取 pipeline, 所以通过 pipeline 向 echo 写入时返回 SIGPIPE 信号,命令因信号
# 的退出码为 141(128+13)
$ echo $?
141

# 虽然有三个命令都异常退出,但是 exit 6 位于最右侧,所以返回它的退出码
$ set -o pipefail
$ echo abc </dev/null | mybad| exit 4 | sleep 5 | echo bcd >/dev/null | echo cde |exit 6
mybad: command not found
$ echo $?
6

$ echo abc </dev/null | mybad | exit 4 | sleep 5 | echo bcd >/dev/null | echo cde |exit 0
mybad: command not found
echo $?$ echo $? # SIGPIPE  信号中止
141

$ echo abc </dev/null | exit 4 | sleep 5 | echo bcd >/dev/null | echo cde |exit 0
echo $?$ echo $?
4

在 job control 未激活的情况下(一般是非交互式的 shell 脚本),通过设置 shopt -s lastpipe, 在当前shell 环境 (而非子 shell) 中执行 pipeline 最后一条命令:

# 虽然开启了 lastpipe, 但是当前 shell 是激活了 job control 的交互式 shell, 所以
# lastpipe 不生效
$ var=1
$ shopt -s lastpipe
$ date | cat  | ( echo $var; var=2; )
1
$ echo $var
1

# 在脚本或 bash -c 等非交互式 shell 中,不激活 job control

# 未开启时  var=2 在自己的 sub shell 中设置
$ bash -c 'var=1; date | cat  | var=2; echo $var'
1

# 开启了 lastpipe 后 var=2 在当前 shell 环境中执行
$ bash -c 'var=1; shopt -s lastpipe; date | cat | var=2; echo $var'
2

bash 变量 PIPESTATUS 记录了最近一次前台 pipeline 命令执行退出码的 数组

$ asdfa | asdfa | asdfa
bash: asdfa: command not found
bash: asdfa: command not found
bash: asdfa: command not found
$ echo ${PIPESTATUS[@]}
127 127 127

$ date | cat - |wc -c
28
$ echo ${PIPESTATUS[@]}
0 0 0

pipeline 前可以加 time 或 !, 分别用于统计 pipline 命令 整体的 执行时间和对返回的状态码取反(正常和异常):

$ time sleep 1

real    0m1.039s
user    0m0.001s
sys     0m0.003s

$ echo | exit 2
$ echo $?
2
$ ! echo | exit 2  # pipeline 退出码取反,结果为 0 或 1;
$ echo $?
0

7 List Command
#

用 4 种分隔符 && || ; & 分割的 simple/pipeline commands:

  1. pipeline 的优先级比这些分隔符高,所以 & 是对整个 pipeline 命令放到后台执行;
  2. && || 优先级比 ; & 高,用来连接 simple/pipeline commands;
  3. && 的优先级比 || 高,所以可以在 a 或 b 失败时,执行 c: a && b || c
  4. ; 和 & 和 newline 用来中止(分割) List Commands;

所以,对于 List Command 命令的分析逻辑是:

  1. 先找中止符号 ; 或 & 或 newline, 它们将 list 分割为各个部分;
  2. 再分析各部分,其中 && 或 || 用于分割 | 串起来的 pipeline;

date; echo aa | cat - | echo bb && echo cc |echo dd & echo ee | echo ff | exit 3:

  1. date
  2. echo aa | cat - | echo bb && echo cc | echo dd &
    1. 整体在后台执行(因 & 优先级比 | 和 && 低);
    2. 两个 pipeline: echo aa | cat - |echo bb 和 echo cc | echo dd
    3. 前一个执行结束并成功后, 后一个 piepeline 才执行;
  3. echo ee | echo ff | exit 3

List Command 的退出码为 形式上最右侧 命令的退出码;

# 在 pipeline 命令最后添加 & 时,整个 pipeline 放到后台运行
$ echo abc </dev/null | sleep 100 | echo bcd >/dev/null | echo cde |cat - &
[1] 34438
cde

$ jobs
[1]+  Running   echo abc < /dev/null | sleep 100 | echo bcd > /dev/null | echo cde | cat - &
$

# set +o 用于关闭 bash 特性,而 set -o 用于开启 bash 特性。

# 关闭 pipefail 特性(默认行为)
$ set +o pipefail
$ date; echo aa | cat - | echo bb && echo cc |echo dd & echo ee | echo ff | exit 3
Sat 20 Aug 2022 05:45:06 PM CST
[1] 928905
bb
dd
[1]+  Done                    echo aa | cat - | echo bb && echo cc | echo dd
$ echo $? # 返回形式上最右侧命令的退出码
3
$

$ set -o pipefail
$ date; echo aa | cat - | echo bb && echo cc |echo dd & echo ee | echo ff
Sat 20 Aug 2022 05:19:36 PM CST
[1] 928720
ff
bb
$
[1]+  Exit 141                echo aa | cat - | echo bb && echo cc | echo dd
$

对于 & 异步执行的命令,shell 不等待命令执行完成,直接返回退出码 0。

对于 ; 分割的 list command, 退出码为最后一个命令的退出码:

$ f1() { return 1; }
$ f2() { return 2; }
$ f3() { return 3; }
$ f1; f2; asdasd; f3
bash: asdasd: command not found
$ echo $?
3
$ f1; f2; asdasd; f3; echo ok
bash: asdasd: command not found
ok
$ echo $?
0
$

#  & 和 ; 都是用于分割命令, 最后一个 done 前使用 & 表示异步执行每个命令.
$ for i in one two three; do command "$i" & done

当使用 & 异步执行命令:

  1. 如果 job control 未生效(如执行脚本的 shell),异步命令的 stdin 被重定向到 /dev/null:
  2. 如果 job control 生效,则前台进程组的异步命令如果读取 stdin 则会被暂停. 如果启动后台任务交互式 shell 退出,则后台暂停的 job 进程将收到 SIGHUP 信号而退出(孤儿进程组)。
# job control 生效的情况下,异步命令读取 stdin 时会被 STOP
$ cat &
[1] 13426
[1]+  Stopped                 cat

$ echo aa && vim & echo bb
[1] 47605
bb
aa
[1]+  Stopped                 echo aa && vim

$ jobs
[1]+  Stopped                 echo aa && vim
$

# bash -c 的 cmd 是在 job control 未生效的 非交互式 shell 执行的
$ bash -c 'cat <(echo -n a) - <(echo -n b)&'
ab$

对于 && 和 || 分割的 list command, 退出码为 最后一个实际执行的命令 而不一定是 && 或 || 右侧的命令(因为它可能实际未执行)。

# && 的优先级比 || 高,所以 [ -n "$fasda" ] && echo "not zero" 整体为 false 时,后面
# 的 || 才会执行。

$ [ -n "$fasda" ] && echo "not zero" || echo "zero"
zero
$ [ -z "$fasda" ] && [ -z "$asdfasd" ] || echo "zero"
$

list command 是在当前 shell 环境执行命令的,而 pipeline 中的各部分是在 sub shell 中执行的。

8 Compound Commands
#

包含 3 种类型:

  • Looping Constructs
  • Conditional Constructs
  • Command Grouping

Compound Commands 是 shell 特殊的构件块,以特殊的 reserved word 或 control operator开头和结尾。对 Compound Commands 的重定向是适用于其中 所有命令的

[], [[]], (()) 都是 command, 所以可以用于 if/while 条件命令或者 && || 表达式。

9 Looping Constructs
#

包括 4 种类型:

  1. until TEST-COMMANDS; do CONSEQUENT-COMMANDS; done
  2. while TEST-COMMANDS; do CONSEQUENT-COMMANDS; done
  3. for NAME [ [in [WORDS …] ] ; ] do COMMANDS; done
  4. for (( EXPR1 ; EXPR2 ; EXPR3 )) ; do COMMANDS ; done

上面结构中的 ; 可以用换行代替。

Looping 结构的退出码都为执行的最后一条 CONSEQUENT-COMMANDS 命令, 如果一次 loop 都没有执行, 则返回 0。

对于 for-in loop,WORDS 是 shell expansions 后的列表,每次迭代 NAME 都会被设置为其中的一个 WORD。如果忽略 in WORDS, 则相当于 in "$@" ,即传递给脚本或 shell 的所有参数。

shell 不会 对引号中的字符串进行 brace expansion/tilde expansion/filename expansion 3 种扩展类型,也不会对字符串分词(所以字符串中换行和连续空白都会被保留)。

$ set -- a b "c d" e
$ for w; do echo $w;done
a
b
c d
e
$

# 支持文件名扩展
$ for f in ./*.org; do echo $f; done
./inbox.org
$
# 支持 brace 扩展
$ for w in {1..5}; do echo $w; done
1
2
3
4
5

# for (()) 算术表达式(算术表达式可以用逗号分割,变量引用可以不加 $ 前缀)
$ for ((i=1,j=1; i+j < 10; i++, j++)) do echo $((i+j));done
2
4
6
8
$

$ for c in $(seq 1 0.2 2); do echo $c;done
1.0
1.2
1.4
1.6
1.8
2.0

# for 和 {1..5} 大括号扩展使用
$ for i in {1..5}; do echo $i;done
$ for i in {1..5..2}; do echo $i;done
$ for i in $(seq 1 5); do echo $i;done

10 Conditional Constructs
#

包括 5 种类型:

  1. if;
  2. case;
  3. select … in …;
  4. (( EXPRESSION )) : 是命令结构,等效为 let "EXPRESSION"
  5. [[ EXPRESSION]] : 也是命令结构,是 test/[] 的超集;

(()) 和 [[]] 都是数学表达式上下文, 可以:

  1. 引用变量时, 变量名前可以不加 $;
  2. 支持变量赋值, 自增自减, 关系表达式, 逻辑表达式, 表达式列表(逗号分割);
  3. 表达式结果为 0 时, 命令返回 1, 否则命令返回 0 (成功);

(()) 或 [[]] 或 test 或 [] 中都是条件表达式(见后文 Condition Expressions)而非命令,支持逻辑运算。

$((EXPRSSION)) 是算术替换而非命令,等效为 expr EXPRESSION

10.1 if
#

语法:

  1. elif 和 else 后面不需要有分号;
  2. then 前面如果有换行,则前面的分号可省略;

if TEST-COMMANDS; then
  CONSEQUENT-COMMANDS;
[elif MORE-TEST-COMMANDS; then
  MORE-CONSEQUENTS;]
[else ALTERNATE-CONSEQUENTS;]
fi

if 的简洁等效模式:

# && 的优先级比 || 高
[ -z "$asdf" ] && echo 'zero' || echo 'not zero'
# 等效为:
if [ -z "$asdf" ]; then
   echo 'zero'
else
   echo 'not zero'
fi

[ -z "$asdf" ] || echo "not zero"
# 等效为
if [ ! -z "$asdf" ]; then # 或者  if ! [ -z "$asdf" ]; then
  echo 'not zero'
fi

# [ -z "$asdf" ] && echo 'zero' || echo 'not zero'

10.2 case
#

case WORD in
    # pattern 是 glob 语法(不被引号引住, 不会被 glob 扩展) , 如果是字符串则是字符串严格匹配。
    [ [(] PATTERN [| PATTERN]...) COMMAND-LIST ;;]...
esac # esac 是 case 的反写

case WORD 支持如下扩展:

  1. tilde expansion (不能使用字符串引住)。
  2. parameter expansion
  3. command substitution
  4. arithmetic expansion
  5. quote removal

WORD 不支持 brace expansion, 分词和 filename 扩展(如果进行了 filename 扩展, 则子句就不能用 glob PATTERN 来匹配了).

各 case 子句以如下操作符结束:

;;
执行完本 case 子句后不再匹配和执行其它子句;
;&
执行完本 case 子句后,再执行紧接着的下一个子句(不判它的条件是否匹配);
;;&
执行完本 case 子句后,再判断和执行下一个子句;

示例: 检查当前 set -o 的设置:

$ echo $-
himBH

$ case "$-" in
>   *i*) echo interactive ;;
>   *) echo non-interactive ;;  # *) 一般放到最后作为缺省匹配
> esac
interactive
$

LEN=72
CHAR='-'
while (( $# > 0 ))
do
    case $1 in
    [0-9]*) LEN=$1;;
    -c) shift
        CHAR=$1;;
    *) usagexit;;
    esac
    shift
done

10.3 select
#

select 用来构造一个用户选择的菜单:

select NAME [in WORDS ...]; do COMMANDS; done

10.4 (())
#

(()) 是专用于 算术运算复合命令 ,等效为 let "ARITHMETIC EXPRESSION"

  • (()) 是复合命令,退出码取决于算术运算的结果是否是 0:0 时返回 1, 其它返回 0;
  • 内部是条件表达式(而非命令),位于算术运算上下文中,使用变量时 不需要加前缀 $;
  • 所有变量都是 数字语义 (如比较运算符是数字值比较) , 如果是 null 变量, 值为 0;

支持:算术运算表达式、逗号表达式,赋值表达式,逻辑表达式。

# 是复合命令,而非算术替换表达式, 所以需要在使用命令而非变量的地方使用。
$ echo ((3+3,3+4))
bash: syntax error near unexpected token '('

# 多个算术运算可以使用 逗号 分割
$ ((3+3,3+4))
# 逗号后面可以有空格,如
$ (( 3+3, 3+4 ))
$ var=-1
# 其它分隔符号都是非法的
$ ((var+=1; var))
bash: ((: var+=1; var: syntax error: invalid arithmetic operator (error token is "; var")
$ ((var+=1,var))
$ echo $?
1

$ var=-1
# 变量不需要加 $ 前缀
# 变量赋值时,赋值后变量值为 0 时返回 1,  否则返回 0
$ ((var+=1))
$ echo $?
1
$ echo $var
0
$ ((var+=4))
$ echo $var
4
$ a=3
$ ((var+=a*3))
$ echo $var
13

# 算术运算结果为 0 时退出码为 1, 否则退出码为 0 时
$ (( 0 ))
$ echo $?
1
$ (( 1 ))
$ echo $?
0

# 算术运算的结果先参考 a 的值,由于为 0, 所以退出码为 1;
$ a=0
$ ((a++))
$ echo $?
1
$ echo $a
1

# 先进行 ++ 计算,然后参考计算结果来计算退出码,由于为 1, 所以退出码为 0;
$ a=0
$ ((++a))
$ echo $?
0
$ echo $a
1
$

# 逻辑运算为真时退出码为 0, 为假时退出码为 1
$ (( 4 > 3))
$ echo $?
0
$ (( 3 > 4))
$ echo $?
1
$ (( 2 > 4 || 4 > 3))
$ echo $?
0
$

$(()) 返回算术运算的结果, 它是算术替换扩展类型而非命令:

  • 对于逻辑运算符,为真时返回 1, 为假时返回 0;
  • 和 (()) 类似, 也都是在 arithmetic context 执行表达式;
$ echo $((0))
0
$ echo $((1))
1
$ echo $((4>3))
1
$ echo $((4<3))
0
$ a=-1
$ echo $((a+=2))
1
$ a=-1
$ echo $((a++))
-1
$

10.5 [[]]
#

[[ EXPRESSION ]]test/[] 的超集,参数为表达式而非命令,支持多种字符串匹配表达式(严格字符串值,glob 或 regexp pattern) 、算术运算表达式、逻辑运算表达式,逗号运算表达式

# 内容是表达式,而非命令
$ [[ echo ]]
bash: syntax error near unexpected token `echo'
$ [[ echo; ]]
bash: unexpected token `;', conditional binary operator expected

$ var=3
# 内容前后必须有空格
$ [[var]]
[[var]]: command not found
$ [[ var]]
bash: unexpected token `newline', conditional binary operator expected
$ [[ var ]]

# 支持算术运算,逗号运算(逗号前后不能有空格),逻辑运算表达式
$ [[ var+=3 ]]
$ [[ var,var++ ]]
$ [[ var,++var ]]
$ [[ var, var ]]
bash: syntax error near unexpected token `var,'
$ [[ 0 && 3 || 4 ]]
$

# 支持各种字符串匹配表达式:严格匹配,glob 匹配,正则匹配
$ [[ "asfas" = "asfsa" ]]; echo $?
1
$ [[ "asfas" = asfa ]]; echo $?
1
$ [[ "asfas" = asfa* ]]; echo $?
0
$ [[ "asfas" =~ asfa.* ]]; echo $?
0

# 支持 test/[] 的条件表达式,并且支持 && || !逻辑运算表达式和 () 分组
$ [[ "asfas" =~ asfa.* && -f /etc/mtab ]]; echo $?
0
$ [[ "asfas" =~ asfa.* && -f /etc/mtab ]]; echo $?
0
$ [[ "asfas" =~ asfa.* && -f /etc/mtab && ! -f /etc/mtab ]]; echo $?
1
$ [[ "asfas" =~ asfa.* && (-f /etc/mtab && ! -f /etc/mtab) ]]; echo $?
1
$ [[ "asfas" =~ asfa.* && (-f /etc/mtab || ! -f /etc/mtab) ]]; echo $?
0

[[ EXPRESSION ]​] 中的 EXPRESSION 是条件表达式而非命令,支持如下扩展:

  1. tilde expansion;
  2. parameter and variable expansion;
  3. arithmetic expansion
  4. command substitution
  5. process substitution
  6. and quote removal are performed

对 EXPRESSION 不进行分词和文件名扩展 (如果文件名扩展,则就不支持不带引号时的 pattern 匹配语义了。)。

[[]]= 或 == 或 != 都用于 glob pattern 匹配:

  1. 如果右侧是 quoting 的字符串, 则进行字符操匹配;
  2. 否则右侧是非字符串格式的 glob, 使用 glob pattern 匹配;
  3. 执行 shopt -s nocasematch 后,glob pattern 和 regexp pattern 匹配都是不区分大小写。
# EXPRESSION 不会进行分词和文件名扩展,所以 z* 不会被模糊替换。右侧是非字符串时,表示
# glob pattern
$ if [[ "zhang" = z* ]]; then echo yes ; else echo no;fi
yes
$

$ if [[ "zhang" = "z*" ]]; then echo yes ; else echo no;fi
no
$

# 支持各种字符串匹配表达式:严格匹配,glob 匹配,正则匹配
$ [[ "asfas" = "asfsa" ]]; echo $?
1
$ [[ "asfas" == asfa ]]; echo $?
1
$ [[ "asfas" == asfa* ]]; echo $?
0
$ [[ "asfas" =~ asfa.* ]]; echo $?
0

[[]]=~ 支持扩展类型的正则表达式匹配,匹配结果保存到数组 BASH_REMATCH 中。

  • 如果运算符右边是 quoting 的字符串,则 没有 正则表达式语义,使用字符串部分匹配;(Bash-3.2 开始有这个语义)
  • 如果不是字符串,则表示扩展正则表达式;
  • 没有 !~ 运算符, 但是可以对表达式分组和结果取反来达到这个目的: ! [[xxx]];

It was changed between 3.1 and 3.2. Guess the advanced guide needs an update.

  • CentOS7 的 Bash 版本是 4.2.46; Ubuntu 18.04 的是 4.4 版本;

This is a terse description of the new features added to bash-3.2 since the release of bash-3.1. As always, the manual page (doc/bash.1) is the place to look for complete descriptions.

New Features in Bash

f. Quoting the string argument to the [[ command’s ~ operator now =forces string matching, as with the other pattern-matching operators.

对于 glob pattern 是 完整匹配 , 但对于正则则是 任意部分匹配 ,可以通过 ^ 或 $ 来定位匹配的位置。

$ [[ "asdfas" = a ]] && echo yes || echo no
no
$ [[ "asdfas" = a* ]] && echo yes || echo no
yes
$ [[ "asdfas" =~ a ]] && echo yes || echo no
yes

[[ $line =~ [[:space:]]*(a)?b ]]

pattern='[[:space:]]*(a)?b'
[[ $line =~ $pattern ]]

pattern='\.'
[[ . =~ $pattern ]]
[[ . =~ \. ]]

# 以下两种都是字符串匹配
[[ . =~ "$pattern" ]]
[[ . =~ '\.' ]]

[[]] 中的 EXPRESSION 支持结果取反和逻辑表达式:

‘( EXPRESSION )’
Returns the value of EXPRESSION. This may be used to override the normal precedence of operators.
‘! EXPRESSION’
True if EXPRESSION is false.
‘EXPRESSION1 && EXPRESSION2’
True if both EXPRESSION1 and EXPRESSION2 are true.
‘EXPRESSION1 || EXPRESSION2’
True if either EXPRESSION1 or EXPRESSION2 is true.

[ xx ] 不支持逻辑表达式(&& 或 ||), 例如 [ -z "$ff" || -z "$bar" ] 是不对的, 但是可以使用 -a 或 -o 来表示 AND 或OR 关系:

$ [ -z "$asdfa" -o -z "$baad" ] && echo or or

test 或 [ xxx ] 如 [ var > 13 ] 则是字符串语义比较, 对于数字需要使用 -gt/-lt/-ge/-le 等, 如 [ var -gt 13 ];

10.6 Conditional Expressions
#

条件表达式是在 [[]] 或 test 或 [] 中使用的表达式(而非命令)。

对于 file 的检查, 默认 follow symbol link。

文件判断:

‘-a FILE’ 或 ‘-e FILE’
True if FILE exists.
‘-s FILE’
True if FILE exists and has a size greater than zero.
‘-b FILE’
True if FILE exists and is a block special file.
‘-c FILE’
True if FILE exists and is a character special file.
‘-d FILE’
True if FILE exists and is a directory.
‘-f FILE’
True if FILE exists and is a regular file.
‘-g FILE’
True if FILE exists and its set-group-id bit is set.
‘-h FILE’
True if FILE exists and is a symbolic link.
‘-k FILE’
True if FILE exists and its “sticky” bit is set.
‘-p FILE’
True if FILE exists and is a named pipe (FIFO).
‘-t FD’
True if file descriptor FD is open and refers to a terminal.
‘-u FILE’
True if FILE exists and its set-user-id bit is set.
‘-G FILE’
True if FILE exists and is owned by the effective group id.
‘-L FILE’
True if FILE exists and is a symbolic link.
‘-N FILE’
True if FILE exists and has been modified since it was last read.
‘-O FILE’
True if FILE exists and is owned by the effective user id.
‘-S FILE’
True if FILE exists and is a socket.

文件权限判断:

‘-r FILE’
True if FILE exists and is readable.
‘-w FILE’
True if FILE exists and is writable.
‘-x FILE’
True if FILE exists and is executable.
‘-o OPTNAME’
True if the shell option OPTNAME is enabled. The list of options appears in the description of the '-o' option to the 'set' builtin (*note The Set Builtin::).

文件对比判断:

‘FILE1 -ef FILE2’
True if FILE1 and FILE2 refer to the same device and inode numbers.
‘FILE1 -nt FILE2’
True if FILE1 is newer (according to modification date) than FILE2, or if FILE1 exists and FILE2 does not.
‘FILE1 -ot FILE2’
True if FILE1 is older than FILE2, or if FILE2 exists and FILE1 does not.

变量判断:

‘-v VARNAME’
True if the shell variable VARNAME is set (has been assigned a value).
  • 变量必须被定义且被赋值过(不管是否是空字符串),即不是 null string;
‘-R VARNAME’
True if the shell variable VARNAME is set and is a name reference.

字符串判断:

‘-z STRING’
True if the length of STRING is zero. 变量未定义或者定义了但是空串。
‘-n STRING’ 或 'STRING'
True if the length of STRING is non-zero.

null string:只定义变量但不赋值,例如 local v 或者 declare v, 但 declare v= 或者 declare v="" 则在定义变量 v 的同时赋值为空字符串, 所以不再是 null string.

$ declare v
# -v 检查变量是否定义且不为 null string
$ if [ -v v ]; then echo ok; else echo bad;fi
bad

# 定义变量并设置为空字符串, 等效为 declare v=""
$ declare v=
$ if [ -v v ]; then echo ok; else echo bad;fi # 变量定义且不为 null string
ok

$ unset a
$ declare a # 只声明 a,未赋值,a 为 null string
$ [ -v a ] && echo ok || echo bad
bad
$ declare a b c   # a/b/c 是 null string,取值时等效于空串
$ echo "#${a}:${b}:${c}#"
#::#
$ unset a
$ declare a= # 声明 a 的同时赋值为空串,a 不为 null string,而是 a == ""
$ [ -v a ] && echo ok || echo bad
ok
$ unset a
$ a= # 声明 a 的同时赋值为空串,a 不为 null string,而是 a == ""
$ [ -v a ] && echo ok || echo bad
ok
$ unset a
$ a=""
$ [ -v a ] && echo ok || echo bad
ok

if [ $var ] 当变量 var 存在且不为空字符串时有效:

$ unset v2
$ if [ $v2 ]; then echo ok; else echo bad;fi
bad
$ v2=""
$ if [ $v2 ]; then echo ok; else echo bad;fi
bad
$ v2=" "
$ if [ $v2 ]; then echo ok; else echo bad;fi
bad
$ v2=0
$ if [ $v2 ]; then echo ok; else echo bad;fi
ok

字符串比较:

  1. ‘STRING1 == STRING2’

  2. ‘STRING1 = STRING2’

    True if the strings are equal. When used with the ‘[[’ command, this performs pattern matching as described above (*note Conditional Constructs::).

    ‘=’ should be used with the ’test’ command for POSIX conformance.

  3. ‘STRING1 != STRING2’ :: True if the strings are not equal.

  4. ‘STRING1 < STRING2’ :: True if STRING1 sorts before STRING2 lexicographically.

  5. ‘STRING1 > STRING2’ :: True if STRING1 sorts after STRING2 lexicographically.

数值比较: ‘ARG1 OP ARG2’

  • ‘OP’ is one of ‘-eq’, ‘-ne’, ‘-lt’, ‘-le’, ‘-gt’, or ‘-ge’. These arithmetic binary operators return true if

  • ARG1 is equal to, not equal to, less than, less than or equal to, greater than, or greater than or equal to

  • ARG2, respectively. ARG1 and ARG2 may be positive or negative integers.

    When used with the ‘[[’ command, ARG1 and ARG2 are evaluated as arithmetic expressions (*note Shell Arithmetic::).

$ unset ff
$ ff=  # 定义为空串
$ test -v ff; echo $? # -v 检查变量是否定义且非 null string
0
$ [ "" ]; echo $?
1

$ [ "asd" ] # 非空串
$ echo $?
0

$ [ "$asd" ] # 空串
$ echo $?
1

11 Grouping Commands
#

将 a list of commands 作为一个 unit 来执行:

( LIST )
在 sub shell 中执行 LIST, LIST 与小括号间 可以没有空格 ;
{ LIST; }
当前 shell 中(见后文例外情况)执行 LIST, 最后一个命令后 必须以分号或换行或 & 结尾 ,而且 LIST 与大括号之间 必须有空格;

() 和 {} 的主要功能是将 command 组合为一个 unit, 这样可以对整体执行重定向和异步命令(因为重定向是针对 simple command,异步是针对 pipline command 的)或者作为 && || 中的一个命令,另外也可以作为 function body 来满足它的 compound command 类型的要求。

注意: 在 () 中 $$ 返回的是 invoking shell 的 PID, 而非 () 所在的 sub shell 的 PID。

# 重定向针对 simple command,所以只会对第二个 cat 输出重定向
$ cat inbox.org; cat inbox.org >/tmp/log

# 对两个 cat 都重定向到一个文件
$ (cat inbox.org; cat inbox.org ) >/tmp/log

# 错误用法
$ {date; } # { 和 date 间没有空格
$ { date } # } 前少了 ;
# 正确
$ { date; }
Sat 27 Aug 2022 09:58:00 PM CST

# && 优先级比 || 高
$ [ -z "$UNSET" ] && { echo unset; exit 1; } || { echo setted; exit 0; }

# mkdir scratch areas and check for failure
# N.B. must have whitespace around the { and } and
#      must have the trailing ; in the {} lists
mkdir "$PRIV1" || { echo "to Unable mkdir '$PRIV1'" ; exit 4; }
mkdir "$PRIV2" || { echo "Unable to mkdir '$PRIV2'" ; exit 5; }

pipeline 中命令或后台命令,都会在 sub shell 中执行,{xx} 在这两个场景下也是这样:

# 在当前 shell 环境执行变量赋值
$ { gv2=2; vg=11; }
$ echo gv2 $vg
gv2 11
$ echo $gv2 $vg
2 11
$

# 位于 pipeline 中时,在一个 sub shell 环境中执行
$ gv=1
$ echo ok1 | { gv2=2; vg=11; } | echo ok2
ok2
$ echo $gv2

$ echo $vg

# 在后台执行时,也是在一个 sub shell 环境中执行
root@devops-109:~# { sleep 1000 && read input && echo $input; } &
[1] 1149940
root@devops-109:~# ps -lfH
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root     1149930 1149721  0  80   0 -  3491 do_wai 10:23 pts/1    00:00:00 -bash
1 S root     1149940 1149930  0  80   0 -  3491 do_wai 10:23 pts/1    00:00:00   -bash
0 S root     1149941 1149940  0  80   0 -  2787 hrtime 10:23 pts/1    00:00:00     sleep 1000
4 R root     1149942 1149930  0  80   0 -  3640 -      10:23 pts/1    00:00:00   ps -lfH

12 coprocess
#

coprocess 是创建两个 Pipe(分别为命令的 stdin 和 stdout)后,异步执行命令的机制,同时,当前 shell 还可以获得命令的输入和输出。

语法: coproc [NAME] COMMAND [REDIRECTIONS]

  1. 如果 COMMAND 是 simple command,则不能指定 NAME,bash 自动使用 simple command 的 command 作为NAME 值;
  2. 如果 COMMAND 是 compound command,则可选的指定 NAME, 如果未指定则为 COPROC;

在执行 COMMAND 期间,bash 在 executing shell 上下文中(不是执行子命令的 sub shell)创建了名为 NAME 的数组,shell 在执行任何重定向之前创建这个数组:

  1. NAME[0]: COMMAND 的 stdout 连接的 Pipe FD, 可读;
  2. NAME[1]: COMMAND 的 stdin 连接的 Pipe FD,可写;

如果是复合命令, shell 创建一个 sub shell 来执行命令。可以通过自动创建的变量 NAME_PID 来获得 simple command 或 sub shell 的 PID, 然后使用内置 wait 命令来等待协程终止。

# 一个复合命令,自定义 NAME 为 tr。
coproc tr { tr a b; }

# 向 coproc 命令写入内容,注意使用的重定向语法。
echo aaa >&"${tr[1]}"

# 关闭 tr[1] FD 的写入, 表示写入完毕。
exec {tr[1]}>&-

# 读取 coproc 命令的输出。
cat <&"${tr[0]}"

对于需要读取输入的协程,当输入完毕后,可以使用 shell 标准语法来关闭文件描述符:

  • 关闭文件描述符时 N>&- 和 N<&- 中间是 不能有任何空格的
  • 文件描述符复制时 N>&M 或 N<&M 中间也是不能有任何空格的。
  • N< WORD 或 N> WORD, WORD 和 文件描述符之间是可以有空格的。
# 错误: > 和 & 间有空格;
root@devops-109:~# echo hello 2 > &1
-bash: syntax error near unexpected token '&'

# 错误: 2 和 > 中间有空格, 故不属于 FD
root@devops-109:~# echo hello 2 >&1
hello 2

# 正确
root@devops-109:~# echo hello 2>&1
hello

# 在一个 sub shell 中执行 compound commands, 返回的 PID 为 sub shell 进程 PID。
root@devops-109:~# coproc myproc3 { echo hello3; cat -; }
[1] 1145641
root@devops-109:~# ps -lfH
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root     1145617 1144545  0  80   0 -  3524 do_wai 21:53 pts/0    00:00:00 -bash
1 S root     1145641 1145617  0  80   0 -  3491 do_wai 21:55 pts/0    00:00:00   -bash
0 S root     1145642 1145641  0  80   0 -  2823 pipe_w 21:55 pts/0    00:00:00     cat -
4 R root     1145646 1145617  0  80   0 -  3640 -      21:55 pts/0    00:00:00   ps -lfH

# calling shell 可以通过 NAME_PID 来获得 sub shell PID;
root@devops-109:~# echo ${myproc3_PID}
1145641

# 写入部分内容,stdin 并未关闭
root@devops-109:~# echo hello cat >&${myproc3[1]}
root@devops-109:~# cat <&${myproc3[0]}
hello3
hello cat
^C

# 关闭文件描述符

# 错误: 这里的语义是 exec 60 > &- , 所以 exec 执行 60 命令出错。
root@devops-109:~# exec ${myproc3[1]}>&-
-bash: exec: 60: not found

# 正确:应该使用 exec {VARNAME}>&- 或 exec {VARNAME}<&- 来关闭变量 VARNAME 对应的 FD
# 的写入或读取。

root@devops-109:~# exec {myproc3[1]}>&-
root@devops-109:~#
[1]+  Done                    coproc myproc3 { echo hello3; cat -; }

13 Functions
#

函数数在当前 shell context 中执行(类似的还有复合命令 { cmds; }):

  • 函数内的 $$ 为执行该函数的 bash pid,如脚本 shell pid;
  • 函数内的 $0 为 bash 名称或脚本路径;
  • 函数可以使用外部的未 export 的变量或函数;
FNAME () COMPOUND-COMMAND [ REDIRECTIONS ]
function FNAME [()] COMPOUND-COMMAND [ REDIRECTIONS ].
  1. 使用 function 关键字后双括号是可选的.
  2. 函数体必需是 COMPOUND COMMAND 类型:
    • until/for/while/if/case/select/(())/[[]] 都是 COMPOUND-COMMAND;
    • 或者使用通用的 group command: ()/{};
  3. 在函数体内使用的变量引用、命令替换等,如 $1, 会延迟到函数执行时才被扩展(而不是一般意义上的命令行执行时替换和扩展);
  4. 实际执行函数时 才会执行重定向操作,适用于整个函数体;
  5. 函数内变量是 dynamic scope,可以定义函数内部变量(local xx)或使用函数外变量;
$ function myfunc() date # 必须是 COMPOUND-COMMAND
-bash: syntax error near unexpected token date

$ function myfunc() { date } # 大括号: 1. 内部前后有空格; 2. 右括号前有分号;
> -bash: syntax error: unexpected end of file
$ function myfunc() { date; }

$ function mytest() if [[ $1 == "zhang" ]]; then echo zhang; else echo who?;fi
$ mytest jun
who?
$ function mycal() ((k+=3))
$ mycal
$ echo $k
3

# subtract the (given) file size from the (global) free space
function REDUCE ()
(( FREE-=${1:-0}))

# 执行函数时才会实际重定向
$ mycat () { cat; cat iptables; } <extfrag.log  >output
$ mycat
$ wc -l output
35 output

执行 function 时 $0 不变 ,一直为执行函数的脚本路径或或 shell 名称, 其它 shell 位置参数临时设置为传给该 function 的参数,当函数执行完毕后,位置参数恢复为外层 shell 或脚本的位置参数。数组变量 FUNCNAME 的第一个元素为当前函数的名称:

# for p; do xx; done 等效为: for p in "$@";do xx; done
$ function mytest() { echo "$0"; echo "$#"; echo "${FUNCNAME[0]}"; for p; do echo $p;done; }
$ mytest 1 2 3 4
/bin/bash
4
mytest
1
2
3
4
$

function usagexit ()
{
    echo "usage: ${0##*/} file1 file2"
    echo "where both files must be .odt files"
    exit $1
} >&2

#cat spool/x.sh
echo ok
echo $(dirname $0)
echo $0

#bash ./spool/x.sh
ok
./spool
./spool/x.sh

#realpath ./spool/x.sh
/var/spool/x.sh

内置命令 return N 可以给函数返回值 N ,如果没有指定返回值 N, 则返回 return 上一条命令的返回值 。如果函数内定义了 RETRUN trap, 则在函数返回前先执行该 trap。

$ return 3  # return 只能在 function 或 script 中使用
bash: return: can only return from a function or sourced script

$ function mytest3() { return 3; }
$ mytest3; echo $?
3

$ function mytest3() { return; }   # return 不带返回值时, 返回上一条命令的返回值
$ mytest3; echo $?
0

$ function mytest3() { d; return; }
$ mytest3; echo $?
bash: d: command not found
127

$ function mytest() { return $(echo OK); }  # return 命令的参数必需是数字
$ mytest; echo $?
bash: return: OK: numeric argument required
255
$

$ function mytest() { d; return $((1+2)); }
$ mytest; echo $?
bash: d: command not found
3
$

函数内不能给位置变量赋值:

$ function mytest() { $1="zhangjun"; echo $1;  }
$ mytest
bash: =zhangjun: command not found

# 如果 $1 未定义或为空, 则返回 zhangjun 且对 $1 赋值。
$ function mytest() { local name="${1:=zhangjun}"; printf "%s\n" name; }
$ mytest
bash: $1: cannot assign in this way

# 如果 $1 未定义或为空, 则返回 zhangjun, 但不对 $1 赋值。
$ function mytest() { echo "${1:-zhangjun}"; }
$ mytest
zhangjun

# 如果 $1 未定义或为空, 则报错, 打印 zhangjun,不再执行后面的语句
$ function mytest() { local name="${1:?zhangjun}"; printf "%s\n" $name; }
$ mytest
bash: 1: zhangjun

# 使用 set 来重新设置函数的位置参数。
$ function mytest() { set -- a b c ; echo "${1:-zhangjun}"; }
$ mytest
a

函数内变量是 动态作用域 ,可以使用 local 命令来定义函数内部变量(local 只能在函数体内使用),否则变量赋值的变量会添加到 全局作用域 中:

$ local a
bash: local: 只能在函数中使用

$ function  mytest () { var=4; }
$ echo $var
$
$ mytest
$ echo $var   # 全局变量 var
4

$ unset var
$ function mytest2 () { local var=1; mytest; }
$ mytest2
$ echo $var  # 全局没有该变量

$

$ var=global
$ function func1 () {
>   local var='func1 local'  # 遮盖外面同名的变量
>   func2
> }
$ function func2 () {
>   echo "in func2, var = $var"
> }
$ func1
in func2, var = func1 local
$

# 可以同时声明多个函数内的 local 变量,未赋值时这些变量为 null string。
fpmul()
{
    # 这些变量是 null string(没有值)
    local places tot qm neg n int dec df

    places=  # 等效于 places="",将变量设置为空值
    tot=1
    qm=
    neg=
    #...
}

# [ -v var ] 检查变量 var 存在且赋值过(非 null string 即可,不管是否是空串)
$ function mytest () { local var; [ -v var ] && echo ok || echo bad; }
$ mytest
bad
# local var=; 将 var 设置为空字符串,var 变量被赋值过
$ function mytest () { local var=; [ -v var ] && echo ok || echo bad; }
$ mytest
ok

declare -f 显示函数定义, declare -F 只显示函数名称, declare -p 显示所有 exported 的变量和函数定义:

$ declare -f | grep mycat # 显示所有函数定义
mycat ()

$ declare -f mycat # 只显示 mycat 函数定义
mycat ()
{
    cat;
    cat iptables
} < extfrag.log > output

$ declare -F  |grep mycat
declare -f mycat
$

unset -f 删除函数定义, unset -v 删除变量定义。unset 也是 dynamic scope, 它优先 unset 当前作用域 中的 var/func 定义;

export -f 导出函数定义 到 subshell(必须加 -f 否则无效)。

变量和函数定义必须 export 后,才能在后续的 subshell 环境中使用,例如后续其它脚本中执行该函数。但是 命令替换 $(xx),小括号分组命令 (xxx), 大括号分组命令 {xxx}, 异步命令 &, pipeline 中的 builtin 命令 ,虽然也是在 sub shell 环境中执行命令,但除了 trap 外,是当前 shell 环境的复制,所以 未导出的 var和 func 也可以使用

  • 除了被 ignored 的 trap 会被子 shell 环境继承, 其它 trap 会被重置为默认值;
$ function add() { return $(($1+$2)); }
$ add 1 2
$ echo $?
3
$ echo a | add 1 2  # add 未导出,但是可以在 pipeline sub shell 环境中执行
$ echo $?
3
$
$ echo a | (add 1 2)
$ echo $?
3
$ echo a | { add 1 2 ; }
$ echo $?
3

$ bash -c 'add 1 2; echo $?'
bash: add: command not found
127
$ export -f add
$ bash -c 'add 1 2; echo $?'
3

shift 将位置参数左移, $# 和 $1, $2 等值是移动后的结果, 但是 $0 一直为执行脚本文件名(如果是交互式 shell 则为 bash 或 -bash):

$ function myfunc () {
>   printf '$#: %d, $0: %s, funcname: %s\n' "$#" "$0" "${FUNCNAME[0]}"
>   for a; do
>       echo $a
>   done
>   shift 2  # 参数左移两位
>   printf '$#: %d, $0: %s, funcname: %s\n' "$#" "$0" "${FUNCNAME[0]}" # $0 不变,但是 $# 减少两位
> }
$ myfunc  1 2 3 "a b" 4
$#: 5, $0: -bash, funcname: myfunc
1
2
3
a b
4
$#: 3, $0: -bash, funcname: myfunc
$

function 的执行环境和 calling shell 一致,差别在于:

  1. 位置参数被临时设置为调用 function 命令时传入参数;
  2. DEBUG 和 RETURN trap 不被继承(除非设置了 set -o functrace), 所以执行函数内语句前不执行 debug trap, 返回时不执行 return trap;
  3. ERR trap 也不被继承(除非设置了 set -o errtrace), 所以函数内某条语句执行失败时不执行 ERR trap;

set -e 可以捕获 直接执行的函数 body 或 body 中"纯粹"的命令替换 中的错误情况。如果是非“纯粹”的命令替换,例如被用做其它为命令的 参数或环境变量 或者 在命令替换中执行函数, 则 set -e 不会捕获命令替换中的错误,典型的场景是在函数内定义一个内部变量 local v=$(xx)

$ bash
$ PS1=">"
>simple() {
>   badcommand
>   echo bad
> }
>set -e
>simple  # 函数作为 simple command 直接执行时,set -e 可以捕获 body 中的错误。
bash: badcommand:未找到命令
$

root@devops-109:~# bash
root@devops-109:~# PS1='>'
>function mytest() { local ts; ts=$(myccc); echo $?; } # 纯粹的命令替换
>set -e
>mytest
myccc: command not found
root@devops-109:~#

# 非纯粹命令替换情况:local 内置命令定义一个变量,执行函数时并不退出
$ bash
$ PS1='>'
>function mytest() { local ts=$(myccc); echo $?; }
>set -e
>mytest
bash: myccc: command not found
0
>

# 如果先用 local 定义变量,然后再做变量赋值,则执行函数时退出
$ bash
$ PS1='>'
>function mytest() { local ts; ts=$(myccc); echo $?; }
>set -e
>mytest
bash: myccc: command not found
$

# 但是通过命令替换执行函数时,不退出。
$ bash
$ PS1='>'
>function mytest() { local ts; ts=$(myccc); echo $?; }
>set -e
>cc=$(mytest)
bash: myccc: command not found
>echo $cc  # 函数输出的内容
127
>

在命令替换中执行函数的场景,函数体属于 compound command, 返回码为最后一个执行的命令, 所以中间的命令失败并不代表函数执行失败。

set -e 也不能捕获 if/until/while 后的判断条件出错(因为非 0 退出是判断条件之一), 所以对于作为 if/until/while 条件的函数,在内部应该检查命令成功与否,出错时就立即返回,否则函数的退出码取决于最后一个命令的退出码。

$ bash
$ PS1=">"
>simple () { badcommand; echo bad; }
>set -e
>if simple; then echo ok; else echo bad; fi  # sub shell 并没有退出
bash: badcommand:未找到命令
bad
ok

总结: 正是有如此多的奇怪特性, 实际 shell 编程时不建议依赖 set -e 来检测错误, 而是确定的自定义检查。

14 Trap
#

trap 用于设置 shell 对于信号的处理方式。

语法:

trap [-lp] [ARG] [SIGSPEC ...]
ARG
shell 收到 SIGSPEC 时读取和执行的命令;
ARG 缺失或为 -
reset SIGNAL 为 shell 启动时的状态, 一般为系统默认处理方式;
ARG 为空串 ""
忽略 SIGNAL (shell 和它启动的命令都将忽略该信号)
ARG 缺失且指定了 -p
打印 指定了 trap 的 SIGNAL;
ARG 缺失且指定了 -l
打印 SIGNAL 列表,类似于 kill -l 命令的输出;

SIGNAL 可以为数字或字符串, 字符串忽略大小写(0 == exit == EXIT),且可以忽略 SIG 前缀。

还支持一些伪信号类型: ERR, EXIT, RETURN, DEBUG ,主要用于定义出错、返回或退出时的清理操作。

Signals ignored upon entry to the shell cannot be trapped or reset. Trapped signals that are not being ignored are reset to their original values in a subshell or subshell environment when one is created.

已经被 ignored 的信号也会被子 shell 或子进程忽略, 这也是 nohup 能起作用的原因

但是被 trap 的信号, 在创建子 shell 或子进程时会被 重置为初始值 ,所以每个 shell 脚本应该设置自己的 trap 处理函数。

对于几种伪信号,shell 可能不会等待命令或脚本执行结束就执行对应的 trap。例如: 对于 DEBUG, 每次执行脚本中的一条命令, 每一次 for/if loop 时都立即执行 trap;

一组进程 发送信号的方式:

  1. 向 job 编号(%d)发送信号: 如 kill %1
  2. 向负的进程组 ID (TGID) 发送信号: 如 kill -9 -123456

向 trap 命令传参数:

# 可以同时指定多个信号
trap 'catch $? $LINENO' ERR DEBUG

catch() {
  echo "Error $1 occurred on $2"
}

echo "Before bad command"
badcommand
echo "After bad command"

trap -p 显示全部或特定信号的 trap:

$ trap -p
trap -- 'echo trap: exit' EXIT  # -- 表示后续不是 trap 命令的选项
trap -- 'echo trap: error' ERR

$ trap -p EXIT
trap -- 'echo trap: exit' EXIT
$

如果 Bash 在执行 command 的过程中收到了它捕获的 trap 信号, 则该 trap 只有 当 command 执行完 成后才会被执行。如果 command 是脚本, 则 bash 需要 等待脚本执行退出后(而不是执行脚本的下一条命令), 才会执行 trap。

但是如果 bash 在执行内置命令 wait 时收到了信号,则 wait 命令将立即返回 ,并且返回值为 128,trap 也会被立即执行。

# 脚本 sleep 期间收到 HUP 信号时并不立即执行 trap, 而是需要等待脚本执行结束
root@devops-109:~# cat test.sh
trap 'echo sighup >/root/trap.sighup.$$;' SIGHUP
sleep 1000
root@devops-109:~# bash test.sh

root@devops-109:~# ps -elfH |grep sleep -C 3
4 S root     1094956     686  0  80   0 -  3451 poll_s 19:28 ?        00:00:00     sshd: root@pts/0,pts/2
4 S root     1095243 1094956  0  80   0 -  3557 do_wai 19:39 pts/0    00:00:00       -bash
0 S root     1095711 1095243  0  80   0 -  3141 do_wai 20:16 pts/0    00:00:00         bash test.sh
0 S root     1095712 1095711  0  80   0 -  2787 hrtime 20:16 pts/0    00:00:00           sleep 1000
# 向 shell 发送 HUP 信号时, shell 因为在等待 sleep 的返回, 所以并不会执行 trap
root@devops-109:~# kill -HUP 1095711
root@devops-109:~# ps -elfH |grep sleep -C 3
4 S root     1094956     686  0  80   0 -  3451 poll_s 19:28 ?        00:00:00     sshd: root@pts/0,pts/2
4 S root     1095243 1094956  0  80   0 -  3557 do_wai 19:39 pts/0    00:00:00       -bash
0 S root     1095711 1095243  0  80   0 -  3141 do_wai 20:16 pts/0    00:00:00         bash test.sh
0 S root     1095712 1095711  0  80   0 -  2787 hrtime 20:16 pts/0    00:00:00           sleep 1000
# 当向 test.sh 进程组发送 HUP 信号时, shell 向进程组所有进程发送 HUP 信号, 这时 sleep
# 被中止, shell 脚本执行结果, shell 执行 trap。
root@devops-109:~# kill -HUP -1095711 # PID 值为负值表示向进程组中所有进行发送信号
root@devops-109:~# bash test.sh
Hangup
root@devops-109:~#
root@devops-109:~# cat trap.sighup.1095711
sighup

# 即使将脚本中 sleep 修改为 while 循环, 向 shell 发送 HUP 信号时, 也不会立即执行trap,
# 需要等待 shell 脚本退出时才执行。
root@devops-109:~# cat test.sh
trap 'echo sighup >/root/trap.sighup.$$;' SIGHUP
sleep 20
while ((i++, i<5)); do
        sleep 1
done
root@devops-109:~# bash test.sh
root@devops-109:~#

root@devops-109:~# ps -elfH |grep test.sh
4 S root     1096646 1094956  0  80   0 -  3524 do_wai 20:31 pts/3    00:00:00       -bash
0 S root     1096889 1096646  0  80   0 -  3141 do_wai 20:35 pts/3    00:00:00         bash test.sh
0 S root     1096890 1096889  0  80   0 -  2787 hrtime 20:35 pts/3    00:00:00           sleep 20
root@devops-109:~# kill -HUP 1096889

# 等待一段时间 shell 脚本退出后, 执行了 trap 指令。
root@devops-109:~# cat trap.sighup.1096889
sighup

另一个常见的错误例子是 docker shell 脚本,如下所示,收到 trap 的信号时并不会立即执行 shut_down 函数, 而是等待 /opt/myqpp 执行完成后才会执行 trap 函数, 导致 trap 逻辑不能正常执行。

#!/bin/bash

echo starting up

function shut_down() {
    echo shutting down

    pid=$(ps -e | grep myapp | awk '{print $1}')
    kill -SIGTERM $pid
    exit
}

trap "shut_down" SIGKILL SIGTERM SIGHUP SIGINT EXIT

/opt/myapp

解法一:在 shell 脚本的最后,使用 exec 命令将自定义程序来取代 calling shell, 这样该程序将直接收到 docker 发送的 TERM 和 KILL 信号(而不是 calling shell 脚本收到这两个信号):

#!/bin/bash
echo starting up

exec /opt/myapp

解法二(推荐):将任务放到后台,然后使用 wait 等待返回, shell 在用 wait 命令等待异步执行的任务返回时,如果收到信号时 wait 命令会立即返回。示例如下

#!/bin/bash

_term() {
  echo "Caught SIGTERM signal!"
  kill -TERM "$child" 2>/dev/null
}

trap _term SIGTERM

echo "Doing some initial work...";
/bin/start/main/server --nodaemon &

# $! 或 ${!} 返回上次执行的后台任务 PID
child=$!
wait "$child"

14.1 EXIT
#

SIGSPEC 为 0 或 EXIT 时,当 shell (脚本)退出时执行 ARG,不管该脚本执行成功与否,常用于清理资源:

$ bash -c 'trap "echo trap:exit" exit;  date'
Thu 01 Sep 2022 05:07:09 PM CST
trap:exit

scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
function finish {
  rm -rf "$scratch"
}
trap finish EXIT

一般 set -e 和 trap exit 连用,出错时立即退出,同时执行清理工作:

#!/bin/bash
set -e
trap 'catch $? $LINENO' EXIT

catch() {
  echo "catching!"
  if [ "$1" != "0" ]; then
    echo "Error $1 occurred on $2"
  fi
}

simple() {
  badcommand
  echo "Hi from simple()!"
}

14.2 DEBUG
#

当 SIGSPEC 是 DEBUG 时,每次执行 simple command、for command、case command、select command、for 的循环变化、shell 函数的第一个命令前,都会执行 ARG。

Refer to the description of the 'extdebug' option to the 'shopt' builtin (*note The Shopt Builtin::) for details of its effect on the ‘DEBUG’ trap.

函数或脚本不会继承 DEBUG trap, 可以设置 set -o functrace 来设置函数内继承 calling shell 的 DEBUG trap;

$ trap 'echo trap:debug $LINENO' DEBUG
trap:debug

# 在执行命令前执行 DEBUG trap 命令
$ date
trap:debug 38
Thu 01 Sep 2022 05:18:40 PM CST

$ for a in 1 2 3 ;do echo $a;done
trap:debug 39  # 各 for 变量前也会执行 DEBUG trap
trap:debug 39  # 这个是执行 echo $a 命令前执行的 DEBUG trap
1
trap:debug 39
trap:debug 39
2
trap:debug 39
trap:debug 39
3

14.3 RETURN
#

通过 . 或 source 执行的 script 正常结束时, 才会执行 calling shell 的 RETURN trap;

$ trap 'echo trap:return $FUNCNAME' return

$ function mytest() { var=1; return 0; }
# 正常执行 shell 命令或函数返回时不会执行 return trap (因为函数默认不继承 calling
# shell 的 return/debug trap)
$ mytest

$ echo 'function mytest() { var=1; echo mytest; }; function mytest2() { echo mytest2; }; mytest; mytest2 ' >x.sh

# 非 source 或 . 执行脚本, 不会执行 return trap
root@devops-109:~# bash x.sh
mytest
mytest2

#  . 执行脚本返回时,执行 return trap
$ . x.sh
mytest
mytest2
trap:return

$ bash
$ cat x.sh
function mytest() { var=1; echo mytest; };
function mytest2() { echo mytest2; };
mytest;
exit 0;
mytest2
$ trap 'echo trap:return' return

# 如果脚本提前退出(exit 或者 set -o errexit 导致的退出),则不执行 return trap
$ . x.sh
mytest

function 内也可以定义 RETURN trap, 在函数 正常 返回前执行,可以用于修改变量值:

root@devops-109:~# function mytest() { trap 'echo "return"; ((var++));' RETURN; var=1; }
root@devops-109:~# mytest
return
root@devops-109:~# echo $var
2

# 如果函数异常返回, 则不会执行 RETURN trap;
root@devops-109:~# cat x.sh
function mytest() {
  trap 'echo "return"; ((var++));' RETURN
  var=1
  exit 1
}
mytest
echo  $var
root@devops-109:~# bash x.sh

14.4 set -o functrace
#

函数默认不会继承 calling shell 定义的 RETURN/DEBUG trap, 通过执行 set -o functrace 命令, 则函数内继承 calling shell 的 RETURN/DEBUG trap:

  1. 进入函数体时执行一次 debug trap, 执行函数体内每条命令前执行一次 debug trap;
  2. 函数返回前执行 return trap;
# 未定义任何 trap
$ trap -p

$ trap 'echo trap:debug' debug
trap:debug

$ function mytest() { echo 'mytest func1'; echo 'mytest func2'; }
# 对于 debug/return trap, 默认 func 内是不继承的, 但是在执行函数命令前执行一次
$ mytest
trap:debug

mytest func1
mytest func2

$ set -o functrace
trap:debug

$ mytest
trap:debug # 执行 mytest 命令前
trap:debug # 进入函数体前
trap:debug # 执行 echo 命令前
mytest func1
trap:debug # 执行 echo 命令前
mytest func2
trap:debug # 函数返回前
$

$ trap 'echo "trap:return"' return
$ f () { echo ok; }
$ echo $'f\nf' >y
$ . y # 执行脚本 y 退出时执行一次 return trap
ok
ok
trap:return
$ set -o functrace
$ . y  # 执行脚本 y 内的每个函数返回时都执行 return trap
ok
trap:return
ok
trap:return
trap:return  # 最终执行 y 脚本退出时, 也会执行 return trap
$

对于 shell 脚本内定义和执行的函数, calling shell 是执行脚本的 shell, 所以需要在 shell 脚本头部定义 RETURN/DEBUG trap, 同时脚本内部执行 set -o functrace 命令(echo $- 结果中包含 T):

root@devops-109:~# trap -p
trap:debug
trap -- 'echo trap:debug' DEBUG
trap -- 'echo "trap:return"' RETURN

root@devops-109:~# cat y
trap:debug
mytest
mytest

root@devops-109:~# bash y
trap:debug
mytest func1
mytest func2
mytest func1
mytest func2

root@devops-109:~# . y   # source/. 执行脚本时, 当脚本正常返回时执行 RETURN trap
trap:debug
mytest func1
mytest func2
mytest func1
mytest func2
trap:return

root@devops-109:~# set -o functrace  # 开启 functrace
trap:debug
root@devops-109:~# echo $-
trap:debug
himBHTs
root@devops-109:~# bash -c 'echo $-'  # 子 shell 并没有继承 functrace 特性(无 T)
trap:debug
hBc
root@devops-109:~# bash y  # calling shell 是 bash 子 shell, 没有开启 functrace 特性
trap:debug
mytest func1
mytest func2
mytest func1
mytest func2
root@devops-109:~# . y # calling shell 是当前 shell
trap:debug
trap:debug
trap:debug
trap:debug
mytest func1
trap:debug
mytest func2
trap:debug
trap:return
trap:debug
trap:debug
trap:debug
mytest func1
trap:debug
mytest func2
trap:debug
trap:return
trap:debug
trap:return
root@devops-109:~#

14.5 ERR
#

If a SIGSPEC is 'ERR', the command ARG is executed whenever a pipeline (which may consist of a single simple command), a list, or a compound command returns a non-zero exit status, subject to the following conditions.

函数体属于 compound command, 返回码为最后一个执行的命令, 所以中间的命令失败并不代表函数执行失败。

执行 ERR trap 时脚本并不退出执行。

The ‘ERR’ trap is not executed if the failed command is part of the command list immediately following an ‘until’ or ‘while’ keyword, part of the test following the ‘if’ or ’elif’ reserved words, part of a command executed in a ‘&&’ or ‘||’ list except the command following the final ‘&&’ or ‘||’, any command in a pipeline but the last, or if the command’s return status is being inverted using ‘!’. These are the same conditions obeyed by the 'errexit' ('-e') option.

trap 'catch' ERR  # catch 为捕获 ERR 时执行的命令, 这里的 catch 为函数命令

catch() {
  echo "An error has occurred but we're going to eat it!!"
}

echo "Before bad command"
badcommand  # 命令执行失败, 执行 ERR trap
echo "After bad command"

对于函数, 当执行完函数且异常返回时才会执行一次 err trap, 对于函数体内执行错误的地方不会执行 err trap (除非开启 set -o errtrace 或 set -E):

$ cat err.sh
trap 'echo "trap:exit"' exit
trap 'echo "trap:err"' err

function foo () {
  echo foo1-1
  ffff
  echo foo1-2
}

function foo2 () {
  echo foo2
  fff
}

echo before ffff
ffff

echo before foo
foo

echo before foo2
foo2

echo before ffff
ffff

echo before 'echo ok'
echo ok

$ bash err.sh
before ffff
err.sh: line 18: ffff: command not found # 脚本命令执行失败,执行 err trap
trap:err
before foo
foo1-1
err.sh: line 8: ffff: command not found  # 函数内命令执行失败,默认不执行 err trap,
					 # 除非先执行 set -o errtrace或 set -E 命令
foo1-2
before foo2
foo2                                     # 函数内最后一条命令执行失败时,函数返回后,
					 # 执行 err trap
err.sh: line 14: fff: command not found
trap:err
before ffff
err.sh: line 27: ffff: command not found # 脚本内的其它非函数命令执行失败时, 执行 err
					 # trap
trap:err
before echo ok
ok
trap:exit
$

14.6 set -e 和 set -E
#

set -o errexit 或 set -e 可以捕获直接执行的函数 body 中的错误情况, 当出错时脚本执行结束:

$ bash
$ PS1=">"
>simple() {
>   badcommand
>   echo bad
> }
>set -e
>simple
bash: badcommand:未找到命令
$

但是如果函数(其它命令也类似)作为 if/until/while 的判断条件或者 作为命令替换 , 则 set -e 不能捕获:

$ bash
$ PS1=">"
>simple () { badcommand; echo bad; }
>set -e
>if simple; then echo ok; else echo bad; fi  # sub shell 并没有退出
bash: badcommand:未找到命令
bad
ok
>myvar=$(simple)                  # sub shell 并没有退出
bash: badcommand:未找到命令
>

如果开启了 set -o errexit 或 set -e 则函数执行错误返回时, 不再执行 trap err 。这是由于 trap err 必须是能拿到 compound command 的非 0 返回码时才执行, 而开启 set -e 后, 函数体内执行出错的位置直接异常返回:

# 不开启 set -e 时, 函数执行出错返回时,执行 trap error
$ cat err2.sh
#set -e
trap 'echo "trap:err"' err

myfunc () {
  foo
}

myfunc
$ bash err2.sh
err2.sh: line 5: foo: command not found
trap:err
$

# 开启 set -e 后,函数执行出错返回时,不再执行 err trap
$ sed -i 's/^#//' err2.sh
$ bash err2.sh
err2.sh: line 5: foo: command not found

通过执行命令 set -o errtrace 或 set -E 可以解决上面两个问题:

  1. 函数体也继承 calling-shell 的 err trap; (不开启时, 函数返回时才执行一次 err trap)

    $ trap 'echo "trap: error"' ERR
    $ set -o errtrace
    $ function myerr () { d; echo 1; }
    $ myerr
    d: command not found
    trap: error
    trap: error
    1
    $
    
    $ cat err.sh
    set -o errtrace
    trap 'echo "trap:err"' err
    
    function foo () {
      echo foo1-1
      ffff
      echo foo1-2
    }
    
    function foo2 () {
      echo foo2
      fff
    }
    
    echo before foo
    foo
    
    echo before foo2
    foo2
    
    echo before ffff
    ffff
    
    echo before 'echo ok'
    echo ok
    
    $ bash err.sh
    before foo
    foo1-1
    err.sh: line 7: ffff: command not found # 捕获函数体内 err, 执行 err trap
    trap:err
    foo1-2
    before foo2
    foo2
    err.sh: line 13: fff: command not found
    trap:err  # 函数体内 err trap
    trap:err  # 函数返回 err trap
    before ffff
    err.sh: line 23: ffff: command not found
    trap:err
    before echo ok
    ok
    $
    
  2. 函数因 set -e 提前出错退出时也执行 err trap:

    $ cat err.sh
    set -o errexit  # 或 set -e
    set -o errtrace # 或 set -E
    trap 'echo "trap:exit"' exit
    trap 'echo "trap:err"' err
    
    function foo () {
      echo foo1-1
      ffff
      echo foo1-2
    }
    
    function foo2 () {
      echo foo2
      fff
    }
    
    echo before foo
    foo
    
    echo before foo2
    foo2
    
    echo before ffff
    ffff
    
    echo before 'echo ok'
    echo ok
    
    $ bash err.sh
    before foo
    foo1-1
    err.sh: line 8: ffff: command not found
    trap:err # set -E 的效果,如果不执行 set -E, 则执行函数体内命令失败时,不会执行 err trap
    trap:exit
    $
    

15 Shell Parameters
#

  • Positional Parameters
  • Special Parameters

A PARAMETER is an entity that stores values. It can be a ’name’, a number, or one of the special characters listed below. A VARIABLE is a parameter denoted by a ’name’. A variable has a VALUE and zero or more ATTRIBUTES. Attributes are assigned using the 'declare' builtin command (see the description of the ‘declare’ builtin in *note Bash Builtins::).

Shell 区分 PARAMETER 和 VARIABLE, PARAMETER 是 VARIABLE 的超集, 可以是 name, 数字和特殊字符,而 VARIABLE 只是 name 类型。

VARIABLE 可以包含 value 和一些 ATTRIBUTES, 用于改变 VARIABLE 的一些特性:

  • 使用 declare 管理变量和它的 ATTRIBUTES;
  • ${variable@a} 显示 VARIABLE 的 ATTRIBUTEs;
  • 只声明但没有赋值的变量是 null string(需要用 declare 或 local 来声明);
# a/b/c 是 null string,取值时等效于空串
$ declare a b c
$ echo "#${a}:${b}:${c}#"
#::#

$ local a
bash: local: 只能在函数中使用
$
$ function mytest () { local var; [ -v var ] && echo ok || echo bad; }
$ mytest
bad
$ function mytest () { local var=; [ -v var ] && echo ok || echo bad; }
$ mytest
ok

# a/b/c 是空串,而非 null string
$ a= b= c=
# 等效于:
$ declare a= b= c=
# 等效于:
$ declare a="" b="" c=""

函数内使用 local 的场景是解决 set -e 的问题:

set -e
# 即使 myccc 执行失败,local 命令还是执行成功,隐藏了错误。
>function mytest() { local ts=$(myccc); echo $?; }
>mytest
bash: myccc:未找到命令
0
>

# 先定义一个 local 变量,然后再赋值,这样如果 myccc 执行出错则赋值命令也返回出错。注
# 意 myccc 如果是函数,则看函数最后一条命令的返回码,而不是中间情况出错时就终止(因为
# 函数体是 compound commands,退出码为最后一条命令)。
>function mytest() { local ts=; ts=$(myccc); echo $?; }
>mytest
bash: myccc:未找到命令
127

变量赋值的 VALUE:

  • 如果没有被引号引住, 则支持各种扩展: tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal (detailed below)
  • 赋值时不会对 VALUE 进行 filename expansion 和 word splitting, 但是后续使用该变量非赋值的情况下, shell 可能会对变量替换后的值进行 filename expansion 和 word splitting;
# 变量赋值的 VALUE 不会被 word splitting, 所以不需要加双引号:
$ aa=$(echo zhang jun)
$ echo $aa
zhang jun

# 变量赋值会被 brace 扩展,但是只有当 ~ 位于等号 = 后或者 : 后才会被扩展。
$ aa=~/.bashrc,~/.bashrc
$ echo "$aa"
/home/zhangjun/.bashrc,~/.bashrc
$ aa=~/.bashrc:~/.bashrc
$ echo "$aa"
/home/zhangjun/.bashrc:/home/zhangjun/.bashrc

# 变量赋值的 VALUE 也不会被 filename expansion, 所以也不需要加双引号:
$ bb=/*
$ echo "$bb"
/*
# 变量替换后,由于没有双引号包围,所以会进行 filename expansion 和 word splitting
$ echo $bb
/Applications /Library /System /Users /Volumes /bin /cores /dev /etc /home /opt /private /sbin /tmp /usr /var
$

# $(cat ~/.bashrc) 的结果会被分词,文件中的换行会被转换为空格。
$ echo $(cat ~/.bashrc)
# 输出: ~/.bashrc: executed by bash(1) for non-login shells. # see

# 添加引号后,内容不再被分词。
$ echo "$(cat ~/.bashrc)" | head
# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples

# If not running interactively, don't do anything
case $- in

unset 删除变量定义, unset -f 删除函数定义。

test -v var 检查变量 var 定义存在且被赋值(不管是否为空串), if [ $var ] 当变量 var 存在且不为空字符串时有效:

$ [ -v aaa ]; echo $?  # -v 的参数是变量名
1
$ [ -v TERM ]; echo $?
0
$

$ unset v
$ [ $v ] && echo ok || echo bad # 等效为  $v == ""
bad
$ v=
$ [ $v ] && echo ok || echo bad
bad
$ v=""
$ [ $v ] && echo ok || echo bad
bad

$ unset v2
$ if [ $v2 ]; then echo ok; else echo bad;fi
bad
$ v2=""
$ if [ $v2 ]; then echo ok; else echo bad;fi
bad
$ v2=" "
$ if [ $v2 ]; then echo ok; else echo bad;fi
bad
$ v2=0
$ if [ $v2 ]; then echo ok; else echo bad;fi
ok

其它可以使用变量赋值的地方:alias/declare/typeset/export/readonly/local

变量 += 操作的语义取决于变量类型或 ATTR:

  1. 默认是字符串添加;
  2. 向 array 添加新的元素;
  3. 对于 integer attribute 的 variable, 是 add 的语义;
$ a="a"  # 字符串添加
$ a+=b
$ echo $a
ab
$ b="cc"+"dd"
$ echo $b
cc+dd

$ r=(a b c)
$ r+=d   # 添加到 array 第一个元素
$ echo "${r[@]}"
ad b c
$
$ r+=(e) # 向 array 添加新的元素
$ echo "${r[@]}"
ad b c e
$

$ declare -i inv # 数字语义
$ inv=3
$ inv+=4
$ echo $inv
7

15.1 Positional Parameters
#

使用一位或多位数字来表示,如果多于一位数字,则需要使用 ${NN} 来表示,否则 $N 只能表示一位数字。位置参数不能被赋值。

$ function mytest() { local name="${1:=zhangjun}"; printf "%s\n" name; }
$ mytest
bash: $1: cannot assign in this way

# $1 未定义或为 null hu, 则返回 zhangjun, 但是不对 $1 赋值。
$ function mytest() { echo "${1:-zhangjun}"; }
$ mytest
zhangjun

# $1 未定义或为 null 则出错(不再执行后面语句)
$ function mytest() { local name="${1:?zhangjun}"; printf "%s\n" name; }
$ mytest
bash: 1: zhangjun

set 或 shift 能创建和修改位置参数($0 固定为执行 shell 的脚本路径或 shell 名称)。

$ echo "$@"

$ set a b c d # set 将 ARGUMENT 作为位置参数,$1, $2
$ echo "$@"
a b c d

$ set -- -a b c d # 如果 ARGUMENT 以 - 开头,则需要在前面添加 --
$ echo "$@"
-a b c d
$ set -a b c d  # 否则 - 开头的参数,会被当作 set 内置命令自身的参数
$ echo "$@"
b c d

$ shift  # 位置参数左移一位,$#, $1, $2 等跟着变化
$ echo $#
3
$ shift 2  # 位置参数左移 2 位;
$ echo "$@"
d

set 命令: The difference between – and - is that when - is used, the -x and -v options are also unset.

  • -v Print shell input lines as they are read.
  • -x Print commands and their arguments as they are executed.
$ set -vx
$ echo "$-"
himvxBHs                # The options -v and -x are set.

$ set - a b c
$ echo "$-  <>  $@"     # The -x and -v options are turned off.
himBHs  <>  a b c

15.2 Special Parameters
#

  1. $* Expands to the positional parameters, starting from one.
  2. $@ Expands to the positional parameters, starting from one.
  3. $# Expands to the number of positional parameters in decimal.
  4. $? Expands to the exit status of the most recently executed foreground pipeline.
  5. $- Expands to the current option flags as specified upon invocation, by the 'set' builtin command, or those set by the shell itself (such as the ‘-i’ option).
  6. $$ Expands to the process ID of the shell. In a ‘()’ subshell, it expands to the process ID of the invoking shell, not the subshell.
    • 在 () 中 $$ 返回的是 invoking shell 的 PID, 而非 () 所在的 sub shell PID;
  7. $! Expands to the process ID of the job most recently placed into the background, whether executed as an asynchronous command or using the ‘bg’ builtin
  8. $0 Expands to the name of the shell or shell script. This is set at shell initialization.
  9. $_ expands to the last argument to the previous simple command executed in the foreground, after expansion
    • 上一个前台命令最后一个经过扩展后的参数;

$! 返回最近一次异步执行的任务 PID:

  • & 是为 simple command 创建异步的,优先级比 pipeline 高;
  • 对于 (xxx) &{xxx} & 都是在 sub shell 中执行, 所以返回的是 sub shell PID;
  • 对于 coproc 创建的异步任务,如果 COMMAND 是复合命令,则 bash 会创建一个 sub shell 来执行它们,这时 PID 是执行这些命令的 sub shell;
myjob &
jobpid=$!

# 检查后台执行任务是否结束,signal 0 表示检查进程是否存在而不发送实际信号
kill -0 "${jobpid}"

# 等待后台任务结束
wait ${jobpid}

while kill -0 "$pid"; do
     sleep 1
done

"$*""$@" 具有特殊含义:

  1. "$*": 返回一个字符串;
  2. "$@": 为每个位置参数返回一个字符串;
  3. 不加双引号的 $* 和 $@ 含义相同;
$ set -- a 'bc d' ef
$ echo $#
3

$ for w in $*; do echo $w;done
a
bc
d
ef
$ for w in $@; do echo $w;done
a
bc
d
ef
$

$ for w in "$*"; do echo $w;done
a bc d ef
$
$ for w in "$@"; do echo $w;done
a
bc d
ef

$0 表示 shell 名称或 shell 脚本路径名称。结合 dirname 和 realpath 命令,可以获得执行的脚本文件所在的目录:

$ echo $0
-bash

$ bash -c 'echo $0'
bash

$ cat spool/x.sh
echo ok
echo $(dirname $0) # 打印 x.sh 脚本文件所在目录
echo $0

$ bash spool/x.sh
ok
spool
spool/x.sh

$ realpath spool/x.sh   # 打印绝对路径
/var/spool/x.sh


$ pwd
/var
$ echo "$(realpath $(dirname "./spool"))"  # 命令替换中的双引号不需要转义。
/var

16 declare/local
#

declare 和 local 用于声明变量和设置它的属性:

  1. array: -a;
  2. Associte Array: -A; 关联数组必须使用 declare -A 来声明。
  3. function name: -f;
  4. interger: -i;
  5. lower case: -l;
  6. upper case: -u;
  7. nameref: -n;
  8. readonly: -r;
  9. export: -x; # 将各 NAME (变量或函数名)标记为 export

declare -f 显示函数定义(含 body), declare -F 只显示函数名称, declare -p 显示所有 exported 的变量和函数定义。

类似的,local 也支持上述 declare 的选项,如 local -a arry 定义一个数组。

declare/local 可以来声明没有值的变量,称为 null string:

$ decalre a b  # a/b 都是没有赋值过的 null string

$ declare a= b= # a/b 都是赋值过的空字符串
# 等效于
$ declare a="" b=""
# 等效于
$ a= b=
# 等效于
$ a="" b=""

delcare:

  • -x: 打开对应的 attribute;
  • +x: 关闭对应的 attribute;
$ declare -i n  # n 具有 integer 属性
$ n=1
$ n+=2
$ echo $n
3

$ declare -ir a=4 # 声明属性的同时赋值。
$ a+=5
-bash: a: readonly variable

$ myint=3
$ declare -i myint # 也可以对已经存在的变量关联属性
$ echo $myint
3
$ myint+=4
$ echo $myint
7

$ declare -l lower  # lower 具有 lower 属性
$ lower=UPperP
$ echo $lower
upperp
$ declare -u upper # upper 具有 upper 属性
$ upper=lower
$ echo $upper
LOWER

${varname@a} 返回变量的属性列表:

$ declare -ir foo=10
$ echo ${foo@a}
ir

nameref: 定义变量名之间的引用,两种定义方式: declare -nlocal -n

$ a="~/*.log"
$ echo $a
~/*.log
$ c=a        # c 的值是其它变量名且没有声明为 nameref
$ echo ${!c} # 这时 ${!c} 表示 indirect expansion
~/*.log

$ declare -n b=a # 如果 ${!b}, b 为 nameref, 则显示 ref 的变量名
$ echo ${!b}
a
$

$ echo ${!a}
-bash: ~/*.log: invalid variable name

$ var1=var1
$ var2=var1
$ declare -n var2  # -n 指定 var2 为 nameref 类型
$ var2=var3
$ echo "$var1 $var2"
var3 var3
$ unset -n var2  # unset -n 删除 var2 的 nameref ATTR 属性和 var2 定义
$ var2=var4
$ echo "$var1 $var2"
var3 var4

$ var1="var1"
$ declare -n var2="var1" # declare -n var2="$var1"
$ var2=var3
$ echo "$var1 $var2"
var3 var3

namref 主要使用场景:

  1. 函数参数,即函数内修改函数外的变量(两者的名称不一致);
  2. for in 循环的变量;
$ var1="old"
$ function cv() {
>   declare -n var2=$1             # var2 是传入的 $1 的 nameref
>   var2=new
> }
$ cv $var1; echo $var1
old
$ cv var1; echo $var1  # 调用 cv 使传入 var name, 而非 var value
new

$ declare -n W
$ for W in a b c d; do W=value;done  # for word-list 为变量名列表
$ echo "$a $b $c $d"
value value value value

示例 2:

#! /usr/bin/env bash
## $1 :: name, passed by value
## $2 :: array, passed by reference
## http://www.skybert.net/bash/pass-bash-arrays-by-reference/

foo() {
  local name=$1
  local -n _array=$2

  printf "${FUNCNAME[0]} Pass by val, name: %s\\n" "${name}"
  local el=
  for el in "${_array[@]}"; do
    printf "${FUNCNAME[0]} Pass by ref, array el: %s\\n" "${el}"
  done

  _array=(baz)
}

main() {
  local _my_array=(0 one two three 4)
  foo "bar" _my_array

  for el in "${_my_array[@]}"; do
    printf "${FUNCNAME[0]} array el: %s\\n" "${el}"
  done
}

main "$@"

使用 unset -n 命令来 unset nameref:

$ var1=var1
$ var2=var1
$ declare -n var2
$ unset -n var2     # unset -n var2 只是取消 nameref 类型的 var2 变量定义,指向的 var1 还是存在的
$ echo "$var1, $var2"
var1,

$ unset -n var1 var2
$ unset  var1 var2
$ var1=var1
$ var2=var1
$ declare -n var2
$ unset var2            # unset var2, 如果 var2 是 nameref 类型,则同时取消 var2 和指向的 var1 的变量定义。
$ echo "$var1, $var2"
,

17 Array
#

17.1 普通数组
#

数组的 index 可以是不连续的。访问数组元素时, 必须使用 ${array[i]} 格式, 而非 $array[i]

定义和初始化数组:

  1. 字面量方式:

    # 字面量定义1:
    $ arr=()  # 空数组
    $ arr=(a b c)      # 数组元素之间空格分隔
    $ echo "${arr[@]}" # 获得数组元素列表
    a b c
    $ echo "${!arr[@]}" #获得数组 index 列表
    0 1 2
    
    # 字面量定义 2:
    # index 可以不连续, 读取不存在的 index 时返回 null string
    $ a=([1]=2 [4]=5)
    $ echo ${#a[@]}
    2
    $ echo ${!a[@]}
    1 4
    
    # 字面量定义 3:
    $ arrb[0]=1  # 自动创建 indexed array
    $ arrb[3]=3
    $ echo "${#arrb[@]}" # 返回数组元素数量
    2
    $ echo "${arrb[@]}"
    1 3
    $ echo "${!arrb[@]}"
    0 3
    $ echo "${arrb[1]}"
    
    $ echo "${arrb[0]}"
    1
    $ echo "${arrb[2]}"
    
    $ echo "${arrb[3]}"
    3
    $
    
    # 普通数组的 key 都是数字,如果为字符串则默认等效为 index 0
    $ unset  a
    $ a=([0]=a [1]=b [2]=c [4]=e)
    $ echo ${a[@]}
    a b c e
    $ echo ${!a[@]} # index 可以有空洞
    0 1 2 4
    $ a+=([d]=d)   # index 非数值时等效为 index 0
    $ echo ${!a[@]}
    0 1 2 4
    $ echo ${a[@]}
    d b c e
    $ echo ${a[abc]}  # 非数值 index 等效为 index 0
    d
    
    # index 可以是变量,必须使用 $var 语法
    $ i1=1
    $ i2=2
    $ arrc[$i1]=1
    $ arrc[$i2]=2
    $ echo "${arrc[@]}"
    1 2
    $
    
    $ declare  -a array=(a b c)
    $ var=value
    $ array+=(var)  # 添加 var 字符串
    $ array+=($var) # 添加 $var 变量值
    $ echo ${array[@]}
    a b c var value
    $ declare  -a array=(a 3 c "d") # 值默认都为字符串
    $ echo ${array[@]}
    a 3 c d
    $ array[2]+=3
    $ echo ${array[@]}
    a 3 c3 d
    $ array[1]+=3
    $ echo ${array[@]}
    a 33 c3 d
    
    $ array=(a b c d)   # array 字面量
    $ echo $array       # $array, ${array}: 返回 array 的第一个元素
    a
    $ echo ${array[@]}  # ${array[@]} 返回数组所有元素
    a b c d
    
    # 以下两者是错误语法,必须使用 ${array[INDEX]}
    # $ echo $array[@]
    # a[@]
    # $ echo $array[1]
    # a[1]
    
    # value 执行变量和命令替换(注意结果会被分词):
    $ arrd=($(date))
    $ echo "${#arrd[@]}"
    6
    $ echo "${arrd[@]}"
    Thu Aug 25 21:08:36 CST 2022
    
    $ a=2; b=3;
    $ array=(a $b)  # 字面量支持变量替换和命令替换
    $ echo "${array[@]}"
    a 3
    
    $ array=("$(date)")
    $ echo "${#array}"
    28
    $ echo "${array[0]}"
    Thu Aug 25 21:11:32 CST 2022
    
    $ array=($(date))
    $ echo "${array[0]}"
    Thu
    $ echo "${#array}"
    3
    $ echo $array[@]
    Thu[@]
    $ echo ${array[@]}
    Thu Aug 25 21:12:17 CST 2022
    
  2. decalre 方式:

    $ declare -a NAME
    $ declare -a NAME[SUBSCRIBE] # SUBSCRIBE 会被忽略。
    $ declare -a ARR=(a b c)  # 定义的同时赋值。
    $ echo ${ARR[@]}
    a b c
    

返回数组长度:

# value 执行变量和命令替换(注意结果会被分词):
$ arrd=($(date))
$ echo "${#arrd[@]}"
6
$ echo "${arrd[@]}"
Thu Aug 25 21:08:36 CST 2022

读取和设置数组值:

$ declare -a array=(a b c)
$ echo ${array[@]}
a b c
$ array[1]=3
$ echo ${array[@]}
a 3 c

$ unset array[3]
$ echo ${array[3]}  # 不存在的 index 访问返回空。

$
$ echo a${array[3]}b
ab

向数组添加新元素, 使用 r+=(…) 语法:

$ r=(a b c)
$ r+=d   # 添加到 array 第一个元素
$ echo "${r[@]}"
ad b c
$

$ r+=(e) # 向 array 添加新的元素
$ echo "${r[@]}"
ad b c e
$

删除数组元素, 使用 unset a[i] 语法:

  • 删除数组元素后,数组对应位置形成空洞,长度也会减小,但剩下的元素的各 index 不变,还是使用以前的 index 来访问。
  • 删除后续添加到数组的元素无效;
$ ar=(a b c d)
$ unset ar[2]  # 必须使用 ARRAY[INDEX] 方式来删除数组元素
$ echo ${ar[@]}
a b d

$ unset ar[0:1] # 不支持批量删除
-bash: 0:1: syntax error in expression (error token is ":1")

$ unset ar
$ ar=(a b c d)
$ i=1
$ unset ar[i]  # 可以使用变量来指定 INDEX
$ echo ${ar[@]}
a c d
$ unset ar[2]
$ echo ${ar[@]}
a d

$ unset ${ar[1]} # 错误
$ echo ${ar[@]}
a b d

$ unset  array
$ declare  -a array=(a 3 c "d")
$ unset array[1]
$ echo ${array[0]}
a
$ echo ${array[@]}
a c d
$ echo ${#array[@]}
3
$ i=2
$ unset array[i]  # 删除的是 declare 时的 index 2
$ echo ${array[@]}
a d
$ unset array[3]  # 删除的是 declare 时的 index 3
$ echo ${array[@]}
a

$ declare  -a array=(a 3 c "d")
$ unset array[2]
$ echo ${array[@]}
a 3 d
$ array+=(e)
$ echo ${array[@]}
a 3 d e
$ echo ${array[4]}
e
$ unset ${array[4]} # 删除后续添加的元素无效
$ echo ${array[4]}
e
$ echo ${array[@]}
a 3 d e
$ unset ${array[2]} # 重复删除已经删除的元素 OK
$ echo ${array[@]}
a 3 d e

返回 array 的 key 或 value 或元素数量:

$ echo ${!r[@]} # 返回 array 的 key
0 1 2 3
$ echo ${r[@]} # 返回 array 的 value
ad b c e
$ echo ${#r[@]} # 返回 array 的元素数量
4

$ echo ${r[@]:1}  # 返回 array 的部分元素
b c e
$ echo ${r[@]:1:2}
b c
$

for i in {0..9}; do echo "i=$i" done

apple_array=("red" "yellow" "green")
for (( i = 0; i < ${#apple_array[@]}; i++ )); do
  echo "Apple number" $i "is" ${apple_array[$i]}
done

17.2 Associative Array
#

https://www.artificialworlds.net/blog/2012/10/17/bash-associative-array-examples/

必须使用 declare -A 来声明变量是关联数组类型。(普通数组可以直接创建)。

关联数组的 key 是字符串类型,可以不使用双引号引住。

访问不存在的 key 时返回空串(普通数组类似)。

# 必须先使用 declare -A 声明关联数组变量。
$ unset a
$ a=(["space key"]=v1 ["space key2"]=v2)
-bash: space key: syntax error in expression (error token is "key")

$ unset a
# key 默认为字符串,引号可选。
$ declare -A a=(["space key"]=v1 [space key2]=v2)

$ echo ${a[@]} # 返回数组值
v1 v2
$ echo ${!a[@]} # 返回数组 key
space key space key2

# 如果不使用 declare -A 声明, 则 ARRAY[KEY]=VALUE 创建的还是普通数组。
$ unset mymap
$ mymap["key1"]=value
$ mymap["key2"]=value2
$ echo ${!mymap[@]}  # 还是只有一个元素,INDEX KEY 是0
0
$ echo ${mymap[@]}
value2
$ index=10
$ myarr[index]=10
$ echo ${#myarr[@]}
1
$ echo ${myarr[0]}
$ echo ${!myarr[@]}
10
$ echo ${#myarr[@]}
1

$ declare -A assArray1
$ assArray1[fruit]=Mango
$ assArray1[bird]=Cockatail
$ assArray1[flower]=Rose
$ assArray1[animal]=Tiger
$ declare -A assArray2=([HDD]=Samsung [Monitor]=Dell [Keyboard]=A4Tech )

# 必须使用 ${Arr[Key]} 访问形式
$ echo ${assArray1[bird]}
Cockatail

# index 可以是变量,但必须是 $var 的形式,否则将 i 插入数组
$ declare -A a
$ a[1]=1
$ a[3]=3
$ i=6
$ a[i]=6  # 插入 key: i
$ i=7
$ a[$i]=7 # 插入 key:7
$ echo ${a[@]}
7 3 1 6
$ echo ${!a[@]}
7 3 1 i
$ unset a

$ for key in "${!assArray1[@]}"; do echo $key; done
$ echo "${!assArray1[@]}"
$ for val in "${assArray1[@]}"; do echo $val; done
$ echo "${assArray1[@]}"
$ for key in "${!assArray1[@]}"; do echo "$key => ${assArray1[$key]}"; done
$ echo "${assArray2[@]}"

# 添加新元素
$ assArray2+=([Mouse]=Logitech)
$ echo "${assArray2[@]}"

# 删除元素,必须使用格式:unset arr[key], 而不是 unset ${arr[key]}
$ unset assArray2[Monitor]   # 正确
$ echo ${assArray2[Monitor]} # 错误

18 Shell Expansions
#

shell 扩展的顺序:

  1. brace expansion: {}
  2. tilde expansion: ~
  3. parameter and variable expansion: ${xx}
  4. command substitution: $()
  5. arithmetic expansion: $(())
  6. word splitting: 分词;
  7. filename expansion: 文件名扩展;

命令替换或变量替换后再执行分词, 所以命令替换结果中的 换行会被替换为空格 。可以使用引号来阻止分词和如下扩展:

  1. brace expansion
  2. tilde expansion
  3. filename expansion

可以自定义 IFS 变量来定义分词时的特性。

当所有扩展都完成后,引号会被从结果中移除:

$ echo $(date) # 分词
Thu Oct 17 14:49:07 CST 2024

$ echo "$(date)" # 不分词
Thu Oct 17 14:49:11 CST 2024

$ date=$(date) # 变量赋值时不分词

$ echo 'asdfa\'bc"asdf"asdf #
asdfa\bcasdfasdf

$ ./yy $(echo zhang jun)
2
zhang
jun

$ ./yy $(echo "zhang jun")  # 结果被分词
2
zhang
jun

$ ./yy "$(echo zhang jun)"  # 结果不会被分词
1
zhang jun

# 变量赋值时不会对 VALUE 进行 word splitting 和 filename expansion
$ f=/*
$ echo "$f"
/*

# 变量替换后会再执行 filename expansion。
$ echo $f
/Applications /Library /System /Users /Volumes /bin /cores /dev /etc /home /opt /private /sbin /tmp /usr /var
$

18.1 brace expansion
#

大括号扩展:

$ echo {1,3}
1 3
$ echo {1, 3} # 逗号前后不能有空格
{1, 3}
$ echo "{1,3}" # 不能被 quoted
{1,3}

$ cp a.log{,.bak} # , 前可以没有值,等同于 cp a.log a.log.bak

$ echo a{1,2{{4,5},F},3}c # 支持嵌套
a1c a24c a25c a2Fc a3c

# brace expansion 在所有扩展之前的执行,所以结果可以包含 glob pattern
$ echo index.html{,*1}
index.html index.html.1

使用 {a..b..c} 语法生成数字或字符串序列,通常和 for 循环连用:

$ echo {1..5..2} # 字符串或数字序列,inclusive 模式
1 3 5

$ echo {1 .. 5} # .. 前后不能用空格
{1 .. 5}
$ echo {1 ..5}
{1 ..5}

$ a=5
$ echo {1..a} # 不支持变量
{1..a}
$ echo {1..$a}
{1..5}

$ echo {1..5..-1}
1 2 3 4 5
$ echo {01..5} # 0 前缀
01 02 03 04 05
$ echo {a..z..2}
a c e g i k m o q s u w y
$ echo {0a..z} # X..Y 的 X 和 Y 必须是一种类型,如数字或字符。
{0a..z}

$ for i in {1..10};do echo $i;done

18.2 tilde expansion
#

  1. 不能使用引号 quoting;
  2. ~ 位于 word 的开头,或者第一个 = 之后,或者各 : 之后;
$ h=~/ # h 值已经被替换为 /root/
$ echo "$h"
/root/
$ h="~/"
$ echo $h
~/
$ h=~/ff/~/ff
$ echo $h
/root/ff/~/ff

root@devops-109:~# echo ":~/"  # 被 quota 的 ~ 不被扩展
:~/

root@devops-109:~# echo a=~/~/  # 紧跟赋值 = 后的 ~ 会被扩展
a=/root/~/

root@devops-109:~# echo a=b~/~/  # 非紧跟 = 后, 不被扩展;
a=b~/~/

root@devops-109:~# echo a=~/~/=~/a:~/b:~/c,  # 每一个 : 后的 ~ 都被扩展
a=/root/~/=~/a:/root/b:/root/c,

~user 表示扩展成用户 user 的主目录:

$ echo ~foo
/home/foo

$ echo ~root
/root

如果 ~user 的 user 是不存在的用户名,则波浪号扩展不起作用:

$ echo ~nonExistedUser
~nonExistedUser

~+ 会扩展成当前所在的目录,等同于 pwd 命令:

$ cd ~/foo
$ echo ~+
/home/me/foo

18.3 Shell Parameter Expansion
#

对于各种 ${PARAMENT:-WORD} 中的 WORD 支持各种扩展 tilde expansion, parameter expansion, command substitution, and arithmetic expansion。

PARAMETER 后面可以加冒号,如 ${name:=opsnull} 或者不加冒号,如 ${name=opsnull},两者的区别是:

  1. 未加冒号:检查变量是否存在且被赋值过(即:不能是 null string,可以为空串);
  2. 加了冒号:还看变量值是否为 null string 或空串;

null string:只定义变量但不赋值,例如 local v 或者 declare v, 注意 declare v= 或者 declare v="" 则在定义变量v 的同时赋值为空字符串, 不再是 null string。可以使用 [ -v VAR ] 来检查变量 VAR 是否为定义或为 null string。

# 未加冒号时,变量存在且不为 null string
zj@a:~/docs$ unset var
zj@a:~/docs$ declare var
zj@a:~/docs$ name=${var=value} # var 为 null sting
zj@a:~/docs$ echo $name
value
zj@a:~/docs$ unset var
zj@a:~/docs$ declare var=
zj@a:~/docs$ name=${var=value} # var 为空串
zj@a:~/docs$ echo $name

zj@a:~/docs$

# 加冒号时,变量必须存在且不为空串和 null string
zj@a:~/docs$ unset var
zj@a:~/docs$ declare var
zj@a:~/docs$ name=${var:=value}
zj@a:~/docs$ echo $name
value
zj@a:~/docs$ unset var
zj@a:~/docs$ declare var=
zj@a:~/docs$ name=${var:=value}
zj@a:~/docs$ echo $name
value
zj@a:~/docs$ unset var
zj@a:~/docs$ var=myvalue
zj@a:~/docs$ name=${var:=value}
zj@a:~/docs$ echo $name
myvalue

# 变量不存在,两者等效
zj@a:~/docs$ unset var
zj@a:~/docs$ name=${var=value}
zj@a:~/docs$ echo $name
value
zj@a:~/docs$ unset var
zj@a:~/docs$ name=${var:=value}
zj@a:~/docs$ echo $name
value

${PARAMETER:-WORD}, ${PARAMETER:=WORD}: 变量未定义或为 null 时,返回 WORD:

  1. :-: 不对变量 PARAMETER 赋值;
  2. :=: 对变量 PARAMETER 赋值;
ot@devops-109:~# FOO=foo
root@devops-109:~# echo ${ FOO}  $ FOO 前后不能有空格
-bash: ${ FOO}: bad substitution
root@devops-109:~# echo ${FOO :=ff}
-bash: ${FOO :=ff}: bad substitution

root@devops-109:~# echo ${KOO:= ff }
ff
root@devops-109:~# echo "a${KOO}b" # 可见 = 号右边开头的空格被保留但是尾部的空格被删除
a ffb

root@devops-109:~# echo "a${ffOO:- afsd asdfa }b" # 带空格的字符串
a afsd asdfa b

$ var=test
$ echo "${FOO:-var}"  # var 默认是字符串,可以不加 quoting
var

$ echo "${FOO:-$var}" # 可以使用变量替换
test
$ echo $FOO

$ echo "${FOO:=$var}"
test
$ echo $FOO
test

$ unset FOO
$ echo "${FOO:=$(date)}"  # 也可以使用命令替换
2022年 08月 25日 星期四 19:01:35 CST
$ echo $FOO
2022年 08月 25日 星期四 19:01:35 CST

root@devops-109:~# echo ${FOO:-$((3+4))}  # 还可以使用算术替换
7
root@devops-109:~# echo ${FOO:-((3+4))}  # 错误: 因为值默认是字符串
((3+4))

$ unset FOO; echo ${FOO:- ~/log}  # tilde expansion 时 ~ 必需位于 WORD 开头
~/log
$ unset FOO; echo ${FOO:-~/log}
/root/log

${PARAMETER:?WORD} : 变量未定义时报错,在 stderr 打印 WORD 后退出:

  • 变量名所在的 simple/list/compound command 都不再执行;
  • 报错输出有特定的格式;
$ bash
$ PS1='>'
>echo "${BAR:?asdfasd}" || echo bad ; echo haha # 打印字符串以 bash 或 -bash 开头, BAR 为未定义的变量名, 后续 echo bad 不再执行
bash: BAR: asdfasd
>echo $?
1
>

>for i in a "${BAR:?asdfad}" b c d; do echo $i;done
bash: BAR: asdfad
>echo $?
1
>

$
$ bash
$ PS1=">"
# 出错后, || 后面的以及函数体命令都不再执行.
>function test_undefine () { "${1:?error: not paas value}" || echo hello1 ; echo hello2; }
>set -e
>test_undefine
bash: 1: error: not paas value
$

${PARAMETER:+WORD}: 变量不为 null 时返回 WORD (为空时返回空)。

[root@izhp3dg2to7txzqy7v2bigz ~]# unset FOO
[root@izhp3dg2to7txzqy7v2bigz ~]# echo "${FOO:+unset FOO}"

[root@izhp3dg2to7txzqy7v2bigz ~]# FOO=foo
[root@izhp3dg2to7txzqy7v2bigz ~]# echo "${FOO:+setted FOO}"
setted FOO
[root@izhp3dg2to7txzqy7v2bigz ~]# echo $?
0
[root@izhp3dg2to7txzqy7v2bigz ~]# echo $FOO
foo

$ for i in a b c d; do
> res+=${sep:+,}${i}
> sep=,
> done
$ echo $res
a,b,c,d

${PARAMETER:OFFSET} 和 ${PARAMETER:OFFSET:LENGTH} 返回子字符串或数组的部分元素:

  • offset 和 length 都是从 0 开始, 可以是负值, 这时含义均是从 value 末尾开始的偏移.
$ var=123456
$ echo ${var:0} # LENGTH 省略时表示到结尾
123456
$ echo ${var:1}
23456
$ echo ${var:1:2}
23
$ echo ${var:0:2}
12
$ echo "${var:0:-2}" # 不包含 -2 处的 5.
1234
$ echo "${var: 0 : -2}" # OFFSET 和 LENGTH 数值前后可以有空格
1234
$ echo "${var: -3: 100}" # 此时的 -3 表示从值末尾的偏移
456
$ echo "${var: -7: 100}" # OFFSET -7 不存在, 返回空值。

$ length=5
$ echo "${var: -4: length}"  # OFFSET 和 LENGTH 都是 shell arithmetic expression
3456
$ echo "${var: -4: length-3}" # shell arithmetic expression 中变量名前的 $ 可省略
34
$ echo "${var: -4: $length-3}"
34
$ echo "${var: -4: $FOO+3}"  # 对于 shell arithmetic expression, 未定义变量值为 0
345

$ set -- 01234567890abcdefgh
$ echo ${1:7}    # 位置参数
7890abcdefgh
$ echo ${1:7:0}

$ echo ${1:7:2}
78
$ echo ${1:7:-2}
7890abcdef
$ echo ${1: -7}
bcdefgh
$ echo ${1: -7:0}

$ echo ${1: -7:2}
bc
$ echo ${1: -7:-2}
bcdef

$ array[0]=01234567890abcdefgh
$ echo ${array[0]:7}  # 数组元素
7890abcdefgh
$ echo ${array[0]:7:0}

$ echo ${array[0]:7:2}
78
$ echo ${array[0]:7:-2}
7890abcdef
$ echo ${array[0]: -7}
bcdefgh
$ echo ${array[0]: -7:0}

$ echo ${array[0]: -7:2}
bc
$ echo ${array[0]: -7:-2}
bcdef

$ set -- 1 2 3 4 5 6 7 8 9 0 a b c d e f g h
$ echo ${@:7}
7 8 9 0 a b c d e f g h
$ echo ${@:7:0}

$ echo ${@:7:2}
7 8
$ echo ${@:7:-2}
bash: -2: substring expression < 0
$ echo ${@: -7:2}
b c
$ echo ${@:0}
./bash 1 2 3 4 5 6 7 8 9 0 a b c d e f g h
$ echo ${@:0:2}
./bash 1
$ echo ${@: -7:0}

function partition() {
  local string="$1"
  local delim="$2"
  local before after

  before=${string%%$delim*}
  after=${string#*$delim}
  echo "$before"
  echo "$delim"
  echo "$after"
}

${!PARAMETER}: 如果 PARAMETER 不是 nameref, 则返回 PARAMETER 变量值对应的变量的值即 = 变量重定向语法= :

root@devops-109:~# myvar1=myvar0
root@devops-109:~# myvar0=0
root@devops-109:~# echo ${myvar1}
myvar0
root@devops-109:~# echo ${!myvar1}
0

${!PREFIX*} 和 ${!PREFIX@}: 展开名称前缀为 PREFIX 的变量名:

$ a=1
$ aa=2
$ aaa=3
$ echo ${!a*}
a aa aaa
$ echo ${!a@}
a aa aaa
$ for w in  ${!a@}; do echo $w;done
a
aa
aaa
$ for w in  ${!a*}; do echo $w;done
a
aa
aaa
$ for w in "${!a*}"; do echo $w;done
a aa aaa
$ for w in "${!a@}"; do echo $w;done
a
aa
aaa
$

${!NAME[@]} 和 ${!NAME[*]}: 返回数组的 key:

$ a=(a b c d)
$ echo ${!a[@]} # 展开为数组 indices
0 1 2 3
$ echo ${a[@]} # 展开为数组值
a b c d
$

$ a=($(ls *.org))  # 普通数组
$ echo ${a[@]}
eintr.org inbox.org

$ a=(["0"]=1 ["1"]=a)  # 定义一个关联数组
$ echo ${!a[@]}  # 展开为数组 indices
0 1
$ echo ${a[@]}   # 展开为数组值
1 a
$

${#PARAMENT} 返回变量字符串值长度或数组元素个数:

$ s4="abcd"
$ echo ${#s4}
4

$ declare -A a2=(["c"]=c1 ["d"]=d1) # 定义一个关联数组
$ echo ${#a2}  # 第一个元素长度
0
$ echo ${#a2[@]} # 关联数组元素个数
2

$ a=(a b c d)
$ echo ${#a}
1
$ echo ${@a[@]}
-bash: ${@a[@]}: bad substitution
$ echo ${#a[@]}
4

${PARAM#WORD}, ${PARAM##WORD} 或 ${PARAM%WORD}, ${PARAM%%WORD} 删除匹配 WORD 的内容:

  • word 是 glob pattern;
  • #: 从左边开始匹配, 短匹配;
  • %: 从右边开始匹配,短匹配;
  • 两个 ## 或 %% 表示尽可能多(次)匹配;
$ path=/path/to/dir/file.ext
$ echo ${path#*/}
path/to/dir/file.ext
$ echo ${path##*/}
file.ext
$ echo ${path%*/}
/path/to/dir/file.ext
$ echo ${path%.ext}
/path/to/dir/file
$ echo ${path%.ext}

$ a=(aaa aba acaaa)  # 对于 arrary, #, ##, %, %% 是作用于数组的每个元素
$ echo ${a[@]#a}
aa ba caaa

${PARAMETER/PATTERN/STRING}:

  1. PATTERN 是 glob 语法, 默认将 PARAMETER 变量值匹配 PATTERN 最长内容替换为 STRING。
  2. PATTERN 的首字符是下列时,具有特殊含义:
    • /: 替换 PARAMETER 变量值中所有匹配 PATTERN 的内容为 STRING (默认只替换一次);
    • #: 从 PARAMETER 变量值开头匹配(默认是任意位置匹配);
    • %: 从 PARAMETER 变量值结尾匹配;
    • 注意: 只是首字符有特殊含义, 后面的内容到第一个 / 字符都是 PATTERN;
  3. /STRING 可以省略,这时匹配 PATTERN 的内容会替换为空串;
$ var=",,/var,/run,/home,,"
$ echo "${var/,/;}"  # 将 , 替换为 ; (只替换一次)
;,/var,/run,/home,,
$ echo "${var//,/;}" # 将 , 替换为 ; (替换所有)
;;/var;/run;/home;;
$ echo "${var//,}"  # 将所有 , 替换为空
/var/run/home
$ echo "${var/#,/;}" # 将开头 , 替换为 ; (只替换一次)
;,/var,/run,/home,,
$ echo ${var/#/,/;}  # # 后的 / 用做分隔符, 也就是指定的 PATTERN 为空, 所以在字符串开头插入 ,/;
,/;,,/var,/run,/home,,
$ echo "${var/%,/;}" # 将结尾 , 替换为 ; (只替换一次)
,,/var,/run,/home,;
$ echo ${var/%/,/;} # % 后的 / 用做分隔符, 也就是指定的 PATTERN 为空, 所以在字符串结尾插入 ,/;
,,/var,/run,/home,,,/;
$ echo "${var/%%,/;}" # 将结尾的 %,  替换为 ; (没有匹配的情况)
,,/var,/run,/home,,
$

${PARAMENT^PATTERN} 和 ${PARAMENT,PATTERN}: 期望 PATTERN 是一个单字符,用来 PARAMENT 中匹配的单字符做大小写转换:

$ v=abCD123
$ echo ${v^[a-z]} # 只匹配一次
AbCD123
$ echo ${v^^[a-z]}  # 匹配多次
ABCD123
$ echo ${v^?} # PATTERN 是 glob PATTERN
AbCD123
$ echo ${v^^?}
ABCD123
$ echo ${v^*}
AbCD123
$ echo ${v^^*}
ABCD123
$ echo ${v^ab} # PATTERN 只预期是单字符 PATTERN,如果是多个字符则不匹配。
abCD123
$ echo ${v^[a-z]*}
AbCD123

${PARAMETER@OPERATOR}:

  • U, u, L, K were added in bash version 5.1;
    • CentOS 8 是 4.4 不支持;
    • Debian 11, Ubuntu 21.04 是 5.1;
$ echo ${v@A}
v='abCD123'
$ echo ${v@U} # 不支持
-bash: ${v@U}: bad substitution
$ bash --version
GNU bash, version 5.0.17(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$

${varname@Q} returns a single-Quoted string with any special characters (such as \n, \t, etc.) escaped.

$ foo="one\ntwo\n\tlast"
$ echo "$foo"
one\ntwo\n\tlast

$ echo ${foo@Q}
'one\ntwo\n\tlast'

$ echo "${IFS@Q}"
$' \t\n'
$

${varname@E} returns a string with all the escaped characters Expanded (e.g. \n -> newline). 不依赖 echo -e 的选项, 所以使用范围更广。

$ foo="one\ntwo\n\tlast"
$ echo "${foo}"
one\ntwo\n\tlast

$ echo "${foo@E}"
one
two
        last

${varname@P} returns a string that shows what the variable would look like if it were used as a Prompt variable (i.e. PS1, PS2, PS3)。主要用于显示 PS1-3 字符串的效果;

$ bar='host: \h'
$ echo ${bar@P}
host: myhost1

${varname@A} returns a string that can be used to Assign the variable with its existing name, value, and declare options if any.

$ foo="test1"
$ echo ${foo@A}
foo='test1'

$ declare -i foo=10
$ echo "${foo@A}"
declare -i foo='10'

${varname@a} returns a string listing the Atributes of the variable.

$ declare -ir foo=10
$ echo ${foo@a}
ir

18.4 Command Substitution
#

bash 在 sub shell 环境中执行 COMMAND, 然后删除 trailing newlines, 中间的 newlines 不会被删除,但是在 word splitting 阶段,它们可能会被删除或替换为空格(如果不想删除,可以用双引号括住)。

变量赋值时,不会对值分词,所以不需要引号引住。

$ echo $(date)  # 分词

$ echo "$(date)" # 不分词
$ v=$(cat FILE) # 不分词

命令替换、括号分组命令 (xxx)、异步命令 3 种类型都是在一个 sub shell 环境中执行, 但是不同于 simple command 的 sub shell, 它是 dup 了执行 shell 的环境, 所以执行 shell 环境中未 export 的变量和函数也可以在命令替换和 (xxx) 中引用和执行。另外 calling shell 忽略的 SIGNAL trap 也会被忽略, calling shell 捕获的 SIGNAL 在 sub shell 中被重置为系统默认处理方式。该 sub shell 中使用的变量也不会影响执行 shell。

当使用 $(COMMAND) 格式时, COMMAND 的所有内容 都不会特殊对待, 它位于一个新的 quote context, 不受外围引号的影响, 不需要转义 , calling shell 也不会替换/执行中的任何内容 (如 $var),而是原样的传给 sub shell 来执行。所以可以直接写:

foo "$(bar "$(baz "$(ban "bla")")")"
$ v1=value1
# 在 dup subshell 中执行  echo a" bc "d $v1 命令。
# $(xx) 中 xx 的双引号不需要转义,会被无差别的传给 sub shell 来解释和执行。
$ res="$(echo a" bc "d $v1)"  # subshell 中可以使用 v1 变量
$ echo $res
a bc d value1

# 在 dup subshell 中执行 echo a" bc "d "$v1"
$ res="$(echo a" bc "d "$v1")"
$ echo $res
a bc d value1

# 在 dup subshell 中执行 echo a" bc "d '$v1'
# 这也说明了对于 $(xx) 中的 xx 的处理和解释是完全在 sub shell 中进行的。
$ res="$(echo a" bc "d '$v1')"
$ echo $res
a bc d $v1

$ read name tag <<< $(echo "zhang4 jun4")
$ echo $name
zhang4
$ echo $tag
jun4

# $(...) 中的双引号不需要转义。
# shell 不对 here string 的内容进行分词, 所以外围的双引号是可选的。
$ read name tag <<< "$(echo "zhang5 jun5")"
$ echo $name
zhang5
$ echo $tag
jun5
$

$ pwd
/var
$ echo "$(realpath $(dirname "./spool"))"  # 命令替换中的双引号 " 不需要转义。
/var

如果命令替换执行的是函数, 则 set -e 不会在函数内部出错时退出(因为检查的是函数的返回码,而函数 body 是 复合命令,返回码为执行结束的最后一条命令语句的返回码。), 但是如果执行的是命令则set -e 可以在命令执行出错时退出:

$ bash
$ PS1='>'
>function mycmd() { date; ccc; date; }
>set -e
>declare v=$(mycmd)                    # 没退出
bash: ccc: command not found
>v=$(mycmd)                            # mycmd 函数作为变量替换, 内部出错时没退出
bash: ccc: command not found
>v=$(ccc)                              # 退出
bash: ccc: command not found
$

18.5 Arithmetic Expansion
#

  1. 操作数的结果被当作数字,操作数支持如下扩展:parameter and variable expansion, command substitution, and quote removal;
  2. Arithmetic expansions may be nested.
# 逗号, 在 $((...)) 内部是求值运算符,执行前后两个表达式,并返回后一个表达式的值。
$ echo $((3+2*3 < 3, 34*2, var+=4))
4
$ echo $var
4
$ a=3
$ echo $((3+2*3 < 3, 34*2, var+=4, ${a}))
3
$ echo $((3+2*3 < 3, 34*2, var+=4, ${a}+4))
7
$ echo $((3+2*3 < 3, 34*2, var+=4, a+"4"))
7
$ echo $((3+2*3 < 3, 34*2, var+=4, a+4, $(echo 7)+1))
8
$ echo $((3+2*3 < 3, 34*2, var+=4, a+4, $(echo 7)+1, $((2-8))))
-6

算术替换失败返回 0:

$ $((fasda))
0: command not found
$ echo $((fasda))
0

# 对于 (( XX )), XX 结果为 0 时退出码 1, 其他结果时退出码为 0
$ (( 0 )); echo $?
1

while (( 1 )); do echo hello; sleep 1; done # 等效
while true; do echo hello; sleep 1; done

18.6 Shell Arithmetic
#

算术表达式的使用场景:

  1. shell arithmetic expansions: $(());

  2. (()) compound command;

  3. let builtin, declare -i buitin

  4. variable post-increment and post-decrement

  5. variable pre-increment and pre-decrement

  6. unary minus and plus

  7. logical and bitwise negation

  8. exponentiation

  9. multiplication, division, remainder

  10. addition, subtraction

  11. left and right bitwise shifts

  12. comparison

  13. equality and inequality

  14. bitwise AND

  15. bitwise exclusive OR

  16. bitwise OR

  17. logical AND

  18. logical OR

  19. conditional operator

  20. assignment, 支持赋值运算

  21. comma, 中间可以有空格

表达式中使用变量时可以不加 $ 引用。对于不存在的变量或 null string, 等效为 0。

$ echo $(( 2 > 3?-1:1)) # 条件表达式
1
$ echo $(( 2 > ff?-1:1)) # ff 变量未定义, 等效为 0
-1
$ echo $(( ff ))
0


$ echo $(( ! 0))  # 对 0 取反为 1, 其他值取反都是 0
1
$ echo $(( ! 2))
0
$ echo $(( ! "" ))  # 取反都是针对数字的
-bash: !  : syntax error: operand expected (error token is "!  ")

$ echo $(( ! 3-5 )) # 等效为 !3 结果为 0, 再 -5 ,所以为 -5.
-5

在 test 或 [] 或 [[]] 中 = != < > 是用于字符串比较的,对于数字之间的比较需要使用 ‘-eq’, ‘-ne’, ‘-lt’, ‘-le’, ‘-gt’, or ‘-ge’。但是在 (())$(()) 中,只支持算术表达式,可以使用上面的算术, 关系, 逻辑 和 位运算符。

Bash 的数值默认都是十进制,但是在算术表达式中,也可以使用其他进制。

  1. number:没有任何特殊表示法的数字是十进制数(以10为底)。
  2. 0number:八进制数。
  3. 0xnumber:十六进制数。
  4. base#number:base 进制的数。
$ echo $((0xff)) # 十六进制
255
$ echo $((2#11111111))  # 二进制
255

算术运行不支持浮点数:

$ echo $(( 2 * 0.9 ))
-bash: 2 * 0.9 : syntax error: invalid arithmetic operator (error token is ".9 ")

18.7 Process Substitution
#

LIST 可以是一个复杂的 shell list command,< 或 > 与 ( 之间不能有空格。

  1. <(LIST)
  2. >(LIST)

进程替换(外围不能有引号)的结果是一个特殊文件路径,命令可以读写这两个路径:

  1. 对于 >(LIST) 的路径,如果命令写该路径文件,则写的内容会作为 LIST 的输入;
  2. 对于 <(LIST) 的路径,如果命令读该路径文件,则读取的内容来自于 LIST 的输出;

由于返回的是一个文件路径,shell 不处理其中的内容,所以不对结果分词。

$ echo <(echo hello)
/dev/fd/63
$ cat <(echo hello)
hello

$ cat <(date=$(date); echo $date)
Sun Jun 4 15:10:28 CST 2023

# LIST 支持复杂命令
$ cat <(date=$(date); echo $date | sed -e 's/CST//')
Sun Jun 4 15:11:26  2023
$ cat <("echo hello")
-bash: echo hello: command not found

# 当重定向输出到 >(LIST) 时,> 之间必须有空格
$ echo $'hello\tworld\nhaha\n' > >(grep hello)
hello   world
$ echo $'hello\tworld\nhaha\n' >>(grep hello)
bash: 未预期的记号 "(" 附近有语法错误
$ echo $'hello\tworld\nhaha\n' > > (grep hello)
bash: 未预期的记号 ">" 附近有语法错误

$ echo $'hello\nworlod' > >(grep hello)
$ hello
$
$ echo $'hello\nhello2\nworlod' > >(grep hello >hello.file)
$ cat hello.file
hello
hello2

$ echo >(echo file)
/dev/fd/63
$ cat file

$ diff <(echo hel) <(echo hello)
1c1
< hel
---
> hello

# process substitution,进程输出的内容也不会被分词,所以内容中的换行会保留。
read name tag < <(split_registry_image_name_tag $image)

# here string, 不对 string 进行分词, 所以内容中的换行会保留。
read name tag <<< $(split_registry_image_name_tag "$image")

18.8 Word Splitting
#

shell 在执行完参数扩展、命令替换、算术扩展后,对于非引号中的内容进行 word splitting。

分词时使用 $IFS 变量中的每个值作为分隔符,默认值为空格,TAB 和换行,分词时将忽略内容 首尾 的空格,TAB 和换行,同时内容内部的空格、TAB 和换行 用于分词 (不被输出,多个连续的空白会被 合并为一个 ):

$ echo "${IFS@Q}" # 显示 IFS 的转义字符串
$' \t\n'

# \换行 只是起着下一行连接作用,前后的空格会被整合为一个(分词)
$ echo abc  \
>    asdfa\
>   asdf  \
>     asdf
abc asdfa asdf asdf
$ echo abc \
> "    asdfa"\
>   asdf \
>     "asfa  "
abc     asdfa asdf asfa

# 有空格的字符串需要转义
$ echo abc\ bcd |{ read var; echo "#$var#"; }
#abc bcd#

# 分词时中间的多个连续换行等效为一个空格,忽略 field 前后的空格分词时, 内容首尾的空
# 格, TAG 和换行被忽略, 中间的空格, TAB 和换行也都用于分词
$ ./yy $(echo "  #zhang" jun $'\n\n' c d)
4
#zhang
jun
c
d
$

不分词的情况:

  1. 使用双引号包围各种扩展和命令替换;
  2. 变量赋值;
# 命令替换如果没有双引号包围,结果会被分词
$ ./yy $(echo zhang jun)
2
zhang
jun
# 用双引号包围后,不分词
$ ./yy "$(echo zhang jun)"
1
zhang jun

# 变量赋值不会被分词
$ p=$(echo zhang jun)
$ echo "$p"
zhang jun
$

$ name=/*  # name 的值为 /*
$ echo "$name"
/*
$ echo $name # 不带双引号时, 会对 $name 的替换结果进行分词, filename expansion
/Applications /Library /System /Users /Volumes /bin /cores /dev /etc /home /opt /private /sbin /tmp /usr /var
$

read 读取的内容,会被 shell 根据 IFS 变量的值进行分词,然后赋值给对应变量。如果将 IFS 设置为空字符串, 即 IFS=, 这时 read 可以读取 整行内容(而不是整个文件输入) 赋值给变量:

# 将 IFS 设置为 null, 则不分词, line 为整行内容。
# IFS 作为 read 的环境变量,只对 read 有影响,不影响 do...done 中内容。
while IFS= read -r line; do
  #...
done  < file

自定义 IFS 值:

  • IFS 可以使用转移字符,如果要包含 \t 和 \n 则需要使用 bash 转义字符串 IFS=$' \t\n'
# read -a std_ips_a:将输入读取到一个普通数组。
# read -r 表示不忽略输入中的转义字符 \, 即认为它是普通字符。(默认忽略转义字符 \)
# <<< 表示 here string, 如 read name tag <<< $(split_registry_image_name_tag "$image")
# shell 不对 here string 的内容进行分词, 所以外围的双引号是可选的。

$ std_ips='a.b.c.de.f'
$ IFS="." read -r -a std_ips_a <<< $std_ips
$ echo ${std_ips_a[@]}
a b c de f
$ echo ${!std_ips_a[@]}
0 1 2 3 4

$ std_ips='a.b.c.de.f g h\tn'
$ read line <<< $std_ips
$ echo $line # 忽略转义字符 \,注意 \t 中的 t 未被删除。
a.b.c.de.f g htn
$ read -r line <<< $std_ips # 保留转义字符
$ echo $line
a.b.c.de.f g h\tn

# read 默认将转义字符 \ 从输入中删除
$ read a b <<< 'name \$(id)'
$ echo $a $b
name $(id)

# 加 -r 后,read 不忽略 \ 字符
$ read -r a b <<< 'name \$(id)'
$ echo $a $b
name \$(id)
$ read -r a b <<< 'name \f $(id)'
$ echo $a $b
name \f $(id)
$ read  a b <<< 'name \f $(id)'
$ echo $a $b
name f $(id)

while read -r line; do
    # <<< WORD 不会对 WORD 分词,所以外围的双引号是可选的。
    IFS="=" read -r key value <<< "${line}"
    printf "key=%s, value=%s\n" "${key}" "${value}"
done </etc/os-release

18.9 Filename Expansion
#

Bash 允许用户关闭和重新打开 filename expansion:

# 关闭
$ set -o noglob
# 或者
$ set -f

# 打开
$ set +o noglob
# 或者
$ set +f

默认情况下,如果没有设置 shopt -s nullglob, 则未匹配的 PATTERN 会被原样保留,否则这个 PATTERN 会被删除。

如果 shopt -s failglob 被设置,则当匹配失败时会打印 error message, 同时对应的命令也不会被执行。

如果 shopt -s nocaseglob 被设置,则进行 PATTERN 匹配时不考虑字母大小写(可以在函数内设置,只对该函数有效)。

开启 shopt -s dotglob 后,? 和 * 能匹配文件名开头的 . (文件名中间的 . 一直都可以用 ? 或 * 来匹配), 但是它们还是不匹配特殊目录 . 和 ..:

$ ls -ld */  # *  默认不能匹配文件名开头的 .
ls: cannot access '*/': No such file or directory
$ ls -ld ./* |head # 同理,* 作为路径中文件名开始的字符串时,也不匹配 .
-rw-r--r--  1 zhangjun zhangjun    50 Dec 13 10:25 ./README.md
drwxr-xr-x 17 zhangjun zhangjun   544 Jun  1 09:30 ./archive
drwxr-xr-x  5 zhangjun zhangjun   160 May 12 23:43 ./blog
drwxr-xr-x  6 zhangjun zhangjun   192 Jun  1 10:44 ./book
drwxr-xr-x 13 zhangjun zhangjun   416 Jun  1 10:46 ./bus-io-storage
drwxr-xr-x 14 zhangjun zhangjun   448 Dec 21 10:17 ./cloudnative
-rw-r--r--  1 zhangjun zhangjun 54067 Dec 25  2021 ./eintr.org
drwxr-xr-x 20 zhangjun zhangjun   640 Jan 20 17:25 ./emacs
-rw-r--r--  1 zhangjun zhangjun  8344 Dec  1  2021 ./inbox.org
-rw-r--r--  1 zhangjun zhangjun   678 Feb 14  2022 ./info.org.gpg

$ shopt -s dotglob
$ ls -ld */  # 不匹配 . 和 .. 目录
drwxr-xr-x 2 root root 4096 Jun 23 17:01 .arkctl/
drwx------ 3 root root 4096 Dec 27  2021 .cache/
drwx------ 4 root root 4096 Jan 22  2022 .config/
drwxr-xr-x 6 root root 4096 Jan 28  2022 .debug/
drwxr-xr-x 3 root root 4096 Aug 15 18:57 .local/
drwxr-xr-x 2 root root 4096 Dec 27  2021 .pip/
drwxr-xr-x 2 root root 4096 Feb  8  2022 .rpmdb/
drwx------ 2 root root 4096 May  7 10:23 .ssh/

$ ls -ld .?/  # 文件名中间的 ? 和 * 可以匹配 .
drwxr-xr-x 19 root root 4096 Jun 27 14:35 ../

$ ls -ld .*/
drwxr-xr-x 19 root root 4096 Jun 27 14:35 ../
drwx------ 10 root root 4096 Aug 28 20:30 ./
drwxr-xr-x  2 root root 4096 Jun 23 17:01 .arkctl/
drwx------  3 root root 4096 Dec 27  2021 .cache/
drwx------  4 root root 4096 Jan 22  2022 .config/

18.10 Glob pattern matching
#

filename expansion 是在 word splitting 之后进行的,只对 unquoted 的 * ? 和 [ 生效。

  • ? : 匹配一个字符;
  • * : 匹配 0 或多个字符,不像正则表达式那样表示重复 0 次或多次;
  • [ab-e0-9] 用于匹配指定字符,可以是 range 语法。要求不能为空字符串,所以 [] 如果要包含 ] 则它必须是第一个字符,如 []1],但 [1]] 是不对的。如果需要匹配 [ 字符,可以放在方括号内,比如[[aeiou]。如果需要匹配连字号-,只能放在方括号内部的开头或结尾,比如 [-aeiou] 或 [aeiou-]。如果 [] 的第一个字符是 ! 或 ^,则表示不匹配 [] 中除 ! 或 ^ 外的其它字符。如果将 ? * [ 放到 [] 中,则不需要转义,它们代表它们自身。
  • [..] 中支持使用字符类,如 [BROKEN LINK: :alpha:]]
$ echo [[:alpha:]]*
abbbc abbc

模式扩展的英文单词是 globbing,这个词来自于早期的 Unix 系统有一个 /etc/glob 文件,保存扩展的模板。后来 Bash 内置了这个功能,但是这个名字就保留了下来。模式扩展早于正则表达式出现,可以看作是原始的正则表达式。它的功能没有正则那么强大灵活,但是优点是简单和方便。

glob 不匹配以 . 开头的文件名,这个字符必须明确指定。所以 * 不匹配以 . 开头的文件名或目录。当 glob 匹配的内容为空时,POSIX 要求必须将 glob 字符串本身 传给程序。

Bash 4.0(CentOS 7 和 Ubuntu 18.04 均支持) 引入了一个参数 globstar,当该参数打开时( shopt -s globstar ),允许 * 匹配零个或多个子目录。因此,**/.txt 可以匹配顶层的文本文件和任意深度子目录的文本文件:

$ shopt -s globstar
$ ls /tmp/arkctl/results/checker-1656*
/tmp/arkctl/results/checker-1656074176176835000  /tmp/arkctl/results/checker-1656305300018666000
$ ls /tmp/**/checker-1656*
/tmp/arkctl/results/checker-1656074176176835000  /tmp/arkctl/results/checker-1656305300018666000
$

打开了 shopt -s extglob 后(默认关闭),还支持几种特殊的 pattern list, 用于控制匹配的次数:

  1. ?(pattern-list):模式匹配零次或一次。
  2. *(pattern-list):模式匹配零次或多次。
  3. +(pattern-list):模式匹配一次或多次。
  4. @(pattern-list):只匹配一次模式。
  5. !(pattern-list):匹配给定模式以外的任何内容。
$ shopt -s extglob
$ shopt extglob
extglob         on

$ touch abbc abbbc
$ ls a?(b)c # 不匹配时,?(xx) 内容原样传给 ls 程序
ls: cannot access 'a?(b)c': No such file or directory
$ ls a?(bb)c
abbc
$ ls a+(b)c
abbbc  abbc
$ ls a*(b)c
abbbc  abbc
$

$ touch adc
$ ls a?(b|d)c
adc
$ ls a+(b|d)c
abbbc  abbc  adc

$ ls a?(*b|d)c
abbbc  abbc  adc

18.11 Here Documents
#

格式: <() 是进程替换, << 是 here document, <<< 是 here string .

 [N]<<[-]WORD
   HERE-DOCUMENT
 DELIMITER
  • 将 WORD 标记的起止位置内容,作为应用 fd N 的输入;
  • 如果 << 后带有 -, 则后续以 TAB 开头的行的 TAB 会被忽略, 这样便于缩进对齐;
  • 默认对内容进行 expand,但如果 WORD 使用双引号括起来,则不扩展;
  • Here 文档的本质是重定向,它将字符串内容重定向输出给某个命令;
# Quoting 的 EOF 表示 shell 不会对内容进行 expand。
$ cat <<"EOF"
> hello
> echo $(id)
> EOF
hello
echo $(id)
$

# << 和 - 间不能有空格,只有 TAB  开头的行输出时才会忽略行首的 TAB
$ cat <<- EOF
>       after tab
>  after space
> begin
> EOF
after tab
 after space
begin
$

18.12 Here Strings
#

格式: [N]<<< WORD

The WORD undergoes tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal.

对 WORD 不进行 filename 扩展和分词,将 WORD 扩展后的内容添加 换行 ,然后作为命令 fd N 的输入(默认 stdin)。

# 由于 here string 是一个 WORD, 所以如果是多个字符串, 外围必须用空格.
$ read name tag <<< zhang jun
$ echo $name
zhang
$ echo $tag

# 等效为: read -r name tag jun <<< "a b c"
$ read name tag <<< "a b c" jun
$ echo $name
a
$ echo $tag
b
$ echo $jun
c
$

# 由于 here string 结果不会被分词, 所以外围不需要加双引号,
# 但是 read 使用 IFS 对输入行的内容进行分词,然后赋值给各变量。

# 等效为 $(echo zhang3 jun3 jun3)
$ read -r name tag <<< "$(echo zhang3 jun3 jun3)"
$ echo $name
zhang3
$ echo $tag
jun3 jun3

# 通过将 IFS 设置为 null 或空, 则不对读取的内容进行分词,read 一次读取一整行,赋值给 name.
$ IFS= read name tag <<< "zhang jun"
$ echo $name
zhang jun
$ echo $tag

$

# here-string, 作为命令的 stdin + newline 输入 hello world!
$ cat <<< "hello world! $(id)"
hello world! uid=0(root) gid=0(root) groups=0(root)
$

19 Redirections
#

所有类型的重定向(包括 here document 和 here string ) 都是 shell 的行为,shell 是在进行各种扩展/分词后, 才执行重定向操作/删除重定向命令行参数, 然后执行命令,命令不感知重定向。

重定向操作符是 simple command 的一部分 (可以简单理解为优先级比管道 | 高), 可以位于命令行的任意位置(甚至命令行只有重定向也是 OK 的)。

通过与 exec 连用,重定向可以改变当前执行环境 shell 的文件处理方式。

重定向功能:

  1. fd duplicated
  2. fd opened
  3. fd closed
  4. refter to other fd

重定向语法:

  1. [N]< WORD: 输入 fd N 从 WORD 对应的文件去读,N 忽略时默认为 1;
  2. [N]>[|] WORD: 输出 fd 的内容写入 WORD 对应的文件,N 忽略时默认为 2;
    • 未开启 shopt -s noclobber 时(默认), > 和 >| 都会覆盖 WORD 文件;
    • 开启 noclobber 后, 如果 WORD 文件已存在则报错,但是 >| 不保错,(>| 无论如何都会覆盖文件的)。
  3. [N]>> WORD: 输出 fd 的内容 append 到 WORD 对应的文件;
  4. &> WORD 或 >& WORD: 同时重定向 stdout 和 stderr 到 WORD 文件, 等效于 >WORD 2>&1;
  5. &>> WORD: 同时重定向 stdout 和 stderr 以 append 的模式写入 WORD 文件,等效于 >>WORD 2>&1
  6. [N]<&WORD 或 [N]>&WORD: 复制文件描述符,中间 不能有任何空格
    • WORD 展开的结果必须为 fd 数字,而且是 open for input 或 output 的,否则 bash 会报错。
    • N 是 WORD 展开的 fd 的拷贝, 即使后续 WORD 对应的 fd 有了新指向,N 也不受影响(也就是值复制, 而非引用)。
    • 如果 WORD 值为 -, 则表示关闭 N 对应文件描述符, 例如: 3>&-, 4<&-
  7. [N]<&DIGIT- 或 [N]>&DIGIT-: 移动文件描述符,将 DIGIT fd 赋值到 N, 然后关闭 DIGIT fd。
  8. [N]<>WORD: 打开文件描述符为 读写

注意:

  • 关闭文件描述符时 >&- 和 <&- 中间是 不能有任何空格的 ,文件描述符复制时 N>&M 或 N<&M 中间也是不能有任何空格的。N< WORD 或 N> WORD, WORD 前后是可以有空格的。
$ echo hello 2 > &1 # 错误: > 和 & 间有空格;
-bash: syntax error near unexpected token '&'
$ echo hello 2 >&1 # 错误: 2 和 > 中间有空格, 故不属于 FD
hello 2
$ echo hello 2>&1 # 正确
hello

# 在一个 sub shell 中执行 compound commands, 返回的 PID 为 sub shell 进程 PID。
$ coproc myproc3 { echo hello3; cat -; }
[1] 1145641
$ ps -lfH
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root     1145617 1144545  0  80   0 -  3524 do_wai 21:53 pts/0    00:00:00 -bash
1 S root     1145641 1145617  0  80   0 -  3491 do_wai 21:55 pts/0    00:00:00   -bash
0 S root     1145642 1145641  0  80   0 -  2823 pipe_w 21:55 pts/0    00:00:00     cat -
4 R root     1145646 1145617  0  80   0 -  3640 -      21:55 pts/0    00:00:00   ps -lfH
$ echo ${myproc3_PID}  # calling shell 可以通过 NAME_PID 来获得 sub shell PID;
1145641
$ echo hello cat >&${myproc3[1]}
$ cat <&${myproc3[0]}
hello3
hello cat
^C
# 关闭文件描述符
$ exec ${myproc3[1]}>&- # 错误: 这里的语义是 exec 60 > &- , 所以 exec 要执行 60 命令出错。
-bash: exec: 60: not found
$ exec {myproc3[1]}>&- # 正确:应该使用 exec {VARNAME}>&- 或 exec {VARNAME}<&- 来关闭变量 VARNAME 对应的 FD 的写入或读取。
$
[1]+  Done                    coproc myproc3 { echo hello3; cat -; }

重定向操作符后的 WORD 支持各种 shell 展开替换,如 brace expansion, tilde expansion, parameter expansion, command substitution, arithmetic expansion, quote removal, filename expansion, and word splitting. 但是展开后 必须为一个 WORD (文件名路径) , 否则报错。

# WORD 会被扩展/分词,所以如果结果不是一个 WORD, 否则 bash 报错。
$ read -r name age < $(echo "zhang jun")
-bash: $(echo "zhang jun"): ambiguous redirect

# 通过将结果 quote, 可以确保为一个 WORD, 但是 ubuntu 20.04.5 bash 5.0.17 的报错结果具有迷惑性:
$ cat < "$(echo zhang jun)"
-bash: $(echo zhang jun): No such file or directory
# 可以通过实现创建对应的文件来验证
$ touch 'zhang jun'
$ cat < "$(echo zhang jun)"

# 而 CentOS 7.9 bash 4.2.46 的结果比较友好
$ read -r name age < $(echo zhangjun)
bash: zhangjun: No such file or directory

# 关闭 1 和 2 fd
echo 1<&-
echo 2>&-

# pipeline stderr
# 对于 pipeline, shell 优先重定向 stdout, 所以 3>&1 实际是将 fd 3 指向 pipeline
find /var/log 3>&1 1>&2 2>&3- | foo.file

# pipeline stdout and stderr
exec 3>&1
command1 2>&1 >&3 3>&- | command2 3>&-
exec 3>&-

类似于 [N]< WORD 的 N 可以是数值或变量,如果是变量则必须使用 {VARNAME}< 或 {VARNAME}> 的形式,用于创建一个文件描述符变量 VARNAME, 保存 shell 为重定向生成的 FD。

由于重定向是命令行级别生效,为了在后续的执行环境中继续使用该 VARNAME,这种语法一般和 exec 连用,实现为当前执行 shell 环境打开和记录 FD 的作用。这种 {VARNAME} 的形式只在两种场景使用:

  1. 创建变量和保持重定向 FD: 第一次用来将将重定向的 FD 存入 VARNAME 中, 如 exec {VARNAME}> /path/to/file;
  2. 关闭该变量的 FD: exec {VARNAME}>&- 或 exec {VARNAME}<&-,这种场景不能使用 exec ${VARNAME}>&-;

exec [COMMAND ARGS] [REDIRECTIONS], 如果省略 COMMAND ARGS, 则 REDIRECTIONS 是针对当前执行 shell 的重定向, 否则只是针对当前执行 COMMAND 的重定向。

# https://unix.stackexchange.com/a/526728
# 临时保存 0/1/2 三个 fd 到 STDIN/STDOUT/STDERR
$ exec {STDIN}>&0
$ exec {STDOUT}>&1
$ exec {STDERR}>&2
$ echo $STDIN $STDOUT $STDERR
10 11 12

# do something

# 恢复 0/1/2 为原始值。
$ exec 0>&$STDIN
$ exec 1>&$STDOUT
$ exec 2>&$STDERR

$ exec {out}>out  # 为写入 out 文件创建一个 FD 变量,名为 out
$ echo $out       # out 变量中保存 FD
10
$ echo hello out  >&$out  # 向 $out 对应 FD 写入内容后, 实际写入 out 文件。
$ cat out
hello out
$ exec {out}>&-  # 必须使用 exec {VARNAME}>&- 或 exec {VARNAME}<&- 来关闭文件描述符
$ exec ${out}>&- # 错误
-bash: exec: 10: not found
$ exec {out} >out  # 错误:{VARNAME} 必须紧挨着重定向运算符 > 或 <
-bash: exec: {out}: not found
$ echo >&{out} # 错误:这会创建一个名为 {out} 的文件。

如果重定向操作符前使用 {VARNAME}, 则 shell 在做完重定向后, 将 fd 保存到 VARNAME 变量中:

  • {VARNAME}>N :是使用变量 VARNAME 中值作为文件描述符的标准语法。

# 为写入 out 文件创建一个 FD 变量,名为 out
$ exec {out}>out
# out 变量中保存 FD
$ echo $out
10
# 向 $out 对应 FD 写入内容后, 实际写入 out 文件。
$ echo hello out  >&$out
$ cat out
hello out
# 必须使用 exec {VARNAME}>&- 或 exec {VARNAME}<&- 来关闭文件描述符
$ exec {out}>&-
$ exec ${out}>&- # 错误
-bash: exec: 10: not found
$ exec {out} >out  # 错误:{VARNAME} 必须紧挨着重定向运算符 > 或 <
-bash: exec: {out}: not found
$ echo >&{out} # 错误:这会创建一个名为 {out} 的文件。

文件描述符复制和关闭:

  • 关闭文件描述符时 N>&- 和 N<&- 中间是 不能有任何空格的
  • 文件描述符复制时 N>&M 或 N<&M 中间也是不能有任何空格的。
  • N< WORD 或 N> WORD, WORD 和 文件描述符之间是可以有空格的。
$ echo 30 >&1  # 30 是 echo 的参数
30
$ echo hello 30>&1 # 为 echo 命令打开了 fd 30, 复制自 fd 1, 但是 echo 实际没有使用该 fd。
hello
$ echo hello >&30  # echo 命令没有先打开 fd 30,所以复制它出错。
-bash: 30: Bad file descriptor
$ echo hello 30>&2 1>&30  # 先为 echo 打开 fd 30,然后复制它,所以 OK;
hello
$ 30>&1               # 只是在该命令行上下文内临时打开了 fd 30
$ echo hello >&30
-bash: 30: Bad file descriptor

$ exec 30>&1          # 必须使用 exec 才能为当前执行 shell 打开新的 fd 并支持生效
$ echo hello >&30     #
hello
$ exec {my_fd}>&1
$ echo $my_fd
10
$ echo hello >&${my_fd}
hello
$ exec ${my_fd}>&-
-bash: exec: 10: not found
$ exec 10>&-
$ echo hello >&${my_fd}
-bash: ${my_fd}: Bad file descriptor
$ echo ${my_fd}
-bash: echo: write error: Bad file descriptor
$ echo hello
-bash: echo: write error: Bad file descriptor
$ exec 1>&2
$ echo hello
hello
$

Bash 支持读写 TCP/UDP 接口,可以使用 读写重定向 FD 来读写 socket:

$ ls /dev/tcp # 虽然不存在,但是没关系。
ls: cannot access '/dev/tcp': No such file or directory

# 获取 SSH Server 版本
$ cat </dev/tcp/jp.opsnull.com/22
SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2

# 探测端口存活
$ echo >/dev/tcp/jp.opsnull.com/443
$ echo >/dev/tcp/jp.opsnull.com/444
^C-bash: connect: Interrupted system call
-bash: /dev/tcp/jp.opsnull.com/444: Interrupted system call

# 从 FD 3 读写 Socket
$ exec 3<>/dev/tcp/xmodulo.com/80

# printf "GET / HTTP/1.1\n\n" > /dev/tcp/google.com/80
$ echo -e "GET / HTTP/1.1rnhost: xmodulo.comrnConnection: closernrn" >&3
$ timeout 1 cat <&3
HTTP/1.1 400 Bad Request
Date: Sun, 02 Apr 2023 09:11:50 GMT
Server: Apache
Content-Length: 226
Connection: close
Content-Type: text/html; charset=iso-8859-1
# 关闭 FD 3 的 read
exec 3 <&-
# 关闭 FD 3 的 write
exec 3 >&-

bash 重定向时的特殊文件:

Bash handles several filenames specially when they are used in redirections, as described in the following table:

      /dev/fd/fd
           If fd is a valid integer, file descriptor fd is duplicated.
      /dev/stdin
           File descriptor 0 is duplicated.
      /dev/stdout
           File descriptor 1 is duplicated.
      /dev/stderr
           File descriptor 2 is duplicated.

      /dev/tcp/host/port
           If host is a valid hostname or Internet address,
           and port is an integer port number or service
           name, bash attempts to open a TCP connection to
           the corresponding socket.
      /dev/udp/host/port
           If host is a valid hostname or Internet address,
           and port is an integer port number or service
           name, bash attempts to open a UDP connection to
           the corresponding socket.

20 Command Execution
#

20.1 Simple Command Expansion
#

Simple Command 的执行顺序:

  1. 标记 varirable assignments 和 redirection 的 word 为延后处理;
  2. 扩展其它类型的 word 和分词, 扩展后的第一个 word 为 command name, 其它为它的参数;
  3. 重定向;
  4. 变量赋值扩展;

如果结果没有 command name, 则变量赋值影响当前 shell 环境,否则被添加到执行的 command name 的环境(当前 shell 环境不受影响)。

如果对 readonly 的变量赋值则报错,所在 command 以 non-zero 值退出。

即使结果没有 command name, 也会执行重定向操作, 但是不会影响当前 shell 环境。

$ name=value # 变量被加到当前 shell 环境
$ name=value date # 变量被加到执行的 date 命令的环境变量中,而不加到当前 shell 环境
$ >/dev/null
$ >/dev/null date

20.2 Command Search and Execution
#

如果 command name 没有包含 slash, 则 shell 负责 locate 它,否则根据 slash 路径来执行它。

如果有 function name 匹配该 name, 则调用该 函数 。如果 name 不是 function name, 则 shell 在 builtins 中查找,如果找到则执行 builtin。

func name 的优先级比 builtins 和 PATH 中命令高

如果 name 不是 function name 和 builtins, 则 shell 在 $PATH 变量对应的目录列表中寻找 name, 没找到则返回 127 退出码。

shell 使用 hash table 来记录找到的 name 对应的 full path, 这样避免每次都搜索 PATH。如果没有找到 name, 则每次都搜索 PATH;

找到 command 后, 在另一个执行环境(EXECUTION ENVIRONMENT)中执行命令, Argument 0 是给出的 name, 其它为它的参数。

如果文件因为不是 executable format 执行失败(不是因为文件没有执行权限),则 shell 认为它是 shell script, 从而重新执行它。

如果命令不是异步执行,则 shell 等待它执行退出,且收集它的退出状态;

20.3 Command Execution Environment
#

shell 的 EXECUTION ENVIRONMENT 包括:

  • open files inherited by the shell at invocation, as modified by redirections supplied to the ’exec’ builtin
  • the current working directory as set by ‘cd’, ‘pushd’, or ‘popd’, or inherited by the shell at invocatio
  • the file creation mode mask as set by ‘umask’ or inherited from the shell’s parent
  • current traps set by ’trap’
  • shell parameters that are set by variable assignment or with ‘set’or inherited from the shell’s parent in the environment
  • shell functions defined during execution or inherited from the shell’s parent in the environmen
  • options enabled at invocation (either by default or with command-line arguments) or by ‘set’
  • options enabled by ‘shopt’ (*note The Shopt Builtin::)
  • shell aliases defined with ‘alias’ (*note Aliases::)
  • various process IDs, including those of background jobs (*noteLists::), the value of ‘$$’, and the value of ‘$PPID’

When a simple command other than a builtin or shell function is to be executed, it is invoked in a separate execution environment that consists of the following. Unless otherwise noted, the values are inherited from the shell.

shell 执行命令或子 sehll时,继承的环境:

  • the shell’s open files, plus any modifications and additions specified by redirections to the command
  • the current working directory
  • the file creation mode mask
  • shell variables and functions marked for export, along with variables exported for the command, passed in the environment (*note Environment::)
  • traps caught by the shell are reset to the values inherited from the shell’s parent, and traps ignored by the shell are ignored

set 和 shopt 设置的参数并没有被 sub shell 继承。

$(xxx), (xxx) 也是在 sub shell 中执行,但是该 sub shell 是执行 shell 的 dup, 所以执行 shell 中未 export 的变量和函数,都可以在该 sub shell 中读取(不能写)和执行。

A command invoked in this separate environment cannot affect the shell’s execution environment.

Command substitution, commands grouped with parentheses, and asynchronous commands are invoked in a subshell environment that is a duplicate of the shell environment , except that traps caught by the shell are reset to the values that the shell inherited from its parent at invocation. Builtin commands that are invoked as part of a pipeline are also executed in a subshell environment. Changes made to the subshell environment cannot affect the shell’s execution environment.

Subshells spawned to execute command substitutions inherit the value of the ‘-e’ option from the parent shell. When not in POSIX mode, Bash clears the ‘-e’ option in such subshells.

If a command is followed by a ‘&’ and job control is not active, the default standard input for the command is the empty file '/dev/null' . Otherwise, the invoked command inherits the file descriptors of the calling shell as modified by redirections.

20.4 Environment
#

When a program is invoked it is given an array of strings called the ENVIRONMENT. This is a list of name-value pairs, of the form ’name=value’.

Bash provides several ways to manipulate the environment. On invocation, the shell scans its own environment and creates a parameter for each name found, automatically marking it for EXPORT to child processes. Executed commands inherit the environment. The 'export' and 'declare -x' commands allow parameters and functions to be added to and deleted from the environment.

If the value of a parameter in the environment is modified, the new value becomes part of the environment, replacing the old. The environment inherited by any executed command consists of the shell’s initial environment, whose values may be modified in the shell, less any pairs removed by the ‘unset’ and ’export -n’ commands, plus any additions via the ’export’ and ‘declare -x’ commands.

The environment for any simple command or function may be augmented temporarily by prefixing it with parameter assignments, as described in *note Shell Parameters::. These assignment statements affect only the environment seen by that command.

If the ‘-k’ option is set (*note The Set Builtin::), then all parameter assignments are placed in the environment for a command, not just those that precede the command name.

When Bash invokes an external command, the variable '$_' is set to the full pathname of the command and passed to that command in its environment.

20.5 Exit Status
#

The exit status of an executed command is the value returned by the WAITPID system call or equivalent function. Exit statuses fall between 0 and 255, though, as explained below, the shell may use values above 125 specially. Exit statuses from shell builtins and compound commands are also limited to this range. Under certain circumstances, the shell will use special values to indicate specific failure modes.

For the shell’s purposes, a command which exits with a zero exit status has succeeded. A non-zero exit status indicates failure. This seemingly counter-intuitive scheme is used so there is one well-defined way to indicate success and a variety of ways to indicate various failure modes. When a command terminates on a fatal signal whose number is N, Bash uses the value 128+N as the exit status.

If a command is not found, the child process created to execute it returns a status of 127. If a command is found but is not executable, the return status is 126.

If a command fails because of an error during expansion or redirection, the exit status is greater than zero.

The exit status is used by the Bash conditional commands (*note Conditional Constructs::) and some of the list constructs (*note Lists::).

All of the Bash builtins return an exit status of zero if they succeed and a non-zero status on failure, so they may be used by the conditional and list constructs. All builtins return an exit status of 2 to indicate incorrect usage, generally invalid options or missing arguments.

21 进程组、信号和作业控制
#

simple command 的 return status(exit status) 分 3 种:

  1. shell 不具有执行命令的权限权限(126) 或未找到命令 (127) 等出错;
  2. 命令自身的退出码;
  3. 被 signal 中止时退出码是 128+N(N 为 signal 编号, 从 0 开始);

所以,为了区分自身退出和信号退出,命令自身的退出码应该位于 0-125。

对于 calling shell ignored 的 SIGNAL trap, 子 shell 或子进程也会忽略该信号。但是 call shell 处理或捕获的 trap SIGNAL, 子 shell 或子进程不会继承,而是重设置为默认系统处理状态。

21.1 进程组
#

交互式 shell 在每次执行 pipeline 时都创建一个新的进程组,进程组 ID 为 pipeline 第一个进程的 PID:

  • 交互式 shell 支持作业控制,而非交互式 shell 不支持作业控制;
    • 非交互式 shell:如执行 shell 脚本的 shell。
  • 只有支持作业控制时,shell 才会对执行的 pipeline 创建不同的进程组 。否则,例如执行 shell 脚本的非交互式 shell,它本身和创建的所有进程(不管是否异步)都位于一个进程组中。
$ sleep 2000 | sleep 20001 &
[1] 1097949
$ sleep 3000 | sleep 3001

$ ps  -eH -o pid,ppid,pgid,sid,args  |grep -E 'PID|test2.sh|sleep'
    PID    PPID    PGID     SID COMMAND
1097971 1096646 1097970 1096646         grep --color=auto -E PID|test2.sh|sleep
1097961 1097599 1097961 1097599         sleep 2000
1097962 1097599 1097961 1097599         sleep 20001
1097967 1097599 1097967 1097599         sleep 3000
1097968 1097599 1097967 1097599         sleep 3001

shell 使用 & ; && || 将一行命令分割为多个 pipeline, 所以对于一行命令可能会 先后创建多个进程组 , 但是同一时刻只能 有一个进程组占有控制终端

可以有多个进程组连接同一控制终端 ,例如下面有三个进程组连接了 pts/0 终端。但是只有 一个进程组 1121398 占有控制终端 pts/0,这可以通过 TPGID 字段输出的 PGID 来确认,也可以通过 stat 的 + 字符来确认, 该进程组为 前台进程组(同时只能有一个,该组中的进程占有控制终端) ,其它进程组为后台进程组。

$ sleep 100 & sleep 200 & sleep 300 && sleep 400 && read
[1] 1121135
[2] 1121136

$ ps  -eH -o pid,ppid,pgid,tty,tpgid,stat,args  |grep -E 'PID|test2.sh|sleep'
    PID    PPID    PGID TT         TPGID STAT COMMAND
1121396 1121073 1121396 pts/0    1121398 S            sleep 100
1121397 1121073 1121397 pts/0    1121398 S            sleep 200
1121398 1121073 1121398 pts/0    1121398 S+           sleep 300

使用 test -t FD 来测试 FD 是否是控制终端

$ [ -t 1 ] && echo true || echo false
true
$ [ -t 4 ] && echo true || echo false
false

对于非交互式 shell,如执行脚本的 shell,它自身和所有启动的任务(不区分是否后台执行)都位于 同一个进程组中 ,脚本启动的所有任务 PGID 相同, 且与 TGPID 一致。如果这个 shell 脚本是交互式 shell 执行的, 那整个脚本的 所有任务都为于前台进程组中

$ cat test2.sh
sleep 1000 | sleep 1001 | sleep 1002 &
sleep 2000 &
sleep 3000 | sleep 3001

# 交互式执行脚本,脚本 shell 和内部启动的所有命令(不管是否异步)都位于一个前台进程组
# 中
$ bash test2.sh

$ ps  -eH -o pid,ppid,pgid,tty,tpgid,stat,args  |grep -E 'PID|test2.sh|sleep'
    PID    PPID    PGID TT         TPGID STAT COMMAND
1121501 1121073 1121501 pts/0    1121501 S+           bash test2.sh
1121502 1121501 1121501 pts/0    1121501 S+             sleep 1000
1121503 1121501 1121501 pts/0    1121501 S+             sleep 1001
1121504 1121501 1121501 pts/0    1121501 S+             sleep 1002
1121505 1121501 1121501 pts/0    1121501 S+             sleep 2000
1121506 1121501 1121501 pts/0    1121501 S+             sleep 3000
1121507 1121501 1121501 pts/0    1121501 S+             sleep 3001

kill -0 PID 可用于检查 PID 对应的进程组是否存在。

21.2 控制终端信号
#

当用户在控制终端敲 C-c 时,终端驱动 只会向前台进程组的所有进程 发送 SIGINT 信号,后台进程组不受影响。特殊情况是 shell 脚本内启动后台任务前,会为其屏蔽 SIGINT/SIGQUIT 信号

所以当 C-c 正在执行的 test2.sh 脚本时, 虽然整个脚本所有任务都位于前台进程组中,但只有正在执行的 sleep 3000 进程被 kill, 其它后台进程正常运行:

$ ps  -eH -o pid,ppid,pgid,tty,tpgid,stat,args  |grep -E 'PID|test2.sh|sleep'
    PID    PPID    PGID TT         TPGID STAT COMMAND
1121502       1 1121501 pts/0    1121073 S      sleep 1000
1121503       1 1121501 pts/0    1121073 S      sleep 1001
1121504       1 1121501 pts/0    1121073 S      sleep 1002
1121505       1 1121501 pts/0    1121073 S      sleep 2000

总结:后台任务不受终端的 SIGINT/SIGQUIT 信号影响。

交互式 shell 忽略 SIGTERM 信号 ,但捕获并处理 SIGINT 信号并打破任何 exec loops。非交互式 shell 会被 SIGTERM 信号终止(子进程被 init 进程接管):

$ ps -elfH
4 S root         686       1  0  80   0 -  3044 poll_s  2022 ?        00:00:00   sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
4 S root     1141452     686  0  80   0 -  3452 -      10:24 ?        00:00:00     sshd: root@pts/0,pts/1,pts/2
4 S root     1141498 1141452  0  80   0 -  3491 do_wai 10:24 pts/0    00:00:00       -bash
0 S root     1141529 1141498  0  80   0 -  2787 hrtime 10:25 pts/0    00:00:00         sleep 10000

0 S root     1141530 1141498  0  80   0 -  3260 do_wai 10:25 pts/0    00:00:00         bash
0 S root     1141539 1141530  0  80   0 -  2787 hrtime 10:25 pts/0    00:00:00           sleep 2000
0 S root     1141540 1141530  0  80   0 -  2787 hrtime 10:25 pts/0    00:00:00           sleep 3000
4 S root     1141515 1141452  0  80   0 -  3524 do_wai 10:24 pts/1    00:00:00       -bash

0 S root     1141600 1141515  0  80   0 -  3141 do_wai 10:26 pts/1    00:00:00         bash test2.sh
0 S root     1141601 1141600  0  80   0 -  2787 hrtime 10:26 pts/1    00:00:00           sleep 11000
0 S root     1141602 1141600  0  80   0 -  2787 hrtime 10:26 pts/1    00:00:00           sleep 11001
0 S root     1141603 1141600  0  80   0 -  2787 hrtime 10:26 pts/1    00:00:00           sleep 11002
0 S root     1141604 1141600  0  80   0 -  2787 hrtime 10:26 pts/1    00:00:00           sleep 12000
0 S root     1141605 1141600  0  80   0 -  2787 hrtime 10:26 pts/1    00:00:00           sleep 13000
0 S root     1141606 1141600  0  80   0 -  2787 hrtime 10:26 pts/1    00:00:00           sleep 13001

# 向交互式 shell 发送 SIGTERM 信号
$ kill 1141530
# 交互式 shell 忽略 SIGTERM 信号
$ ps -elfH
4 S root     1141452     686  0  80   0 -  3452 -      10:24 ?        00:00:00     sshd: root@pts/0,pts/1,pts/2
4 S root     1141498 1141452  0  80   0 -  3491 do_wai 10:24 pts/0    00:00:00       -bash
0 S root     1141529 1141498  0  80   0 -  2787 hrtime 10:25 pts/0    00:00:00         sleep 10000
0 S root     1141530 1141498  0  80   0 -  3260 do_wai 10:25 pts/0    00:00:00         bash
0 S root     1141539 1141530  0  80   0 -  2787 hrtime 10:25 pts/0    00:00:00           sleep 2000
0 S root     1141540 1141530  0  80   0 -  2787 hrtime 10:25 pts/0    00:00:00           sleep 3000

# 向非交互式 shell 发送 SIGTERM 信号。
$ kill -TERM 1141600
# shell 进程退出,它的子进程被 init 接管。
$ ps  -eH -o pid,ppid,pgid,tty,tpgid,stat,args  |grep -E 'PID|test2.sh|sleep'
    PID    PPID    PGID TT         TPGID STAT COMMAND
1141529 1141498 1141529 pts/0    1141540 S            sleep 10000
1141539 1141530 1141539 pts/0    1141540 S              sleep 2000
1141540 1141530 1141540 pts/0    1141540 S+             sleep 3000
1141621 1141607 1141620 pts/2    1141620 S+           grep --color=auto -E PID|test2.sh|sleep
1141601       1 1141600 pts/1    1141515 S      sleep 11000
1141602       1 1141600 pts/1    1141515 S      sleep 11001
1141603       1 1141600 pts/1    1141515 S      sleep 11002
1141604       1 1141600 pts/1    1141515 S      sleep 12000
1141605       1 1141600 pts/1    1141515 S      sleep 13000
1141606       1 1141600 pts/1    1141515 S      sleep 13001
$

交互式 shell 启动的后台任务读终端时会被 STOP, 但是非交互式 shell 如脚本启动的后台任务读终端时对应 /dev/null:

# job control 生效的情况下,异步命令读取 stdin 时会被 STOP
$ cat &
[1] 13426
[1]+  Stopped                 cat

$ echo aa && vim & echo bb
[1] 47605
bb
aa
[1]+  Stopped                 echo aa && vim

$ jobs
[1]+  Stopped                 echo aa && vim
$

# bash -c 的 cmd 是在 job control 未生效的 非交互式 shell 执行的,所以独到 /dev/null
$ bash -c 'cat <(echo -n a) - <(echo -n b) &'
ab$

Bash 默认忽略 SIGQUIT 信号。

21.3 作业控制
#

作业控制(job control) 是 shell 的一个特性,可以自动开启和关闭:

  1. 交互式 shell 默认开启;
  2. 非交互式 shell (如 shell 脚本) 默认关闭;

在 job control 生效的情况下,shell 忽略 SIGTTIN/SIGTTOUT/SIGTSTP 信号。命令替换中的命令忽略 SIGTTIN/SIGTTOU/SIGTSTP 信号。

开启 job control 后,才会为不同的 pipeline 创建不同的进程组,而且 shell 可以将这些进程组放到前台或后台运行,前台运行的进程组是占有 控制终端

如交互式 shell 默认开启 job control, 将命令行命令和参数按 pipeline 进行分组, 一个 pipeline 对应一个进程组 ,例如:date & date & id |grep root && whoami && sleep 10 && date 对应 6 个进程组。 这些进程组都连接控制终端 , 但是同一时刻只能有一个进程组占有控制终端,称为 前台进程组

未开启 job control 的 shell, 不会动态创建进程组 ,不管前台还是后台 pipeline 都位于一个脚本 shell 对应的进程组中 。例如 shell 脚本和 bash -c “xxx” 执行的命令都是关闭了 job control 的 shell 环境。脚本 shell 和它执行的所有命令都位于一个进程组中,进程组 ID 为脚本 shell PID。

支持进程组主要目的是对一组进程进行管理,例如 shell 会同时启动 pipeline 中的所有命令,后续 C-c 时会向进程组发 SIGKILL 信号,内核 kill 进程组中所有进程。后台进程组不受终端信号 SIGINT/SIGQUIT 影响。

$ ssh devops
Last login: Sun Apr  2 18:14:02 2023 from 120.244.142.45
# 交互式登录 shell, 使用 pts/1 终端
$ echo $0
-bash
# 创建多级交互式子 shell, 都连接到 pts/1 终端
$ bash
$ sleep 1000 &
[1] 1094485
$ bash
$ sleep 2000 &
[1] 1094496
$ bash
$ ps -lfH
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root     1094465 1094037  0  80   0 -  3491 do_wai 18:14 pts/1    00:00:00 -bash
0 S root     1094476 1094465  0  80   0 -  3227 do_wai 18:14 pts/1    00:00:00   bash
0 S root     1094485 1094476  0  80   0 -  2787 hrtime 18:14 pts/1    00:00:00     sleep 1000
0 S root     1094486 1094476  0  80   0 -  3227 do_wai 18:14 pts/1    00:00:00     bash
0 S root     1094496 1094486  0  80   0 -  2787 hrtime 18:14 pts/1    00:00:00       sleep 2000
0 S root     1094497 1094486  0  80   0 -  3227 do_wai 18:14 pts/1    00:00:00       bash
4 R root     1094507 1094497  0  80   0 -  3640 -      18:14 pts/1    00:00:00         ps -lfH

21.4 交互式登录 shell 和交互式 shell 正常退出
#

例如执行 exit/C-d,它启动的后台任务 被 init 1 接管 且能正常运行:

$ ssh devops
Last login: Sun Apr  2 18:14:02 2023 from 120.244.142.45
$ echo $0 # 交互式登录shell, 使用 pts/1 终端
-bash
$ bash    # 交互式子 shell
$ sleep 1000 & # 后台任务
[1] 1094485
$ bash
$ sleep 2000 &
[1] 1094496
$ bash
$ ps -lfH
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root     1094465 1094037  0  80   0 -  3491 do_wai 18:14 pts/1    00:00:00 -bash
0 S root     1094476 1094465  0  80   0 -  3227 do_wai 18:14 pts/1    00:00:00   bash
0 S root     1094485 1094476  0  80   0 -  2787 hrtime 18:14 pts/1    00:00:00     sleep 1000
0 S root     1094486 1094476  0  80   0 -  3227 do_wai 18:14 pts/1    00:00:00     bash
0 S root     1094496 1094486  0  80   0 -  2787 hrtime 18:14 pts/1    00:00:00       sleep 2000
0 S root     1094497 1094486  0  80   0 -  3227 do_wai 18:14 pts/1    00:00:00       bash
4 R root     1094507 1094497  0  80   0 -  3640 -      18:14 pts/1    00:00:00         ps -lfH
$ exit
$ exit
$ exit
$ ps -lf
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root     1094465 1094037  0  80   0 -  3491 do_wai 18:14 pts/1    00:00:00 -bash
0 S root     1094485       1  0  80   0 -  2787 hrtime 18:14 pts/1    00:00:00 sleep 1000
0 S root     1094496       1  0  80   0 -  2787 hrtime 18:14 pts/1    00:00:00 sleep 2000
4 R root     1094515 1094465  0  80   0 -  3640 -      18:15 pts/1    00:00:00 ps -lf
$ logout
$ ssh devops
$ ps -elfH |grep sleep -A 3
--
0 S root     1094485       1  0  80   0 -  2787 hrtime 18:14 ?        00:00:00   sleep 1000
0 S root     1094496       1  0  80   0 -  2787 hrtime 18:14 ?        00:00:00   sleep 2000
$

21.5 kill 交互式非登录 shell
#

例如 kill 正在执行脚本的 shell,该 shell 脚本启动的后台进程会被 init 接管且能正常运行:

# kill 交互式 shell 后,后台任务正常运行。
$ ps -lfH
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root     1095060 1094956  0  80   0 -  3491 do_wai 19:31 pts/0    00:00:00 -bash
0 S root     1095071 1095060  0  80   0 -  2787 hrtime 19:31 pts/0    00:00:00   sleep 100
0 S root     1095072 1095060  0  80   0 -  3227 do_wai 19:31 pts/0    00:00:00   bash
0 S root     1095081 1095072  0  80   0 -  2787 hrtime 19:31 pts/0    00:00:00     sleep 200
0 S root     1095082 1095072  0  80   0 -  3227 do_wai 19:31 pts/0    00:00:00     bash
0 S root     1095091 1095082  0  80   0 -  2787 hrtime 19:32 pts/0    00:00:00       sleep 300
4 R root     1095093 1095082  0  80   0 -  3640 -      19:32 pts/0    00:00:00       ps -lfH
$ kill -9 1095072
$ Killed
$
exit
$
$ ps -elfH |grep -E 'sh|sleep'
4 S root         686       1  0  80   0 -  3044 poll_s  2022 ?        00:00:00   sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
4 S root     1094037     686  0  80   0 -  3452 poll_s 17:10 ?        00:00:00     sshd: root@pts/1
4 S root     1094653 1094037  0  80   0 -  3491 poll_s 18:31 pts/1    00:00:00       -bash
4 S root     1094956     686  0  80   0 -  3451 poll_s 19:28 ?        00:00:00     sshd: root@pts/0
4 S root     1095060 1094956  0  80   0 -  3491 do_wai 19:31 pts/0    00:00:00       -bash
0 S root     1095071 1095060  0  80   0 -  2787 hrtime 19:31 pts/0    00:00:00         sleep 100
0 S root     1095106 1095060  0  80   0 -  3026 pipe_w 19:33 pts/0    00:00:00         grep --color=auto -E sh|sleep
4 S root      662283       1  0  80   0 - 203605 futex_ Feb06 ?       00:27:45   /usr/local/share/aliyun-assist/2.2.3.398/aliyun-service
4 S root      662508       1  0  80   0 -  4450 futex_ Feb06 ?        00:06:53   /usr/local/share/assist-daemon/assist_daemon
0 S root     1095081       1  0  80   0 -  2787 hrtime 19:31 pts/0    00:00:00   sleep 200
0 S root     1095091       1  0  80   0 -  2787 hrtime 19:32 pts/0    00:00:00   sleep 300
$

21.6 kill 交互式登录 shell
#

分两种情况:

# 1. 如果直接子进程不能被 init 接管,则 kill 交互式登录 shell 后,所有子进程都被 kill
$ ps -lfH
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root     1094574 1094037  0  80   0 -  3491 do_wai 18:27 pts/2    00:00:00 -bash
0 S root     1094614 1094574  0  80   0 -  3260 do_wai 18:29 pts/2    00:00:00   bash
0 S root     1094623 1094614  0  80   0 -  2787 hrtime 18:29 pts/2    00:00:00     sleep 1111
0 S root     1094626 1094614  0  80   0 -  2787 hrtime 18:29 pts/2    00:00:00     sleep 2222
4 R root     1094628 1094614  0  80   0 -  3640 -      18:29 pts/2    00:00:00     ps -lfH
$ kill -9 1094574
$ Shared connection to 8.130.17.11 closed.
$ ssh devops
$ ps -elfH |grep sleep
0 S root     1094645 1094633  0  80   0 -  3026 pipe_w 18:30 pts/0    00:00:00         grep --color=auto sleep

# 2. 如果直接子进程(如 sleep 1000)能被 init 接管,则 kill 交互式登录 shell 后这些进程
# 还能正常运行。
$ ps -lfh
4     0 1097483 1094956  20   0  13964  5224 do_wai Ss   pts/0      0:00 -bash
0     0 1097494 1097483  20   0  11148   580 hrtime S    pts/0      0:00  \_ sleep 1000
0     0 1097495 1097483  20   0  13040  4148 do_wai S    pts/0      0:00  \_ bash
0     0 1097504 1097495  20   0  11148   580 hrtime S    pts/0      0:00      \_ sleep 2000
0     0 1097505 1097495  20   0  11148   580 hrtime S    pts/0      0:00      \_ sleep 2200
0     0 1097507 1097495  20   0  13040  4176 do_wai S    pts/0      0:00      \_ bash
0     0 1097516 1097507  20   0  11148   580 hrtime S    pts/0      0:00          \_ sleep 3000
0     0 1097517 1097507  20   0  11148   580 hrtime S    pts/0      0:00          \_ sleep 3300
0     0 1097522 1097507  20   0  12696  3564 do_wai S    pts/0      0:00          \_ bash test.sh
0     0 1097536 1097522  20   0  11148   580 hrtime S    pts/0      0:00          |   \_ sleep 1
0     0 1097524 1097507  20   0  13040  4016 do_wai S    pts/0      0:00          \_ bash
0     0 1097534 1097524  20   0  11148   580 hrtime S    pts/0      0:00              \_ sleep 4000
4     0 1097537 1097524  20   0  14480  3104 -      R+   pts/0      0:00              \_ ps -lfh
4     0 1096646 1094956  20   0  14096  5848 poll_s Ss+  pts/3      0:00 -bash
$ kill -9 1097483
$ Shared connection to 8.130.17.11 closed.
$ ssh devops
Last login: Sun Apr  2 21:26:10 2023 from 120.244.142.45
$ ps -elfH |grep sleep
0 S root     1097554 1097543  0  80   0 -  3026 pipe_w 21:27 pts/1    00:00:00         grep --color=auto sleep
0 S root     1097494       1  0  80   0 -  2787 hrtime 21:26 ?        00:00:00   sleep 1000
0 S root     1097504       1  0  80   0 -  2787 hrtime 21:26 ?        00:00:00   sleep 2000
0 S root     1097505       1  0  80   0 -  2787 hrtime 21:26 ?        00:00:00   sleep 2200
0 S root     1097516       1  0  80   0 -  2787 hrtime 21:26 ?        00:00:00   sleep 3000
0 S root     1097517       1  0  80   0 -  2787 hrtime 21:26 ?        00:00:00   sleep 3300

可见当 kill shell 或父进程时, 子进程不一定都退出, 父进程也不会传递 KILL 信号给子进程, 保险起见可以向进程组发送信号, 这时内核会向组内的所有进程发送信号:

  1. 向 job 编号发送信号: kill %N, 如 kill %1
  2. 向负的进程组 ID 发送信号: kill -TGID, 如 kill -9 -123456

如果交互式登录 shell 不是由于收到 SIGHUP 信号而退出, 例如正常的 C-d/exit 或者被 TERM/KILL 退出, 默认是不会向所有子进程发送 SIGHUP 信号的,也不会传递 SIGTERM/SIGKILL/SIGINT 信号给子进程,所以它们都还正常运行 。但是开启 Bash 特性 huponexit 后 , 交互式登录 shell 进程退出时会向所有子进程任务发送 SIGHUP 信号从而导致它们中止。

21.7 SSH 连接断开
#

使用 SSH 连接到远程服务器后,当网络连接丢失时,SSH 守护程序会关闭登录会话并 发送 SIGHUP(挂起)信号到登录 shell 进程

  1. 对于交互式 shell(不管是否是登录 shell ), 收到 SIGHUP 信号时会退出,退出前会给它启动的 所有 jobs (不管 running 还是 stopped, 只要还位于它的子进程树中) 发送 SIGHUP 信号。
  2. 对于以前由该 shell 或子进程创建, 但是当前已经 不在它子进程树中的进程 , 例如父进程为 init 1, 则 shell 退出前不会向它发送 SIGHUP 信号。
sleep 1000
Ctrl-Z
bg
disown
$ ps -e -H -opid,ppid,sid,tgid,cmd  |grep -E 'bash|sleep'
1096646 1094956 1096646 1096646       -bash
1097130 1094956 1097130 1097130       -bash
1097147 1097130 1097130 1097147         sleep 1000
1097148 1097130 1097130 1097148         bash
1097158 1097148 1097130 1097158           sleep 2000
1097159 1097148 1097130 1097159           bash
1097168 1097159 1097130 1097168             sleep 3000
1097169 1097159 1097130 1097169             bash
1097178 1097169 1097130 1097178               sleep 4000
1097188 1097169 1097130 1097188               grep --color=auto -E bash|sleep
# 退出最后一个 bash
$ exit
# sleep 4000 父进程为 1, 不再位于上面进程树中(但是 sid 还是登录 shell)
$ ps -e -H -opid,ppid,sid,tgid,cmd  |grep -E 'bash|sleep'
1096646 1094956 1096646 1096646       -bash
1097130 1094956 1097130 1097130       -bash
1097147 1097130 1097130 1097147         sleep 1000
1097148 1097130 1097130 1097148         bash
1097158 1097148 1097130 1097158           sleep 2000
1097159 1097148 1097130 1097159           bash
1097168 1097159 1097130 1097168             sleep 3000
1097192 1097159 1097130 1097192             grep --color=auto -E bash|sleep
1097178       1 1097130 1097178   sleep 4000
# 向交互式 shell (1097148, 非登录交互式 shell) 发送 HUP 信号
$ kill -HUP 1097148
$ Hangup
# 1097148 退出前, 向它的所有子进程发送发送 HUP 信号导致它们退出。
$ ps -e -H -opid,ppid,sid,tgid,cmd  |grep -E 'bash|sleep'
1096646 1094956 1096646 1096646       -bash
1097130 1094956 1097130 1097130       -bash
1097147 1097130 1097130 1097147         sleep 1000
1097198 1097130 1097130 1097198         grep --color=auto -E bash|sleep
1097178       1 1097130 1097178   sleep 4000
$

总结: shell 收到 SIGHUP 信号时,会传递 SIGHUP 信号给子进程,但是不会传递 SIGTERM/SIGKILL/SIGINT 等信号。

21.8 disown 和 nohup
#

由于 SSH 连接端口后,sshd 会给登录 shell 发送 SIGHUP 信号,登录 shell 再将该信号发送给所有启动的进程,所以影响在后台执行的进程。

disown 是 shell 的内置命令,用于将 当前交互式 shell 已经启动的异步任务 jobs 从 shell 内部维护的 jobs 列表中去掉。这样后续当交互式 shell 收到 SIGHUP 信号时,遍历自己的 jobs 列表时就会 忽略已经去掉的 job ,这样就不会给它发送 SIGHUP 信号,从而避免被终止。

  • disown 必须在交换式环境中使用,而且是去掉所在回话启动的 jobs。 它不能去掉其它回话启动的 jobs。

disown 的主要使用场景是在登录 shell 中后台运行长时间的 jobs,在退出登录 shell 前将该 jobs 从登录 shell 回话中移除。

alizj@lima-dev2:~$ type disown
disown is a shell builtin

nohup 的原理和 disown 不同,它是依赖于父进程忽略 SIGHUP 信号, 这个忽略会被子进程继承来实现的。

21.9 su/sudo 会传递信号
#

shell 在收到 TERM/KILL 信号时并不会传递。但是 su/sudo 不同,它们会将收到的信号传递给子进程:

$ ps -elfH |grep -E 'sudo|2000'
0 S root     1141895 1141515  0  80   0 -  2993 pipe_w 11:07 pts/1    00:00:00         grep --color=auto -E sudo|2000
4 S root     1141886 1141741  0  80   0 -  3786 poll_s 11:07 pts/2    00:00:00         sudo sleep 2000
4 S root     1141888 1141886  0  80   0 -  2787 hrtime 11:07 pts/2    00:00:00           sleep 2000
0 S root     1141604       1  0  80   0 -  2787 hrtime 10:26 pts/1    00:00:00   sleep 12000

$ kill 1141886  # kill sudo 后,sudo kill 所有它启动的命令

$ ps -elfH |grep -E 'sudo|2000'
0 S root     1141898 1141515  0  80   0 -  2993 pipe_w 11:07 pts/1    00:00:00         grep --color=auto -E sudo|2000
0 S root     1141604       1  0  80   0 -  2787 hrtime 10:26 pts/1    00:00:00   sleep 12000

22 Shell Scripts
#

shell fork 一个 sub shell 来执行脚本,所以 sub shell 是脚本中所有命令的父进程。可以在脚本的最后通过 exec 来替换掉 sub shell 进程为指定的命令。

执行脚本的 shell 是 不支持作业控制的非交互式 shell, 所以脚本运行的所有命令(不管前台还是后台)都 位于一个进程组中

  • 同时 !命令行历史记录机制,alias 机制都失效。

在终端交互执行脚本时,该脚本的进程组为前台进程组。当用户在控制终端敲 C-c 时,终端驱动 只会向前台进程组的所有进程 发送 SIGINT 信号,后台进程组不受影响。特殊情况是 shell 脚本内启动后台任务前,会为其屏蔽 SIGINT/SIGQUIT 信号 。 所以当 C-c 正在执行的 test2.sh 脚本时, 虽然整个脚本所有任务都位于前台进程组中,但只有正在执行的 sleep 3000 进程被 kill, 其它后台进程正常运行。

$0 表示 shell 名称或 shell 脚本路径名称。结合 dirname 和 realpath 命令,可以获得执行的脚本文件所在的目录:

$ echo $0
-bash

$ bash -c 'echo $0'  # bash -c 是在非交互式 shell 中执行字符串命令
bash
$ bash -c 'echo "$#" "$@"' a b c d </dev/null 3</dev/null e f
5 b c d e f
$

$ cat spool/x.sh
echo ok
echo $(dirname $0) # 打印 x.sh 脚本文件所在目录
echo $0

$ bash spool/x.sh
ok
spool
spool/x.sh

$ realpath spool/x.sh   # 打印绝对路径
/var/spool/x.sh


$ pwd
/var
$ echo "$(realpath $(dirname "./spool"))"  # 命令替换中的双引号不需要转义。
/var

如果 filename 是可执行脚本文件,则执行 ./filename ARGUMENTS 等效为 bash ./filename ARGUMENTS

shell 脚本第一行可以是特殊的 shebang 字符串:

  • 当前各发行版 /bin 实际是软链接到 /usr/bin 目录
#! /bin/bash
#!/usr/bin/env bash

相关文章

内置类型:type
··16432 字
Rust
Rust 内置基本类型介绍
tokio
··24064 字
Rust Rust-Crate
Tokio 是 Rust 主流的异步运行时库。它提供了异步编程所需要的所有内容:单线程或多线程的异步任务运行时、工作窃取、异步网络/文件/进程/同步等 APIs。
cilium/ebpf
·3283 字
Ebpf
广泛使用的 cilium/ebpf go 库分析,涵盖了 Go 开发 eBPF 程序的各方面内容。
eBPF 常见错误
·9722 字
Ebpf
总结了 eBPF 开发过程中常见的报错和兼容性问题。