2

How can I mimic the tree command and iterate through all the files and subdirectories of a directory and echo all the file names?

I thought that a subdirectory within a directory is still counted as a directory, so I did:

for FILE in *; do
if [ -d $FILE ]
then echo "$FILE is a directory";
else echo "$FILE is a file"
fi
done

How can I make this recursive and loop inside the subdirectories and iterate through all the files and print their file name + path?

This is a shell script using bash. Thanks!

6
  • I hope you didn't use this exact code, because '$FILE' evaluates to the string $FILE and not to the file name. (and assuming you have nothing that is called $FILE in your directory, this nonexistent filesystem entry is of course always not a directory) Commented Apr 8, 2023 at 13:42
  • 1
    By the way, the inference "is not a directory, so must be file" is wrong; can be a symlink, a device node, a socket or a named pipe, too. Commented Apr 8, 2023 at 13:44
  • Edited the post, thanks for the clarification. Also, would you know the answer to the remaining question? Do I just put another for loop inside the original loop?
    – John Smith
    Commented Apr 8, 2023 at 13:51
  • With regards to the "is not a directory, so must be file" is wrong, is that very important? I just need to list all the files that has lines inside of it. I assume that symlink, a device node, a socket or a named pipe does not have lines inside of it?
    – John Smith
    Commented Apr 8, 2023 at 13:55
  • 1
    Symlinks might, depending on whether you think of the symlink itself as a file or of whatever it is the link is pointing to, but it doesn't matter. One of the main points of the Unix philosophy is that everything (and that actually includes directories) is a file.
    – terdon
    Commented Apr 8, 2023 at 14:08

3 Answers 3

3

Here's an equivalent to terdon's shell function implemented as a find command:

find . -exec sh -c 'for f; do
                      [ -d "$f" ] && is_dir="" || is_dir="not "
                      printf "\"%s\" is %sa directory\n" "$f" "$is_dir"
                    done' sh {} +

Or with find & perl (indenting two spaces per directory level and using NUL as the filename separator):

find . -print0 | \
  perl -0ne '$is_dir = -d $_ ? "" : "not ";
             $f = $_; $indent = ($f =~ s=/==g);
             printf "  " x $indent . "%s is %sa directory\n", $_, $is_dir'

Most of the perl one-liner is straight-forward and obvious, but one line might require some explanation:

$f = $_; $indent = ($f =~ s=/==g);

This copies the current input record (i.e. the filename, $_) into variable $f. $f is then modified to remove all / characters while the count of changes is stored in variable $indent. $f isn't used again, it's just a throwaway variable to avoid changing $_.

The $indent variable is used with the repetition operator (x - similar to multiplication but for strings, see man perlop) to construct the printf format string.


To count the files and directories, with 3-digit wide numbering of each line:

$ find . -print0 | perl -0ne '
    if ( -d $_ ) {
      $is_dir = "";
      $dirs++
    } else {
      $is_dir = "not ";
      $files++
    };

    $f = $_; $indent = ($f =~ s=/==g);
    #printf "  " x $indent . "%s is %sa directory\n", $_, $is_dir;
    printf "%3i: " . "  " x $indent . "%s is %sa directory\n", $., $_, $is_dir;

    END {
      # simple method to determine singular or plural word forms
      # without using the Lingua::EN::Inflect module
      # (see https://metacpan.org/release/Lingua-EN-Inflect)
      my $d_word = ($dirs  != 1) ? "directories" : "directory";
      my $f_word = ($files != 1) ? "files" : "file";

      print "\n$dirs $d_word, $files $f_word\n";
    }'

The perl part of this one-liner has got to the point where it is no longer reasonable to edit on the command line, because dealing with absurdly long lines is a complete PITA - it's much easier to use a text editor. It should be saved to a script file somewhere in your $PATH (e.g. ~/bin/ or /usr/local/bin - add to your PATH if it's not already there) with #!/usr/bin/perl -0ne as the first line and made executable with chmod +x.

10
  • it doesn't do the indentation like terdon's edited version....and it's too late at night for me to want to write shell code to do that. if anyone wants to take it from here, feel free.
    – cas
    Commented Apr 8, 2023 at 15:03
  • Note that this, unlike mine, will not descend into symlinked directories. No idea if this is what the OP wants or not.
    – terdon
    Commented Apr 8, 2023 at 15:03
  • 1
    it would be easy with a simple perl one-liner to count the / characters to figure out the indentation level. maybe tomorrow.
    – cas
    Commented Apr 8, 2023 at 15:04
  • find has a -L option for following symlinks if that is required.
    – cas
    Commented Apr 8, 2023 at 15:05
  • Hello, I tried both solutions and it worked. I was wondering if I wanted to add wc -l to count the lines per log file, where would I put it?
    – John Smith
    Commented Apr 9, 2023 at 4:32
