如何在非Bash Shell中迭代空分隔结果

ecfdbz9o  于 2023-04-07  发布在  Shell
关注(0)|答案(6)|浏览(118)

好了,我有一个脚本来处理find的null分隔输出,我可以很容易地使用bash shell来处理它,如下所示:

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

这是一个很傻的例子,因为它只是将结果转换为新行,但它只是给予你知道我想做什么。这个基本方法工作得很好,避免了由于各种文件系统上的文件可能包含新行而导致的潜在问题。
然而,我需要在非bash shell上做同样的事情,这意味着我失去了对read -d的支持。那么,在不求助于bash(或其他shell)特定功能的情况下,有没有一种方法可以像上面一样处理空分隔的结果?
如果没有,那么保护自己免受结果中新行的最佳方法是什么呢?我在想也许可以使用find-exec选项将文件名中的新行替换为某种转义值,但我不确定查找和替换新行的最佳方法(例如,我不能使用tr)或使用什么替换,这就是为什么空字符是最好的选择。

yptwkmov

yptwkmov1#

参见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

你是对的,这是一个真实的的bummer,这个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
tmb3ates

tmb3ates2#

添加到Adrian Frühwirth's excellent answer
这里是一个严格符合POSIX的解决方案,包括shell代码 * 和 * 实用程序及其使用的选项:

find . -exec sh -c 'for f in "$@"; do printf "%s\n" "$f"; done' - {} +

这避免了find-print0read -d

注意:有一个-很大程度上是假设的-你的shell代码将被调用 * 不止一次 * 的风险,即当有这么多的输入文件名,他们的组合字节计数太大,无法传递给一个单一的sh -c调用-请参阅this article about ARG_MAX的详细信息和blubberdiblub的评论如下。

也就是说,-exec ... +,像xargs一样,注意ARG_MAX的限制,并将太大的参数集分散在 * 多个 * 调用中。

s71maibg

s71maibg3#

主题是“如何在非Bash Shell中迭代空分隔结果”。到目前为止,大多数答案都提供了find . -print0的特殊解决方案,实际上是通过一系列空分隔字符串(例如find . -exec ...或shell globbing)来避免迭代。
文件“/proc//environ”或“/proc//cmdline”是很好的(Linux)示例,它们确实需要迭代一系列以null结尾的字符串。对于仅支持POSIX的shell(例如dash)AFAIK,唯一能正确工作的解决方案是使用xargs -0(或类似的工具,如parallel -0),正如在Adrian FrühwirthFatalError的答案中已经提到的:

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

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

uqcuzwp8

uqcuzwp84#

1.使用zsh

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

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

使用zsh,我们还可以很容易地避免空分隔符的问题(至少在find . -print的情况下),通过使用setopt globdots,它使globs匹配隐藏文件,以及**,它递归到子目录。这基本上适用于所有版本的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中),这意味着在该循环中设置的变量在代码的其余部分中不可见。如果这是不可接受的,则可以从主shell运行while循环,其输入可以存储在变量中:

#!/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不支持%-替换,并且文件名中的尾随换行符需要以另一种方式保留,例如:

#!/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

正如评论中指出的,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
vcirk6k6

vcirk6k65#

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

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

57hvy0tb6#

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支持的测试结合起来使用,这样我就可以尽可能简单地使用它,但是上面的方法应该是安全的,或者至少它可以在我迄今为止测试的所有环境中工作,并且在我不能使用更漂亮的方法时似乎可以完成这项工作;例如,如果我不能使用globbing,因为我需要来自findls的更具体的结果

相关问题