play shell

play shell. 本篇关于 shell 相关概念的不完全整理,包括其中各种奇奇怪怪的符号含义,运行机理等。因为 shell 和 linux 系统密不可分,所以内容会比较杂。

如何阅读 man 手册

man 命令手册页通常使用 pager 工具显示,常用的 pager 工具有 lessmore,默认使用 less
如果忘记某个命令名,可以使用 man -k 关键字 来搜索,如 man -k network

手册页惯用段名

段名描述
Name命令的名字和简短描述
Synopsis命令语法
Configuration命令配置信息
Description命令描述
Options命令选项
Exit Status命令退出状态
Return Value命令返回值
Errors命令错误信息
Environment命令环境变量
Files命令相关文件
Version命令版本
Conforming To遵循的命名标准
Notes注意事项以及资料
Examples用法示例
Copyright版权信息
See Also与该命令类似的相关命令

手册页的节号

节号所涵盖的内容
1可执行程序或 shell 命令
2系统调用(内核函数)
3库函数(通常是 C 标准库函数)
4特殊文件(通常是 /dev 中的设备文件)
5文件格式和约定(如 /etc/passwd)
6游戏
7惯例和协议(如 IP、Ethernet、ASCII 码等)
8系统管理员命令和守护进程(通常只有 root 用户才能运行)
9内核源代码(routine)

不同发行版节的编号也可能不同,但标准节号如上

子 shell 与进程列表

登入 shell CLI 时是一个父 shell, 当执行 zsh 时会创建一个子 shell,子 shell 会继承父 shell 的环境变量,但是父 shell 无法获取子 shell 的环境变量。
例如当我连接到某个服务器时,执行多次 zsh 命令,每次执行都会创建一个子 shell,可通过 ps 命令查看进程列表。

1
2
3
4
5
6
7
╭─niku at vps in ~ 24-06-27 - 11:49:22
╰─○ ps --forest -f
UID          PID    PPID  C STIME TTY          TIME CMD
niku       25644   25643  0 11:31 pts/0    00:00:00 -zsh
niku       25883   25644  2 11:49 pts/0    00:00:00  \_ zsh
niku       25923   25883  4 11:49 pts/0    00:00:00      \_ zsh
niku       25962   25923  0 11:49 pts/0    00:00:00          \_ ps --forest -f

执行多个命令 () 的影响

执行多个命令可用 ; 连接,表示多个命令依次执行,例如 pwd; ls; cd /etc; pwd,但如果在命令外添加圆括号 (pwd; ls; cd /etc; pwd) 则会作为进程列表运行,生成一个子 shell。
但使用花括号 { command; } 的方式进行命令分组则不会生成子 shell。
可通过 echo $ZSH_SUBSHELL 验证是否创建了子 shell, 0 表示未创建子 shell,1 表示创建了子 shell。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
╭─niku at vps in ~ 24-06-27 - 11:57:43
╰─○ pwd; ls; cd /etc; pwd; ls; echo $ZSH_SUBSHELL
/home/niku
container
/etc
adduser.conf            console-setup   deluser.conf            fstab        hosts.allow      ld.so.conf.d    magic           netconfig      perl        rc5.d           shadow     sudo_logsrvd.conf  w3m
adjtime                 containerd      dhcp                    gai.conf     hosts.deny       letsencrypt     magic.mime      network        profile     rc6.d           shadow-    sv                 wgetrc
alternatives            cron.d          dictionaries-common     ghostscript  init.d           libaudit.conf   mailcap         networks       profile.d   rc.local        shells     sysctl.conf        X11
apparmor                cron.daily      discover.conf.d         groff        initramfs-tools  libnl-3         mailcap.order   nftables.conf  protocols   rcS.d           skel       sysctl.d           xattr.conf
apparmor.d              cron.hourly     discover-modprobe.conf  group        inputrc          libpaper.d      manpath.config  nginx          python3     reportbug.conf  ssh        systemd            xdg
apt                     cron.monthly    docker                  group-       iproute2         locale.alias    mime.types      nsswitch.conf  python3.11  resolv.conf     ssl        terminfo           zsh
bash.bashrc             crontab         dpkg                    grub.d       issue            locale.gen      mke2fs.conf     opt            qemu        rmt             subgid     timezone
bash_completion         cron.weekly     e2scrub.conf            gshadow      issue.net        localtime       modprobe.d      os-release     ranger      rpc             subgid-    tmpfiles.d
bash_completion.d       cron.yearly     emacs                   gshadow-     kernel           logcheck        modules         pam.conf       rc0.d       rsyslog.d       subuid     ucf.conf
bindresvport.blacklist  dbus-1          environment             gss          kernel-img.conf  login.defs      modules-load.d  pam.d          rc1.d       runit           subuid-    udev
binfmt.d                debconf.conf    ethertypes              host.conf    ldap             logrotate.conf  monit           papersize      rc2.d       security        sudo.conf  ufw
ca-certificates         debian_version  fail2ban                hostname     ld.so.cache      logrotate.d     motd            passwd         rc3.d       selinux         sudoers    update-motd.d
ca-certificates.conf    default         fonts                   hosts        ld.so.conf       machine-id      mtab            passwd-        rc4.d       services        sudoers.d  vim
0

╭─niku at vps in ~ 24-06-27 - 11:58:37
╰─○ (pwd; ls; cd /etc; pwd; ls; echo $ZSH_SUBSHELL)
/home/niku
container
/etc
adduser.conf            console-setup   deluser.conf            fstab        hosts.allow      ld.so.conf.d    magic           netconfig      perl        rc5.d           shadow     sudo_logsrvd.conf  w3m
adjtime                 containerd      dhcp                    gai.conf     hosts.deny       letsencrypt     magic.mime      network        profile     rc6.d           shadow-    sv                 wgetrc
alternatives            cron.d          dictionaries-common     ghostscript  init.d           libaudit.conf   mailcap         networks       profile.d   rc.local        shells     sysctl.conf        X11
apparmor                cron.daily      discover.conf.d         groff        initramfs-tools  libnl-3         mailcap.order   nftables.conf  protocols   rcS.d           skel       sysctl.d           xattr.conf
apparmor.d              cron.hourly     discover-modprobe.conf  group        inputrc          libpaper.d      manpath.config  nginx          python3     reportbug.conf  ssh        systemd            xdg
apt                     cron.monthly    docker                  group-       iproute2         locale.alias    mime.types      nsswitch.conf  python3.11  resolv.conf     ssl        terminfo           zsh
bash.bashrc             crontab         dpkg                    grub.d       issue            locale.gen      mke2fs.conf     opt            qemu        rmt             subgid     timezone
bash_completion         cron.weekly     e2scrub.conf            gshadow      issue.net        localtime       modprobe.d      os-release     ranger      rpc             subgid-    tmpfiles.d
bash_completion.d       cron.yearly     emacs                   gshadow-     kernel           logcheck        modules         pam.conf       rc0.d       rsyslog.d       subuid     ucf.conf
bindresvport.blacklist  dbus-1          environment             gss          kernel-img.conf  login.defs      modules-load.d  pam.d          rc1.d       runit           subuid-    udev
binfmt.d                debconf.conf    ethertypes              host.conf    ldap             logrotate.conf  monit           papersize      rc2.d       security        sudo.conf  ufw
ca-certificates         debian_version  fail2ban                hostname     ld.so.cache      logrotate.d     motd            passwd         rc3.d       selinux         sudoers    update-motd.d
ca-certificates.conf    default         fonts                   hosts        ld.so.conf       machine-id      mtab            passwd-        rc4.d       services        sudoers.d  vim
1

子 shell 常见用法

  1. 后台模式 后台模式运行 command &: 例如 sleep 30&,会在后台运行 sleep 30 秒。通过 jobs 命令可以查看后台运行的作业,执行完成后会输出 done。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    ╭─niku at vps in ~ 24-06-27 - 12:14:36
    ╰─○ sleep 30&
    [1] 26260
    ╭─niku at vps in ~ 24-06-27 - 12:14:40
    ╰─○ jobs -l
    [1]  + 26260 running    sleep 30
    ╭─niku at vps in ~ 24-06-27 - 12:14:44
    ╰─○
    [1]  + 26260 done       sleep 30
  2. 进程列表置入后台 可以在子 shell 中进行多进程处理,这样终端不再和子 shell 的 I/O 绑定在一起。
    例如通过将 tar 命令置入后台,可以在后台执行压缩操作,不会阻塞终端,从而可以方便管理员继续执行其他操作。
    1
    
     (tar -czf /tmp/test.tar.gz /tmp/test) &
  3. 协程 coproc sleep 10 该命令同样生成一个子 shell,同时可以为该作业命名,coproc my_job { sleep 10; }注意花括号两侧必须有空格

外部命令和内建命令

  • 外部命令:存在于 shell 之外的命令,不属于 shell 的一部分。例如 ps 等一系列命令,通常位于 /bin/usr/bin 目录下。外部命令执行时需要创建子 shell。
  • 内部命令:已经和 shell 编译成一体的命令,不需要创建子 shell。例如 cdechoexit 等。
  • 可以通过 type 命令判断命令是内部命令还是外部命令。

环境变量

  • 全局环境变量 全局环境变量是在 shell 启动时就加载的环境变量,对于所有子 shell 可见,可以通过 printenvenv 命令查看。
    • 设置全局变量可以通过 export 命令,例如 export my_var=xxx(子 shell 中修改同名全局变量不会影响父 shell)。
    • 删除全局变量可以通过 unset 命令,例如 unset my_var。同样子进程中删除全局变量不会影响父 shell。
  • 局部环境变量 局部环境变量是在当前 shell 中定义的环境变量,只对当前 shell 可见,set 命令可以查看局部变量,全局变量和用户自己定义的变量。
  • 用户自定义环境变量 启动 shell 后,用户可以自定义环境变量,例如 my_value="hello world"echo $my_value 可以查看,如果此时生成子 shell,该变量在子 shell 中不可用。(局部变量建议使用小写表示。系统变量全大写,变量名、等号和值之间没有空格,如果有空格 shell 会将其视为单独命令)。

常见的默认环境变量

变量名描述
HOME当前用户的家目录
PATHshell 查找命令的路径
BASHbash shell 的路径
LANG语言环境
HOSTNAME主机名
PWD当前工作目录
RANDOM随机数

PATH 环境变量

PATH 环境变量是 shell 查找命令的路径,可以通过 echo $PATH 查看,如果一些程序执行路径没有添加到 PATH 中则只能使用绝对变量调用。

可以联系到常见的 shell 脚本起手式:#!/usr/bin/env bash env 会在环境变量 PATH 中查找 bash 这样的脚本具有更好的可移植性。

  • 追加新的路径到 PATH 中可以使用 export PATH=$PATH:/home/niku/myscripts.

环境变量定位&持久化

  • 环境变量定位,登入 Linux 系统时 shell 启动加载的文件:
    • /etc/profile:系统级别的环境变量配置文件,对所有用户生效。
    • ~/.profile:用户级别的环境变量配置文件,对当前用户生效。
    • ~/.bash_profile:用户级别的环境变量配置文件,对当前用户生效。
    • ~/.bashrc:用户级别的 bash 配置文件,对当前用户生效。
    • ~/.zshrc:用户级别的 zsh 配置文件,对当前用户生效。

目录规范:/etc/profile.d目录中用来放不同的配置文件,profile 脚本执行时会循环处理 .d 目录中的文件。

  • 环境变量持久化
    • 全局系统变量:慎用 /etc/profile 放置系统变量发行版升级该文件可能被更新,建议使用 /etc/profile.d 目录创建一个 .sh 文件并分类存放。
    • 个人环境变量:建议使用 ~/.bashrc~/.zshrc 文件,将个人环境变量写入该文件中,这样每次登入 shell 时都会加载。

