Go into every subdirectory and mass rename files by stripping leading characters

Question

From the current directory I have multiple sub directories:

subdir1/
 001myfile001A.txt
 002myfile002A.txt
subdir2/
 001myfile001B.txt
 002myfile002B.txt

where I want to strip every character from the filenames before myfile so I end up with

subdir1/
 myfile001A.txt
 myfile002A.txt
subdir2/
 myfile001B.txt
 myfile002B.txt

I have some code to do this...

#!/bin/bash
for d in `find . -type d -maxdepth 1`; do
   cd "$d"
   for f in `find . "*.txt"`; do
        mv "$f" "$(echo "$f" | sed -r 's/^.*myfile/myfile/')"
   done

done

however the newly renamed files end up in the parent directory

i.e.

 myfile001A.txt
 myfile002A.txt
 myfile001B.txt
 myfile002B.txt
 subdir1/
 subdir2/

In which the sub-directories are now empty.

How do I alter my script to rename the files and keep them in their respective sub-directories? As you can see the first loop changes directory to the sub directory so not sure why the files end up getting sent up a directory...


Show source
| find   | bash   | file   | rename   2017-01-04 23:01 4 Answers

Answers to Go into every subdirectory and mass rename files by stripping leading characters ( 4 )

  1. 2017-01-05 00:01

    You can do it without find and sed:

    $ for f in */*.txt; do echo mv "$f" "${f/\/*myfile/\/myfile}"; done
    mv subdir1/001myfile001A.txt subdir1/myfile001A.txt
    mv subdir1/002myfile002A.txt subdir1/myfile002A.txt
    mv subdir2/001myfile001B.txt subdir2/myfile001B.txt
    mv subdir2/002myfile002B.txt subdir2/myfile002B.txt
    

    If you remove the echo, it'll actually rename the files.

    This uses shell parameter expansion to replace a slash and anything up to myfile with just a slash and myfile.

    Notice that this breaks if there is more than one level of subdirectories. In that case, you could use extended pattern matching (enabled with shopt -s extglob) and the globstar shell option (shopt -s globstar):

    $ for f in **/*.txt; do echo mv "$f" "${f/\/*([!\/])myfile/\/myfile}"; done
    mv subdir1/001myfile001A.txt subdir1/myfile001A.txt
    mv subdir1/002myfile002A.txt subdir1/myfile002A.txt
    mv subdir1/subdir3/001myfile001A.txt subdir1/subdir3/myfile001A.txt
    mv subdir1/subdir3/002myfile002A.txt subdir1/subdir3/myfile002A.txt
    mv subdir2/001myfile001B.txt subdir2/myfile001B.txt
    mv subdir2/002myfile002B.txt subdir2/myfile002B.txt
    

    This uses the *([!\/]) pattern ("zero or more characters that are not a forward slash"). The slash has to be escaped in the bracket expression because we're still inside of the pattern part of the ${parameter/pattern/string} expansion.

  2. 2017-01-05 00:01

    a slight modification should fix your problem:

    #!/bin/bash
    for f in `find . -maxdepth 2 -name "*.txt"`; do
      mv "$f" "$(echo "$f" | sed -r 's,[^/]+(myfile),\1,')"
    done
    

    note: this sed uses , instead of / as the delimiter.

    however, there are much faster ways.

    here is with the rename utility, available or easily installed wherever there is bash and perl:

    find . -maxdepth 2 -name "*.txt" | rename 's,[^/]+(myfile),/$1,'
    

    here are tests on 1000 files:

    for `find`; do mv        9.176s
    rename                   0.099s
    

    that's 100x as fast.

    John Bollinger's accepted answer is twice as fast as the OPs, but 50x as slow as this rename solution:

    for|for|mv "$f" "${f//}"   4.316s
    

    also, it won't work if there is a directory with too many items for a shell glob. likewise any answers that use for f in *.txt or for f in */*.txt or find * or rename ... subdir*/*. answers that begin with find ., on the other hand, will also work on directories with any number of items.

  3. 2017-01-05 00:01

    Maybe you want to use the following command instead:

    rename 's#(.*/).*(myfile.*)#$1$2#' subdir*/*
    

    You can use rename -n ... to check the outcome without actually renaming anything.

    Regarding your actual question:
    The find command from the outer loop returns 3 (!) directories:

    .
    ./subdir1
    ./subdir2
    

    The unwanted . is the reason why all files end up in the parent directory (that is .). You can exclude . by using the option -mindepth 1. Unfortunately, this was onyl the reason for the files landing in the wrong place, but not the only problem. Since you already accepted one of the answers, there is no need to list them all.

  4. 2017-01-05 00:01

    Your script has multiple problems. In the first place, your outer find command doesn't do quite what you expect: it outputs not only each of the subdirectories, but also the search root, ., which is itself a directory. You could have discovered this by running the command manually, among other ways. You don't really need to use find for this, but supposing that you do use it, this would be better:

    for d in $(find * -maxdepth 0 -type d); do
    

    Moreover, . is the first result of your original find command, and your problems continue there. Your initial cd is without meaningful effect, because you're just changing to the same directory you're already in. The find command in the inner loop is rooted there, and descends into both subdirectories. The path information for each file you choose to rename is therefore stripped by sed, which is why the results end up in the initial working directory (./subdir1/001myfile001A.txt --> myfile001A.txt). By the time you process the subdirectories, there are no files left in them to rename.

    But that's not all: the find command in your inner loop is incorrect. Because you do not specify an option before it, find interprets "*.txt" as designating a second search root, in addition to .. You presumably wanted to use -name "*.txt" to filter the find results; without it, find outputs the name of every file in the tree. Presumably you're suppressing or ignoring the error messages that result.

    But supposing that your subdirectories have no subdirectories of their own, as shown, and that you aren't concerned with dotfiles, even this corrected version ...

    for f in `find . -name "*.txt"`;
    

    ... is an awfully heavyweight way of saying this ...

    for f in *.txt;
    

    ... or even this ...

    for f in *?myfile*.txt;
    

    ... the latter of which will avoid attempts to rename any files whose names do not, in fact, change.

    Furthermore, launching a sed process for each file name is pretty wasteful and expensive when you could just use bash's built-in substitution feature:

        mv "$f" "${f/#*myfile/myfile}"
    

    And you will find also that your working directory gets messed up. The working directory is a characteristic of the overall shell environment, so it does not automatically reset on each loop iteration. You'll need to handle that manually in some way. pushd / popd would do that, as would running the outer loop's body in a subshell.

    Overall, this will do the trick:

    #!/bin/bash
    
    for d in $(find * -maxdepth 0 -type d); do
       pushd "$d"
       for f in *.txt; do
            mv "$f" "${f/#*myfile/myfile}"
       done
       popd
    done
    

Leave a reply to - Go into every subdirectory and mass rename files by stripping leading characters

◀ Go back