上傳檔案明明存在,shell腳本卻說找不到。檔名叫site notes.txt,變數也印出正確路徑;真正送進命令時,空白把它拆成兩個參數。對一個已存好的檔案路徑,使用雙引號包住變數展開,才能讓整段值保持一個參數。
這個問題會出現在備份、匯入、圖片處理與部署腳本。不能要求所有使用者永遠不用空白檔名,也不宜靠刪掉空白讓錯誤暫時消失。先查命令收到幾個參數,再決定怎麼引用,較容易找出真正的展開問題。
變數值正確,不表示參數邊界正確
建立一個新的示範目錄,放入含空白的檔名,再把完整路徑存到變數。賦值這一行用單引號保存字面值;之後讀變數時,用雙引號讓變數展開但保留整段值。
mkdir -p /tmp/quote-demo
printf 'one file\n' > '/tmp/quote-demo/site notes.txt'
path='/tmp/quote-demo/site notes.txt'
cat "$path"
這個目錄與檔案只供示範。執行前確認你自己的測試路徑沒有需要保留的內容,不要把正式檔案搬到這裡再覆寫。範例只是讀取資料,不處理檔名清理或重新命名。
寫成cat $path時,shell不是先把變數變成一個不可分割的路徑。一般未引用的變數展開,還可能經過分詞與檔名展開;預設分詞分隔包含空白、Tab與換行。命令因此收到/tmp/quote-demo/site與notes.txt,而不是原本那個檔名。
直接印出參數個數,比看echo更清楚
show() {
printf 'argc=%s\n' "$#"
printf '<%s>\n' "$@"
}
show $path
show "$path"
在隔離Ubuntu 24.04、Bash 5.2.21的實跑,未引用得到argc=2,兩行分別是路徑前半與notes.txt;引用後得到argc=1,整個含空白的路徑在同一對尖括號中。這個小函式只查看參數,不修改檔案。
echo印出的外觀可能讓兩者很像:兩個參數之間也會用空白分隔,於是你看到的字串和原路徑相同。排查參數問題時,應印出個數與每項邊界,而不是只核對一整行文字。字串內容相同,傳入方式仍可能不同。
同一組案例中,cat $path回傳1,標準錯誤指出拆開後的路徑不存在;cat “$path”印出one file並回傳0。這證明差異發生在命令參數,而不是檔案讀取權限或原檔案已遺失。
雙引號和單引號有不同用途
雙引號中的$path仍會展開成變數值,但一般不再進行這個值的分詞與檔名展開。單引號中的$path則是字面的美元符號與名稱,不會讀變數。所以cat ‘$path’不會打開變數指向的檔案,而是嘗試找一個叫$path的檔名。
賦值時需要保存固定文字,可以用單引號;要把變數的值傳進命令,通常用雙引號。兩者不是誰比較安全就一律取代另一種,而是依你希望哪些字元被解釋來選。含單引號的字面內容則還需按shell引用規則處理,不要把任意外部文字直接貼進單引號模板。
這篇處理的是Bash命令列展開。PHP、Python或其他程式用參數陣列直接啟動程式時,不一定再經過shell分詞;相反地,把資料拼成一整段shell命令又會引入解釋規則。先確認呼叫途徑,不能只看到同一個路徑就假設引用方法相同。
星號也可能把一個值變成多個檔名
除了空白,變數裡的星號也需要想清楚。若pattern保存*.txt,未引用展開可能依當前目錄找到多個文字檔;引用後則把字面*.txt傳給程式。這不是哪一種結果永遠正確,而是你應知道自己要傳一個模式還是多個路徑。
pattern='*.txt'
show $pattern
show "$pattern"
本例在新的示範目錄只放a.txt與b.txt,未引用得到兩個檔名,引用得到一項*.txt。這個比較是在Bash預設行為下觀察,不把它推成每個shell選項、每個當前目錄都相同。若沒有匹配或開啟其他展開選項,結果應按你的環境另核。
當你已取得一個真實路徑,通常不希望它因星號、問號或空白再次變成搜尋模式。保持雙引號引用可以把資料當成一個參數;若目的真的是批次匹配,則應清楚設計模式來源與要處理的路徑,避免把使用者檔名誤當成規則。
多個路徑不要塞進一個空白字串
如果原本要處理兩個檔案,把兩條路徑用空白黏成files變數,再故意不引用去拆開,遇到任一檔名自帶空白就失去邊界。Bash中可使用陣列保存每個路徑,傳入時採用引用的陣列展開;這和一個路徑變數是不同需求。
files=('/tmp/quote-demo/site notes.txt' '/tmp/other.txt')
show "${files[@]}"
這裡的第二個檔案不用存在,show只是展示兩項參數。真正讀取前仍要檢查來源與處理錯誤。陣列寫法是Bash功能,不應直接照抄到要求POSIX sh的腳本;選定執行shell後,再使用它支援的資料結構。
空值也有參數是否存在的差別
如果path是空字串,未引用展開可能完全沒有傳入參數;引用後則傳入一個內容為空的參數。本例另跑show確認,前者個數為零,後者為一。兩者對程式可能有不同含意,所以加引號不是替空值補出一個有效路徑,而是保持原本的資料邊界。需要拒絕空路徑時,應在呼叫前明確檢查,不能靠展開時剛好消失來處理。
引用保住邊界,其他條件仍要核對
加引號不會修復不存在的檔案、沒有讀取權限或錯誤的工作目錄。相對路徑還是依目前目錄解析;如果變數內容已經寫錯,它只會完整傳入那個錯誤值。先用這個最小比較確認分詞,再分別查檔案存在、權限與目錄。
檔名若以減號開頭,引用也不會自動取消程式的選項解釋。支援雙減號分隔的命令可按手冊使用–,或採清楚的路徑形式;不同程式是否接受這個分隔要核對,不能把shell引號當成所有選項問題的通用解法。
也不要改用eval讓路徑「重新解釋一次」。資料如果又被當成命令文字處理,會引入另一輪展開,反而更難保住原始邊界。對讀取一個檔案的需求,直接把引用的變數傳給cat就足夠,不需要增加命令字串層。
把空白檔名和含模式字元的值保存為測試資料,核對每個參數都符合預期,再接回真正的工具。若腳本可由多個入口呼叫,還要把相同檢查放在實際執行環境確認。這個案例只驗Bash本地參數與讀檔,不表示備份或部署已完成。

評論0