如何从通配符扩展中获得第一场比赛?


38

诸如Bash和Zsh之类的Shell会将通配符扩展为参数,与模式匹配的参数一样多:

$ echo *.txt
1.txt 2.txt 3.txt

但是,如果我只希望返回第一个比赛而不是所有比赛,该怎么办?

$ echo *.txt
1.txt

我不介意特定于shell的解决方案,但是我想要一个可以在文件名中使用空格的解决方案。


ls * .txt | 头-1?
Archemar 2014年

1
@Archemar:不适用于文件名中的换行符。
Flimm

Answers:


25

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命令的完成参数(覆盖正常完成)。上面的代码中附加了一个额外的“ *”,您可以单击部分文件名上的tab键,希望会发生正确的事情。

代码有些复杂,而不是设置或假设nullglobshopt -s nullglob),我们检查compgen -G可以将glob扩展到某些匹配项,然后安全地扩展到数组中,最后设置COMPREPLY以使引用更可靠。

您可以使用bash的一部分执行此操作(以编程方式扩展glob)compgen -G,但是它不健壮,因为它会将未引用的内容输出到stdout。

像往常一样,完成工作充满烦恼,这破坏了其他事情的完成,包括环境变量(有关仿真默认行为的详细信息,请参见此处_bash_def_completion()函数)。

您也可以在compgen完成功能之外使用:

files=( $(compgen -W "$pattern") )

需要注意的一点是“〜”不是一个全局变量,它是由bash在扩展的单独阶段处理的,$变量和其他扩展也是如此。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 FAQ):

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并且可能会(也可能不会)折叠大小写),但对于这么小的问题,它还是一个很大的锤子;-)


20

在zsh中,使用[1] glob限定符。请注意,即使这种特殊情况最多返回一个匹配项,它仍然是一个列表,并且在期望使用单个单词(例如赋值(不包括数组赋值))的上下文中,不会扩展glob。

echo *.txt([1])

在ksh或bash中,您可以将整个匹配项列表填充到数组中并使用第一个元素。

tmp=(*.txt)
echo "${tmp[0]}"

在任何外壳程序中,您都可以设置位置参数并使用第一个。

set -- *.txt
echo "$1"

这使位置参数变得混乱。如果您不想这样做,则可以使用子外壳。

echo "$(set -- *.txt; echo "$1")"

您还可以使用一个函数,该函数具有自己的一组位置参数。

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"

1
要获得前$ n $场比赛,您可以使用*.txt([1,n])
Emre

6

尝试:

for i in *.txt; do printf '%s\n' "$i"; break; done
1.txt

请注意,文件名扩展是根据当前语言环境中有效的整理顺序进行排序的。



1

我在想知道同一件事的同时,偶然发现了这个老问题。我结束了这个:

echo $(ls *.txt | head -n1)

当然,您可以headtail和替换-n1为任何其他数字。


如果您正在使用名称中包含换行符的文件,则上述方法将无效。要使用换行符,可以使用以下任意一种:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (不适用于BSD)
  • ls -b *.txt | head -n1 | sed -e 's/\\n/\'$'\n/g'
  • ls -b *.txt | head -n1 | perl -pe 's/\\n/\n/g'
  • echo -e "$(ls -b *.txt | head -n1)" (可以使用任何特殊字符)

3
不,如果文件名包含换行符,那将失败。
以撒

7
文件名包含换行符,我们生活在什么样的疯狂世界中?
billynoah

-1

我经常遇到的用例是在glob扩展后定义top / bottom目录(例如,充满版本SDK或构建工具的目录)。在这种情况下,我通常要将该目录名保存到一个变量中,以便在Shell脚本中的几个地方使用。

这个命令通常为我做:

export SDK_DIR=$(dirname /path/to/versioned/sdks/*/. | tail -n1)

免责声明:Glob扩展不会按字母顺序对文件夹进行排序;您已被警告。如果您Dockerfile只有一个版本的目录,但是该目录的版本可能因映像而异,那么这很好。


欢迎来到U&L!确实可以处理大多数目录名称,但是不能使用换行符处理目录名称。尝试创建这样的目录mkdir "$(echo one; echo two)",看看我的意思。
Flimm

与其他替代产品(尤其是所使用的版本)相比有什么优势tail
RalfFriedl

Standard dirname仅采用一个路径名,因此除非您知道特定的实现支持它,否则您不能依靠它在多个路径名上工作。
库萨兰达

@Flimm好点;我认为,如果开发人员的文件夹结构中包含换行符,则大多数开发人员将面临更大的问题……我从没有处理过,也不要指望我使用的任何像样的容器和软件
安德鲁·奥德里

@RalfFriedl好问题;这实际上会过滤掉不是有效目录的任何内容(并且不会列出/遍历。和..)
Andrew Odri
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.