2

This Q and A will cover a very specific yet common case: given many files with the same or similar name each in its own directory, create a copy of each of them in their original directory but with another name.

You might wonder which rare case would require such operation. It's not so rare. Some emerging software, expecially those based on the MVC (Model / View / Controller) design pattern may require the programmer to implement the various components each in a different directory.

Example:

software root
|
|-- Model => MyCustomComponent.php
|
|-- View => MyCustomComponent.php
|
|-- Controller => MyCustomComponent.php
|
\-- Translations
         |
         |-- English => MyCustomComponent.php
         |
         |-- French => MyCustomComponent.php
         |
         |-- Italian => MyCustomComponent.php
         |
         |-- Spanish => MyCustomComponent.php
         |

An example of a quite famous software implementing such structure would be the popular e-commerce application OpenCart. But there are many others.

A common practice is to create new components by copying an existing one or a core one, apply variations and save them. In case inheritance and similar can't help us, we end up having to duplicate 10-20 files. An utterly boring and error prone process.

This is a typical example: from a bank transfer script implement a direct debit script by using the former as "boilerplate code" (don't bash me, I did not create this architecture). Between translations, Model, View and Controller management and others, the number of files to copy each in its deep directory quickly becomes cumbersome.

Lots of same name files, one per directory

1 Answer 1

1

This little script comes to help exactly in this situation.

Given a starting directory, a file name pattern, an original file name portion to change and the destination file name portion to use, it will:

  • Deep copy all the matching files preserving attributes over new ones (copy)

or

  • Deep rename all the matching files to the new names (rename)

or

  • Just show a preview list of the potentially affected files (preview).

The last option is expecially useful because selecting the right match that covers a mass of files with names variations or extensions variations may quicky become a challenge. Avoid getting a complete mess by previewing the changes before applying them!

The script is very simple and has NO pretense about being the best, the quickest, the cleanest, the safest. It's what works for me and I have decided to share it in hope somebody else will take benefit.

If you start it with no parameters or a wrong command, it will show some brief instructions:

./copy_multi_files.sh

Usage:
./copy_multi_files.sh command path name_pattern src dest
If command is 'copy' then the script copies src to dest
If command is 'rename' then the script renames src to dest
If command is 'preview' then the script just shows the matched files
Inside path, for every file name matching name_pattern, it copies every src file to dest file


Parameters:

  • Command: it may be one of copy, rename or preview. Copy performs the recursive deep copy onto new files, rename just mass renames existing files, preview just shows which files will be affected and how.

  • Path: it's the starting directory. I suggest doing experiments on small directories to get confidence with the script before trying on your 4 peta-trillion entries file system.

  • Name_pattern: this decides which files shall be affected, so it's important to pick a sensible value. In example, if I wanted to select a lot of scripts whose name starts with "sendmail" I could specify s* for this parameter. For those who know about shell commands, this pattern is internally passed to find.

  • src: this is the portion, a template of file name to replace with dest when forming the copied files names.

  • dest: this is what replaces src in the new copied files names.

A bit of sanity check is performed so that name_pattern and src produce consistent results. That is, only files that match name_pattern and also contain src are selected.

Confusing? Don't worry, I have prepared some examples and "gotchas".


Simple, one folder example:

We have some files in a sub-directory called: apps/res/

We want to copy all the files whose name contains the word: sendmail into new files that in place of sendmail have newthing.

Here is a partial list of apps/res/ contents:

robot.php
sendmail_test.php
sendmail_test2.php
sendmail_test3.php
showinfo.php
show_server_vars.php
version.php

We may issue the following command:

./copy_multi_files.sh preview apps/res/ s* sendmail newthing

This command line tells us that we just want a preview; the starting directory is apps/res/ (that is, it's under our current directory), that we are going to search for all files beginning with s (s*). Among the found files, we are going to pick only those whose name contains sendmail and copy them to files residing in the same directories but with sendmail replaced by newthing.

Here is the output of this preview:

apps/res/sendmail_test.php => apps/res/newthing_test.php
apps/res/sendmail_test2.php => apps/res/newthing_test2.php
apps/res/sendmail_test3.php => apps/res/newthing_test3.php

You see, even if we specified s* as search pattern, the script was "smart" enough to just pick the subset of "beginning by s" files that also contains "sendmail".

Let's run the command:

./copy_multi_files.sh copy apps/res/ s* sendmail newthing

ls apps/res/ will show how now we have both the original files but also the same files with newthing. If we used "rename" in the line above, we'd only have the newthing files.

newthing_test.php
newthing_test2.php
newthing_test3.php
robot.php
sendmail_test.php
sendmail_test2.php
sendmail_test3.php
showinfo.php
show_server_vars.php
version.php

Let's change the files again. This time we rename them:

./copy_multi_files.sh rename apps/res/ new* new even_newer_

Notice the values used for the various parameters. The directory now is the following:

even_newer_thing_test.php
even_newer_thing_test2.php
even_newer_thing_test3.php
robot.php
sendmail_test.php
sendmail_test2.php
sendmail_test3.php
showinfo.php
show_server_vars.php
version.php


More complex example: let's move to OpenCart's top install folder and return to the the bank transfer MVC files. We need to create copies of those files, each in its respective directory. They will be used to implement a direct debit payment module. Here's the command line and an extract of the output:

./copy_multi_files.sh preview . ban* bank_transfer.* direct_debit

./catalog/view/language/russian/payment/bank_transfer.php => ./catalog/view/language/russian/payment/direct_debit
./catalog/view/language/czech/payment/bank_transfer.php => ./catalog/view/language/czech/payment/direct_debit
./catalog/view/theme/default/template/payment/bank_transfer.tpl => ./catalog/view/theme/default/template/payment/direct_debit


Oops! Something went wrong! Lucky us, for having used the preview. What's wrong in the above command? The "src" argument is the wrong one. In fact, bank_transfer.* is internally passed to a direct "search and replace" function (sed). It's not expanded with a "file globbing" function but as regular expression. Therefore all the files in the preview are transformed into: direct_debit.


Now that we know about this gotcha, we can rewrite the command line to achieve the desired result:

/var/www/copy_multi_files.sh preview . ban* bank_transfer direct_debit

./catalog/view/language/russian/payment/bank_transfer.php => ./catalog/view/language/russian/payment/direct_debit.php
./catalog/view/language/czech/payment/bank_transfer.php => ./catalog/view/language/czech/payment/direct_debit.php
./catalog/view/theme/default/template/payment/bank_transfer.tpl => ./catalog/view/theme/default/template/payment/direct_debit.tpl

Now it works! Now extensions are preserved, even when they change (.php and .tpl).

Follows the little script source code. I strongly suggest to save backups before doing "live" renames and to always use the preview functionality. Remember, this is a recursive Unix script, its effects may be extremely nasty and irreversible!

Source code:

#!/bin/sh

if [ "$#" -eq 0 ] || ( [ "$1" != "preview" ] && [ "$1" != "copy" ] && [ "$1" != "rename" ] )
        then
        echo "Usage:"
        echo "$0 command path name_pattern src dest"
        echo "If command is 'copy' then the script copies src to dest"
        echo "If command is 'rename' then the script renames src to dest"
        echo "If command is 'preview' then the script just shows the matched files"
        echo "Inside path, for every file name matching name_pattern, it copies every src file to dest file"
        echo
        exit 1
fi

for f in $(find "$2" -type f -name "$3" -name "*$4*" )
do
        z=`echo "$f" | sed s/"$4"/"$5"/`

        case "$1" in
                "preview") echo "$f => $z"
                ;;
                "copy") cp -p "$f" "$z"
                ;;
                "rename") mv "$f" "$z"
                ;;
        esac
done

You must log in to answer this question.

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