第一次打开终端:提示符那行字在说什么

第一次打开终端:提示符那行字在说什么
隔壁老康难度 ★☆☆☆☆ · 前置:Linux 是什么,以及你为什么老是装不上软件 | 一句话结论:提示符里的
user@host:~#是 shell 拼出来的字符串而不是某种状态;路径只有绝对、相对、./..三条规则;Ctrl-C和kill不是同一件事,前者由 tty 层产生信号。
我第一次用 SSH 连服务器是入职前照着教程试的,密码输完,屏幕刷出一行 root@web-01:~#,光标在后面闪,除此之外什么都没有。我愣了几秒,试着敲了 ls,回车,出来一堆文件名,然后又回到那一行。
当时我心里是有点发虚的。前面这些字是谁打的,为什么是 # 而不是 $,为什么换个教程截图里又是 $。这些问题当时我一个都没敢问,因为看起来太蠢了,蠢到像是应该自己知道的。后来我发现不是,它们背后是四件不相关的事被塞进了同一个字符串里,而且其中一件(最后那个 #)如果看漏了,删库事故就发生在你身上。我在很长一段时间里都把那行字当成一个整体,就像看红绿灯只看颜色不看是哪个方向。
那一行字,四个字段
拿 root@web-01:~# 拆,四段各管一摊:
| 片段 | 含义 | 从哪来 |
|---|---|---|
root |
当前用户名 | $USER,底下一层是 uid |
web-01 |
主机名短名 | hostname,等同 uname -n |
~ |
当前目录,~ 是家目录缩写 |
$PWD |
# |
# 是 root,$ 是普通用户 |
PS1 里的 \$ |
最后那个符号值得单独说,因为它不是装饰,是当前权限的即时提示。同一个人、同一台机器,su 切到 root 之后它就变成 #,exit 回来又变回 $,这套切换是 PS1 里的一个转义 \$ 自己算的,不是你手动改的。很多删库事故的第一现场,是当事人根本不知道这个字符会变。
普通用户登进去长这样,alice@web-01:/etc/nginx$,目录段变成了完整路径。注意 ~ 只在当前目录恰好是家目录时才显示,它不是家目录的固定写法,是 bash 的一种省略($HOME 一样的时候用 ~,不一样就老实打印全路径)。
想看自己的提示符怎么配的,直接问变量就行:
1 | echo "$PS1" |
Ubuntu 默认输出大概是这么一坨:
1 | \[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\u@\h:\w\$ |
\u 用户名、\h 主机名、\w 完整路径、\$ 那个会自己切换的 #/$。最前面 \e]0;...\a 那段跟正文无关,它是给终端窗口标题栏设文字的。这一段我当初学的时候被绕进去很久,因为它看起来像乱码混进了提示符,而网上讲 PS1 的文章大多懒得提它。
你敲的命令,shell 去哪找
ls 这个字符串敲进去,shell 得先把它变成一个可以执行的文件,规则写在 PATH 里:
1 | echo "$PATH" |
1 | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
(这是桌面终端里的样子。SSH 登录进去的话通常不含 /sbin 和 /usr/sbin,为什么,后面讲目录树那篇会说。)
冒号分隔,从左往右扫,第一个撞上的就是它。type 能告诉你当前这个名字到底指向哪,而且它比 which 诚实:
1 | type -a ls |
1 | ls is aliased to `ls --color=auto' |
type -a 会把所有候选都列出来,从前往后就是优先级,所以上面这个输出的意思是,你敲 ls 实际跑的是一个别名,别名的本体在 /usr/bin/ls。顺带一句,/bin 和 /usr/bin 在很多发行版里已经是同一个目录了,那是后面目录树那篇的话题。
这里有个新手必踩的坑,PATH 里没有 .,所以你写 run.sh 会报 command not found,得写 ./run.sh:
1 | printf '#!/bin/bash\necho hi\n' > run.sh |
为什么这么设计,假设 PATH 里有当前目录,你在 /tmp 下解开一个别人给的压缩包,里面有 ls、cd,你敲 ls,跑的是他的 ls。这在早期 Unix 上是真被利用过的攻击路径(那个年代还流行互相 write 到别人终端上恶作剧),所以后来的默认设置干脆就不给。./ 的作用就是绕过 PATH,明确告诉 shell 就在当前目录找这个文件。这个设计我第一次听说的时候觉得有点小题大做,毕竟谁会傻到在别人的目录里敲 ls。后来自己给项目写部署脚本,PATH 里加了个 ./bin 方便调试,过了一个月完全忘了这回事,再在 ~/Downloads 里解压一个来路不明的包、顺手敲了个 make,那种后背发凉的感觉记到现在。规则不是给小心的人准备的,是给忘了的人准备的,而人一定会忘。
路径只有三条规则
这一层看着复杂,其实规则少得可怜:
- 以
/开头的,是绝对路径,从根开始,/var/log/nginx - 不以
/开头的,是相对路径,从当前目录开始,log/nginx .是当前目录,..是上一级,就这两个,没有例外
另外三个符号属于一年用不到几次、但看到别慌的那种:
| 写法 | 意思 |
|---|---|
~ |
你的家目录 |
~alice |
alice 的家目录 |
- |
上一个目录,只在 cd 里有效 |
cd - 有个小细节,它会把切换后的路径打印出来:
1 | $ cd /tmp |
因为这行输出,cd - 比手敲 cd .. 好检查,你能看见自己到底跳到哪了。我现在的习惯是目录跳多了之后一律用 cd - 在两个工作目录之间来回,比记路径可靠。
Tab 的价值不在省打字
按 Tab 补全谁都会,但有三个细节比这重要得多。第一,按一下没补全说明有歧义,按两下会把候选全列出来;第二,它补的不只是文件名,命令、用户名、主机名都补,ssh ro<Tab> 能补出 root,这一步在记不住机器上的用户名时很省事。
真正要命的是第三条,补全的结果当场就展开在屏幕上了,不是执行时才展开:
1 | $ rm -rf /tmp/foo<Tab> |
中间那一下,是你最后一次看清要删的是一批文件还是一个目录。rm -rf /tmp/foo* 和 rm -rf /tmp/foo/ 只差一个字符,后果差得很远,而 Tab 就是把那个字符提前摆到你眼前。刚学的时候我还嫌它慢,觉得手敲路径更有掌控感,直到有次在 ~/work 下面清空一个目录,按了 Tab 发现展开出来一串我压根没见过的目录名,才意识到自己其实站在哪都不确定。从那天起我养了个习惯,所有删东西的命令执行前先 pwd 一下,多花半秒,换来的确定感值这个价。
三个按键,三件不同的事
新手最容易在这里翻车,因为 Ctrl-C、Ctrl-D、Ctrl-Z 表面上都像取消,实际是三个毫不相干的机制。
| 按键 | 实际发生的事 | 结果 |
|---|---|---|
Ctrl-C |
发出字节 \x03,终端驱动把它翻成 SIGINT |
中断前台进程,进程可以捕获它做清理 |
Ctrl-D |
发出字节 \x04,被解释成 EOF |
输入结束;空行上按等于退出 shell |
Ctrl-Z |
发出字节 \x1a,翻成 SIGTSTP |
挂起前台进程,进程还在,只是停住了 |
区别摊开说。Ctrl-C 会杀进程,Ctrl-Z 不会,挂起之后进程还活着,状态是 T,fg 拉回前台继续,bg 放到后台继续跑。Ctrl-D 根本不杀进程,它只是告诉读输入的那个程序没有更多了,cat 不带参数时按 Ctrl-D 能退出,是因为 cat 读到 EOF 就正常返回了,不是被杀。而 Ctrl-C 也未必杀得掉,进程可以捕获 SIGINT,可以忽略它,如果它根本没在终端前台跑,终端发的信号也到不了它身上。我最早分不清这三者是因为把它们当成三个快捷键,后来才明白只有 Ctrl-C 和 Ctrl-Z 是 tty 层翻译出来的信号,Ctrl-D 从头到尾就是个文件结束符,压根不在信号体系里。真正让我记住差别的是第一次跑数据库导入,手快按了 Ctrl-Z,进程挂了但连接没断,端口还占着,重启服务一直报 Address already in use,那会儿还不知道 jobs 和 fg 是什么,最后靠 ps 找到那个 T 状态的进程才收拾干净。
想看清这条链,跑一个傻等的东西,sleep 100,然后按 Ctrl-C,屏幕出现 ^C,命令结束。中间的链条是这样的,终端程序把你按的 \x03 交给内核的 tty 层,tty 层看到这个字节、且 ISIG 标志开着,就给前台进程组发 SIGINT,sleep 收到,默认动作是终止。所以 Ctrl-C 从来不是取消输入,它是发信号,这个认知差决定了你以后能不能看懂,为什么我按了好几遍它还不死。
那个 tty 层还能调
上面说的 tty 层把字节翻译成信号这件事,本身是可以关掉的,因为整个翻译规则就是一组终端设置,用 stty 就能看和改:
1 | stty -a | head -3 |
1 | speed 38400 baud; rows 24; columns 80; line = 0; |
intr = ^C 就是哪个字节算中断,susp = ^Z 同理。这些都能改,把中断键换成别的,或者干脆关掉:
1 | stty intr ^X # 以后 Ctrl-X 才是中断 |
-isig 之后 Ctrl-C 就不再发 SIGINT 了,它只是一个字节 \x03 被送到前台程序,程序爱怎么处理怎么处理。vim 和 less 就是靠这套东西实现自己的键位(顺带说,ssh 也是,它进远程之前会把本地 tty 整个切成 raw 模式,isig、icanon、echo、ixon 一次全关掉,这样你按的 Ctrl-C 是以字符形式传过去、由远端 tty 翻译的,这就是为什么在 ssh 里按 Ctrl-C 不会杀掉 ssh 本身)。
同一层还管三样你迟早会遇到的东西:
| 设置 | 作用 | 典型症状 |
|---|---|---|
-echo |
别把输入回显到屏幕 | 输密码时看不到星号 |
icanon |
关掉行缓冲,逐字符读取 | 按了键不等回车就有反应 |
SIGTTIN/SIGTTOU |
后台进程想读(写)终端时收到的信号 | 后台跑的程序莫名停住,ps 里是 T |
最后那个值得单独记,bg 之后一个需要读终端输入的程序会立刻停住,不是它卡了,是内核发了 SIGTTIN 把它拦下来了。用 stty tostop 可以连写操作也一起拦。
改一次提示符
观察提示符和路径:
1 | echo "$PS1" |
看 Ctrl-C 究竟发了什么信号。开两个终端,第一个跑 sleep 100,第二个查它的 PID:
1 | pgrep -a sleep |
1 | 48213 sleep 100 |
回到第一个终端按 Ctrl-C,再在第二个终端查一次,什么都没了。进程是被一个信号终结的,不是被按键本身终结的。
再验证 Ctrl-Z 只是冻结。重新跑 sleep 100,按 Ctrl-Z:
1 | ^Z |
Stopped 是关键词,jobs 里也还是它。进程还在,fg 拉回来,或者 kill %1 干掉。这个还在的性质值得记住,跑一个长任务时误按 Ctrl-Z,它不是死了,是冻住了,nohup 和 & 都救不了它,得 bg 让它继续。
终端和 shell 是两个进程
终端就是 shell?不是,终端是窗口程序,shell 是跑在窗口里的那个进程。Ctrl-C 那套是终端和 tty 层处理的,跟 shell 关系不大,shell 这时只是在等你回来。
Ctrl-C 一定能杀掉进程?它发的是 SIGINT,可以被捕获、被忽略。还有一种情况连 kill -9 都无效,进程处于 D 状态,不可中断睡眠,那是进程状态那篇要处理的事。
# 只是样式?它意味着你现在是 root。养成扫一眼的习惯,能省掉几次心跳。
Tab 补全是 shell 干的?大部分情况是 bash 的内建补全,但也可以被 bash-completion 这类包接管,行为会更丰富。到底谁在干活,complete -p 能看出来。
~ 永远是家目录?它是 shell 展开的,展开成 $HOME,$HOME 被改坏,~ 跟着坏。真正的家目录记在 /etc/passwd 里。
终端里没有撤销这回事,rm 删掉的东西不会进任何回收站,这也是为什么上面 Tab 那一条值得花两分钟记住。











