tags:

views:

321

answers:

8

Hi,

I need to write a bash script that will iterate through the contents of a directory (including subdirectories) and perform the following replacements:

  • replace 'foo' in any file names with 'bar'
  • replace 'foo' in the contents of any files with 'bar'

So far all I've got is

find . -name '*' -exec {} \;

:-)

+1  A: 

With RH rename:

find -f \( -exec sed -i s/foo/bar/g \; , -name \*foo\* -exec rename foo bar {} \; \)
Ignacio Vazquez-Abrams
+1  A: 
find "$@" -depth -exec sed -i -e s/foo/bar/g {} \; , -name '*foo*' -print0 |
while read -d '' file; do
    base=$(basename "$file")
    mv "$file" "$(dirname "$file")/${base//foo/bar}"
done
ephemient
Sadly, the `,` operator is not standard, and appears to only be present in GNU `find`. Also, this passes directories as well as files into `sed`, you need to add a `-type f` to avoid errors. I like it because it's much more succinct than most of the other answers here.
Brian Campbell
Well, `sed` will error on a directory but the `find` will continue ;-) Without `,` I think this would have to either be split up into two `find` invocations or make the Bash part do more work.
ephemient
A: 

UPDATED: 1632 EST

  • Now handles whitespace but 'while read item' never terminates. Better, but still not right. Will keep working on this.

aj@mmdev0:~/foo_to_bar$ cat script.sh

#!/bin/bash

dirty=true
while ${dirty}
do
    find ./ -name "*" |sed -s 's/ /\ /g'|while read item
    do
        if [[ ${item} == "./script.sh" ]]
        then
            continue
        fi
        echo "working on: ${item}"

        if [[ ${item} == *foo* ]]
        then
            rename 's/foo/bar/' "${item}"
            dirty=true
            break
        fi

        if [[ ! -d ${item} ]]
        then
            cat "${item}" |sed -e 's/foo/bar/g' > "${item}".sed; mv "${item}".sed "${item}"
        fi
        dirty=false
    done
done
AJ
`for item in \`...\`` does the wrong thing if any filenames contain whitespace.
ephemient
@ephemient right you are, working on this fix now...
AJ
+1  A: 
#!/bin/bash
function RecurseDirs
{
oldIFS=$IFS
IFS=$'\n'
for f in *
do
  if [[ -f "${f}" ]]; then
    newf=`echo "${f}" | sed -e 's/foo/bar/g'`
    sed -e 's/foo/bar/g' < "${f}" > "${newf}"
  fi
  if [[ -d "${f}" && "${f}" != '.' && "${f}" != '..' && ! -L "${f}" ]]; then
    cd "${f}"
    RecurseDirs .
    cd ..
  fi
done
IFS=$oldIFS
}
RecurseDirs .
dreynold
very nice, godawful language :)
Matt Joiner
@Don: Sounds like a copy and paste error. Try typing this out instead. @dreynold: watch out for symlinks, you could get stuck in directory loops or go somewhere else altogether as `..` might not be what you think it is. (Yes, Bash usually tries to fix this up, but it's a heuristic. See http://stackoverflow.com/questions/2105572)
ephemient
@ephemient: Yes, you are correct -- this isn't the safest thing around. Corrected, I think.
dreynold
A: 

bash 4.0

#!/bin/bash
shopt -s globstar
path="/path"
cd $path
for file in **
do
    if [ -d "$file" ] && [[ "$file" =~ ".*foo.*" ]];then
        echo mv "$file" "${file//foo/bar}"
    elif [ -f "$file" ];then
        while read -r line
        do
            case "$line" in
                *foo*) line="${line//foo/bar}";;
            esac
            echo "$line"
        done < "$file"  > temp
        echo mv temp "$file"

    fi
done

remove the 'echo' to commit changes

ghostdog74
Minor nit: if `$line` is `'-e'` or `'-n'` or some other switch that Bash `echo` recognizes, it'll get mangled in the file. There's no harm in unconditionally performing the substitution, so I'd cut that whole while body down to `printf '%s\n' "${line//foo/bar}"`. Another nit: you're using Bash's double-bracket test already, why not write the shorter `[[ $file = *foo* ]]`? You also have harder-to-fix issues when a higher-level dirname changes and you're working on something further down the directory tree, and no renames of non-directories (which should happen, by my reading of OP's question).
ephemient
A: 

for f in `tree -fi | grep foo`; do sed -i -e 's/foo/bar/g' $f ; done

keep it simple
A: 

Yet another find-exec solution:

find . -type f -exec bash -c '
path="{}"; 
dirName="${path%/*}";
baseName="${path##*/}";
nbaseName="${baseName/foo/bar}";
#nbaseName="${baseName//foo/bar}";
# cf. http://www.bash-hackers.org/wiki/doku.php?id=howto:edit-ed
ed -s "${path}" <<< $'H\ng/foo/s/foo/bar/g\nwq';
#sed -i "" -e 's/foo/bar/g' "${path}";  # alternative for large files
exec mv -iv "{}" "${dirName}/${nbaseName}"
' \;
gregb
In case the filename contains " or \, you should use something more like `-exec bash -c 'path=$1; ...' - {} \;`.
ephemient
A: 

correction to find-exec approach by gregb (adding quotes):

# compare 

bash -c '
   echo $'a\nb\nc'
'

bash -c '
   echo $'"'a\nb\nc'"'
'

# therefore we need

find . -type f -exec bash -c ' 
...
ed -s "${path}" <<< $'"'H\ng/foo/s/foo/bar/g\nwq'"'; 
...
' \;
mpage