Living Miracles
Living Miracles

Reputation: 103

How to Make a "for loop" Recursive

I have a command that seems to successfully replace double quotes (") with single quotes (') for filenames within the current directory. However, I need to do this recursively (i.e., all files within all subdirectories). Here is the code I am working with:

for f in *; do
    if [[ -f "$f" ]]; then
        new_name=$(echo "$f" | sed "s/\"/'/g")
        mv "$f" "$new_name"
    fi
done

Any advice would be greatly appreciated.

Upvotes: 4

Views: 643

Answers (1)

Charles Duffy
Charles Duffy

Reputation: 295403

Best practice (and the much more efficient solution when you're dealing with a large and deeply-nested directory tree) is to use find for this, not a for loop in bash at all. Using Find goes into the tradeoffs between -print0, -exec and -execdir; here, I'm using the last of these:

#!/usr/bin/env bash

# define the function we're going to export for access from a subprocess
do_replace() {
  local name new_name old_quotes='"' new_quotes="'"
  for name do  # loops by default over "$@", the argument list given to the function
    new_name=${name//$old_quotes/$new_quotes}
    mv -- "$name" "$new_name"
  done
}
export -f do_replace # actually export that function

# tell find to start a new copy of bash that can run the function we exported
# in each directory that contains one or more file or directory names with quotes.
# Using ``-execdir ... {} ';'`` to work around a MacOS bug
# ...use ``-execdir ... {} +`` instead with GNU find for better performance.
find . -depth -name '*"*' -execdir bash -c 'do_replace "$@"' _ {} ';'

That way there's a new copy of bash for each directory, so you aren't operating on names with /s in them; this avoids some security holes that can happen if you're renaming files in directories a different user can write to.


That said, the easy thing (in bash 4.0 or later) is to enable globstar, after which ** will recurse:

#!/usr/bin/env bash
# WARNING: This calculates the whole glob before it runs any renames; this can be very
# inefficient in a large directory tree.

case $BASH_VERSION in ''|[123].*) echo "ERROR: Bash 4.0+ required" >&2; exit 1;; esac
shopt -s globstar
for f in **; do
  : "put your loop's content here as usual"
done

Upvotes: 4

Related Questions