首頁·終端機命令

終端機命令速查

檔案、文字、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列出所有開啟的檔案和 socket,這就是回答「誰占著 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 上列出已開啟 socket 的現代做法,是 netstat 的接班人,在連線很多的機器上快得多;注意 `-p` 只有以 root 執行時才顯示占用行程。netstat查看 socket 和路由表的老辦法;在 Linux 上這活已交給 ss,而且常常根本沒裝,macOS 雖然保留了它,卻無法說出 socket 屬於哪個行程,那要靠 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 塊起被覆蓋,不問一聲也無法還原,所以按下 Enter 前一定要用 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 會丟掉沒提交的工作。