2

I got into a situation that I need to rename lots of files in the form of:

file.csv
file_1.csv
file_2.csv
file_3.csv
file_4.csv
file_5.csv
file_6.csv
file_7.csv
file_8.csv

To better order, i.e.:

file_1.csv
file_2.csv
file_3.csv
file_4.csv
file_5.csv
file_6.csv
file_7.csv
file_8.csv
file_9.csv

I.e., I can manually rename the first file, but for the remaining "_#" files, I need to renumber them with +1. I tried to use

rename -n -v 's/_(.*)./.(\1+1)./' 

but got

file_(1+1).csv
file_(2+1).csv

etc.

Any easy way to batch remain the "_#" files please?

PS, better as

file_01.csv
. . .
file_09.csv
file_10.csv
. . .

if possible.

UPDATE:

Thanks to cas' answer. Because I'll rename them as zero-padded names, so there won't be conflict in file names, so to me the command can be simplified as:

touch file.csv file_{1..9}.csv

$ rename -v 's/^(file_)(\d+)(.csv)$/$1 . (sprintf "%02i", $2 + 1) . $3/e' file_* file_1.csv renamed as file_02.csv file_2.csv renamed as file_03.csv file_3.csv renamed as file_04.csv file_4.csv renamed as file_05.csv file_5.csv renamed as file_06.csv file_6.csv renamed as file_07.csv file_7.csv renamed as file_08.csv file_8.csv renamed as file_09.csv file_9.csv renamed as file_10.csv

Note the last two file names:

file_09.csv
file_10.csv
xpt
  • 1,530
  • You could not have got the output you show you got based on the command you ran. The greedy regex .* Would have swallowed upto .cs and leaving only v. – guest_7 Aug 21 '21 at 05:55
  • 1
    See https://unix.stackexchange.com/a/497960/100397 for a way to do this using rename – Chris Davies Aug 21 '21 at 06:32

3 Answers3

3

You need to use perl's /e regex modifier to cause it to evaluate the right-hand-side of the s/// operation as a perl expression.

You also need to sort the filenames in reverse numerical order so that the highest-numbered filenames are renamed before the lower-numbered filenames (otherwise there will be filename collisions - by default, unless you use -f to force it, rename will refuse to overwrite existing files). To do this, I'll use GNU find with -print0 and GNU sort with -z for NUL-terminated input, -r and -V for a reverse version (i.e. "natural") sort. -t _ and -k 2 options are also used to sort from the second field.

rename's -d option is used to make it rename the filename portion of a pathname only, and -0 to make it take a NUL-separated list of files on stdin.

e.g.

$ touch file.csv file_{1..8}.csv
$ find . -name 'file_*.csv' -print0 |
    sort -z -t _ -k2 -r -V |
    rename -d -0 's/^(file_)(\d+)(\.csv)$/$1 . ($2 + 1) . $3/e'
$ mv file.csv file_1.csv
$ ls -l
total 5
-rw-r--r-- 1 cas cas 0 Aug 21 15:22 file_1.csv
-rw-r--r-- 1 cas cas 0 Aug 21 15:22 file_2.csv
-rw-r--r-- 1 cas cas 0 Aug 21 15:22 file_3.csv
-rw-r--r-- 1 cas cas 0 Aug 21 15:22 file_4.csv
-rw-r--r-- 1 cas cas 0 Aug 21 15:22 file_5.csv
-rw-r--r-- 1 cas cas 0 Aug 21 15:22 file_6.csv
-rw-r--r-- 1 cas cas 0 Aug 21 15:22 file_7.csv
-rw-r--r-- 1 cas cas 0 Aug 21 15:22 file_8.csv
-rw-r--r-- 1 cas cas 0 Aug 21 15:22 file_9.csv

This could be simplified a little, but I've made the regex explicitly look for and capture file_, one-or-more digits, and the .csv extension to avoid any possibility of renaming files it shouldn't.

To make the file numbering zero-padded, you can use the sprintf function. e.g.

... | rename -0 -d -v 's/^(file_)(\d+)(\.csv)$/$1 . (sprintf "%02i", $2 + 1) . $3/e'
Reading filenames from file handle (GLOB(0x555555905960))
./file_8.csv renamed as ./file_09.csv
./file_7.csv renamed as ./file_08.csv
./file_6.csv renamed as ./file_07.csv
./file_5.csv renamed as ./file_06.csv
./file_4.csv renamed as ./file_05.csv
./file_3.csv renamed as ./file_04.csv
./file_2.csv renamed as ./file_03.csv
./file_1.csv renamed as ./file_02.csv
cas
  • 78,579
