Shell script to rename multiple files and folders
I have the following structure
Before
-1.2.3.4
--1.2.3.4
---1.2.3.4-->1.2.3.4.file
After
-5.6.7.8
--5.6.7.8
---5.6.7.8-->5.6.7.8.file
...
I would like to use a bash script that searches files and subdirectories name in a specific directory /home for a specific string, and when it finds the search string (in file or
name, file content, subdirectories name), replaces the string (old1) with new string (new1), and so on old2 with new2 ....oldn with newn.#!/bin/bash
FILES_PATH="/home/pons/test"
FILES=$(find $FILES_PATH -type f -name "*")
for FILE in $FILES; do
sed -i 's/old1/new1/ ; .... ; s/oldn/newn/' $FILE
done
e.g. replace fadi to server in all
[pons@server1 ~]$ pwd
/home/pons
[pons@server1 ~]$ cd test
[pons@server1 test]$ ls -al
total 12
drwxrwxr-x 3 pons pons 4096 Sep 29 09:50 .
drwx------. 26 pons pons 4096 Sep 29 10:21 ..
drwxrwxr-x 2 pons pons 4096 Sep 29 09:47 fadi
[pons@server1 test]$ cd fadi/
[pons@server1 fadi]$ pwd
/home/pons/test/fadi
[pons@server1 fadi]$ ls -al
total 12<br/>
drwxrwxr-x 2 pons pons 4096 Sep 29 09:47 .
drwxrwxr-x 3 pons pons 4096 Sep 29 09:50 ..
-rw-rw-r-- 1 pons开发者_运维知识库 pons 27 Sep 29 09:47 xxxfadixx.txt
[pons@server1 fadi]$ cat xxxfadixx.txt
xxxxxxxxxxxxfadixxxxxxxxxx
[pons@server1 fadi]$
Can I add multiple string in one file and do the replace
e.g. create a file that specify filtering routines: filter.txt
s/old1/new1/ s/old2/new2/ s/old3/new3/ ...... s/oldn/newn/and then read that file in the script
#!/bin/bash
ROOT_DIR="$HOME/root" # your target dir
P_FROM="1.2.3.4"
P_TO="5.6.7.8"
# for each directory, starting from deepest first
while IFS= read -r -d $'\0' DIR_NAME; do
cd "$DIR_NAME" # go to target dir
rename $P_FROM $P_TO * # rename files
find . -type f -maxdepth 1 -exec sed -i "s/$P_FROM/$P_TO/g" {} \;
cd - > /dev/null # back to original dir. Suppress stdout
done < <(find $ROOT_DIR -type d -depth -print0) # get only dirs
This above script from Shawn Chin worked perfectly replacing/renaming strings, how can I modify it if I have multiple string to be modified and these strings allocated in a file
filter.txt s/old1/new1/ s/old2/new2/ s/old3/new3/ ...... s/oldn/newn/#!/bin/bash
ROOT_DIR="$HOME/test" # your target dir
FILTER_FILE="$HOME/filter.sed" # the sed script for renaming
# custom rename function that uses $FILTER_FILE (via sed)
function rename_using_filter {
CURRENT_NAME="$1"
NEW_NAME="$(echo $1 | sed -f $FILTER_FILE)" # derive new name
if [ "$CURRENT_NAME" != "$NEW_NAME" ]; then # rename if diff
mv "$CURRENT_NAME" "$NEW_NAME"
fi
}
# for each directory, starting from deepest first
while IFS= read -r -d $'\0' DIR_NAME; do
cd "$DIR_NAME" # go to target dir
# for each file/dir at this level
while IFS= read -r -d $'\0' FILE_NAME; do
rename_using_filter "$FILE_NAME" # rename it
if [ -f "$FILE_NAME" ]; then # if it's a file
sed -i -f "$FILTER_FILE" "$FILE_NAME"; # replace content
fi
done < <(find . -maxdepth 1 -print0)
cd - > /dev/null # back to original dir. Suppress stdout
done < <(find $ROOT_DIR -depth -type d -print0) # get only dirs
I did the above with filter.sed, it works replacing name files/directories but didnt replace the content of the file... can you check it please.
[pons@server1 test]$ find .
.
./boy_dir
./boy_dir/boy.txt
./papa
./papa/papa_new
./papa/papa_new/papa.txt
./boy
[pons@server1 test]$ cat ./papa/papa_new/papa.txt
xxxxxxxxxxxxxxxxxxxxpapaxxxxxxxxxxxx
[pons@server1 test]$ cat ./boy_dir/boy.txt
xxxxxxxxxxxxboyxxxxxxxxx
[pons@server1 test]$ cd ..
[pons@server1 ~]$ ./replace.sh
[pons@server1 ~]$ cd test
[pons@server1 test]$ find .
.
./girl_dir
./girl_dir/girl.txt
./girl
./mama
./mama/mama_new
./mama/mama_new/mama.txt
[pons@server1 test]$ cat ./mama/mama_new/mama.txt
xxxxxxxxxxxxxxxxxxxxpapaxxxxxxxxxxxx
[pons@server1 test]$ cat ./girl_dir/girl.txt
xxxxxxxxxxxxboyxxxxxxxxx
[pons@server1 test]$
I used this filter
[pons@server1 ~]$ cat filter.sed
s/papa/mama/g;
s/boy/girl/g;
s/old3/new3/g;
s/old3/new4/g;
s/oldn/newn/g;
"filter.sed" 5L, 95C written
[pons@server1 ~]$ ./replace.sh
[pons@server1 ~]$ cd test
[pons@server1 test]$ find .
.
./A.B.C.D.E_
./a.b.c.d.e
./a.b.c.d.e/a.b.c.d.e
./a.b.c.d.e_
./A.B.C.D.E
./A.B.C.D.E/A.B.C.D.E
[pons@server1 test]$ cat ./A.B.C.D.E/A.B.C.D.E
1.2.3.4.5
[pons@server1 test]$ cat ./a.b.c.d.e/a.b.c.d.e
6.7.8.9.0
[pons@server1 test]$ cat ./A.B.C.D.E_
1.2.3.4.5
[pons@server1 test]$ cat ./a.b.c.d.e_
6.7.8.9.0
[pons@server1 test]$ cd ..
[pons@server1 ~]$ ./replace.sh
[pons@server1 ~]$ cd test
[pons@server1 test]$ find .
.
./A.B.C.D.E_
./a.b.c.d.e
./a.b.c.d.e/a.b.c.d.e
./a.b.c.d.e_
./A.B.C.D.E
./A.B.C.D.E/A.B.C.D.E
[pons@server1 test]$ cat ./A.B.C.D.E/A.B.C.D.E
A.B.C.D.E
[pons@server1 test]$ cat ./a.b.c.d.e/a.b.c.d.e
a.b.c.d.e
[pons@server1 test]$ cat ./A.B.C.D.E_
A.B.C.D.E
[pons@server1 test]$ cat ./a.b.c.d.e_
a.b.c.d.e
[pons@server1 test]$
[pons@server1 ~]$ vi filter.sed
s/1.2.3.4.5/A.B.C.D.E/g;
s/6.7.8.9.0/a.b.c.d.e/g;
s/old3/new3/g;
s/old3/new4/g;
s/oldn/newn/g;
~
From what I gather, you want to traverse a directory structure and do the following:
- Replace keywords/patterns within file or directory names.
- Replace occurrences of the keywords/patterns within the content of files.
Here's how I would do it.
Warning: long answer. If you're in a hurry, jump straight to the last section for complete script. Might also be over-engineer (there should be an easier way to do this, really...).
Assuming the following file structure (based on your first example):
[me@home]$ find .
.
./1.2.3.4
./1.2.3.4/1.2.3.4
./1.2.3.4/1.2.3.4/1.2.3.4
./1.2.3.4/1.2.3.4/1.2.3.4/1.2.3.4.file
./1.2.3.4/1.2.3.4/1.2.3.4/1.2.3.4-2.file
[me@home]$ cat ./1.2.3.4/1.2.3.4/1.2.3.4/1.2.3.4.file
IP: 1.2.3.4
[me@home]$ cat ./1.2.3.4/1.2.3.4/1.2.3.4/1.2.3.4-2.file
IP: 1.2.3.4 (also)
Replacing patterns within files
This is the easy bit. One can do that quite simply using find
and sed
[me@home]$ find . -type f -exec sed -i "s/1\.2\.3\.4/5.6.7.8/g" {} \;
[me@home]$ cat ./1.2.3.4/1.2.3.4/1.2.3.4/1.2.3.4.file
IP: 5.6.7.8
[me@home]$ cat ./1.2.3.4/1.2.3.4/1.2.3.4/1.2.3.4-2.file
IP: 5.6.7.8 (also)
Recursively renaming files/dirs
I first assumed it would be as simple as find . -exec rename 1.2.3.4 5.6.7.8 {} \;
.
Unfortunately, that does not work. Prepending the command with an echo
shows why it won't work.
[me@home]$ find . -exec echo rename 1.2.3.4 5.6.7.8 {} \;
rename 1.2.3.4 5.6.7.8 .
rename 1.2.3.4 5.6.7.8 ./1.2.3.4
rename 1.2.3.4 5.6.7.8 ./1.2.3.4/1.2.3.4
rename 1.2.3.4 5.6.7.8 ./1.2.3.4/1.2.3.4/1.2.3.4
rename 1.2.3.4 5.6.7.8 ./1.2.3.4/1.2.3.4/1.2.3.4/1.2.3.4.file
rename 1.2.3.4 5.6.7.8 ./1.2.3.4/1.2.3.4/1.2.3.4/1.2.3.4-2.file
The renaming of the top-level directory (./1.2.3.4
) will succeed, but once we go down a level to rename ./1.2.3.4/1.2.3.4
this will fail because the parent directory has already been renamed.
We can reverse the order of traversal (using -depth
) but that only solves half the problem since we need each step to rename only the lowest-level match and not the whole path.
What we therefore have to do is start the renaming process from the lowest level and work upwards. Here's one way to do it:
#!/bin/bash
ROOT_DIR="$HOME/root" # your target dir
P_FROM="1.2.3.4"
P_TO="5.6.7.8"
# for each directory, starting from deepest first
while IFS= read -r -d $'\0' DIR_NAME; do
(cd "$DIR_NAME" && rename $P_FROM $P_TO *)
done < <(find $ROOT_DIR -depth -type d -print0) # get only dirs
The complicated-looking while
loop allows us to handle filenames that contain spaces (see http://mywiki.wooledge.org/BashFAQ/020 for details).
This script assumes the rename
command exists.
Running that script on our test directory gives the following results:
[me@home]$ bash renamer.sh
[me@home]$ find .
.
./5.6.7.8
./5.6.7.8/5.6.7.8
./5.6.7.8/5.6.7.8/5.6.7.8
./5.6.7.8/5.6.7.8/5.6.7.8/5.6.7.8.file
./5.6.7.8/5.6.7.8/5.6.7.8/5.6.7.8-2.file
Combining both steps into one script
We can combine both the steps above into one solution by simple adding the find
+sed
call into the loop.
#!/bin/bash
ROOT_DIR="$HOME/root" # your target dir
P_FROM="1.2.3.4"
P_TO="5.6.7.8"
# for each directory, starting from deepest first
while IFS= read -r -d $'\0' DIR_NAME; do
cd "$DIR_NAME" # go to target dir
rename $P_FROM $P_TO * # rename files
find . -type f -maxdepth 1 -exec sed -i "s/$P_FROM/$P_TO/g" {} \;
cd - > /dev/null # back to original dir. Suppress stdout
done < <(find $ROOT_DIR -depth -type d -print0) # get only dirs
Update (renaming and rewriting files based on external sed file)
Assuming you have a sed file filter.sed
:
s/old1/new1/g;
s/old2/new2/g;
s/old3/new3/g;
s/old3/new4/g;
s/oldn/newn/g;
Replacing contents of a file is simple as long as filter.sed
is a valid sed file. We simply use the -f
option:
[me@home]$ cat somefile.txt
hello old1 old2 old3 world
[me@home]$ sed -i -f filter.sed somefile.txt
[me@home]$ cat somefile.txt
hello new1 new2 new3 world
However, renaming files/dirs however is trickier (unless you use the Perl-based rename command available only on debian-based systems. With this you may be able to do something along the lines of rename "$(cat filter.sed)" *
).
Using only the standard mv
command, here's one way to do it. I suspect this may not the most efficient approach, but after briefly testing it, it appears to work. YMMV.
#!/bin/bash
ROOT_DIR="$HOME/root" # your target dir
FILTER_FILE="$HOME/filter.sed" # the sed script for renaming
# custom rename function that uses $FILTER_FILE (via sed)
function rename_using_filter {
CURRENT_NAME="$1"
NEW_NAME="$(echo $1 | sed -f $FILTER_FILE)" # derive new name
if [ "$CURRENT_NAME" != "$NEW_NAME" ]; then # rename if diff
mv "$CURRENT_NAME" "$NEW_NAME"
fi
}
# for each directory, starting from deepest first
while IFS= read -r -d $'\0' DIR_NAME; do
cd "$DIR_NAME" # go to target dir
# for each file/dir at this level
while IFS= read -r -d $'\0' FILE_NAME; do
if [ -f "$FILE_NAME" ]; then # if it's a file
sed -i -f "$FILTER_FILE" "$FILE_NAME" # replace content
fi
rename_using_filter "$FILE_NAME" # rename it
done < <(find . -maxdepth 1 -print0)
cd - > /dev/null # back to original dir. Suppress stdout
done < <(find $ROOT_DIR -depth -type d -print0) # get only dirs
Here's the script in action:
[me@home]$ find .
.
./old1_old2
./old1_old2/old3_old4
./old1_old2/old3_old4/some-oldn.txt
./renamr.sh
[me@home]$ cat old1_old2/old3_old4/some-oldn.txt
hello old1 old2 old3 old4 oldn world
[me@home]$ bash renamr.sh
[me@home]$ find .
.
./.renamr.sh.swp
./new1_new2
./new1_new2/new3_new4
./new1_new2/new3_new4/some-newn.txt
./renamr.sh
[me@home]$ cat ./new1_new2/new3_new4/some-newn.txt
hello new1 new2 new3 new4 newn world
You can run rename
(yes, that's an existing program) for each replacement. For example, in Bash >=4.0, here's how to remove a ".bak" extension from all files within the current directory:
shopt -s globstar # Enable recursive globs with **
rename 's/\.bak$//' **.bak
Another example, to do several replacements:
$ touch /tmp/foobarbaz
$ rename 's/foo/bar/g;s/bar/baz/g' /tmp/foobarbaz
$ ls /tmp
bazbazbaz
For your specific replacements, this should work:
rename 's/old1/new1/ ; .... ; s/oldn/newn/' /home/pons/test/**
I didn't completely understand what you want to achieve, but I think this will at least fix your code:
mv "$FILE" "$(echo "$FILE" | sed -i 's/old1/new1/ ; .... ; s/oldn/newn/')"
- Sed works on standard input or file content, not argument text or file names or anything like that.
- When reading output of find, do not store it all in variable, but read it line by line.
So you want to do something like:
find $FILES_PATH -type f | while read FILE; do
NEWNAME="$(printf '%s\n' "$FILE" | sed 's/old1/new1/ ; .... ; s/oldn/newn/')"
mv "$FILE" "$NEWNAME"
done
Note, that you do not want -i
for sed, stdin can't be modified in place.
It can be more efficient by having sed output both names, so you can run it just once on the whole output, but with multiple substitutions that requires using the hold space. It goes like:
find $FILES_PATH -type f | sed 'h;s/old1/new1/;...;s/oldn/newn/;x;G' | while read OLD && read NEW; do
mv "$OLD" "$NEW"
done
精彩评论