I know you can use the find
command for this simple job, but I got an assignment not to use find
or ls
and do the job. How can I do that?
10 Answers
you can do it with just the shell
#!/bin/bash
recurse() {
for i in "$1"/*;do
if [ -d "$i" ];then
echo "dir: $i"
recurse "$i"
elif [ -f "$i" ]; then
echo "file: $i"
fi
done
}
recurse /path
OR if you have bash 4.0
#!/bin/bash
shopt -s globstar
for file in /path/**
do
echo $file
done
-
1why don't you downvote the one with the tree command as well? or the one with echo, those are valid answers as well. Commented Jan 28, 2010 at 12:05
-
This will fail if a directory is empty, i.e. it will print
dir: bla/*
sincebla/*
is seen as a literal as it has no descendants. You can either usepushd/popd
, or addif [[ "$1/*" = "$i" ]]; then continue; fi
as the first line afterdo
.– BogdanCommented Oct 18, 2015 at 20:47 -
This could also be fixed (to not emit
bla/*
) by use ofshopt -s nullglob
, or[ -e "$i" ] || [ -L "$i" ] || continue
inside the loop. Commented Aug 13, 2018 at 16:07
Try using
tree -d
-
1
-
2I didn't give you the -1, but it's obvious to me why you got it. Why not give as answer
alias bla ls ; bla
? The question that was asked was to write the algorithm, not find some command which does the same thing asls
but is not calledls
.– vladrCommented Mar 7, 2010 at 4:50 -
8@Vlad Romascanu: Oh... ok, but from "I got an assignment not to use find or ls and do the job" I understood that everything was good enough, except find and ls ^^ Commented Mar 8, 2010 at 9:09
-
On the other hand maybe his assignment has to run on Solaris or AIX or HPUX, not Linux. :) Let me assure you there is no
tree
command available by default on non-Linuces etc.– vladrCommented Mar 8, 2010 at 14:38
Below is one possible implementation:
# my_ls -- recursively list given directory's contents and subdirectories
# $1=directory whose contents to list
# $2=indentation when listing
my_ls() {
# save current directory then cd to "$1"
pushd "$1" >/dev/null
# for each non-hidden (i.e. not starting with .) file/directory...
for file in * ; do
# print file/direcotry name if it really exists...
test -e "$file" && echo "$2$file"
# if directory, go down and list directory contents too
test -d "$file" && my_ls "$file" "$2 "
done
# restore directory
popd >/dev/null
}
# recursively list files in current
# directory and subdirectories
my_ls .
As an exercise you can think of how to modify the above script to print full paths to files (instead of just indented file/dirnames), possibly getting rid of pushd
/popd
(and of the need for the second parameter $2
) in the process.
Incidentally, note the use of test XYZ && command
which is fully equivalent to if test XYZ ; then command ; fi
(i.e. execute command
if test XYZ
is successful). Also note that test XYZ
is equivalent to [ XYZ ]
, i.e. the above is also equivalent to if [ XYZ ] ; then command ; fi
. Also note that any semicolon ;
can be replaced with a newline, they are equivalent.
Remove the test -e "$file" &&
condition (only leave the echo
) and see what happens.
Remove the double-quotes around "$file"
and see what happens when the directory whose contents you are listing contains filenames with spaces in them. Add set -x
at the top of the script (or invoke it as sh -x scriptname.sh
instead) to turn on debug output and see what's happenning in detail (to redirect debug output to a file, run sh -x scriptname.sh 2>debugoutput.txt
).
To also list hidden files (e.g. .bashrc
):
...
for file in * .?* ; do
if [ "$file" != ".." ] ; then
test -e ...
test -d ...
fi
done
...
Note the use of !=
(string comparison) instead of -ne
(numeric comparison.)
Another technique would be to spawn subshells instead of using pushd
/popd
:
my_ls() {
# everything in between roundbrackets runs in a separatly spawned sub-shell
(
# change directory in sub-shell; does not affect parent shell's cwd
cd "$1"
for file in ...
...
done
)
}
Note that on some shell implementations there is a hard limit (~4k) on the number of characters which can be passed as an argument to for
(or to any builtin, or external command for that matter.) Since the shell expands, inline, *
to a list of all matching filenames before actually performing for
on it, you can run into trouble if *
is expanded inside a directory with a lot of files (same trouble you'll run into when running, say ls *
in the same directory, e.g. get an error like Command too long
.)
-
-
@ghostdog -- not portable.
.*
(or.?*
to skip.
off the bat) does the trick just fine and is portable too.– vladrCommented Mar 7, 2010 at 4:52
Since it is for bash, it is a surprise that this hasn't been already said:
(globstar valid from bash 4.0+)
shopt -s globstar nullglob dotglob
echo **/*/
That's all.
The trailing slash /
is there to select only dirs.
Option globstar
activates the **
(search recursivelly).
Option nullglob
removes an *
when it matches no file/dir.
Option dotglob
includes files that start wit a dot (hidden files).
The du
command will list subdirectories recursively.
I'm not sure if empty directories get a mention, though
Like Mark Byers said you can use echo *
to get a list of all files in the current directory.
The test
or []
command/builtin has an option to test if a file is a directory.
Apply recursion and you're done.
-
2As an alternative to
echo
,for
will glob, so OP can usefor file in * ; do ... ; done
.– outisCommented Mar 7, 2010 at 4:48
$ function f { for i in $1/*; do if [ -d $i ]; then echo $i; f $i; fi; done }
$ mkdir -p 1/2/3 2/3 3
$ f .
./1
./1/2
./1/2/3
./2
./2/3
./3
-
1-1 if you're going to provide a complete answer to a homework question, make it a correct one. what about spaces in directory entry names? Commented Jan 28, 2010 at 12:05
-
and it does not enter directories and does not list text files Commented Feb 21, 2013 at 11:13
-
it DOES enter directories, and the OP said nothing about text files (or any files apart from directories). I'm not going to fix the 'directories with spaces' issue. Commented Mar 6, 2013 at 7:57
-
Too bad. This was almost a great answer, but lacks the information I need (as a n00b) to tinker with it. Looks like just another string of bash-foo to me.– wybeCommented Aug 24, 2017 at 15:06
-
So you don’t like an answer written in bash to a question asking for bash because it’s too bashy? This may not be the site for you. Commented Aug 24, 2017 at 15:10
Technically, neither find nor ls are used by find2perl|perl or File::Find directly.
$ find2perl -type d | perl $ perl -MFile::Find -e'find(sub{-d&&print"$File::Find::name\n"},".")'
-
but then, if that's the case, using Python/Ruby/PHP etc any language that can list files will be technically not using
ls
orfind
Commented Mar 7, 2010 at 5:49 -
@ghostdog74 Obviously. But since all the serious answers have gotten less than amazing feedback, this is something... less serious. (I was actually going to write up a clone of
find
in C and a shell script which compiles+runs it, but decided it was too much effort for a joke.) Commented Mar 7, 2010 at 5:55
Based on this answer; use shell options for the desired globbing behaviour:
- enable
**
withglobstar
(Bash 4.0 or newer) - include hidden directories with
dotglob
- expand to the empty string instead of
**/*/
if there is no match withnullglob
and then use printf
with the %q
formatting directive to quote directory names with special characters in them:
shopt -s globstar dotglob nullglob
printf '%q\n' **/*/
so if you have directories like has space
or even containing a newline, you'd get output like
$ printf '%q\n' **/*/
$'has\nnewline/'
has\ space/
with one directory per line.
I wrote a another solution iterative instead of recursive.
iterativePrintDir() {
dirs -c;
currentd=$1
if [ ! -d $currentd ];then
echo "Diretorio não encontrado. \"$currentd\""
fi
pushd $(readlink -f $currentd)
while popd 2>&-; do
echo $currentd
for d in $(dir $currentd); do
test -d "${currentd}/${d}" && pushd "${currentd}/${d}"
done
currentd=$(dirs -l +0)
done
}
echo *
or variations on that to emulatels
.