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


12

给定以下文件名:

$ ls -1
file
file name
otherfile

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
#?

但是,有时我可能不想使用每个文件,或者甚至不想严格使用in $PWD,它是其中的find源。它也名义上处理空格:

$ find -type f -name file\*
./file
./file name
./directory/file
./directory/file name

我正在尝试编写脚本的whispace安全版本,它将其输出find并呈现为select

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

但是,这在文件名中使用空格爆炸:

$ 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

通常,我可以通过和解决这个问题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'

有什么解决方案?



1
如果你使用find其以匹配特定的文件名的能力,你可以简单地使用select file in **/file*(在设置shopt -s globstar中)bash4或更高版本。
chepner'7

Answers:


14

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

$ ls -1
file
other file
somefile

然后

$ 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

如果您确实需要处理换行符,并且您的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

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


2
我从未用过的另一个动词+1
roaima

的确,mapfile对我来说也是新的。荣誉
DopeGhoti

while IFS= read版本可在bash v3中使用(这对于使用macOS的我们来说很重要)。
戈登·戴维森

3
find -print0变体+1 ;了把它经过一个已知版本不正确,并说明它仅使用如果知道,他们需要处理换行。如果仅在预期的地方处理意外事件,则根本不会处理意外事件。
Charles Duffy

8

该答案提供了适用于任何类型文件的解决方案。用换行符或空格。
对于最近的bash,古代bash甚至旧的posix外壳都有解决方案。

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

选择

select使用数组很容易:

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

或使用位置参数:

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done

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

重击

我看不到您用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>

或者,如果您喜欢循环:

$ 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>

请注意,以上语法可在任何(合理的)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>

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

$ 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>

强大的

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

关于空格(或换行符)的最简单方法是拒绝对具有空格(或换行符)的输入的处理。在外壳中执行此操作的一种非常简单的方法是,如果任何文件名使用空格扩展,则退出并显示错误。有几种方法可以做到这一点,但是最紧凑的(和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.

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

在bash中,可以使用上述**立即测试子目录。

包含点文件的方法有两种,Posix解决方案是:

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*

如果由于某种原因必须使用find,请使用NUL(0x00)替换定界符。

bash 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>

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[@]}"

适当地

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

我们需要使用-execfind和调用shell 的复合体:

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +

或者,如果需要选择(select是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>

[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

可以使用以下两个命令来构建:

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}

6

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

如上文参数中所述,可以通过为任何简单命令或功能的环境添加参数赋值来临时扩展其环境。

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

这是演示失败和成功方案的常用构造:

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success

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

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success

这是我可以看到的结构select

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

在编写代码的防守,我建议该条款无论是在子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

5
假定IFS=$'\n'安全是没有根据的。文件名完全能够包含换行符。
Charles Duffy

4
坦率地说,我很犹豫地接受这样一个关于人的可能数据集的断言,即使存在时也是如此。我遇到过的最严重的数据丢失事件是,负责清理旧备份的维护脚本试图删除由Python脚本使用带有错误指针取消引用的C模块创建的文件,该模块转储了随机垃圾。 -在名称中包括一个用空格分隔的通配符。
Charles Duffy

2
建立shell脚本来清​​理那些文件的人不会费心引用,因为名称“不可能”不匹配[0-9a-f]{24}。用于支持客户计费的数据备份TB丢失了。
Charles Duffy

4
完全同意@CharlesDuffy。仅当您以交互方式工作并且可以看到自己在做什么时,不处理极端情况才是好的。 select就其本身的设计而言,它是针对脚本解决方案的,因此应始终将其设计为处理边缘情况。
通配符

2
@ilkkachu,当然-你不会永远调用select从外壳那里你在打字的命令运行,但只在一个脚本,在那里你回答提供一个提示由脚本,并在该脚本是根据该输入执行预定义的逻辑(在不知道要操作的文件名的情况下进行构建)。
查尔斯·达菲

4

我可能不在我的管辖范围内,但也许您可以从这样的事情开始,至少它在空白方面没有任何问题:

find -maxdepth 1 -type f -printf '%f\000' | {
    while read -d $'\000'; do
            echo "$REPLY"
            echo
    done
}

如注释中所述,为避免任何潜在的错误假设,请注意上述代码等效于:

   find -maxdepth 1 -type f -printf '%f\0' | {
        while read -d ''; do
                echo "$REPLY"
                echo
        done
    }

read -d是一个聪明的解决方案;谢谢你
DopeGhoti

2
read -d $'\000'正好相同read -d '',但带有误导性质的bash的能力(这意味着,错误地认为它能够字符串中表示文本完全无效)人。运行s1=$'foo\000bar'; s2='foo',然后尝试找到一种方法来区分两个值。(将来的版本可能通过使存储值等于来使用命令替换行为进行规范化foobar,但今天情况并非如此)。
查尔斯·达菲
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.