127

When I echo * I get the following output:

file1 file2 file3 ...

What I want is to pick out the first word. How can I proceed?

3
  • 2
    @mattdm Using ls won't work if one of the filenames contains a blank. Commented Feb 25, 2013 at 17:07
  • 3
    How do you define word? If the first file is Sunset on a beach.jpg, should it be Sunset or the whole file name? What about Sea, sex and sun.ogg? Sea, Sea, or the whole file name? Commented Mar 21, 2016 at 12:46
  • @mattdm ls | head -1 gives me random things like a.patch p.py which is not the first word and not even files in alphabetical order.
    – phuclv
    Commented Feb 13, 2017 at 9:10

10 Answers 10

160

You can pipe it through awk and make it echo the first word

echo * | head -n1 | awk '{print $1;}'

or you cut the string up and select the first word:

echo *  | head -n1 | cut -d " " -f1

or you pipe it thorugh sed and have it remove everything but the first word

echo * | head -n1 | sed -e 's/\s.*$//'

Added the | head -n1 to satisfy nitpickers. In case your string contains newlines | head -n1 will select the first line first before the important commands select the first word from the string passed to it.

7
  • 2
    The would return the first word of every line, not the first word. Commented Feb 24, 2013 at 20:19
  • 3
    i wonder how many lines echo * generates
    – Bananguin
    Commented Feb 25, 2013 at 14:10
  • 3
    That depends how many files have newline characters in their name or depending on the environment or how bash was compiled or whether some file called -e or -ee... appears in the list, how many time \n appears in a file name. If there's a file called -n, it might not even return any line at all... Commented Feb 25, 2013 at 15:05
  • Well, we're still talking bash and 1. usually bash will not pass a string with newlines as one string with new lines 2. it's very hard and unusual (impossible?) to have a newline in a filename 3. a \n in a filename will show up as a \n 4. 3 holds for filenames starting with - 5. even when called with -n or -e echo will open stdout and close it when it's done so of course it will return a line, and at least one string, for that matter 6. i edited my advice to at least take care of the multiline problem
    – Bananguin
    Commented Feb 25, 2013 at 16:34
  • 2
    All 5 points are false. Try in an empty dir: touch '$a\nb' 'a\nb'; env BASHOPTS=xpg_echo bash -c 'echo * | wc -l' (xpg_echo is enabled wherever bash is required to be Unix conformant). And in another empty directory: touch ./-n; bash -c 'echo * | wc -l'. A line is a sequence of characters terminated by a newline character. If echo doesn't output a newline character, it doesn't output any line. Behavior of text utilities like cut, awk or sed is unspecified if the input has extra characters after the last newline character and behavior varies across implementations. Commented Feb 25, 2013 at 17:52
58

Assuming a posixy shell (/bin/sh or /bin/bash can do this)

all=$(echo *)
first=${all%% *}

The construct ${all%% *} is an example of substring removal. The %% means delete the longest match of * (a space followed by anything) from the right-hand end of the variable all. You can read more about string manipulation here.

This solution assumes that the separator is a space. If you're doing this with file names then any with spaces will break it.

3
  • Nice one. But I liked the simple and more non-hacky other version with head and cut
    – Anwar
    Commented Jul 22, 2017 at 18:24
  • 13
    IMO, head, cut & sed are less simple. Why spawn another tool when builtin parameter substitution does the job efficiently. This answer is most efficient and most portable way to pick off the first word of a list of space-separated words (which is all the OP asked for).
    – Juan
    Commented Dec 6, 2018 at 14:00
  • @Juan , first how is builtin substitution efficient - not intended as rhetorical, but who knows what the actual complexity is. Second, unlikely, but if echo * were a stream or some # of lines approaching infinity, wouldn't this be a particularly fun way to OOM your box? I don't if you can OOM of an assignment like this, as in does any shell provide constraints (like buffer + block writes to memory), but my guess is that there arent and that you definitely can write to all available memory on a casual, nothing to see here, assignment. Commented Apr 3, 2021 at 23:08
13

Assuming that you really want the first filename and not the first word, here's a way that doesn't break on whitespace:

shopt -s nullglob
files=(*)
printf '%s\n' "${files[0]}"
12
  • 2
    First word would be easy, too: files=($(echo *)) Commented Feb 24, 2013 at 12:54
  • 2
    It would break on "-n", "-e", "-ne", "-Enenene"..., and depending on how bash was compiled or the environment, possibly on backslash characters, though. Commented Feb 24, 2013 at 20:18
  • 2
    @HaukeLaging, you'd need to disable globbing. Like: text=$(echo *); set -f; files=($text), otherwise more wildcards could be expanded. Commented Feb 24, 2013 at 20:23
  • 3
    and files=$(echo *); echo ${files%% *}
    – vdegenne
    Commented Feb 27, 2013 at 19:41
  • 1
    @memnoch_proxy It will break on filenames containing whitespace, use it with caution.
    – Chris Down
    Commented May 22, 2013 at 2:22
11

You can use the positional parameters

set -- *
echo "$1"
2
  • 4
    Be aware that this will destroy any other arguments to your script, unless run in an auxiliary scope. It will also expand to * if there are no files in the directory.
    – Chris Down
    Commented Feb 24, 2013 at 12:47
  • shopt -s nullglob would handle that Commented Feb 24, 2013 at 21:13
5

This works:

echo * | grep -o "^\w*\b"

Creds to https://unix.stackexchange.com/a/57879/3920

2
  • 1
    Unless the first filename is something like -n , -e, -ne, -en, etc. On some platforms. Commented Apr 4, 2022 at 8:20
  • I like this one, which can catch all rows with the first word. I am using NAME="Red Hat Enterprise Linux Server" VERSION="7.9 (Maipo)"
    – dave
    Commented Mar 28, 2023 at 19:11
3

Check one of the following alternatives:

$ FILE=($(echo *))
$ FILE=$(echo * | grep -o "^\S*")
$ FILE=$(echo * | grep -o "[^ ]*")
$ FILE=$(find . -type f -print -quit)

Then you can print it via echo $FILE.

See also: grep the only first word from output?

1

Getting the whole first file name:

shopt -s nullglob
printf '%s\000' * | grep -z -m 1 '^..*$'
printf '%s\000' * | ( IFS="" read -r -d "" var; printf '%s\n' "$var" )
0

Another approach is to list all the file names as an array, and then index the array for the first element:

STRARRAY=($(echo *))
FIRST=${STRARRAY[0]}
1
  • 1
    Already in Chris Down's answer and equivalent to kenorb's but you actually need only $STRARRAY because bash automatically selects [0] Commented Apr 4, 2022 at 1:19
0

This is like @Bananguin's answer but without using head

echo * | awk '{ print $1; exit }'
echo * | sed -e 's/\s.*$//;1q' # don't use this for large inputs. See below

Moreover, I was looking for the most performant way and this is my results:

cut

The disadvantages of this code using cut are that it only allows a single delimiter and it will need to pass through head -n 1 if the input has several lines.

$ time seq 100000000 | paste -sd " " | cut -d " " -f1
1

real    0m1.262s
user    0m2.094s
sys     0m0.584s

awk

With awk, I can separate fields better than with cut, even when there are multiple spaces or tabs.

$ time seq 100000000 | paste -sd " " | awk '{ print $1; exit }'
1

real    0m1.671s
user    0m2.781s
sys     0m0.910s

sed

The top answer for a large input will comsume all the RAM.

# don't run this
$ # time seq 100000000 | paste -sd " " | sed -e 's/\s.*//;1q'

The following is a version with sed and head that is fast. (I couldn't find a more efficient solution than this one. Any help to improve it would be greatly appreciated)

$ time seq 100000000 | paste -sd " " | sed -e 's/^\([^ \t\n]*\)/\1\n/;1q' | head -n 1
1

real    0m1.507s
user    0m1.686s
sys     0m0.869s

C programming language

The issue with awk, cut, and sed is that they read the entire line even after encountering the first space character.

#include <stdio.h>

#define BUFFER_SIZE 256

int main()
{
        char buffer[BUFFER_SIZE];
        int found = 0;

        while (!found && fgets(buffer, 256, stdin)) {
                for (char *c = buffer; c < buffer + BUFFER_SIZE && *c; c++) {
                        putc(*c, stdout);
                        if (*c == ' ' || *c == '\n' || *c == '\t') {
                                found = 1;
                                break;
                        }
                }
        }
        putc('\n', stdout);
        return 0;
}

And this code have constant execution time, no matter how long is the line.

$ gcc main.c -o main
$ time seq 10000000 | paste -sd " " | ./main
1

real    0m0.005s
user    0m0.002s
sys     0m0.009s

Conclusion (TL;DR)

Use the awk '{ print $1; exit }. If the input is very large, use the C version for achieve constant time.

0

In zsh, you can use the [n] or [n,m] glob qualifiers to only select the nth or nth to mth files from the result of a glob expansion.

Examples (here using print -rC1 to print the list raw on 1 Column instead of the unreliable and non-portable echo; and using the N qualifier (nullglob) so as to avoid the error if there's no match):

print -rC1 -- *(N[1])     # first (here in default alphabetical order)
print -rC1 -- *(N[-1])    # last
print -rC1 -- *(N[1,3])   # first 3
print -rC1 -- *(N[-3,-1]) # last 3

You often combine it with the n (numeric sort when by name), o or O glob qualifiers to change the sorting criteria (which defaults to by name).

print -rC1 -- *(NOL[1]) # largest file oL sorts by Length
print -rC1 -- *(Nom[1]) # newest file

In bash, you could use a helper function.

print_range() {
  local first="$1" last="$2"; shift 2
  set -- "${@:first:last-first+1}"
  [ "$#" -eq 0 ] || printf '%s\n' "$@"
}

Then:

shopt -s nullglob
print_range  1  1 * # first
print_range -1 -1 * # last
print_range  1  3 * # first 3
print_range -3 -1 * # last 3

In bash 5.3 or newer, you can use the GLOBSORT new variable to specify how to sort the results of pathname expansion (name, size, blocks, mtime, atime, ctime, none) in ascending or descending order. Though beware that it's done after symlink resolution (as if using the - qualifier in zsh) with no way to change it, and like that global nullglob option affects all globs with no way to apply it on a per glob basis.

You must log in to answer this question.

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