Reputation: 89
Please find the code below for displaying the directories in the current folder.
${arg##/*/}
in shell scripting. (Both arg#*
and arg##/*/
gives the same output. ){} \;
in for loop statement.for arg in `find . -type d -exec ls -d {} \;`
do
echo "Output 1" ${arg##/*/}
echo "Output 2" ${arg#*}
done
Upvotes: 0
Views: 3047
Reputation: 26501
${arg##/*/}
is an application of "parameter expansion". (Search for this term in your shell's manual, e.g. type man bash
in a linux shell). It expands to arg
without the longest prefix of arg that matches /*/
as a glob pattern. E.g. if arg
is /foo/bar/doo
, it expands to doo
.
That's bad shell code (similar to item #1 on Bash Pitfalls). The {} \;
has not so much to do with shell, but more with the arguments that the find
command expects to an -exec
subcommand. The {}
is replaced with the current filename, e.g. this results in find
executing the command ls -d FILENAME
with FILENAME replaced by each file it found. The \;
serves as a terminator of the -exec
argument. See the manual page of find
, e.g. type man find
on a linux shell, and look for the string -exec
there to find the description.
Upvotes: 2
Reputation: 437111
Adding to @JoSo's helpful answer:
${arg#*}
is a fundamentally pointless expansion, as its result is always identical to $arg
itself, since it strips the shortest prefix matching any character (*
) and the shortest prefix matching any character is the empty string.
${arg##/*/}
- stripping the longest prefix matching pattern /*/
- is useless in this context, because the output paths will be ./
-prefixed due to use of find .
, so there will be no prefix starting with /
. By contrast, ${arg##*/}
will work and strip the parent path (leaving the folder-name component only).
Aside from it being ill-advised to parse command output in a for
loop, as @JoSo points out,
the find
command in the OP is overly complicated and inefficient
(as an aside, just to clarify, the find
command lists all folders in the current folder's subtree, not just immediate subfolders):
find . -type d -exec ls -d {} \;
can be simplified to:
find . -type d
The two commands do the same: -exec ls -d {} \;
simply does what find
does by default anyway (an implied -print
).
If we put it all together, we get:
find . -mindepth 1 -type d | while read -r arg
do
echo "Folder name: ${arg##*/}"
echo "Parent path: ${arg%/*}"
done
Note that I've used ${arg%/*}
as the second output item, which strips the shortest suffix matching /*
and thus returns the parent path; furthermore, I've added -mindepth 1
so that find
doesn't also match .
@JoSo, in a comment, demonstrates a solution that's both simpler and more efficient; it uses -exec
to process a shell command in-line and +
to pass as many paths as possible at once:
find . -mindepth 1 -type d -exec /bin/sh -c \
'for arg; do echo "Folder name: ${arg##*/}"; echo "Parent: ${arg%/*}"; done' \
-- {} +
Finally, if you have GNU find
, things get even easier, as you can take advantage of the -printf
primary, which supports placeholders for things like filenames and parent paths:
find . -type d -printf 'Folder name: %f\nParen path: %h\n'
Here's a bash-only solution based on globbing (pathname expansion), courtesy of @Adrian Frühwirth:
Caveat: This requires bash 4+
, with the shell option globstar
turned ON (shopt -s globstar
) - it is OFF by default.
shopt -s globstar # bash 4+ only: turn on support for **
for arg in **/ # process all directories in the entire subtree
do
echo "Folder name: $(basename "$arg")"
echo "Parent path: $(dirname "$arg")"
done
Note that I'm using basename
and dirname
here for parsing, as they conveniently ignore the terminating /
that the glob **/
invariably adds to its matches.
Afterthought re processing find
's output in a while
loop: on the off chance that your filenames contain embedded \n
chars, you can parse as follows, using a null char. to separate items (see comments for why -d $'\0'
rather than -d ''
is used):
find . -type d -print0 | while read -d $'\0' -r arg; ...
Upvotes: 7