给定这些文件名:
$ 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
我正在尝试编写一个whspace安全版本这scriptlet 它将获取输出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
如果您只需要处理空格和制表符(而不是嵌入换行符),那么您可以使用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
版本提供了空分隔的mapfile
1,那么您可以将其修改为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
选项是在4.4 iirc 版本mapfile
中添加的bash
答案2
这个答案有解决方案任何文件类型。带换行符或空格。
对于最近的 bash 以及古老的 bash 甚至旧的 posix shell 都有解决方案。
下面这个答案[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>
强壮的
当您放置“空格-安全的“在标题中,我假设您的意思是”强壮的”。
对空格(或换行符)保持鲁棒性的最简单方法是拒绝处理具有空格(或换行符)的输入。在 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.
如果所使用的解决方案在这些项目中的任何一个方面都可靠,则删除该测试。
在 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[@]}"
POSIXLY
为了制定一个有效的 POSIX 解决方案,其中 find 没有 NUL 分隔符并且没有-d
(nor -a
) 可供读取,我们需要一种完全不同的方法。
我们需要使用-exec
find 的复合体来调用 shell:
find "$dir" -type f -exec sh -c '
for f do
echo "<$f>"
done
' sh {} +
或者,如果需要的是选择(选择是 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}
答案3
您不能在循环结构前面设置变量,但可以在条件前面设置它。这是手册页中的片段:
任何环境简单的命令或功能可以通过在其前面加上参数分配来暂时增强,如上面的参数中所述。
(循环不是简单的命令.)
以下是演示失败和成功场景的常用构造:
IFS=$'\n' while read -r x; do ...; done </tmp/file # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file # Success
不幸的是,我看不到一种方法可以将更改嵌入IFS
到select
构造中,同时使其影响关联的处理$(...)
。然而,没有什么可以阻止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 中运行,要么在块周围保存和恢复IFS
:SHELLOPTS
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
答案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
}