bash:空格安全的程序使用 find into select

Dop*_*oti 12 bash text-processing whitespace select

鉴于这些文件名:

$ ls -1
file
file name
otherfile
Run Code Online (Sandbox Code Playgroud)

bash 本身对于嵌入的空白完全没问题:

$ for file in *; do echo "$file"; done
file
file name
otherfile
$ select file in *; do echo "$file"; done
1) file
2) file name
3) otherfile
#?
Run Code Online (Sandbox Code Playgroud)

但是,有时我可能不想处理每个文件,甚至不希望使用严格的 in $PWD,这是find进来的地方。名义上也处理空格:

$ find -type f -name file\*
./file
./file name
./directory/file
./directory/file name
Run Code Online (Sandbox Code Playgroud)

我正在尝试编造这个scriptlet的 whispace-safe 版本,它将获取输出find并将其呈现到select

$ select file in $(find -type f -name file); do echo $file; break; done
1) ./file
2) ./directory/file
Run Code Online (Sandbox Code Playgroud)

但是,这会因文件名中的空格而爆炸:

$ select file in $(find -type f -name file\*); do echo $file; break; done
1) ./file        3) name          5) ./directory/file
2) ./file        4) ./directory/file  6) name
Run Code Online (Sandbox Code Playgroud)

通常,我会通过使用IFS. 然而:

$ IFS=$'\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'
$ IFS='\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'
Run Code Online (Sandbox Code Playgroud)

解决这个问题的方法是什么?

ste*_*ver 15

如果您只需要处理空格和制表符(而不是嵌入的换行符),那么您可以使用mapfile(或其同义词,readarray)读入数组,例如给定

$ ls -1
file
other file
somefile
Run Code Online (Sandbox Code Playgroud)

然后

$ IFS= mapfile -t files < <(find . -type f)
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
#? 3
./other file
Run Code Online (Sandbox Code Playgroud)

如果您确实需要处理换行符,并且您的bash版本提供了空分隔的mapfile1,那么您可以将其修改为IFS= mapfile -t -d '' files < <(find . -type f -print0). 否则,find使用read循环从以空分隔的输出组装等效数组:

$ touch $'filename\nwith\nnewlines'
$ 
$ files=()
$ while IFS= read -r -d '' f; do files+=("$f"); done < <(find . -type f -print0)
$ 
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
4) ./filename
with
newlines
#? 4
./filename?with?newlines
Run Code Online (Sandbox Code Playgroud)

1-d选项被添加到mapfilebash版本4.4 IIRC

  • +1 表示 `find -print0` 变体;**抱怨** 将它放在*已知不正确的版本之后*,并且仅在*知道*他们需要处理换行符的情况下才对其进行描述。如果一个人只在预期的地方处理意外,一个人永远不会处理意外。 (3认同)
  • +1 表示我以前没有使用过的另一个动词 (2认同)

ImH*_*ere 8

这个答案对任何类型的文件都有解决方案。带有换行符或空格。
最近的 bash 以及古老的 bash 甚至旧的 posix shell 都有解决方案。

此答案[1]下面列出的树用于测试。

选择

select使用数组很容易开始工作:

