为什么*不*解析`ls`(以及该怎么做)?

mik*_*erv 249 ls shell

我一直看到引用此链接的答案明确指出“不要解析ls!” 这让我感到困扰有几个原因:

  1. 似乎该链接中的信息已被批量接受,几乎没有问题,尽管我可以在随意阅读中至少挑出一些错误。

  2. 该链接中所述的问题似乎也引发了不想找到解决方案的愿望。

从第一段:

...当你要求[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^@^@^@$
^@
Run Code Online (Sandbox Code Playgroud)

请参阅了解更多信息。

现在,这篇文章的下一部分真正让我着迷:

$ 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
Run Code Online (Sandbox Code Playgroud)

问题是,从 的输出中ls,您或计算机都无法判断它的哪些部分构成了文件名。是每个字吗?不是,是每一行吗?不。这个问题没有正确的答案,除了:你不知道。

还要注意ls有时文件名数据是如何乱码的(在我们的例子中,它把\n单词“a”“换行符”之间的字符变成了一个?问号......

...

如果您只想遍历当前目录中的所有文件,请使用for循环和 glob:

for f in *; do
    [[ -e $f ]] || continue
    ...
done
Run Code Online (Sandbox Code Playgroud)

作者在返回包含shell glob的文件名列表时将其称为乱码文件ls,然后建议使用 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'
Run Code Online (Sandbox Code Playgroud)

POSIX 将-1-q ls操作数定义为:

-q- 强制将不可打印的文件名字符和<tab>s 的每个实例写为问号 ( '?') 字符。如果输出到终端设备,则实现可能会默认提供此选项。

-1- (数字一位。)强制输出为每行一个条目。

Globbing 并非没有问题 -?匹配任何字符,因此?列表中的多个匹配结果将多次匹配同一个文件。这很容易处理。

虽然如何做这件事不是重点——毕竟它不需要做太多事情,下面演示了——我对为什么不感兴趣。在我看来,该问题的最佳答案已被接受。我建议您尝试更多地关注告诉人们他们可以做什么而不是他们不能做什么我认为,至少你被证明是错误的可能性要小得多。

但为什么还要尝试呢?不可否认,我的主要动机是其他人一直告诉我我不能。我非常清楚,ls只要您知道要寻找什么,输出就像您希望的那样有规律和可预测。错误信息比大多数事情更困扰我。

然而,事实是,除了 Patrick's 和 Wumpus Q. Wumbley 的答案(尽管后者的句柄很棒)之外,我认为这里的答案中的大部分信息大多是正确的——shell glob 更易于使用并且在搜索当前目录时通常比解析ls. 然而,至少在我看来,它们不足以证明传播上述文章中引用的错误信息是合理的,也不是“永不解析ls”的可接受的理由

请注意,帕特里克的答案不一致的结果主要是他使用zshthen的结果bashzsh- 默认情况下 - 不分词$(命令以)可移植的方式替换结果。所以当他问其余的文件去了哪里时?这个问题的答案是你的壳吃了它们。这就是为什么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" $# )
}
Run Code Online (Sandbox Code Playgroud)

输出

`ls` ?QUOTED `-m` COMMA,SEP
??\, ??^, ??`, ??b, [?\, [?\, ]?^, ]?^, _?`, _?`, a?b, a?b

NOW LITERAL - COMMA,SEP
?
 \, ?
     ^, ?
         `, ?
             b, [       \, [
\, ]    ^, ]
^, _    `, _
`, a    b, a
b

FILE COUNT: 12
Run Code Online (Sandbox Code Playgroud)

现在,我将保护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
Run Code Online (Sandbox Code Playgroud)

输出:

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'
Run Code Online (Sandbox Code Playgroud)

下面我再次解决这个问题,但我使用了不同的方法。请记住 - 除了\0空值 - /ASCII 字符是路径名中唯一被禁止的字节。我把 globs 放在一边,而是将 POSIX 指定的-d选项ls和 POSIX 指定的-exec $cmd {} +构造结合起来find。因为find只会自然地/按顺序发出一个,所以下面很容易获得一个递归且可靠分隔的文件列表,包括每个条目的所有 dentry 信息。想象一下你可以用这样的东西做什么:

#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/?
        `///
...
Run Code Online (Sandbox Code Playgroud)

ls -i 可能非常有用 - 特别是当结果唯一性有问题时。

ls -1iq | 
sed '/ .*/s///;s/^/-inum /;$!s/$/ -o /' | 
tr -d '\n' | 
xargs find
Run Code Online (Sandbox Code Playgroud)

这些只是我能想到的最便携的方式。使用 GNU,ls您可以:

ls --quoting-style=WORD
Run Code Online (Sandbox Code Playgroud)

最后,这里有一种更简单的解析ls方法,我在需要 inode 编号时经常使用它:

ls -1iq | grep -o '^ *[0-9]*'
Run Code Online (Sandbox Code Playgroud)

这只是返回 inode 编号 - 这是另一个方便的 POSIX 指定选项。

zwo*_*wol 212

我完全不相信这一点,但是为了争论,让我们假设您可以,如果您准备付出足够的努力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))
Run Code Online (Sandbox Code Playgroud)

这对于文件名中的异常字符没有任何问题——输出是模糊的,就像输出是模糊的一样ls,但这在“真实”程序中无关紧要(与这样的演示相反),这将os.path.join(subdir, f)直接使用结果。

同样重要的是,与你写的东西形成鲜明对比的是,六个月后它仍然有意义,并且当你需要它做一些稍微不同的事情时很容易修改。作为说明,假设您发现需要排除 dotfiles 和编辑器备份,并按 basename 按字母顺序处理所有内容:

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))
Run Code Online (Sandbox Code Playgroud)

脚注 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 放入搅拌机中,但我不知道结果会是什么样子。

  • 这是非常具有误导性的。Shell 不是一种好的编程语言,只是因为它不是一种编程语言。它是一种脚本语言。它是一种很好的脚本语言。 (23认同)
  • 没有递归,只是嵌套了 `for` 循环。`os.walk` 在幕后做了一些非常繁重的工作,但你不必担心它,就像你不必担心 `ls` 或 `find` 在内部如何工作一样。 (12认同)
  • 从技术上讲,`os.walk` 返回一个 [生成器对象](https://wiki.python.org/moin/Generators)。生成器是 Python 版本的惰性列表。每次外部 for 循环迭代时,都会调用生成器并“产生”另一个子目录的内容。Perl 中的等效功能是 [`File::Find`](http://perldoc.perl.org/File/Find.html),如果有帮助的话。 (8认同)
  • @iconoclast 郑重声明,我断言 Bourne shell 既是一种糟糕的编程语言,也是一种糟糕的脚本语言,无论您选择如何定义这些术语。这很糟糕,期间。 (7认同)
  • 您应该知道,我 100% 同意您批评的文件以及 Patrick 和 Terdon 的回答。我的回答旨在提供一个*额外*的独立原因,以避免解析 `ls` 输出。 (6认同)
  • 这很好。是否`for in | 因为在` 谈到递归?我不知道。即使是它也不能超过一个,对吧?这是迄今为止唯一对我有意义的答案。 (5认同)
  • @MilesRout 我并不是特别希望继续这个论点,但为了让我的立场绝对明确,我承认有很多程序在 Python 中比在 Bourne shell 的任何变体中都要长 5 倍,但我断言它们中的大多数_应该用Python(或其他一些不错的脚本语言)编写**即使如此**_。因为尽管它们更长,但更容易正确地编写、更容易地阅读并确认其正确性,并且将来更容易修改。 (5认同)
  • @MilesRout是否有一个单一的原则可以用来区分“编程语言”和“脚本语言”,并且跨越时间、空间和用户?我非常怀疑。如果我输入 `git some_command`,我不在乎它是用 C、Perl 还是 Bash 实现的。事实上,一些 git 命令一开始是一个命令,后来被重写为另一个命令。有些语言可以解释型也可以编译型。即使代码相同,Lisp 在解释时是“脚本语言”,在编译时是“编程语言”吗? (4认同)
  • @MilesRout 如果它已经建立良好,那么请给我一个区分它们的单一原则。无论如何,为什么要把它放在一个不同的类别中以使其免受所有批评呢?这是没有意义的。 (3认同)
  • 我应该注意到,在各种评论中,mikeserv 解析 `ls` 的主要原因是他可以在遍历之前进行一些额外的预处理(例如使用 `grep` 进行排序或过滤)。此替代方案目前不这样做。 (2认同)
  • 顺便说一句,为什么 sys.stdout.write 而不是打印? (2认同)
  • 如果没有完全或正确地理解所建模的过程,则任何算法都是复杂的。关于 Bourne Shell 或 BASH 的有用性,这是一个似是而非的主观结论。Shell 脚本对于在命令行中工作很重要。我不认为它的目的是与完全为算法处理创建的环境相提并论。 (2认同)
  • 当我们询问如何在 X 中做某件事时,我们会告诉你应该使用 Y。那么不能运行 python 的平台(例如 android)呢? (2认同)
  • @MilesRout 另外,_Bash 很好_ 的说法并没有解决这个答案中提出的批评。你能支持这个说法吗?尽管存在所描述的问题,但它的优点是什么? (2认同)

phe*_*mer 199

该链接被大量引用,因为该信息完全准确,并且已经存在很长时间了。


ls用 glob 字符替换不可打印的字符是,但这些字符不在实际文件名中。为什么这很重要?2个原因:

  1. 如果将该文件名传递给程序,则该文件名实际上并不存在。它必须扩展 glob 才能获得真实的文件名。
  2. 文件 glob 可能匹配多个文件。

例如:

$ touch a$'\t'b
$ touch a$'\n'b
$ ls -1
a?b
a?b
Run Code Online (Sandbox Code Playgroud)

请注意我们有 2 个看起来完全相同的文件。如果它们都表示为 ,您将如何区分它们a?b


当 ls 返回包含 shell glob 的文件名列表时,作者将其称为 garbling filenames,然后建议使用 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
Run Code Online (Sandbox Code Playgroud)

注意xxd输出如何显示$file包含原始字符\tand \n,而不是?

如果你使用ls,你会得到这个:

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

“反正我要迭代,为什么不使用ls?”

你给出的例子实际上不起作用。看起来它有效,但它没有。

我指的是这个:

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

我创建了一个包含一堆文件名的目录:

$ 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
Run Code Online (Sandbox Code Playgroud)

当我运行你的代码时,我得到了这个:

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

剩下的文件去哪儿了?

让我们试试这个:

$ 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
Run Code Online (Sandbox Code Playgroud)

现在让我们使用一个实际的 glob:

$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a  b
./a?b
./a?b
./a b
./a
b
Run Code Online (Sandbox Code Playgroud)

用 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
Run Code Online (Sandbox Code Playgroud)

与您的代码完全不同的结果:

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
Run Code Online (Sandbox Code Playgroud)

使用 shell glob,它可以正常工作:

$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a  b
./a?b
./a?b
./a b
./a
b
Run Code Online (Sandbox Code Playgroud)

bash 这种行为的原因可以追溯到我在答案开头提出的观点之一:“文件 glob 可能匹配多个文件”。

lsa?b为多个文件返回相同的 glob ( ),所以每次我们展开这个 glob 时,我们都会得到每个匹配它的文件。


如何重新创建我正在使用的文件列表:

touch 'a b' 'a  b' a$'\xe2\x80\x82'b a$'\xe2\x80\x83'b a$'\t'b a$'\n'b
Run Code Online (Sandbox Code Playgroud)

十六进制代码是 UTF-8 NBSP 字符。

  • @mikeserv 不,这一切仍然适用于 bash。虽然我已经完成了这个问题,因为你没有听我在说什么。 (29认同)
  • “不是其他的”?这是不一致的行为和意想不到的结果,这怎么不是原因? (19认同)
  • @mikeserv 你没有看到我对你的问题的评论吗?Shell globbing 比 `ls` 快 2.5 倍。我还要求您测试您的代码,因为它不起作用。zsh 与这些有什么关系? (11认同)
  • 你知道吗,我想*我会*赞成这个答案,并在我的书中澄清我同意它所说的一切。;-) (8认同)
  • @mikeserv 实际上他的解决方案没有返回一个 glob。我刚刚更新了我的答案以澄清这一点。 (5认同)
  • @mikeserv 您可以使用类似 `for f in $(ls -1q | tr " " "?" | sed 's/^/"/; s/$/"/') 之类的内容来避免重复。做 echo "$f"; 完成`。但为什么不只是 `for f in *; 做 echo "$f"; 完成`? (4认同)
  • @mikeserv 再次更新。请参阅以下部分:关于“无论如何我都要迭代,为什么不使用 ls?” (2认同)
  • @mikeserv zsh。当我切换到 bash 时,情况也同样糟糕。我已经更新了 bash。 (2认同)
  • 好吧,@terdon 我可以避免像`set $(ls -1q | uniq)` 这样的重复项,并且*大部分*我会使用shell glob - 但我不喜欢错误信息的传播。如果我想做一个递归的`ls`怎么办?在 shell 中做同样的事情是*慢的*。我仍然没有看到不解析 `ls` 的真正理由 - 没有人以不同的方式向我展示。 (2认同)

小智 59

的输出ls -q根本不是一个glob。它?过去的意思是“这里有一个不能直接显示的字符”。Glob 用于?表示“此处允许使用任何字符”。

水珠有其它特殊字符(*[]至少,和内侧[]对有以上)。这些都没有被ls -q.

$ touch x '[x]'
$ ls -1q
[x]
x
Run Code Online (Sandbox Code Playgroud)

如果你对待ls -1q输出有一组 globs 并展开它们,你不仅会得到x两次,而且会[x]完全错过。作为一个 glob,它不会将自己作为字符串匹配。

ls -q 旨在保护您的眼睛和/或终端免受疯狂角色的伤害,而不是产生可以反馈给外壳的东西。


ter*_*don 55

让我们试着简化一下:

$ touch a$'\n'b a$'\t'b 'a b'
$ ls
a b  a?b  a?b
$ IFS="
"
$ set -- $(ls -1q | uniq)
$ echo "Total files in shell array: $#"
Total files in shell array: 4
Run Code Online (Sandbox Code Playgroud)

看?那里已经错了。有3个文件,但庆典的报告4.这是因为set正考虑所产生的水珠,ls它被传递到之前的外壳中展开set。这就是为什么你得到:

$ for x ; do
>     printf 'File #%d: %s\n' $((i=$i+1)) "$x"
> done
File #1: a b
File #2: a b
File #3: a    b
File #4: a
b
Run Code Online (Sandbox Code Playgroud)

或者,如果您更喜欢:

$ printf ./%s\\0 "$@" |
> od -A n -c -w1 |
> sed -n '/ \{1,3\}/s///;H
> /\\0/{g;s///;s/\n//gp;s/.*//;h}'
./a b
./a b
./a\tb
./a\nb
Run Code Online (Sandbox Code Playgroud)

以上是运行bash 4.2.45

  • @mikeserv:如果你有时间和耐心,它可以做得对。但事实是,它天生就容易出错。你自己弄错了。*在争论它的优点时!*这是对它的*巨大*打击,即使是为它而战的人也没有正确地做到这一点。而且很有可能,你可能会花更多的时间在做对之前把它弄错。我不知道你是怎么想的,但大多数人更喜欢利用他们的时间而不是用同一行代码摆弄多年。 (42认同)
  • @mikeserv:反对它的论点是有根据的,当之无愧。甚至你已经证明它们是真实的。 (18认同)
  • 我赞成这个。很高兴看到你自己的代码咬你。但仅仅因为我做错了并不意味着它不能做对。今天早上我向你展示了一个非常简单的方法,使用 `ls -1qRi | grep -o '^ *[0-9]*'` - 这是解析 `ls` 输出,伙计,这是我所知道的获取 inode 编号列表的最快和最好的方法。 (2认同)
  • @cHao - 我不同意。咒语和智慧之间的界限并不那么清晰。 (2认同)

Bra*_*iam 41

答案很简单:ls您必须处理的特殊情况超过任何可能的好处。如果您不解析ls输出,则可以避免这些特殊情况。

这里的口头禅是从不信任用户文件系统(相当于从不信任用户输入)。如果有一种方法可以 100% 确定地始终有效,那么它应该是您更喜欢的方法,即使效果ls相同但确定性较低。我不会深入技术细节,因为terdonPatrick 已经广泛地介绍了这些细节。我知道,由于ls在我的工作/声望岌岌可危的重要(并且可能是昂贵的)交易中使用的风险,如果可以避免,我会更喜欢任何没有不确定性等级的解决方案。

我知道有些人更喜欢风险而不是确定性,但我已经提交了错误报告


Voo*_*Voo 35

人们说从不做某事的原因不一定是因为它绝对不能正确完成。我们或许能够这样做,但它可能更复杂,空间或时间方面的效率都较低。例如,可以说“永远不要在 x86 程序集中构建大型电子商务后端”。

现在解决手头的问题:正如您所展示的,您可以创建一个解析 ls 并给出正确结果的解决方案 - 所以正确性不是问题。

是不是更复杂?是的,但我们可以将其隐藏在辅助函数后面。

所以现在要提高效率:

空间效率:您的解决方案依赖于uniq过滤掉重复项,因此我们不能懒惰地生成结果。所以无论是O(1)vs.O(n)还是两者都有O(n)

时间效率:最好的情况是uniq使用 hashmap 方法,所以我们仍然有一个O(n)关于采购元素数量的算法,尽管它可能是O(n log n).

现在真正的问题是:虽然你的算法看起来还不错,但我非常小心地使用采购的元素而不是 n 的元素。因为这确实有很大的不同。假设您有一个文件\n\n,它将导致??匹配列表中的每 2 个字符的文件。有趣的是,如果你有另一个文件\n\r也会导致??并返回所有 2 个字符文件..看看这是怎么回事?指数而不是线性行为当然可以称为“更糟糕的运行时行为”。这是实用算法与您在理论 CS 期刊上写论文的算法之间的区别。

每个人都喜欢例子,对吗?开始了。创建一个名为“test”的文件夹,并在该文件夹所在的同一目录中使用此 python 脚本。

#!/usr/bin/env python3
import itertools
dir = "test/"
filename_length = 3
options = "\a\b\t\n\v\f\r"

for filename in itertools.product(options, repeat=filename_length):
        open(dir + ''.join(filename), "a").close()
Run Code Online (Sandbox Code Playgroud)

唯一的作用是为 7 个字符生成长度为 3 的所有产品。高中数学告诉我们应该是 343 个文件。嗯,打印起来应该很快,所以让我们看看:

time for f in *; do stat --format='%n' "./$f" >/dev/null; done
real    0m0.508s
user    0m0.051s
sys 0m0.480s
Run Code Online (Sandbox Code Playgroud)

现在让我们尝试您的第一个解决方案,因为我真的无法理解

eval set -- $(ls -1qrR ././ | tr ' ' '?' |
sed -e '\|^\(\.\{,1\}\)/\.\(/.*\):|{' -e \
        's//\1\2/;\|/$|!s|.*|&/|;h;s/.*//;b}' -e \
        '/..*/!d;G;s/\(.*\)\n\(.*\)/\2\1/' -e \
        "s/'/'\\\''/g;s/.*/'&'/;s/?/'[\"?\$IFS\"]'/g" |
uniq)
Run Code Online (Sandbox Code Playgroud)

在这里可以在 Linux mint 16 上工作(我认为这说明了这种方法的可用性)。

无论如何,由于上述几乎只在获得结果后过滤结果,因此较早的解决方案应该至少与较晚的解决方案一样快(该解决方案中没有 inode 技巧 - 但这些不可靠,因此您会放弃正确性)。

那么现在多久

time for f in $(ls -1q | tr " " "?") ; do stat --format='%n' "./$f" >/dev/null; done
Run Code Online (Sandbox Code Playgroud)

拿?好吧,我真的不知道,检查343^343个文件名需要一段时间-我会在宇宙热死后告诉你。

  • 当然,正如在 [another answer](http://unix.stackexchange.com/a/128989/135943) 下的评论中提到的,声明“......你已经证明你可以创建一个解析 ls 并给出正确的结果......”实际上并非如此。 (6认同)

小智 27

OP 的声明意图得到解决

前言和原始答案的基本原理更新于 2015-05-18

mikeserv(OP)在对他的问题的最新更新中表示:“虽然我第一次问这个问题是为了指出错误信息的来源,但我确实认为这是一种耻辱,不幸的是,这里最受好评的答案在很大程度上具有误导性。 ”

哦,那好吧; 我觉得很遗憾,我花了这么多时间试图弄清楚如何解释我的意思,却在我重新阅读问题时发现了这一点。这个问题最终以“[生成] 讨论而不是答案” 结束,并最终占到了大约 18K 的文本(仅就问题而言,只是为了清楚起见),这对于一篇博文来说也是很长的。

但 StackExchange 不是您的肥皂盒,也不是您的博客。但是,实际上,您至少将它用作两者的一部分。人们最终花了很多时间来回答你的“To-Point-Out”,而不是回答人们的实际问题。鉴于 OP 已明确表示它甚至根本不是一个问题,因此我将标记该问题不适合我们的格式。

在这一点上,我不确定我的回答是否切中要害;可能不是,但它是针对你的一些问题,也许它可以是对其他人有用的答案;初学者要振作起来,一旦您有更多经验,其中一些“不做”就会变成“有时做”。:)

作为基本规则...

请原谅剩余的粗糙边缘;我已经在这方面花费了太多时间......而不是直接引用 OP(如最初预期的那样),我将尝试进行总结和释义。

[主要从我的原始答案中重新设计]
经过考虑,我认为我误读了 OP 对我回答的问题的强调;然而,点寻址长大了,我已经离开了答案基本完好,因为我认为他们是对的点,并解决问题,我已经看到自小在其他情况下,以及有关建议初学者。

最初的帖子以多种方式询问了为什么各种文章给出了诸如“不要解析ls输出”或“您永远不应该解析ls输出”等建议。

我对这个问题的建议解决方案是,这种陈述的实例只是一个习语的例子,以略有不同的方式表达,其中绝对量词与命令式配对[例如,“不要[永远] X”, «[you should] always Y», «[one should] never Z»] 形成旨在用作一般规则或指导方针的陈述,特别是当给予那些刚接触主题的人时,而不是作为绝对真理,尽管有这些陈述的明显形式

当你开始学习新的主题时,除非你很好地理解为什么你可能需要做其他事情,否则最好无一例外地遵循公认的一般规则——除非在更有经验的人的指导下那是你自己。随着技能和经验的提高,您将能够进一步确定规则何时以及是否适用于任何特定情况。一旦您确实达到了重要的经验水平,您可能会首先了解一般规则背后的推理,然后您就可以开始判断规则背后的原因是否适用以及适用于什么级别这种情况,以及是否存在压倒一切的担忧。

这就是专家可能会选择做违反“规则”的事情的时候。但这不会使他们减少“规则”。

所以,对于手头的话题:在我看来,仅仅因为专家可能会违反这条规则而不会被完全击倒,我看不出有任何理由告诉初学者“有时”是可以解析ls输出,因为:它不是. 或者,至少,初学者这样做肯定是不正确的。

你总是把你的棋子放在中间;开场一首,一招;尽早进入城堡;主教面前的骑士;篮下的骑士是冷酷的;并始终确保您可以看到您的计算到底!(哎呀,对不起,累了,这是国际象棋 StackExchange。)

规则,意味着被打破?

在阅读针对初学者或可能为初学者阅读的主题的文章时,您通常会看到以下内容:

  • “你永远不应该做 X。”
  • “永远不要Q!”
  • “别做Z。”
  • “一个人应该永远做Y!”
  • “C,无论如何。”

虽然这些陈述似乎确实在陈述绝对和永恒的规则,但事实并非如此;相反,这是一种陈述一般规则 [又名“指南”、“经验法则”、“基础知识”等] 的方式,对于可能正在阅读这些文章的初学者来说,这至少可以说是一种适当的方式来陈述它们。然而,仅仅因为它们被表述为绝对的,这些规则当然不会约束专业人士和专家[他们很可能是首先总结这些规则的人,作为记录和传递他们在处理重复出现时获得的知识的一种方式他们特定工艺的问题。]

这些规则当然不会揭示专家如何处理复杂或细微的问题,例如,这些规则相互冲突;或者首先导致规则的担忧根本不适用。专家不害怕(或不应该害怕!)只是打破他们碰巧知道在特定情况下没有意义的规则。专家们在自己的技艺中不断地平衡各种风险和顾虑,必须经常用自己的判断来选择打破这些规则,必须平衡各种因素,不能仅仅依靠一张规则表来遵循。以Goto作为一个例子:有许多人对他们是否有害长,反复发作,辩论。(是的,永远不要使用 goto。;D)

模态命题

一个奇怪的特征,至少在英语中,而且我想在许多其他语言中,一般规则是它们以与模态命题相同的形式陈述,但一个领域的专家愿意给出一个一般规则情况,同时知道他们会在适当的时候打破规则。因此,显然,这些语句并不意味着等同于模态逻辑中的相同语句。

这就是为什么我说它们必须是惯用的。这些规则并非真正成为“从不”或“总是”的情况,而是通常用于编纂一般性指导方针,这些指导方针往往适用于各种情况,并且当初学者盲目地遵循它们时,可能会导致远比初学者在没有充分理由的情况下选择反对它们更好的结果。有时,他们编纂规则只是导致不合标准的结果,而不是在违反规则时伴随错误选择的彻底失败。

因此,一般规则并不是它们表面上的绝对模态命题,而是一种隐含标准样板的给出规则的速记方式,如下所示:

除非您有能力指出本指南在特定情况下不正确,并向自己证明自己是对的,否则 ${RULE}

当然,您可以用“从不解析ls输出”代替 ${RULE}。:)

哦耶!什么关于解析ls输出?

嗯,所以,考虑到所有这些......我认为很明显这条规则是一条很好的规则。首先,真正的规则必须被理解为惯用的,如上所述......

但此外,在某些特定情况下,不仅仅是您必须非常擅长 shell 脚本才能知道它是否会被破坏。同样,当您试图在测试中打破它时,需要同样多的技巧才能告诉您做错了!而且,我自信地说,此类文章的绝大多数可能受众(提供诸如“不要解析输出ls!”之类的建议)无法做这些事情,而那些确实具有这种技能的人可能会意识到他们自己想出来,无论如何都无视规则。

但是......看看这个问题,即使是那些可能有技能的人也认为这样做是一个糟糕的决定;以及问题的作者花了多少努力才达到当前最佳示例的要点!我向你保证,在一个如此困难的问题上,99% 的人都会弄错,并且可能会产生非常糟糕的结果!即使所决定的方法是好的;直到它(或另一个)ls解析思想被 IT/开发人员整体采用,经受住了大量测试(尤其是时间的考验),并最终设法升级到“通用技术”状态,很可能很多人可能会尝试,但会弄错……后果是灾难性的。

所以,我将最后一次重申......,特别是在这种情况下就是为什么“从不解析ls输出!” 绝对是正确的表达方式。

[更新 2014-05-18:澄清了回答(以上)的理由以回应 OP 的评论;以下补充是为了回应 OP 对昨天问题的补充]

[更新 2014-11-10:添加标题和重组/重构内容;还有:重新格式化、重新措辞、澄清和嗯……“简洁化”……我打算这只是一个清理,尽管它确实变成了一点返工。我把它放在了一个遗憾的状态,所以我主要是试图给它一些命令。我确实觉得保持第一部分完整很重要;所以那里只有两个小改动,删除了多余的“但是”,并强调了“那个”。]

† 我最初的目的只是为了澄清我的原件;但经过深思熟虑后决定添加其他内容

‡有关帖子的指南,请参阅https://unix.stackexchange.com/tour

  • [这篇精彩评论](https://security.stackexchange.com/questions/210114/why-cant-i-share-a-one-use-code-with-anyone-else#comment423558_210114) 简单解释一下这次对话. 我会引用它:**你认为他们为什么说“不要低头看枪管”而不是“除非枪管是空的,否则不要低头看枪管”?或者“不要尝试将手指插入电源插座”而不是“不要尝试将手指插入电源插座,除非它们太大而无法插入或除非您已关闭电源”?等等。** (3认同)
  • 从来都不是惯用语。这不是对任何事情的回答。 (2认同)

god*_*eek 17

ls在某些情况下是否可以解析输出?当然。从目录中提取 inode 编号列表的想法是一个很好的例子 - 如果您知道您的实现ls支持-q,因此每个文件将产生一行输出,而您只需要 inode 编号,将它们解析出来ls -Rai1q输出当然是一个可能的解决方案。当然,如果作者之前没有看到像“永远不要解析 ls 的输出”这样的建议,他可能不会考虑包含换行符的文件名,因此可能会省略“q”,而在这种极端情况下,代码会被巧妙地破坏 - 因此,即使在解析ls的输出合理的情况下,这个建议仍然有用。

更广泛的一点是,当 shell 脚本的新手试图让脚本找出(例如)目录中最大的文件是什么,或者目录中最近修改的文件是什么时,他的第一直觉是解析ls's输出 - 可以理解,因为这ls是新手学习的第一个命令之一。

不幸的是,这种直觉是错误的,这种方法被打破了。更不幸的是,它被巧妙地破坏了 - 它在大部分时间都可以工作,但在可能被了解代码的人利用的边缘情况下会失败。

新手可能会认为ls -s | sort -n | tail -n 1 | awk '{print $2}'这是一种获取目录中最大文件的方法。它可以工作,直到您有一个名称中带有空格的文件。

好的,那怎么样ls -s | sort -n | tail -n 1 | sed 's/[^ ]* *[0-9]* *//'?工作正常,直到您的文件名称中包含换行符。

当文件名中有换行符时,添加-qtols的参数有帮助吗?它可能看起来确实如此,直到您有 2 个不同的文件,这些文件在文件名的同一位置包含不可打印的字符,然后ls的输出无法让您区分哪些是最大的。更糟糕的是,为了扩展“?”,他可能会求助于他的 shell eval- 如果他点击一个名为的文件,例如,这将导致问题,

foo`/tmp/malicious_script`bar
Run Code Online (Sandbox Code Playgroud)

--quoting-style=shell帮助ls吗(如果你甚至支持它)?不行,还是显示?对于不可打印的字符,因此多个匹配中哪一个最大仍然不明确。 --quoting-style=literal? 不,一样。 --quoting-style=locale或者--quoting-style=c如果您只需要明确地打印最大文件的名称可能会有所帮助,但如果您之后需要对文件做一些事情可能不会有帮助 - 这将是一堆代码来撤消引用并返回到真实的文件名所以你可以将它传递给 gzip。

在所有这些工作结束时,即使他所拥有的对于所有可能的文件名都是安全且正确的,但它是不可读和不可维护的,并且本可以在 python、perl 或 ruby​​ 中更容易、安全和可读地完成。

或者甚至使用其他 shell 工具 - 在我的脑海中,我认为这应该可以解决问题:

find . -type f -printf "%s %f\0" | sort -nz | awk 'BEGIN{RS="\0"} END{sub(/[0-9]* /, "", $0); print}'
Run Code Online (Sandbox Code Playgroud)

并且至少应该像--quoting-style现在一样便携。