AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / unix / 问题 / 754193
Accepted
terdon
terdon
Asked: 2023-08-17 00:08:44 +0800 CST2023-08-17 00:08:44 +0800 CST 2023-08-17 00:08:44 +0800 CST

现在解析 GNU ls 的输出是否安全?

  • 772

过去几十年来公认的观点是,解析ls( [1] , [2] ) 的输出从来都不是一个好主意。例如,如果我想将文件的修改日期及其名称保存到 shell 变量中,则这不是正确的方法:

$ ls -l file
-rw-r--r-- 1 terdon terdon 0 Aug 15 19:16 file
$ foo=$(ls -l file | awk '{print $9,$6,$7,$8}')
$ echo "$foo"
file Aug 15 19:16

只要文件名稍有不同,该方法就会失败:

$ ls -l file*
-rw-r--r-- 1 terdon terdon 0 Aug 15 19:16 'file with spaces'
$ foo=$(ls -l file* | awk '{print $9,$6,$7,$8}')
$ echo "$foo"
file Aug 15 19:16

如果文件的修改日期与今天不接近,情况会变得更糟,因为这可能会更改时间格式:

$ ls -l
total 0
-rw-r--r-- 1 terdon terdon 0 Aug 15 19:21  file
-rw-r--r-- 1 terdon terdon 0 Aug 15  2018 'file with spaces'

然而,较新版本的 GNU coreutilsls有两个选项,可以组合起来设置特定的时间格式并生成 NULL 界定的输出:

      --time-style=TIME_STYLE
              time/date format with -l; see TIME_STYLE below
[...]
     --zero end each output line with NUL, not newline
[...]
       The TIME_STYLE argument can be full-iso,  long-iso,  iso,  locale,  or
       +FORMAT.   FORMAT  is  interpreted like in date(1).  If FORMAT is FOR‐
       MAT1<newline>FORMAT2, then FORMAT1 applies  to  non-recent  files  and
       FORMAT2  to recent files.  TIME_STYLE prefixed with 'posix-' takes ef‐
       fect only outside the POSIX locale.  Also the  TIME_STYLE  environment
       variable sets the default style to use.

