开发者

Shell script to rename multiple files and folders

开发者 https://www.devze.com 2023-04-09 01:07 出处:网络
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

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:

  1. Replace keywords/patterns within file or directory names.
  2. 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
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号