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 当前用户的家目录
PATH shell 查找命令的路径
BASH bash 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%