Bash 学习笔记
Bash 启动配置
bash (Bourne Again SHell) 是默认的 shell,有必要了解一下它不同启动方式下的配置过程。
当执行bash命令或者用户登录系统时,会陆续加载各种bash配置文件,还会设置或清空一系列变量,有时还会执行一些自定义的命令,这些行为都算是启动bash时的过程。在不同的系统中具体的逻辑还是不同的,目前只关注Ubuntu和CentOS。
启动过程有两个维度的分类方式:
- 交互式和非交互式
- 交互式的标准情景:输入一个命令,然后输出
user@host:path$,等待用户输入; - 非交互式的标准情景:
bash执行一个脚本,例如bash demo.sh。
- 交互式的标准情景:输入一个命令,然后输出
- 登录和非登录:顾名思义,非登录情景可以加上
--login或-l选项来伪装为登录情况。
可以通过下面的方法进行判断:
- 交互式的判断:
- 交互式环境下的
$-变量会含有字母i; - 交互式定义了提示符
$PS1,但是非交互式会清空这个变量,因此echo $PS1可以区分。
- 交互式环境下的
- 登录和非登录的判断:
shopt login_shell,返回on或off。
1 | echo $PS1;shopt login_shell |
使用ssh命令时:
ssh远程登录:交互式、登录式;ssh远程执行命令但不登录:非交互式、非登录式
使用bash命令启动shell时:
bash:交互式、非登录式bash -l:交互式、登录式
使用bash命令执行脚本时:
bash demo.sh:非交互式、非登录式bash -l demo.sh:非交互式、登录式
使用su命令切换用户时,加不加-有不同的效果:
su user2:切换到user2,打开的shell是交互式、非登录式su - user2:切换到user2,打开的shell是交互式、登录式
在图形用户界面下打开终端时,默认为交互式、非登录式;但是可以在设置中改为交互式、登录式。
bash不同启动过程中涉及到的配置文件可能有
- 系统级:
/etc/profile(通常在脚本中自动加载/etc/profile.d/*.sh)/etc/bash.bashrc/etc/bashrc
- 用户级:
~/.bash_profile~/.bash_login~/.profile(这三个文件的角色几乎是等价的,只是出于兼容性进行的保留,系统通常只会自动生成其中的一个文件,并且只会加载第一个存在且可读的文件)~/.bashrc(通常用户只需要在这里进行修改自定义配置,各种启动方式通常都会直接或间接地加载这个文件)
bash退出时还涉及
~/.bash_logout等文件,这里不做讨论。此外,~/.bash_history文件记录了bash的历史命令。(但是如果命令以空格开头,就不会记录)
对于一个交互式、登录式启动的shell,或者非交互式但是使用-l选项的情况:
- 首先读取
/etc/profile; - 然后依次搜索
~/.bash_profile、~/.bash_login和~/.profile(在某些系统中,某一个文件会自动生成,其它文件则默认不存在),仅加载其中第一个存在到且可读的文件。
使用--noprofile可以跳过这个过程。如果这里使用的是sh名称,出于兼容性考虑,只会依次加载etc/profile和~/.profile。
对于一个交互式、非登录式启动的shell:
- 读取
/etc/bash.bashrc和~/.bashrc;
对于一个非交互式、非登录式的shell(通常是执行一个脚本),不会加载类似的配置文件,但是会尝试加载$BASH_ENV这个环境变量所代表的配置文件,如果存在的话。
实践中发现,CentOS会自动生成~/.bash_profile,Ubuntu会自动生成~/.profile。两者的作用是类似的,大意是:
- 如果存在
~/.bashrc,读取这个文件; - 如果存在
~/.local/bin这个目录,添加到PATH中。具体的措施不太一样,有的会加在PATH的头部,有的则会加在PATH的最后。
因此,在默认情况下,无论是哪一种方式启动,~/.bashrc中的内容都会被加载执行一次。
系统通常都会自动生成~/.bashrc文件,其中的内容却大不相同:
对于CentOS,自动生成的文件中会尝试加载/etc/bashrc文件
1 | # Source global definitions |
对于Ubuntu,自动生成的文件头部有如下内容
1 | # If not running interactively, don't do anything |
具体含义是:通过$-判断当前shell是不是交互式的,如果不是就直接结束,不再执行后面的命令。
上面这个例子说明,不同的系统中这些配置文件发挥的作用是很不一样的。
对于 Ubuntu,在启动时默认会输出一些信息,可以通过在家目录下创建一个空的
.hushlogin文件来关闭这些信息。
后台任务
通常在shell中执行的任务都是前台任务,即任务会占用前台,在任务结束之前无法进行下一个任务。在命令结尾使用&可以将命令使用后台进程执行,例如
1 | ./test.sh & |
其中的测试脚本在不断写入日志,每隔4秒写入一次
1 |
|
使用jobs命令可以查看当前的后台任务,每一个后台任务都有独立的序号(注意不是pid),例如
1 | [1]+ Running ./test.sh & |
如果返回值为空,代表没有后台任务。后台任务的状态可能是Running(正在运行),也可能是Stopped(挂起,暂停),Terminated(终止),终止的任务稍后就会从jobs的输出中消失。
需要注意的是,后台任务仍然会向当前终端的输出流写入信息!这可能会混淆其他命令的输出,对于后台任务,最好还是将输出重定向比较稳妥
1 | ./test.sh > output.log 2>&1 & |
如果当前的前台任务太长或者卡死了,我们可以使用ctrl+c强行结束前台任务,也可以使用ctrl+z将前台任务挂起,此时任务会转为后台任务,并且设置为暂停状态,此时jobs输出形如
1 | [1]+ Stopped ./test.sh |
使用fg %1命令可以将指定的后台任务(正在执行或挂起的)转到前台执行。
使用bg %1命令则可以将指定的后台挂起任务在后台执行,这些命令的百分号很重要)
1 | bg %1 |
如果缺省编号,默认操作对象是标记为+的最近任务,此外还有标记为-的任务,含义是如果+标记的任务结束,-标记的任务就会变成+标记。
使用kill命令可以终止后台任务,例如 kill %jobnumber 或 kill PID。
需要注意的是,在一个会话中,无论是前台任务还是后台任务都是从属于当前会话的,如果退出当前的登陆,后台任务也会被终止,
有两种方法来解决这种问题:
- 使用
nohup命令; - 使用
setsid命令。
第一种是基于nohup的方法,可以使用nohup搭配 & 来解决会话退出导致后台任务终止的问题,此时对应的进程不会因为退出会话而被终止,例如
1 | nohup ./test.sh & |
nohup命令默认会将输出重定向到当前位置下的nohup.out文件(如果当前位置无权限则会回到家目录下创建nohup.out文件),在事后可以使用nohup.out文件来查看这个后台任务的输出。
可以指定全部输出重定向到指定文件,例如
1 | nohup ./test.sh > out.log 2>&1 & |
nohup命令有提示信息,需要回车确认一下。
如果对一个已经正在运行的后台任务忘记使用nohup执行了,还可以使用disown命令进行补救,例如
1 | ./test.sh & |
第二种是基于setsid的方法,它的做法在原理上更加彻底,此时的进程和当前会话不再是从属关系,因此当前会话的终止不会影响该进程,例如
1 | setsid ./test.sh & |
使用小括号似乎有一样的效果
1 | (./test.sh &) |
对后台任务的进一步分析必须从Linux中的进程,信号等机制出发,但是我对此没啥兴趣,这里只是从实用的角度进行学习,浅尝辄止。
Here Document(heredoc)
Bash 允许在命令行中创建一个 here document,这里 document 的内容会作为 stdin 传递给命令。
heredoc = 把一段文本作为 stdin 输入给命令
基本语法
1 | command <<WORD |
要点:
<<WORD作为开始- 中间内容作为 stdin
- 单独一行的
WORD结束 WORD只是一个分隔标记符,可以自定义(EOF、END、PY等)
heredoc 可以根据内容是否被 shell 展开,分为两种情况:
- 不加引号(会展开),可能发生:
$VAR变量替换,$(cmd)命令替换,反引号替换等 - 加引号(不展开,更安全)
不加引号例如
1 | cat <<EOF |
输出
1 | home: /home/user |
加引号例如
1 | cat <<'EOF' |
输出
1 | home: $HOME |
一个更实际的例子:给 Python 传多行代码并立刻执行(习惯上使用 'PY')
1 | python3 - <<'PY' |
除此之外,下面的特殊写法还支持自动移除前导的 TAB 缩进:(不包括空格)
1 | cat <<-EOF |
补充:在大多数 Unix shell 中,快捷键 Ctrl+D 表示 EOF(End Of File)。
如果当前程序正在读取 stdin,并且输入缓冲区为空,按 Ctrl+D 会告诉程序输入结束,常用于结束交互式输入。
