Neville Hillyer
Neville Hillyer

Reputation: 354

Recursively list hidden files without ls, find or extendedglob

As an exercise I have set myself the task of recursively listing files using bash builtins. I particularly don't want to use ls or find and I would prefer not to use setopt extendedglob. The following appears to work but I cannot see how to extend it with /.* to list hidden files. Is there a simple workaround?

g() { for k in "$1"/*; do # loop through directory
[[ -f "$k" ]] && { echo "$k"; continue; }; # echo file path
[[ -d "$k" ]] && { [[ -L "$k" ]] && { echo "$k"; continue; }; # echo symlinks but don't follow
g "$k"; }; # start over with new directory
done; }; g "/Users/neville/Desktop" # original directory

Added later: sorry - I should have said: 'bash-3.2 on OS X'

Upvotes: 2

Views: 354

Answers (3)

dannysauer
dannysauer

Reputation: 3867

Set the GLOBIGNORE file to exclude . and .., which implicitly turns on "shopt -u dotglob". Then your original code works with no other changes.

user@host [/home/user/dir]
$ touch file
user@host [/home/user/dir]
$ touch .dotfile
user@host [/home/user/dir]
$ echo *
file
user@host [/home/user/dir]
$ GLOBIGNORE=".:.."
user@host [/home/user/dir]
$ echo *
.dotfile file

Note that this is bash-specific. In particular, it does not work in ksh.

Upvotes: 1

Score_Under
Score_Under

Reputation: 1216

You can specify multiple arguments to for:

for k in "$1"/* "$1"/.*; do

But if you do search for .* in directories , you should be aware that it also gives you the . and .. files. You may also be given a nonexistent file if the "$1"/* glob matches, so I would check that too.

With that in mind, this is how I would correct the loop:

g() {
    local k subdir
    for k in "$1"/* "$1"/.*; do # loop through directory
        [[ -e "$k" ]] || continue  # Skip missing files (unmatched globs)
        subdir=${k##*/}
        [[ "$subdir" = . ]] || [[ "$subdir" = .. ]] && continue  # Skip the pseudo-directories "." and ".."

        if [[ -f "$k" ]] || [[ -L "$k" ]]; then
            printf %s\\n "$k"  # Echo the paths of files and symlinks
        elif [[ -d "$k" ]]; then
            g "$k"  # start over with new directory
        fi
    done
}
g ~neville/Desktop

Here the funky-looking ${k##*/} is just a fast way to take the basename of the file, while local was put in so that the variables don't modify any existing variables in the shell.

One more thing I've changed is echo "$k" to printf %s\\n "$k", because echo is irredeemably flawed in its argument handling and should be avoided for the purpose of echoing an unknown variable. (See Rich's sh tricks for an explanation of how; it boils down to -n and -e throwing a spanner in the works.)

By the way, this will NOT print sockets or fifos - is that intentional?

Upvotes: 0

rici
rici

Reputation: 241701

Change

for k in "$1"/*; do

to

for k in "$1"/* "$1"/.[^.]* "$1"/..?*; do

The second glob matches all files whose names start with a dot followed by anything other than a dot, while the third matches all files whose names start with two dots followed by something. Between the two of them, they will match all hidden files other than the entries . and ...

Unfortunately, unless the shell option nullglob is set, those (like the first glob) could remain as-is if there are no files whose names match (extremely likely in the case of the third one) so it is necessary to verify that the name is actually a file.

An alternative would be to use the much simpler glob "$1"/.*, which will always match the . and .. directory entries, and will consequently always be substituted. In that case, it's necessary to remove the two entries from the list:

for k in "$1"/* "$1"/.*; do
  if ! [[ $k =~ /\.\.?$ ]]; then
    # ...
  fi
done

(It is still possible for "$1"/* to remain in the list, though. So that doesn't help as much as it might.)

Upvotes: 2

Related Questions