18

I have a directory which contains numbered image files, something like this:

01.png
02.png
03.png
03.svg
04.png
05.png
06.jpg
07.png
08.png
09.png
09.svg
10.png

Sometimes there may be multiple versions of a file in different formats (eg. a png and svg version of the 03 and 09 files above) but the numbers are otherwise consecutive. Typically there are 40-80 such files in each directory. The numbers correspond to the order these images appear in a manuscript (a Word document, but that's not important). There is no other way to determine the order of the images.

If I add a new image to the manuscript I need to place a copy of the image in this directory with the correct numbering. So if the new image is the fifth in the manuscript I need to rename the files in the directory to this in order to make room for it:

01.png
02.png
03.png
03.svg
04.png
06.png
07.jpg
08.png
09.png
10.png
10.svg
11.png

What is the most straightforward way from the command line, or from a script or macro to renumber all the files starting at a certain number? I have a standard Fedora Linux install using bash.

  • 1
    @LoïcReynier Please be more careful not to make typos in your edit comments; AFAIK they’re just about impossible to fix. – G-Man Says 'Reinstate Monica' Jun 21 at 20:18
16

I think that it should do the work:

#!/bin/bash

NEWFILE=$1

for file in `ls|sort -g -r`
do
    filename=$(basename "$file")
    extension=${filename##*.}
    filename=${filename%.*}

    if [ $filename -ge $NEWFILE ]
    then
        mv "$file" "$(($filename + 1))".$extension
    fi
done

Script takes one parameter - number of you new image.

PS. Put script in another directory than your images. In images directory there should be only images named in this way that you described.

| improve this answer | |
  • This looks promising, I'll try it in a few hours when I'm back on my laptop. – robertc Jun 11 '12 at 16:33
  • This will only works if your filename is not prefixed by non-numerics chars – mems Nov 15 '16 at 18:35
  • @mems The OP clearly states that filenames begin with a number – xhienne Jan 8 '17 at 13:16
  • 1
    (1) Note that, since the numbers in the input are all integers, you don’t need the inefficient -g option to sort; -n will suffice.  And, since the values all have the same number of digits, the default sort order will work.  In fact, the default ls order will work; i.e., ls -r.  (2) You might want to change `…` to $(…) — see this, this, and this.  (3) Parsing the output of ls is generally discouraged. … (Cont’d) – G-Man Says 'Reinstate Monica' Jun 23 at 0:33
  • (Cont’d) …  Although, as long as the directory contains nothing but image files with standard extensions (jpg, png and svg) whose names are all numbers, it probably won’t fail.  (And getting a list of filenames in reverse order can be somewhat tricky.)  (4) If the OP has 288 files (01.jpg through 96.svg) and wants to insert a new 94.png, he needs only to rename 96 → 97, 95 → 96 and 94 → 95.  But your code will loop through all the files.  Consider adding a break when you reach the last rename.  … (Cont’d) – G-Man Says 'Reinstate Monica' Jun 23 at 0:33
5

This would be easier in zsh, where you can use

  • the On glob qualifier to sort matches in decreasing order (and n to use numerical order, in case the file names don't all have leading zeroes to the same width);
  • the (l:WIDTH::FILLER:) parameter expansion flag to pad all numbers to the same width (the width of the larger number).
break=$1   # the position at which you want to insert a file
setopt extended_glob
width=
for x in [0-9]*(nOn); do
  n=${x%%[^0-9]*}
  if ((n < break)); then break; fi
  ((++n))
  [[ -n $width ]] || width=${#n}
  mv $x ${(l:$width::0:)n}${x##${x%%[^0-9]*}}
done

In bash, here's a script that assumes files are padded to a fixed width (otherwise, the script won't rename the right files) and pads to a fixed width (which must be specified).

break=$1      # the position at which you want to insert a file
width=9999    # the number of digits to pad numbers to
files=([0-9]*)
for ((i=#((${#files}-1)); i>=0; --i)); do
  n=${x%%[^0-9]*}
  x=${files[$i]}
  if ((n < break)); then continue; fi
  n=$((n + 1 + width + 1)); n=${n#1}
  mv -- "${files[$i]}" "$n${x##${x%%[^0-9]*}}"
done
| improve this answer | |
2

There doesn't seem to be much recent interest in this question but, should someone stumble upon it, there are three issues here.

  • One is how to select files to rename based on semantic criteria (range is not lexical and cannot be specified by wildcards or even regular expressions-- automata theory says that this is more complex than an NFA).

  • The second is how to change a name by modifying a portion of it.

  • The third is how to avoid name collision.

A script in Bash and many other languages can do this specific transform, but most of us would rather not have to write a program every time we want to rename a bunch of files.

With my (free and open source) rene.py you can do what you want but it takes two invocations to avoid the name collision problem.

  1. First
    rene ?.*/#7-80  %?.* B
    
    increments all names in the range, adding a prefix of % to avoid existing names.
  2. Then
    rene %* *
    
    removes this prefix from those files that have it.

I describe this at https://sourceforge.net/p/rene-file-renamer/discussion/examples/thread/f0fe8aa63c/

| improve this answer | |
  • Ranges of decimal integers can be specified with zsh's <x-y> wildcard. – Stéphane Chazelas Jun 19 at 11:02
  • ?, and * are wildcard operators in most shells, # also in zsh -o extendedglob. # is special in most shells, and in some even if not delimited so if those rene ... are meant to be shell command lines, those arguments should probably be quoted (rene '?.*/#7-80' '%?.*' B...) – Stéphane Chazelas Jun 19 at 11:58
  • Thanks for pointing that out. Those examples work in a Windows Cmd shell, which doesn't generally glob, but they do need quoting in Linux shells. – David McCracken Jun 22 at 19:55
  • I don’t understand what you’re saying about specifying numbers in a range. Using your example of 7-80, we can specify that (1) in many shells, with {0{7,8,9},{1,2,3,4,5,6,7}{0,1,2,3,4,5,6,7,8,9},80}, (2) in bash (and maybe some others) with {07..30}, or (3) in bash, with extglob set, with @(0[7-9]|[1-7][0-9]|80). … … … … … … … … … … … P.S. I would be amazed if your rene %* * command worked in a Unixy shell without quoting at least the second *. Probably the %*, too. – G-Man Says 'Reinstate Monica' Jun 23 at 0:46
  • @G-Man, {a,b} (from csh), or {x..y} (from zsh) are not matching/glob operators they are brace expansion which expands to the corresponding list of strings. @(x|y) (from ksh), or zsh's (x|y) or zsh's <x-y> are pattern matching, but only the latter can easily match arbitrary ranges of decimal integers. – Stéphane Chazelas Jun 23 at 5:42
1

This exact issue is covered in this article. Note that you would have to modify it to support the SVG and PNG formats, by adding a second MV step.

| improve this answer | |
  • I don't think it is the exact issue, that's going renumber all the images every time. I just want to renumber the images from a particular point. – robertc Jun 11 '12 at 16:30
1

Another zsh approach using its zmv autoloadable function for batch renaming:

$ autoload -Uz zmv # best in ~/.zshrc
$ zmv -f -n '(<5->)(.*)(#qnOn)' '${(l[2][0])$(($1 + 1))}$2'
mv -- 10.png 11.png
mv -- 09.svg 10.svg
mv -- 09.png 10.png
mv -- 08.png 09.png
mv -- 07.png 08.png
mv -- 06.jpg 07.jpg
mv -- 05.png 06.png

Remove the -n (for dry-run) if happy.

The -f (to disable safety checks) is needed as otherwise zmv would complain that we're renaming 09.png to 10.png which already exists for instance. However here, it's fine as we're processing the files in reverse numeric order (with the n and On qualifiers), so 10.png will already have been renamed to 11.png by the time we rename 09.png to 10.png.

| improve this answer | |
1

If you save the next script as renn.sh, after installing it (see help for how to install it), you should be able to use:

renn "@." "[(02)i+1]." "{i -ge 5}" '<dir_path>'

To find out how to use the script, you can call it with the --help flag.

# REName with Numbers - a .sh (dash; bash; zsh - compatible) rename script 
# that can do basic arithmetic operations (+, -, padding with 0's) on 
# numbers in file names
# Author: Ioan Marin

# Disclaimer:
# By using this program you are assuming full responsibility (and the 
# author of this program shall not be held liable) for any data loss 
# or damage that may result from the use or misuse of this program.

DetectShell () {
    eval $1=\"\";
    if [ -n "$BASH_VERSION" ]; then
        eval $1=\"bash\";
    elif [ -n "$ZSH_VERSION" ]; then
        eval $1=\"zsh\";
    elif [ "$PS1" = '$ ' ]; then
        eval $1=\"dash\";
    else
        eval $1=\"undetermined\";
    fi
}

GetCharsArray () {
    DestroyArray $2

    eval str=\"\$$1\"
    i1=0;
    qm_mask=""
    if [ -z "$str" ]; then
        eval $2\_0="0";
    else
        rem_str="$str"
        while [ "$i1" -le 1000000000 ]; do
            i1=$((i1+1));
            rem_str="${rem_str#?}"
            qm_mask="$qm_mask?"
            if [ -z "$rem_str" ]; then
                break;
            fi
        done
        eval $2\_0="$i1"
        i2=1;
        while [ "$i2" -le "$i1" ]; do
            qm_mask="${qm_mask#?}"
            eval crt_char=\"\$\{str\%$qm_mask\}\"
            str="${str#?}"
            eval $2\_$i2="\"\$crt_char\""
            i2=$((i2+1))
        done
    fi
}

GetStrLen () {
    eval str=\"\$$1\"
    str_len=0;
    qm_mask=""
    if [ -z "$str" ]; then
        eval $2="0";
    else
        rem_str="$str"
        while [ "$str_len" -le 1000000000 ]; do
            str_len=$((str_len+1));
            rem_str="${rem_str#?}"
            qm_mask="$qm_mask?"
            if [ -z "$rem_str" ]; then
                break;
            fi
        done
        eval $2="$str_len"
    fi
}

DestroyArray () {
    eval array_len=\$\(\($1\_0\)\)

    j=0;
    while [ "$j" -lt "$array_len" ]; do
        j=$((j+1))
        unset $1\_$j
    done
    unset $1\_0
}

ValidateNaturalNumber () {
    eval tested_number=\"\$$1\"
    condition="\$tested_number -ge 0" #test if natural number
    result="true"
    if [ -n "$tested_number" ]; then
        eval [ $condition ] 2>/dev/null || {
            result="false";
        }
    fi
    eval $2=$result
}

ProcedureGetMarginsAndNumber () {

    eval input_f_n=\"\$$1\"

    neg_number_detected="false"

    if [ -n "$pylon1_mask1" ]; then
        margin1="${input_f_n%%"$pylon1_mask1"*[0-9]*"$pylon2_mask1"*}"
    else
        if [ "${input_f_n%%"-"[0-9]*"$pylon2_mask1"*}" = "$input_f_n" ]; then #if a negative number is not match:
            margin1="${input_f_n%%[0-9]*"$pylon2_mask1"*}"
        else #if a negative number is match:
            margin1="${input_f_n%%"-"[0-9]*"$pylon2_mask1"*}"

            neg_number_detected="true"
        fi
    fi
    if [ -n "$pylon2_mask1" ]; then
        margin2="${input_f_n##"$margin1""$pylon1_mask1"*[0-9]*"$pylon2_mask1"}"
    else
        if [ "$neg_number_detected" = "false" ]; then #if a negative number is not match:
            margin2="${input_f_n##"$margin1""$pylon1_mask1"*[0-9]}"
        else #if a neg number is match:
            margin2="${input_f_n##"$margin1""$pylon1_mask1"*"-"[0-9]}"
        fi
    fi

    number_temp="${input_f_n#"$margin1""$pylon1_mask1"}"
    number="${number_temp%"$pylon2_mask1""$margin2"}"
}

PrintInTitle () {
    printf "\033]0;%s\007" "$1"
}

PrintJustInTitle () {
    PrintInTitle "$1">/dev/stdin
}

CleanUp () {

    #Restore CTRL-C, CTRL-Z:
    trap - INT
    trap - TSTP

    DestroyArray params
    DestroyArray rn_from
    DestroyArray rn_to
    DestroyArray numbers
    DestroyArray margin1
    DestroyArray margin2
    DestroyArray marks_temp_fns
    DestroyArray coll_chk_1_markings
    DestroyArray marks_special_temp_fns

    #Restore the initial directory:
    cd "$initial_dir"

    #Clear the title:
    PrintJustInTitle ""

    #Restore IFS:
    IFS="$old_IFS"
}

trap1 () {
    if [ "$started_rn" = "false" ]; then
        printf "Cleaning up. Please wait..."
        CleanUp
        SPACE=" "
        BS="\b"
        SPACE_str=""
        BS_str=""
        for i in $(seq 1 40); do SPACE_str="$SPACE_str""$SPACE"; done
        for i in $(seq 1 40); do BS_str="$BS_str""$BS"; done
        printf "$BS_str"
        printf "$SPACE_str"
        printf "$BS_str"
        printf "Aborted. Nothing done.\n"
        exit
    fi
}

DisplayHelp () {
    printf "\n"
    printf "renn - REName with Numbers\n"
    printf "\n"
    printf "    Description:\n"
    printf "        - renames filenames in a directory that match a mask (<mask1>) inexactly:\n"
    printf "            - e.g.: <mask1>=\"(@)\" matches \"*(@)*\"\n"
    printf "            - a '@' wildcard represents a number in the matched file/folder names.\n"
    printf "            - note that the first match is taken into consideration - so the strings surrounding the '@' mask character should uniquelly identify the matched number in each file/folder name\n"
    printf "\n"
    printf "    Syntax:\n"
    printf "    renn <mask1> <mask2> <directory_path> [<condition>] [flags]\n"
    printf "        where:\n"
    printf "        <mask1> is a string containing exactly one '@' wildcard.\n"
    printf "        <mask2> is a string containing one \"[...]\" arithmetic expression.\n"
    printf "            An arithmetic expression is of the form \"[expr(i)]\", where:\n"
    printf "                \"i\" is a variable containing the number that the \"@\" wildcard replaces for each file/folder name\n"
    printf "                \"expr(i)\" represents an arithmetic expression containing any of: the variable \"i\" (without the \$ in front of it), \"-\", \"+\", \"(\", \")\", SPACE, TAB. Note that only addition and substraction are permitted - this is with of scope of not loosing information from file names\n"
    printf "                To pad the current arithmetic expression evaluation (\"expr(i)\") with N zeroes use the syntax: \"[(0N)expr(i)]\"\n"
    printf "            Example of arithmetic expression: [i+1]\n"
    printf "        <directory_path> represents the directory in which to try to match <mask1> and in which to perform the rename operation.\n"
    printf "        <condition>:\n"
    printf "            A { <condition> } has the syntax of an \"if [ <condition> ]; then\" statement, in which \"-o\" (logical OR) and \"-a\" (logical AND) can be used, except for the fact that the variable \"i\" is used without the character \$ in front (i.e.: i and not \$i). See above what \"i\" represents.\n"
    printf "            Like with the \"if\" syntax, subconditions can be grouped with escaped paranthesis: \(, \)\n"
    printf "            For example: { \(i -ge 10 -a i -le 100\) -o \(i -ge 210 -a i -le 300\) } - represents a valid condition\n"
    printf "        [flags] can be:\n"
    printf "            --help or -h\n"
    printf "                Displays this help information\n"
    printf "            --install or -i\n"
    printf "                Creates an executable symbolic link, in the location (directory) of the script\n"
    printf "            --file or -f\n"
    printf "                Tries to match file names (default)\n"
    printf "            --dir or -d\n"
    printf "                Tries to match directory names\n"
    printf "            --preview or -p\n"
    printf "                Previews the rename operation instead of properly renaming\n"
    printf "\n"
    printf "    Recommendations/Notes:\n"
    printf "       To avoid unexpected behaviour, always quote parameters.\n"
    printf "       In order for this program to work correctly: new files must not be added and files must not be renamed/deleted in \"<directory_path>\", while the program is running!\n"
    printf "       Also, the rename operation shall not be interrupted (there are two major steps: Analyze and Preview/Rename).\n"
    printf "       For more complex file name opperations, this script can be used together with the \"mmv\" utility\n"
    printf "       Please note that: if they contain the newline character in them:\n"
    printf "        - file names are ignored\n"
    printf "        - masks are not handled properly\n"
    printf "       When temporary file name collision is detected, the \"▄\" character is used at the beginning of the file names for two step rename\n"
    printf "\n"
}


## MAIN START ##

#Store initial IFS:
old_IFS="$IFS"
IFS=" $(printf "\t")
";

started_rn="false"
E="ERROR"

DetectShell crt_shell

#Trap CTRL-C, CTRL-Z:
trap 'trap1' INT
trap 'trap1' TSTP

err="false"
ls -1 "/dev/null">/dev/null 2>/dev/null || {
    printf "\n$E: \"ls -1\" (list one file per line) is needed in order for this program to function correctly!\n\n"
    err="true"
}

printf "test"|sort -n>/dev/null 2>/dev/null || {
    printf "\n$E: \"sort -n\" (numeric sort) is needed in order for this program to function correctly!\n\n"
    err="true"
}

if [ "$err" = "true" ]; then
    CleanUp && exit 1
fi

#Get the program parameters into the array "params":
count=0
for i; do
    count=$((count+1))
    eval params_$count=\"\$i\"
done
params_0=$((count))

if [ "$params_0" = "0" ]; then #if no parameters are provided: display help
    DisplayHelp
    CleanUp && exit 0
fi


#Create a flags array. A flag denotes special parameters:
help_flag="0"
install_flag="0"
file_flag="0"
directory_flag="0"
preview_flag="0"
i=1;
j=0;
while [ "$i" -le "$((params_0))" ]; do
    eval params_i=\"\$\{params_$i\}\"
    case "${params_i}" in
    "--help" | "-h" )
        help_flag="1"
    ;;
    "--install" | "-i" )
        install_flag="1"
    ;;
    "--file" | "-f" )
        file_flag="1"
    ;;
    "--dir" | "-d" )
        directory_flag="1"
    ;;
    "--preview" | "-p" )
        preview_flag="1"
    ;;

    * )
        j=$((j+1))
        eval selected_params_$j=\"\$params_i\"
    ;;
    esac

    i=$((i+1))
done

selected_params_0=$j
#Rebuild params array:
DestroyArray params
for i in $(seq 1 $selected_params_0); do
    eval params_$i=\"\$\{selected_params_$i\}\"
done
params_0=$selected_params_0

if [ "$help_flag" = "1" ]; then
    DisplayHelp
elif [ "$install_flag" = "1" ]; then

    file_path="$0"
    file_name_plus_ext="${file_path##*/}"
    file_dir_path="${file_path%/*}"
    file_name="${file_name_plus_ext%.*}"
    link_name="$file_name"
    install_path="$file_dir_path"

    #Install script (doesn't require root):
    {
        cd "$install_path"
    } && {
        ln -sfn "$file_name_plus_ext" "$link_name" #Create the link (or override/update it)
    } && {
        chmod u+x "$link_name" #Make the link 'executable' (+x) for the current user (u)"
    } && {
        printf '%s\n' "If not done already, please update the PATH variable (for the current user) by adding the next line to the end of \"~/.bashrc\" file or \"~/.zshrc\" (depending on the shell used) file:"
        printf '\n%s\n\n' "export PATH=\"\$PATH:\"'$install_path' #This is for updating the PATH variable"
        printf "and then close and reopen the terminal, in order for the new PATH to be loaded.\n"
    }||{
        printf '\n%s\n\n' "Could not install to the location: \"$install_path\"!"
        CleanUp && exit 1
    }
else
    #RUN PROGRAM

    PrintJustInTitle "A: Initialising, please wait..."


    if [ "$directory_flag" = "1" ]; then
        if [ ! "$file_flag" = "1" ]; then
            dir="-d"; file="-d"
        else
            dir="-d"; file="-f"
        fi
    else
        dir="-f"; file="-f" #(default)
    fi

    err="false"

    #Detect paths, conditions and parameters that are not paths or conditions:

    i=3; #count - jump over <mask1> and <mask2>
    p=0; #number of parameters that are paths
    c=0; #number of parameters that are conditions
    n=0; #number of parameters that are not paths or conditions

    extra_cond="\$i -eq \$i" #this condition is used to test if "$i" is a number or not
    one_cond="1 -eq 1" #default value in case one_cond is not set further

    while [ "$i" -le "$params_0" ]; do
        eval params_i=\"\$params_$i\"
        GetCharsArray params_i params_i_chars_array

        first_char="$params_i_chars_array_1"
        eval last_char=\"\$params_i_chars_array_$params_i_chars_array_0\"

        if [ "$first_char" = "{" -a "$last_char" = "}" ]; then
            c=$((c+1))
            one_cond="${params_i#?}" #? = Remove "{" prefix:
            one_cond="${one_cond%?}" # %? = Remove "}" suffix:
            GetCharsArray one_cond one_cond_chars_array
            j=1
            new_one_cond="" #new_one_cond should contain i replaced with $i
            while [ "$j" -le "$one_cond_chars_array_0" ]; do
                eval crt_char=\"\$one_cond_chars_array_$j\"
                if [ "$crt_char" = "i" ]; then
                    new_one_cond="$new_one_cond""\$i"
                else
                    new_one_cond="$new_one_cond""$crt_char"
                fi
                j=$((j+1))
            done
            one_cond="$new_one_cond"
        elif [ \( "$params_i" = "." -o "$params_i_chars_array_1$params_i_chars_array_2" = "./" -o "$params_i_chars_array_1$params_i_chars_array_2$params_i_chars_array_3" = "../" -o "$params_i_chars_array_1" = "/" \) ]; then #if a path is detected
            p=$((p+1))
            one_path="$params_i"
        else
            n=$((n+1))
        fi
        i=$((i+1))
    done

    set +f #enable globbing

    mask1="$params_1"
    mask2="$params_2"
    directory="$one_path"

    printf 'mask1="%s"\n' "$mask1"
    printf 'mask2="%s"\n' "$mask2"
    printf 'directory="%s"\n' "$directory"
    if [ ! "$c" = "0" ]; then
        printf '%s\n' "Condition = $one_cond"
    fi
    printf "\n"

    err=false

    if [ "$mask1" = "" ]; then
        printf "$E: <mask1> must not be empty!\n"
        err=true
    fi

    if [ "$mask2" = "" ]; then
        printf "$E: <mask2> must not be empty!\n"
        err=true
    fi

    #mask1 must contain a "@" character = number wildcard:
    first_rem_part="${mask1#*"@"}"
    if [ ! "$first_rem_part" = "${mask1}" ]; then #mask1 contains @
        second_rem_part="${first_rem_part#*"@"}"
        if [ ! "$second_rem_part" = "${first_rem_part}" ]; then #mask1 contains two @
            printf "$E: <mask1> must contain exactly one @ wildcard!\n"
            err=true
        fi
    else #mask1 does not contain @
        printf "$E: <mask1> must contain the @ wildcard!\n"
        err=true
    fi

    if [ "$directory" = "" ]; then
        printf "$E: <directory_path> must not be empty (must be \".\" or start with \"./\" or \"/\")!\n"
        err=true
    fi

    if [ -n "$mask1" -a "${mask1%%*"/"*}" = "" ]; then
        printf "$E: <mask1> must not contain directory sepparators ('/')!\n"
        err=true
    fi
    if [ -n "$mask2" -a "${mask2%%*"/"*}" = "" ]; then
        printf "$E: <mask2> must not contain directory sepparators ('/')!\n"
        err=true
    fi

    if [ "$n" -gt "0" ]; then
        printf "$E: Too many parameters: expected <mask1>, <mask2>, <directory_path> and eventually <condition>!\n"
        err="true"
    fi

    if [ "$p" -gt "1" ]; then
        printf "\n$E: Too many <directory_path> parameters provided: expected one after <mask1> and <mask2>!\n"
        err="true"
    fi

    if [ "$c" -gt "1" ]; then
        printf "\n$E: Too many <condition> parameters provided: expected one after <mask1> and <mask2>!\n"
        err="true"
    fi

    if [ "$err" = "true" ]; then
        printf "\n"
        CleanUp && exit 1
    fi

    rn_from_0=0
    #<mask1> contains exactly one @ wildcard

    #Generate pylon1 and pylon2 for mask1:
    pylon1_mask1="${mask1%%"@"*}"
    pylon2_mask1="${mask1#*"@"}"

    #Generate pylon1 and pylon2 for mask2:
    pylon1_mask2="${mask2%%"["*}"
    pylon2_mask2="${mask2##*"]"}"

    #Detect arithmetic expression:
    found_arith_expr="true"
    arith_expr="$mask2"
    if [ "${mask2#*"["}" = "$mask2" ]; then
        found_arith_expr="false"
    else
        arith_expr="${mask2#*"["}"
        if [ "${arith_expr%"]"*}" = "$arith_expr" ]; then
            found_arith_expr="false"
        else
            arith_expr="${arith_expr%"]"*}"
        fi
    fi
    if [ "$found_arith_expr" = "false" ]; then
        printf "\n$E: Expected at least one non-empty arithmetic expression ([...]) in <mask2>!\n\n"
        CleanUp && exit 1
    fi

    #Validate arithmetic expression (characters permitted are +, -, SPACE, TAB, (, ) and i ):
    GetCharsArray arith_expr arith_expr_chars_array
    #Count special characters in arithmetic expression:
    i=1;
    count=0;
    while [ "$i" -le "$arith_expr_chars_array_0" ]; do
        eval crt_char=\"\$arith_expr_chars_array_$i\"
        case "$crt_char" in
            [0-9] | " " | "\t" | "+" | "-" | "(" | ")" | "i" )
            count=$((count+1))
        ;;
        esac
        i=$((i+1))
    done
    if [ "$arith_expr_chars_array_0" -gt "$count" ]; then
        printf "\n$E: Characters not permited in arithmetic expression (the characters permitted are: +, -, SPACE, TAB, (, ) and i)!\n\n"
        CleanUp && exit 1
    fi

    #Check if the arithmetic expression has padding activated:
    #initially, assume no padding:
    arith_expr_padding="false"
    arith_expr_wo_padding="${arith_expr#"(0"*")"}"
    if [ -n "$arith_expr" ]; then
        if [ -n "$arith_expr_wo_padding" ]; then
            arith_expr_padding="${arith_expr%"$arith_expr_wo_padding"}"
            arith_expr_padding_number="${arith_expr_padding#"(0"}"
            arith_expr_padding_number="${arith_expr_padding_number%")"}"

            ValidateNaturalNumber arith_expr_padding_number result
            if [ "$result" = "true" ]; then
                #remove padding from the arithmetic expression:
                arith_expr="$arith_expr_wo_padding"
            else
                printf "\n$E: Invalid padding, in arithmetic expression!\n\n"
                CleanUp && exit 1
            fi
        else
            printf "\n$E: Number expected after padding, in arithmetic expression!\n\n"
            CleanUp && exit 1
        fi
    else
        printf "\n$E: Arithmetic expression must not be empty!\n\n"
        CleanUp && exit 1
    fi

    initial_dir="$PWD" #Store initial dir
    cd "$directory" 1>/dev/null 2>&1||{
        printf '\n%s\n\n' "$E: Cannot access directory: \"$directory\"!"
        CleanUp && exit 1
    }

    #Proceed to find matching file names and store them in numerically sorted order:
    #Check file names starting with "▄" (error):
    j=0
    k=0
    current_IFS="$IFS";
    IFS="
