Patch-ID: bash40-021
[bash.git] / examples / scripts.v2 / ren
blobda76026445b4dfc14c0e4b9c995abff2071e2388
1 #!/bin/bash
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
28 # todo: select them.
30 # Modifications for bash made by Chet Ramey <chet@po.cwru.edu>
32 name=${0##*/}
33 Usage="Usage:
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 ..."
38 tell=false
39 verbose=false
40 warn=true
41 warnNoFiles=true
42 debug=false
43 recurse=false
44 inclPat=
45 exclPat=
46 declare -i inclCt=0 exclCt=0
47 check=true
48 declare -i j op_end_seg
50 # Begin bash additions
51 shopt -s extglob
54 # ksh print emulation
56 # print [-Rnprsu[n]] [-f format] [arg ...]
58 # - end of options
59 # -R BSD-style -- only accept -n, no escapes
60 # -n do not add trailing newline
61 # -p no-op (no coprocesses)
62 # -r no escapes
63 # -s print to the history file
64 # -u n redirect output to fd n
65 # -f format printf "$format" "$@"
68 print()
70 local eflag=-e
71 local nflag= fflag= c
72 local fd=1
74 OPTIND=1
75 while getopts "fRnprsu:" c
77 case $c in
78 R) eflag= ;;
79 r) eflag= ;;
80 n) nflag=-n ;;
81 s) sflag=y ;;
82 f) fflag=y ;;
83 u) fd=$OPTARG ;;
84 p) ;;
85 esac
86 done
87 shift $(( $OPTIND - 1 ))
89 if [ -n "$fflag" ]; then
90 builtin printf "$@" >&$fd
91 return
94 case "$sflag" in
95 y) builtin history -s "$*" ;;
96 *) builtin echo $eflag $nflag "$@" >&$fd
97 esac
100 # End bash additions
102 while getopts :htvxp:P:fqQrm:z: opt; do
103 case $opt in
105 print -r -- \
106 "$name: rename files by changing parts of filenames that match a pattern.
107 $Usage
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
120 arguments are given.
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.
123 Examples:
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.
133 Options:
134 -h: Print this help.
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
139 components.
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.
171 Examples:
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
188 rename anyway.
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."
196 exit 0
199 check=false
202 warn=false
205 warnNoFiles=false
208 warnNoFiles=false
209 recurse=true
212 tell=true
215 verbose=true
218 verbose=true
219 debug=true
222 inclPats[inclCt]=$OPTARG
223 ((inclCt+=1))
226 exclPats[exclCt]=$OPTARG
227 ((exclCt+=1))
230 # Store operation for each segment number in ops[num]
231 # Store ending segment number in op_end_seg[num]
232 range=${OPTARG%%=*}
233 op=${OPTARG#*=}
234 start=${range%%:*}
235 end=${range#*:}
236 if [[ "$start" != +([0-9]) || "$start" -eq 0 ]]; then
237 print -ru2 -- "$name: Bad starting segment number given with -m: $start"
238 exit 1
240 if [[ "$end" != +([0-9]) || "$end" -eq 0 ]]; then
241 print -ru2 -- "$name: Bad ending segment number given with -m: $end"
242 exit 1
244 if [[ start -gt end ]]; then
245 print -ru2 -- "$name: Ending segment ($end) is less than starting segment ($start)"
246 exit 1
248 if [[ "$op" != @(|*[!_a-zA-Z0-9])i@(|[!_a-zA-Z0-9]*) ]]; then
249 print -ru2 -- \
250 "$name: Operation given with -m does not reference 'i': $op"
251 exit 1
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 || {
258 print -ru2 -- \
259 "$name: Bad operation given with -m: $op"
260 exit 1
262 ops[start]=$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"
268 exit 1
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 '+'."
274 exit 1
277 print -r -u2 \
278 "$name: Option -$OPTARG requires a value.
279 $Usage
280 Use -h for help."
281 exit 1
283 \?)
284 print -r -u2 \
285 "$name: -$OPTARG: no such option.
286 $Usage
287 Use -h for help."
288 exit 1
290 esac
291 done
293 # remove args that were options
294 let OPTIND=OPTIND-1
295 shift $OPTIND
297 oldpat=$1
298 newpat=$2
300 # If -m is given, a non-existant or null newpat should be set to oldpat
301 if [ ${#ops[*]} -gt 0 ]; then
302 case $# in
306 set -- "$oldpat" "$oldpat"
307 newpat=$oldpat
308 $debug && print -ru2 -- "Set new pattern to: $newpat"
311 if [ -z "$newpat" ]; then
312 shift 2
313 set -- "$oldpat" "$oldpat" "$@"
314 newpat=$oldpat
315 $debug && print -ru2 -- "Set new pattern to: $newpat"
318 esac
321 # Make sure input patterns that contain whitespace can be expanded properly
322 IFS=
324 origPat=$oldpat
326 # Generate list of filenames to act on.
327 case $# in
328 [01])
329 print -u2 "$Usage\nUse -h for help."
330 exit 1
333 if $recurse; then
334 print -r -u2 "$name: No directory names given with -r. Use -h for help."
335 exit 1
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"
340 exit
344 shift 2
346 esac
348 integer patSegNum=1 numPatSegs
350 # For old ksh
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
359 # to be replaced.
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
377 # pattern
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
382 # patterns.
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
389 print -ru2 -- \
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"
392 exit 1
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=?
413 pats[patSegNum]=$pat
414 ((patSegNum+=1))
415 done
417 if [ patSegNum -eq 1 ]; then
418 print -u2 "No globbing chars in pattern."
419 exit 1
422 oldpre[patSegNum]=${oldpat%%"$pat"*} # oldpre[2]=.a
423 oldsuf[patSegNum]=${oldpat#*"$pat"} # oldsuf[2]=.a
424 newpre[patSegNum]=${newpat%%"$pat"*} # newpre[2]=.b
426 numPatSegs=patSegNum
428 if $debug; then
429 patSegNum=1
430 while [[ patSegNum -le numPatSegs ]]; do
431 print -ru2 -- \
432 "Old prefix: <${oldpre[patSegNum]}> Old suffix: <${oldsuf[patSegNum]}> New prefix: <${newpre[patSegNum]}> Pattern: <${pats[patSegNum]}>"
433 ((patSegNum+=1))
434 done
437 # Example filename: foox.a
438 # Example oldpat: foo*.a
439 # Example newpat: bar*.b
441 integer numFiles=0
443 # Usage: renameFile filename [dirname]
444 # [dirname] is a directory name to prefix filenames with when they are printed
445 # for informational purposes.
446 # Uses globals:
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
467 patnum=0
468 while [ patnum -lt inclCt ]; do
469 [[ "$file" = ${inclPats[patnum]} ]] && break
470 ((patnum+=1))
471 done
472 if [ patnum -eq inclCt ]; then
473 $debug && print -ru2 -- "Skipping not-included filename '$porigname'"
474 return 1
477 patnum=0
478 while [ patnum -lt exclCt ]; do
479 if [[ "$file" = ${exclPats[patnum]} ]]; then
480 $debug && print -ru2 -- "Skipping excluded filename '$porigname'"
481 return 1
483 ((patnum+=1))
484 done
485 # Extract matching segments from filename
486 ((numFiles+=1))
487 patSegNum=1
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
497 # next character.
498 if [ "${pats[patSegNum]}" == \? ]; then
499 matchtext=${file#?}
500 matchtext=${file%$matchtext}
501 else
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
508 ((patSegNum+=1))
509 done
511 # Paste fixed and matching segments together to form new filename.
512 patSegNum=0
513 newfile=
514 while [[ patSegNum -le numPatSegs ]]; do
515 matchtext=${matchsegs[patSegNum]}
516 startseg=patSegNum
517 if [ -n "${ops[startseg]}" ]; then
518 endseg=${op_end_seg[startseg]}
519 while [ patSegNum -lt endseg ]; do
520 ((patSegNum+=1))
521 matchtext=$matchtext${matchsegs[patSegNum]}
522 done
523 if [[ "$matchtext" != +([-0-9]) ]]; then
524 print -ru2 -- \
525 "Segment(s) $startseg - $endseg ($matchtext) of file '$porigname' do not form an integer; skipping this file."
526 return 2
528 i=$matchtext
529 let "j=${ops[startseg]}" || {
530 print -ru2 -- \
531 "Operation failed on segment(s) $startseg - $endseg ($matchtext) of file '$file'; skipping this file."
532 return 2
534 $debug && print -ru2 -- "Converted $matchtext to $j"
535 matchtext=$j
537 newfile=$newfile${newpre[startseg]}$matchtext # newfile=barx newfile=barx.b
538 ((patSegNum+=1))
539 done
541 pnewfile=$subdir$newfile
542 if $check && [ -e "$newfile" ]; then
543 $warn &&
544 print -ru2 -- "$name: Not renaming \"$porigname\"; destination filename \"$pnewfile\" already exists."
545 return 2
547 if $tell; then
548 print -n -r -- "Would move: $porigname -> $pnewfile"
549 $warn && [ -e "$newfile" ] && print -n -r " (destination filename already exists; would replace it)"
550 print ""
551 else
552 if $verbose; then
553 print -n -r -- "Moving: $porigname -> $pnewfile"
554 $warn && [ -e "$newfile" ] && print -n -r -- " (replacing old destination filename \"$pnewfile\")"
555 print ""
556 elif $warn && [ -e "$newfile" ]; then
557 print -r -- "$name: Note: Replacing old file \"$pnewfile\""
559 mv -f -- "$origname" "$newfile"
563 if $recurse; then
564 oPWD=$PWD
565 find "$@" -depth -type d ! -name '*
566 *' -print | while read dir; do
567 cd -- "$oPWD"
568 if cd -- "$dir"; then
569 for file in $origPat; do
570 renameFile "$file" "$dir/"
571 done
572 else
573 print -ru2 -- "$name: Could not access directory '$dir' - skipped."
575 done
576 else
577 for file; do
578 renameFile "$file"
579 done
582 if [ numFiles -eq 0 ]; then
583 $warnNoFiles && print -ru2 -- \
584 "$name: All filenames were excluded by patterns given with -p or -P."