【问题标题】:How to Iterate Null Separated Results in non-Bash Shell如何在非 Bash Shell 中迭代空分隔结果
【发布时间】:2014-03-31 13:27:43
【问题描述】:

好的,所以我有一个脚本处理 find 的空分隔输出,我可以像这样使用 bash shell 轻松处理它:

#!/bin/sh
find "$1" -print0 | while read -rd '' path; do echo "$path"; done

相当愚蠢的示例,因为它只是将结果转换为换行符,但这只是为了让您了解我想要做什么。这种基本方法效果很好,并且避免了由于文件可能包含各种文件系统上的换行符而导致的潜在问题。

但是,我需要在非 bash shell 上做同样的事情,这意味着我失去了对 read -d 的支持。那么,在不诉诸 bash(或其他 shell)特定功能的情况下,有没有一种方法可以像上面那样处理空分隔的结果?

如果不是,最好的方法是保护自己免受结果中的换行符的影响?我在想我也许可以使用find-exec 选项将文件名中的换行符替换为某种转义值,但我不确定查找和替换换行符的最佳方法(例如,我不能使用tr)或使用什么替换,这就是为什么空字符是最好的选择。

【问题讨论】:

  • 这个问题的答案真的很令人沮丧。我想解析env -0 并修改变量,所以 xargs 是不行的,find 与我的问题无关。这里的所有答案都以 find 或 xargs 为中心。我想知道如何在非 bash shell 中迭代空分隔的结果,而不是在一些令人讨厌的子 shell 中!请回答标题,请有人,我求求你。
  • @rektide 标题中确切问题的答案是你可以在zsh中使用read -rd '',这是一个非bash shell。

标签: bash shell


【解决方案1】:

How can I find and safely handle file names containing newlines, spaces or both?

你可以例如使用find -exec:

find [...] -exec <command> {} \;

xargs -0:

find [...] -print0 | xargs -r0 <command>

请注意,在上面的示例中,您仍然需要设置 IFS 否则您将剪掉前导/尾随空格:

while IFS= read -rd '' file; do
   do_something_with "${file}"
done

你说得对,这个read 只能在bash 中正常工作,这真是令人遗憾。我通常不会在乎文件名中可能的换行符,只是确保如果它们发生(而不是忽略问题和你的脚本爆炸),否则可移植代码不会中断,我相信这对于大多数情况来说已经足够了,例如

while IFS= read -r file; do
    [ -e "${file}" ] || continue # skip over truncated filenames due to newlines
    do_something_file "${file}"
done < <(find [...])

或使用行为正确的globbing(如果可能):

for file in *.foo; do
    [ -e "${file}" ] || continue # or use nullglob
    do_something_file "${file}"
done

【讨论】:

  • @mklement0 很好,-print0 也是如此 :-) 这就是为什么在大多数情况下,我会按照我解释的那样做,这适用于任何 POSIX 兼容的 shell(如果你替换进程用find | while read 循环替换),而不是为空分隔而烦恼。在第一个示例中,[ -e ] 是必要的,以避免由于可能的换行符而处理截断的文件名,而第二个示例中的 [ -e ] 是必需的,因为如果没有匹配的文件,file 可能会扩展为文字 *.foo(因为shopt -s nullglob 也不是可移植的,所以我更喜欢显式测试)。
  • 我想这取决于您的要求,(非)bashGNU-userland 与任何bash/ksh(/zsh) 与POSIX-userland 与带有POSIX-userland 的“普通”POSIX shell。我个人不喜欢针对第一个场景/环境。
  • 感谢所有这些;不知道-print0 是非POSIX。不过,离得太近了 :) “其他实现已经添加了其他方法来解决这个问题,特别是 -print0 主文件,它使用空字节终止符编写文件名。这里考虑过,但没有采用。” - man.cx/find 也就是说,-print0 的实施可能比 xargs -r 更广泛,从其他 1 个平台系列的样本来看:) - 那些具有 BSD 传统的平台,包括 OSX。
  • 我会完全同意,但我给了它一个快速的谷歌,似乎所有的 NetBSD、OpenBSD 和 FreeBSD 都附带了一个支持 -rxargs 这些天是 GNU-兼容,只是 Apple 的旧 BSD 实现不附带 -r,所以我部分同意:) 另一个原因是我避免这种混乱,不尝试使用换行符处理文件名,只是让我的代码故障安全,所以它不会爆炸。
  • 很好的答案,不幸的是,我认为我无法在我的目标系统上支持xargs(Synology NASes 真的在 shell 脚本工作中扔了一把扳手)。所以我想我将不得不尝试你关于仔细检查文件名是否存在的建议,但也许可以使用前瞻来尝试用换行符“修复”文件名。还要感谢您提醒我IFS=,我在我的示例中忘记了这一点!实际上,我只是突然想到我可以利用查找结果的结构,因为它们应该具有一致的有效行开头(在我的示例中为 $1 的值)。
