Reputation: 354
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
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
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
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