34

Shells like Bash and Zsh expand wildcard into arguments, as many arguments as match the pattern:

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

But what if I only want the first match to be returned, not all the matches?

$ echo *.txt
1.txt

I don't mind shell-specific solutions, but I would like a solution that works with whitespace in filenames.

  • ls *.txt | head -1 ? – Archemar Sep 18 '14 at 9:41
  • 1
    @Archemar: doesn't work with newlines in filenames. – Flimm Sep 18 '14 at 9:44
24

One robust way in bash is to expand into an array, and output the first element only:

pattern="*.txt"
files=( $pattern )
echo "${files[0]}"  # printf is safer!

(You can even just echo $files, a missing index is treated as [0].)

This safely handles space/tab/newline and other metacharacters when expanding the filenames. Note that locale settings in effect can alter what "first" is.

You can also do this interactively with a bash completion function:

_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

This binds the _echo function to complete arguments to the echo command (overriding normal completion). An extra "*" is appended in the code above, you can just hit tab on a partial filename and hopefully the right thing will happen.

The code is slightly convoluted, rather than set or assume nullglob (shopt -s nullglob) we check compgen -G can expand the glob to some matches, then we expand safely into an array, and finally set COMPREPLY so that quoting is robust.

You can partly do this (programmatically expand a glob) with bash's compgen -G, but it's not robust as it outputs unquoted to stdout.

As usual, completion is rather fraught, this breaks completion of other things, including environment variables (see the _bash_def_completion() function here for the details of emulating the default behaviour).

You could also just use compgen outside of a completion function:

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

One point to note is that "~" is not a glob, it's handled by bash in a separate stage of expansion, as are $variables and other expansions. compgen -G just does filename globbing, but compgen -W gives you all of bash's default expansion, though possibly too many expansions (including `` and $()). Unlike -G, the -W is safely quoted (I can't explain the disparity). Since the purpose of -W is that it expands tokens, this means it will expand "a" to "a" even if no such file exists, so it's perhaps not ideal.

This is easier to understand, but may have unwanted side-effects:

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local files=( $(compgen -W "$cur") ) 
    printf -v COMPREPLY %q "${files[0]}"  
}

Then:

touch $'curious \n filename'

echo curious*tab

Note the use of printf %q to safely quote the values.

One final option is to use 0-delimited output with GNU utilities (see the 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 )

This option gives you a little more control over the sorting order (the order when expanding a glob will be subject to your locale/LC_COLLATE and may or may not fold case), but is otherwise a rather large hammer for such a small problem ;-)

17

In zsh, use the [1] glob qualifier. Note that even though this special case returns at most one match, it's still a list, and globs are not expanded in contexts that expect a single word such as assignments (array assignments aside).

echo *.txt([1])

In ksh or bash, you can stuff the whole list of matches in an array and use the first element.

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

In any shell, you can set the positional parameters and use the first one.

set -- *.txt
echo "$1"

This clobbers the positional parameters. If you don't want that, you can use a subshell.

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

You can also use a function, which has its own set of positional parameters.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"
  • And to get the first $n$ matches, you can use *.txt([1,n]) – Emre Mar 2 '16 at 21:39
6

Try:

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

A note that filename expansion is sorted according to the collating sequence in effect in the current locale.

3

A simple solution:

sh -c 'echo "$1"' sh *.txt

Or use printf if you prefer.

1

I just stumbled across this old question while wondering the same thing. I ended up with this:

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

You can, of course, replace head with tail and -n1 with any other number.

  • 3
    No, that will fail if the filename has newlines. – Isaac Aug 30 '18 at 1:48
  • 4
    what kind of crazy world do we live in where filenames contain newlines? – billynoah Nov 21 '18 at 18:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.