aboutsummaryrefslogtreecommitdiffstats
path: root/examples/scripts.v2/ren
blob: da76026445b4dfc14c0e4b9c995abff2071e2388 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
#!/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 <chet@po.cwru.edu>

name=${0##*/}
Usage="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

# 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<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)
	# 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