“無論如何我都會迭代,為什麼不使用呢ls?”

“無論如何我都會迭代,為什麼不使用呢ls?”

我一直看到答案引用這個連結明確地陳述“不要解析ls!”這讓我困擾有幾個原因:

  1. 儘管我可以在隨意閱讀時至少找出一些錯誤,但似乎該連結中的信息已被批量接受,幾乎沒有任何問題。

  2. 該連結中提到的問題似乎也沒有引發尋找解決方案的願望。

從第一段開始:

……當您要求[ls]文件列表時,存在一個巨大的問題:Unix 幾乎允許文件名中的任何字符,包括空格、換行符、逗號、管道符號以及幾乎任何您嘗試用作文件名的其他字符。之外的分隔符號。 ...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”“新隊”變成一個

如果您只想迭代當前目錄中的所有文件,請使用for循環和 glob:

for f in *; do
    [[ -e $f ]] || continue
    ...
done

作者稱之為亂碼檔名Whenls傳回包含 shell 全域變數的檔名列表進而建議使用 shell glob 來檢索檔案清單!

考慮以下:

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-q ls如下:

-q- 強制將不可列印檔案名稱符號和<tab>s 的每個實例寫為問號 ( '?') 字元。如果輸出到終端設備,則實作可以預設提供此選項。

-1-(數字一。)強制輸出為每行一個條目。

通配符也有其自身的問題 -?匹配任何字符,因此?列表中的多個匹配結果將多次匹配同一文件。這很容易處理。

雖然如何做這件事不是重點 - 畢竟不需要做太多事情,並且在下面進行了演示 - 我感興趣的是為什麼不。據我認為,該問題的最佳答案已被接受。我建議你嘗試更多地專注於告訴人們他們所知道的事情做而不是他們做的事情不能。我認為,至少你被證明是錯誤的可能性要小得多。

但為什麼還要嘗試呢?誠然,我的主要動機是其他人一直告訴我我不能。我非常清楚,ls只要您知道要尋找什麼,輸出就會像您希望的那樣有規律和可預測。錯誤訊息比大多數事情更讓我煩惱。

但事實是,除了 Patrick 和 Wumpus Q 的明顯例外。(儘管後者的手柄很棒)我認為這裡答案中的大部分資訊大部分都是正確的 - shell glob 在搜尋當前目錄時比解析更容易使用,而且通常更有效ls。然而,至少在我看來,它們並不足以成為傳播上述文章中引用的錯誤訊息的理由,也不是“可以接受的理由”從不解析ls

zsh請注意,帕特里克的答案不一致的結果主要是他使用then的結果bashzsh- 預設 - 不會以可移植的方式$(取代單字分割指令的結果。)所以當他問起時其餘的文件都去了哪裡?這個問題的答案是你的殼把它們吃了。這就是為什麼在使用和處理可移植 shell 程式碼時需要設定該SH_WORD_SPLIT變數。zsh我認為他在回答中沒有註意到這一點是非常誤導的。

Wumpus 的答案不適合我 - 在列表上下文中的?角色一個外殼球。我不知道還能怎麼說。

為了處理多個結果的情況,您需要限制 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、或字母數字字符的每個字符,然後保護唯一結果的清單。這是安全的,因為已經為我們保存了所有不可列印的字元。手錶::colonsort -uls

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'

下面我再次處理這個問題,但我使用了不同的方法。請記住,除了\0null 之外,/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

這些只是我能想到的最便攜的手段。使用 GNUls你可以這樣做:

ls --quoting-style=WORD

最後,這是一個更簡單的方法解析ls當需要 inode 編號時我經常使用它:

ls -1iq | grep -o '^ *[0-9]*'

它只會傳回索引節點號碼——這是另一個方便的 POSIX 指定選項。

答案1

我一點也不相信這一點,但為了論證,讓我們假設你可以,如果您準備好付出足夠的努力,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都比原始版本好得多。 GNU 對核心「shell 實用程式」(find、grep 等)的擴充也有很大幫助。但即使有了所有的擴展,shell環境也沒有改善足夠的為了與實際上很好的腳本語言競爭,所以我的建議仍然是“不要使用 shell 來做任何複雜的事情”,無論您談論的是哪種 shell。

“一個好的互動式 shell 同時也是一種好的腳本語言會是什麼樣子?”是一個即時研究問題,因為互動式 CLI 所需的便利性(例如允許鍵入cc -c -g -O2 -o foo.o foo.c而不是subprocess.run(["cc", "-c", "-g", "-O2", "-o", "foo.o", "foo.c"]))與避免複雜腳本中的細微錯誤(例如不是將隨機位置中未加引號的單字解釋為字串文字)。如果我嘗試設計這樣的東西,我可能會先將 IPython、PowerShell 和 Lua 放入攪拌機中,但我不知道結果會是什麼樣子。

答案2

該連結被多次引用,因為該資訊完全準確,並且已經存在很長時間了。


ls用全域字元替換不可列印的字元是的,但這些字元不在實際檔案名稱中。為什麼這很重要? 2個原因:

  1. 如果您將該檔案名稱傳遞給程序,則該檔案名稱實際上並不存在。它必須擴展 glob 才能取得真實的檔案名稱。
  2. 檔案 glob 可能會匹配多個檔案。

