Tato14
Tato14

Reputation: 435

Update numbers in filenames

I have a set of filenames which are ordered numerically like:

13B12363_1B1_0.png
13B12363_1B1_1.png
13B12363_1B1_2.png
13B12363_1B1_3.png
13B12363_1B1_4.png
13B12363_1B1_5.png
13B12363_1B1_6.png
13B12363_1B1_7.png
13B12363_1B1_8.png
13B12363_1B1_9.png
13B12363_1B1_10.png
[...]
13B12363_1B1_495.png
13B12363_1B1_496.png
13B12363_1B1_497.png
13B12363_1B1_498.png
13B12363_1B1_499.png

After some postprocessing, I removed some files and I would like to update the ordering number and replace the actual number by its new position. Looking at this previous question I end up doing something like:

(1) ls -v | cat -n | while read n f; do mv -i $f ${f%%[0-9]+.png}_$n.png; done

However, this command do not recognize the "ordering number + png" and just append the new number at the end of the filename. Something like 13B12363_1B1_10.png_9.png

On the other hand, if I do:

(2) ls -v * | cat -n | while read n f; do mv $f ${f%.*}_$n.png; done

The ordering number is added without issues. Like 13B12363_1B1_10_9.png

So, for (1) it seems I am not specifying the digit correctly but I am not able to find the correct syntax. So far I tried [0-9], [0-9]+, [[:digits:]] and [[:digits:]]+. Which should be the proper one?

Additionally, in (2) I am wondering how I should specify rename (CentOS version) to remove the numbers between the second and the third underscore. Here I have to say that I have some filenames like 20B12363_22_10_9.png, so I should somehow specify second and third underscore.

Upvotes: 2

Views: 581

Answers (3)

Léa Gris
Léa Gris

Reputation: 19545

Using Bash's built-in Basic Regex Engine and a null delimited list of files.

Tested with sample

#!/usr/bin/env bash

prename=$1

# Bash setting to return empty result if no match found
shopt -s nullglob

# Create a temporary directory to prevent file rename collisions
tmpdir=$(mktemp -d) || exit 1

# Add a trap to remove the temporary directory on EXIT
trap 'rmdir -- "$tmpdir"'  EXIT

# Initialize file counter
n=0

# Generate null delimited list of files
printf -- %s\\0 "${prename}_"*'.png' |

# Sort the null delimited list on 3rd field numeric order with _ separator
sort --zero-terminated --field-separator=_ --key=3n |

# Iterate the null delimited list
while IFS= read -r -d '' f; do
  
  # If Bash Regex match the file name AND
  # file has a different sequence number

  if [[ "$f" =~ (.*)_([0-9]+)\.png$ ]] && [[ ${BASH_REMATCH[2]} -ne $n ]]; then

    # Use captured Regex match group 1 to rename file with incrementing counter
    # and move it to the temporary folder to prevent rename collision with
    # existing file
    echo mv -- "$f" "$tmpdir/${BASH_REMATCH[1]}_$((n)).png"
  fi

  # Increment file counter
  n=$((n+1))
done

# Move back the renamed files in place
mv --no-clobber -- "$tmpdir/*" ./

# $tempdir removal is automatic on EXIT
# If something goes wrong, some files remain in it and it is not deleted
# so these can be dealt with manually

Remove the echo if the result matches your expectations.

Output from the sample

mv -- 13B12363_1B1_495.png /tmp/tmp.O2HmbyD7d5/13B12363_1B1_11.png
mv -- 13B12363_1B1_496.png /tmp/tmp.O2HmbyD7d5/13B12363_1B1_12.png
mv -- 13B12363_1B1_497.png /tmp/tmp.O2HmbyD7d5/13B12363_1B1_13.png
mv -- 13B12363_1B1_498.png /tmp/tmp.O2HmbyD7d5/13B12363_1B1_14.png
mv -- 13B12363_1B1_499.png /tmp/tmp.O2HmbyD7d5/13B12363_1B1_15.png

Upvotes: 3

Julio
Julio

Reputation: 5308

Another alternative, with perl:

perl -e 'while(<@ARGV>){$o=$_;s/\d+(?=\D*$)/$i++.".renamed"/e;die if -e $_;rename $o,$_}while(<*.renamed>){$o=$_;s/\.renamed$//;die if -e $_;rename $o,$_}' $(ls -v|sed -E "s/$|^/'/g"|paste -sd ' ' -)

This solution should avoid rename collisions by: first renaming files adding extra ".renamed" extension. And then removing the ".renamed" extension as the last step. Also, There are checks to detect rename collision.

Anyways, please backup your data before trying :)


The perl script unrolled and explained:

while(<@ARGV>){ # loop through arguments. 
                # filenames are passed to "$_" variable
    
    # save old file name
    $o=$_;

    # if not using variable, regex replacement (s///) uses topic variable ($_)
    # e flag ==> evals the replacement
    s/\d+(?=\D*$)/$i++.".renamed"/e;  # works on $_

    # Detect rename collision
    die if -e $_;

    rename $o,$_
}
while(<*.renamed>){
    $o=$_;
    s/\.renamed$//; # remove .renamed extension
    die if -e $_;
    rename $o,$_
}

The regex:

\d+       # one number or more
(?=\D*$)  # followed by 0 or more non-numbers and end of string

Upvotes: 0

KamilCuk
KamilCuk

Reputation: 140900

Do not parse ls.

read interprets \ and splits on IFS. bashfaq how to read a stream line by line

In ${f%%replacement} expansion the replacement is not regex, but globulation. Rules differ. + means literally +.

You could shopt -o extglob and then ${f%%+([0-9]).png}. Or write a loop. Or match the _ too and do f=${f%%.png}; f="${f%_[0-9]*}_".

Or something along (untested):

find . -maxdepth 1 -mindepth 1 -type f -name '13B12363_1B1_*.png' |
sort -t_ -n -k3 |
sed 's/\(.*\)[0-9]+\.png$/&\t\1/' |
{
    n=1;
    while IFS=$'\t' read -r from to; do
       echo mv "$from" "$to$((n++)).png";
    done;
}

Upvotes: 2

Related Questions