11

How do you get the first file in a directory in bash? First being the what the shell glob finds first.

My search for getting the first file in a directory in bash brought me to an old post with a very specific request. I'd like to document my solution to the general question for posterity and make a place for people to put alternative solutions they'd like to share.

young_souvlaki
  • 657
  • 1
  • 6
  • 15

5 Answers5

27

To get the first file in the current dir you can put the expansion in an array and grab the first element:

files=(*)
echo "${files[0]}"
# OR
echo "$files" # since we are only concerned with the first element

Assuming your current dir contains multiple dirs you can loop through and grab the first file like so:

for dir in *; do
    files=($dir/*)    
    echo "${files[0]}"
done

But be aware that, depending on your shell settings, files=(*) may return an array of one element (that is '*') if there are no files. So you have to check that the file names in the array correspond to files which actually exist (and bear in mind that * is a valid file name).

Stephen Kitt
  • 411,918
  • 54
  • 1,065
  • 1,164
young_souvlaki
  • 657
  • 1
  • 6
  • 15
  • This gives you the first _name_ from the lexicographically sorted list of non-hidden names in the current directory. This may not be a regular file. In comments to the question, you [specifically asked about regular files](https://unix.stackexchange.com/questions/583156/getting-the-first-file-in-a-directory-in-bash#comment1155075_583156), though. – Kusalananda May 21 '22 at 07:51
  • any idea why such a simple scenario is so complicated. I asked chatGPT and gave me `mv "$(ls | head -n 1)" /path/to/target/directory/` which lists 2 files – Andre Leon Rangel Jul 31 '23 at 23:02
2

Almost is an this answer:

shopt -s nullglob
set -- *
printf "%s\n" "$1"
Arkadiusz Drabczyk
  • 25,049
  • 5
  • 53
  • 68
2

In zsh:

  • first non-hidden file in locale collation order: first=(*(N[1]))
  • same, but restricted to non-directory files: first=(*(N^/[1]))
  • same, but also excluding symlinks to directories: first=(*(N^-/[1]))
  • restricting to regular files: first=(*(N.[1]))
  • same but including symlinks to regular files: first=(*(N-.[1]))

Some more notes:

  • those define an array variable as it still needs to be able to store a variable number of elements: 0 (no matching file) or 1 (matching files, among which only the first is selected). To define a $first scalar variable instead (and have it contain the empty string if there's no matching file), you can do (){ first=$1; } *(N[1]) instead. Or to leave the $first scalar variable untouched instead if there's no matching file: (){ (($#)) && first=$1; } *(N[1]).
  • to include hidden files, add the D glob qualifier
  • in some locales (including most of the ones typically used on GNU systems from 2020 like en_US.UTF-8), collation order is not always deterministic as some characters sort the same. See for instance after touch on Ubuntu 20.04. All those files will have the same sorting order, so which one you'll get first will me more or less random.
  • with zsh glob qualifiers, it's also possible to change the order: n makes the filename comparison numerical (so that file2 comes before file10 for instance), and with the o glob qualifier, one can sort based on other criteria than name (such as age, size...).
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
1

To get the name of the regular file that sorts first in a directory, you may use

shopt -s nullglob dotglob
unset -v name

for name in some/path/*; do
    [ -f "$name" ] && break
    unset -v name
done

After this loop, $name would either be the name of the regular file that sorts first, or the variable would be unset if there are no regular files in the directory some/path.

The shell options used here make sure that the loop is not run if the pattern does not match (nullglob), and that we also match hidden names (dotglob).

You'll get the first name since filename globbing patterns are expanded into lexicographically sorted lists.

Kusalananda
  • 320,670
  • 36
  • 633
  • 936
1

All existing answers seem to first create a full listing of the directory, which can take a while with thousands of elements, so I ended up with the simple:

ls -AU | head -1

Where -U means unsorted and thus produces an immediate result.

xeruf
  • 499
  • 4
  • 16
  • This also returns names that may not be regular files. Note that `-U` is a non-standard GNU extension. Using `find . ! -name .` may be better, but you still would only get the first part of the name if it contains newlines since `head` acts on _text_, not filenames. – Kusalananda May 21 '22 at 07:58
  • You can use `-f`, seems to do the same and is standard. Filetypes are not a requirement here, but one could filter using `file` and `test` if needed. As for newlines, one can use `--zero` in ls, `-z` on head and `-print0` on find (or use its `exec` flag). – xeruf May 24 '22 at 22:06