I need shell script which prepends some string into the file-name (.c files) present in a directory and sub-directory.

For Example: If lokesh is parent directory and inside it lokesh1 and lokesh2 is the subdirectory and inside lokesh1 (1.c , 2.c ...) and inside lokesh2(a.c , b.c ...). Hence after running the script I want to change it to (x_1.c, x_2.c ... ) and (x_a.c, x_b.c ...).

  • Larry Wall's rename could be useful. (never tried it recursive, but you could combine with find) – ctrl-alt-delor Apr 6 '18 at 15:48
  • @ctrl-alt-delor I tend to stay away from rename, especially in answers here, as there are at least two different utilities with that name. It would probably do the job though. – Kusalananda Apr 6 '18 at 15:50
  • Yes I should have been clearer, Larry Wall's version (of rename) not any other one. There are at least two. – ctrl-alt-delor Apr 6 '18 at 15:52
  • @ctrl-alt-delor Ah, I'm getting older and my hearing is failing me. ;-) – Kusalananda Apr 6 '18 at 15:58
  • This all i have tried its not working please suggest some different script. – Lokesh Kumar Apr 9 '18 at 9:01

NOTE: When writing the below, I was using OpenBSD find with which -execdir utility {} ';' will replace {} with the basename of the found files. With GNU find, the {} will also be the basename of the found file, but it will additionally be prepended by ./, which renders the first find command below useless. For users of GNU find (most people on Linux, for example), scroll down to the other variations of the solution.

You want to find all .c files in or beneath the lokesh directory and prepend their names with x_.

find lokesh -type f -name '*.c' -execdir echo mv {} x_{} ';'

The -type f and -name '*.c' expressions will find all the relevant files while -execdir mv {} x_{} ';' will rename the found files.

The -execdir expression is not standard, but most implementations of find supports it. It differs from -exec in that the given utility is executed with the parent directory of the found pathname as the working directory. The {} in the command line will therefore be the basename of the files that we want to rename (not the complete pathname as with -exec).

Run this once, and then remove the echo when you have seen that it does the correct thing. The echo will prevent the mv from actually renaming the files.

On systems which doesn't support -execdir (or which doesn't do -execdir like OpenBSD does):

find lokesh -type f -name '*.c' \
    -exec sh -c 'for name do echo mv "$name" "${name%/*}/x_${name##*/}"; done' sh {} +

or, shorter but ever so slightly less efficient,

find lokesh -type f -name '*.c' \
    -exec sh -c 'echo mv "$1" "${1%/*}/x_${1##*/}"' sh {} ';'

Both of these variation employ a short shell script that basically does the same thing in the end:

mv "$name" "${name%/*}/x_${name##*/}"

This moves the file given its full pathname in $name to a new name that is prefixed by x_ in the same directory. The ${name%/*} parameter substitution is equivalent to $( dirname "$name" ) and will give parent directory of the pathname, while ${name##*/} is equivalent to $( basename "$name" ) which will give the basename (the filename component) of the pathname.

  • Thanks for you script but its not working please suggest some different script. – Lokesh Kumar Apr 9 '18 at 9:01
  • @LokeshKumar Ah, I see now. There's a typo in my solution (which I did not have while testing). Fixed now. I was missing -c after sh in the find commands. – Kusalananda Apr 9 '18 at 9:06
  • yes this script worked for me: find . -type f -name '.c' \ -exec sh -c ' mv "$1" "${1%/}/x_${1##*/}"' sh {} ';' – Lokesh Kumar Apr 9 '18 at 9:09
  • Thanks for your help, but kindly could you please explain the whole part of script. find . -type f -name '.c' \ -exec sh -c ' mv "$1" "${1%/}/x_${1##*/}"' sh {} ';' – Lokesh Kumar Apr 9 '18 at 9:10
  • @LokeshKumar The mv part is already explained in the answer. The -exec makes find call sh -c which executes the mv. This happens for the files that match *.c. Which part exactly do you not understand? – Kusalananda Apr 9 '18 at 9:13

You can use Perl rename, if available. By Perl rename, I mean either prename or file-rename, and not rename.ul which might be what rename points to your system, and which will definitely error out if you try to use it with the code arguments I have suggested below...

On my system (Ubuntu 17.10 - I installed the rename package to get Perl rename):

$ readlink -e $(type -P rename)

This is the only environment in which I have tested this answer!

If you have only two directory levels as in your example, you could run from the lokesh directory something like this:

rename -n 's|/|/x_|' */*.c

Always run first with the -n flag, which shows what change will be attempted if you run the same code without -n. If you are seeing the right result, remove -n.

If you have a complex directory structure, you can use recursive globbing if available. In Bash:

shopt -s globstar

Then try rename:

rename -n 's|(.*)/|$1/x_|' ./**/*.c

This captures everything before the last path element (.*)/ and reproduces it with the backreference $1.

Otherwise (if you can't use recursive globbing), use find:

find . -type f -name "*.c" -exec rename -n 's|(.*)/|$1/x_|' {} +

Although it will be slower if the files are in many directories, you can also use -execdir which allows a simpler regex (I found a noticeable speed difference between -exec (less than a second) and -execdir (3-4 seconds) with ~1113 .c files in ~175 directories):

find . -type f -name "*.c" -execdir rename -n 's|/|/x_|' {} +

The -n gives confusing-looking results with -execdir but if the base names look right, we are ok.

However, thanks to a comment by Kusalananda, I realise this depends on having GNU find where all the -execdir arguments start with ./, and not the OpenBSD version where they do not. If the filename has no path prefix at all, then I would guess you could use this version:

find . -type f -name "*.c" -execdir rename -n -- 's|^|x_|' {} +

but I can't test that as I don't have OpenBSD find.

However, Eliah Kagan very helpfully came up with this much better regex that works regardless of the implementation of find you are using and whether you use -exec or -execdir, and works with recursive globbing too:

rename -n 's|[^/]+$|x_$&|'

This matches at least 1 character(s) that are not / at the end ($) of the path, and then reproduces the whole match ($&) after the added x_. This command should work anywhere find and Perl rename are available:

find . -type f -name "*.c" -exec rename -n 's|[^/]+$|x_$&|' {} +
  • Is the regular expression correct for the -execdir variation? The arguments will not have a / in them... Can't test here ATM, sorry. – Kusalananda Apr 6 '18 at 16:54
  • @Kusalananda yes, it works because the arguments all start with ./. But perhaps there is a more efficient way to prepend... I am not very good at finding the nicest possible regex! here's the result – Zanna Apr 6 '18 at 16:55
  • Oh, they do start with ./ if I use GNU find, but not if I use my native OpenBSD find! That's awkward since it probably means my own answer is wrong for GNU find... – Kusalananda Apr 6 '18 at 17:02
  • oooh that's good to know! I better make a note O.O. (also hmm... looking at my pastie, I see the function I wrote to make pastebinit include the command run is capturing that command after quote removal, which is probably not fixable...) – Zanna Apr 6 '18 at 17:03
  • Thanks for your time, i tried this script but its not working. Please suggest some different script. – Lokesh Kumar Apr 9 '18 at 9:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.