6

What rename command can I use which will delete the present filenames in a directory and replace them with alphanumeric filenames?

Such as gg0001, gg0002, et cetera? And I'd like the command to work on files with any type of extension. And I'd like the extension to be retained.

I'd prefer that the Perl rename be used.

I've tried this command, but it just prepends the "gg" to the filenames whereas I want the original filenames replaced:

rename 's/^/gg_/' *

  • rename 's/*/gg_/' * ? – DisplayName Nov 8 '14 at 17:27
  • @DisplayName I get the following error with that command: i.imgur.com/3oFaaOz.png – user8547 Nov 8 '14 at 17:31
  • Yeah, its probably wrong. A variation of that can be used in sed. – DisplayName Nov 8 '14 at 17:31
  • Try: rename 's/.*/gg_/' * I't won't add the numeric suffixes but i think it will atleast replace the filenames. – DisplayName Nov 8 '14 at 17:33
  • @DisplayName Since there are no numbers it only replaces the name of 1 file. – user8547 Nov 8 '14 at 17:37
6

You don't really need the rename command for this, you can do it directly in the shell:

c=0; for i in *; do let c++; mv "$i" "gg_$(printf "%03d" $c)${i#"${i%.*}"}"; done

The printf "%03d" $c will print the $c variable, padding it with 3 leading 0s as needed. The let c++ increments the counter ($c). The ${i#"${i%.*}"} extracts the extension. More on that here.

I would not use rename for this. I don't think it can do calculations. Valid Perl constructs like s/.*/$c++/e fail and I don't see any other way to have it do calculations. That means you would still need to run it in a loop and have something else increment the counter for you. Something like:

c=0;for i in *; do let c++; rename -n 's/.*\.(.*)/'$c'.$1/' "$i"; done

But there's no advantage over using the simpler mv approach.

  • Will this command work in renaming filenames that have spaces and underscores? – user8547 Nov 8 '14 at 18:11
  • @user8547 yes, and \ and \n and anything else you can think of. I also added a version using rename but there really is no point for this particular thing you're trying to do. – terdon Nov 8 '14 at 18:13
  • @mikeserv thanks, I knew there was a simpler way to do that. – terdon Nov 8 '14 at 18:56
  • @mikeserv do you? Even within the ${}? I can't come up with a filename that breaks this on my bash. I tried ff*, file.ext* and neither had issues. There also was a file called ff.avi when I used the ff* pattern. As far as I know, globs are safe within ${}. – terdon Nov 8 '14 at 19:28
  • 1
    @mikeserv Got it. I thought the problem would be in the pattern, not the replacement. Your last example illustrates it. Thanks, answer edited. – terdon Nov 8 '14 at 19:40
4

Provided that there is an absence of ' single-quotes in the filenames, you might do this like:

i=0; set --
for f in *
do set -- "$@" "$f" gg "$((i+=1))" "${f#"${f%.*}"}"
done
printf "mv '%s' '%s%03d%s'\n" "$@" | sh

That way if a filename contains a . then it and whatever follows it is retained, else that field is empty and printf prints nothing there.

I suppose that even if there are single-quotes in the filenames, it could be possible to do...

i=0; set --
for f in *
do set -- "$@" "$((i+=1))" gg "$i" "$i#\"\${$i%.*}\""
done
printf 'mv "${%d}" "%s%03d${%s}"\n' "$@" | sh -s -- *

...which is probably far safer overall.

But you know, after thinking about it, I've come to suspect that both are too much work. The only reason I pipe either list is to avoid doing the checks for the zero-padding. I prefer to build a list in one subshell and stream it out with one write() to a single other subshell when necessary.

But that said, zero-padding can't be too hard. I tooled around with it a little and I came up with this little math statement:

$((z/=(m*=((i+=1)<$m?1:10))/$m))

If placed in an iteration loop it will increment $i once per item, advance the multiplier $m on an exponential back-off for each factor of ten, and tithe $z for each of the same. The idea is to get the value in $z before starting a loop that holds the highest possible increment $i will reach. This is simply done:

set -- *; max=$# z=1
until [ "${#z}" -gt "${#max}" ]; do : "$((z*=10))"; done

That's a fully automated way - it will automatically pad to whatever the highest number it might increment. But, honestly, $z just needs to be a number beginning with a 1 and followed only by zeroes, so:

set -- *; z=1000

... would pad to the thousands field. Anyway, here's a bit of a demo that I used when testing:

time dash <<\CMD
    str=100000 z=1 m=1 i=0 
    until [ "${#z}" -gt "${#str}" ]; do : "$((z*=10))"; done
    while : "$((z/=(m*=((i+=1)<$m?1:10))/$m))" &&
    case "$i" in (*[2-8]*|*9*[10]*|*1*[19]*) continue;;
    ([019]*) set -- "$@" "z: $z m: $m i: $i" "${z#?}$i";;esac
    do [ "$i" = "$str" ] && printf %s\\n "$@" && break; done
CMD

Most of that is filtering output because I wanted to be sure it would keep working for large item counts, but I didn't want to read 100000 results.

OUTPUT

z: 100000 m: 10 i: 1
000001
z: 100000 m: 10 i: 9
000009
z: 10000 m: 100 i: 10
000010
z: 10000 m: 100 i: 99
000099
z: 1000 m: 1000 i: 100
000100
z: 1000 m: 1000 i: 999
000999
z: 100 m: 10000 i: 1000
001000
z: 100 m: 10000 i: 9999
009999
z: 10 m: 100000 i: 10000
010000
z: 10 m: 100000 i: 99999
099999
z: 1 m: 1000000 i: 100000
100000
dash <<<''  0.32s user 0.00s system 99% cpu 0.320 total

$m should be initialized to the first stage at which you want leading zeroes to begin being stripped - I just did m=1 above. $z, as mentioned, stores the top order-of-magnitude. And $i counts the loops, so i=0 is probably a safe bet. Anyway, the rest just happens. The idea is just to strip the leading 1 from $z and tack it on anywhere.

set -- $(seq 1000); m=1 i=0 z=10000
while [ "$#" -gt 0 ] && : "$((z/=(m*=((i+=1)<$m?1:10))/$m))"
do printf 'mv "%s" "lead_string_%s"\n' "$1" "${z#1}$i${1#"${1%.*}"}"
shift; done

I use printf there just to show what it looks like, though, of course, the whole point is to avoid printing data out and instead hand it over in the current shell as a prepared, delimited argument. Anyway, it looks like:

mv "1" "lead_string_0001"
mv "2" "lead_string_0002"
mv "3" "lead_string_0003"
...
mv "9" "lead_string_0009"
mv "10" "lead_string_0010"
mv "11" "lead_string_0011"
...
mv "15" "lead_string_0015"
...
mv "96" "lead_string_0096"
...
mv "99" "lead_string_0099"
mv "100" "lead_string_0100"
mv "101" "lead_string_0101"
mv "102" "lead_string_0102"
...
mv "998" "lead_string_0998"
mv "999" "lead_string_0999"
mv "1000" "lead_string_1000"

And it makes no difference whether it is a for or while loop you use. So with files you could just do:

m=1 i=0 z=10000
for f in *; do : "$((z/=(m*=((i+=1)<$m?1:10))/$m))"
mv -- "$f" "lead_string_${z#1}$i${f#"${f%.*}"}"
done
  • 1
    Tx, that works. I changed the command to one line: i.imgur.com/GvSmHRn.png – user8547 Nov 8 '14 at 19:04
  • @user8547 - please see the update. I think is a far better way to go about it. – mikeserv Nov 8 '14 at 20:16
  • I just tried the updated command, but it didn't rename the files and had errors: i.imgur.com/KD257ut.png Actually disregard. I tried it again and it worked fine. Tx! – user8547 Nov 8 '14 at 20:22
  • @user8547 - Yeah, my bad. Might have been because I forgot to -s -- * for a couple of minutes there, but your error string doesn't look like it. Maybe your first go round you forgot to change the quote-style on the printf format string? – mikeserv Nov 8 '14 at 20:31
  • 1
    Possibly, I don't know. But it works fine now. – user8547 Nov 8 '14 at 20:36
2

This one worked for me. (But the rename command used here is a perl rename).

ls -1 -c | xargs rename -n 's/.*/our $i; sprintf("gg%04d", $i++)/e'

However, the above assumes there are no files that have spaces in their names. Otherwise it breaks. Also, parsing ls output is really not a good idea.

However, since it breaks when there are spaces on file names, let us try to fix the above issues.

find -print0 | xargs -0 rename 's/.*/our $i; sprintf("gg%04d", $i++)/e'

The above syntax works but it still has issues. The issue is when you have sub directories, it will rename the sub directories as well which is what we desire.

To fix that issue, we could rephrase our command as @terdon suggests in his comments. The final modified command looks like,

find -maxdepth 1 -type f -print0 | xargs -0 rename 's/.*/our $i; sprintf("gg%04d", $i++)/e'

Testing

I have the following files inside my folder. Don't rename me is a sub directory which we don't want to rename.

ls
Don't rename me  file1  file with spaces  hello

Now, I execute the command and I get the below output.

find -maxdepth 1 -type f -print0 | xargs -0 rename -n 's/.*/our $i; sprintf("gg%04d", $i++)/e'
./file1 renamed as gg0000
./hello renamed as gg0001
./file with spaces renamed as gg0002

The -n flag could be removed if we are satisfied with what we see as the final output.

References

https://stackoverflow.com/a/23207377/1742825

  • This breaks on spaces or pretty much any other non-standard file name. – terdon Nov 8 '14 at 17:47
  • That worked, but I'd prefer if it use a GNU rename command, and that it work on standard and non-standard filenames. – user8547 Nov 8 '14 at 17:53
  • @user8547 are you sure you mean GNU rename? The syntax you are attempting is not for the GNU version but for the Perl one. What OS are you running? – terdon Nov 8 '14 at 18:05
  • @Ramesh just use a glob instead of ls. You know that parsing ls is usually not a good idea :). Also note that this is not the GNU rename but the Perl one. The GNU one is rename.ul on Debian. Finally, your find version will also rename files in subdirectories which might not be desired. – terdon Nov 8 '14 at 18:07
  • To use find correctly, you want something like find -maxdepth 1 -type f -print0. – terdon Nov 8 '14 at 18:08
