rfs 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +7 -0
- data/Rakefile.rb +193 -0
- data/bin/RenameFileSet.rbw +14 -0
- data/bin/rfs.rb +214 -0
- data/bin/rfsd.rb +8 -0
- data/description.txt +24 -0
- data/lib/RenameFileSet.bak.rb +372 -0
- data/lib/errors.rb +15 -0
- data/lib/filters.rb +68 -0
- data/lib/gui.rb +216 -0
- data/lib/innate/array.rb +104 -0
- data/lib/innate/coverage.rb +315 -0
- data/lib/innate/debug.rb +12 -0
- data/lib/innate/debugger.rb +944 -0
- data/lib/innate/file.rb +5 -0
- data/lib/innate/filelines.rb +148 -0
- data/lib/innate/kernel.rb +41 -0
- data/lib/innate/metaid.rb +30 -0
- data/lib/innate/mkdirs.rb +12 -0
- data/lib/innate/regexp.rb +80 -0
- data/lib/innate/reload.rb +11 -0
- data/lib/innate/roman.rb +72 -0
- data/lib/innate/scriptlines.rb +60 -0
- data/lib/innate/string.rb +56 -0
- data/lib/innate/test/all_tests.rb +11 -0
- data/lib/innate/test/files/mkdirs_dummy.file +1 -0
- data/lib/innate/test/files/reloadtarget.rb +5 -0
- data/lib/innate/test/files/reloadtarget1.rb +5 -0
- data/lib/innate/test/files/reloadtarget2.rb +5 -0
- data/lib/innate/test/test_coverage.rb +13 -0
- data/lib/innate/test/testarray.rb +98 -0
- data/lib/innate/test/testfile.rb +15 -0
- data/lib/innate/test/testfilelines.rb +106 -0
- data/lib/innate/test/testkernel.rb +30 -0
- data/lib/innate/test/testmkdirs.rb +19 -0
- data/lib/innate/test/testregexp.rb +86 -0
- data/lib/innate/test/testreload.rb +61 -0
- data/lib/innate/test/testroman.rb +54 -0
- data/lib/innate/test/testscriptlines.rb +39 -0
- data/lib/innate/test/teststring.rb +52 -0
- data/lib/innate/test/testtitlecase.rb +20 -0
- data/lib/innate/titlecase.rb +64 -0
- data/lib/innate/tracerequire.rb +22 -0
- data/lib/namesource.rb +53 -0
- data/lib/options.rb +64 -0
- data/lib/providers.rb +93 -0
- data/lib/regexp.rb +56 -0
- data/lib/rename_functions.rb +142 -0
- data/lib/renamer.rb +210 -0
- data/lib/results.rb +83 -0
- data/lib/test/test_helper.rb +147 -0
- data/tests/dir/file1 +0 -0
- data/tests/dir/file2 +0 -0
- data/tests/dir/file3 +0 -0
- data/tests/test_helper.rb +147 -0
- data/tests/test_rename_functions.rb +605 -0
- metadata +105 -0
@@ -0,0 +1,372 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require_gem 'fxruby'
|
3
|
+
require 'innate/mkdirs'
|
4
|
+
require 'innate/roman'
|
5
|
+
require 'innate/titlecase'
|
6
|
+
|
7
|
+
include Fox
|
8
|
+
|
9
|
+
#TO DO:
|
10
|
+
# change pattern to combobox with some useful patterns
|
11
|
+
# \d.+(\d+).+
|
12
|
+
# (\d+[AB])
|
13
|
+
|
14
|
+
#user interface
|
15
|
+
class RenameWindow < FXMainWindow
|
16
|
+
def initialize(app)
|
17
|
+
super(app, 'Rename File Set', nil, nil, DECOR_ALL, 20, 100, 1000, 500)
|
18
|
+
|
19
|
+
FXHorizontalFrame.new(self, LAYOUT_FILL | PACK_UNIFORM_WIDTH) { |mainFrame|
|
20
|
+
# navigation on left
|
21
|
+
@dirList = FXDirList.new mainFrame, nil, 0, LAYOUT_FILL | FRAME_NORMAL
|
22
|
+
# controls in centre
|
23
|
+
FXVerticalFrame.new(mainFrame, LAYOUT_FILL | PACK_UNIFORM_WIDTH) { |controls|
|
24
|
+
FXLabel.new controls, 'Search (replace first capture):', nil, LAYOUT_FILL_X | JUSTIFY_LEFT
|
25
|
+
@search = FXTextField.new controls, 0, nil, LAYOUT_FILL
|
26
|
+
|
27
|
+
FXGroupBox.new(controls, 'Replacement Method', LAYOUT_FILL | FRAME_GROOVE | JUSTIFY_LEFT ) { |group|
|
28
|
+
@selReplace = FXRadioButton.new(group, 'Replace with:') { |r|
|
29
|
+
connectRadio r
|
30
|
+
}
|
31
|
+
@replace = FXTextField.new group, 30, nil, PACK_UNIFORM_WIDTH
|
32
|
+
@selAdd = FXRadioButton.new(group, 'Add to captured digits: ', nil, LAYOUT_FILL_Y) { |r|
|
33
|
+
connectRadio r
|
34
|
+
}
|
35
|
+
@add = FXSpinner.new(group, 10, nil, FRAME_NORMAL) { |s| s.range = (-500..500)}
|
36
|
+
@selRoman = FXRadioButton.new(group, 'Convert to Roman Numerals', nil, LAYOUT_FILL_X ) { |r|
|
37
|
+
connectRadio r
|
38
|
+
}
|
39
|
+
@selTapeNumbers = FXRadioButton.new(group, 'Convert Tape Numbers', nil, LAYOUT_FILL_X) { |r|
|
40
|
+
connectRadio r
|
41
|
+
}
|
42
|
+
@selFill = FXRadioButton.new(group, 'Replace capture with last match capture:', nil, LAYOUT_FILL_X) { |r|
|
43
|
+
connectRadio r
|
44
|
+
}
|
45
|
+
@fill = FXTextField.new group, 30, nil, PACK_UNIFORM_WIDTH
|
46
|
+
@selFile = FXRadioButton.new(group, 'Replace from files.txt', nil, LAYOUT_FILL_X) { |r|
|
47
|
+
connectRadio r
|
48
|
+
}
|
49
|
+
@selCount = FXRadioButton.new(group, 'Count', nil, LAYOUT_FILL_X) { |r|
|
50
|
+
connectRadio r
|
51
|
+
}
|
52
|
+
@selMarkChange = FXRadioButton.new(group, 'Mark Changes', nil, LAYOUT_FILL_X) { |r|
|
53
|
+
connectRadio r
|
54
|
+
}
|
55
|
+
@selFromFullName = FXRadioButton.new(group, 'Replace with capture in full name:', nil, LAYOUT_FILL_X) { |r|
|
56
|
+
connectRadio r
|
57
|
+
}
|
58
|
+
@fullNamePattern = FXTextField.new group, 30, nil, PACK_UNIFORM_WIDTH
|
59
|
+
@selMoveUp = FXRadioButton.new(group, 'Move to Parent Directory', nil, LAYOUT_FILL_X) { |r|
|
60
|
+
connectRadio r
|
61
|
+
}
|
62
|
+
@selCapitalize = FXRadioButton.new(group, 'Capitalize', nil, LAYOUT_FILL_X) { |r|
|
63
|
+
connectRadio r
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
FXButton.new(controls, '&List Files', nil, nil, 0, LAYOUT_FILL_X | BUTTON_NORMAL) { |b|
|
68
|
+
b.connect SEL_COMMAND, method(:onListClick)
|
69
|
+
}
|
70
|
+
FXButton.new(controls, '&Preview', nil, nil, 0, LAYOUT_FILL_X | FRAME_RAISED | FRAME_THICK) { |b|
|
71
|
+
b.connect SEL_COMMAND, method(:onPreviewClick)
|
72
|
+
}
|
73
|
+
FXButton.new(controls, '&Apply', nil, nil, 0, LAYOUT_FILL_X | FRAME_RAISED | FRAME_THICK) { |b|
|
74
|
+
b.connect SEL_COMMAND, method(:onReplaceClick)
|
75
|
+
}
|
76
|
+
}
|
77
|
+
# results on right
|
78
|
+
@results = FXText.new mainFrame, nil, 0, LAYOUT_FILL | FRAME_NORMAL | TEXT_READONLY
|
79
|
+
}
|
80
|
+
|
81
|
+
@selReplace.checkState = TRUE
|
82
|
+
onRadioUpdate @selReplace, nil, nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def connectRadio(r)
|
86
|
+
r.connect SEL_COMMAND, method(:onRadioUpdate)
|
87
|
+
end
|
88
|
+
|
89
|
+
def onPreviewClick(sender, sel, ptr)
|
90
|
+
start false
|
91
|
+
end
|
92
|
+
|
93
|
+
def onReplaceClick(sender, sel, ptr)
|
94
|
+
start true
|
95
|
+
end
|
96
|
+
|
97
|
+
def onListClick(sender, sel, ptr)
|
98
|
+
start false, true
|
99
|
+
@dirList.setFocus
|
100
|
+
end
|
101
|
+
|
102
|
+
def onRadioUpdate(sender, sel, ptr)
|
103
|
+
sender.parent.each_child do |c|
|
104
|
+
if FXRadioButton === c and c != sender
|
105
|
+
c.checkState = FALSE
|
106
|
+
end
|
107
|
+
end
|
108
|
+
@selReplace.checked? ? @replace.enable : @replace.disable
|
109
|
+
@selAdd.checked? ? @add.enable : @add.disable
|
110
|
+
@selFill.checked? ? @fill.enable : @fill.disable
|
111
|
+
@selFromFullName.checked? ? @fullNamePattern.enable : @fullNamePattern.disable
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
#the real work of it
|
116
|
+
class RenameWindow
|
117
|
+
|
118
|
+
def start(commit, listonly = false)
|
119
|
+
@results.text = ''
|
120
|
+
traverseList @dirList, commit, listonly
|
121
|
+
end
|
122
|
+
|
123
|
+
def traverseList(list, commit, listonly)
|
124
|
+
list.each do |i|
|
125
|
+
if i.selected?
|
126
|
+
doReplace @dirList.itemPathname(i), commit, listonly
|
127
|
+
end
|
128
|
+
traverseList(i, commit, listonly) if i.expanded?
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def subst(src, matchdata, repl)
|
133
|
+
return src unless matchdata and matchdata[1]
|
134
|
+
t = repl
|
135
|
+
c = 1
|
136
|
+
matchdata.to_a[c..-1].each do |m|
|
137
|
+
t.gsub!("%#{c}", m ? m : '')
|
138
|
+
c += 1
|
139
|
+
end
|
140
|
+
src[0, matchdata.offset(1)[0]] + (t == nil ? '' : t) + src[matchdata.offset(1)[1], src.length]
|
141
|
+
end
|
142
|
+
|
143
|
+
def result(t)
|
144
|
+
@results.text += t.to_s + "\n"
|
145
|
+
end
|
146
|
+
|
147
|
+
def rename(path, oldfn, newfn, commit)
|
148
|
+
unless oldfn == newfn
|
149
|
+
t = newfn
|
150
|
+
if commit
|
151
|
+
begin
|
152
|
+
fn = File.join(path, newfn)
|
153
|
+
mkdirs File.split(fn).first
|
154
|
+
File.rename(File.join(path, oldfn), fn)
|
155
|
+
rescue
|
156
|
+
t += ' - Error'
|
157
|
+
else
|
158
|
+
t += ' - OK'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
result t
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def rename_each_match(re, path, commit)
|
166
|
+
return unless Regexp === re
|
167
|
+
dir = Dir.new(path) rescue return
|
168
|
+
dir.each do |file|
|
169
|
+
next if /^\.+$/ =~ file
|
170
|
+
md = re.match(file)
|
171
|
+
if md
|
172
|
+
newfn = yield(file, md)
|
173
|
+
rename path, file, newfn, commit if newfn
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def new_regexp(pattern)
|
179
|
+
begin
|
180
|
+
Regexp.new(pattern)
|
181
|
+
rescue
|
182
|
+
result "Error parsing regular expression:\n#{$!}\n------------"
|
183
|
+
nil
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def doReplace(path, commit, listonly)
|
188
|
+
re = new_regexp @search.text
|
189
|
+
|
190
|
+
if listonly
|
191
|
+
Dir.new(path).each do |file, md|
|
192
|
+
result "#{(File.directory?(File.join(path, file)) ? '\\' : '')}#{file}"
|
193
|
+
end
|
194
|
+
return
|
195
|
+
end
|
196
|
+
|
197
|
+
if @selReplace.checked?
|
198
|
+
rename_each_match(re, path, commit) do |file, md|
|
199
|
+
if md.length >= 2
|
200
|
+
subst(file, md, @replace.text)
|
201
|
+
else
|
202
|
+
file.sub(re, @replace.text)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
elsif @selFile.checked?
|
206
|
+
fn = File.join(path, 'files.txt')
|
207
|
+
return unless File.exist? fn
|
208
|
+
File.open(fn) do |f|
|
209
|
+
rename_each_match(re, path, commit) do |file, md|
|
210
|
+
if md.length >= 2
|
211
|
+
t = f.readline.chomp rescue '--no data--'
|
212
|
+
subst(file, md, t)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
elsif @selAdd.checked?
|
217
|
+
rename_each_match(re, path, commit) do |file, md|
|
218
|
+
if md.length >= 2
|
219
|
+
int = md[1].to_i rescue int = md[1] rescue int = 0
|
220
|
+
subst(file, md, (int + @add.value).to_s)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
elsif @selCount.checked?
|
224
|
+
count = 0
|
225
|
+
rename_each_match(re, path, commit) do |file, md|
|
226
|
+
if md.length >= 2
|
227
|
+
count += 1
|
228
|
+
subst(file, md, count.to_s)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
elsif @selFromFullName.checked?
|
232
|
+
r = new_regexp(@fullNamePattern.text)
|
233
|
+
if r
|
234
|
+
rename_each_match(re, path, commit) do |file, md|
|
235
|
+
fn = path + '\\' + file
|
236
|
+
m = r.match(fn)
|
237
|
+
if m
|
238
|
+
if m.length >= 2
|
239
|
+
replace = m[1]
|
240
|
+
else
|
241
|
+
replace = m
|
242
|
+
end
|
243
|
+
if md.length >= 2
|
244
|
+
subst(file, md, replace)
|
245
|
+
else
|
246
|
+
file.sub(re, replace)
|
247
|
+
end
|
248
|
+
else
|
249
|
+
result "no match: #{fn}"
|
250
|
+
nil
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
elsif @selMoveUp.checked?
|
255
|
+
p = File.split(path).first
|
256
|
+
unless p == '\\'
|
257
|
+
rename_each_match(re, path, commit) do |file, md|
|
258
|
+
d = File.join(p, file)
|
259
|
+
File.rename File.join(path, file), d if commit
|
260
|
+
result d
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
else
|
264
|
+
nil
|
265
|
+
end
|
266
|
+
elsif @selRoman.checked?
|
267
|
+
rename_each_match(re, path, commit) do |file, md|
|
268
|
+
if md.length >= 2
|
269
|
+
t = md[1].to_i rescue t = md[1] rescue t = 0
|
270
|
+
subst(file, md, t.roman) if Integer === t
|
271
|
+
end
|
272
|
+
end
|
273
|
+
elsif @selMarkChange.checked?
|
274
|
+
prev = ''
|
275
|
+
rename_each_match(re, path, commit) do |file, md|
|
276
|
+
if md.length >= 2
|
277
|
+
unless prev == md[1]
|
278
|
+
prev = md[1]
|
279
|
+
subst(file, /()\..+?$/.match(file), '-change-')
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
elsif @selTapeNumbers.checked?
|
284
|
+
rename_each_match(re, path, commit) do |file, md|
|
285
|
+
if md.length >= 2
|
286
|
+
t = md[1]
|
287
|
+
n = t.to_i * 2 + (/a/i =~ t[-1,1] ? -1 : 0)
|
288
|
+
n += 2 if n < 1
|
289
|
+
subst(file, md, n.to_s)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
elsif @selCapitalize.checked?
|
293
|
+
always_lower = %w{ vol
|
294
|
+
mp3 wav txt avi nfo pdf rar zip rb ape sfk mpg rm qt doc }
|
295
|
+
lower = %w{ as of in the and at or a to is on by for with an } + always_lower
|
296
|
+
split_chars = /[ �\-().]/
|
297
|
+
upper = /^[MDCLXVIW]+$/
|
298
|
+
rename_each_match(re, path, commit) do |file, md|
|
299
|
+
o = md[1]
|
300
|
+
prev = ''
|
301
|
+
if md.length >= 2
|
302
|
+
res = o.split(split_chars).inject([]) do |r, w|
|
303
|
+
if r.empty? or prev =~ /^([\d.]+|.*[;.!_]|)$/
|
304
|
+
if always_lower.include?(w.downcase)
|
305
|
+
r << w.downcase
|
306
|
+
elsif w =~ upper
|
307
|
+
r << w.upcase
|
308
|
+
else
|
309
|
+
r << w.capitalize
|
310
|
+
end
|
311
|
+
else
|
312
|
+
if lower.include?(w.downcase)
|
313
|
+
r << w.downcase
|
314
|
+
elsif w =~ upper
|
315
|
+
r << w.upcase
|
316
|
+
else
|
317
|
+
r << w.capitalize
|
318
|
+
end
|
319
|
+
end
|
320
|
+
prev = w
|
321
|
+
r
|
322
|
+
end
|
323
|
+
r = res.inject('') do |s, w|
|
324
|
+
unless w.empty?
|
325
|
+
until o[s.length, 1] =~ Regexp.new(Regexp.escape(w[0,1]), 'i') do
|
326
|
+
s << o[s.length, 1]
|
327
|
+
end
|
328
|
+
s << w
|
329
|
+
end
|
330
|
+
s
|
331
|
+
end
|
332
|
+
subst(file, md, r)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
elsif @selFill.checked?
|
336
|
+
begin
|
337
|
+
fillRE = Regexp.new(@fill.text)
|
338
|
+
rescue
|
339
|
+
result "Error parsing regular expression:\n#{$!}\n------------"
|
340
|
+
return
|
341
|
+
end
|
342
|
+
fillText = ''
|
343
|
+
dir = Dir.new(path) rescue return
|
344
|
+
dir.each do |file|
|
345
|
+
next if /^\.+$/ =~ file
|
346
|
+
md = re.match(file)
|
347
|
+
if md or fillText
|
348
|
+
if md and md.length >= 2 and md[1].length > 0 and not (fillRE and fillRE.match(file))
|
349
|
+
fillText = md[1]
|
350
|
+
result "source: #{file}"
|
351
|
+
else
|
352
|
+
md = fillRE.match(file)
|
353
|
+
if md and md.length >= 2
|
354
|
+
t = subst(file, md, fillText)
|
355
|
+
rename path, file, t, commit
|
356
|
+
else
|
357
|
+
result "* No capture to fill * #{file}"
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
if $0 == __FILE__
|
367
|
+
theApp = FXApp.new
|
368
|
+
theMainWindow = RenameWindow.new theApp
|
369
|
+
theApp.create
|
370
|
+
theMainWindow.show
|
371
|
+
theApp.run
|
372
|
+
end
|
data/lib/errors.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class RenamerError < RuntimeError
|
2
|
+
end
|
3
|
+
|
4
|
+
class ActionError < RenamerError
|
5
|
+
def initialize *opts
|
6
|
+
if opts.length > 0
|
7
|
+
super
|
8
|
+
else
|
9
|
+
super 'Unrecognized or missing action'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class DirectoryExistsError < RuntimeError
|
15
|
+
end
|
data/lib/filters.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
class Filter
|
3
|
+
def self.add(orig, nxt)
|
4
|
+
if orig
|
5
|
+
orig.add nxt
|
6
|
+
else
|
7
|
+
nxt
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :next, :name, :regexp, :full_name
|
12
|
+
|
13
|
+
def initialize(regexp, nxt = nil, full_name = false)
|
14
|
+
@next = nxt
|
15
|
+
@regexp = regexp
|
16
|
+
@full_name = full_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(nxt)
|
20
|
+
if @next
|
21
|
+
@next.add nxt
|
22
|
+
else
|
23
|
+
@next = nxt
|
24
|
+
end
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def filter(path, file)
|
29
|
+
unless @regexp =~ text(path, file)
|
30
|
+
next_filter(path, file)
|
31
|
+
else
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def each(&b)
|
37
|
+
yield self
|
38
|
+
@next.each(&b) if @next
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
def text path, file
|
43
|
+
if @full_name
|
44
|
+
File.expand_path(File.join(path, file))
|
45
|
+
else
|
46
|
+
file
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def next_filter path, file
|
51
|
+
if @next
|
52
|
+
@next.filter path, file
|
53
|
+
else
|
54
|
+
true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class FilterNonMatches < Filter
|
60
|
+
def filter(path, file)
|
61
|
+
if @regexp =~ text(path, file)
|
62
|
+
next_filter(path, file)
|
63
|
+
else
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|