如何从通配符扩展中获得第一个匹配项?

如何从通配符扩展中获得第一个匹配项?

像 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 nullglobcompgen -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或者如果您愿意也可以使用。

相关内容