#!/bin/bash #@ This program came from: ftp://ftp.armory.com/pub/scripts/ren #@ Look there for the latest version. #@ If you don't find it, look through http://www.armory.com/~ftp/ # # @(#) ren 2.1.1 2002-03-17 # 1990-06-01 John H. DuBois III (john@armory.com) # 1991-02-25 Improved help info # 1992-06-07 Remove quotes from around shell pattern as required by new ksh # 1994-05-10 Exit if no globbing chars given. # 1995-01-23 Allow filename set to be given on command line. # 1997-09-24 1.4 Let [] be used for globbing. Added x option. # 1997-11-26 1.4.1 Notice if the sequences of globbing chars aren't the same. # 1999-05-13 Changed name to ren to avoid conflict with /etc/rename # 2000-01-01 1.4.2 Let input patterns that contain whitespace be used. # 2001-02-14 1.5 Better test for whether old & new globbing seqs are identical. # 2001-02-20 1.6 Added pP options. # 2001-02-27 1.7 Added qf options. Improved interpretation of rename patterns. # 2001-05-10 1.8 Allow multiple pP options. Added Qr options. # 2001-07-25 2.0 Added mz options. # 2001-11-25 2.1 Allow segment ranges to be given with -m. Work under ksh93. # 2002-03-17 2.1.1 Fixed bug in test for legal expressions. # todo: It would be nice to be able to escape metacharacters with '\' # todo: Should enhance patterns to make ] in a pair of brackets work ([]]) # todo: Allow use of all ksh globbing patterns. # todo: Allow use of extended regexps, with () to enumerate pieces and \num to # todo: select them. # # Modifications for bash made by Chet Ramey name=${0##*/} Usage="Usage: $name [-fhqtv] [-m] [-z] [-[pP]] oldpattern [newpattern [filename ...]] or $name -r [same options as above] oldpattern newpattern directory ..." tell=false verbose=false warn=true warnNoFiles=true debug=false recurse=false inclPat= exclPat= declare -i inclCt=0 exclCt=0 check=true declare -i j op_end_seg # Begin bash additions shopt -s extglob # # ksh print emulation # # print [-Rnprsu[n]] [-f format] [arg ...] # # - end of options # -R BSD-style -- only accept -n, no escapes # -n do not add trailing newline # -p no-op (no coprocesses) # -r no escapes # -s print to the history file # -u n redirect output to fd n # -f format printf "$format" "$@" # print() { local eflag=-e local nflag= fflag= c local fd=1 OPTIND=1 while getopts "fRnprsu:" c do case $c in R) eflag= ;; r) eflag= ;; n) nflag=-n ;; s) sflag=y ;; f) fflag=y ;; u) fd=$OPTARG ;; p) ;; esac done shift $(( $OPTIND - 1 )) if [ -n "$fflag" ]; then builtin printf "$@" >&$fd return fi case "$sflag" in y) builtin history -s "$*" ;; *) builtin echo $eflag $nflag "$@" >&$fd esac } # End bash additions while getopts :htvxp:P:fqQrm:z: opt; do case $opt in h) print -r -- \ "$name: rename files by changing parts of filenames that match a pattern. $Usage oldpattern and newpattern are subsets of sh filename patterns; the only globbing operators (wildcards) allowed are ?, *, and []. All filenames that match oldpattern will be renamed with the filename characters that match the constant (non-globbing) characters of oldpattern changed to the corresponding constant characters of newpattern. The characters of the filename that match the globbing operators of oldpattern will be preserved. Globbing operators in oldpattern must occur in the same order in newpattern; for every globbing operators in newpattern there must be an identical globbing operators in oldpattern in the same sequence. Both arguments should be quoted since globbing operators are special to the shell. If filenames are given, only those named are acted on; if not, all filenames that match oldpattern are acted on. newpattern is required in all cases except when -m is given and no further arguments are given. If you are unsure whether a $name command will do what you intend, issue it with the -t option first to be sure. Examples: $name \"/tmp/foo*.ba.?\" \"/tmp/new*x?\" All filenames in /tmp that match foo*.ba.? will have the \"foo\" part replaced by \"new\" and the \".ba.\" part replaced by \"x\". For example, /tmp/fooblah.ba.baz would be renamed to /tmp/newblahxbaz. $name \* \*- foo bar baz foo, bar, and baz will be renamed to foo-, bar-, and baz-. $name '????????' '????-??-??' All filenames that are 8 characters long will be changed such that dashes are inserted after the 4th and 6th characters. Options: -h: Print this help. -r: Recursive operation. Filenames given on the command line after oldpattern and newpattern are taken to be directories to traverse recursively. For each subdirectory found, the specified renaming is applied to any matching filenames. oldpattern and newpattern should not include any directory components. -p, -P: Act only on filenames that do (if -p is given) or do not (if -P is given) match the sh-style filename globbing pattern . This further restricts the filenames that are acted on, beyond the filename selection produced by oldpattern and the filename list (if any). must be quoted to prevent it from being interpreted by the shell. Multiple instances of these options may be given. In this case, filenames are acted on only if they match at least one of the patterns given with -p and do not match any of the patterns given with -P. -m: For each file being renamed, perform a mathematical operation on the string that results from concatenating together the filename segments that matched globbing operator numbers segstart through segend, where operators are numbered in order of occurrence from the left. For example, in the pattern a?b*c[0-9]f, segment 1 consists of the character that matched ?, segment 2 consists of the character(s) that matched *, and segment 3 consists of the character that matched [0-9]. The selected segments are replaced with the result of the mathematical operation. The concatenated string must consist of characters that can be interpreted as a decimal integer; if it does not, the filename is not acted on. This number is assigned to the variable 'i', which can be referenced by the operation. The operations available are those understood by the ksh interpreter, which includes most of the operators and syntax of the C language. The original filename segment is replaced by the result of the operation. If -m is used, newpattern may be an empty string or not given at all (if no directory/file names are given). In this case, it is taken to be the same as oldpattern. If segend is given, any fixed text that occurs in the pattern between the starting and ending globbing segments is discarded. If there are fewer globbing segments than segend, no complaint is issued; the string is formed from segment segstart through the last segment that does exist. If segend is not given, the only segment acted on is startseg. Examples: $name -m3=i+6 '??*.ppm' This is equivalent to: $name -m3=i+6 '??*.ppm' '??*.ppm' Since the old pattern and new pattern are identical, this would normally be a no-op. But in this case, if a filename of ab079.ppm is given, it is changed to ab85.ppm. $name '-m1:2=i*2' 'foo??bar' This will change a file named foo12bar to foo24bar $name '-m1:2=i*2' 'foo?xyz?bar' This will also change a file named foo1xyz2bar to foo24bar -z: Set the size of the number fields that result when -m is used. The field is truncated to the trailing digits or filled out to digits with leading zeroes. In the above example, if -z3 is given, the output filename will be ab085.ppm. -f: Force rename. By default, $name will not rename files if a file with the new filename already exists. If -f is given, $name will carry out the rename anyway. -q: Quiet operation. By default, if -f is given, $name will still notify the user if a rename results in replacement of an already-existing filename. If -q is given, no notification is issued. -Q: Suppress other warnings. By default, a warning is issued if no files are selected for acting upon. If -Q is given, no warning is issued. -v: Show the rename commands being executed. -t: Show what rename commands would be done, but do not carry them out." exit 0 ;; f) check=false ;; q) warn=false ;; Q) warnNoFiles=false ;; r) warnNoFiles=false recurse=true ;; t) tell=true ;; v) verbose=true ;; x) verbose=true debug=true ;; p) inclPats[inclCt]=$OPTARG ((inclCt+=1)) ;; P) exclPats[exclCt]=$OPTARG ((exclCt+=1)) ;; m) # Store operation for each segment number in ops[num] # Store ending segment number in op_end_seg[num] range=${OPTARG%%=*} op=${OPTARG#*=} start=${range%%:*} end=${range#*:} if [[ "$start" != +([0-9]) || "$start" -eq 0 ]]; then print -ru2 -- "$name: Bad starting segment number given with -m: $start" exit 1 fi if [[ "$end" != +([0-9]) || "$end" -eq 0 ]]; then print -ru2 -- "$name: Bad ending segment number given with -m: $end" exit 1 fi if [[ start -gt end ]]; then print -ru2 -- "$name: Ending segment ($end) is less than starting segment ($start)" exit 1 fi if [[ "$op" != @(|*[!_a-zA-Z0-9])i@(|[!_a-zA-Z0-9]*) ]]; then print -ru2 -- \ "$name: Operation given with -m does not reference 'i': $op" exit 1 fi # Test whether operation is legal. let returns 1 both for error # indication and when last expression evaluates to 0, so evaluate 1 # after test expression. i=1 let "$op" 1 2>/dev/null || { print -ru2 -- \ "$name: Bad operation given with -m: $op" exit 1 } ops[start]=$op op_end_seg[start]=$end ;; z) if [[ "$OPTARG" != +([0-9]) || "$OPTARG" -eq 0 ]]; then print -ru2 -- "$name: Bad length given with -z: $OPTARG" exit 1 fi typeset -Z$OPTARG j || exit 1 ;; +?) # no way to tell getopts to not treat +x as an option print -r -u2 "$name: Do not prefix options with '+'." exit 1 ;; :) print -r -u2 \ "$name: Option -$OPTARG requires a value. $Usage Use -h for help." exit 1 ;; \?) print -r -u2 \ "$name: -$OPTARG: no such option. $Usage Use -h for help." exit 1 ;; esac done # remove args that were options let OPTIND=OPTIND-1 shift $OPTIND oldpat=$1 newpat=$2 # If -m is given, a non-existant or null newpat should be set to oldpat if [ ${#ops[*]} -gt 0 ]; then case $# in 0) ;; 1) set -- "$oldpat" "$oldpat" newpat=$oldpat $debug && print -ru2 -- "Set new pattern to: $newpat" ;; *) if [ -z "$newpat" ]; then shift 2 set -- "$oldpat" "$oldpat" "$@" newpat=$oldpat $debug && print -ru2 -- "Set new pattern to: $newpat" fi ;; esac fi # Make sure input patterns that contain whitespace can be expanded properly IFS= origPat=$oldpat # Generate list of filenames to act on. case $# in [01]) print -u2 "$Usage\nUse -h for help." exit 1 ;; 2) if $recurse; then print -r -u2 "$name: No directory names given with -r. Use -h for help." exit 1 fi set -- $oldpat # Get list of all filenames that match 1st globbing pattern. if [[ ! -a $1 ]]; then $warnNoFiles && print -r -- "$name: No filenames match this pattern: $oldpat" exit fi ;; *) shift 2 ;; esac integer patSegNum=1 numPatSegs # For old ksh # while [[ "$oldpat" = *'[\*\?]'* ]]; do # Example oldpat: foo*.a # Example newpat: bar*.b # Build list of non-pattern segments and globbing segments found in arguments. # Note the patterns given are used to get the list of filenames to act on, # to delimit constant segments, and to determine which parts of filenames are # to be replaced. # Examples given for first iteration (in the example, the only iteration) # The || newpat is to ensure that new pattern does not have more globbing # segments than old pattern while [[ "$oldpat" = *@([\*\?]|\[+([!\]])\])* || "$newpat" = *@([\*\?]|\[+([!\]])\])* ]]; do ## Get leftmost globbing pattern in oldpat # Make r be oldpat with smallest left piece that includes a globbing # pattern removed from it r=${oldpat#*@([\*\?]|\[+([!\]])\])} # r=.a # Make pat be oldpat with the above removed from it, leaving smallest # left piece that includes a globbing pattern pat=${oldpat%%"$r"} # pat=foo* # Make l be pat with the globbing pattern removed from the right, # leaving a constant string l=${pat%@([\*\?]|\[+([!\]])\])} # l=foo # Remove the constant part of pat from the left, leaving the globbing # pattern pat=${pat#"$l"} # pat=* # Do the same thing for newpat, solely to provide a reliable test that # both oldpat & newpat contain exactly the same sequence of globbing # patterns. r=${newpat#*@([\*\?]|\[+([!\]])\])} # r=.b npat=${newpat%%"$r"} # pat=bar* l=${npat%@([\*\?]|\[+([!\]])\])} # l=bar npat=${npat#"$l"} # npat=* if [[ "$pat" != "$npat" ]]; then print -ru2 -- \ "$name: Old-pattern and new-pattern do not have the same sequence of globbing chars. Pattern segment $patSegNum: Old pattern: $pat New pattern: $npat" exit 1 fi ## Find parts before & after pattern # oldpre[] stores the old constant part before the pattern, # so that it can be removed and replaced with the new constant part. oldpre[patSegNum]=${oldpat%%"$pat"*} # oldpre[1]=foo # oldsuf stores the part that follows the globbing pattern, # so that it too can be removed. # After oldpre[] & oldsuf[] have been removed from a filename, what remains # is the part matched by the globbing pattern, which is to be retained. oldsuf[patSegNum]=${oldpat#*"$pat"} # oldsuf[1]=.a # newpre[] stores the new constant part before the pattern, # so that it can be used to replace the old constant part. newpre[patSegNum]=${newpat%%"$pat"*} # newpre[1]=bar # Get rid of processed part of patterns oldpat=${oldpat#${oldpre[patSegNum]}"$pat"} # oldpat=.a newpat=${newpat#${newpre[patSegNum]}"$pat"} # newpat=.b # Store either * or ? in pats[], depending on whether this segment matches 1 # or any number of characters. [[ "$pat" = \[* ]] && pat=? pats[patSegNum]=$pat ((patSegNum+=1)) done if [ patSegNum -eq 1 ]; then print -u2 "No globbing chars in pattern." exit 1 fi oldpre[patSegNum]=${oldpat%%"$pat"*} # oldpre[2]=.a oldsuf[patSegNum]=${oldpat#*"$pat"} # oldsuf[2]=.a newpre[patSegNum]=${newpat%%"$pat"*} # newpre[2]=.b numPatSegs=patSegNum if $debug; then patSegNum=1 while [[ patSegNum -le numPatSegs ]]; do print -ru2 -- \ "Old prefix: <${oldpre[patSegNum]}> Old suffix: <${oldsuf[patSegNum]}> New prefix: <${newpre[patSegNum]}> Pattern: <${pats[patSegNum]}>" ((patSegNum+=1)) done fi # Example filename: foox.a # Example oldpat: foo*.a # Example newpat: bar*.b integer numFiles=0 # Usage: renameFile filename [dirname] # [dirname] is a directory name to prefix filenames with when they are printed # for informational purposes. # Uses globals: # inclCt exclCt inclPats[] exclPats[] ops[] # numPatSegs oldpre[] oldsuf[] newpre[] pats[] # check warn tell verbose name # Modifies globals: numFiles function renameFile { typeset file=$1 subdir=$2 integer patSegNum patnum typeset origname porigname newfile matchtext pnewfile matchsegs integer startseg endseg origname=$file # origname=foox.a porigname=$subdir$file # Unfortunately, ksh88 does not do a good job of allowing for patterns # stored in variables. Without the conditional expression being eval'ed, # only sh patterns are recognized. If the expression is eval'ed, full # ksh expressions can be used, but then expressions that contain whitespace # break unless the user passed a pattern with the whitespace properly # quoted, which is not intuititive. This is fixed in ksh93; full patterns # work without being eval'ed. if [ inclCt -gt 0 ]; then patnum=0 while [ patnum -lt inclCt ]; do [[ "$file" = ${inclPats[patnum]} ]] && break ((patnum+=1)) done if [ patnum -eq inclCt ]; then $debug && print -ru2 -- "Skipping not-included filename '$porigname'" return 1 fi fi patnum=0 while [ patnum -lt exclCt ]; do if [[ "$file" = ${exclPats[patnum]} ]]; then $debug && print -ru2 -- "Skipping excluded filename '$porigname'" return 1 fi ((patnum+=1)) done # Extract matching segments from filename ((numFiles+=1)) patSegNum=1 while [[ patSegNum -le numPatSegs ]]; do # Remove a fixed prefix iteration: 1 2 file=${file#${oldpre[patSegNum]}} # file=x.a file= # Save the part of this suffix that is to be retained. To do this, we # need to know what part of the suffix matched the current globbing # segment. If the globbing segment is a *, this is done by removing # the minimum part of the suffix that matches oldsuf (since * matches # the longest segment possible). If the globbing segment is ? or [] # (the latter has already been coverted to ?), it is done by taking the # next character. if [ "${pats[patSegNum]}" == \? ]; then matchtext=${file#?} matchtext=${file%$matchtext} else matchtext=${file%${oldsuf[patSegNum]}} # matchtext=x matchtext= fi $debug && print -ru2 -- "Matching segment $patSegNum: $matchtext" file=${file#$matchtext} # file=.a file=.a matchsegs[patSegNum]=$matchtext ((patSegNum+=1)) done # Paste fixed and matching segments together to form new filename. patSegNum=0 newfile= while [[ patSegNum -le numPatSegs ]]; do matchtext=${matchsegs[patSegNum]} startseg=patSegNum if [ -n "${ops[startseg]}" ]; then endseg=${op_end_seg[startseg]} while [ patSegNum -lt endseg ]; do ((patSegNum+=1)) matchtext=$matchtext${matchsegs[patSegNum]} done if [[ "$matchtext" != +([-0-9]) ]]; then print -ru2 -- \ "Segment(s) $startseg - $endseg ($matchtext) of file '$porigname' do not form an integer; skipping this file." return 2 fi i=$matchtext let "j=${ops[startseg]}" || { print -ru2 -- \ "Operation failed on segment(s) $startseg - $endseg ($matchtext) of file '$file'; skipping this file." return 2 } $debug && print -ru2 -- "Converted $matchtext to $j" matchtext=$j fi newfile=$newfile${newpre[startseg]}$matchtext # newfile=barx newfile=barx.b ((patSegNum+=1)) done pnewfile=$subdir$newfile if $check && [ -e "$newfile" ]; then $warn && print -ru2 -- "$name: Not renaming \"$porigname\"; destination filename \"$pnewfile\" already exists." return 2 fi if $tell; then print -n -r -- "Would move: $porigname -> $pnewfile" $warn && [ -e "$newfile" ] && print -n -r " (destination filename already exists; would replace it)" print "" else if $verbose; then print -n -r -- "Moving: $porigname -> $pnewfile" $warn && [ -e "$newfile" ] && print -n -r -- " (replacing old destination filename \"$pnewfile\")" print "" elif $warn && [ -e "$newfile" ]; then print -r -- "$name: Note: Replacing old file \"$pnewfile\"" fi mv -f -- "$origname" "$newfile" fi } if $recurse; then oPWD=$PWD find "$@" -depth -type d ! -name '* *' -print | while read dir; do cd -- "$oPWD" if cd -- "$dir"; then for file in $origPat; do renameFile "$file" "$dir/" done else print -ru2 -- "$name: Could not access directory '$dir' - skipped." fi done else for file; do renameFile "$file" done fi if [ numFiles -eq 0 ]; then $warnNoFiles && print -ru2 -- \ "$name: All filenames were excluded by patterns given with -p or -P." fi