user3395194
user3395194

Reputation: 89

What is the meaning of ${arg##/*/} and {} \; in shell scripting

Please find the code below for displaying the directories in the current folder.

  1. What is the meaning of ${arg##/*/} in shell scripting. (Both arg#* and arg##/*/ gives the same output. )
  2. What is the meaning of {} \; 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

Answers (2)

Jo So
Jo So

Reputation: 26501

  1. ${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.

  2. 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

mklement0
mklement0

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

Related Questions