首页  /  Linux 进阶  /  正文

扩展变量与父子 Shell

Linux 进阶 2026-10-02📖 18 分钟👁 —
🐚 Linux 进阶 · Shell 编程第 3 / 10 页12345678910📖 完整导航 →

一、扩展变量

1. 四种写法

变量为空时返回 word 并赋值给 result:result=${parameter:-word};变量为空时用 word 替代变量值:result=${parameter:=word}
变量为空时把 word 当 stderr 输出:${parameter:?word};变量为空时什么都不做:${parameter:+word}

本质上是 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 目录
find /var/log -type f								   # 查普通文件
find /etc/nginx -type d                # 只查目录
find /etc/nginx -type l                # 查软链接

删除某个数据文件夹的备份文件,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

三种执行方式的区别:source script 在当前 shell 跑,指定 bash/sh 解释器跑会开子 shell,./script 也会通过 shebang 开子 shell

2. 父 Shell

连接终端时默认的交互式环境。

  1. pstree 看到 ─sshd───sshd───bash───pstree,位置是 sshd 后面的第一个 bash
  2. ps -ef --forest

ps -ef 只能看到一行进程的信息,如果还想看子进程就需要 --forest。

左边是 PID,右边是 PPID:

ps -ef --forest 里的父进程链:sshd → sshd: ubuntu → -bash → ps -ef --forest

3. 子 Shell 具体概念

输入 bash 开启一个子 shell,再看 ps 的显示:

再敲一个 bash 之后,进程树上多出父子两层 bash,红黄绿线标出了 PID 和 PPID 的对应关系

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

示意图:父 shell 发出 bash 命令,创建出子 shell,子 shell 里再执行 ps -f

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
一层层套下来的子 shell:bash 里面开 bash,再开 sh,sh 里面又是 dash

输入 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

外置命令的特点:执行效率稍慢、一定会开启子进程执行:

# 比如外置的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 的探针,内外置命令知道个大概就行。