file_editor 0.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.txt ADDED
@@ -0,0 +1,57 @@
1
+ File Editor makes it easy to edit a file in place. Just send it a file name and a short block with some instructions, and it will perform string substitution as indicated in the block
2
+
3
+ ===Using File Editor
4
+
5
+ Your program block can use File Editor in two modes, with or without an explicit receiver:
6
+
7
+ FileEditor.edit('test.txt') do |editor|
8
+ editor.regex = /java/i
9
+ editor.substitution_string = 'ruby'
10
+ editor.run
11
+ end
12
+
13
+ OR
14
+
15
+ FileEditor.edit('test.txt') do
16
+ @regex = /java/i
17
+ @substitution_string = 'ruby'
18
+ run
19
+ end
20
+
21
+ The first version does not use instance_eval internally and is therefore able to use variables in the program's local scope. The second version does use instance_eval internally.
22
+
23
+ Here are some examples, alternating between approaches:
24
+
25
+ ====Standard substitution. File editor will do a line-by-line gsub on the given file
26
+
27
+ FileEditor.edit('test.txt') do |editor|
28
+ editor.regex = /java/i
29
+ editor.substitution_string = 'ruby'
30
+ editor.run
31
+ end
32
+
33
+ ====Setting global to false triggers the use of sub instead of gsub (in effect replacing only the first match on each line)
34
+
35
+ FileEditor.edit('test.txt') do
36
+ @regex = /java/i
37
+ @substitution_string = 'ruby'
38
+ @global = false
39
+ run
40
+ end
41
+
42
+ ====Setting keep_backup = true ensures that you'll have a backup of your original file
43
+
44
+ FileEditor.edit('test.txt') do |editor|
45
+ editor.regex = /java/i
46
+ editor.substitution_string = 'ruby'
47
+ editor.keep_backup = true
48
+ editor.run
49
+ end
50
+
51
+ ====If you pass in a multiline regex, FileEditor will detect it, read the entire file into a string, and match across new lines
52
+
53
+ FileEditor.edit('test.txt') do
54
+ @regex = /ja.*?va/mi
55
+ @substitution_string = 'ruby'
56
+ run
57
+ end
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ task :default => [:test]
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |s|
2
+ s.name="file_editor"
3
+ s.version="0.5"
4
+ s.platform=Gem::Platform::RUBY
5
+ s.date="2013-02-14"
6
+ s.summary="Edit a file in place"
7
+ s.description="File editor makes it easy to edit a file in place, with options for saving a backup"
8
+ s.author="Mark Buechler"
9
+ s.files=Dir["file_editor.gemspec", "Rakefile", "README.txt", "lib/**/*", "test/test*.rb"]
10
+ s.homepage="http://rubygems.org/sqlfinder"
11
+ s.require_paths=["lib"]
12
+ s.has_rdoc=true
13
+ s.extra_rdoc_files=["README.txt"]
14
+ s.rdoc_options=["--main", "README.txt"]
15
+ end
@@ -0,0 +1,84 @@
1
+ class FileEditor
2
+
3
+ attr_accessor :file_name, :regex, :substitution_string, :keep_backup, :global
4
+
5
+ def initialize(file_name)
6
+ @file_name = file_name
7
+ @keep_backup = false
8
+ @global = true #this will yield the equivalent of gsub instead of sub
9
+ end
10
+
11
+ #You can send an explicit receiver as an argument to this block
12
+ #and set variables on it (useful inside another object where you need to access its
13
+ #own methods inside the block), or simply set variables without an explicit receiver.
14
+ #Either way, the block must call the run method. See Ruby Best Practices by Gregory Brown, p. 63-64,
15
+ #for elaboration of this approach
16
+ def self.edit(file_name, &b)
17
+ editor = FileEditor.new(file_name)
18
+ b.arity == 0 ? editor.instance_eval(&b) : b.call(editor)
19
+ end
20
+
21
+ #runs file editor
22
+ def run
23
+ raise "You need to provide a regular expression to the editor" unless @regex
24
+ raise "You need to provide a substitution string" unless @substitution_string
25
+ raise "The file #{@file_name} does not exist" unless File.exist?(@file_name)
26
+ raise "The path #{@file_name} does not point to a file" unless File.file?(@file_name)
27
+ raise "You do not have permission to read #{@file_name}" unless File.readable?(@file_name)
28
+ raise "You do not have permission to edit #{@file_name}" unless File.writable?(@file_name)
29
+ begin
30
+ if @keep_backup
31
+ temp_file_name = @file_name + ".backup"
32
+ File.delete(temp_file_name) if File.exist?(temp_file_name)
33
+ else
34
+ temp_file_name = @file_name + "." + get_random_extension
35
+ end
36
+ File.rename(@file_name, temp_file_name)
37
+ read_handle = File.new(temp_file_name, "r")
38
+ write_handle = File.new(@file_name, "w")
39
+ write_to_file(read_handle, write_handle)
40
+ ensure
41
+ read_handle.close if read_handle
42
+ write_handle.close if write_handle
43
+ File.delete(temp_file_name) unless @keep_backup
44
+ end
45
+ end
46
+
47
+ #testable write method, using io handles as arguments
48
+ #to enable the use of String.IO in testing
49
+ def write_to_file(read_handle, write_handle)
50
+ if is_multiline(@regex.to_s)
51
+ file_string = read_handle.read #no sense doing a line-by-line multiline
52
+ if (@global)
53
+ write_handle.puts(file_string.gsub(@regex, @substitution_string))
54
+ else
55
+ write_handle.puts(file_string.sub(@regex, @substitution_string))
56
+ end
57
+ else
58
+ if (@global)
59
+ read_handle.each do |line|
60
+ write_handle.puts (line.gsub(@regex, @substitution_string))
61
+ end
62
+ else
63
+ read_handle.each do |line|
64
+ write_handle.puts (line.sub(@regex, @substitution_string))
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ #Determines if a string derived from regexp.to_s
71
+ #represents a regular expression with the multiline option turned on
72
+ def is_multiline(regex_string)
73
+ regex_string.match(/\?.*?m.*?-/) || regex_string.match(/\?[^-]+:/)
74
+ end
75
+
76
+ #returns a random 6-letter extension (e.g., 'kjeihl');
77
+ #this ensures that we don't inadvertantly delete a pre-existing
78
+ #backup file if we're not saving the backup
79
+ def get_random_extension
80
+ ('a'..'z').to_a.shuffle.slice(0..5).join
81
+ end
82
+
83
+ end
84
+
@@ -0,0 +1,469 @@
1
+ #ruby -Ilib -I. test/test_file_editor.rb
2
+
3
+ require 'test/unit'
4
+ require 'file_editor'
5
+ require 'stringio'
6
+
7
+ #NOTE: these tests are not examples of how to use FileEditor in code;
8
+ #the gem was designed with a separate method for the actual IO operations,
9
+ #precisely so it could be tested without recourse to external files,
10
+ #using StringIO
11
+ #See README.txt for actual code examples
12
+
13
+ class FileEditorTest < Test::Unit::TestCase
14
+
15
+ def setup
16
+ @original = %q[
17
+ I like java and java likes me.
18
+ Java is a great language
19
+
20
+ Vote for ja
21
+ va.
22
+
23
+ ]
24
+ end
25
+
26
+ #NO OPTIONS
27
+
28
+ #defaults to global, don't ignore case
29
+ def test_no_options_without_explicit_receiver
30
+ expected = %q[
31
+ I like ruby and ruby likes me.
32
+ Java is a great language
33
+
34
+ Vote for ja
35
+ va.
36
+
37
+ ]
38
+ read_handle = StringIO.new(@original)
39
+ write_handle = StringIO.new("")
40
+ FileEditor.edit("dummy.txt") do
41
+ @regex = /java/
42
+ @substitution_string = "ruby"
43
+ write_to_file(read_handle, write_handle)
44
+ end
45
+ read_handle.close()
46
+ write_handle.close()
47
+ assert_equal(expected, write_handle.string)
48
+
49
+ end
50
+
51
+ def test_no_options_with_explicit_receiver
52
+ expected = %q[
53
+ I like ruby and ruby likes me.
54
+ Java is a great language
55
+
56
+ Vote for ja
57
+ va.
58
+
59
+ ]
60
+ read_handle = StringIO.new(@original)
61
+ write_handle = StringIO.new("")
62
+ FileEditor.edit("dummy.txt") do |editor|
63
+ editor.regex = /java/
64
+ editor.substitution_string = "ruby"
65
+ editor.write_to_file(read_handle, write_handle)
66
+ end
67
+ read_handle.close()
68
+ write_handle.close()
69
+ assert_equal(expected, write_handle.string)
70
+
71
+ end
72
+
73
+ #IGNORE CASE
74
+
75
+ def test_ignore_case_without_explicit_receiver
76
+ expected = %q[
77
+ I like ruby and ruby likes me.
78
+ ruby is a great language
79
+
80
+ Vote for ja
81
+ va.
82
+
83
+ ]
84
+ read_handle = StringIO.new(@original)
85
+ write_handle = StringIO.new("")
86
+ FileEditor.edit("dummy.txt") do
87
+ @regex = /java/i
88
+ @substitution_string = "ruby"
89
+ write_to_file(read_handle, write_handle)
90
+ end
91
+ read_handle.close()
92
+ write_handle.close()
93
+ assert_equal(expected, write_handle.string)
94
+
95
+ end
96
+
97
+ def test_ignore_case_with_explicit_receiver
98
+ expected = %q[
99
+ I like ruby and ruby likes me.
100
+ ruby is a great language
101
+
102
+ Vote for ja
103
+ va.
104
+
105
+ ]
106
+ read_handle = StringIO.new(@original)
107
+ write_handle = StringIO.new("")
108
+ FileEditor.edit("dummy.txt") do |editor|
109
+ editor.regex = /java/i
110
+ editor.substitution_string = "ruby"
111
+ editor.write_to_file(read_handle, write_handle)
112
+ end
113
+ read_handle.close()
114
+ write_handle.close()
115
+ assert_equal(expected, write_handle.string)
116
+
117
+ end
118
+
119
+ #IGNORE CASE AND GLOBAL SUBSTITUTION
120
+
121
+ def test_ignore_case_and_global_false_without_explicit_receiver
122
+ expected = %q[
123
+ I like ruby and java likes me.
124
+ ruby is a great language
125
+
126
+ Vote for ja
127
+ va.
128
+
129
+ ]
130
+ read_handle = StringIO.new(@original)
131
+ write_handle = StringIO.new("")
132
+ FileEditor.edit("dummy.txt") do
133
+ @regex = /java/i
134
+ @substitution_string = "ruby"
135
+ @global = false
136
+ write_to_file(read_handle, write_handle)
137
+ end
138
+ read_handle.close()
139
+ write_handle.close()
140
+ assert_equal(expected, write_handle.string)
141
+
142
+ end
143
+
144
+ def test_ignore_case_and_global_substitution_with_explicit_receiver
145
+ expected = %q[
146
+ I like ruby and java likes me.
147
+ ruby is a great language
148
+
149
+ Vote for ja
150
+ va.
151
+
152
+ ]
153
+ read_handle = StringIO.new(@original)
154
+ write_handle = StringIO.new("")
155
+ FileEditor.edit("dummy.txt") do |editor|
156
+ editor.regex = /java/i
157
+ editor.substitution_string = "ruby"
158
+ editor.global = false
159
+ editor.write_to_file(read_handle, write_handle)
160
+ end
161
+ read_handle.close()
162
+ write_handle.close()
163
+ assert_equal(expected, write_handle.string)
164
+
165
+ end
166
+
167
+ #MULTILINE (can't imagine doing this with global set to false, but we test that option anyway)
168
+
169
+ def test_multiline_and_global_without_explicit_receiver
170
+ expected = %q[
171
+ I like ruby and ruby likes me.
172
+ ruby is a great language
173
+
174
+ Vote for ruby.
175
+
176
+ ]
177
+ read_handle = StringIO.new(@original)
178
+ write_handle = StringIO.new("")
179
+ FileEditor.edit("dummy.txt") do
180
+ @regex = /ja.*?va/mi
181
+ @substitution_string = "ruby"
182
+ write_to_file(read_handle, write_handle)
183
+ end
184
+ read_handle.close()
185
+ write_handle.close()
186
+ assert_equal(expected, write_handle.string)
187
+
188
+ end
189
+
190
+ def test_multiline_and_not_global_without_explicit_receiver
191
+ expected = %q[
192
+ I like ruby and java likes me.
193
+ Java is a great language
194
+
195
+ Vote for ja
196
+ va.
197
+
198
+ ]
199
+ read_handle = StringIO.new(@original)
200
+ write_handle = StringIO.new("")
201
+ FileEditor.edit("dummy.txt") do
202
+ @regex = /ja.*?va/mi
203
+ @substitution_string = "ruby"
204
+ @global = false
205
+ write_to_file(read_handle, write_handle)
206
+ end
207
+ read_handle.close()
208
+ write_handle.close()
209
+ assert_equal(expected, write_handle.string)
210
+
211
+ end
212
+
213
+ def test_multiline_global_with_explicit_receiver
214
+ expected = %q[
215
+ I like ruby and ruby likes me.
216
+ Java is a great language
217
+
218
+ Vote for ruby.
219
+
220
+ ]
221
+ read_handle = StringIO.new(@original)
222
+ write_handle = StringIO.new("")
223
+ FileEditor.edit("dummy.txt") do |editor|
224
+ editor.regex = /ja.*?va/m
225
+ editor.substitution_string = "ruby"
226
+ editor.write_to_file(read_handle, write_handle)
227
+ end
228
+ read_handle.close()
229
+ write_handle.close()
230
+ assert_equal(expected, write_handle.string)
231
+
232
+ end
233
+
234
+ def test_no_regex
235
+ non_existent_file = 'no_file.txt'
236
+ e = assert_raise(RuntimeError) do
237
+ FileEditor.edit("#{non_existent_file}") do
238
+ #@regex = /a/
239
+ @substitution_string = 'b'
240
+ run
241
+ end
242
+ end
243
+ assert_equal("You need to provide a regular expression to the editor", e.message)
244
+ end
245
+
246
+ def test_no_substitution_string
247
+ non_existent_file = 'no_file.txt'
248
+ e = assert_raise(RuntimeError) do
249
+ FileEditor.edit("#{non_existent_file}") do
250
+ @regex = /a/
251
+ #@substitution_string = 'b'
252
+ run
253
+ end
254
+ end
255
+ assert_equal("You need to provide a substitution string", e.message)
256
+ end
257
+
258
+ def test_file_not_found
259
+ non_existent_file = 'no_file.txt'
260
+ e = assert_raise(RuntimeError) do
261
+ FileEditor.edit("#{non_existent_file}") do
262
+ @regex = /a/
263
+ @substitution_string = 'b'
264
+ run
265
+ end
266
+ end
267
+ assert_equal("The file #{non_existent_file} does not exist", e.message)
268
+ end
269
+
270
+ def test_not_a_file
271
+ path_to_dir = 'lib'
272
+ e = assert_raise(RuntimeError) do
273
+ FileEditor.edit("#{path_to_dir}") do
274
+ @regex = /a/
275
+ @substitution_string = 'b'
276
+ run
277
+ end
278
+ end
279
+ assert_equal("The path #{path_to_dir} does not point to a file", e.message)
280
+ end
281
+
282
+ #THE FOLLOWING TESTS DO NOT TEST THE APPLICATION;
283
+ #THEY WERE AD HOC TEST TO DETERMINE HOW MULTILINE REGEXES WORK, AMONG OTHER THINGS
284
+
285
+ def test_is_multiline
286
+
287
+ editor = FileEditor.new("dummy.txt")
288
+
289
+ #these should not match
290
+ ignore = /anything/i.to_s #(?i-mx:anything)
291
+ extended = /anything/x.to_s #(?x-mi:anything)
292
+ ignore_plus_extended = /anything/ix.to_s #(?ix-m:anything)
293
+
294
+ not_multi = [ignore, extended, ignore_plus_extended]
295
+ not_multi.each do |r|
296
+ assert(!editor.is_multiline(r))
297
+ end
298
+
299
+ #these should match
300
+ multi = /anything/m.to_s #(?m-ix:anything)
301
+ multi_plus_ignore = /anything/mi.to_s #(?mi-x:anything)
302
+ multi_plus_extended = /anything/mx.to_s #(?mx-i:anything)
303
+
304
+ #special case that should match
305
+ all = /anything/mix.to_s #(?mix:anything)
306
+
307
+ is_multi = [multi, multi_plus_ignore, multi_plus_extended, all]
308
+ is_multi.each do |r|
309
+ assert(editor.is_multiline(r))
310
+ end
311
+
312
+ end
313
+
314
+ #second test is for special case of all: if there's no hyphen, the we know all options are included
315
+ def is_multiline(regex_string)
316
+ regex_string.match(/\?.*?m.*?-/) || regex_string.match(/\?[^-]+:/)
317
+ end
318
+
319
+ def test_regex_object_and_modifiers
320
+ r = Regexp.new("ell", Regexp::IGNORECASE)
321
+ #puts "ignore case: #{r.to_s}"
322
+ assert_equal(1, r.options)
323
+
324
+ r = Regexp.new("ell", Regexp::EXTENDED)
325
+ #puts "extended: #{r.to_s}"
326
+ assert_equal(2, r.options)
327
+
328
+ r = Regexp.new("ell", Regexp::MULTILINE)
329
+ #puts "MULTILINE: #{r.to_s}"
330
+ assert_equal(4, r.options)
331
+
332
+ r = Regexp.new("ell", (Regexp::EXTENDED | Regexp::IGNORECASE))
333
+ #puts "EXTENDED AND IGNORE: #{r.to_s}"
334
+ assert_equal(3, r.options)
335
+
336
+ r = Regexp.new("ell", (Regexp::MULTILINE | Regexp::IGNORECASE))
337
+ #puts "MULTILINE AND IGNORE: #{r.to_s}"
338
+ assert_equal(5, r.options)
339
+
340
+ r = Regexp.new("ell", (Regexp::MULTILINE | Regexp::EXTENDED))
341
+ #puts "MULTILINE AND EXTENDED: #{r.to_s}"
342
+ assert_equal(6, r.options)
343
+
344
+ r = Regexp.new("ell", (Regexp::EXTENDED | Regexp::IGNORECASE | Regexp::MULTILINE))
345
+ #puts "all three: #{r.to_s}"
346
+ assert_equal(7, r.options)
347
+
348
+ #puts
349
+
350
+ ops_orred = [Regexp::IGNORECASE, Regexp::EXTENDED].reduce{|a,b| a|b }
351
+ assert_equal(3, ops_orred) #0001 | 0010 => 0011
352
+
353
+ ops_orred = [Regexp::IGNORECASE, Regexp::MULTILINE].reduce{|a,b| a|b }
354
+ assert_equal(5, ops_orred) #0001 | 0100 => 0101
355
+
356
+ ops_orred = [Regexp::IGNORECASE, Regexp::EXTENDED, Regexp::MULTILINE].reduce{|a,b| a|b }
357
+ assert_equal(7, ops_orred) #0001 | 0010 | 0100 => 0111
358
+ end
359
+
360
+ def test_multiline
361
+ #note we have to use non-greedy quantifiers (probably always with multiline, or results could be way unexpected)
362
+ multiline_string = %q[
363
+ You are fine.
364
+ I like you.
365
+ Y
366
+ ou are weird.
367
+ ]
368
+ expected_multiline_string = %q[
369
+ yous are fine.
370
+ I like yous.
371
+ yous are weird.
372
+ ]
373
+ edited_multiline_string = multiline_string.gsub(/y.*?ou/im, 'yous');
374
+ assert_equal(expected_multiline_string, edited_multiline_string)
375
+
376
+ m = "you".match (/y.*ou/)
377
+ assert m
378
+ m = "you".match (/y.*ou/m)
379
+ assert m
380
+ m = "y\nou".match (/y.*ou/m)
381
+ assert m
382
+ s = %q[
383
+ y
384
+ ou
385
+ ]
386
+ m = s.match (/y.*ou/m)
387
+ assert m
388
+
389
+ result = "you".gsub(/y.*ou/m, 'Z')
390
+ assert_equal('Z', result)
391
+
392
+ #shows one of the tricks that greedy interpretations will play on multiline searches
393
+ test = %q[
394
+ you
395
+ y
396
+ ou
397
+ ]
398
+ expected =
399
+ %q[
400
+ Z
401
+ ]
402
+ result = test.gsub(/y.*ou/m, 'Z')
403
+ assert_equal(expected, result)
404
+ #note that we end up with one replacement instead of two,
405
+ #because the greedy interpretation matches the first y and then the last ou
406
+
407
+ #here's what we probably intended
408
+ test = %q[
409
+ you
410
+ y
411
+ ou
412
+ ]
413
+ expected =
414
+ %q[
415
+ Z
416
+ Z
417
+ ]
418
+ result = test.gsub(/y.*?ou/m, 'Z')
419
+ assert_equal(expected, result)
420
+
421
+
422
+ end
423
+
424
+ end
425
+
426
+
427
+ =begin
428
+
429
+ #TEMP
430
+ def test_ell
431
+ FileEditor.new("test.txt") do |editor|
432
+ editor.regex = /ell/i
433
+ editor.substitution_string = "ojj"
434
+ editor.keep_backup = false
435
+ editor.run
436
+ end
437
+ end
438
+
439
+ def test_ojj
440
+ FileEditor.new("test.txt") do |editor|
441
+ editor.regex = /ojj/i
442
+ editor.substitution_string = "ell"
443
+ editor.keep_backup = false
444
+ editor.run
445
+ end
446
+ end
447
+
448
+ #this feels like proper course going forward,
449
+ #pending the problem with including it in another class
450
+ def test_ell_with_instance_eval
451
+ FileEditor.edit("test.txt") do
452
+ @regex = /ell/i
453
+ @substitution_string = "ojj"
454
+ @keep_backup = false
455
+ end
456
+ end
457
+
458
+
459
+ def test_regex
460
+ s = "heLlo"
461
+ r = Regexp.new("ell", Regexp::IGNORECASE)
462
+ #r = Regexp.new(/ell/ix)
463
+ puts "r.to_s: #{r.to_s}"
464
+ puts "r.options: #{r.options}"
465
+ result = s.sub(r, "ojj")
466
+ puts "result: #{result}"
467
+ end
468
+
469
+ =end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: file_editor
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.5'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mark Buechler
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-14 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: File editor makes it easy to edit a file in place, with options for saving
15
+ a backup
16
+ email:
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files:
20
+ - README.txt
21
+ files:
22
+ - file_editor.gemspec
23
+ - Rakefile
24
+ - README.txt
25
+ - lib/file_editor.rb
26
+ - test/test_file_editor.rb
27
+ homepage: http://rubygems.org/sqlfinder
28
+ licenses: []
29
+ post_install_message:
30
+ rdoc_options:
31
+ - --main
32
+ - README.txt
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 1.8.24
50
+ signing_key:
51
+ specification_version: 3
52
+ summary: Edit a file in place
53
+ test_files: []