这里再次是设置了这些选项的文件(每行输出末尾的零被替换为#换行符,以稍微提高可读性):

$ ls -l --zero --time-style=long-iso -- *
-rw-r--r--+ 1 terdon terdon 0 2023-08-16 21:35 a file with a
newline#
-rw-r--r--+ 1 terdon terdon 0 2023-08-15 19:16 file#
-rw-r--r--+ 1 terdon terdon 0 2018-08-15 12:00 file with spaces#

ls有了这些可用的选项,我可以做许多传统上有害的事情。例如:

  1. 将最近修改的文件名放入变量中:

    $ touch 'a file with a'$'\n''newline'
    $ last=$(ls -tr --zero | tail -z -n1)
    bash: warning: command substitution: ignored null byte in input
    $ printf -- 'LAST: "%s"\n' "$last"
    LAST: "a file with a 
    newline"
    
  2. 引发这个问题的例子。另一个问题,在 Ask Ubuntu 上,OP 想要打印文件名和修改日期。有人使用和 一个巧妙的技巧发布了答案,如果我们添加到,它似乎非常强大:lsawk--zerols

    $ output=$(ls -l --zero --time-style=long-iso -- * | 
               awk 'BEGIN{RS="\0"}{ t=index($0,$7); print substr($0,t+6), $6 }')
    $ printf 'Output: "%s"\n' "$output"
    Output: "a file with a
    newline 2023-08-16"
    

我找不到一个可以打破这两个例子的名字。所以,我的问题是:

  1. 是否存在上述两个示例之一会失败的情况?也许有一些奇怪的地方?
  2. 如果不是,这是否意味着现代版本的 GNUls实际上可以安全地使用任意文件名?
shell
  • 3 3 个回答
  • 3946 Views

3 个回答

  • Voted
  1. Best Answer
    ilkkachu
    2023-08-17T00:59:19+08:002023-08-17T00:59:19+08:00

    现在解析 GNU ls 的输出是否安全?(与--zero)

    --zero确实有很大帮助,但这里使用的方式仍然不安全。ls其输出格式本身以及问题中用于解析输出的命令都 存在问题。--zero实际上在 ParsingLs wiki 页面中提到过,但他们在示例中没有使用长格式(可能是因为这里的问题!)。此答案中的许多问题是由 Stéphane Chazelas 在评论中提出的。


    首先,ls -l这是一个问题,因为它仍然愉快地按原样打印包含空格的用户/组名称,弄乱了列数(--zero这里并不重要):

    $ ls -l --time-style=long-iso foo.txt
    -rw-rw-r-- 1 foo bar users 0 2023-08-16 16:45 foo.txt
    

    至少,您需要--numeric-uid-gid/ -n,它将 UID 和 GID 打印为数字,或者-go完全忽略它们。两者也都包含其他长格式字段。

    ls还将列出参数中出现的任何目录的内容,因此您可能-d还需要 。

    我认为其他列不能包含空格或 NUL,所以

    ls -dgo --time-style=long-iso --zero -- *
    

    可能是安全的。或许。

    它仍然不是最容易解析的,因为如果有多个文件,它会用空格填充列,而不是仅使用一个作为字段分隔符,因此您不能在输出上使用 eg cut。--zero即使输出到带有和省略 UID 和 GID 的管道时,也会发生这种情况,因为文件大小和链接计数的宽度可能会有所不同:

    $ ls -dgo --zero --time-style=long-iso -- *.txt |tr '\0' '\n'
    -rw-rw-r-- 21    0 2023-08-16 17:24 bar.txt
    -rw-rw-r--  1 1234 2023-08-16 17:30  leading space.txt
    

    文件名没有填充到右侧(这样做会很奇怪),因此可以安全地假设时间戳和文件名之间只有一个空格。

    --time-style=long-iso不包括 UTC 偏移量,这意味着日期可能不明确。最坏的情况是,在夏令时结束时创建的两个文件可能会显示日期顺序错误的情况。(ls如果要求的话,仍然会正确地对它们进行排序,但输出会令人困惑。)--full-time/ --time-style=full-iso(或自定义格式)在这方面会更好,并且显式设置TZ=UTC0将使日期更容易作为字符串进行比较:

    $ TZ=Europe/Helsinki ls -dgo --time-style=long-iso -- *
    -rw-rw-r-- 1 0 2023-10-29 03:30 first
    -rw-rw-r-- 1 0 2023-10-29 03:20 second
    
    $ TZ=UTC0 ls -dgo --full-time -- *
    -rw-rw-r-- 1 0 2023-10-29 00:30:00.000000000 +0000 first
    -rw-rw-r-- 1 0 2023-10-29 01:20:00.000000000 +0000 second
    
    $ TZ=UTC0 ls -dgo --time-style=+%FT%T.%NZ -- *
    -rw-rw-r-- 1 0 2023-10-29T00:30:00.000000000Z first
    -rw-rw-r-- 1 0 2023-10-29T01:20:00.000000000Z second
    

    如果除了常规文件之外还有其他东西,情况会变得更糟。在很多情况下可能不是问题,但无论如何:

    对于设备文件,ls不打印其大小,而是打印主/次设备编号。用逗号和空格分隔,使列数与其他文件不同。您可以通过逗号区分这两个变体,但这会使解析更加痛苦。

    $ ls -dgo --zero --time-style=long-iso -- /dev/null somefile.txt |tr '\0' '\n'
    crw-rw-rw- 1  1, 3 2023-07-16 15:37 /dev/null
    -rw-rw-r-- 1 12345 2023-08-17 06:14 somefile.txt
    

    然后是符号链接,其长格式打印为link name -> link target,但没有什么可说链接或目标名称本身可以包含->...

    $ ls -dgo --zero --time-style=long-iso -- how* what* |tr '\0' '\n'
    lrwxrwxrwx 1 14 2023-08-17 06:05 how -> about -> this?
    lrwxrwxrwx 1  5 2023-08-17 05:54 what -> is -> this?
    

    好吧,我想从技术上讲,大小字段告诉了链接名称的长度(以字节为单位,而不是字符)......

    在这种情况下, --quoting-style=shell-escape-always实际上会比 更好--zero,因为它会打印两个单独引用的内容,并在内部转义一些特殊或不可打印的字符$'':

    $ ls -dgo --quoting-style=shell-escape-always --time-style=long-iso -- how* what*  |cat
    lrwxrwxrwx 1 14 2023-08-17 06:05 'how' -> 'about -> this?'
    lrwxrwxrwx 1  5 2023-08-17 05:54 'what -> is' -> 'this?'
    

    即使使用 shell,解析它也不是很有趣。


    如果我们可以明确选择我们想要的字段,那就更好了,但我没有看到ls这样的选项。GNU find 有-printf我认为可以产生安全输出的功能,如果你只想按时间ls排序,则不需要打印时间戳,只需ls --zero使用-t//即可。见下文。(zsh 本身可以做到这一点,但 Bash 不太好。)-u-c

    如果你想要时间戳和文件名,类似的事情 find ./* -printf '%TY-%Tm-%Td %TT %p\0'应该做,尽管默认情况下它会递归到子目录,所以如果你不想要它,你将不得不做一些事情。也许只是添加-prune到最后。也--没有帮助find,所以你需要./前缀。

    也许stat --printf会更容易。


    是否存在上述两个示例之一会失败的情况?也许有一些奇怪的地方?

    在问题中使用的命令中,last=$(ls -tr --zero | tail -z -n1)其本身在 Bash 中是不安全的,因为命令替换会在忽略最后的 NL 后删除尾随换行符。正如Ed Morton 指出的那样,无论其输出有多安全,至少特定的 AWK 命令会被破坏ls。

    我认为 AWK 不太适合具有固定数量字段的输入,其中最后一个字段本身可以包含字段分隔符。Perlsplit()有一个额外的参数来限制要生成的字段数量,但当某些(不是全部)字段分隔符可以是多个空格时,使用该参数不太容易。天真的人split/ +/, $_, 6会吃掉文件名中的前导空格。您可以构建一个正则表达式来处理该问题和设备节点问题,但这开始就像将圆钉强行插入方孔中一样,并且不能解决符号链接输出问题。


    如果没有长格式输出,ls --zero应该只给出以 NUL 结尾的原始文件名,因此输出应该是安全且易于解析的。

    对于$n最旧的文件,维基页面有:

    readarray -t -d '' -n 5 sorted < <(ls --zero -tr)
    # check the number of elements you got
    

    对于只有一个,您可以使用read -rd ''would do,正如评论中提到的:

    IFS= read -rd '' newest < <(ls -t --zero)
    # check the exit status or make sure "$newest" is not empty
    
    • 18
  2. Kaz
    2023-08-17T00:53:47+08:002023-08-17T00:53:47+08:00

    如果您要ls专门依赖 GNU 的输出,则意味着您依赖 GNU Coreutils 包。这意味着您可以使用另一个 Coreutils 实用程序,即stat. Stat 具有格式字符串,用于以所需的方式获取有关对象的信息。

    例如以以下形式打印当前目录的修改时间MMM DD HH:MM:

    $ echo $(date -d @$(stat --format="%Y" .) +"%b %m %H:%M")
    Aug 08 07:57
    

    该命令以十进制整数形式stat --format=%Y .获取对象的修改时间,表示自纪元以来熟悉的秒数。.

    我们使用@前缀作为-d参数date(GNU Coreutils 的一个功能date)对其进行插值,然后使用strftime代码以所需的格式获取时间。

    遗憾的是stat没有使用strftime内置方法来格式化日期。如果我们想要获取多个信息字段,包括修改时间,而不需要多次调用stat,我们必须让它打印多字段行,然后我们必须解析该行。这仍然是比抓取 的输出更好的措施ls。如果最大效率并不重要(如果重要的话,我们为什么要在 Bash 中编码),我们可能会遭受多次调用stat.

    评论中提出了stat不能用于​​发现修改时间最早的文件的声明。确实stat单独做不到,但实际上stat结合 shell 通配符扩展也可以做到,依赖ls -1t.

    $ for x in *.txt ; do stat --format="%Y %n" "$x" ; done | sort -n | head -1
    1328379315 readme-mt.txt
    

    该文件可以追溯到相当早以前:

    $ date -d @1328379315
    Sat Feb  4 10:15:15 PST 2012
    

    现在我们遇到的问题是,如果名称包含换行符,则会弄乱排序。我们可以用ls.

    例如,我们可以将名称读入 Bash 数组,然后将时间戳与数组索引一起打印,而不是名称。从输出中,sort -n | head -1我们获得一个项目,其第二个字段为我们提供了最近最少修改的文件名称的数组索引。

    我们可以完全回避处理具有编码空格和换行符的输出的问题,ls而我们必须以某种方式解析该输出。

    $ array=(*.txt)
    $ for x in ${!array[@]}; do 
    >   printf "%s %s\n" $(stat --format="%Y" "${array[$x]}") $x 
    > done | sort -n | head -1
    1328379315 29
    $ echo "${array[29]}"
    readme-mt.txt
    

    array[29]将保存 遇到的第 30 个文件*.txt,无论该名称由什么字符组成。我们的sort工作不受此影响,因为它看不到该名称。

    因此,为了回答这个问题,GNU ls 有一些功能可以更安全地解析其输出,但是在 shell 语言中安全地解析输出仍然不容易。

    GNU ls 可以被 C 程序安全地使用,该程序使用popen("ls ...", "r")正确的选项ls和 正确的解析逻辑。

    规则“不要抓取ls”的输出是在脚本编写的上下文中。

    • 9
  3. Ed Morton
    2023-08-17T01:09:24+08:002023-08-17T01:09:24+08:00

    鉴于问题中最后一个示例的代码:

    ls -l --zero --time-style=long-iso -- * | 
        awk 'BEGIN{RS="\0"}{ t=index($0,$7); print substr($0,t+6), $6 }'
    

    并发布了该ls命令的示例输出(#<newline>替换 NUL 以提高可见性):

    $ ls -l --zero --time-style=long-iso -- *
    -rw-r--r--+ 1 terdon terdon 0 2023-08-16 21:35 a file with a
    newline#
    -rw-r--r--+ 1 terdon terdon 0 2023-08-15 19:16 file#
    -rw-r--r--+ 1 terdon terdon 0 2018-08-15 12:00 file with spaces#
    

    看起来应该$7是时间戳。如果是这样,那么t=index($0,$7)对于超过 1 个单词的用户名/组将会失败,例如:

    -rw-r--r--+ 1 terdon Domain Users 0 2023-08-15 19:16 file#
    

    从那时起,您的时间戳将位于$8(或更高的数字,具体取决于用户名和/或组中有多少个单词),而不是$7。

    鉴于用户名/组不能包含:,您可以通过仅查找:行中的第一个而不是查找特定字段来解决该问题:

    ls -l --zero --time-style=long-iso -- * | 
        awk -v RS='\0' 'p=index($0,":") { print substr($0,p+4), substr($0,p-13,10) }'
    

    或者使用 GNU awk (你可能正在使用它RS='\0')将第三个参数设置为match():

    ls -l --zero --time-style=long-iso -- * | 
        awk -v RS='\0' 'match($0,/(.{10}) ..:.. (.*)/,a) { print a[2], a[1] }'
    
    • 4

相关问题

  • 这个命令是如何工作的?mkfifo /tmp/f; 猫/tmp/f | /bin/sh -i 2>&1 | 数控 -l 1234 > /tmp/f

  • FreeBSD 的 sh:列出函数

  • 有没有办法让 ls 只显示某些目录的隐藏文件?

  • grep -v grep 有什么作用

  • 如何将带有〜的路径保存到变量中?

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    模块 i915 可能缺少固件 /lib/firmware/i915/*

    • 3 个回答
  • Marko Smith

    无法获取 jessie backports 存储库

    • 4 个回答
  • Marko Smith

    如何将 GPG 私钥和公钥导出到文件

    • 4 个回答
  • Marko Smith

    我们如何运行存储在变量中的命令?

    • 5 个回答
  • Marko Smith

    如何配置 systemd-resolved 和 systemd-networkd 以使用本地 DNS 服务器来解析本地域和远程 DNS 服务器来解析远程域?

    • 3 个回答
  • Marko Smith

    dist-upgrade 后 Kali Linux 中的 apt-get update 错误 [重复]

    • 2 个回答
  • Marko Smith

    如何从 systemctl 服务日志中查看最新的 x 行

    • 5 个回答
  • Marko Smith

    Nano - 跳转到文件末尾

    • 8 个回答
  • Marko Smith

    grub 错误:你需要先加载内核

    • 4 个回答
  • Marko Smith

    如何下载软件包而不是使用 apt-get 命令安装它?

    • 7 个回答
  • Martin Hope
    user12345 无法获取 jessie backports 存储库 2019-03-27 04:39:28 +0800 CST
  • Martin Hope
    Carl 为什么大多数 systemd 示例都包含 WantedBy=multi-user.target? 2019-03-15 11:49:25 +0800 CST
  • Martin Hope
    rocky 如何将 GPG 私钥和公钥导出到文件 2018-11-16 05:36:15 +0800 CST
  • Martin Hope
    Evan Carroll systemctl 状态显示:“状态:降级” 2018-06-03 18:48:17 +0800 CST
  • Martin Hope
    Tim 我们如何运行存储在变量中的命令? 2018-05-21 04:46:29 +0800 CST
  • Martin Hope
    Ankur S 为什么 /dev/null 是一个文件?为什么它的功能不作为一个简单的程序来实现? 2018-04-17 07:28:04 +0800 CST
  • Martin Hope
    user3191334 如何从 systemctl 服务日志中查看最新的 x 行 2018-02-07 00:14:16 +0800 CST
  • Martin Hope
    Marko Pacak Nano - 跳转到文件末尾 2018-02-01 01:53:03 +0800 CST
  • Martin Hope
    Kidburla 为什么真假这么大? 2018-01-26 12:14:47 +0800 CST
  • Martin Hope
    Christos Baziotis 在一个巨大的(70GB)、一行、文本文件中替换字符串 2017-12-30 06:58:33 +0800 CST

热门标签

linux bash debian shell-script text-processing ubuntu centos shell awk ssh

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve