像 Bash 和 Zsh 这样的 shell 将通配符扩展为参数,参数数量与模式匹配:
$ echo *.txt
1.txt 2.txt 3.txt
但是如果我只想返回第一个匹配项而不是所有匹配项怎么办?
$ echo *.txt
1.txt
我不介意特定于 shell 的解决方案,但我想要一个可以处理文件名中空格的解决方案。
答案1
bash 中的一个可靠方法是扩展为数组,并仅输出第一个元素:
pattern="*.txt"
files=( $pattern )
echo "${files[0]}" # printf is safer!
(您甚至可以将echo $files
缺失的索引视为 [0]。)
扩展文件名时,这可以安全地处理空格/制表符/换行符和其他元字符。请注意,有效的区域设置可以改变“first”的含义。
您还可以使用 bash 交互地执行此操作完成功能:
_echo() {
local cur=${COMP_WORDS[COMP_CWORD]} # string to expand
if compgen -G "$cur*" > /dev/null; then
local files=( ${cur:+$cur*} ) # don't expand empty input as *
[ ${#files} -ge 1 ] && COMPREPLY=( "${files[0]}" )
fi
}
complete -o bashdefault -F _echo echo
这将_echo
函数绑定到echo
命令的完整参数(覆盖正常完成)。上面的代码中附加了一个额外的“*”,您只需按部分文件名上的选项卡即可,希望会发生正确的事情。
代码是轻微地令人费解的是,我们检查可以将 glob 扩展为某些匹配项,而不是设置或假设nullglob
( ),然后安全地扩展为数组,最后设置 COMPREPLY 以便引用可靠。shopt -s nullglob
compgen -G
你可以部分地使用 bash 执行此操作(以编程方式扩展全局)compgen -G
,但它并不健壮,因为它输出不带引号的到标准输出。
像往常一样,完成是相当令人担忧的,这会破坏其他事情的完成,包括环境变量(请参阅_bash_def_completion()
函数这里有关模拟默认行为的详细信息)。
您也可以compgen
在完成函数之外使用:
files=( $(compgen -W "$pattern") )
需要注意的一点是“~”不是一个 glob,它由 bash 在单独的扩展阶段处理,$variables 和其他扩展也是如此。compgen -G
只是进行文件名通配,但compgen -W
为您提供了 bash 的所有默认扩展,尽管可能有太多扩展(包括``
和 $()
)。不像-G
-W
是安全引用(我无法解释这种差异)。由于它的目的-W
是扩展标记,这意味着即使不存在这样的文件,它也会将“a”扩展为“a”,所以它可能并不理想。
这更容易理解,但可能会产生不需要的副作用:
_echo() {
local cur=${COMP_WORDS[COMP_CWORD]}
local files=( $(compgen -W "$cur") )
printf -v COMPREPLY %q "${files[0]}"
}
然后:
touch $'curious \n filename'
echo curious*
tab
请注意使用 来printf %q
安全地引用这些值。
最后一个选项是在 GNU 实用程序中使用 0 分隔的输出(请参阅bash 常见问题解答):
pattern="*.txt"
while IFS= read -r -d $'\0' filename; do
printf '%q' "$filename";
break;
done < <(find . -maxdepth 1 -name "$pattern" -printf "%f\0" | sort -z )
此选项使您可以更好地控制排序顺序(扩展全局时的顺序将取决于您的语言环境/LC_COLLATE
并且可能会或可能不会折叠大小写),但对于这样一个小问题来说,这是一个相当大的锤子;-)
答案2
在 zsh 中,使用[1]
全局限定符。请注意,即使这种特殊情况最多返回一个匹配项,它仍然是一个列表,并且在需要单个单词(例如赋值)的上下文中不会扩展全局变量(除了数组赋值)。
echo *.txt([1])
在 ksh 或 bash 中,您可以将整个匹配列表填充到一个数组中并使用第一个元素。
tmp=(*.txt)
echo "${tmp[0]}"
在任何 shell 中,您都可以设置位置参数并使用第一个参数。
set -- *.txt
echo "$1"
这会破坏位置参数。如果您不想这样做,可以使用子 shell。
echo "$(set -- *.txt; echo "$1")"
您还可以使用一个函数,它有自己的一组位置参数。
set_to_first () {
eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"
答案3
尝试:
for i in *.txt; do printf '%s\n' "$i"; break; done
1.txt
请注意,文件名扩展是根据当前语言环境中有效的整理顺序进行排序的。
答案4
一个简单的解决方案:
sh -c 'echo "$1"' sh *.txt
printf
或者如果您愿意也可以使用。