skip to Main Content

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.

3

Answers


  1. 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$/&t1/' |
    {
        n=1;
        while IFS=$'t' read -r from to; do
           echo mv "$from" "$to$((n++)).png";
        done;
    }
    
    Login or Signup to reply.
  2. 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
    
    Login or Signup to reply.
  3. 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
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search