“无论如何我都会迭代,为什么不使用呢ls?”

“无论如何我都会迭代,为什么不使用呢ls?”

我一直看到答案引用这个链接明确地陈述“不要解析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^@^@^@$
^@

了解更多。

现在真正让我感兴趣的是本文的下一部分:

$ 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 的明显例外。 Wumbley 的答案(尽管后者的手柄很棒)我认为这里答案中的大部分信息大部分都是正确的 - 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包含原始字符\tand \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,如果可以避免的话,我会更喜欢任何没有不确定性的解决方案。

我知道有些人更喜欢风险大于确定性, 但我已提交错误报告

相关内容