【解决方案2】:

补充@Adrian Frühwirth 的出色回答:

这是一个严格符合 POSIX 的解决方案,无论是在 shell 代码所使用的实用程序及其选项方面:

find . -exec sh -c 'for f in "$@"; do echo "$f"; done' - {} +

这避免了find-print0read -d

(假设您的 shell 代码将被多次调用,即当输入文件名太多以至于它们不适合单个命令行时。
getconf ARG_MAX 告诉您您平台的最大. 用于调用外部实用程序的命令行长度,但请注意实际上限制较低;请参阅http://www.in-ulm.de/~mascheck/various/argmax/)

【讨论】:

  • 感谢您的补充,但我可能应该在我的问题中指定我已经使用 -exec 的原因是因为我需要调用一个函数,而 find 没有出现能够做到(它有自己的“干净”范围),但如果可以的话,我会这样做!
  • @Haravikk:感谢您的澄清。 (顺便说一句:如果你可以使用bash(我知道你不能,很遗憾),你可以用export -f导出函数,然后find也会看到它们(当与-exec bash -c结合时。)
  • +1,但您能否详细说明-exec 被多次调用的可能性? POSIX states 表示 “任何两个或多个路径名的集合的大小都应受到限制,以便实用程序的执行不会导致超出系统的 ARG_MAX 限制。” 但也可以解释为可以在点击ARG_MAX 和 ... 时截断/失败
  • @AdrianFrühwirth:您引用的来源还指出“评估主要的路径名应聚合到集合中。”。连同您引用的内容,我将其解释为 将参数分组为集合(复数)的唯一原因 是为了解决 ARG_MAX 限制 - 否则您可以简单地始终使用 单个 集并使其可能中断。 (只有 single 路径名长到导致命令行长度超过 ARG_MAX 时才会遇到麻烦。)
  • @AdrianFrühwirth: gerne;至于使用 glob - 它们实际上不受 ARG_MAX 本身的约束(“[shell] 扩展仅受虚拟内存系统资源的限制” - in-ulm.de/~mascheck/various/argmax) - ARG_MAX 仅在 外部实用程序被调用。
【解决方案3】:

主题是“如何在非 Bash Shell 中迭代空分隔结果”。到目前为止,大多数答案都为find . -print0 提供了特殊的解决方案,实际上是通过一个空分隔字符串列表(例如find . -exec ... 或shell globbing)来规避迭代。

文件 "/proc//environ" 或 "/proc//cmdline" 是很好的 (Linux) 示例,它们确实需要遍历以空字符结尾的字符串列表。正如Adrian FrühwirthFatalError 的答案中已经提到的,唯一适用于仅POSIX 外壳(例如破折号)AFAIK 的解决方案是使用xargs -0(或类似的工具,如parallel -0):

#!/bin/sh
xargs -0 sh -c 'for i; do printf "%s\n" "$i"; done' my_cmd </proc/1/environ

以上示例需要以“root”身份运行。它也适用于包含换行符和其他特殊字符的字符串。

【讨论】:

    【解决方案4】:

    您可以做的一件事是使用xargs -0 选项将参数传递给另一个shell,例如:

    $ find . -print0 | xargs -0 sh -c 'for f in "$@"; do echo "$f"; done'
    

    【讨论】:

    • 可行,但您可以将其简化为 find . -exec sh -c 'for f in "$@"; do echo "$f"; done' {} +
    【解决方案5】:

    1。使用zsh

    最简单的解决方案是使用zsh,它是一个非bash的shell,支持通过read -d ""读取空分隔值(从4.2版开始,2004年发布),是唯一可以存储的主流shell变量中的空值。此外,管道的最后一个组件没有在zsh 的子shell 中运行,因此在那里设置的变量不会丢失。我们可以简单地写:

    #!/usr/bin/env zsh
    find . -print0 |while IFS="" read -r -d "" file; do
      echo "$file"
    done
    

    使用zsh,我们还可以通过使用setopt globdots(使全局匹配隐藏文件)和**(递归到子目录。这适用于 zsh 的所有版本,甚至是那些早于 4.2 的版本:

    #!/usr/bin/env zsh
    setopt globdots
    for file in **/*; do
      echo "$file"
    done
    

    2。使用 POSIX shell 和 od

    2.1 使用管道

    一个通用的、与 POSIX 兼容的迭代空分隔值的解决方案需要以一种不会丢失信息的方式转换输入,并且将空值转换为更易于处理的其他内容。我们可以使用od 转储所有输入字节的八进制值,并使用printf 轻松地将数据转换回来:

    #!/usr/bin/env sh
    
    find . -print0 |od -An -vto1 |xargs printf ' %s' \
                   |sed 's/ 000/@/g' |tr @ '\n' \
                   |while IFS="" read -r file; do
      file=`printf '\134%s' $file`
      file=`printf "$file@"`
      file="${file%@}"
      echo "$file"
    done
    

    2.2 使用变量存储中间结果

    请注意,while 循环将在子 shell 中运行(至少在 zsh 和原始的非公共域 Korn shell 之外的 shell 中),这意味着在该循环中设置的变量不会在其余代码中可见。如果这是不可接受的,while 循环可以从主 shell 运行,它的输入可以存储在一个变量中:

    #!/usr/bin/env sh
    
    VAR=`find . -print0 |od -An -vto1 |xargs printf ' %s' \
                         |sed 's/ 000/@/g' |tr @ '\n'`
    while IFS="" read -r file; do
      file=`printf '\134%s' $file`
      file=`printf "$file@"`
      file="${file%@}"
      echo "$file"
    done <<EOF
    $VAR
    EOF
    

    2.3 使用临时文件存储中间结果

    如果find 命令的输出很长,脚本将无法将输出存储在变量中,并且可能会崩溃。而且most shells use temporary files to implement heredocs,所以与其使用变量,不如显式写入临时文件,避免使用变量存储中间结果的问题。

    #!/usr/bin/env sh
    
    TMPFILE="/tmp/$$_`awk 'BEGIN{srand(); print rand()}'`"
    find . -print0 |od -An -vto1 |xargs printf ' %s' \
                   |sed 's/ 000/@/g' |tr @ '\n' >"$TMPFILE"
    while IFS="" read -r file; do
      file=`printf '\134%s' $file`
      file=`printf "$file@"`
      file="${file%@}"
      echo "$file"
    done <"$TMPFILE"
    rm -f "$TMPFILE"
    

    2.4 使用命名管道

    我们可以使用命名管道来解决上述两个问题:现在读写可以并行完成,我们不需要将中间结果存储在变量中。但是请注意,这在 Cygwin 中可能不起作用。

    #!/usr/bin/env sh
    
    TMPFILE="/tmp/$$_`awk 'BEGIN{srand(); print rand()}'`"
    mknod "$TMPFILE" p
    {
      exec 3>"$TMPFILE"
      find . -print0 |od -An -vto1 |xargs printf ' %s' \
                     |sed 's/ 000/@/g' |tr @ '\n' >&3
    } &
    while IFS="" read -r file; do
      file=`printf '\134%s' $file`
      file=`printf "$file@"`
      file="${file%@}"
      echo "$file"
    done <"$TMPFILE"
    rm -f "$TMPFILE"
    

    3。修改上述解决方案以使用原始 Bourne shell

    上述解决方案应该可以在任何 POSIX shell 中工作,但在原始 Bourne shell 中失败,这是 Solaris 10 和更早版本中的默认 /bin/sh。此 shell 不支持 %-substitution,并且文件名中的尾随换行符需要以另一种方式保留,例如:

    #!/usr/bin/env sh
    
    TMPFILE="/tmp/$$_`awk 'BEGIN{srand(); print rand()}'`"
    mknod "$TMPFILE" p
    {
      exec 3>"$TMPFILE"
      find . -print0 |od -An -vto1 |xargs printf ' %s' \
                     |sed 's/ 000/@/g' |tr @ '\n' >&3
    } &
    while read -r file; do
      trailing_nl=""
      for char in $file; do
        if [ X"$char" = X"012" ]; then
          trailing_nl="${trailing_nl}
    "
        else
          trailing_nl=""
        fi
      done
      file=`printf '\134%s' $file`
      file=`printf "$file"`
      file="$file$trailing_nl"
      echo "$file"
    done <"$TMPFILE"
    rm -f "$TMPFILE"
    

    4。使用 null 以外的分隔符

    正如 cmets 所指出的,Haravikk 的答案并不完全正确。这是他的代码的修改版本,可以处理各种奇怪的情况,例如以~:/\/: 开头的路径和文件名中的尾随换行符。请注意,它仅适用于相对路径名;可以通过在绝对路径名前面加上/./ 来完成类似的技巧,但是需要更改read_path() 来处理它。这个方法的灵感来自Rich’s sh (POSIX shell) tricks

    #!/usr/bin/env sh
    
    read_path() {
        path=
        IFS=
        read -r path || return $?
        read -r path_next || return 0
        if [ X"$path" = X"././" ]; then
            path="./"
            read -r path_next || return 0
            return
        fi
        path="./$path"
        while [ X"$path_next" != X"././" ]; do
            path=`printf '%s\n%s' "$path" "$path_next"`
            read -r path_next || return 0
        done
    }
    
    find ././ |sed 's,^\./\./,&\n,' |while read_path; do
      echo "$path"
    done
    

    【讨论】:

      【解决方案6】:

      Adrian Frühwirth 的回答绝对是最正确和最完整的,但是对于那些对这个问题感兴趣的人,我只想分享我现在最终使用的代码:

      NL=$'\n'
      read_path() {
          path=
          IFS=
          while [ -z "$path" ]; do
              read -r path || return $?
              while [ ! -e "$path" ]; do
                  read -r path_next || { path=; return $?; }
                  [ "${path_next:0:6}" != '~:/\/:' -o ! -e "$find_path_next" ] && path="$path$NL$path_next" || path="$path_next"
              done
          done
      }
      

      当您像这样运行 find 时,这有效:

      find . -exec printf '~:/\/:%s\n' {} \; | while read_path; do echo "$path"; done
      

      由于在结果开头添加的字符串永远不会出现在实际的文件名中(如果有更简单的字符串,请告诉我!)那么在决定是否将结果连接到一个文件时使用它应该是安全的单个字符串。

      我将把它与-print0read -d 支持的测试结合使用,所以我可以尽可能简单地使用它,但上面应该是安全的,或者至少它适用于所有到目前为止我测试过的环境,当我不能使用更漂亮的方法时,它们似乎可以完成这项工作;例如 - 如果我不能使用通配符,因为我需要来自 findls 的更具体的结果

      【讨论】:

      • 您选择的字符串可以出现在路径名中。试试这个:mkdir -p \~:/\\/:./ &amp;&amp; touch \~:/\\/:./abcd &amp;&amp; find \~: -type f。但是,还有另一个字符串确实不能出现在路径名中:././
      • 另一个问题:touch abcd $'abcd\n'。由于abcd 存在,代码不会读取下一行,也无法读取带有尾随换行符的文件。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-13
      • 1970-01-01
      • 2016-11-03
      • 2013-12-21
      • 2016-01-17
      • 2010-09-05
      相关资源
      最近更新 更多