例如:

$ touch a$'\t'b
$ touch a$'\n'b
$ ls -1
a?b
a?b

請注意我們有兩個看起來完全相同的文件。如果它們都表示為 ,您將如何區分它們a?b


當 ls 傳回包含 shell glob 的檔案名稱清單時,作者稱之為亂碼檔案名,然後建議使用 shell glob 來擷取檔案清單!

這裡有一個區別。當您傳回一個 glob 時,如圖所示,該 glob 可能會符合多個檔案。但是,當您迭代與 glob 匹配的結果時,您將返回確切的文件,而不是 glob。

例如:

$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6109 62                                  a.b
0000000: 610a 62                                  a.b

請注意輸出如何xxd顯示$file包含原始字元\t\n, not ?

如果你使用ls,你會得到這個:

for file in $(ls -1q); do printf '%s' "$file" | xxd; done
0000000: 613f 62                                  a?b
0000000: 613f 62                                  a?b

“無論如何我都會迭代,為什麼不使用呢ls?”

你給出的例子實際上不起作用。看起來好像有效,但事實並非如此。

我指的是這個:

 for f in $(ls -1q | tr " " "?") ; do [ -f "$f" ] && echo "./$f" ; done

我建立了一個包含一堆檔案名稱的目錄:

$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6120 62                                  a b
0000000: 6120 2062                                a  b
0000000: 61e2 8082 62                             a...b
0000000: 61e2 8083 62                             a...b
0000000: 6109 62                                  a.b
0000000: 610a 62                                  a.b

當我運行你的程式碼時,我得到這個:

$ for f in $(ls -1q | tr " " "?") ; do [ -f "$f" ] && echo "./$f" ; done
./a b
./a b

其餘的文件都去哪了?

讓我們試試這個:

$ for f in $(ls -1q | tr " " "?") ; do stat --format='%n' "./$f"; done
stat: cannot stat ‘./a?b’: No such file or directory
stat: cannot stat ‘./a??b’: No such file or directory
./a b
./a b
stat: cannot stat ‘./a?b’: No such file or directory
stat: cannot stat ‘./a?b’: No such file or directory

現在讓我們使用一個實際的 glob:

$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a  b
./a b
./a b
./a b
./a
b

用bash

上面的範例是使用我的普通 shell zsh。當我使用 bash 重複該過程時,我得到了另一組與您的範例完全不同的結果:

同一組文件:

$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6120 62                                  a b
0000000: 6120 2062                                a  b
0000000: 61e2 8082 62                             a...b
0000000: 61e2 8083 62                             a...b
0000000: 6109 62                                  a.b
0000000: 610a 62                                  a.b

與您的程式碼完全不同的結果:

for f in $(ls -1q | tr " " "?") ; do stat --format='%n' "./$f"; done
./a b
./a b
./a b
./a b
./a
b
./a  b
./a b
./a b
./a b
./a b
./a b
./a b
./a
b
./a b
./a b
./a b
./a b
./a
b

使用 shell glob,它工作得非常好:

$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a  b
./a b
./a b
./a b
./a
b

bash 如此表現的原因可以追溯到我在答案開頭提出的觀點之一:「檔案 glob 可能匹配多個檔案」。

ls為多個檔案傳回相同的 glob ( a?b),因此每次擴充此 glob 時,我們都會得到與其相符的每個檔案。


如何重新建立我正在使用的文件清單:

touch 'a b' 'a  b' a$'\xe2\x80\x82'b a$'\xe2\x80\x83'b a$'\t'b a$'\n'b

十六進位代碼是 UTF-8 NBSP 字元。

答案3

的輸出ls -q根本不是一個球體。它的?意思是「這裡有一個不能直接顯示的字元」。 Glob 過去的?意思是「此處允許使用任何字元」。

Globs 還有其他特殊字元(*至少[],在這[]對字元中還有更多)。這些都沒有被 逃脫ls -q

$ touch x '[x]'
$ ls -1q
[x]
x

如果你將ls -1q輸出視為一組 glob 並展開它們,你不僅會得到x兩次,而且會[x]完全錯過。作為一個 glob,它與作為字串的自身不匹配。

ls -q是為了保護你的眼睛和/或終端免受瘋狂角色的傷害,而不是產生一些你可以回饋給 shell 的東西。

答案4

答案很簡單:ls您必須處理的特殊情況超過了任何可能的好處。如果您不解析ls輸出,則可以避免這些特殊情況。

這裡的咒語是永遠不要信任使用者檔案系統(相當於從不相信使用者輸入)。如果有一種方法始終有效且具有 100% 的確定性,那麼它應該是您喜歡的方法,即使其ls效果相同但確定性較低。我不會討論技術細節,因為這些內容已被涵蓋特登派崔克廣泛地。我知道,由於在重要的(可能是昂貴的)交易中使用我的工作/聲望的風險ls,如果可以避免的話,我會更喜歡任何沒有不確定性的解決方案。

我知道有些人更喜歡風險大於確定性, 但我已提交錯誤報告

相關內容