play shell. 本篇关于 shell 相关概念的不完全整理,包括其中各种奇奇怪怪的符号含义,运行机理等。因为 shell 和 linux 系统密不可分,所以内容会比较杂。
如何阅读 man 手册
man 命令手册页通常使用 pager 工具显示,常用的 pager 工具有 less
和 more
,默认使用 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 常见用法
- 后台模式
后台模式运行
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
|
- 进程列表置入后台
可以在子 shell 中进行多进程处理,这样终端不再和子 shell 的 I/O 绑定在一起。
例如通过将 tar
命令置入后台,可以在后台执行压缩操作,不会阻塞终端,从而可以方便管理员继续执行其他操作。
1
|
(tar -czf /tmp/test.tar.gz /tmp/test) &
|
- 协程
coproc sleep 10
该命令同样生成一个子 shell,同时可以为该作业命名,coproc my_job { sleep 10; }
。 注意花括号两侧必须有空格
外部命令和内建命令
- 外部命令:存在于 shell 之外的命令,不属于 shell 的一部分。例如
ps
等一系列命令,通常位于 /bin
或 /usr/bin
目录下。外部命令执行时需要创建子 shell。
- 内部命令:已经和 shell 编译成一体的命令,不需要创建子 shell。例如
cd
、echo
、exit
等。
- 可以通过
type
命令判断命令是内部命令还是外部命令。
环境变量
- 全局环境变量
全局环境变量是在 shell 启动时就加载的环境变量,对于所有子 shell 可见,可以通过
printenv
或 env
命令查看。
- 设置全局变量可以通过
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
该指令常用于隐藏上一条指令的输出。
- > 代表重定向到哪里,例如:echo “123” > /home/123.txt
- /dev/null 代表空设备文件
- 2 表示stderr标准错误
- & 表示等同于的意思,2>&1,表示2的输出重定向等同于1
- 1 表示stdout标准输出,系统默认值是1,所以">/dev/null"等同于 “1>/dev/null”
- &> 表示标准输出和标准错误一起重定向
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 的历史扩展。
!
在常见的终端比如 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 脚本编程大全