Output of loop variable shows different value than expected

Imagine I have two folders in my current working directory: back/ and front/. I am trying to enter inside each of them and do some stuff. From the simple script below:

for dir in */ ; do
    echo "$(dir)"
done

I was expecting an output like:

back
front

However, what I am getting is:

back  front
back  front

I don’t understand. The consequence is that, if I try to cd into the folder:

for dir in */ ; do
    echo "$(dir)"
    cd $(dir)
    echo "$(pwd)"
    cd ..
done

I get:

back  front
/path/to/back
back  front
/path/to/back

I.e. I never enter front/. Could someone please help me understand what I am doing wrong?

Asked By: Rigel F. do C.

||

In POSIX-like shells, $(cmd) is the syntax for command substitution, $(cmd) is expanded to the output of cmd minus the trailing newline characters.

What you want instead is parameter expansion: $dir or ${dir} (here with the parameter being the dir variable).

pwd (print working directory) is the command that outputs the contents of the $PWD¹ special variable which itself contains a path to the current working directory, so $(pwd) expands to the same thing as $PWD as long as $PWD doesn’t end in newline characters.

So you want:

for dir in */; do 
  (
    printf '%sn' "$dir"
    cd -- "$dir" &&
      printf '%sn' "$PWD"
  )
done

Or:

for dir in */; do 
  (
    printf '%sn' "$dir"
    cd -- "$dir" &&
      pwd
  )
done

Also remember that

  • echo can’t be used to output arbitrary data, use printf instead
  • you should generally check the exit status of commands. Here not checking the output of cd would mean that the following cd .. would land you in the wrong place if the first cd failed.
  • better to run cd in a subshell (using (...)) rather than using cd .. as the latter is not always guaranteed to get you back to the initial place (if there are symlinks involved and/or some path components have been renamed in the interval)
  • -- must be used to separate options from non-options when the non-options can’t be guaranteed not to start with - (or + which can be a problem as well for some commands).
  • in POSIX-like shells, parameter expansions and command substitutions (and arithmetic expansions) must be quoted as otherwise they undergo implicit split+glob (except in zsh) which you generally don’t want.

Also beware that in bash (and other POSIX shells except zsh), when a glob doesn’t match any file, it is left unexpanded.

So if the current working directory doesn’t contain any non-hidden file that can be determined to be of type directory, the */ glob pattern will just expand to itself */ instead of an empty list and you’ll likely see an error by cd that it can’t enter that */ directory.

In zsh, you’d use the N glob qualifier as in for dir in */(N); do... (or for dir in *(N-/); do... to avoid $dir ending in /).

In ksh93: for dir in ~(N)*/; do....

In bash, you can set the nullglob option on globally temporarily:

shopt -s nullglob
for dir in */; do
  ...
done
shopt -u nullglob

For completeness, $(var) is the syntax for macro expansions in Makefiles as used by the make command.

In the awk language, $ is a unary operator to dereference fields, and you just use var to refer to an awk variable. So there, $(var) would be the same as $var or $ var and expand to the varth field (or the whole record if var is 0).

In rc-like shells, $(var) same as $var, $ var, $ 'var', $ ( 'var' ) would also work, as that’s a list of one element passed to $, so you’re dereferencing the var variable, though you would normally use just $var.

In TCL, $(var) would expand to the value of the array variable with empty name for the var key:

$ tclsh
% array set {} {foo 1 bar 2}
% puts $(foo)
1

¹ Yes, P for Print doesn’t make much sense if this context. As you can guess the pwd command (from Unix V5 in the mid-70s) was first, and $PWD and the logical handling of the current working directory both of which have been specified by POSIX came later (in ksh in the early 80s, where pwd was actually an alias for print - $PWD (yes with the missing -r and quotes!) and $PWD was bracronymed Present Working Directory in the documentation). tcsh has a $cwd (current working directory) variable for the same purpose

Answered By: Stéphane Chazelas