77

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.

3
  • 1
    ls *.txt | head -1 ?
    – Archemar
    Commented Sep 18, 2014 at 9:41
  • 2
    @Archemar: doesn't work with newlines in filenames.
    – Flimm
    Commented Sep 18, 2014 at 9:44
  • Can we get a more lightweight solution that works for the easier case without whitespace?
    – DJG
    Commented Jun 2, 2021 at 15:20

6 Answers 6

52

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 ;-)

39

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"
1
  • 1
    And to get the first $n$ matches, you can use *.txt([1,n])
    – Emre
    Commented Mar 2, 2016 at 21:39
11

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.

5

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.


The above wont work if you're working with files that have newlines in their name. To work with newlines you can use any of these:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (Doesn't work on 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)" (Works with any special character)
4
  • 3
    No, that will fail if the filename has newlines.
    – user232326
    Commented Aug 30, 2018 at 1:48
  • 14
    what kind of crazy world do we live in where filenames contain newlines? Commented Nov 21, 2018 at 18:10
  • 1
    @billynoah I personally think that Linux filesystems should ban newlines in filenames and require that filenames be valid UTF-8, but apparently, the decision-makers in filesystems disagree with me.
    – Flimm
    Commented Jul 23, 2020 at 10:33
  • @EatenbyaGrue Also, don't forget that attackers exploit details like this to find security vulnerabilities. Some systems accept files that could be named by an untrusted user.
    – Flimm
    Commented Dec 19, 2022 at 13:09
3

A simple solution:

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

Or use printf if you prefer.

-1

A use case I often encounter is defining either the top/bottom directory after glob expansion (e.g. a directory full of versioned SDKs or build tools). In this situation I'll usually want to save that directory name to a variable for use in a few places within a shell script.

This command usually does it for me:

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

Disclaimer: Glob expansion isn't going to sort your folders by semver; you've been warned. This is great if you have a Dockerfile that only one version of a directory, but that directories version could vary from image to image 🤷

6
  • Welcome to U&L! That does handle most directory names, but it doesn't handle directory names with a newline. Try creating a directory like this mkdir "$(echo one; echo two)" and see what I mean.
    – Flimm
    Commented Jun 6, 2019 at 19:17
  • What is the advantage compared to the other alternatives, especially the version that uses tail?
    – RalfFriedl
    Commented Jun 6, 2019 at 19:57
  • Standard dirname only takes a single pathname, so you can't rely on it working on multiple pathnames unless you know your particular implementation supports it.
    – Kusalananda
    Commented Jun 6, 2019 at 20:55
  • @Flimm Good point; I think most developers would have bigger problems to deal with if their folder structure has newlines in it... I've never had to deal with that, and don't expect to with any half-decent containers and software I'm using Commented Jun 6, 2019 at 20:57
  • @RalfFriedl Good question; this essentially filters out anything that isn't a valid directory (and won't list/traverse . and ..) Commented Jun 6, 2019 at 20:58

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .