What does $(ls *.txt) do?

In a unix terminal, if I do:

egrep "Stuff" $(ls *.txt)

The result is obvious. But what does

$(ls *.txt) do on its own? I see I can replicate the effect by

egrep "Stuff" *.txt

So what is $(ls *.txt)?


"what does $(ls *.txt) do"

Short answer

$(ls *.txt) gathers a list of file names and then mangles them. Do not use this.

Longer answer

  • ls is intended for human-readable output. As one of ls's maintainers wrote in response to why ls would not offer a --null option:

    If we were to do this then this is the interface we would use. However ls is really a tool for direct consumption by a human, and in that case further processing is less useful. For futher processing, find(1) is more suited. [emphasis added]

    In other words, use of ls for anything other than display to humans is just not supported. ls maintainers, for example, recently changed the default output format to something they though more human-friendly without warning. So, follow their advice: if you are going to do further processing of file names, use find instead of ls.

  • $(...) is command substitution. One consequence of using command substitution is that trailing newline characters are removed. If the last file name in the list happens to contain trailing newlines, they will be removed.

  • Since $(...) is not double-quoted, the text it produces will be subject to:

    • Word splitting

    • Pathname expansion

    The result of both of these is further mangling of the file names.

  • not to mention the issues with file names starting with - because of the missing -- (for both ls and egrep as Ubuntu's egrep being the GNU implementation accepts options even after non-option arguments unless POSIXLY_CORRECT is in the environment).

  • also, if any of those txt files were of type directory, ls would list their content instead of themselves.

  • and if there's no txt files, the output of ls will be empty (though you'll see an error about a missing *.txt file) and as egrep will receive no file argument, it will look for Stuff in its standard input (and seemingly hang).

Example

Let's create 4 files containing Stuff in our directory:

$ echo Stuff | tee file1 file2 'a b c.txt' 'f* .txt'
Stuff
$ ls -Q
"a b c.txt"  "file1"  "file2"  "f* .txt"

Now, let's run the egrep command:

$ egrep "Stuff" $(ls *.txt)
grep: a: No such file or directory
grep: b: No such file or directory
grep: c.txt: No such file or directory
file1:Stuff
file2:Stuff
f* .txt:Stuff
grep: .txt: No such file or directory

Observe that we get 4 error messages about nonexistent files. This is due to word splitting. The result also shows matches with two files, file1 and file2 that should not have been searched because they don't end with .txt. This is because of _pathname expansion`.

The correctly written command produces two successful matches and no errors:

$ egrep -- "Stuff" *.txt
a b c.txt:Stuff
f* .txt:Stuff

Recommended solution

Use:

egrep -- "Stuff" *.txt

or POSIXly:

grep -E -- "Stuff" *.txt

or:

grep -E -e Stuff -- *.txt

This will work with any file name and has none of the limitations of the ls approach.


The $( ) runs the command ls *.txt and returns the STDOUT of the command.

What this particular usage is, is a newbie programming mistake on at least three levels:

  1. egrep 'Stuff' *.txt works, like you said, except for files named something like A File.txt
  2. Using ls output for program input is unwise. See reasons
  3. $( ls *.txt) mishandles filenames with spaces and other funny characters. find . -maxdepth 1 -type f -iname '*.txt' -print0 | xargs -0 egrep 'Stuff' is a more bullet-resistant way.

A contrived example of shell glob expansion/confusion, in response to @8bittree is:

$ /bin/ls -l -b
total 36
-rw------- 1 w3 walt 2 Aug 11 13:41 A\ \\012\ File.txt
-rw------- 1 w3 walt 2 Aug 11 13:42 A\ \n\ File.txt
-rw------- 1 w3 walt 2 Aug 11 13:40 A\ File.txt

$ grep "STUFF" $(ls *.txt)
grep: A: No such file or directory
grep: \012: No such file or directory
grep: File.txt: No such file or directory
grep: A: No such file or directory
grep: File.txt: No such file or directory
grep: A: No such file or directory
grep: File.txt: No such file or directory

$ find . -maxdepth 1 -type f -iname '*.txt' -print0 | xargs -0 egrep 'Stuff'

$ find . -maxdepth 1 -type f -iname '*.txt' -print0 | xargs -0 egrep '1'
./A File.txt:1

Wow, for a moment I thought you were wondering about `$(ls *.txt)` (with $(..) plus backquotes)

As the question has already been answered, I will just warn you about one thing. Here is a sequence of commands to show you why it can be dangerous:

  • set -x
    To see what is executed
  • touch 'rm -f *.txt'
    Create a text file named 'rm -f *.txt', I use simple quotes here so Bash won't expand the wildcard and spaces.
  • `$(ls *.txt)`
    Here is the tricky part. The command ls *.txt will be run, then redirect the result to STDOUT. This result, rm -f *.txt will be now executed because of the backquotes.
    So, rm will remove every text files, in our case the file rm -f *.txt

I hope you understand, - as a demonstration just run the previous commands in an empty directory, so you won't break anything.