2

If you are just interested in separating directories from everything else, you can do something like this, using a recursive function:

#!/bin/bash

## Do not expand globs to themselves if they don't match
shopt -s nullglob

list_files(){
  ## if a target has been passed as an argument, use that; if not,
  ## default to '.', the current directory.
  target=${1:-.}
  for i in "$target"/*; do
    if [ -d "$i" ]; then
      list_files "$i"
    else
      echo "$i is not a directory"
    fi
  done
}

list_files "$@"

Given a directory structure like this:

$ tree
.
├── 1.txt
├── 2.txt
├── 3.txt
├── dir1
│   ├── subdir1
│   │   ├── file
│   │   ├── subsubdir1
│   │   └── subsubdir2
│   └── subdir2
│       ├── file
│       ├── subsubdir1
│       └── subsubdir2
├── dir2
│   ├── subdir1
│   │   ├── subsubdir1
│   │   └── subsubdir2
│   └── subdir2
│       ├── subsubdir1
│       └── subsubdir2
└── symlink -> 2.txt

15 directories, 6 files

The code above produces:

$ bar.sh
"./1.txt" is not a directory
"./2.txt" is not a directory
"./3.txt" is not a directory
"./dir1" is a directory
"./dir1/subdir1" is a directory
"./dir1/subdir1/file" is not a directory
"./dir1/subdir1/subsubdir1" is a directory
"./dir1/subdir1/subsubdir2" is a directory
"./dir1/subdir2" is a directory
"./dir1/subdir2/file" is not a directory
"./dir1/subdir2/subsubdir1" is a directory
"./dir1/subdir2/subsubdir2" is a directory
"./dir2" is a directory
"./dir2/subdir1" is a directory
"./dir2/subdir1/subsubdir1" is a directory
"./dir2/subdir1/subsubdir2" is a directory
"./dir2/subdir2" is a directory
"./dir2/subdir2/subsubdir1" is a directory
"./dir2/subdir2/subsubdir2" is a directory
"./symlink" is not a directory

You can also make it a little easier to read by adding an offset to indicate the depth in the directory tree, and by processing all directories first so as to have a more ordered output:

#!/bin/bash

## Do not expand globs to themselves if they don't match
shopt -s nullglob

list_files(){
  ## if a target has been passed as an argument, use that; if not,
  ## default to '.', the current directory.
  target=${1:-.}

  for i in "$target"/*; do
    offset=${2:-""}
    if [ -d "$i" ]; then
      printf '%s"%s" is a directory\n' "$offset" "$i"
      for subdir in "$i"/*/; do
        new_offset="$offset   "
        list_files "$i" "$new_offset"
        new_offset=""
      done
    else
      printf '%s"%s" is not a directory\n' "$offset" "$i" 
    fi
  done
}

list_files "$@"

This will produce the following output on the example input above:

$ foo.sh 
"./1.txt" is not a directory
"./2.txt" is not a directory
"./3.txt" is not a directory
"./dir1" is a directory
   "./dir1/subdir1" is a directory
      "./dir1/subdir1/file" is not a directory
      "./dir1/subdir1/subsubdir1" is a directory
      "./dir1/subdir1/subsubdir2" is a directory
         "./dir1/subdir1/subsubdir2/file" is not a directory
   "./dir1/subdir2" is a directory
      "./dir1/subdir2/file" is not a directory
      "./dir1/subdir2/subsubdir1" is a directory
      "./dir1/subdir2/subsubdir2" is a directory
"./dir2" is a directory
   "./dir2/subdir1" is a directory
      "./dir2/subdir1/subsubdir1" is a directory
      "./dir2/subdir1/subsubdir2" is a directory
   "./dir2/subdir2" is a directory
      "./dir2/subdir2/subsubdir1" is a directory
      "./dir2/subdir2/subsubdir2" is a directory
"./symlink" is not a directory

Note that this approach will descend into symlinked directories. For example, if I add this:

$ ln -s dir2 dirsym
$ ls -ld dirsym
lrwxrwxrwx 1 terdon terdon 4 Apr  8 16:03 dirsym -> dir2

Then dirsym will be treated as a directory and its contents will be reported just like its target, dir2, effectively repeating the dir2 output. I don't know if this is the desired behavior or not. If it isn't, use caz's find approach instead.

0
0
(
  find . -type f -print0 | xargs -0 printf '%s is a file\0'
  find . -type d -print0 | xargs -0 printf '%s is a directory\0'
) | sort -z | tr '\0' '\n'

I'm not sure if I consider this to be a good answer, was something of a "I wonder if I could..."

Produce a list of files and directories.

Print them with appropriate suffix.

Then sort the entire result so items appear in the right context.

Some extra fiddling needed to handle items with newline characters in their name.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .