0

I have a script that looks for files in the present working directories and says if a file is a file or a directory

for file in $(ls | sort) ; do
        if [ -d $file ]
        then
                echo "$file is a directory"
        fi
        if [ -f $file ]
        then
                echo "$file is a file"
        fi
done

And that works fine. However, when I edit it to view the files in the parent directory I get no output. I changed this

for file in $(ls | sort) ; do

to this:

for file in $(ls .. | sort) ; do

Am I missing something here?

Raffa
  • 34,963

2 Answers2

3

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
Raffa
  • 34,963
2

Yup, you're missing something. You're listing files in the parent directory, but when you check whether it is a file or directory, you're still in the current directory - you have to modify it like this to work with parent directory:

for file in $(ls .. | sort) ; do
        if [ -d "../$file" ]
        then
                echo "$file is a directory"
        fi
        if [ -f "../$file" ]
        then
                echo "$file is a file"
        fi
done

However, as the comments indicate, this isn't necessarily the best way to go about this.

Artur Meinild
  • 31,035