1

Just increment a variable at each iteration. Since rename sets use strict, you'll need to use a global variable $::i (or use no strict, or declare the variable with our).

rename 'our $i; s/.*/gg_$i/; ++$i' *

If you want to pad the number with leading zeroes, count the number of files. The list of files is in @ARGV.

rename 'our $i; my $width = length(scalar(@ARGV)); s/.*/sprintf("gg_%0${width}d", $i++)/e' *

You can test if your counter variable is defined to execute code only on the first iteration.

rename 'our ($i, $width); if (!defined $width) {$width = length(scalar(@ARGV)); $i = 0;} s/.*/sprintf("gg_%0${width}d", $i++)/e' *

In all the above, if you pass file names with a directory part (i.e. if you pass file names containing a / — not happening here with *), change s/.*/…/ to s/[^/]*\z/…/.

  • The first command did not rename any of the files and returned this error: i.imgur.com/3BO1OM1.png The second and third commands renamed the files but returned these errors: i.imgur.com/3cTwCUh.png And the files were not renamed well, being prepended with gg_ but followed by long numbers. It also doesn't seem to work on names with spaces. – user8547 Nov 9 '14 at 15:55
  • @user8547 Ah, I explained that our was necessary in the first example but forgot to include it in my code, fixed, sorry about that. In the second example, length(@ARGV) was what I meant to write; I changed it to avoid the warning. What do you mean by “not renamed well” and by “long numbers”? Spaces aren't treated specially here. – Gilles Nov 9 '14 at 18:53
  • @user8547 Oh, I think I get the “not renamed well”: you wanted to replace the file name by the number, not prepend it. My mistake. I've changed ^ to .* to replace the old name. – Gilles Nov 9 '14 at 19:03
  • The first modified command renamed the files gg_1, gg_2, etc, but the extension was not retained. It also returned the following error: i.imgur.com/pIEXPcz.png The second code renamed them and returned no errors, but didn't retain the extension. The third command resulted in the same as the second command. So this would be useful if it retained the extensions. – user8547 Nov 10 '14 at 1:18
  • @user8547 The warning is harmless. None of my solutions retain the extension because your original question said to replace the whole file name, it didn't say anything about preserving the extension. Please don't keep changing your question after it's been answered. – Gilles Nov 10 '14 at 13:45

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.