";
    err="false"
    for f in $(ls -1|sort -n); do

        j=$((j+1))

        PrintJustInTitle "A: Step 1 of 4 - checking file name ${j}..."

        if [ ! "$f" = "${f#"▄"}" ]; then #first char is "▄"
            printf '%s\n' "$E: The first character of \"$f\" is \"▄\" (probably remainings from a interrupted temporary rename)!"
            err="true"
        elif [ ! \( "$err" = "true" \) -a \( \( $file "$f" \) -o \( $dir "$f" \) \) ]; then
            case $f in
                *"$pylon1_mask1"*[0-9]*"$pylon2_mask1"* ) #$f is match for <mask1>

                    ProcedureGetMarginsAndNumber f

                    i=$number
                    eval [ \\\( $extra_cond \\\) -a \\\( $one_cond \\\) ] 2>/dev/null && {
                        #If <condition> is met:

                        #input file name = margin1 pylon1_mask1 number pylon2_mask1 margin2

                        k=$((k+1))

                        eval rn_from_$k=\"\$f\"
                        eval numbers_$k=\"\$number\"
                        eval margin1_$k=\"\$margin1\"
                        eval margin2_$k=\"\$margin2\"
                    }
                ;;
            esac
        fi
    done
    IFS="$current_IFS";
    eval rn_from_0=$((k))

    if [ "$err" = "true" ]; then
        printf "\n"
        CleanUp && exit 1
    fi

    if [ "$rn_from_0" = "0" ]; then
        printf "\nNo file names match the specified mask!\n\n"
        CleanUp && exit 0
    fi

    #Generate 'renamed to' file names:
    arith_err_encountered="false"
    for ii in $(seq 1 $rn_from_0); do

        PrintJustInTitle "A: Step 2 of 4: file name $ii of ${rn_from_0}..."

        #output file name = margin1 pylon1_mask2 arith_expr(number) pylon2_mask2 margin2

        eval new_f_n=\"\$margin1_$ii\$pylon1_mask2\"
        eval i=\"\$numbers_$ii\"; #should be ok becauses numbers are validated at mask1 matching; i is used in arithmetic expressions

        #Remove Zero's from the beginning of "i":
        sign=""
        if [ ! "${i#"-"}" = "$i" ]; then #i has a '-' sign
            sign="-"
        fi
        t="${i#"$sign""0"*[1-9]}"
        zeros="${i%"$t"}"
        zeros="${zeros%?}" #Remove last non-0 digit
        zeros="${zeros#"-"}" #Remove "-" sign
        i="$sign""${i#"$sign""$zeros"}"

        if [ "$crt_shell" = "dash" ]; then
            arith_expr_eval=$(($arith_expr)) || arith_err_encountered="true" #halts on error; catch error output
        else
            eval arith_expr_eval=\$\(\($arith_expr\)\) 2>/dev/null || arith_err_encountered="true" #does not halt on error; don't catch error output
        fi

        if [ "$arith_err_encountered" = "false" ]; then
            if [ ! "$arith_expr_padding" = "false" ]; then
                arith_expr_eval_padded="$(eval printf \"\%0$arith_expr_padding_number\d\" \"$arith_expr_eval\")"
                unset arith_expr_eval #zsh bug?
                arith_expr_eval="$arith_expr_eval_padded"
            fi
            new_f_n="$new_f_n$arith_expr_eval"
        else break; fi

        eval new_f_n=\"\$new_f_n\$pylon2_mask2\$margin2_$ii\"
        eval rn_to_$ii=\"\$new_f_n\"
    done
    rn_to_0=$rn_from_0

    if [ "$arith_err_encountered" = "true" ]; then
        printf "Arithmetic errors were encoutered: nothing done!\n\n"
        CleanUp && exit 1
    fi

    #Check if the arithmetic expression is constant:
    if [ ! "$rn_from_0" = "0" ]; then
        i=1
        number1=$(($arith_expr))
        i=2
        number2=$(($arith_expr))
        if [ "$number1" = "$number2" ]; then
            printf "$E: Constant arithmetic expression not allowed (=$number1)!\n\n"
            CleanUp && exit 1
        fi
    fi

    coll_err="false"

    ### Check for file name collision (1):
    coll_chk_1_markings_0=$rn_to_0
    PrintJustInTitle "A: Step 3 of 4: file name 1 of ${rn_to_0}..."
    if [ "$rn_to_0" -ge "2" ]; then
        prev_rn_from_fn="$rn_from_1"
        prev_rn_to_fn="$rn_to_1"
        fn_coll_detected="false"
        for ii in $(seq 2 $rn_to_0); do
            PrintJustInTitle "A: Step 3 of 4: file name $ii of ${rn_to_0}..."
            eval crt_rn_from_fn=\"\$rn_from_$ii\"
            eval crt_rn_to_fn=\"\$rn_to_$ii\"

            if [ "$prev_rn_to_fn" = "$crt_rn_to_fn" ]; then
                eval coll_chk_1_markings_$ii="1"
                eval coll_chk_1_markings_$((ii-1))="1"
                if [ "$fn_coll_detected" = "false" ]; then
                    printf '%s' "$E: File name collision: \"$prev_rn_from_fn\"; \"$crt_rn_from_fn\""
                    fn_coll_detected="true"
                    coll_err="true"
                else
                    printf '%s' "; \"$crt_rn_from_fn\""
                fi
            else
                if [ "$fn_coll_detected" = "true" ]; then
                    printf '%s\n' " -> \"$prev_rn_to_fn\""
                    fn_coll_detected="false"
                fi
            fi

            prev_rn_from_fn="$crt_rn_from_fn"
            prev_rn_to_fn="$crt_rn_to_fn"
        done
        #Treat last file separately:
        if [ "$fn_coll_detected" = "true" ]; then
            eval coll_chk_1_markings_$ii="1"
            printf '%s\n' " -> \"$crt_rn_to_fn\""
            fn_coll_detected="false"
        fi
    fi

    ### Check for temporary file name collision (2):
    temp_coll_detected="false"
    for j in $(seq 1 $rn_to_0); do
        PrintJustInTitle "A: Step 4 of 4: file name $j of ${rn_to_0}..."
        eval crt_rn_to_fn=\"\$rn_to_$j\"
        eval crt_rn_from_fn=\"\$rn_from_$j\"
        eval crt_coll_chk_1_marking=\"\$coll_chk_1_markings_$j\"
        if [ ! "$crt_coll_chk_1_marking" = "1" ]; then
            if [ -e "$crt_rn_to_fn" ]; then
                if [ \( $file "$crt_rn_to_fn" \) -o \( $dir "$crt_rn_to_fn" \) ]; then
                    case $crt_rn_to_fn in
                        *"$pylon1_mask1"*[0-9]*"$pylon2_mask1"* ) #file name is match for <mask1>

                            ProcedureGetMarginsAndNumber crt_rn_to_fn

                            i=$number
                            eval [ \\\( $extra_cond \\\) -a \\\( $one_cond \\\) ] 2>/dev/null && {
                                if [ ! "$temp_coll_detected" = "true" ]; then
                                    temp_coll_detected="true"
                                fi
                                eval marks_temp_fns_$j=\"\1\"
                            } || {
                                if [ ! "$coll_err" = "true" ]; then
                                    coll_err="true"
                                fi
                                eval marks_temp_fns_$j=\"\0\"
                                printf '%s\n' "$E: file name collision: $crt_rn_from_fn -> $crt_rn_to_fn"
                            }
                        ;;
                    esac
                else
                    if [ ! "$coll_err" = "true" ]; then
                        coll_err="true"
                    fi
                    printf '%s\n' "$E: file name collision: $crt_rn_from_fn -> $crt_rn_to_fn"
                fi
            else
                eval marks_temp_fns_$j=\"\0\"
            fi
        fi
    done

    if [ "$coll_err" = "true" ]; then
        if [ "$preview_flag" = "1" ]; then
            printf "\nPreview - File name collision errors were encoutered!\n\n"
        else
            printf "\nRename - File name collision errors were encoutered: nothing done!\n\n"
        fi
        CleanUp && exit 1
    fi

    #If no errors were encountered:

    if [ "$preview_flag" = "1" ]; then

        printf "Previewing rename:\n"
        for i in $(seq 1 $rn_from_0); do

            PrintJustInTitle "P: Step 1 of 1: file name $i of ${rn_from_0}..."

            eval crt_rn_from_fn=\"\$rn_from_$i\"
            eval crt_rn_to_fn=\"\$rn_to_$i\"
            printf '%s\n' "$crt_rn_from_fn -> $crt_rn_to_fn"
        done
    else
        printf "\n"
        err_encoutered="false"

        #Disable CTRL-C, CTRL-Z while the rename is in progress:
        trap '' INT
        trap '' TSTP

        printf "Rename in progress.. Please do not interrupt...\n">/dev/stdin

        if [ "$temp_coll_detected" = "false" ]; then
            for i in $(seq 1 $rn_from_0); do

                PrintJustInTitle "R: Step 1 of 1: file name $i of ${rn_from_0}..."

                eval crt_rn_from_fn=\"\$rn_from_$i\"
                eval crt_rn_to_fn=\"\$rn_to_$i\"
                {
                    mv -n "./$crt_rn_from_fn" "./$crt_rn_to_fn"
                }&&{
                    started_rn="true"
                }||{
                    printf '%s\n' "$E: RENAMING \"$crt_rn_from_fn\" TO \"$crt_rn_to_fn\"!"
                    err_encoutered="true"
                }
            done
        elif [ "$temp_coll_detected" = "true" ]; then

            for i in $(seq 1 $rn_from_0); do

                PrintJustInTitle "R: Step 1 of 3: file name $i of ${rn_from_0}..."

                eval crt_rn_from_fn=\"\$rn_from_$i\"
                eval crt_rn_to_fn=\"\$rn_to_$i\"
                eval crt_marks_temp_fn=\"\$marks_temp_fns_$i\"
                if [ "$crt_marks_temp_fn" = "0" ]; then

                    {
                        mv -n "./$crt_rn_from_fn" "./$crt_rn_to_fn"
                    }&&{
                        started_rn="true"
                    }||{
                        printf '%s\n' "$E: RENAMING \"$crt_rn_from_fn\" TO \"$crt_rn_to_fn\"!"
                        err_encoutered="true"
                    }
                fi
            done

            for i in $(seq 1 $rn_from_0); do

                PrintJustInTitle "R: Step 2 of 3: file name $i of ${rn_from_0}..."

                eval crt_rn_from_fn=\"\$rn_from_$i\"
                eval crt_rn_to_fn=\"\$rn_to_$i\"
                eval crt_marks_temp_fn=\"\$marks_temp_fns_$i\"
                if [ "$crt_marks_temp_fn" = "1" ]; then
                    if [ ! "$crt_rn_from_fn" = "$crt_rn_to_fn" ]; then #skip rename to itself cases
                        {
                            mv -n "./$crt_rn_from_fn" "./▄$crt_rn_to_fn"
                        }&&{
                            started_rn="true"
                        }||{
                            printf '%s\n' "$E: RENAMING \"$crt_rn_from_fn\" TO TEMPORARY FILE NAME \"▄$crt_rn_to_fn\"!"
                            err_encoutered="true"
                        }
                    fi
                fi
            done

            for i in $(seq 1 $rn_from_0); do

                PrintJustInTitle "R: Step 3 of 3: file name $i of ${rn_from_0}..."

                eval crt_rn_from_fn=\"\$rn_from_$i\"
                eval crt_rn_to_fn=\"\$rn_to_$i\"
                eval crt_marks_temp_fn=\"\$marks_temp_fns_$i\"
                if [ "$crt_marks_temp_fn" = "1" ]; then
                    if [ ! "$crt_rn_from_fn" = "$crt_rn_to_fn" ]; then #skip rename to itself cases
                        #started_rn is "true"
                        {
                            mv -n "./▄$crt_rn_to_fn" "./$crt_rn_to_fn"
                        }||{
                            printf '%s\n' "$E: RENAMING TEMPORARY FILE NAME \"▄$crt_rn_to_fn\" TO \"$crt_rn_to_fn\"!"
                            err_encoutered="true"
                        }
                    fi
                fi
            done
        fi

        printf "\n\nRename done.\n">/dev/stdin
    fi

    PrintJustInTitle "Done. Cleaning up..."

    CleanUp

fi
 
## MAIN END ##
| improve this answer | |
0

Easier:

touch file`ls file* | wc -l`.ext

You'll get:

$ ls file*
file0.ext  file1.ext  file2.ext  file3.ext  file4.ext  file5.ext  file6.ext
| improve this answer | |
  • How would I add the leading zero for 1-9? Also remember there may be two files 03.png and 03.svg. – robertc Apr 5 '18 at 11:39
  • This is barely close to what the question asks for.  (0) When the OP provides sample data, it’s rude to make up your own sample data in your answer.  Worse, it’s not clear that you even looked at the OP’s data.  (1) With the OP’s data — files ranging from 01 to 10 (Note: two-digit numbers (with leading zeros) starting at 01 (not 0) ) and the requirement to insert a new image #5, he needs to rename the 05-10 files to 06-11.  But your code creates a file12.ext (because, as the OP points out, two of the numbers between  01 and 10 are represented twice),  … (Cont’d) – G-Man Says 'Reinstate Monica' Jun 23 at 0:45
  • (Cont’d) …  which isn’t helpful.   (2) As the OP points out, if there are fewer than ten files, your code will not generate the required leading zero. – G-Man Says 'Reinstate Monica' Jun 23 at 0:46

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.