数组变量

某个变量设置多个值:my_array=(value1 value2 value3),通过 ${my_array[0]} 可以获取数组中的值。

  • echo ${my_array[*]}:输出数组中的所有值。

常见符号含义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$   # 引用
''  # 强引用,优先级大于 $ 
""  # 若引用,优先级小于 $
!!  # 上一条命令,并执行

() # 子 shell,会创建一个子 shell
{} # 命令分组,不会创建子 shell
[] # test 表达式,例如 [ -e /etc/passwd ]
(( 算术表达式 )) # 内部'<' '>' 不需要转义
[[ $var == "test" ]] # 字符串比较、模式匹配

-eq:等于
-ne:不等于
-le:小于等于
-ge:大于等于
-lt:小于
-gt:大于

=~       # 左侧是字符串,右侧是一个模式,判断左侧的字符串能否被右侧的模式所匹配:但是必须在[[]]中执行模式匹配。
!=, <>   # 不等于
>        # 输出重定向
>>       # 代表追加重定向
<        # 输入重定向 command < inputfile
<<       # 内联输入重定向, 例如 cat << EOF

指令解析

>/dev/null 2>&1

该指令常用于隐藏上一条指令的输出。

  1. > 代表重定向到哪里,例如:echo “123” > /home/123.txt
  2. /dev/null 代表空设备文件
  3. 2 表示stderr标准错误
  4. & 表示等同于的意思,2>&1,表示2的输出重定向等同于1
  5. 1 表示stdout标准输出,系统默认值是1,所以">/dev/null"等同于 “1>/dev/null”
  6. &> 表示标准输出和标准错误一起重定向
1
2
3
# 以下两条命令等价。
ls /home > /dev/null 2>&1
ls /home &> /dev/null

exit code

bash 参考 使用 $? 命令可以查看上一条命令的退出状态。 脚本中可以通过 exit 命令设置退出状态码,例如 exit $var 将退出状态指定为一个变量值,需要注意退出码最大到 255。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
状 态 码         描 述
Exit code 0        Success
Exit code 1        General errors, Miscellaneous errors, such as "divide by zero" and other impermissible operations
Exit code 2        Misuse of shell builtins (according to Bash documentation)        Example: empty_function() {}
126           命令不可执行
127           没找到命令
128           无效的退出参数
128+x          与Linux信号x相关的严重错误
130           通过Ctrl+C终止的命令
255           正常范围之外的退出状态码

shell 语法

命令替换

将命令输出赋值给变量,需要注意命令替换会创建子 shell 运行指定的命令。

1
2
3
4
my_var=$(ls)
my_var=`ls`

echo $my_var // 输出 ls 命令的结果

控制结构

if-then 语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# if-then
if command      # if 后会根据命令返回的退出码为 0(成功),则执行 then 后的命令
then
    command 1
    command 2
    ...
fi

# if-then-else
if command1
then
    command2
else
    command3
fi

# elif
if command1
then
    command2
elif command3
then
    command4
fi

# 简化写法
if command1; then # 与上面等价
    command2
fi

if command1; then
    command2
else
    command3
fi

case 语句:

1
2
3
4
5
6
7
8
case $var in
    pattern1 | pattern2)
        command1
        command2
        ;;
    pattern3) command3;;
    *) defaultcommand;;
esac

for 循环

循环结束后 test 会保持最后一次的值,for 结束后依然可以使用 test。

1
2
3
4
for test in Alabama Alaska Arizona Arkansas California Colorado
do
   echo The next state is $test
done

如果遍历的字符串中包含空格,单引号等,可以使用双引号包裹。

1
2
3
4
for test in I don't know if "this'll" work
do
   echo "word:$test"
done
IFS