2

With zsh:

$ autoload -Uz zmv # best in ~/.zshrc
$ LC_ALL=C zmv -f -n '(file)(|_(<->))(.csv)(#qnOn)' '${1}_$(($3+1))$4'
mv -- file_11.csv file_12.csv
mv -- file_10.csv file_11.csv
mv -- file_9.csv file_10.csv
mv -- file_8.csv file_9.csv
mv -- file_7.csv file_8.csv
mv -- file_6.csv file_7.csv
mv -- file_5.csv file_6.csv
mv -- file_4.csv file_5.csv
mv -- file_3.csv file_4.csv
mv -- file_2.csv file_3.csv
mv -- file_1.csv file_2.csv
mv -- file.csv file_1.csv

Or with the 0-padding using the ${(l[2][0])...} left-padding parameter expansion flag:

$ LC_ALL=C zmv -fn '(file)(|_(<->))(.csv)(#qnOn)' '${1}_${(l[2][0])$(($3+1))}$4'
mv -- file_11.csv file_12.csv
mv -- file_10.csv file_11.csv
mv -- file_9.csv file_10.csv
mv -- file_8.csv file_09.csv
mv -- file_7.csv file_08.csv
mv -- file_6.csv file_07.csv
mv -- file_5.csv file_06.csv
mv -- file_4.csv file_05.csv
mv -- file_3.csv file_04.csv
mv -- file_2.csv file_03.csv
mv -- file_1.csv file_02.csv
mv -- file.csv file_01.csv

(remove the -n (dry-run) if OK).

That renames files that are generated by that extendedglob glob where:

  • <-> matches any sequence of digits (it's the <x-y> decimal integer number matching operators with no bound specified)
  • (#q...) specifies glob qualifiers where:
    • n: toggles numericglobsort
    • On: sorts in reverse by name. So file8 comes before file7 but also thanks to numericglobsort above, file10 comes before file9. The LC_ALL=C is to make sure the _ and . are not ignored in the sorting algorithm so file.csv comes after file_1.csv

In the replacement, $1, $2... contain what is matched by each respective pair of (, ) in the pattern like in perl's s/// operator.

The -f is there to disable zmv's sanity checks which would complain for instance that file6.csv would be renamed to an already existing file7.csv (even though by that time, file7.csv would have already been renamed to file8.csv).

You could use perl's rename to do the renaming but you'd still need something like zsh globs to pass the list in the right order:

$ (LC_ALL=C; rename -f -n '
    s{^(file)(?:_(\d*))?(\.csv)\Z}{
      "${1}_" . (($2 // 0) + 1) . $3}se
   ' file(|_<->).csv(nOn))
rename(file_11.csv, file_12.csv)
rename(file_10.csv, file_11.csv)
rename(file_9.csv, file_10.csv)
rename(file_7.csv, file_8.csv)
rename(file_6.csv, file_7.csv)
rename(file_5.csv, file_6.csv)
rename(file_4.csv, file_5.csv)
rename(file_3.csv, file_4.csv)
rename(file_2.csv, file_3.csv)
rename(file_1.csv, file_2.csv)
rename(file.csv, file_1.csv)

Replace (($2 // 0) + 1) with sprintf("%02d", ($2 // 0) + 1) for instance to do the 0-padding.

If you don't have and can't install zsh, but you're on a GNU system, you can get those files in the right order with bash as:

eval "files=(
  $(
    shopt -s extglob failglob
    export LC_ALL=C
    ls --quoting-style=shell-always -rvd file?(_+([0-9])).csv
   )
)"

Relying on GNU ls -rv to list the files in reverse version order.

And then call rename ... "${files[@]}" with the same rename code as above to do the renaming.

Here, as the names of the files are very simple and assuming there's no other file that matches file*.csv, and with the default value of $IFS, you could also do (still with GNU ls but with any Bourne-like shell):

rename ... $(LC_ALL=C ls -rvd file*.csv)
1

One way to do that can be as shown:

set -- file_*.csv

while [ "$#" -gt 0 ]; do set -- "$#" "$@" mv -- "file_$1.csv" "file_$#.csv" shift 2 done

mv file.csv file_1.csv

If you wanted left zero padded filenames you change the mv statements to:

mv -- "file_$1.csv" "$(printf 'file_%02d.csv\n' "$#")"
mv file.csv file_01.csv

If it is a concern that your command line arguments are getting destroyed then you can take all this action inside a function and call that function.

guest_7
  • 5,728
  • 1
  • 7
  • 13