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 whyls
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, usefind
instead ofls
. $(...)
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 bothls
andegrep
as Ubuntu'segrep
being the GNU implementation accepts options even after non-option arguments unlessPOSIXLY_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 ofls
will be empty (though you'll see an error about a missing*.txt
file) and asegrep
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:
-
egrep 'Stuff' *.txt
works, like you said, except for files named something likeA File.txt
- Using
ls
output for program input is unwise. See reasons -
$( 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 commandls *.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 filerm -f *.txt
I hope you understand, - as a demonstration just run the previous commands in an empty directory, so you won't break anything.