、符號(hào)鏈接及腳本實(shí)戰(zhàn))
剛接觸Linux那會(huì)我特別不理解為啥敲個(gè)pwd還有那么多講究不就是打印當(dāng)前目錄嘛。直到有一次在腳本里拼路徑因?yàn)闆]搞清楚pwd -P和pwd -L的區(qū)別把日志輸出位置搞錯(cuò)了排查了半天才回過神。從那以后我就明白越是基礎(chǔ)的Linux命令越值得把它背后的邏輯徹底弄透。這篇內(nèi)容圍繞Linux里出現(xiàn)頻率最高的pwd命令展開把它的用法、參數(shù)、原理、腳本技巧和運(yùn)維實(shí)戰(zhàn)場(chǎng)景一次講清楚適合剛?cè)腴T的新手也適合想補(bǔ)基礎(chǔ)短板的運(yùn)維、開發(fā)和面試黨。pwd全稱是print working directory作用就一個(gè)把當(dāng)前所在的目錄路徑打印出來??雌饋砗?jiǎn)單但它在shell交互、腳本編寫、路徑拼接、日志定位這些場(chǎng)景里屬于不可或缺的底層命令。很多人用了好幾年Linux可能只在迷路的時(shí)候才想起敲一下pwd實(shí)際上它有幾個(gè)參數(shù)和行為細(xì)節(jié)一旦沒弄明白真到了線上環(huán)境就會(huì)踩坑。1. 認(rèn)識(shí)pwd命令基本用法與Shell工作機(jī)制1.1 pwd的基本命令格式與參數(shù)先看最基礎(chǔ)的東西。pwd的語法格式非常簡(jiǎn)單通常不需要任何參數(shù)直接執(zhí)行就能輸出當(dāng)前目錄的絕對(duì)路徑$ pwd /home/ubuntu/project它支持的參數(shù)主要有兩個(gè)pwd -L # 邏輯路徑跟隨環(huán)境變量PWD默認(rèn)行為 pwd -P # 物理路徑繞過符號(hào)鏈接顯示真實(shí)目錄-L是logical的縮寫-P是physical的縮寫。不同發(fā)行版、不同shell里默認(rèn)行為略有差異Bash里默認(rèn)等效于-L而/bin/pwd這個(gè)獨(dú)立程序在不加參數(shù)時(shí)默認(rèn)是-P。這個(gè)差異非常關(guān)鍵后面我會(huì)用實(shí)際案例演示它到底怎么坑人。提示在絕大多數(shù)Linux發(fā)行版里shell內(nèi)置的pwd是默認(rèn)優(yōu)先的。你可以用type -a pwd查看它的完整來源路徑通常能看到pwd is a shell builtin和/usr/bin/pwd兩個(gè)結(jié)果。1.2 為什么需要pwdShell工作目錄機(jī)制理解pwd之前得先理解Shell的當(dāng)前工作目錄Current Working Directory簡(jiǎn)稱CWD到底是什么。每個(gè)Shell進(jìn)程甚至每個(gè)子進(jìn)程都維護(hù)著一個(gè)屬于自己的目錄狀態(tài)。當(dāng)你執(zhí)行l(wèi)s、touch、cat這些命令時(shí)如果沒有指定絕對(duì)路徑操作系統(tǒng)就會(huì)默認(rèn)在這個(gè)狀態(tài)對(duì)應(yīng)的目錄里尋找文件。這個(gè)狀態(tài)本質(zhì)上是由內(nèi)核維護(hù)的進(jìn)程屬性Shell只是負(fù)責(zé)展示和修改它。cd命令修改的是當(dāng)前Shell進(jìn)程的目錄屬性pwd則是向內(nèi)核查詢當(dāng)前目錄并打印出來。用一句生活化的話說Shell進(jìn)程就像你在辦公樓里的工位cd是換工位pwd是告訴別人你現(xiàn)在坐在哪一層哪一排。由于每個(gè)子Shell都有自己獨(dú)立的目錄狀態(tài)所以在腳本里用cd切換目錄后不會(huì)影響父Shell的所在位置。這就是為什么很多部署腳本里常用(cd /path command)這種子Shell寫法也是理解后面腳本技巧的基礎(chǔ)。2. pwd兩個(gè)核心參數(shù)-L與-P符號(hào)鏈接的邏輯差異2.1 符號(hào)鏈接場(chǎng)景下的pwd -P真實(shí)路徑獲取這個(gè)案例是我當(dāng)年踩坑的起點(diǎn)。假如你有一個(gè)目錄/data/web它其實(shí)是指向/home/www/htdocs的符號(hào)鏈接$ ls -ld /data/web lrwxrwxrwx 1 root root 14 Jan 1 10:00 /data/web - /home/www/htdocs當(dāng)你cd /data/web之后再執(zhí)行pwd很多時(shí)候會(huì)看到路徑仍然是/data/web而不是解析后的真實(shí)路徑/home/www/htdocs。這就是pwd -L的邏輯路徑行為它信任PWD環(huán)境變量記錄的內(nèi)容而PWD這個(gè)變量在cd的時(shí)候就被Shell更新成了你輸入的路徑。如果你使用pwd -PShell會(huì)調(diào)用系統(tǒng)調(diào)用getcwd()向內(nèi)核重新查詢真實(shí)路徑輸出/home/www/htdocs。那到底什么時(shí)候必須用-P最常見的場(chǎng)景是配置文件和啟動(dòng)腳本。有些程序會(huì)把當(dāng)前路徑寫入配置如果當(dāng)前路徑包含符號(hào)鏈接后續(xù)進(jìn)程從不同入口訪問目錄時(shí)可能出現(xiàn)路徑不一致的問題。例如Nginx的nginx.pid路徑、日志切割腳本里的BASE_DIR如果路徑解析不徹底用符號(hào)鏈接進(jìn)入和用真實(shí)路徑進(jìn)入會(huì)看到兩個(gè)不一樣的結(jié)果排查起來非常痛苦。2.2 默認(rèn)-L行為與PWD環(huán)境變量的關(guān)系-L邏輯路徑的邏輯基礎(chǔ)是Shell內(nèi)部維護(hù)的PWD環(huán)境變量。這個(gè)變量在每次cd時(shí)都會(huì)被更新你可以手動(dòng)驗(yàn)證一下$ export PWD/tmp $ pwd /tmp $ pwd -P /home/ubuntu看到?jīng)]有在不改變當(dāng)前實(shí)際目錄的情況下我把PWD變量手動(dòng)改成/tmppwd -L輸出就變成了/tmp而pwd -P輸出的仍然是真實(shí)路徑。這說明-L本質(zhì)上就是打印PWD變量是個(gè)純邏輯行為。這個(gè)特性在某些特殊場(chǎng)景下會(huì)被刻意利用。有的腳本不想暴露服務(wù)器的真實(shí)目錄結(jié)構(gòu)會(huì)在子進(jìn)程啟動(dòng)前重設(shè)PWD變量讓程序以為自己工作在另一個(gè)目錄。但絕大多數(shù)情況下這種花活不建議用因?yàn)樗菀鬃屓罩韭窂藉e(cuò)亂出了問題非常難定位。我見過有同事在CI流水線里為了圖省事直接導(dǎo)出PWD結(jié)果后面所有相對(duì)路徑解析全亂了。2.3 Shell內(nèi)置版與/bin/pwd外部命令的區(qū)別pwd在Linux里有兩套實(shí)現(xiàn)一套是Bash等Shell自帶的builtin命令另一套是GNU coreutils提供的/bin/pwd獨(dú)立程序。日常使用中你敲的pwd大概率命中內(nèi)置版本需要留意的是它們默認(rèn)參數(shù)不一樣實(shí)現(xiàn)方式默認(rèn)行為查看方法Shell內(nèi)置pwd默認(rèn)-Ltype -a pwd/bin/pwd程序默認(rèn)-P/bin/pwd這么設(shè)計(jì)有歷史原因。Bash作為交互Shell希望盡量保持用戶看到的路徑與輸入一致所以跟隨PWD變量而GNU工具集作為系統(tǒng)命令更強(qiáng)調(diào)返回真實(shí)可靠的內(nèi)核路徑。如果你在腳本里為了追求確定性建議不要只寫pwd而是顯式寫pwd -P或/bin/pwd避免不同系統(tǒng)、不同shell帶來的行為漂移。這個(gè)細(xì)節(jié)在面試?yán)镆步?jīng)常被拿來考察候選人是否真的理解命令底層差異。3. pwd在Shell腳本與自動(dòng)化運(yùn)維中的實(shí)戰(zhàn)技巧3.1 腳本中獲取當(dāng)前目錄的經(jīng)典寫法寫Shell腳本時(shí)經(jīng)常需要知道腳本自身所在目錄然后基于這個(gè)目錄去加載配置、引用依賴或者拼接日志路徑。很多新手直接寫cd $(dirname $0)這行代碼在大多數(shù)情況下沒問題但一旦腳本被符號(hào)鏈接調(diào)用或者從別的目錄執(zhí)行$0的值未必是你以為的那個(gè)路徑。更穩(wěn)妥的寫法是結(jié)合pwd -P做一次路徑解析#!/bin/bash # 獲取腳本真實(shí)路徑的經(jīng)典寫法 SOURCE${BASH_SOURCE[0]} while [ -h $SOURCE ]; do DIR$(cd -P $(dirname $SOURCE) /dev/null 21 pwd -P) SOURCE$(readlink $SOURCE) [[ $SOURCE ! /* ]] SOURCE$DIR/$SOURCE done SCRIPT_DIR$(cd -P $(dirname $SOURCE) /dev/null 21 pwd -P)這段代碼雖然看起來啰嗦但其中每一步都有明確目的循環(huán)處理符號(hào)鏈接直到拿到真實(shí)路徑使用pwd -P確保返回的目錄不包含任何軟鏈成分。對(duì)于分發(fā)到多臺(tái)服務(wù)器并且經(jīng)常被軟鏈到/usr/local/bin的運(yùn)維腳本來說這套寫法能直接避免路徑錯(cuò)亂問題。注意腳本里cd到某個(gè)目錄后后續(xù)如果要回到原始目錄不要想當(dāng)然以為pwd輸出不會(huì)變。建議在腳本開頭先用ORIG_DIR$(pwd -P)保存現(xiàn)場(chǎng)所有操作結(jié)束后再cd $ORIG_DIR回來。3.2 目錄被刪除后pwd拋出的異常行為還有一個(gè)容易被忽略的細(xì)節(jié)是當(dāng)Shell當(dāng)前所在目錄被其他進(jìn)程刪除后執(zhí)行pwd會(huì)看到什么實(shí)測(cè)結(jié)果很有意思。$ mkdir /tmp/testdir $ cd /tmp/testdir $ rmdir /tmp/testdir $ pwd -bash: pwd: (2, No such file or directory) /tmp/testdir這里的輸出其實(shí)是Bash內(nèi)置pwd的一種特殊表現(xiàn)它先嘗試查詢真實(shí)物理路徑發(fā)現(xiàn)目錄已經(jīng)不存在于是打印一條錯(cuò)誤信息到標(biāo)準(zhǔn)錯(cuò)誤然后仍把PWD變量中的邏輯路徑打印出來。而/bin/pwd行為不太一樣它直接返回非零退出碼并且不輸出路徑。這個(gè)現(xiàn)象在日常操作中一旦遇到說明你所在目錄已經(jīng)被外部清理掉了。在很多日志清理腳本、臨時(shí)目錄自動(dòng)刪除服務(wù)存在的情況下進(jìn)程長(zhǎng)時(shí)間運(yùn)行后目錄被刪是常態(tài)。這時(shí)候如果腳本里還在用相對(duì)路徑讀寫文件就會(huì)意外地寫到別的地方去因?yàn)楫?dāng)前目錄其實(shí)已經(jīng)變成了文件系統(tǒng)根目錄或其他無法確定的位置。所以監(jiān)控腳本里檢測(cè)工作目錄有效性時(shí)不能只看命令輸出有沒有內(nèi)容還要檢查退出碼if ! pwd -P /dev/null 21; then echo 當(dāng)前目錄已失效嘗試切換到安全目錄 2 cd /tmp || exit 1 fi3.3 pwd與cd、dirname、OLDPWD的組合用法pwd單獨(dú)用價(jià)值有限真正有威力的是和cd、dirname、basename、OLDPWD等合起來用。OLDPWD是Shell自動(dòng)維護(hù)的上一次目錄變量配合cd -可以快速在最近兩個(gè)目錄間切換。很多時(shí)候我排查問題時(shí)先記下當(dāng)前目錄再跳去配置文件目錄改完直接cd -回來靠的就是$OLDPWD。另一個(gè)高頻組合是取上級(jí)目錄。比如當(dāng)前在/home/ubuntu/app/bin想回到/home/ubuntu/app$ cd $(dirname $(pwd))這個(gè)寫法比cd ..更穩(wěn)健尤其當(dāng)路徑是以變量形式從配置中心讀進(jìn)來時(shí)你根本不知道它末尾帶不帶斜杠、到底有幾層目錄。還有更簡(jiǎn)潔的$ cd ..這里兩種方式有本質(zhì)區(qū)別cd ..依賴Shell對(duì)路徑字符串的解析如果當(dāng)前目錄被刪除cd ..可能失敗而通過dirname $(pwd -P)先生成目標(biāo)路徑再cd能多一層保障。我在清理臨時(shí)目錄時(shí)習(xí)慣用后者比如刪除某個(gè)目錄后需要自動(dòng)化切換到它的上一級(jí)這樣即使目標(biāo)已被刪路徑仍然可計(jì)算。pwd還可以用來校驗(yàn)?zāi)_本的執(zhí)行上下文。比如你有一個(gè)只允許在項(xiàng)目根目錄運(yùn)行的部署腳本可以直接寫if [ $(basename $(pwd -P)) ! myproject ]; then echo 請(qǐng)?jiān)陧?xiàng)目根目錄執(zhí)行本腳本 2 exit 1 fi這種方式比單純檢查某個(gè)文件是否存在更能表達(dá)意圖也方便別人維護(hù)時(shí)理解腳本的運(yùn)行前提。4. pwd相關(guān)面試考點(diǎn)與常見問題排查4.1 面試和考證中關(guān)于pwd的高頻問題pwd在Linux面試題里出現(xiàn)頻率不低原因在于它能夠連帶考察環(huán)境變量、符號(hào)鏈接、Shell內(nèi)置命令優(yōu)先級(jí)等多個(gè)知識(shí)點(diǎn)。常見的問題包括問題1pwd和/bin/pwd有什么區(qū)別核心考點(diǎn)是Shell builtin與外部命令的關(guān)系以及兩者默認(rèn)參數(shù)不同。回答時(shí)如果能提到type -a pwd查看完整路徑、-L與-P的差異基本就能拿分。問題2在符號(hào)鏈接目錄里執(zhí)行pwd為什么顯示的是鏈接路徑而不是真實(shí)路徑核心考點(diǎn)是PWD環(huán)境變量的作用機(jī)制。如果候選人不清楚Bash默認(rèn)-L行為很容易在這個(gè)問題上卡住。問題3Shell腳本如何可靠獲取自身所在目錄這個(gè)屬于實(shí)操考察。答案里如果沒有pwd -P和readlink對(duì)符號(hào)鏈接的處理通常是經(jīng)驗(yàn)不足的表現(xiàn)。問題4為什么有時(shí)候執(zhí)行pwd會(huì)打印No such file or directory核心是當(dāng)前目錄已被刪除內(nèi)置pwd與外部pwd對(duì)錯(cuò)誤處理不同。這屬于比較深入的問題能答上來的人很少是背題背出來的。遇到這類題目我的建議是別只背結(jié)論自己搭幾個(gè)目錄結(jié)構(gòu)實(shí)驗(yàn)一遍印象會(huì)深得多。4.2 pwd使用中的常見問題速查表現(xiàn)象原因解決辦法pwd和pwd -P輸出不一致當(dāng)前目錄包含符號(hào)鏈接默認(rèn)邏輯路徑與物理路徑不同需要真實(shí)路徑時(shí)顯式加-P腳本里用pwd拿到和預(yù)期不一樣的路徑PWD環(huán)境變量被修改或腳本被符號(hào)鏈接執(zhí)行統(tǒng)一改用pwd -P執(zhí)行pwd返回No such file or directory當(dāng)前目錄已被外部刪除檢查業(yè)務(wù)進(jìn)程是否清理了工作目錄cd /tmp后重新操作type -a pwd顯示兩個(gè)結(jié)果一個(gè)是Shell內(nèi)置一個(gè)是外部程序正?,F(xiàn)象不必處理想用外部版可寫/bin/pwdcd進(jìn)目錄后pwd沒變化可能是調(diào)用了別名或函數(shù)并沒真正切換使用type cd查看是不是被別名劫持排查問題時(shí)pwd輸出正確但程序仍找不到文件程序的工作目錄可能是它自己?jiǎn)?dòng)時(shí)設(shè)定的不是當(dāng)前Shell目錄檢查進(jìn)程的/proc/PID/cwd符號(hào)鏈接這里特別提一下/proc/PID/cwd它是一個(gè)指向進(jìn)程當(dāng)前工作目錄的符號(hào)鏈接。實(shí)際排查問題時(shí)如果一個(gè)后臺(tái)進(jìn)程行為異??梢杂? ls -l /proc/12345/cwd來查看這個(gè)進(jìn)程真正的工作目錄。這條命令是pwd思路的延伸——它是從內(nèi)核視角看問題而不是依賴Shell環(huán)境變量。我在定位定時(shí)任務(wù)腳本路徑問題時(shí)這招救過好幾次場(chǎng)。4.3 讓pwd輸出更易讀的現(xiàn)場(chǎng)小技巧純pwd輸出的是絕對(duì)路徑但有時(shí)候路徑太長(zhǎng)看起來不方便。有幾個(gè)思路可以優(yōu)化比如在交互Shell里的提示符中加入當(dāng)前目錄。大部分人會(huì)用\w完整路徑或\W僅當(dāng)前目錄名來定制PS1。我個(gè)人喜歡把PS1里的路徑部分設(shè)置成export PS1[\u\h \W]\$ \W只顯示最后一級(jí)目錄名這樣即使深陷/home/ubuntu/app/config/nginx/conf.d這種層層嵌套的目錄提示符也不會(huì)被撐爆。需要完整路徑時(shí)再敲pwd或者用\w這個(gè)平衡在長(zhǎng)時(shí)間運(yùn)維操作中很實(shí)用。再分享一個(gè)給pwd輸出加顏色的方式。如果你在終端里希望當(dāng)前路徑高亮可以在~/.bashrc里定義個(gè)函數(shù)function pwd_color() { pwd -P | sed s|^$HOME|~| }這只是個(gè)簡(jiǎn)單示例實(shí)際生產(chǎn)環(huán)境里我很少給命令輸出做過多美化因?yàn)槟_本解析時(shí)顏色轉(zhuǎn)義碼會(huì)干擾輸出。如果只是給人看怎么舒服怎么來如果要供程序讀取請(qǐng)保持pwd輸出純凈絕對(duì)不要包顏色碼。5. 我對(duì)pwd的最佳實(shí)踐總結(jié)與一條額外提醒從入門到現(xiàn)在我對(duì)pwd的認(rèn)知經(jīng)歷了一個(gè)從太簡(jiǎn)單不用學(xué)到細(xì)節(jié)里藏坑的過程。如果你只想記一條最重要的經(jīng)驗(yàn)?zāi)蔷褪窃谀_本里獲取當(dāng)前目錄一律顯式使用pwd -P不要依賴Shell默認(rèn)行為。默認(rèn)行為在不同環(huán)境里可能不同而顯式參數(shù)能保證你的腳本在任何發(fā)行版、任何shell下行為一致。這也是很多開源項(xiàng)目里的安裝腳本為什么總是寫$(cd $(dirname $0) pwd -P)的原因——它不是炫技而是為了讓腳本在符號(hào)鏈接、跨目錄執(zhí)行等情況下依然拿到準(zhǔn)確位置。最后再提醒一個(gè)容易被忽略的點(diǎn)pwd是查詢當(dāng)前Shell進(jìn)程工作目錄如果你在管道、子Shell或者$(...)中執(zhí)行命令它拿到的是子進(jìn)程的目錄狀態(tài)不是父Shell的。比如你寫cat file | while read line; do pwd; done當(dāng)循環(huán)里執(zhí)行了cd它只影響子Shell內(nèi)部退出循環(huán)后并不會(huì)改變你的當(dāng)前位置。想要在循環(huán)里累積目錄切換該用變量傳遞就用變量傳遞不要指望cd能透過子Shell生效。理解了這個(gè)層次你對(duì)pwd的掌握就算真正通透以后再遇到路徑相關(guān)的詭異問題至少知道從哪里開始排查。