终端命令速查
文件、文本、git、进程、网络——227 条命令,附常用选项和示例
⌨️ 命令和选项要照原样敲,所以不翻译,只有说明跟着你的语言变。
文件与目录32
移动、删除和查找。有些命令不会再问一遍,所以顺手加 -i 是在救自己。
ls列出目录里有什么;以点开头的名字要加 -a 才会显示。cd切换 shell 的当前目录;它是 shell 内建命令,所以脚本或子 shell 里的 cd 不会影响外面的 shell。pwd打印当前所在目录;如果你是从符号链接进来的,默认显示链接路径,真实路径要用 -P。mkdir新建目录;不加 -p 时,父目录不存在或目录已存在都会报错。rmdir只删除空目录;里面有任何东西都会拒绝,所以大家才改用 rm -r。rm彻底删除文件;没有回收站,rm -rf 里多一个空格就会毁掉不该删的东西。cp复制文件或整个目录树;目标是否已存在,决定源是变成那个名字还是被放进它里面。mv重命名或移动;不加 -i 或 -n 会不声不响地覆盖目标文件。touch把文件时间改成现在,文件不存在就建一个空的;内容一个字节都不动。cat把文件一个接一个原样打印出来;一次全倒出来,长文件该用 less,而 cat file | grep 多了一根管子。less一屏一屏地看文件,不必整份读入;/ 搜索、q 退出,和 more 不同,它到末尾不会自己关闭。head打印文件开头;给多个文件时,除非加 -q,每个文件前都会插一行标题。tail打印文件末尾,并用 -f 持续跟着看;日志轮转之后还想跟上,必须用 -F。find遍历目录树逐个匹配条件;把模式写成 -name '*.log' 加引号,否则 shell 会先把它展开。locate查的是事先建好的索引而不是磁盘,所以秒出结果,但看不到上次 updatedb 之后新建的文件。tree把目录结构画成缩进的树;macOS 和精简的 Linux 镜像里默认没有,得先安装。du统计文件真正占用的磁盘空间;它按分配的块来算,所以总数和文件大小之和、和 df 都不一样。df按文件系统报告已用和剩余空间;字节还有但 inode 用光也会写不进去,这只有 df -i 看得到。ln给文件再起一个名字;用 -s 建的符号链接可以指向任何地方,但目标一移动就断,而相对路径是相对链接所在目录解析的。stat把 inode 里的记录原样显示:大小、权限位、属主和三个时间;格式选项在 GNU 是 -c,在 BSD 是 -f,而 GNU 的 -f 完全是另一个意思。file靠内容而不是文件名判断文件类型;它无视扩展名,所以名为 .jpg 其实是 PDF 的文件会在这里露馅。basename去掉路径里的目录部分,需要时还能去掉后缀;它只处理字符串,从不检查文件是否存在。dirname只保留路径的目录部分;路径里没有斜杠时结果是一个点,和 basename 一样只动字符串。realpath把任意路径解成一条绝对路径,符号链接和点号都展开;macOS 上是 BSD 版,没有 --relative-to 这类 GNU 选项。rsync只传两棵目录树之间的差异,本地或经 ssh 都行;源路径末尾的斜杠表示它的内容,不写就会多套一层目录。mount把文件系统挂到某个目录上;挂载点原有的内容只是被遮住而不是被删掉,卸载后又会出现。umount把已挂载的文件系统卸下;命令名里没有 n,只要还有进程在里面开着文件或把工作目录留在其中,它就会报 device is busy。readlink打印符号链接指向什么,只走一跳,除非加 -f;对不是链接的文件它什么都不输出并以错误退出。mktemp用别人猜不到的名字创建临时文件或目录并打印路径;它不会自己清理,要配一个退出时的 trap。sha256sum打印文件的 SHA-256 指纹,用来核对下载是否完整;哈希相同只说明字节一致,并不说明来源可信。truncate把文件长度设成正好那么大:变大只是打个稀疏空洞并不真占空间,变小则不作提醒地丢掉尾部。shred删除前先覆写内容让它难以恢复,但在 SSD 和 btrfs、APFS 这类写时复制文件系统上旧块可能还在,真正的办法是整盘加密。
文本处理32
它们本来就是用管道串起来用的。串三条往往比让一条命令做完所有事更短。
grep从文本里挑出匹配模式的行;默认是基本正则,所以 + ? | 和 () 需要 -E 或反斜杠才生效。sed逐行改写文本,最常用 s/old/new/;原地修改的 -i 在 GNU 和 BSD 上写法不同,末尾不加 g 只会替换每行的第一处。awk把每行切成字段再跑一段小程序;$0 是整行、$1 是第一个字段,默认分隔符是连续的空白而不是单个空格。sort给行排序;不加 -n 就按文本比较,于是 10 排在 9 前面,而顺序由 locale 决定,除非在前面加 LC_ALL=C。uniq只把相邻的重复行合成一行,看不到散落各处的重复,所以几乎总是接在 sort 之后。wc统计行数、词数和字节数;-l 数的是换行符,所以最后一行没有换行就不会被算进去。cut切出固定字段或字符位置;分隔符只能是单个字符,且连续分隔符不会合并,所以按空格对齐的表格该交给 awk。paste把文件并排拼在一起,中间用制表符隔开;和 join 不同,它按行的位置对应,从不看内容。tr逐个字符地替换或删除;它只读标准输入,直接给文件名会失败,必须用 < 重定向或用管道喂给它。join按共同字段把两个文件连起来,就像数据库的 join;两边必须先按该字段排好序,否则会悄悄丢行。comm比较两个已排序的文件,输出三列:只在左、只在右、两边都有;输入没排序结果就是乱的,而选项是用来隐藏列而不是选列。diff显示两个文件之间改了什么;patch 和代码评审期待的是 -u 统一格式,而有差异时退出码是 1,会让开了 set -e 的脚本中断。patch把 diff 真正打到文件上;-p 决定丢掉路径前面几层,而 git 的补丁因为有 a/ 和 b/ 前缀需要 -p1。tee把输入同时写进文件并送到屏幕;要写 root 拥有的文件就靠它,因为 sudo cmd > file 里的重定向是你自己的 shell 打开的。xargs把输入的行变成另一个命令的参数;名字里有空格就会出错,除非两边都用 NUL,也就是 find -print0 配 xargs -0。echo原样打印参数并换行;-e 和反斜杠的处理在不同 shell 里不一样,要稳妥就用 printf。printf像 C 的 printf 那样按格式串生成文本;它不会自己加换行,还会在参数没用完时把格式从头再走一遍。jq用自带的一套小语言过滤和重塑 JSON;它常常不是默认装好的,而去掉字符串引号要靠 -r。column给字段补空格让各列对齐成表格;连续的分隔符只算一个,所以有空单元格的 CSV 会整行错位。fold按固定宽度硬折长行,插入真正的换行符;不加 -s 会把词切断,而且与终端的软换行不同,它改的是文件内容。nl一边输出一边给行编号;默认跳过空行,所以编号和编辑器不一致,加上 -b a 才对得上。rev把每一行内部的字符顺序颠倒,而不是把行的顺序倒过来,那是 tac 的事。split把文件切成若干块;表头只留在第一块里,文件名以 aa、ab 结尾,除非用 -d 改成数字。csplit按模式出现的位置切文件,而不是按固定大小;它打印的数字是每块的字节数,{*} 表示匹配多少次就重复多少次。expand把制表符换成空格;Makefile 必须是真正的 tab,对它跑 expand 会让构建失败。shuf把输入的行按随机顺序输出;它属于 GNU coreutils,macOS 上没有,而常用替代 sort -R 会把相同的行聚在一起。tac把文件的行从最后一行倒着打印;它属于 GNU coreutils,macOS 上用 tail -r,也不要和倒转字符的 rev 混淆。strings从二进制文件里抽出看起来像文字的部分;只要像文字就捞,所以大部分输出是巧合而不是含义。iconv把文本从一种字符编码转成另一种;遇到无法映射的字节就停下,除非加 -c,而且它不会替你猜输入编码。vim带模式的编辑器:不按 i 之前打字什么都不会输入,退出要先按 Esc,再用 :wq 保存或 :q! 丢弃。nano一个把命令写在屏幕下方的简单全屏编辑器;^O、^X 里的尖号表示 Ctrl,不是那个字符本身。base64把字节转成 ASCII 文本再转回来,让二进制能走只允许文本的通道;这是编码不是加密,解码选项在 GNU 是 -d,老 macOS 是 -D。
git45
难的地方在于撤销有好几种:把分支的指向挪走,和补一个反向提交,不是一回事。
git init把一个文件夹变成新的 git 仓库;只用 init 不会跟踪任何文件,还要再 add 和 commit。git clone把远端仓库连历史一起复制下来;--depth 1 的浅克隆没有旧提交和别的分支,之后得用 --unshallow 补齐。git config读写 git 的配置;不加 --global 只写进当前仓库,重新克隆一份就没有这条设置了。git init --bare建立没有工作目录的仓库,专门用来接收 push;里面的文件无法编辑,但也不会因为分支正被检出而拒绝 push。git add把文件当下的内容放进暂存区;add 之后再改,这次的改动不会进去,得再 add 一次。git commit把暂存区里的内容记成一个提交;-a 只会带上已跟踪文件的改动,新建的文件不算。git status显示哪些已暂存、哪些改了、哪些还没被跟踪;未跟踪的文件默认只按文件夹折叠显示,要逐个看得加 -uall。git diff只显示还没暂存的改动;所以刚 git add 完看着是空的,那时要用 --staged。git log按从新到旧列出提交历史,但只列当前 HEAD 的祖先,要看别的分支得加 --all。git show把一个提交的说明和改动一起打印出来;写成 <commit>:<path> 就能取出文件的旧内容而不动你的工作副本。git blame给每一行标出最后改动它的提交;只做格式化的提交会盖住真正的作者,这就是 -w 和 --ignore-rev 的用处。git branch在本地列出、创建和删除分支;删掉本地的不会动远端,服务器那边还要 git push origin --delete。git checkout既能切分支又能还原文件的旧命令,在 git 2.23 里拆成了 switch 和 restore;checkout -- <file> 会把未提交的改动彻底删掉。git switchgit 2.23 引入的命令,只负责在分支之间切换;它从不改写文件内容,那是 restore 的事。git restoregit 2.23 引入的命令,用来还原文件内容;默认会覆盖工作区的文件,未提交的改动就没了,而 --staged 只是取消暂存。git merge把另一个分支的工作并进当前分支;--squash 不会留下合并记录,所以之后 git 仍然认为那个分支没被合并。git rebase把你的提交重新放到另一个基点上;哈希会变,所以别对别人已经拉走的分支做 rebase,那种情况用 merge。git reset把分支指针移到别的提交:--soft 让改动全留在暂存区,--mixed(默认)连暂存区一起清掉但改动还在文件里,--hard 连文件里的改动也一起丢掉,找不回来。git revert通过新增一个相反的提交来撤销旧提交;它不删历史,这正是它和移动指针的 reset 的区别,已经推上去的提交要用 revert。git cherry-pick把某一个提交以新的哈希复制到当前分支;之后再合并那个分支时,同一处改动可能出现两次。git stash把没做完的改动先收起来,让工作区变干净;不加 -u 时未跟踪文件会留在原地,pop 取出后会删掉那条记录,apply 则保留。git clean删除未跟踪的文件;git 从未保存过它们,删了就回不来,-fdx 连本地 .env 这类被忽略的文件也一起清掉,先跑一次 -n。git remote管理远端仓库的名字和地址;它只是记下来,在你真正 fetch 或 push 之前什么都不会传输。git fetch只把远端的新提交取下来,不动你的文件和分支,要生效还得 merge 或 rebase;服务器上已删的分支在 --prune 之前还会留着。git pull把 fetch 和 merge 一次做完,这正是那些意外合并提交的来源;用 --rebase 或 --ff-only 可以避免。git push把你的提交推到远端;--force 会覆盖远端分支,连别人推的提交都可能被抹掉,所以用 --force-with-lease,远端一动它就拒绝。git tag给某个提交贴上版本之类的名字;普通的 git push 不会带上标签,得按名字单独推。git rm把文件从索引和磁盘上一起删掉;--cached 会保留文件、只停止跟踪,但旧内容仍留在历史里,泄露的密钥并不会因此消失。git mv移动文件并顺手把这次改动放进暂存区;git 不保存重命名,而是靠内容相似度推断,所以只改大小写的重命名在 macOS 和 Windows 上需要 -f。git ls-files列出索引里的路径而不是磁盘上的文件,所以脚本遍历已跟踪文件时靠它最稳;-i 不能单独用,必须配 -c 或 -o 再加 --exclude-standard。git update-index直接改动索引;这些标记只在本地、不会被推送,而想保留自己对已跟踪配置文件的改动,该用 --skip-worktree 而不是 --assume-unchanged。git sparse-checkoutgit 2.25 加入的命令,只把你指定的目录留在磁盘上;历史仍是完整的,cone 模式接受目录名而不是 glob 通配符。git reflog记录 HEAD 和各分支待过的每个位置;被 reset --hard 或删分支弄丢的提交就在这里找,条目默认 90 天后过期,而且这份记录只在你本机。git bisect不断把提交区间对半切,找出第一个出问题的提交;结束时必须 git bisect reset,否则你会一直停在游离的提交上。git describe以最近的标签为基准,把当前提交命名成 v1.2.0-5-gabc1234 这样;默认只看附注标签,轻量标签要加 --tags,一个标签都没有时要加 --always。git rev-parse把 HEAD 或 v1.2.0^ 这类名字解析成真正的哈希,也能告诉你仓库在哪;脚本用 --abbrev-ref HEAD 取分支名,在仓库之外它会以错误退出。git fsck检查对象库并列出没有任何引用指向的提交,这是 reflog 条目也没了之后找回被 reset 弄丢的工作的第二条路;它只报告,不修复。git gc把零散对象打包、丢掉无人引用的,从而缩小 .git;--prune=now 连 reflog 还拉着的提交一起丢掉,撤销 reset 的安全网也就没了。git archive把某个提交时的文件导出成 tar 或 zip;不会带上 .git 和未跟踪文件,所以比直接压缩文件夹更干净。git apply把一个 diff 文件打到当前工作区;它不创建提交,也不读取补丁里记的作者,所以邮件收到的补丁该用 am。git am把邮件里的补丁变成提交,保留原作者和原始信息;中途失败时会停在系列中间,得用 --continue、--skip 或 --abort 收尾。git format-patch为每个提交写出一个邮件格式的 .patch 文件;format-patch main 指的是 main 之后的提交,不含 main 本身。git shortlog按作者把提交分组计数,写发布说明很方便;同一个人用了两个邮箱时,除非用 .mailmap 归并,否则会被算成两个人。git worktree从同一个仓库再签出一个工作目录,不用重新克隆就能同时开两个分支;同一个分支不能被签出两次,手动删掉目录后条目要等 prune 才清掉。git submodule把另一个仓库固定在某个提交上,嵌进你的仓库里;普通克隆后那个目录是空的,要 update --init,而父仓库记住的是提交而不是分支。
进程与系统36
看什么在跑、什么在吃机器。先弄清是什么,再决定杀不杀。
ps把此刻正在运行的进程拍成一张快照;`ps aux` 是不带减号的 BSD 写法,`ps -ef` 是 POSIX 写法,把两种混着用是最常见的错误。top每隔几秒重画一次进程列表;它的选项在 Linux 和 macOS 上完全不同,只取一次要用 Linux 的 `-b -n 1` 或 macOS 的 `-l 1`。htop带颜色和鼠标支持的 top 重写版,F9 就能直接发信号,但系统默认不带,得先 `apt install htop` 或 `brew install htop`。kill给指定 PID 发信号让进程退出;默认的 TERM 会留出清理时间,而 `-9` 无法被拦截,打开的文件和锁文件会留在原地。killall按名字而不是 PID 一次结束多个进程;名字必须和可执行文件完全一致,所以 `killall chrome` 抓不到叫 "Google Chrome" 的进程。pkill把匹配某个模式的进程全部杀掉;不加 `-f` 只比对进程名,而模式写得太宽会连带杀掉别的,所以先用同样的模式跑一遍 pgrep。pgrep用和 pkill 完全相同的规则,只打印匹配到的 PID,所以它正好是动手杀之前的试运行。jobs这是 shell 内建命令,只给当前 shell 的后台任务编号,别的终端里的任务不会出现,引用时写 `%1` 而不是 PID。bg把用 Ctrl+Z 停住的任务放回后台继续跑;它仍然属于当前 shell,关掉终端就一起结束,除非用了 nohup 或 disown。fg把后台或已暂停的任务拉回终端前台;只对当前 shell 的任务有效,指定时用 `%1` 这样的任务号,而不是 PID。nohup让命令忽略挂断信号,这样退出登录后它还在跑;它并不会帮你放到后台,`&` 得自己加,输出会悄悄堆在 ./nohup.out 里。nice以更低的 CPU 优先级启动命令;取值从 19(最谦让)到 -20(最抢),负值要 root,而且它只影响 CPU 不影响磁盘(磁盘用 ionice)。renice修改已经在运行的进程的优先级;普通用户只能把它调得更谦让,想调回来需要 root。systemctl启动、停止和查看 systemd 服务;`enable` 只决定开机行为,想现在也启动要用 `enable --now`,改过 unit 文件后必须 `daemon-reload`。journalctl读取 systemd 以二进制格式收集的日志;没有 /var/log/journal 时日志一重启就没了,而 `-u` 要写单元的准确名字。service这是 SysV 时代留下的一层薄壳;在 systemd 机器上它只是把命令转给 systemctl,而且认得的动词更少。crontab管理每个用户的定时任务表;cron 运行时 PATH 几乎是空的,也不会读登录配置,所以路径要写绝对路径,而 `crontab -r` 会不问一声删掉整张表。at在你指定的时刻只运行命令一次;需要 atd 守护进程在跑(很多发行版根本没装),而且输出是发邮件给你,不会打印到屏幕。uptime一行显示机器已开机多久和平均负载;那三个数字是 1、5、15 分钟内可运行进程的平均数,不是百分比,所以四核上 4.00 就是满负荷。free在 Linux 上统计还剩多少内存和交换区;该看的一列是 available 而不是 free,因为缓存需要时会还回来,而 macOS 根本没有 free(用 vm_stat)。vmstat按固定间隔不断打印内存、交换、磁盘和 CPU 的计数;第一行要丢掉,它是开机以来的平均值而不是此刻,然后先看 si/so 和 wa 两列。iostat按磁盘显示读写量和请求等待时间;第一段是开机以来的平均值,Linux 上需要装 sysstat,而 macOS 的选项不同,例如 `-w 1 -c 5`。lsblk以树状列出磁盘和其中的分区;它不显示尚未分区的空闲空间(那要用 fdisk 或 parted),而且只有 Linux 有,macOS 用 `diskutil list`。uname报出内核名称、内核版本和机器架构;它不会告诉你发行版,那要读 /etc/os-release(macOS 用 `sw_vers`),而 `-o` 只有 GNU 才有。hostname打印或设置本机名字;`hostname web01` 只保留到下次重启,要永久生效得用 Linux 的 `hostnamectl` 或 macOS 的 `scutil --set HostName`,而列出全部 IP 的 `-I` 只有 Linux 有。date按你要的格式打印当前时间,也能做日期加减;`+` 后面的格式各平台一致,但挪动日期在 GNU 是 `-d`,在 macOS/BSD 是 `-v` 或 `-j -f`,完全不同。env打印整个环境变量表,或者带上额外变量运行一条命令;那些变量只对这一次运行生效,你的 shell 里不留下任何东西。export给变量打上标记,让子进程能继承它;它随这个 shell 一起消失,想长期保留就写进 ~/.bashrc 或 ~/.zshrc,而只写 `VAR=x` 不加 export,你启动的程序看不到它。which在 PATH 里查找同名可执行文件在哪;因为它只看 PATH,对别名、函数和 shell 内建命令会给出错误答案,老实的答案来自 `type -a` 或 `command -v`。type直接问 shell 某个名字到底是什么——别名、函数、内建还是文件,这正是只看 PATH 的 `which` 看不到的部分。lsof列出所有打开的文件和套接字,这就是回答“谁占着 3000 端口”的办法;不加 sudo 只能看到自己的进程,所以结果为空并不表示端口是空的。fuser找出正在使用某个文件、挂载点或端口的进程,需要时把它杀掉;`-k` 发的是 SIGKILL,不留清理时间,而且它只在 Linux(psmisc)上有,macOS 版只有 `-cfu`。watch每隔几秒重新运行同一条命令并重画屏幕;带管道或 `*` 的命令一定要加引号,否则 shell 只会先对 watch 本身展开一次,而且 macOS 默认没有它。timeout给命令加上时间上限,超时就掐掉并以 124 退出;默认信号是 TERM,卡死的程序可以不理它,所以要加 `-k` 随后补一个 KILL(属 GNU coreutils,macOS 没有)。strace把进程发出的每个系统调用逐条打印出来,用来看它卡在哪里;输出走的是 stderr,要用 `2>&1` 转过来,需要 ptrace 权限(通常是 sudo),而且只有 Linux 有(macOS 用 dtruss)。dmesg读取内核的环形缓冲区,磁盘或 USB 出问题后第一个该看的地方;缓冲区大小固定,旧行早被挤掉了,而现在多数发行版要 root(macOS 用 `log show`)。
网络28
连不上的时候,这些能把范围缩小——是名字解析不了、没有路由,还是端口被挡。
curl取回一个网址并把正文打印到终端;和浏览器不同,不加 `-L` 它不会跟随跳转,所以遇到 301 看起来像是空响应。wget把网址下载成文件,并且会自己跟随跳转;这正是它和 curl 的区别,curl 不特意指定就打印到终端,而 macOS 默认不带 wget。ping发送 ICMP echo 请求,看对方是否应答、要多久;没有应答不等于机器挂了,很多防火墙只丢 ICMP 而 TCP 照常。traceroute逐行列出数据包到目的地途经的路由器;中间出现 `* * *` 不代表链路断了,只是那一跳不愿意回应。dig直接向 DNS 服务器提问并原样显示记录;它绕过 /etc/hosts 和系统缓存,所以结果可能和浏览器看到的不一样,有些发行版还要先装 dnsutils 或 bind-utils。nslookup把名字解析成地址的老工具:它还能跑,但输出格式和退出状态都不稳定,写脚本没法用,如今这活由 dig 接了过去。host查一个名字时用两三行短短地给出地址和邮件服务器;快速确认最省事,但要看完整记录还得用 dig。ss在 Linux 上列出已打开套接字的现代做法,是 netstat 的接班人,在连接很多的机器上快得多;注意 `-p` 只有以 root 运行时才显示占用进程。netstat查看套接字和路由表的老办法;在 Linux 上这活已交给 ss,而且常常根本没装,macOS 虽然保留了它,却无法说出套接字属于哪个进程,那要靠 lsof。ipLinux 上统管地址、路由和邻居表的单一命令,取代了 ifconfig、route 和 arp;用它改的东西一重启就没了,要长期生效还得写进网络配置。ifconfig查看和修改网络接口的老命令;在 Linux 上已由 `ip addr` 取代,甚至可能没装,而且看不到用 ip 添加的全部地址——不过在 macOS 上它仍然是正主。route读取和修改内核路由表,但在 Linux 上这活已让给 `ip route`,而 BSD/macOS 的语法完全不同,例如 `route -n get default`,两边的例子不能互抄。arp显示同一局域网内邻居的 MAC 地址表;路由器之外的机器绝不会出现在里面,而 Linux 上现在的工具是 `ip neigh`。nc (netcat)开一条裸的 TCP 或 UDP 连接来看端口是否应答,也能在两台机器间当管道;同名却互不兼容的 netcat 有三种(OpenBSD、GNU、Nmap 的 ncat),所以 `-z` 或 `-p` 在你这台上可能根本没有。telnet开一条明文 TCP 会话,来回的内容原样走在线路上,所以绝不能用它登录(那是 ssh 的活);它剩下的用处只是看端口是否应答,退出要按 Ctrl+] 再输 quit。ssh在另一台机器上打开一个加密的 shell;私钥文件只要别人也能读,ssh 就直接拒绝,所以要 `chmod 600`,而端口在这里写 `-p`,到了 scp 和 sftp 里却是 `-P`。ssh-keygen生成一对公钥和私钥;放到服务器上的只有 `.pub` 那一半,而不设口令生成的密钥就是一个普通文件,谁复制走都能直接登录。ssh-copy-id把你的公钥追加到服务器的 ~/.ssh/authorized_keys,好让它不再问密码;第一次做这件事时密码登录必须还开着,而 `-i` 要给 `.pub` 文件而不是私钥。sftp架在 ssh 连接之上的交互式传输工具;端口选项是大写 `-P` 而不是 ssh 的 `-p`,而且它不能执行远端 shell 命令,只认自己的 put、get、ls。scp通过 ssh 一次性复制文件;端口是大写 `-P`,远端路径要带冒号,而体量大或要反复传的场合 rsync 更合适,因为它会跳过没变的文件并能续传。iptables修改正在生效的包过滤规则;规则自上而下匹配、先命中者胜,所以追加在 ACCEPT 之后的 DROP 永远不会生效,不保存的话重启就没了,而在放通 22 端口之前把默认策略设成 DROP,会把你自己关在远程机器外面。ufw在 Debian 和 Ubuntu 上用简单词汇包装 iptables 的防火墙;`enable` 之前先放通 SSH,否则会切断你正坐着的这条连接,而 Docker 把端口直接插进 iptables,ufw 的规则管不到那里。tcpdump把线路上经过的数据包原样抓下来;它需要 root,抓什么写在选项之后、用它自己的过滤语法如 `port 443 and host x`,而在繁忙接口上不加过滤直接抓,屏幕瞬间就被埋掉。whois查询域名或 IP 段的注册信息;个人字段如今大多被隐去,而每个顶级域的服务器各按自己的格式回复,所以在脚本里解析这些输出很容易失效。nmap扫一台主机看哪些端口应答;`-sS` 和 `-O` 需要 root,而扫描不属于你的机器会在对方日志里被记成攻击,在有些国家还违法,所以只在自己的网络里用。openssl s_client开一条裸的 TLS 连接,用来查看证书和握手过程;不写 `-servername`,一个地址上放多个站点的服务器就会给你默认证书,而不加 `< /dev/null` 它会一直等着输入不返回。speedtest通过真的向附近服务器收发流量来测线路速度;任何系统都不自带它,而且同名的其实有两个程序(Ookla 的 speedtest 和 Python 的 speedtest-cli),选项互不相同。mtr把 traceroute 和 ping 合成一张持续刷新的表,正是抓时断时续丢包的工具;中间某跳显示的丢包通常只是那台路由器在限速回应,只有一直延续到最后一跳的丢包才是真问题。
压缩与传输14
打包和压缩是两件事:tar 打包,gzip 压小,.tar.gz 是两样一起。
tar它只把文件打成一个包,本身并不压缩——压缩由 `-z`(gzip)、`-j`(bzip2)、`-J`(xz)来做;创建 `-c`、解压 `-x`、查看 `-t` 三种模式只能选一个且要写在前面,而经典写法里 `-f` 必须是最后一个选项,包名紧跟其后。gzip把单个文件压成 .gz,而且默认在成功后删掉原文件,想留着就加 `-k` 或用 `-c` 写到标准输出;它从不打包多个文件,所以前面要配 tar。gunzip把 .gz 解开还原成原文件,成功后就删掉 .gz——想留着用 `-k`,同名文件已存在时得加 `-f`,而 `-c` 会把内容直接送到标准输出,一点也不落盘,和 zcat 一样。zip和 tar 不同,它自己就带压缩,也是 Windows 不装任何东西就能打开的那种格式;装目录必须加 `-r`,否则只把目录这一条记进去、里面的东西一个也没有,而 `-e` 设的密码是老式 ZipCrypto,很弱。unzip解开 .zip;不给 `-d` 就直接倒在当前目录里,所以先用 `-l` 看看包内有没有一层顶层文件夹,旧版 Windows 打的包要靠 `-O CP932` 才不乱码,而 `-P` 写在命令行里的密码会留在 shell 历史和 ps 里。bzip2比 gzip 压得更小,代价是更慢,而且和 gzip 一样会删掉原文件、只留下 .bz2,除非加 `-k`;配 tar 时对应的字母是 `-j`,解压用 `-d`,也就是 bunzip2 做的事。xz三种常见格式里它压得最小,但慢,而且 `-9` 大约要吃 700 MB 内存;默认只用一个核,加 `-T0` 才用满,不加 `-k` 原文件就没了,配 tar 时的字母是大写的 `-J`。zcat把 .gz 里的内容直接送到标准输出,完全不在磁盘上解开,所以能就地 grep 一份已经轮转的日志;`-f` 让没压缩的普通文件也一起通过,而 bzip2、xz、zstd 各有自己的 bzcat、xzcat、zstdcat。7z它把动作写在最前面——`a` 添加、`x` 带路径解压、`e` 摊平解压、`l` 列出——能读的远不止 .7z,zip、tar、rar 都行,压缩率也比多数工具高;`-o` 后面不能空格,目录要紧贴着写,而想连文件名一起藏起来,得在 `-p` 之外再加 `-mhe=on`。dd它按块原样复制,装机镜像就是这样写进设备的;`of=` 指到哪里,那里就从第 0 块起被覆盖,不问一声也没法撤销,所以按下回车前一定要用 lsblk 再确认一次设备名,用 `bs=4M` 提速,用 `status=progress`(macOS 按 Ctrl+T)看进度。split把大文件切成若干块;默认是按行切、每 1000 行一块,想按大小切就得写 `-b 100M`,不给前缀时碎片叫 xaa、xab,拼回去只用一句 `cat part_* > file`,而这能成立全靠后缀本身是有序的。cpio这是个老式打包工具,要打进去哪些文件由标准输入送来的名单决定,所以喂它的通常是 find;`-o` 写包、`-i` 解包、`-t` 看目录,initramfs 用的可移植格式是 `-H newc`,而别人给的包要加 `--no-absolute-filenames`,免得它往绝对路径里写。zstd这是现在这一代的压缩器:达到 gzip 的体积要快好几倍,`-19` 时能接近 xz 的体积,花的时间却只有 xz 的一小截;和 gzip 不同,它默认保留原文件(要删就加 `--rm`),`-T0` 吃满所有核心,在 tar 里写成 `--zstd`。rar这是私有格式,免费的只有解包那一头:要新建得用付费的 rar,而别人发来的 .rar 用 `unrar x` 就能解开,或者干脆 `7z x`,不必从 RARLAB 装任何东西;`x` 保留包里记的路径,`e` 把它们摊平,`-hp` 连文件名一起藏,`-v100m` 切成多卷。
权限与归属15
数字模式和符号模式说的是同一件事:755 和 u=rwx,go=rx 是同一个权限。
chmod设置文件和目录的权限位;chmod -R 755 会把普通文件也变成可执行,所以想放过文件就用大写 X。chown修改文件的属主和属组;把文件送给别人只有 root 能做,所以它几乎总要配 sudo。chgrp只改文件的属组,chown 用 :group 也能做到;不是 root 的话,只能指定自己所属的组。umask决定新建文件要扣掉哪些权限位;它是减法而不是赋权,所以 022 得到文件 644、目录 755,而且不会动已经存在的文件。sudo以别的用户(默认 root)身份跑一条命令;sudo cmd > file 里的 > 是你自己的 shell 打开的,所以对 root 拥有的文件会失败,那时要用 tee。su用目标用户自己的密码切换成那个用户,和问你自己密码的 sudo 相反;不写那个短横线就会保留你自己的 PATH,这正是 command not found 的常见原因。id打印进程运行时的用户和组 id;id -u 返回 0 就是 root,这也是脚本判断管理员权限的标准做法。groups列出一个用户属于哪些组;刚用 usermod 加的组要重新登录才会出现,因为这份列表在会话开始时就定下了。whoami打印当前生效的用户名,而不是你登录时的名字,所以在 sudo 或 su 里它显示 root。passwd修改登录密码并把哈希写进 /etc/shadow;macOS 几乎没有 Linux 那些选项,改用 dscl 或系统设置。getfacl打印访问控制列表,也就是超出属主、属组和其他人的按用户额外权限;这是装了 acl 包的 Linux 的东西,macOS 用 ls -le 显示它自己那套 ACL。setfacl在不改属主的前提下给某个额外用户或组加权限;一旦有了 ACL,ls 会在权限后面显示一个 +,而 mask 条目可能悄悄压低你给出的权限。chattr设置文件系统级别的属性,比如不可修改;加了 +i 连 root 也得先取消才能改,而且只在 Linux 的 ext4、xfs、btrfs 上有效。xattr在 macOS 上读取和删除扩展属性,最常见的是 com.apple.quarantine,也就是应用已损坏那个提示的真正来源;Linux 用 getfattr 和 setfattr。visudo在保存前带着加锁和语法检查来编辑 /etc/sudoers;直接改那个文件一旦写错,所有人都用不了 sudo,而 visudo 正是为了防这件事。
包与运行时25
问题永远是什么装到哪里:装进这个项目,还是装到整台机器上。
npm install把 package.json 里写的依赖装进 node_modules 并更新 package-lock.json;从 npm 7 起 peer 依赖也会一起装,--production 已被 --omit=dev 取代。npm ci先删掉 node_modules,再严格按 package-lock.json 安装;它绝不改动 lockfile,一旦和 package.json 不一致就直接失败,所以 CI 用它而不是 install。npm run运行 package.json 里 scripts 段落中的命令;要把参数传给脚本,前面得先写一个单独的 --,而只有 start 和 test 可以不写 run。npm init生成一个 package.json;npm init <名字> 是完全不同的事,它会下载并运行 create-<名字>,就像 npx 那样。npx运行某个包的可执行文件,优先用 node_modules/.bin,找不到就临时下载;打错一个字就可能拉来陌生的包,加 --no 可以只用本地的。yarn add添加一个包并更新 yarn.lock;只敲 yarn 等于 install,而 Yarn 2 取消了 yarn global add,一次性工具改用 yarn dlx。pnpm add把包放进共享存储,再在 node_modules 里用链接指过去;在 workspace 根目录 pnpm 会拒绝直接 add,要用 -w 或 --filter 指明装到哪。npm audit报告已安装依赖中已知的安全漏洞;fix --force 会升到会破坏兼容的大版本,而藏在开发依赖深处的告警往往根本修不了。npm publish把包上传到 registry;带 @scope 的名字默认是私有的,所以要加 --access public,而版本号一旦用过就不能再用,先用 --dry-run 看清会打包哪些文件。node --version打印 PATH 上第一个 node 的版本;正因如此,用了版本管理器时编辑器终端或 CI 里看到的常常是另一个版本。nvm use切换当前 shell 使用的已装 Node 版本;改动只作用于这个 shell,新开的终端或 CI 会回到默认值,而 nvm 是个 shell 函数,脚本或 Makefile 里得先 source nvm.sh。brew install在 macOS 和 Linux 上下载预编译的 bottle 并安装;图形界面应用要加 --cask,而升到新版本是 brew update 和 brew upgrade 的事,不是 install 的。apt-get install从已经下载好的软件包列表里挑着装,所以列表过期时会报 "Unable to locate package",得先跑 apt-get update;脚本里用 apt-get 更稳,apt 是给人用的。apt update只是把软件包列表刷新一遍,什么都不会装或升,要真装还得跟一个 apt upgrade;刚建好的容器列表是空的,所以这是第一条命令。dnf install从启用的仓库里连依赖一起装,元数据到期后自己刷新,不像 apt 那样要单独 update;RHEL 8 以后 yum 其实是指向 dnf 的链接,Fedora 41 起换成了 dnf5。pacman -S从 Arch 的 sync 仓库安装软件包;因为是滚动发行,只用 -Sy 刷新列表再安装会造成部分升级、把库搞坏,所以一律用 -Syu。pip install把包装进当前生效的那个 Python;用 python -m pip 调用可以避开 pip 其实属于另一个解释器的经典问题,而 PEP 668 之后系统 Python 会拒绝装到虚拟环境之外。pip freeze把已安装的每个包和确切版本逐行打印,可直接重定向进 requirements.txt;它连带出的间接依赖也会列出,所以必须在虚拟环境里运行。python -m venv在一个文件夹里建出独立的解释器和 site-packages;只建不用没有任何效果,得先 activate 或直接调用 .venv/bin/python,而且路径是写死在里面的,事后改文件夹名就坏了。docker run每次都会从镜像新建并启动一个容器,所以敲两次就有两个容器,重启已有的那个要用 docker start;不加 -p 主机访问不到,不加 -v 写下的东西会随容器一起消失。docker ps只列出此刻正在运行的容器;启动后马上退出的那个要加 -a 才看得到,docker run 好像什么都没做时就该去那里看。docker build按 Dockerfile 构建镜像;结尾那个点是要送给守护进程的构建上下文,目录太大构建就慢,得靠 .dockerignore 瘦身,而 COPY 只能看到上下文里的文件。docker compose up把 compose.yaml 里的所有服务当成一个项目一起启动;带连字符的 docker-compose 是已退役的 v1,现在是 docker compose 插件,而已存在的容器不加 --build 或 --force-recreate 就会被直接复用。docker exec在一个已经运行的容器里再跑一个进程;容器停着就会失败,而精简镜像里没有 bash,得用 sh 进去。docker logs显示容器主进程写到 stdout 和 stderr 的内容;如果应用把日志写进容器里的文件,这里什么都看不到,而容器一删日志也跟着没了。
怎么看
- 方括号 [ ] 表示这部分可以不写。
- 省略号 … 表示可以写多个。
- 选项区分大小写——有些命令里 -r 和 -R 不是一回事。
常见问题
Q. 为什么命令名不翻译?
因为要照原样敲。翻译过的 ls 是 shell 不认识的名字。选项同理,只有说明跟着语言变。
Q. macOS 和 Linux 的选项不一样。
确实不一样。macOS 用的是 BSD 工具,有些 GNU 选项没有——sed -i 要带参数,ps 不接受 -ef。分歧的地方,说明里写了属于哪一边。
Q. 怎么看出哪些命令不能撤销?
说明里写了。rm 不经过回收站,of= 写错时 dd 会覆盖整块磁盘,git reset --hard 和 git clean -fd 会丢掉没提交的工作。