本文并不会讲解如何设置PS1以获得你喜欢的提示符;本文会围绕PS1这个变量,就其涉及到的一些概念展开讨论
实际上 bash 专门用来显示环境变量的命令是 printenv,但是很多文章中使用 env 来显示环境变量,当然也没有错,但是 Linux 下这两个命令的设计初衷确实完全不同的,让我们来分别看一下相应的 man 手册
图1:env命令的man手册
env 这个命令主要是用来执行命令的,它可以在一个指定的环境中(意为不是当前的环境)去执行一个命令;比如当前登录用户为 demouser,那么肯定环境变量 USER=demouser,我们可以临时将 USER 改为 testuser 去运行程序;
env USER=testuser <你要运行的程序名>
下面这个例子首先用 printenv USER 显示当前环境变量 USER 的值,为 demouser,然后我们用 env 来执行这个命令,在执行前将环境变量临时改为 testuser,这时在 env 命令下执行的 printenv USER 显示的结果就变成了 testuser;命令退出后再次执行 printenv USER,可以看到显示的还是 demouser
demouser@ubuntu:~$ printenv USER
demouser
demouser@ubuntu:~$ env USER=testuser printenv USER
testuser
demouser@ubuntu:~$ printenv USER
demouser
demouser@ubuntu:~$
但是 env 这个命令确实可以显示出所有的环境变量来,在 man 手册的后面有这样一行字
图2:env命令的man手册
但是 env 命令并不能向 printenv 命令那样显示指定环境变量的值,比如下面例子
demouser@ubuntu:~$ printenv USER
demouser
demouser@ubuntu:~$ env USER
env: "USER": 没有那个文件或目录
demouser@ubuntu:~$
我个人并不反对使用 env 显示所有的环境变量,只是希望大家不要忘记另一个命令:printenv
我们使用命令 printenv PS1(或者 env | grep PS1),我们发现在环境变量里找不到这个 PS1,这并不是我们哪里做错了,这是因为 PS1 并不是一个环境变量
有人可能说,你用 set 命令就可以显示出 PS1 了,是的,没错,但是 set 命令显示的是什么呢?我们来看手册,因为 set 是 bash 的内建命令,所以需要用 help 来显示它的手册
图3:set命令的help手册
set 的手册中强调,set 显示的是 shell 变量,而非环境变量,既然用 set 可以显示出 PS1,那 PS1 肯定是一个 shell 变量
shell 变量真的没有什么好解释的,在 shell 下建立的变量都是 shell 变量;环境变量也是 shell 变量,只不过和普通的 shell 变量有一点不同,下面会说到
我理解环境变量有一个显著的特征,就是它会传递到子进程中去,所以环境变量不仅会对当前进程,也可能会对其建立的子进程产生影响;而shell变量是不会传递到子进程中去的,所以它仅对当前进程产生影响,无法影响其建立的子进程
一个 shell 变量可以通过 export 命令成为环境变量,一个环境变量本身也是一个 shell 变量,仅仅是带有一个环境变量的标志而已,使用 declare +x 命令可以去掉这个环境变量的标志,使其不再是环境变量,而仅仅是一个 shell 变量
$ printenv PS1
$ echo $PS1
\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$
$ export PS1
$ printenv PS1
\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$
$ declare +x PS1
$ printenv PS1
$ echo $PS1
\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$
$
很多很多的文章中都把 PS1 当做环境变量,其实这是不正确的;
但是 PS1 是不是环境变量并不怎么重要,重要的是这其中的很多概念混淆了会给我们带来不少困惑和麻烦
先看一下我的 ubuntu 终端上看到的 PS1 是什么内容
$ echo $PS1
\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$
要看懂这一大串乱七八糟的东西先要了解 《PS1 的控制码》以及 《ANSI 的 ESCAPE 转义》,这两篇文章在附录里有链接,可以先看一下
CSI 序列的开始字符
CSI 序列的结束字符
关于 \[...\]
\[
” 和 “\]
”,这些又是什么呢?\[
” 和 “\]
” 之间的是不打印字符,也就是控制符\[\e]0;\u@\h: \w\a\]
\[\033[01;32m\]
\[\033[00m\]
\[\033[01;34m\]
"\[\033[00m\]
"终端窗口的标题
打开一个终端窗口(不论是桌面还是仿真终端远程登录),窗口上都有一个标题
图4:终端窗口标题
上面提到的第 1 组控制序列正是控制这个窗口标题的,我们称之为 窗口标题控制序列,这部分的资料非常少,我也只在 wikipedia 中找到了一句关于这组控制码的说明,具体出处请看我的另一篇文章《ANSI 的 ESC 转义》,在最后一部分"OSC 序列"中有说明
定义终端窗口标题的序列:\[\e]0;控制码\a\]
;其中控制码部分可以使用文章 《PS1 的控制码》 中的大部分控制码
在定义窗口标题的第 1 组序列中,控制码 “\u” 表示当前用户名(user);“\h” 表示当前主机名(hostname);“\w” 表示当前工作目录(work directory);这些均在文章 《PS1 的控制码》 中有说明,还有一个 “@” 字符会直接显示出来,看看你的窗口标题是不是和定义的一样
改变一下终端标题:
提示符的设定
\[\033[
开头,以 m\]
结束的,从文章 《ANSI 的 ESCAPE 转义》 中可以查到,请看该文中CSI序列的第14个控制序列 - CSI n m;其格式与第 2 - 5 组控制序列相符,所以这是在设置 SGR 参数\[\033[01;32m\]
,其中的 n 就是 “01;32”,其含义仍然需要在文章 《ANSI 的 ESCAPE 转义》 中查,在【SGR 参数】一节中可以查到,“1” - 表示粗体或高亮;“31-37” - 前景色,再查颜色表,可以查到,“32” - 绿色;所以第 2 组控制序列的含义就是:以加粗(高亮)绿色显示文字\[\033[00m\]
,含义是去掉前面设置的属性,恢复到默认属性\[\033[01;34m\]
,含义是以加粗(高亮)蓝色显示文字我们把设定提示符的控制序列重新写在这里:\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$
如果我们把所有的控制序列都去掉的话变成这样:\u@\h:\w$
\u
、\h
、\w
在前面讲窗口标题时已经介绍过,所以,暂且不提字体颜色的话,我们设置的提示符是:用户名@主机名:<当前工作路径>$
我们可以实际试一下
PS1="\u@\h:\w$ "
注意:$后面有一个空格,仅仅是为了美观一点
图8:初次设置PS1
结合前面的分析,实际设置的提示符应该是:[ 绿色字 ]用户名@主机名[ 默认颜色 ]:[ 蓝色字 ]<当前工作路径>[ 默认颜色 ]$
看一下实际情况
图9:当前提示符实际效果
关于 ${debian_chroot:+($debian_chroot)},我们会在后面单独拿出来一节说明
我们用 info bash (或者 man bash)查看 bash 的在线手册,可以找到 bash 启动时执行脚本的过程,在此我们仅讨论交互式 shell 的启动过程,其实,交互式 shell 还分为登录(interactive login shell)和非登录(non-interactive login shell),这些都不在本文的讨论范围内
图10:交互式bash启动时执行脚本的顺序
由此可见,在交互式登录 shell 时(interactive login shell), bash 在启动时执行脚本的顺序为:
在交互式非登录 shell 时(non-interactive login shell), bash 在启动时执行脚本的顺序为:
但是实际上,在交互式 shell 下,不论是登录还是非登录模式,bash 启动执行的脚本区别不大,我们先来看交互式登录 shell 启动时需要执行的第一个文件 /et/profile
whowin@whowin-ubuntu:~$ cat -n /etc/profile
1 # /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
2 # and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
3
4 if [ "$PS1" ]; then
5 if [ "$BASH" ] && [ "$BASH" != "/bin/sh" ]; then
6 # The file bash.bashrc already sets the default PS1.
7 # PS1='\h:\w\$ '
8 if [ -f /etc/bash.bashrc ]; then
9 . /etc/bash.bashrc
10 fi
11 else
12 if [ "`id -u`" -eq 0 ]; then
13 PS1='# '
14 else
15 PS1='$ '
16 fi
17 fi
18 fi
19
20 if [ -d /etc/profile.d ]; then
21 for i in /etc/profile.d/*.sh; do
22 if [ -r $i ]; then
23 . $i
24 fi
25 done
26 unset i
27 fi
whowin@whowin-ubuntu:~$
很显然,正常情况下,/etc/profile 会去执行 /etc/bash.bashrc(第 9 行),这个脚本正是交互式非登录 shell 启动时执行的第一个脚本
在我的系统中,没有 ~/.bash_profile
和 ~/.bash_login
这两个文件,按照 bash 官方文档的说明,交互式非登录 shell 启动时在执行完 /etc/profile 后会去执行 ~/.profile
,让我们来看看这个文件
whowin@whowin-ubuntu:~$ cat -n ~/.profile
1 # ~/.profile: executed by the command interpreter for login shells.
2 # This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login
3 # exists.
4 # see /usr/share/doc/bash/examples/startup-files for examples.
5 # the files are located in the bash-doc package.
6
7 # the default umask is set in /etc/profile; for setting the umask
8 # for ssh logins, install and configure the libpam-umask package.
9 #umask 022
10
11 # if running bash
12 if [ -n "$BASH_VERSION" ]; then
13 # include .bashrc if it exists
14 if [ -f "$HOME/.bashrc" ]; then
15 . "$HOME/.bashrc"
16 fi
17 fi
18
19 # set PATH so it includes user's private bin directories
20 PATH="$HOME/bin:$PATH"
whowin@whowin-ubuntu:~$
很显然,正常情况下,会执行 ~/.bashrc
(第 15 行),这个脚本正是交互式非登录 shell 启动时执行的第二个脚本
至此,研究在 bash 启动过程中 PS1 变量的设置过程,只需研究 /etc/bash.bashrc 和 ~/.bashrc
这两个脚本即可
/etc/bash.bashrc
whowin@whowin-ubuntu:~$ cat -n /etc/bash.bashrc
......
13 # set variable identifying the chroot you work in (used in the prompt below)
14 if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
15 debian_chroot=$(cat /etc/debian_chroot)
16 fi
17
18 # set a fancy prompt (non-color, overwrite the one in /etc/profile)
19 PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
......
因为下面还要执行一个
~/.bashrc
脚本,所以其实这里不用设置 PS1 ,只需要在后面执行的文件中设置即可,但我们要清楚,这个文件是放在 /etc 目录下的,需要有比较高权限的用户才可以修改,而~/.bashrc
这个文件是放在用户目录的,用户自己是可以随便修改的,如果在这里没有设置 PS1,而用户自己又修改了~/.bashrc
文件,导致在~/.bashrc
中也没有设置 PS1 的话,PS1 这个变量会不存在,这就有些尴尬了,因为提示符没有了,你可以试一下在你的终端上执行 unset PS1,看看会发生什么
~/.bashrc
whowin@whowin-ubuntu:~$ cat -n ~/.bashrc
......
59 if [ "$color_prompt" = yes ]; then
60 PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
61 else
62 PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
63 fi
64 unset color_prompt force_color_prompt
65
66 # If this is an xterm set the title to user@host:dir
67 case "$TERM" in
68 xterm*|rxvt*)
69 PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
70 ;;
71 *)
72 ;;
73 esac
......
$ echo $PS1
\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$
正常的 linux 启动后根目录在 “/”,下面有一些其它必须的目录,比如:/dev、/etc、/bin、…等等,这个文件系统让你可以做很多操作,启动各种应用程序;但是 Linux 有一个非常有趣且重要的功能,就是改变根目录,比如,我们可以在 /home/ 目录下建立一个新目录 new_root,然后通过命令 chroot /home/new_root 把文件系统的根目录变成 /home/new_root/,执行成功后,当你执行 ls / 命令时,实际显示的是 /home/new_root/ 目录下的文件,当然,在 chroot 之前你必须在 /home/new_root/ 下建立一个与原文件系统类似的文件系统。比如有 dev 目录, bin 目录,lib 目录等,否则你会什么也干不了;当你 chroot 成功后,显然你将只能访问原来文件系统中 /home/new_root/ 目录及其子目录,原文件系统中的其它目录将无法访问;不知道是否解释清楚了 chroot
我们用于开发的系统往往由于安装了一些软件而变得不那么干净,我们开发的应用程序在你用于开发的环境下可以正常运行并不表示在一个标准的干净的环境中也可以正常运行,这时,我们可以在某个目录下建立一个干净的文件系统,然后 chroot 过去,在这个干净的环境中进行测试
开发的应用程序有时需要打包发布,打包时需要把这个应用程序所需的依赖都打到一起,但由于我们用于开发的系统并不干净,我们无法准确地判断出哪些库文件需要打包,为此我们可以建立一个 chroot 环境,环境中仅存放应用程序所需的依赖,在确保程序能够正常运行的情况下,可以很方便地打包
有些文章翻译成 “chroot监狱”,我感觉有些别扭,就没有采用;这个东西的最典型的应用是 FTP;因为很多服务器使用 ftp 向用户提供资源,为了安全,当一个 guest 用户(或普通用户)登录 ftp 服务器时,被 chroot 到一个特定的目录下,无法访问服务器上的其他目录,这种限制方式被称为 chroot jail,比如著名的 vsftpd 这个 FTP 服务器端软件,在配置文件里有一个选项 chroot_local_user,当该选项为 “YES” 时,可以为每个用户指定其根目录 “/”,从而将用户关闭在这个 chroot jail 中
${parameter:-word}
和 ${parameter:+word}
${parameter
字符串可以找到这类语法的说明${parameter:-word}
:如果 parameter 不存在或者为 null,则返回 word,否则,返回 parameter${parameter:+word}
:如果 parameter 存在且不为 null,则返回 word,否则返回一个空值(nothing)~/.bashrc
脚本对 debian_chroot 变量的操作
${debian_chroot:+($debian_chroot)}
到底是什么?
${debian_chroot:+($debian_chroot)}
返回什么取决于文件 /etc/debian_chroot 是否存在,如果文件存在,则文件中的内容就是 ${debian_chroot:+($debian_chroot)}
的值${debian_chroot:+($debian_chroot)}
肯定是空值,我现在用的电脑系统是 ubuntu 20.04,我在这台电脑的 /home/whowin/chroot-fs/ubuntu-xenial-16.04/ 目录下做了一个 ubuntu 16.04 的 chroot 目录,并且在 /home/whowin/chroot-fs/ubuntu-xenial-16.04/etc/ 目录下建立了一个文件 debian_chroot
我们先来看看这个文件里有什么
图11:文件debian_chroot
现在我们 chroot 到这个目录下,我们看一下会有什么不同
图12:chroot提示符
我们看到在提示符的最前面多出了 “(u16.04)”,而 “u16.04” 正是文件 debian_chroot 中的内容
PS1="\u@\h [\t]$ "
\t
- 表示当前时间whowin@whowin-ubuntu:~$
whowin@whowin-ubuntu:~$ echo $PS1
\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\u@\h:\w\$
whowin@whowin-ubuntu:~$ PS1="\u@\h [\t]$ "
whowin@whowin-ubuntu [17:27:46]$
whowin@whowin-ubuntu [17:27:49]$
whowin@whowin-ubuntu [17:27:53]$
PS1="\#|\h|$(uname -r)|\$?$ "
\#
- 输入的命令数;$(uname -r) - 内核版本号;\$?
- 最后一个命令的返回码(正常返回应为0,非0表示出错)whowin@whowin-ubuntu:~$
whowin@whowin-ubuntu:~$
whowin@whowin-ubuntu:~$ PS1="\#|\h|$(uname -r)|\$?$ "
3|whowin-ubuntu|4.15.0-175-generic|0$ echo "hello"
hello
4|whowin-ubuntu|4.15.0-175-generic|0$ LS
程序“LS”尚未安装。 如需运行 'LS',请要求管理员安装 'sl' 软件包
5|whowin-ubuntu|4.15.0-175-generic|127$ uname -r
4.15.0-175-generic
6|whowin-ubuntu|4.15.0-175-generic|0$
whowin@whowin-ubuntu:~$ function cpucount {
> cat /proc/cpuinfo|grep processor|wc -l
> }
whowin@whowin-ubuntu:~$ PS1="\u@\h:[`cpucount`]$ "
whowin@whowin-ubuntu:[6]$
~/.bashrc
脚本中,使你的提示符与众不同(\e[4m)
,让 “$” 提示符闪烁(\e[5m)
PS1="\e[4m\u\e[0m@\h:\w\e[5m$\e[0m "
既然有 PS1 变量,自然还有 PS2 变量,实际上还有 PS2、PS3 和 PS4,这些变量又是做什么的呢?先看 man 手册中的说明
图13:man手册中关于PS2,PS3,PS4的说明
PS2 变量
先来看一下 PS2 的值
图14:PS2变量的值
PS2 是一个二级提示符,在交互式 shell 中允许在命令行中输入脚本(比如循环、条件),我们可以在 shell 提示符下试一下下面一段脚本
whowin@whowin-ubuntu:~$ if who | grep -q whowin
> then
> echo whowin is working online
> else
> echo whowin is a lazy boy
> fi
whowin is working online
whowin@whowin-ubuntu:~$
其中的提示符 “>” 就是由 PS2 变量定义的,我们可以试着改一下 PS2 变量卡按一下效果
whowin@whowin-ubuntu:~$ PS2="# "
whowin@whowin-ubuntu:~$ if who |grep -q whowin
# then
# echo whowin is working online
# else
# echo whowin os a lazy boy
# fi
whowin is working online
whowin@whowin-ubuntu:~$
很显然,二级提示符由 "> " 变成了 "# "
PS3 变量
PS3 变量定义的是使用 select 命令时的提示符,select 是 bash 的一个内建命令,但其具体用法不在本文的讨论范围内
先来看一下 PS3 的值
图15:PS3变量并不存在
PS3 并不存在,我们暂时不管这个,直接运行一个例子,下面例子在交互式 shell 下写了一段 select 的脚本
图16:PS3的默认值
按照 man 手册的说明,标记成黄色的 "#? " 就应该是 PS3 的值,可是前面看到明明 PS3 不存在呀,实际上,当 PS3 不存在时,会使用其默认值 "#? " 来代替 PS3
为了明显起见,我们姑且先把 PS3 指定一个值 "## " (有别于其默认值 "#? ")
PS3="## "
我们重新运行上面那段脚本
whowin@whowin-ubuntu:~$ set|grep PS3
whowin@whowin-ubuntu:~$ PS3="## "
图17:select的例子
在这个例子中,红框内的是 PS2 定义的二级提示符,绿框内的数字是从键盘输入的,黄色的 "## " 就是 PS3 定义的提示符
但是我们如果把这段脚本写入一个脚本文件运行,可能又有些不同
whowin@whowin-ubuntu:~$ cat ps3.sh
#!/bin/bash
select command in who ps quit
do
case $command in
who)
who
;;
ps)
ps
;;
quit)
break
;;
esac
done
exit 0
whowin@whowin-ubuntu:~$ chmod +x ps3.sh
whowin@whowin-ubuntu:~$ PS3="## "
图18:在脚本文件中运行的 select 的例子
这个地方就有点怪了,尽管我们在运行脚本文件前已经把 PS3 设置成 "## ",但运行起来时 select 的提示符仍然是默认的 "#? “;其实这没什么奇怪的,我们前面在交互式 shell 命令符下运行这段脚本时,是在当前环境下,我们在当前环境下设置了 PS3,然后又在当前环境下运行了脚本;而在脚本文件中运行时,bash 会首先为脚本建立一个子进程,然后在这个新环境中运行这个脚本文件,而我们并没有把 PS3 设置成环境变量,所以 PS3 的值并不会传递到新环境中,那么在运行脚本文件的新环境下实际上就是没有 PS3 的,那自然就会使用 PS3 的默认值去显示了;解决的办法也很简单,一是执行 export PS3 把 PS3 变成环境变量;二是在脚本文件里添加 PS3=”## " 的语句。
export PS3
./ps3.sh
whowin@whowin-ubuntu:~$ cat ps3.sh
#!/bin/bash
PS3="## "
select command in who ps quit
do
case $command in
who)
who
;;
ps)
ps
;;
quit)
break
;;
esac
done
exit 0
whowin@whowin-ubuntu:~$
PS4 变量
按照 man 手册的说明,PS4 定义的提示符会在跟踪脚本时,在显示 bash 命令前显示这个提示符,这个可能需要进一步说明一下
bash 在执行的时候,有一种调试(跟踪)模式,在这种模式下,bash 从脚本文件中读出一行,先把读出的命令显示出来,然后再显示执行结果,PS4 定义的提示符就会放在显示命令的这一行的开头
PS4 的默认值是 "+ "
先看一下当前 PS4 的值
图19:PS4 的值
我们使用调试模式运行前面的脚本 ps3.sh,在运行之前,先打印 PS4 的值和 ps3.sh 的内容
whowin@whowin-ubuntu:~$ set|grep PS4
PS4='+ '
whowin@whowin-ubuntu:~$ cat -n ps3.sh
1 #!/bin/bash
2
3 PS3="## "
4
5 select command in who ps quit
6 do
7 case $command in
8 who)
9 who
10 ;;
11 ps)
12 ps
13 ;;
14 quit)
15 break
16 ;;
17 esac
18 done
19 exit 0
20
whowin@whowin-ubuntu:~$
图20:调试模式下运行脚本
bash -x 是一种以调试模式运行脚本的方法,所有标记为黄色的地方就是 PS4 提示符,可以把这些行与 ps3.sh 文件内容比较一下,应该就能明白 PS4 提示符的作用
总结一下本文涉及的内容
email: [email protected]