$ dir='deep/inside/a/dir'
$ arr=( "$dir"/* )
$ select var in "${arr[@]}"; do echo "$var"; break; done
Run Code Online (Sandbox Code Playgroud)

或者使用位置参数:

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done
Run Code Online (Sandbox Code Playgroud)

因此,唯一真正的问题是在数组或位置参数中获取“文件列表”(正确分隔)。继续阅读。

猛击

我没有看到你用 bash 报告的问题。Bash 能够在给定目录中进行搜索:

$ dir='deep/inside/a/dir'
$ printf '<%s>\n' "$dir"/*
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>
Run Code Online (Sandbox Code Playgroud)

或者,如果您喜欢循环:

$ set -- "$dir"/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>
Run Code Online (Sandbox Code Playgroud)

请注意,上述语法适用于任何(合理的)shell(至少不是 csh)。

上述语法的唯一限制是下降到其他目录。
但是 bash 可以做到这一点:

$ shopt -s globstar
$ set -- "$dir"/**/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>
Run Code Online (Sandbox Code Playgroud)

要仅选择某些文件(例如以文件结尾的文件),只需替换 *:

$ set -- "$dir"/**/*file
$ printf '<%s>\n' "$@"
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/zz last file>
Run Code Online (Sandbox Code Playgroud)

强壮的

当您在标题中放置“空间安全”时,我将假设您的意思是“健壮”。

对空格(或换行符)保持鲁棒性的最简单方法是拒绝处理具有空格(或换行符)的输入。在 shell 中执行此操作的一种非常简单的方法是,如果任何文件名以空格扩展,则退出并显示错误。有几种方法可以做到这一点,但最紧凑(和 posix)(但仅限于一个目录内容,包括 suddirectories 名称和避免点文件)是:

$ set -- "$dir"/file*                            # read the directory
$ a="$(printf '%s' "$@" x)"                      # make it a long string
$ [ "$a" = "${a%% *}" ] || echo "exit on space"  # if $a has an space.
$ nl='
'                    # define a new line in the usual posix way.  

$ [ "$a" = "${a%%"$nl"*}" ] || echo "exit on newline"  # if $a has a newline.
Run Code Online (Sandbox Code Playgroud)

如果使用的解决方案在任何这些项目中都是可靠的,则删除测试。

在 bash 中,可以使用上面解释的 ** 立即测试子目录。

有几种方法可以包含点文件,Posix 解决方案是:

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*
Run Code Online (Sandbox Code Playgroud)

如果出于某种原因必须使用 find,请将分隔符替换为 NUL (0x00)。

重击 4.4+

$ readarray -t -d '' arr < <(find "$dir" -type f -name file\* -print0)
$ printf '<%s>\n' "${arr[@]}"
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/file>
Run Code Online (Sandbox Code Playgroud)

bash 2.05+

i=1  # lets start on 1 so it works also in zsh.
while IFS='' read -d '' val; do 
    arr[i++]="$val";
done < <(find "$dir" -type f -name \*file -print0)
printf '<%s>\n' "${arr[@]}"
Run Code Online (Sandbox Code Playgroud)

POSIXLY

要创建一个有效的 POSIX 解决方案,其中 find 没有 NUL 分隔符并且没有-d(也没有-a)用于读取,我们需要一种完全不同的方法。

我们需要使用-exec来自 find的复杂调用 shell:

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +
Run Code Online (Sandbox Code Playgroud)

或者,如果需要的是选择(选择是 bash 的一部分,而不是 sh):

$ find "$dir" -type f -exec bash -c '
      select f; do echo "<$f>"; break; done ' bash {} +

1) deep/inside/a/dir/file name
2) deep/inside/a/dir/zz last file
3) deep/inside/a/dir/file with a
newline
4) deep/inside/a/dir/directory/file name
5) deep/inside/a/dir/directory/zz last file
6) deep/inside/a/dir/directory/file with a
newline
7) deep/inside/a/dir/directory/file
8) deep/inside/a/dir/file
#? 3
<deep/inside/a/dir/file with a
newline>
Run Code Online (Sandbox Code Playgroud)

[1]这棵树(\012 是换行符):

$ tree
.
??? deep
    ??? inside
        ??? a
            ??? dir
                ??? directory
                ?   ??? file
                ?   ??? file name
                ?   ??? file with a \012newline
                ??? file
                ??? file name
                ??? otherfile
                ??? with a\012newline
                ??? zz last file
Run Code Online (Sandbox Code Playgroud)

可以用这两个命令构建:

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}
Run Code Online (Sandbox Code Playgroud)


roa*_*ima 6

您不能在循环结构前面设置变量,但可以在条件前面设置它。这是手册页中的部分:

任何简单命令或函数的环境都可以通过给它加上参数分配的前缀来临时增加,如上面参数中所述。

(循环不是简单的命令。)

这是一个常用的构造,用于演示失败和成功场景:

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success
Run Code Online (Sandbox Code Playgroud)

不幸的是,我看不到一种方法可以将更改嵌入IFSselect构造中,同时使其影响关联的$(...). 但是,没有什么可以防止IFS在循环之外设置:

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success
Run Code Online (Sandbox Code Playgroud)

这是我可以看到的这种构造select

IFS=$'\n'; select file in $(find -type f -name 'file*'); do echo "$file"; break; done
Run Code Online (Sandbox Code Playgroud)

在编写代码的防守,我建议该条款无论是在子shell中运行,或者IFSSHELLOPTS保存,各地块的恢复:

OIFS="$IFS" IFS=$'\n'                     # Split on newline only
OSHELLOPTS="$SHELLOPTS"; set -o noglob    # Wildcards must not expand twice

select file in $(find -type f -name 'file*'); do echo $file; break; done

IFS="$OIFS"
[[ "$OSHELLOPTS" !~ noglob ]] && set +o noglob
Run Code Online (Sandbox Code Playgroud)

  • 假设 `IFS=$'\n'` 是安全的是没有根据的。文件名完全能够包含换行文字。 (5认同)
  • 坦率地说,即使存在,我也不愿意接受这种关于一个人可能的数据集的表面价值的断言。我遇到过的最严重的数据丢失事件是,负责清理旧备份的维护脚本试图删除由 Python 脚本创建的文件,该文件使用 C 模块和一个坏指针取消引用,该模块转储了随机垃圾-- 包括一个以空格分隔的通配符 -- 到名称中。 (4认同)
  • 完全同意@CharlesDuffy。只有当您以交互方式工作并且可以*看到*您在做什么时,才可以不处理边缘情况。`select` 的设计本身就是针对 * 脚本化 * 解决方案的,因此它应该始终被设计为处理边缘情况。 (4认同)
  • 构建 shell 脚本清理这些文件的人没有费心引用,因为名称“不可能”无法匹配 `[0-9a-f]{24}`。用于支持客户计费的 TB 数据备份丢失。 (2认同)
  • @ilkkachu,当然——你永远不会从你输入命令运行的 shell 中调用 `select`,而只能在脚本中调用,在那里你回答*由该脚本*提供的提示,以及该脚本基于该输入执行预定义逻辑(在不知道正在操作的文件名的情况下构建)的位置。 (2认同)