為什麼 not 解析 ls
(以及要做什麼)?
我一直看到引用此連結的答案明確指出***“不要解析
ls
!”*** 這讓我感到困擾有幾個原因:
- 似乎該連結中的資訊已被廣泛接受,幾乎沒有問題,儘管我可以在隨意閱讀中找出至少一些錯誤。
- 似乎該連結中所述的問題並沒有引發尋找解決方案的願望。
從第一段開始:
…當您詢問
[ls]
文件列表時,會出現一個大問題:Unix 允許文件名中包含幾乎任何字元,包括空格、換行符、逗號、管道符號以及幾乎任何您曾經嘗試用作NUL 以外的分隔符。…ls
用換行符分隔文件名。這很好,直到您有一個名稱中包含換行符的文件。而且由於我不知道有任何實現ls
允許您使用 NUL 字元而不是換行符來終止文件名,這使我們無法使用ls
.無賴,對吧?我們如何處理可能包含換行符的數據的換行符終止列出的數據集*?*好吧,如果在這個網站上回答問題的人不是每天都做這種事情,我可能會認為我們遇到了一些麻煩。
但事實是,大多數
ls
實現實際上都提供了一個非常簡單的 api 來解析它們的輸出,而我們一直都在這樣做,甚至沒有意識到這一點。您不僅可以使用 null 結束文件名,還可以使用 null 或您可能需要的任何其他任意字元串開始文件名。更重要的是,您可以為每個文件類型分配這些任意字元串。請考慮:LS_COLORS='lc=\0:rc=:ec=\0\0\0:fi=:di=:' ls -l --color=always | cat -A total 4$ drwxr-xr-x 1 mikeserv mikeserv 0 Jul 10 01:05 ^@^@^@^@dir^@^@^@/$ -rw-r--r-- 1 mikeserv mikeserv 4 Jul 10 02:18 ^@file1^@^@^@$ -rw-r--r-- 1 mikeserv mikeserv 0 Jul 10 01:08 ^@file2^@^@^@$ -rw-r--r-- 1 mikeserv mikeserv 0 Jul 10 02:27 ^@new$ line$ file^@^@^@$ ^@
有關更多資訊,請參閱此。
現在,這篇文章的下一部分真的讓我很感動:
$ ls -l total 8 -rw-r----- 1 lhunath lhunath 19 Mar 27 10:47 a -rw-r----- 1 lhunath lhunath 0 Mar 27 10:47 a?newline -rw-r----- 1 lhunath lhunath 0 Mar 27 10:47 a space
問題是,從 的輸出中
ls
,您或電腦都無法分辨出它的哪些部分構成了文件名。是每一個字嗎?不,是每一行嗎?不,這個問題沒有正確的答案,除了:你不知道。還要注意
ls
有時你的文件名數據是如何亂碼的(在我們的例子中,它把\n
單詞*“a”*和 “newline”之間的字元變成了一個?問號………
如果您只想遍歷目前目錄中的所有文件,請使用
for
循環和 glob:for f in *; do [[ -e $f ]] || continue ... done
作者在返回包含 shell glob 的文件名列表時稱其為亂碼文件名***,然後***建議使用 shell glob 來檢索文件列表!
ls
考慮以下:
printf 'touch ./"%b"\n' "file\nname" "f i l e n a m e" | . /dev/stdin ls -1q f i l e n a m e file?name IFS=" " ; printf "'%s'\n" $(ls -1q) 'f i l e n a m e' 'file name'
POSIX 定義了
-1
and-q
ls
操作數:
-q
- 強制將每個不可列印的文件名字元和<tab>
s 實例寫為問號 ('?'
) 字元。如果輸出到終端設備,實現可以預設提供此選項。
-1
- *(數字一。)*強制輸出為每行一個條目。萬用字元並非沒有其自身的問題——
?
匹配任何字元,因此?
列表中的多個匹配結果將多次匹配同一個文件。這很容易處理。雖然如何做這件事並不是重點——畢竟它不需要做太多事情,並且在下面進行了展示——我對為什麼不感興趣。在我看來,該問題的最佳答案已被接受。我建議您嘗試更多地專注於告訴人們他們可以做什麼而不是他們不能做什麼*。*正如我認為的那樣,你至少被證明是錯誤的可能性要小得多。
但為什麼還要嘗試?誠然,我的主要動機是其他人一直告訴我我做不到。我非常清楚,
ls
只要您知道要查找什麼,輸出就如您所願一樣有規律且可預測。錯誤資訊比大多數事情更困擾我。但是,事實是,除了 Patrick 和 Wumpus Q. Wumbley 的答案*(儘管後者的處理很棒)之外,我認為這裡答案中的大部分資訊大部分都是正確的——shell glob 都更易於使用在搜尋目前目錄時通常比解析更有效
ls
。然而,至少在我看來,它們不足以證明傳播上述文章中引用的錯誤資訊是合理的,也不是“從不解析ls
”的可接受理由。*請注意,帕特里克的答案不一致的結果主要是他使用
zsh
then的結果bash
。zsh
- 預設情況下 - 不會以可移植的方式將單詞拆分$(
命令替換)
為結果。所以當他問其餘的文件去哪兒了?這個問題的答案是你的殼吃了它們。SH_WORD_SPLIT
這就是為什麼在使用zsh
和處理可移植 shell 程式碼時需要設置變數的原因。我認為他沒有在回答中指出這一點是非常具有誤導性的。Wumpus 的答案對我來說並不計算 - 在列表上下文中,該
?
字元是一個 shell glob。我不知道還能怎麼說。為了處理多個結果的情況,您需要限制 glob 的貪婪。以下將創建一個糟糕的文件名的測試庫並為您顯示它:
{ printf %b $(printf \\%04o `seq 0 127`) | sed "/[^[-b]*/s///g s/\(.\)\(.\)/touch '?\v\2' '\1\t\2' '\1\n\2'\n/g" | . /dev/stdin echo '`ls` ?QUOTED `-m` COMMA,SEP' ls -qm echo ; echo 'NOW LITERAL - COMMA,SEP' ls -m | cat ( set -- * ; printf "\nFILE COUNT: %s\n" $# ) }
輸出
`ls` ?QUOTED `-m` COMMA,SEP ??\, ??^, ??`, ??b, [?\, [?\, ]?^, ]?^, _?`, _?`, a?b, a?b NOW LITERAL - COMMA,SEP ? \, ? ^, ? `, ? b, [ \, [ \, ] ^, ] ^, _ `, _ `, a b, a b FILE COUNT: 12
現在,我將保護 shell glob 中不是
/slash
、-dash
、:colon
或字母數字字元的每個字元,然後sort -u
是唯一結果的列表。這是安全的,因為ls
已經為我們保護了任何不可列印的字元。手錶:for f in $( ls -1q | sed 's|[^-:/[:alnum:]]|[!-\\:[:alnum:]]|g' | sort -u | { echo 'PRE-GLOB:' >&2 tee /dev/fd/2 printf '\nPOST-GLOB:\n' >&2 } ) ; do printf "FILE #$((i=i+1)): '%s'\n" "$f" done
輸出:
PRE-GLOB: [!-\:[:alnum:]][!-\:[:alnum:]][!-\:[:alnum:]] [!-\:[:alnum:]][!-\:[:alnum:]]b a[!-\:[:alnum:]]b POST-GLOB: FILE #1: '? \' FILE #2: '? ^' FILE #3: '? `' FILE #4: '[ \' FILE #5: '[ \' FILE #6: '] ^' FILE #7: '] ^' FILE #8: '_ `' FILE #9: '_ `' FILE #10: '? b' FILE #11: 'a b' FILE #12: 'a b'
下面我再次處理這個問題,但我使用了不同的方法。請記住,除了
\0
null 之外,/
ASCII 字元是路徑名中唯一被禁止的字節。我把 glob 放在一邊,而是將 POSIX 指定的-d
選項ls
和 POSIX 指定的-exec $cmd {} +
構造組合在一起find
。因為find
只會自然地/
按順序發出一個,所以以下內容很容易獲得一個遞歸且可靠分隔的文件列表,包括每個條目的所有目錄資訊。想像一下你可以用這樣的東西做什麼:#v#note: to do this fully portably substitute an actual newline \#v# #v#for 'n' for the first sed invocation#v# cd .. find ././ -exec ls -1ldin {} + | sed -e '\| *\./\./|{s||\n.///|;i///' -e \} | sed 'N;s|\(\n\)///|///\1|;$s|$|///|;P;D' ###OUTPUT 152398 drwxr-xr-x 1 1000 1000 72 Jun 24 14:49 .///testls/// 152399 -rw-r--r-- 1 1000 1000 0 Jun 24 14:49 .///testls/? \/// 152402 -rw-r--r-- 1 1000 1000 0 Jun 24 14:49 .///testls/? ^/// 152405 -rw-r--r-- 1 1000 1000 0 Jun 24 14:49 .///testls/? `/// ...
ls -i
可能非常有用 - 特別是當結果唯一性存在問題時。ls -1iq | sed '/ .*/s///;s/^/-inum /;$!s/$/ -o /' | tr -d '\n' | xargs find
這些只是我能想到的最便攜的方式。使用 GNU
ls
,您可以:ls --quoting-style=WORD
最後,這是一種更簡單的*解析
ls
*方法,我碰巧在需要 inode 編號時經常使用它:ls -1iq | grep -o '^ *[0-9]*'
這只是返回 inode 編號 - 這是另一個方便的 POSIX 指定選項。
我完全不相信這一點,但是為了論證,假設你可以,如果你準備付出足夠的努力,
ls
可靠地解析輸出,即使面對一個“對手”——一個知道您編寫的程式碼,並且故意選擇旨在破壞它的文件名。即使你能做到這一點,這仍然是一個壞主意。
Bourne shell 1是一種糟糕的語言。它不應該用於任何復雜的事情,除非極端的可移植性比任何其他因素(例如
autoconf
)更重要。我聲稱,如果您遇到解析輸出
ls
似乎是 shell 腳本阻力最小的路徑的問題,這強烈表明您所做的任何事情都太複雜而不能成為 shell 腳本,您應該重寫Perl、Python、Julia 或任何其他現成的*優秀腳本語言。*作為展示,這是您在 Python 中的最後一個程序:import os, sys for subdir, dirs, files in os.walk("."): for f in dirs + files: ino = os.lstat(os.path.join(subdir, f)).st_ino sys.stdout.write("%d %s %s\n" % (ino, subdir, f))
這對於文件名中的異常字元沒有任何問題——輸出是模棱兩可的,就像輸出
ls
模棱兩可一樣,但這在“真實”程序中無關緊要(與這樣的展示相反),這將直接使用結果os.path.join(subdir, f)
。同樣重要的是,與你寫的東西形成鮮明對比的是,六個月後它仍然有意義,當你需要它做一些稍微不同的事情時,它很容易修改。舉例來說,假設您發現需要排除點文件和編輯器備份,並按基本名稱的字母順序處理所有內容:
import os, sys filelist = [] for subdir, dirs, files in os.walk("."): for f in dirs + files: if f[0] == '.' or f[-1] == '~': continue lstat = os.lstat(os.path.join(subdir, f)) filelist.append((f, subdir, lstat.st_ino)) filelist.sort(key = lambda x: x[0]) for f, subdir, ino in filelist: sys.stdout.write("%d %s %s\n" % (ino, subdir, f))
腳註 1:是的,Bourne shell 的擴展版本現在很容易獲得:
bash
並且zsh
都比原來的要好得多。核心“shell 實用程序”(find、grep 等)的 GNU 擴展也有很大幫助。但是即使有了所有的擴展,shell 環境也沒有得到足夠的改進,無法與實際上很好的腳本語言競爭,所以我的建議仍然是“不要將 shell 用於任何復雜的事情”,不管你在談論哪個 shell。“一個很好的互動式 shell 又是一種很好的腳本語言會是什麼樣子?” 是一個實時研究問題,因為在互動式 CLI 所需的便利性(例如允許鍵入
cc -c -g -O2 -o foo.o foo.c
而不是位置作為字元串文字)。如果我要嘗試設計這樣的東西,我可能會先將 IPython、PowerShell 和 Lua 放入攪拌機中,但我不知道結果會是什麼樣子。subprocess.run(["cc", "-c", "-g", "-O2", "-o", "foo.o", "foo.c"])