IFS(internal field separator) 是 shell 的一个环境变量,用于定义字段分隔符,默认为空格、制表符和换行符。
将 IFS 修改为换行符:IFS=$'\n'
规范写法,修改 IFS 前保存原 IFS 值,修改后恢复原 IFS 值。

1
2
3
4
IFS.OLD=$IFS 
IFS=$'\n' 
<在代码中使用新的 IFS 值> 
IFS=$IFS.OLD

指定多个分隔符:IFS=$'\n:;', 将换行符、冒号和分号作为分隔符。

通过通配符遍历文件目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for file in /home/niku/*
do
    if [ -d "$file" ]
    then
        echo "$file is a directory"
    elif [ -f "$file" ]
    then
        echo "$file is a file"
    fi
done
linux 文件名
linux 文件名中可以包含空格,因此在处理文件名时需要使用双引号包裹,例如 [ -d "$file" ]。 否则含有空格的文件名会发生错误。

c 风格 for 循环

1
2
3
4
for (( a=1, b=10; a <= 10; a++, b-- ))
do
    echo "$a - $b"
done

while 与 until 循环

while 需要结合 test 命令使用,test 命令用于判断条件是否成立,test 命令退出状态为 0 时条件成立,否则不成立。

1
2
3
4
5
6
var1=10
while [ $var1 -gt 0 ]
do
    echo $var1
    var1=$[ $var1 - 1 ]
done

until 与 while 相反,当条件不成立时执行循环体。

1
2
3
4
5
6
var1=10
until [ $var1 -eq 0 ]
do
    echo $var1
    var1=$[ $var1 - 1 ]
done

break 跳出循环,可指定跳出的循环层数,默认 n 为 1。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for (( a=1; a <= 10; a++ ))
do
    echo $a
    for (( b=1; b <= 10; b++ ))
    do
        if [ $b -eq 5 ]
        then
            break 2
        fi
        echo "  $b"
    done
done
  • 在循环的 done 后可指定输出的文件描述符,例如 done > output.txt

test 命令

若 if 需要判断非命令退出状态的条件,可以使用 test 命令。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 语法格式
test condition
# 或者使用 [],大部分 shell 支持,前后需要空格
[ condition ]

# example
my_var="test"
if test $my_var # 判断变量是否为空
then
    echo "yes"
fi

test 也可做数值条件判断,例如:

1
2
3
4
5
6
[ $my_var -eq 0 ] # 判断变量是否等于 0
[ $my_var -ne 0 ] # 判断变量是否不等于 0
[ $my_var -lt 0 ] # 判断变量是否小于 0
[ $my_var -gt 0 ] # 判断变量是否大于 0
[ $my_var -le 0 ] # 判断变量是否小于等于 0
[ $my_var -ge 0 ] # 判断变量是否大于等于 0

字符串比较:

1
2
3
4
[ $my_var = "test" ] # 判断变量是否等于 test
[ $my_var != "test" ] # 判断变量是否不等于 test
[ -z $my_var ] # 判断变量长度是否为0
[ -n $my_var ] # 判断变量长度是否不为0
注意
  • 比较字符串大小时大于号和小于号需要转义,否则将被 shell 视为重定向符,例如 [ "a" \< "b" ]
  • 比较方式和 sort 命令不同,sort 命令默认是按照字典序排序,而 test 命令是按照 ASCII 码排序。

文件比较:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[ -f $file ] # 判断文件是否存在
[ -d $file ] # 判断文件是否是目录
[ -e $file ] # 判断文件是否存在
[ -r $file ] # 判断文件是否可读
[ -w $file ] # 判断文件是否可写
[ -x $file ] # 判断文件是否可执行
[ -s $file ] # 判断文件是否为空
[ -L $file ] # 判断文件是否是软链接
[ -O $file ] # 判断文件是否属于当前用户
[ -G $file ] # 判断文件是否属于当前用户组
[ $file1 -nt $file2 ] # 判断文件1是否比文件2新
[ $file1 -ot $file2 ] # 判断文件1是否比文件2旧
[ $file1 -ef $file2 ] # 判断文件1和文件2是否是同一个文件

复合条件:

1
2
3
[ condition1 ] && [ condition2 ] # 与
[ condition1 ] || [ condition2 ] # 或
[ ! condition1 ] # 非

shell 参数

  • $0:脚本名称(bash xx.sh)方式运行时,$0 为 xx.sh,如果 ./xx.sh 运行则会包含路径名可以使用 basename 返回不包含路径名的脚本名。
1
2
3
4
#!/bin/bash
# test basename

echo "The script name is: $(basename $0)"
  • $1:第一个参数
  • $#:参数个数,同样代表最后一个参数的索引(不能再花括号内使用 $, 可以换成 !#),如果没有参数则为 0 !# 返回脚本名。
1
2
3
4
5
6
7
8
#!/bin/bash
# test $#
echo "The number of parameters is: $#"
echo "The last parameter is: ${!#}"

output:
The number of parameters is: 4
The last parameter is: four
  • $*:所有参数,作为一个整体,将所有参数看作一个单词,不会对参数进行分割。
  • $@:所有参数,作为独立的单词,会对参数进行分割,通常使用 for 进行遍历。
1
2
3
4
#!/bin/bash
# test $* and $@
echo "Using the \$* method: $*"
echo "Using the \$@ method: $@"

选项处理

  • - 后跟单个字母选项,例如 ls -l, 可以多个字符组合 ps -ef
  • -- 后跟字符串选项,例如 ls --all
  • getopts 命令用于处理选项,可以处理短选项和长选项。

输入输出和重定向

每个进程对的可打开文件描述符数量是有限的,通常为 1024 个,其中 bash shell 保留了前 3 个文件描述符,0 为标准输入,1 为标准输出,2 为标准错误输出。

  • >: 输出重定向.
  • >>: 输出重定向追加.
  • <: 输入重定向.
  • <<: 内联输入重定向,会进入 here,<< 后跟着的是终止符,退出输入需键入终止符并回车.
  • 2>: 错误重定向.
  • &>: 标准输出和标准错误一起重定向.
  • >&2: 将标准输出重定向到标准错误.
  • exec: 用于重定向文件描述符,例如 exec 3> output.txt 将文件描述符 3 重定向到 output.txt 文件,之后可以通过 echo "hello" >&3 将输出重定向到 output.txt 文件。
  • > /dev/null 2>&1: 用于隐藏命令输出,将标准输出和标准错误重定向到 /dev/null 文件中,即丢弃输出。
  • tee: 用于同时输出到文件和屏幕,例如 ls | tee output.txt 将 ls 命令的输出同时输出到屏幕和 output.txt 文件中(tee 默认覆盖文件原内容,追加需使用 tee -a)。

shell or 终端命令常见坑点

!

当你运行以下命令时,可能会莫名其妙进入一个内联输入状态,这是因为 ! 触发了 bash 或 zsh 的历史扩展。

1
echo "hello, world!"

! 在常见的终端比如 bash、zsh 中有特殊含义,表示历史命令中的上一条命令。当需要在 终端 中输出 "!" 时,需要转义,例如 echo "hello, world\!" > hello.txt,或者使用单引号同样可防止历史扩展。 如果不使用历史扩展功能,可以通过 set +H 或者 set +o histexpand 关闭历史扩展功能。

Can’t use exclamation mark (!) in bash?

&

后台进程,例如 sleep 10 &,在后台运行 sleep 10 秒。& 也可以用于将命令置入后台,例如 ./xx.sh &。 后台作业会跟终端进程关联,当终端关闭时,后台作业会被终止。可以通过 nohup 命令将后台作业与终端进程分离,例如 nohup ./xx.sh &

推荐阅读

Linux Shell Scripting Tutorial Linux 命令行与 shell 脚本编程大全

0%