2

I have several directories through which I want to recursively iterate over and change the file name of

*.GEOT14246.*

to

*.GEOT15000.*

Is there a one liner on the bash script to do this or do I have to the write a script with a for loop. My try (which doesn't work) is below:

#!/bin/bash

for file in `find . -type f -name "*.GEOT14246.*"`  
do
    echo "file = $file"
    mv $file *.GEOT15000.*
done

I then call with:

cd /path/to/script/
sh ./script1.sh /path/to/starting/dir

Obviously this doesn't work because I don't think I'm passing the starting directory path as an argument. I'm very new to Unix, so how can I pass arguments to it to say from which directory it should start searching and how do I get the script to work?

1

3 Answers 3

3

This should work:

find . -type f -name "*.GEOT14246.*" -print0 | \
xargs -0 rename 's/GEOT14246/GEOT15000/'

given there is not directories named *.GEOT14246.*


A bash variant using find could be something like:

while IFS= read -r -d $'\0' file; do
    printf "MV: %-40s => %s\n" "$file" "${file/GEOT14246/GEOT15000}"
    mv "$file" "${file/GEOT14246/GEOT15000}"
done < <(find . -type f -name "*.GEOT14246.*" -print0)
              |                                 |
              +--- this is starting directory   +--- This ensures no hiccup
                                                     if newline etc. in name

Relative, but full, paths are passed from find – which you should see from the printf statement.

The new name is compiled by using bash:

${some_variable/find/replace}

to replace all find's use:

${some_variable//find/replace}

etc. More here. This could also be a good read: BashPitfalls, UsingFind.

Read some guides like BashGuide. Find some tutorials on-line, but usually never trust them, ask here or on irc.freenode.net #bash.


You do not need to invoke the script by calling sh. That would also call Bourne shell (sh) and not Bourne Again shell (bash). If you intend to run it with bash; issue bash file. .sh extension is also out of place.

What you normally do is make the file executable by:

chmod +x script_file

and then run it with:

./script_file

The shebang takes care of what environment the script should run in.


In your script you do not use the passed path-name anywhere. The script has a "argument list" starting from $0 which is the script name, $1 first argument, $2 second, - and so on.

In your script you would do something in the direction of:

# Check if argument 1 is a directory, if not exit with error
if ! [[ -d "$1" ]]; then
    printf "Usage: %s [PATH]\n" "$(basename "$0")" >&2
    exit 1
fi

# OK
path="$1"

while ...

done < <(find "$path" -type f ...)

Your current mv statement would move all files to wherever you issue the command – to one file named .GEOT14246. (as in overwrite for each mv statement):

Before script run:

$ tree
.
└── d1
    ├── a1
    │   ├── a2
    │   │   ├── a3
    │   │   │   └── foo.GEOT14246.faa
    │   │   └── foo.GEOT14246.faa
    │   └── foo.GEOT14246.faa
    └── b1
        ├── b2
        │   ├── b3
        │   │   └── foo.GEOT14246.faa
        │   └── foo.GEOT14246.faa
        └── foo.GEOT14246.faa

After script run:

$ tree
.
├── d1
│   ├── a1
│   │   └── a2
│   │       └── a3
│   └── b1
│       └── b2
│           └── b3
└── *.GEOT15000.*

Also files / paths with spaces or other funny things would make the script go bad and spread havoc. Therefor you have to quote your variables (such as "$file").

2

In bash 4.0 or later you could also use globstar:

shopt -s globstar
for f in **/*GEOT14246*; do mv "$f" "$f{/14246/15000}"; done
rename s/14246/15000/ **/*GEOT14246*
  • The glob pattern also matches folders that contain GEOT14246
  • Both options apply to the whole path
  • There can be an argument list too long error if the glob pattern is expanded to be longer than getconf ARG_MAX

Or using find and read:

find . -name '*GEOT14246*' | while read f; do mv "$f" "${f/14246/15000}"; done
  • -print0 is not needed if the paths don't contain linefeeds
  • IFS= is not needed if the paths don't start or end with characters in IFS
  • -r is not needed if the paths don't contain backslashes
  • < <() is not needed if you don't modify variables outside the while loop or something
1

There isn't a simple one-liner in bash, but there is one in zsh. Add the line autoload -U zmv to your ~/.zshrc. Then you can use zmv and zsh's wildcards and parameter substitution.

zmv '/path/to/starting/dir/**/*GEOT14246*' '${f//GEOT14246/GEOT15000}'

Note that this replaces GEOT14246 in file names as well. You need the quotes around both arguments because zmv parses them again.

On Linux, with bash or zsh, you can use rename instead. This replaces only the first occurrence of GEOT14246 in each path.

rename GEOT14246 GEOT15000 /path/to/starting/dir/**/*GEOT14246*

In bash, you need to enable the ** recursive glob first with shopt -s globstar (add this line to your ~/.bashrc).

If your Linux distribution is Debian, Ubuntu or a derivative, the rename command is a different one. Use either of these:

rename.ul GEOT14246 GEOT15000 /path/to/starting/dir/**/*GEOT14246*
rename 's/GEOT14246/GEOT15000/g' /path/to/starting/dir/**/*GEOT14246*

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .