Suggestion
Shell globbing for filenames is the most effective and reliable way when listing files and directories in bash … Almost all other external file listing tools are prone to errors e.g. word splitting when processing their output in bash and they need special care/output formatting and error handling mechanisms implemented in your script to work with them reliably in bash … Their output might not be formatted to be ,by default, reliably processable in bash.
Therefore, …
To work on files and directories in the current directory, use:
for f in *; do
if [ -d "$f" ]; then
echo "$f is a directory"
elif [ -f "$f" ]; then
echo "$f is a file"
fi
done
Notice the double quotes("
) around the parameter $f
must be used inside the loop’s body(i.e. between the two command constructs do … done
) but must not be used in the loop’s head(i.e. for f in *;
).
To work on directories only, use:
for f in */; do ...
To work on parent directory contents, use:
for f in ../*; do ...
Or to limit to directories only in parent directory, use:
for f in ../*/; do ...
Notice that shell filename globbing in bash will be alphabetically and numerically sorted by default.
Notice that the output of ls
will be sorted by default as well so you don't need to pipe it to sort
the way you did in your example ... Please also refrain from parsing the output of ls
that way unless you know the consequences ... Please see Why not parse ls
(and what to do instead)?
Demonstration
To further understand the difference, please see the demonstration below:
Create these four files:
touch 3_with_tab$'\t'file.txt; touch 1_with_newline$'\n'file.txt; touch '2_with_space file.txt'; touch 4_normal_file.txt
List them using shell globbing ... Works perfectly:
for f in *; do [ -f "$f" ] && echo "$f is a file"; done
1_with_newline
file.txt is a file
2_with_space file.txt is a file
3_with_tab file.txt is a file
4_normal_file.txt is a file
With find
piped to a while
loop ... Fails on filenames containing a new line:
find | while IFS= read -r f; do [ -f "$f" ] && echo "$f is a file"; done
./3_with_tab file.txt is a file
./4_normal_file.txt is a file
./2_with_space file.txt is a file
With find
in a for
loop ... Fails on filenames containing spaces, tabs or newlines:
for f in $(find); do [ -f "$f" ] && echo "$f is a file"; done
./4_normal_file.txt is a file
With ls
piped to a while
loop ... Fails on filenames containing a new line:
ls | while IFS= read -r f; do [ -f "$f" ] && echo "$f is a file"; done
2_with_space file.txt is a file
3_with_tab file.txt is a file
4_normal_file.txt is a file
With ls
in a for
loop ... Fails on filenames containing spaces, tabs or newlines:
for f in $(ls); do [ -f "$f" ] && echo "$f is a file"; done
4_normal_file.txt is a file
Mechanisms(find
as example)
If, however, you must use an external file listing tool, then you need to pay attention formatting its output and reading it as input in bash … With find
for example, your best bet is to use find
's action -print0
(NULL delimited output) piped to a while
loop that makes use of read
's option -d
to set the input delimiter to NULL using the bash
's ANSI-C quoting $'\0'
like so:
find -print0 | while IFS= read -r -d $'\0' f; do [ -f "$f" ] && echo "$f is a file"; done
./3_with_tab file.txt is a file
./4_normal_file.txt is a file
./1_with_newline
file.txt is a file
./2_with_space file.txt is a file
Or sorted with sort
's option -z
(line delimiter is NULL, not newline) like so:
find -print0 | sort -z | while IFS= read -r -d $'\0' f; do [ -f "$f" ] && echo "$f is a file"; done
./1_with_newline
file.txt is a file
./2_with_space file.txt is a file
./3_with_tab file.txt is a file
./4_normal_file.txt is a file