54

The following command attempts to enumerate all *.txt files in the current directory and process them one by one:

for line in "find . -iname '*.txt'"; do 
     echo $line
     ls -l $line; 
done

Why do I get the following error?:

ls: invalid option -- 'e'
Try `ls --help' for more information.
0x90
  • 39,472
  • 36
  • 165
  • 245

4 Answers4

125

Here is a better way to loop over files as it handles spaces and newlines in file names:

#!/bin/bash

find . -type f -iname "*.txt" -print0 | while IFS= read -r -d $'\0' line; do
    echo "$line"
    ls -l "$line"    
done
Matt Clegg
  • 584
  • 1
  • 4
  • 16
dogbane
  • 266,786
  • 75
  • 396
  • 414
  • I'm having trouble getting this to work when the script containing this snippet is called from within a cronjob @reboot. It complains about the -d flag of the read command and then fails to execute. Otherwise it works great. – Daniel F May 02 '14 at 10:05
  • 11
    Works on bash, not on sh. – Antzi Aug 27 '15 at 10:35
  • @jww Works for me on macOS Sierra, `GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin16)` – fnkr Jan 17 '18 at 13:17
19

The for-loop will iterate over each (space separated) entry on the provided string.

You do not actually execute the find command, but provide it is as string (which gets iterated by the for-loop). Instead of the double quotes use either backticks or $():

for line in $(find . -iname '*.txt'); do 
     echo "$line"
     ls -l "$line"
done

Furthermore, if your file paths/names contains spaces this method fails (since the for-loop iterates over space separated entries). Instead it is better to use the method described in dogbanes answer.


To clarify your error:

As said, for line in "find . -iname '*.txt'"; iterates over all space separated entries, which are:

  • find
  • .
  • -iname
  • '*.txt' (I think...)

The first two do not result in an error (besides the undesired behavior), but the third is problematic as it executes:

ls -l -iname

A lot of (bash) commands can combine single character options, so -iname is the same as -i -n -a -m -e. And voila: your invalid option -- 'e' error!

Community
  • 1
  • 1
Veger
  • 37,240
  • 11
  • 105
  • 116
  • 11
    If there are spaces in the name, it will appear as two separate entries in the list that the for loop iterates over. – chepner Feb 25 '13 at 15:54
18

More compact version working with spaces and newlines in the file name:

find . -iname '*.txt' -exec sh -c 'echo "{}" ; ls -l "{}"' \;
jserras
  • 711
  • 1
  • 7
  • 13
  • 2
    This answer is much simpler and it works in both Bash and the simpler sh (Ash) shells. – Bernard Sep 24 '16 at 05:05
  • This answer is **seriously buggy** right now; a malicious filename can run arbitrary code. Consider what happens if someone created `$(rm -rf ~).txt`, or `"; rm -rf ~; echo ".txt` (with the double-quotes as literal part of the filenames). – Charles Duffy Oct 24 '20 at 15:15
  • The safe (and more efficient) way to write this would be `find . -iname '*.txt' -exec sh -c 'for arg in "$@"; do echo "$arg"; ls -l "$arg"; done' _ {} +` – Charles Duffy Oct 24 '20 at 15:16
2

Use command substitution instead of quotes to execute find instead of passing the command as a string:

for line in $(find . -iname '*.txt'); do 
     echo $line
     ls -l $line; 
done
kenorb
  • 155,785
  • 88
  • 678
  • 743
Jens Erat
  • 37,523
  • 16
  • 80
  • 96
  • 4
    if you have a file named "single file" you will get two `$lines` in the loop. One for "single" another for "file". – gcb Apr 19 '17 at 23:19
  • See [BashPitfalls #1](http://mywiki.wooledge.org/BashPitfalls#for_f_in_.24.28ls_.2A.mp3.29) re: `for line in $(...anything...)`, and [BashPitfalls #14](http://mywiki.wooledge.org/BashPitfalls#echo_.24foo) re: `echo $line`. For the latter, also see [I just assigned a variable, but `echo $variable` shows something else!](https://stackoverflow.com/questions/29378566/i-just-assigned-a-variable-but-echo-variable-shows-something-else) – Charles Duffy Oct 24 '20 at 15:14