2 #@ This program came from: ftp://ftp.armory.com/pub/scripts/ren
3 #@ Look there for the latest version.
4 #@ If you don't find it, look through http://www.armory.com/~ftp/
6 # @(#) ren 2.1.1 2002-03-17
7 # 1990-06-01 John H. DuBois III (john@armory.com)
8 # 1991-02-25 Improved help info
9 # 1992-06-07 Remove quotes from around shell pattern as required by new ksh
10 # 1994-05-10 Exit if no globbing chars given.
11 # 1995-01-23 Allow filename set to be given on command line.
12 # 1997-09-24 1.4 Let [] be used for globbing. Added x option.
13 # 1997-11-26 1.4.1 Notice if the sequences of globbing chars aren't the same.
14 # 1999-05-13 Changed name to ren to avoid conflict with /etc/rename
15 # 2000-01-01 1.4.2 Let input patterns that contain whitespace be used.
16 # 2001-02-14 1.5 Better test for whether old & new globbing seqs are identical.
17 # 2001-02-20 1.6 Added pP options.
18 # 2001-02-27 1.7 Added qf options. Improved interpretation of rename patterns.
19 # 2001-05-10 1.8 Allow multiple pP options. Added Qr options.
20 # 2001-07-25 2.0 Added mz options.
21 # 2001-11-25 2.1 Allow segment ranges to be given with -m. Work under ksh93.
22 # 2002-03-17 2.1.1 Fixed bug in test for legal expressions.
24 # todo: It would be nice to be able to escape metacharacters with '\'
25 # todo: Should enhance patterns to make ] in a pair of brackets work ([]])
26 # todo: Allow use of all ksh globbing patterns.
27 # todo: Allow use of extended regexps, with () to enumerate pieces and \num to
30 # Modifications for bash made by Chet Ramey <chet@po.cwru.edu>
34 $name [-fhqtv] [-m<segstart[:segend]=operation>] [-z<len>] [-[pP]<pattern>]
35 oldpattern [newpattern [filename ...]]
37 $name -r [same options as above] oldpattern newpattern directory ..."
46 declare -i inclCt
=0 exclCt
=0
48 declare -i j op_end_seg
50 # Begin bash additions
56 # print [-Rnprsu[n]] [-f format] [arg ...]
59 # -R BSD-style -- only accept -n, no escapes
60 # -n do not add trailing newline
61 # -p no-op (no coprocesses)
63 # -s print to the history file
64 # -u n redirect output to fd n
65 # -f format printf "$format" "$@"
75 while getopts "fRnprsu:" c
87 shift $
(( $OPTIND - 1 ))
89 if [ -n "$fflag" ]; then
90 builtin printf "$@" >&$fd
95 y
) builtin history -s "$*" ;;
96 *) builtin echo $eflag $nflag "$@" >&$fd
102 while getopts :htvxp
:P
:fqQrm
:z
: opt
; do
106 "$name: rename files by changing parts of filenames that match a pattern.
108 oldpattern and newpattern are subsets of sh filename patterns; the only
109 globbing operators (wildcards) allowed are ?, *, and []. All filenames that
110 match oldpattern will be renamed with the filename characters that match the
111 constant (non-globbing) characters of oldpattern changed to the corresponding
112 constant characters of newpattern. The characters of the filename that match
113 the globbing operators of oldpattern will be preserved. Globbing operators
114 in oldpattern must occur in the same order in newpattern; for every globbing
115 operators in newpattern there must be an identical globbing operators in
116 oldpattern in the same sequence. Both arguments should be quoted since
117 globbing operators are special to the shell. If filenames are given, only
118 those named are acted on; if not, all filenames that match oldpattern are acted
119 on. newpattern is required in all cases except when -m is given and no further
121 If you are unsure whether a $name command will do what you intend, issue it
122 with the -t option first to be sure.
124 $name \"/tmp/foo*.ba.?\" \"/tmp/new*x?\"
125 All filenames in /tmp that match foo*.ba.? will have the \"foo\" part
126 replaced by \"new\" and the \".ba.\" part replaced by \"x\".
127 For example, /tmp/fooblah.ba.baz would be renamed to /tmp/newblahxbaz.
128 $name \* \*- foo bar baz
129 foo, bar, and baz will be renamed to foo-, bar-, and baz-.
130 $name '????????' '????-??-??'
131 All filenames that are 8 characters long will be changed such that dashes
132 are inserted after the 4th and 6th characters.
135 -r: Recursive operation. Filenames given on the command line after oldpattern
136 and newpattern are taken to be directories to traverse recursively. For
137 each subdirectory found, the specified renaming is applied to any matching
138 filenames. oldpattern and newpattern should not include any directory
140 -p<pattern>, -P<pattern>: Act only on filenames that do (if -p is given) or do
141 not (if -P is given) match the sh-style filename globbing pattern
142 <pattern>. This further restricts the filenames that are acted on, beyond
143 the filename selection produced by oldpattern and the filename list (if
144 any). <pattern> must be quoted to prevent it from being interpreted by the
145 shell. Multiple instances of these options may be given. In this case,
146 filenames are acted on only if they match at least one of the patterns
147 given with -p and do not match any of the patterns given with -P.
148 -m<segstart[:segend]=operation>: For each file being renamed, perform a
149 mathematical operation on the string that results from concatenating
150 together the filename segments that matched globbing operator numbers
151 segstart through segend, where operators are numbered in order of
152 occurrence from the left. For example, in the pattern a?b*c[0-9]f, segment
153 1 consists of the character that matched ?, segment 2 consists of the
154 character(s) that matched *, and segment 3 consists of the character that
155 matched [0-9]. The selected segments are replaced with the result of the
156 mathematical operation.
157 The concatenated string must consist of characters that can be interpreted
158 as a decimal integer; if it does not, the filename is not acted on. This
159 number is assigned to the variable 'i', which can be referenced by the
160 operation. The operations available are those understood by the ksh
161 interpreter, which includes most of the operators and syntax of the C
162 language. The original filename segment is replaced by the result of the
163 operation. If -m is used, newpattern may be an empty string or not given
164 at all (if no directory/file names are given). In this case, it is taken
165 to be the same as oldpattern.
166 If segend is given, any fixed text that occurs in the pattern between the
167 starting and ending globbing segments is discarded. If there are fewer
168 globbing segments than segend, no complaint is issued; the string is formed
169 from segment segstart through the last segment that does exist.
170 If segend is not given, the only segment acted on is startseg.
172 $name -m3=i+6 '??*.ppm'
173 This is equivalent to:
174 $name -m3=i+6 '??*.ppm' '??*.ppm'
175 Since the old pattern and new pattern are identical, this would
176 normally be a no-op. But in this case, if a filename of ab079.ppm is
177 given, it is changed to ab85.ppm.
178 $name '-m1:2=i*2' 'foo??bar'
179 This will change a file named foo12bar to foo24bar
180 $name '-m1:2=i*2' 'foo?xyz?bar'
181 This will also change a file named foo1xyz2bar to foo24bar
182 -z<len>: Set the size of the number fields that result when -m is used. The
183 field is truncated to the trailing <len> digits or filled out to <len>
184 digits with leading zeroes. In the above example, if -z3 is given, the
185 output filename will be ab085.ppm.
186 -f: Force rename. By default, $name will not rename files if a file with the
187 new filename already exists. If -f is given, $name will carry out the
189 -q: Quiet operation. By default, if -f is given, $name will still notify the
190 user if a rename results in replacement of an already-existing filename.
191 If -q is given, no notification is issued.
192 -Q: Suppress other warnings. By default, a warning is issued if no files are
193 selected for acting upon. If -Q is given, no warning is issued.
194 -v: Show the rename commands being executed.
195 -t: Show what rename commands would be done, but do not carry them out."
222 inclPats
[inclCt
]=$OPTARG
226 exclPats
[exclCt
]=$OPTARG
230 # Store operation for each segment number in ops[num]
231 # Store ending segment number in op_end_seg[num]
236 if [[ "$start" != +([0-9]) ||
"$start" -eq 0 ]]; then
237 print
-ru2 -- "$name: Bad starting segment number given with -m: $start"
240 if [[ "$end" != +([0-9]) ||
"$end" -eq 0 ]]; then
241 print
-ru2 -- "$name: Bad ending segment number given with -m: $end"
244 if [[ start
-gt end
]]; then
245 print
-ru2 -- "$name: Ending segment ($end) is less than starting segment ($start)"
248 if [[ "$op" != @
(|
*[!_a-zA-Z0-9
])i@
(|
[!_a-zA-Z0-9
]*) ]]; then
250 "$name: Operation given with -m does not reference 'i': $op"
253 # Test whether operation is legal. let returns 1 both for error
254 # indication and when last expression evaluates to 0, so evaluate 1
255 # after test expression.
257 let "$op" 1 2>/dev
/null ||
{
259 "$name: Bad operation given with -m: $op"
263 op_end_seg
[start
]=$end
266 if [[ "$OPTARG" != +([0-9]) ||
"$OPTARG" -eq 0 ]]; then
267 print
-ru2 -- "$name: Bad length given with -z: $OPTARG"
270 typeset
-Z$OPTARG j ||
exit 1
272 +?
) # no way to tell getopts to not treat +x as an option
273 print
-r -u2 "$name: Do not prefix options with '+'."
278 "$name: Option -$OPTARG requires a value.
285 "$name: -$OPTARG: no such option.
293 # remove args that were options
300 # If -m is given, a non-existant or null newpat should be set to oldpat
301 if [ ${#ops[*]} -gt 0 ]; then
306 set -- "$oldpat" "$oldpat"
308 $debug && print
-ru2 -- "Set new pattern to: $newpat"
311 if [ -z "$newpat" ]; then
313 set -- "$oldpat" "$oldpat" "$@"
315 $debug && print
-ru2 -- "Set new pattern to: $newpat"
321 # Make sure input patterns that contain whitespace can be expanded properly
326 # Generate list of filenames to act on.
329 print
-u2 "$Usage\nUse -h for help."
334 print
-r -u2 "$name: No directory names given with -r. Use -h for help."
337 set -- $oldpat # Get list of all filenames that match 1st globbing pattern.
338 if [[ ! -a $1 ]]; then
339 $warnNoFiles && print
-r -- "$name: No filenames match this pattern: $oldpat"
348 integer patSegNum
=1 numPatSegs
351 # while [[ "$oldpat" = *'[\*\?]'* ]]; do
353 # Example oldpat: foo*.a
354 # Example newpat: bar*.b
356 # Build list of non-pattern segments and globbing segments found in arguments.
357 # Note the patterns given are used to get the list of filenames to act on,
358 # to delimit constant segments, and to determine which parts of filenames are
360 # Examples given for first iteration (in the example, the only iteration)
361 # The || newpat is to ensure that new pattern does not have more globbing
362 # segments than old pattern
363 while [[ "$oldpat" = *@
([\
*\?]|\
[+([!\
]])\
])* ||
364 "$newpat" = *@
([\
*\?]|\
[+([!\
]])\
])* ]]; do
365 ## Get leftmost globbing pattern in oldpat
367 # Make r be oldpat with smallest left piece that includes a globbing
368 # pattern removed from it
369 r
=${oldpat#*@([\*\?]|\[+([!\]])\])} # r=.a
370 # Make pat be oldpat with the above removed from it, leaving smallest
371 # left piece that includes a globbing pattern
372 pat
=${oldpat%%"$r"} # pat=foo*
373 # Make l be pat with the globbing pattern removed from the right,
374 # leaving a constant string
375 l
=${pat%@([\*\?]|\[+([!\]])\])} # l=foo
376 # Remove the constant part of pat from the left, leaving the globbing
378 pat
=${pat#"$l"} # pat=*
380 # Do the same thing for newpat, solely to provide a reliable test that
381 # both oldpat & newpat contain exactly the same sequence of globbing
383 r
=${newpat#*@([\*\?]|\[+([!\]])\])} # r=.b
384 npat
=${newpat%%"$r"} # pat=bar*
385 l
=${npat%@([\*\?]|\[+([!\]])\])} # l=bar
386 npat
=${npat#"$l"} # npat=*
388 if [[ "$pat" != "$npat" ]]; then
390 "$name: Old-pattern and new-pattern do not have the same sequence of globbing chars.
391 Pattern segment $patSegNum: Old pattern: $pat New pattern: $npat"
395 ## Find parts before & after pattern
396 # oldpre[] stores the old constant part before the pattern,
397 # so that it can be removed and replaced with the new constant part.
398 oldpre
[patSegNum
]=${oldpat%%"$pat"*} # oldpre[1]=foo
399 # oldsuf stores the part that follows the globbing pattern,
400 # so that it too can be removed.
401 # After oldpre[] & oldsuf[] have been removed from a filename, what remains
402 # is the part matched by the globbing pattern, which is to be retained.
403 oldsuf
[patSegNum
]=${oldpat#*"$pat"} # oldsuf[1]=.a
404 # newpre[] stores the new constant part before the pattern,
405 # so that it can be used to replace the old constant part.
406 newpre
[patSegNum
]=${newpat%%"$pat"*} # newpre[1]=bar
407 # Get rid of processed part of patterns
408 oldpat
=${oldpat#${oldpre[patSegNum]}"$pat"} # oldpat=.a
409 newpat
=${newpat#${newpre[patSegNum]}"$pat"} # newpat=.b
410 # Store either * or ? in pats[], depending on whether this segment matches 1
411 # or any number of characters.
412 [[ "$pat" = \
[* ]] && pat
=?
417 if [ patSegNum
-eq 1 ]; then
418 print
-u2 "No globbing chars in pattern."
422 oldpre
[patSegNum
]=${oldpat%%"$pat"*} # oldpre[2]=.a
423 oldsuf
[patSegNum
]=${oldpat#*"$pat"} # oldsuf[2]=.a
424 newpre
[patSegNum
]=${newpat%%"$pat"*} # newpre[2]=.b
430 while [[ patSegNum
-le numPatSegs
]]; do
432 "Old prefix: <${oldpre[patSegNum]}> Old suffix: <${oldsuf[patSegNum]}> New prefix: <${newpre[patSegNum]}> Pattern: <${pats[patSegNum]}>"
437 # Example filename: foox.a
438 # Example oldpat: foo*.a
439 # Example newpat: bar*.b
443 # Usage: renameFile filename [dirname]
444 # [dirname] is a directory name to prefix filenames with when they are printed
445 # for informational purposes.
447 # inclCt exclCt inclPats[] exclPats[] ops[]
448 # numPatSegs oldpre[] oldsuf[] newpre[] pats[]
449 # check warn tell verbose name
450 # Modifies globals: numFiles
451 function renameFile
{
452 typeset
file=$1 subdir
=$2
453 integer patSegNum patnum
454 typeset origname porigname newfile matchtext pnewfile matchsegs
455 integer startseg endseg
457 origname
=$file # origname=foox.a
458 porigname
=$subdir$file
459 # Unfortunately, ksh88 does not do a good job of allowing for patterns
460 # stored in variables. Without the conditional expression being eval'ed,
461 # only sh patterns are recognized. If the expression is eval'ed, full
462 # ksh expressions can be used, but then expressions that contain whitespace
463 # break unless the user passed a pattern with the whitespace properly
464 # quoted, which is not intuititive. This is fixed in ksh93; full patterns
465 # work without being eval'ed.
466 if [ inclCt
-gt 0 ]; then
468 while [ patnum
-lt inclCt
]; do
469 [[ "$file" = ${inclPats[patnum]} ]] && break
472 if [ patnum
-eq inclCt
]; then
473 $debug && print
-ru2 -- "Skipping not-included filename '$porigname'"
478 while [ patnum
-lt exclCt
]; do
479 if [[ "$file" = ${exclPats[patnum]} ]]; then
480 $debug && print
-ru2 -- "Skipping excluded filename '$porigname'"
485 # Extract matching segments from filename
488 while [[ patSegNum
-le numPatSegs
]]; do
489 # Remove a fixed prefix iteration: 1 2
490 file=${file#${oldpre[patSegNum]}} # file=x.a file=
491 # Save the part of this suffix that is to be retained. To do this, we
492 # need to know what part of the suffix matched the current globbing
493 # segment. If the globbing segment is a *, this is done by removing
494 # the minimum part of the suffix that matches oldsuf (since * matches
495 # the longest segment possible). If the globbing segment is ? or []
496 # (the latter has already been coverted to ?), it is done by taking the
498 if [ "${pats[patSegNum]}" == \? ]; then
500 matchtext
=${file%$matchtext}
502 matchtext
=${file%${oldsuf[patSegNum]}} # matchtext=x matchtext=
504 $debug && print
-ru2 -- "Matching segment $patSegNum: $matchtext"
505 file=${file#$matchtext} # file=.a file=.a
507 matchsegs
[patSegNum
]=$matchtext
511 # Paste fixed and matching segments together to form new filename.
514 while [[ patSegNum
-le numPatSegs
]]; do
515 matchtext
=${matchsegs[patSegNum]}
517 if [ -n "${ops[startseg]}" ]; then
518 endseg
=${op_end_seg[startseg]}
519 while [ patSegNum
-lt endseg
]; do
521 matchtext
=$matchtext${matchsegs[patSegNum]}
523 if [[ "$matchtext" != +([-0-9]) ]]; then
525 "Segment(s) $startseg - $endseg ($matchtext) of file '$porigname' do not form an integer; skipping this file."
529 let "j=${ops[startseg]}" ||
{
531 "Operation failed on segment(s) $startseg - $endseg ($matchtext) of file '$file'; skipping this file."
534 $debug && print
-ru2 -- "Converted $matchtext to $j"
537 newfile
=$newfile${newpre[startseg]}$matchtext # newfile=barx newfile=barx.b
541 pnewfile
=$subdir$newfile
542 if $check && [ -e "$newfile" ]; then
544 print
-ru2 -- "$name: Not renaming \"$porigname\"; destination filename \"$pnewfile\" already exists."
548 print
-n -r -- "Would move: $porigname -> $pnewfile"
549 $warn && [ -e "$newfile" ] && print
-n -r " (destination filename already exists; would replace it)"
553 print
-n -r -- "Moving: $porigname -> $pnewfile"
554 $warn && [ -e "$newfile" ] && print
-n -r -- " (replacing old destination filename \"$pnewfile\")"
556 elif $warn && [ -e "$newfile" ]; then
557 print
-r -- "$name: Note: Replacing old file \"$pnewfile\""
559 mv -f -- "$origname" "$newfile"
565 find "$@" -depth -type d
! -name '*
566 *' -print |
while read dir
; do
568 if cd -- "$dir"; then
569 for file in $origPat; do
570 renameFile
"$file" "$dir/"
573 print
-ru2 -- "$name: Could not access directory '$dir' - skipped."
582 if [ numFiles
-eq 0 ]; then
583 $warnNoFiles && print
-ru2 -- \
584 "$name: All filenames were excluded by patterns given with -p or -P."