#!/bin/bash
name=${0Usage="Usage:
$name [-fhqtv] [-m<segstart[:segend]=operation>] [-z<len>] [-[pP]<pattern>]
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
shopt -s extglob
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
}
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<pattern>, -P<pattern>: Act only on filenames that do (if -p is given) or do
not (if -P is given) match the sh-style filename globbing pattern
<pattern>. This further restricts the filenames that are acted on, beyond
the filename selection produced by oldpattern and the filename list (if
any). <pattern> 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<segstart[:segend]=operation>: 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<len>: Set the size of the number fields that result when -m is used. The
field is truncated to the trailing <len> digits or filled out to <len>
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)
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
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
;;
+?) 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
let OPTIND=OPTIND-1
shift $OPTIND
oldpat=$1
newpat=$2
if [ ${ case $ 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
IFS=
origPat=$oldpat
case $[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 if [[ ! -a $1 ]]; then
$warnNoFiles && print -r -- "$name: No filenames match this pattern: $oldpat"
exit
fi
;;
*)
shift 2
;;
esac
integer patSegNum=1 numPatSegs
while [[ "$oldpat" = *@([\*\?]|\[+([!\]])\])* ||
"$newpat" = *@([\*\?]|\[+([!\]])\])* ]]; do
r=${oldpat pat=${oldpat%%"$r"} l=${pat%@([\*\?]|\[+([!\]])\])} pat=${pat
r=${newpat npat=${newpat%%"$r"} l=${npat%@([\*\?]|\[+([!\]])\])} npat=${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
oldpre[patSegNum]=${oldpat%%"$pat"*} oldsuf[patSegNum]=${oldpat newpre[patSegNum]=${newpat%%"$pat"*} oldpat=${oldpat newpat=${newpat [[ "$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"*} oldsuf[patSegNum]=${oldpatnewpre[patSegNum]=${newpat%%"$pat"*}
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
integer numFiles=0
function renameFile {
typeset file=$1 subdir=$2
integer patSegNum patnum
typeset origname porigname newfile matchtext pnewfile matchsegs
integer startseg endseg
origname=$file porigname=$subdir$file
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
((numFiles+=1))
patSegNum=1
while [[ patSegNum -le numPatSegs ]]; do
file=${file if [ "${pats[patSegNum]}" == \? ]; then
matchtext=${file matchtext=${file%$matchtext}
else
matchtext=${file%${oldsuf[patSegNum]}} fi
$debug && print -ru2 -- "Matching segment $patSegNum: $matchtext"
file=${file
matchsegs[patSegNum]=$matchtext
((patSegNum+=1))
done
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 ((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