一、扩展变量
1. 四种写法


本质上是 parameter 和 word 之间的关系,和 result 关系不大。
前三个(:-、:=、:?)只要记住变量为空才动手,变量有值就什么都不做。
2. 具体说明
# 使用:-
# 变量为空,赋值给变量然后赋值给result
ubuntu@VM-0-9-ubuntu:~$ echo $chaoge
ubuntu@VM-0-9-ubuntu:~$ result=${chaoge:-111}
ubuntu@VM-0-9-ubuntu:~$ echo $result
111
# 变量不为空,什么都不做
# 相当于变量直接赋值给result:result=$change
ubuntu@VM-0-9-ubuntu:~$ chaoge="nbb"
ubuntu@VM-0-9-ubuntu:~$ result=${chaoge:-111}
ubuntu@VM-0-9-ubuntu:~$ echo $result
nbb
# 使用:=
# 变量为空,赋值变量且返回值
ubuntu@VM-0-9-ubuntu:~$ unset chaoge
ubuntu@VM-0-9-ubuntu:~$ unset result
ubuntu@VM-0-9-ubuntu:~$ echo $chaoge $result
ubuntu@VM-0-9-ubuntu:~$ result=${chaoge:=apple}
ubuntu@VM-0-9-ubuntu:~$ echo $chaoge $result
apple apple
# 变量不为空,什么都不做
# 使用:?
# 变量值为空,主动抛出标准错误输出
ubuntu@VM-0-9-ubuntu:~$ echo ${new_name}
ubuntu@VM-0-9-ubuntu:~$ echo ${new_name:?}
-bash: new_name: parameter null or not set
ubuntu@VM-0-9-ubuntu:~$ echo ${new_name:?该变量值为空}
-bash: new_name: 该变量值为空
# 标准输出stdout执行代码为1,stderr标准错误输出为2
# 变量不为空,什么都不做
# 使用:+
# 变量值为空,什么都不做
ubuntu@VM-0-9-ubuntu:~$ echo $result
ubuntu@VM-0-9-ubuntu:~$ echo ${result:+heiheihei}
# 变量不为空,说明有值,字符本身返回
ubuntu@VM-0-9-ubuntu:~$ result="aaa"
ubuntu@VM-0-9-ubuntu:~$ echo ${result:+heiheihei}
heiheihei
3. 具体应用:删过期备份
数据备份、删除过期数据的脚本里,最常见的是 find | xargs rm -f,搜索且删除,xargs 把搜索结果转化为 rm 的参数。
find 的基本用法:
find 目录
-name指定的文件名-type文件类型
find /var/log -type f # 查普通文件
find /etc/nginx -type d # 只查目录
find /etc/nginx -type l # 查软链接
-mtime文件修改日期,-mtime +7表示超过 7 天,-7是 7 天之内-size +30M文件大小,超过和少于,单位大写
删除某个数据文件夹的备份文件,dir_path="/data/mysql/_back_data/":
find ${dir_path} -name '*.tar.gz' -type f -mtime +7|xargs rm -f
假设目录不存在、写错了,找不到那个目录就会在当前目录搜索删除,所以最好做变量扩展的改进,防止删错,:= 是一定能赋值的:
find ${dir_path:=/data/mysql/_back_data/} -name '*.tar.gz' -type f -mtime +7|xargs rm -f
可以直接写目录,但脚本里不写死路径是为了防止删错。
${dir_path:=/data/mysql/_back_data/} 这行的逻辑是:如果你先定义了 dir_path,就用你定义的;如果没定义或者为空,就用默认值。等于给脚本加了一层保险。哪天有人改了这个变量但忘了赋值,脚本不会在当前目录乱删文件,而是回退到默认路径。
直接写死路径也能跑,但如果这段代码出现在多个地方,改一次要全局搜索替换。抽成变量改一行就行。生产脚本里这是常规操作。
二、父子 Shell
1. 子 Shell

2. 父 Shell
连接终端时默认的交互式环境。
pstree看到─sshd───sshd───bash───pstree,位置是 sshd 后面的第一个 bashps -ef --forest
-f显示 UID、PID(进程本身)、PPID(父进程)-e列出所有进程信息
ps -ef 只能看到一行进程的信息,如果还想看子进程就需要 --forest。
左边是 PID,右边是 PPID:

3. 子 Shell 具体概念
输入 bash 开启一个子 shell,再看 ps 的显示:

新开出来的那个 bash 就是子 shell:

4. 多个子 Shell
开启多个 shell(模拟通过解释器运行脚本的情况):
root 3535716 1034 0 11:58 ? 00:00:00 \_ sshd: ubuntu [priv]
ubuntu 3535796 3535716 0 11:58 ? 00:00:00 \_ sshd: ubuntu@pts/2
ubuntu 3535797 3535796 0 11:58 pts/2 00:00:00 \_ -bash
ubuntu 3538289 3535797 0 12:09 pts/2 00:00:00 \_ bash
ubuntu 3540249 3538289 0 12:17 pts/2 00:00:00 \_ bash
ubuntu 3540271 3540249 0 12:18 pts/2 00:00:00 \_ sh
ubuntu 3540289 3540271 0 12:18 pts/2 00:00:00 \_ dash
ubuntu 3540303 3540289 0 12:18 pts/2 00:00:00 \_ ps -ef --forest

输入 exit 就会退出一个。
5. 创建进程列表(创建子 shell 执行命令)
开启子 shell 运行命令:使用 ()。
为什么要开启子 shell 运行命令?
假设执行了 ping 命令,那么就会一直执行,且卡住当前终端无法操作。有一些耗时的命令,需要开启子 shell 单独运行。
ubuntu@VM-0-9-ubuntu:~$ (cd ~; pwd; ls; cd /tmp; ls)
/home/ubuntu
nohup.out shell special.sh sub_str
57a3e8564b8fbda80fd64c0329310ec6 sgdaemon.log
cpuidle_support.log snap-private-tmp
cvm_init.log stargate.lock
disable_rt_runtime_share.log systemd-private-ca5b9391601f4b019fb54bb62f91a814-chrony.service-8Ifbzf
gpudirect_rdma_setup.log systemd-private-ca5b9391601f4b019fb54bb62f91a814-ModemManager.service-ggmlWm
gpudirect_rdma_setup.sh systemd-private-ca5b9391601f4b019fb54bb62f91a814-polkit.service-lZl1YC
index.html systemd-private-ca5b9391601f4b019fb54bb62f91a814-systemd-logind.service-hKxSs8
net_affinity.log systemd-private-ca5b9391601f4b019fb54bb62f91a814-systemd-resolved.service-ZBEpsy
nv_driver_install.log systemd-private-ca5b9391601f4b019fb54bb62f91a814-upower.service-xektIu
nvenc_ai_sdk_install.log tlinux_xps.log
nvenc_ai_sdk_install.sh virtio_blk_affinity.log
nv_gpu_conf.log ydeye_install.log
setRps.log yunjing_install
set_xps.log yunjing_install_nohup
ubuntu@VM-0-9-ubuntu:~$ echo $BASH_SUBSHELL
0
# 还在当前shell环境
检测是否开启子 shell 运行的命令:linux 有默认变量 BASH_SUBSHELL,该值为 0 是当前 shell 环境,否则代表开启了 shell 运行的命令。
cd ~; pwd; ls; cd /tmp; ls
# 结果还是0,这些也没有开启子shell
下面是开启子 shell 运行的命令:
(cd ~; pwd; ls; cd /tmp; ls; echo $BASH_SUBSHELL)
# 新开一个终端在父shell
├─sshd─┬─sshd───sshd───bash
│ └─sshd───sshd───bash───pstree
# 如果执行的同时输出
ubuntu@VM-0-9-ubuntu:~$ (cd ~; pwd; ls; cd /tmp; ls; echo $BASH_SUBSHELL)
/home/ubuntu
nohup.out shell special.sh sub_str
57a3e8564b8fbda80fd64c0329310ec6 sgdaemon.log
...
1
为什么执行后输出是 0,执行同时输出是 1?因为这个变量检测的是当前命令的执行环境,什么命令都没有,单独输出相当于没有执行命令,还在当前环境肯定是 0。
不加括号执行:
ubuntu@VM-0-9-ubuntu:~$ cd ~; pwd; ls; cd /tmp; ls; echo $BASH_SUBSHELL
/home/ubuntu
...
0
得出结论:加括号会开启子 shell 运行。
6. 子 shell 嵌套
一个括号开启一个子 shell,多个括号就会开多个:
# 开1个
ubuntu@VM-0-9-ubuntu:/tmp$ (pwd; echo $BASH_SUBSHELL)
/tmp
1
# 开2个
ubuntu@VM-0-9-ubuntu:/tmp$ (pwd;(echo $BASH_SUBSHELL))
/tmp
2
# 开3个
ubuntu@VM-0-9-ubuntu:/tmp$ (pwd;(pwd;(echo $BASH_SUBSHELL)))
/tmp
/tmp
3
shell 脚本开发经常用到子 shell 进行多进程处理,提高程序并发执行效率。
三、shell 内置和外置命令(了解即可)
本块内容了解,父子 shell 后台进程的关系。
内置命令:系统启动加载到内存,执行效率很高一直跑内存,但是占资源。比如 cd。
外置命令:系统需要从硬盘读取文件加载到内存。比如 nginx 默认开机不自启,可开启。
外置命令也称为自己单独下载的文件系统命令,出于 bash shell 之外的程序,一般的目录路径是 /bin、/usr/bin、/sbin、/usr/sbin:
# 可以看到cd没有路径,是内置命令
root@VM-0-9-ubuntu:~# which cd
root@VM-0-9-ubuntu:~#
root@VM-0-9-ubuntu:~# which pwd
/usr/bin/pwd
判断外置内置
type [命令] 可以找到命令的路径:
root@VM-0-9-ubuntu:~# type ps
ps is /usr/bin/ps
# 看到是一个可执行的文件
root@VM-0-9-ubuntu:~# ls -l /usr/bin/ps
-rwxr-xr-x 1 root root 146424 Sep 26 2024 /usr/bin/ps
打开之后是无法查看的格式,因为是二进制的文件:
![用 vim 打开 /usr/bin/ps,满屏的 ELF 乱码,最后一行写着 /usr/bin/ps [noeol] 358L](images/sh1-44.jpg)
外置命令的特点:执行效率稍慢、一定会开启子进程执行:
# 比如外置的ps,bash开启了子进程执行
root@VM-0-9-ubuntu:~# ps -f --forest
UID PID PPID C STIME TTY TIME CMD
root 3549537 3544020 0 12:58 pts/3 00:00:00 su - root
root 3549538 3549537 0 12:58 pts/3 00:00:00 \_ -bash
root 3550738 3549538 0 13:03 pts/3 00:00:00 \_ ps -f --forest
内置命令的特点:不会产生子进程,和 shell 为一体,是 shell 的一部分,无需单独读取文件,系统启动后执行在内存中。
查看 linux 的内置 shell 命令:compgen -b。
至此 linux 的基础篇完结。

到这儿基础篇就收尾了:${var:-默认值} 这一族扩展变量是脚本的保险丝, () 开子 shell 是并发处理起手式,BASH_SUBSHELL 是判断有没有真的开子 shell 的探针,内外置命令知道个大概就行。