file_overwrite 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.
@@ -0,0 +1,1103 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+ begin
6
+ require 'file_overwrite/file_overwrite_error'
7
+ rescue LoadError
8
+ # In case this file is singly used.
9
+ warn 'Failed to load file_overwrite/file_overwrite_error.rb . It is not essential, except some error messages can be less helpful.' if $DEBUG
10
+ end
11
+
12
+ # Controller class to backup a file and overwrite it
13
+ #
14
+ # Ruby iterators and chaining-methods are fully exploited to edit the file.
15
+ #
16
+ # = Examples
17
+ #
18
+ # f1 = FileOverwrite.new('a.txt', noop: true, verbose: true)
19
+ # # Treat the content as String
20
+ # f1.sub(/abc/, 'xyz').gsub(/(.)ef/){|i| $1}.run!
21
+ # f1.sizes # => { :old => 40, :new => 50 }
22
+ # f1.backup # => 'a.txt.20180915.bak'
23
+ # # However, the file has not been created
24
+ # # and the original file has not been modified, either,
25
+ # # due to the noop option
26
+ #
27
+ # f2 = FileOverwrite.new('a.txt', suffix: '~')
28
+ # f2.backup # => 'a.txt~'
29
+ # f2.completed? # => false
30
+ # # Treat the content as String inside the block
31
+ # f2.read{ |str| "\n" + i + "\n" }.gsub(/a\nb/m, '').run!
32
+ # f2.completed? # => true
33
+ # FileOverwrite.new('a.txt', suffix: '~').sub(/a/, '').run!
34
+ # # => RuntimeError, because the backup file 'a.txt~' exists.
35
+ # FileOverwrite.new('a.txt', suffix: '~').sub(/a/, '').run!(clobber: true)
36
+ # # => The backup file is overwritten.
37
+ #
38
+ # f3 = FileOverwrite.new('a.txt', backup: '/tmp/b.txt')
39
+ # # Backup file can be explicitly specified.
40
+ # f3.backup # => '/tmp/b.txt'
41
+ # f3.backup = 'original.txt'
42
+ # f3.backup # => 'original.txt'
43
+ # # Treat the file as IO inside the block
44
+ # f3.open{ |ior, iow| i + "XYZ" }
45
+ # f3.reset # The modification is discarded
46
+ # f3.reset? # => true
47
+ # f3.open{ |ior, iow| i + "XYZ"; raise FileOverwriteError, 'I stop.' }
48
+ # # To discard the modification inside the block
49
+ # f3.reset? # => true
50
+ # f3.open{ |ior, iow| "\n" + i + "\n" }
51
+ # f3.run!(noop: true, verbose: true) # Dryrun
52
+ # f3.completed? # => true
53
+ # f3.backup = 'change.d' # => FrozenError (the state can not be modified after run!(), including dryrun)
54
+ #
55
+ # f4 = FileOverwrite.new('a.txt', suffix: nil)
56
+ # f4.backup # => nil (No backup file is created.)
57
+ # f4.readlines{|ary| ary+["last\n"]}.each{|i| 'XX'+i}.run!
58
+ # IO.readlines('a.txt')[-1] # => "XXlast\n"
59
+ #
60
+ # f5 = FileOverwrite.new('a.txt', suffix: '.bak')
61
+ # f5.backup # => 'a.txt.bak'
62
+ # f5.read{|i| i}.run!
63
+ # FileUtils.identical? 'a.txt', 'a.txt.bak' # => true
64
+ # File.mtime('a.txt') == File.mtime('a.txt.bak') # => true
65
+ # # To forcibly update the Timestamp, give touch option as true
66
+ # # either in new() or run!(), ie., run!(touch: true)
67
+ #
68
+ # @author Masa Sakano
69
+ #
70
+ class FileOverwrite
71
+ # In order to use fu_output_message()
72
+ include FileUtils
73
+
74
+ # Sets the backup filename. Read method ({#backup}) is provided separately.
75
+ # @!attribute [w] backup
76
+ # @return [String] Keys: :old and :new
77
+ attr_writer :backup
78
+
79
+ # Hash of the file sizes of before (:old) and after (:new).
80
+ #
81
+ # This is set after {#run!} and if setsize option in {#run!} is given true (Default)
82
+ # or if verbose option is true (Def: false). Else, nil is returned.
83
+ #
84
+ # @!attribute [r] sizes
85
+ # @return [Hash, NilClass] Keys: :old and :new
86
+ attr_reader :sizes
87
+
88
+ # Verbose flag can be read or set any time, except after the process is completed
89
+ # @!attribute [rw] verbose
90
+ # @return [Boolean]
91
+ attr_accessor :verbose
92
+
93
+ # Encoding of the content of the input file. Default is nil (unspecified).
94
+ # @!attribute [rw] ext_enc
95
+ # @return [Encoding]
96
+ attr_accessor :ext_enc_old
97
+
98
+ # Encoding of the content of the output file. Default is nil (unspecified).
99
+ # @!attribute [rw] ext_enc
100
+ # @return [Encoding]
101
+ attr_accessor :ext_enc_new
102
+
103
+ # Encoding of the content (String, Array) of the file or IO to be passed to the user.
104
+ # @!attribute [rw] int_enc
105
+ # @return [Encoding]
106
+ attr_accessor :int_enc
107
+
108
+ # @param fname [String] Input filename
109
+ # @param backup: [String, NilClass] File name to which the original file is backed up. If non-Nil, suffix is ignored.
110
+ # @param suffix: [String, TrueClass, FalseClass, NilClass] Suffix of the backup file. True for Def, or false if no backup.
111
+ # @param noop: [Boolean] no-operationor dryrun
112
+ # @param verbose: [Boolean, NilClass] the same as $VERBOSE or the command-line option -W, i.e., the verbosity is (true > false > nil). Forced to be true if $DEBUG
113
+ # @param clobber: [Boolean] raise Exception if false(Def) and fname exists and suffix is non-null.
114
+ # @param touch: [Boolean] if true (non-Def), when the file content does not change, the timestamp is updated, unless aboslutely no action is taken for the file.
115
+ def initialize(fname, backup: nil, suffix: true, noop: false, verbose: $VERBOSE, clobber: false, touch: false)
116
+ @fname = fname
117
+ @backup = backup
118
+ @suffix = (backup ? true : suffix)
119
+ @noop = noop
120
+ @verbose = $DEBUG || verbose
121
+ @clobber = clobber
122
+ @touch = touch
123
+
124
+ @ext_enc_old = nil
125
+ @ext_enc_new = nil
126
+ @int_enc = nil
127
+
128
+ @outstr = nil # String to write. This is nil if the temporary file was already created with modify().
129
+ @outary = nil # or Array to write
130
+ @iotmp = nil # Temporary file IO to replace the original
131
+ @is_edit_finished = false # true if the file modification is finished.
132
+ @is_completed = false # true after all the process has been completed.
133
+ @sizes = nil
134
+ end
135
+
136
+ ########################################################
137
+ # State-related methods
138
+ ########################################################
139
+
140
+ # Gets a path of the filename for backup
141
+ #
142
+ # If suffix is given, the default suffix and backup filename are ignored,
143
+ # and the (backup) filename with the given suffix is returned
144
+ # (so you can tell what the backup filename would be if the suffix was set).
145
+ #
146
+ # @param suffix [String, TrueClass, FalseClass, NilClass] Suffix of the backup file. True for Def, or false if no backup.
147
+ # @param backupfile: [String, NilClass] Explicilty specify the backup filename.
148
+ # @return [String, NilClass]
149
+ def backup(suffix=nil, backupfile: nil)
150
+ return backup_from_suffix(suffix) if suffix # non-nil suffix explicitly given
151
+ return backupfile if backupfile
152
+ return @backup if @backup
153
+ return backup_from_suffix(@suffix) if @suffix
154
+ nil
155
+ end
156
+
157
+
158
+ # Returns true if the instance is chainable.
159
+ #
160
+ # In other words, whether a further process like {#gsub} can be run.
161
+ # This returns nil if {#fresh?} is true.
162
+ #
163
+ # @return [Boolean, NilClass]
164
+ def chainable?
165
+ return nil if fresh?
166
+ return false if completed?
167
+ return !@is_edit_finished # ie., (@outary || @outstr) b/c one of the three must be non-false after the 2 clauses above.
168
+ end
169
+
170
+
171
+ # Returns true if the process has been completed.
172
+ def completed?
173
+ @is_completed
174
+ end
175
+
176
+
177
+ # Returns the (current) content as String to supercede the input file
178
+ #
179
+ # If the file has been already overwritten, this returns the content of the new one.
180
+ # Note it would be impossible to return the old one anyway,
181
+ # if no backup is left, as the user chooses.
182
+ #
183
+ # Even if the returned string is destructively modified,
184
+ # it has no effect on the final output to the overwritten file.
185
+ #
186
+ # @return [String]
187
+ def dump
188
+ return @outstr.dup if @outstr
189
+ return join_outary() if @outary
190
+ return File.read(@iotmp.path) if @is_edit_finished
191
+ File.read(@fname)
192
+ end
193
+
194
+
195
+ # True if the (current) content to supercede the input file is empty.
196
+ #
197
+ # @return [String]
198
+ def empty?
199
+ dump.empty?
200
+ end
201
+
202
+
203
+ # Implement {String#encode}[https://ruby-doc.org/core-2.5.1/String.html#method-i-encode]
204
+ #
205
+ # If it is in the middle of the process, the internal encoding for
206
+ # String (or Array) changes. Note if the current proces is in the IO-mode,
207
+ # everything has been already written in a temporary file, and hence
208
+ # there is no effect.
209
+ #
210
+ # Once this is called, @int_enc is overwritten (or set),
211
+ # and it remains so even after reset() is called.
212
+ #
213
+ # It is advisable to call {#force_encoding} or {#ext_enc_old=} before this is called
214
+ # to set the encoding of the input file.
215
+ #
216
+ # @param *rest [Array]
217
+ # @param **kwd [Hash]
218
+ # @return [String]
219
+ # @see https://ruby-doc.org/core-2.5.1/String.html#method-i-encode
220
+ def encode(*rest, **kwd)
221
+ enc = (rest[0] || Encoding.default_internal)
222
+ @int_enc = enc # raises an Exception if called after "completed"
223
+ return enc if @is_edit_finished || fresh?
224
+ return @outstr.encode(*rest, **kwd) if @outstr
225
+ if @outary
226
+ @outary.map!{|i| i.encode(*rest, **kwd)}
227
+ return enc
228
+ end
229
+ raise 'Should not happen. Contact the code developer.'
230
+ end
231
+
232
+
233
+ # True if the (current) content to supercede the input file end with the specified.
234
+ #
235
+ # Wrapper of String#end_with?
236
+ #
237
+ # @return [String]
238
+ def end_with?(*rest)
239
+ dump.end_with?(*rest)
240
+ end
241
+
242
+
243
+ # Implement {String#force_encoding}[https://ruby-doc.org/core-2.5.1/String.html#method-i-force_encoding]
244
+ #
245
+ # Once this is called, @ext_enc_old is overwritten (or set),
246
+ # and it remains so even after reset() is called.
247
+ #
248
+ # @return [Encoding]
249
+ # @see https://ruby-doc.org/core-2.5.1/String.html#method-i-force_encoding
250
+ def force_encoding(enc)
251
+ @ext_enc_old = enc # raises an Exception if called after "completed"
252
+ return enc if @is_edit_finished || fresh?
253
+ return @outstr.force_encoding(enc) if @outstr
254
+ if @outary
255
+ @outary.map!{|i| i.force_encoding(enc)}
256
+ return enc
257
+ end
258
+ raise 'Should not happen. Contact the code developer.'
259
+ end
260
+
261
+
262
+ # Returns true if the process has not yet started.
263
+ def fresh?
264
+ !state
265
+ end
266
+ alias_method :reset?, :fresh? if ! self.method_defined?(:reset?)
267
+
268
+
269
+ # Returns true if the instance is ready to run (to execute overwriting the file).
270
+ def ready?
271
+ !fresh? && !completed?
272
+ end
273
+ alias_method :reset?, :fresh? if ! self.method_defined?(:reset?)
274
+
275
+
276
+ # Reset all the modification which is to be applied
277
+ #
278
+ # @return [NilClass]
279
+ def reset
280
+ @outstr = nil
281
+ @outary = nil
282
+ @is_edit_finished = nil
283
+ close_iotmp # @iotmp=nil; immediate deletion of the temporary file
284
+ warn "The modification process is reset." if $DEBUG
285
+ nil
286
+ end
287
+
288
+
289
+ # Returns the temporary filename (or nil), maybe for debugging
290
+ #
291
+ # It may not be open?
292
+ #
293
+ # @return [String, NilClass] Filename if exists, else nil
294
+ def temporary_filename
295
+ @iotmp ? @iotmp.path : nil
296
+ end
297
+
298
+
299
+ # Returns the current state
300
+ #
301
+ # nil if no modification has been attempted.
302
+ # IO if the modification has been made and it is wating to run.
303
+ # String or Array (or their equivalent), depending how it has been chained so far.
304
+ # true if the process has been completed.
305
+ #
306
+ # @return [Class, TrueClass, NilClass]
307
+ def state
308
+ return true if completed?
309
+ return IO if @is_edit_finished
310
+ return @outstr.class if @outstr
311
+ return @outary.class if @outary
312
+ nil
313
+ end
314
+
315
+ # String#valid_encoding?()
316
+ #
317
+ # returns nil if the process has been already completed.
318
+ #
319
+ # @return [Boolean, NilClass]
320
+ def valid_encoding?()
321
+ return nil if completed?
322
+ dump.valid_encoding?
323
+ end
324
+
325
+
326
+ ########################################################
327
+ # run
328
+ ########################################################
329
+
330
+ # If identical, just touch (if specified) and returns true
331
+ #
332
+ # @param noop [Boolean]
333
+ # @param verbose [Boolean]
334
+ # @param touch [Boolean] if true (non-Def), when the file content does not change, the timestamp is updated
335
+ # @return [Boolean]
336
+ def run_identical?(noop, verbose, touch)
337
+ if !identical?(@iotmp.path, @fname) # defined in FileUtils
338
+ return false
339
+ end
340
+
341
+ @iotmp.close(true) # immediate deletion of the temporary file
342
+
343
+ msg = sprintf("%sNo change in (%s).", prefix(noop), @fname)
344
+ if touch
345
+ touch(@fname, noop: noop) # defined in FileUtils
346
+ msg.chop! # chop a full stop.
347
+ msg << " but timestamp is updated to " << File.mtime(@fname).to_s << '.'
348
+ end
349
+ fu_output_message msg if verbose
350
+
351
+ @is_completed = true
352
+ self.freeze
353
+ true
354
+ end
355
+ private :run_identical?
356
+
357
+
358
+ # Actually performs the file modification
359
+ #
360
+ # If setsize option is true (Default) or verbose, method {#sizes} is activated after this method,
361
+ # which returns a hash of file sizes in bytes before and after, so you can chain it.
362
+ # Note this method returns nil if the input file is not opened at all.
363
+ #
364
+ # @example With setsize option
365
+ # fo.run!(setsize: true).sizes
366
+ # # => { :old => 40, :new => 50 }
367
+ #
368
+ # @example One case where this returns nil
369
+ # fo.new('test.f').run! # => nil
370
+ #
371
+ # The folloing optional parameters are taken into account.
372
+ # Any other options are ignored.
373
+ #
374
+ # @param backup: [String, NilClass] File name to which the original file is backed up. If non-Nil, suffix is ignored.
375
+ # @param suffix: [String, TrueClass, FalseClass, NilClass] Suffix of the backup file. True for Def, or false if no backup.
376
+ # @param noop: [Boolean]
377
+ # @param verbose: [Boolean]
378
+ # @param clobber: [Boolean] raise Exception if false(Def) and fname exists and suffix is non-null.
379
+ # @param touch: [Boolean] Even if true (non-Def), when the file content does not change, the timestamp is updated, unless aboslutely no action has been taken for the file.
380
+ # @param setsize: [Boolean]
381
+ # @return [NilClass, self] If the input file is not touched, nil is returned, else self.
382
+ # @raise [FileOverwriteError] if the process has been already completed.
383
+ def run!(backup: @backup, suffix: @suffix, noop: @noop, verbose: @verbose, clobber: @clobber, touch: @touch, setsize: true, **kwd)
384
+ raise FileOverwriteError, 'The process has been already completed.' if completed?
385
+
386
+ bkupname = get_bkupname(backup, suffix, noop, verbose, clobber)
387
+ sizes = write_new(verbose, setsize)
388
+ return nil if !sizes
389
+
390
+ return self if run_identical?(noop, verbose, touch)
391
+
392
+ if bkupname
393
+ msg4bkup = ", Backup: " + bkupname if verbose
394
+ else
395
+ io2del = tempfile_io
396
+ io2delname = io2del.path
397
+ end
398
+
399
+ fname_to = (bkupname || io2delname)
400
+ mv( @fname, fname_to, noop: noop, verbose: $DEBUG) # defined in FileUtils
401
+ begin
402
+ mv(@iotmp.path, @fname, noop: noop, verbose: $DEBUG) # defined in FileUtils
403
+ rescue
404
+ msg = sprintf("Process halted! File system error in renaming the temporary file %s back to the original %s", @iotmp.path, @fname)
405
+ warn msg
406
+ raise
407
+ end
408
+
409
+ # @iotmp.close(true) # to immediate delete the temporary file
410
+ # If commented out, GC looks after it.
411
+
412
+ File.unlink io2delname if io2delname && !noop
413
+ # if noop, GC will delete it.
414
+
415
+ if verbose
416
+ msg = sprintf("%sFile %s updated (Size: %d => %d bytes%s)\n", prefix(noop), @fname, sizes[:old], sizes[:new], msg4bkup)
417
+ fu_output_message msg
418
+ end
419
+
420
+ @is_completed = true
421
+ self.freeze
422
+
423
+ return self
424
+ end
425
+ alias_method :run, :run! if ! self.method_defined?(:run)
426
+
427
+
428
+ ########################################################
429
+ # IO-based manipulation
430
+ ########################################################
431
+
432
+ # Modify the content in the block (though not committed, yet)
433
+ #
434
+ # Two parameters are passed to the block: io_r and io_w.
435
+ # The former is the read-descriptor to read from the original file
436
+ # and the latter is the write-descriptor to write whatever to the temporary file,
437
+ # which is later moved back to the original file when you {#run!}.
438
+ #
439
+ # Note the IO pointer for the input file is reset after this method.
440
+ # Hence, chaining this method makes no effect (warning is issued),
441
+ # but only the last one is taken into account.
442
+ #
443
+ # @example
444
+ # fo.modify do |io_r, io_w|
445
+ # io_w.print( "\n" + io_r.read + "\n" )
446
+ # end
447
+ #
448
+ # If you want to halt, undo and reset your modification process in the middle, issue
449
+ # raise FileOverwriteError [Your_Message]
450
+ # and it will be rescued. Your_Message is printed to STDERR if verbose was specified in {#initialize} or $DEBUG
451
+ #
452
+ # @param **kwd [Hash] keyword parameters passed to File.open. Notably, ext_enc and int_enc .
453
+ # @return [self]
454
+ # @yieldparam ioin [IO] Read IO instance from the original file
455
+ # @yieldparam @iotmp [IO] Write IO instance to the temporary file
456
+ # @yieldreturn [Object] ignored
457
+ # @raise [ArgumentError] if a block is not given
458
+ def modify(**kwd)
459
+ raise ArgumentError, 'Block must be given.' if !block_given?
460
+ normalize_status(:@is_edit_finished)
461
+
462
+ kwd_def = {}
463
+ kwd_def[:ext_enc] = @ext_enc_old if @ext_enc_old
464
+ kwd_def[:int_enc] = @int_enc if @int_enc
465
+ kwd = kwd_def.merge kwd
466
+
467
+ begin
468
+ File.open(@fname, **kwd) { |ioin|
469
+ @iotmp = tempfile_io
470
+ yield(ioin, @iotmp)
471
+ }
472
+ rescue FileOverwriteError => err
473
+ warn err.message if @verbose
474
+ reset
475
+ end
476
+ self
477
+ end
478
+ alias_method :open, :modify if ! self.method_defined?(:open)
479
+
480
+
481
+ # Alias to self.{#modify}.{#run!}
482
+ #
483
+ # @return [self]
484
+ # @yieldparam ioin [IO] Read IO instance from the original file
485
+ # @yieldparam @iotmp [IO] Write IO instance to the temporary file
486
+ # @yieldreturn [Object] ignored
487
+ # @raise [ArgumentError] if a block is not given
488
+ def modify!(**kwd, &bloc)
489
+ modify(&bloc).run!(**kwd)
490
+ end
491
+ alias_method :open!, :modify! if ! self.method_defined?(:open!)
492
+
493
+
494
+ ########################################################
495
+ # Array-based manipulation
496
+ ########################################################
497
+
498
+ # Takes a block in which the entire String of the file is passed.
499
+ #
500
+ # IO.readlines(infile) is given to the block, where
501
+ # Encode may be taken into account if specified already.
502
+ #
503
+ # The block must return an Array, the number of the elements of which
504
+ # can be allowed to differ from the input. The elements of the Array
505
+ # will be joined to output to the overwritten file in the end.
506
+ #
507
+ # @param *rest [Array] separator etc
508
+ # @param **kwd [Hash] ext_enc, int_enc
509
+ # @return [self]
510
+ # @yieldparam str [String]
511
+ # @yieldreturn [String] to be written back to the original file
512
+ def readlines(*rest, **kwd, &bloc)
513
+ raise ArgumentError, 'Block must be given.' if !block_given?
514
+
515
+ if :first == normalize_status(:@outary)
516
+ adjust_input_encoding(**kwd){ |f| # @fname
517
+ @outary = IO.readlines(f, *rest)
518
+ }
519
+ end
520
+
521
+ @outary = yield(@outary)
522
+ self
523
+ end
524
+
525
+
526
+ ########################################################
527
+ # String-based manipulation
528
+ ########################################################
529
+
530
+ # Takes a block in which each line of the file (or current content) is passed.
531
+ #
532
+ # In the block each line as String is given as a block argument.
533
+ # Each iterator must return a String (or an object having to_s method),
534
+ # which replaces the input String to be output to the overwritten file later.
535
+ #
536
+ # This method can be chained, as String-type processing.
537
+ #
538
+ # @param *rest [Array] separator etc
539
+ # @param **kwd [Hash] ext_enc, int_enc
540
+ # @return [self]
541
+ # @yieldparam str [String]
542
+ # @yieldreturn [String] to be written back to the original file
543
+ # @raise [ArgumentError] if a block is not given
544
+ def each_line(*rest, **kwd, &bloc)
545
+ raise ArgumentError, 'Block must be given.' if !block_given?
546
+ read(**kwd){ |outstr|
547
+ outstr.each_line(*rest).map{|i| yield(i).to_s}.join('')
548
+ }
549
+ end
550
+
551
+ # Alias to self.{#sub}.{#run!}
552
+ #
553
+ # @param *rest [Array<Regexp,String>]
554
+ # @param **kwd [Hash] setsize: etc
555
+ # @return [self]
556
+ # @yield the same as {String#sub!}
557
+ def each_line!(*rest, **kwd, &bloc)
558
+ send(__method__.to_s.chop, *rest, **kwd, &bloc).run!(**kwd)
559
+ end
560
+
561
+ # # Takes a block to perform {IO#each_line} for the input file
562
+ # #
563
+ # # @return [self]
564
+ # # @yieldparam *rest [Object] Read IO instance from the original file
565
+ # # @yieldreturn [String] to be written back to the original file
566
+ # # @raise [ArgumentError] if a block is not given
567
+ # def each_line(*rest)
568
+ # raise ArgumentError, 'Block must be given.' if !block_given?
569
+ # modify { |io|
570
+ # io.each_line(*rest) do |*args|
571
+ # yield(*args)
572
+ # end
573
+ # }
574
+ # self
575
+ # end
576
+
577
+
578
+ # Handler to process the entire string of the file (or current content)
579
+ #
580
+ # If block is not given, just sets the state as String
581
+ #
582
+ # Else, File.read(infile) is given to the block.
583
+ # Then, the returned value is held as a String, hence this method can be chained.
584
+ # If the block returns nil (or Boolean), {FileOverwriteError} is raised.
585
+ # Make sure to return a String (whether an empty string or "true")
586
+ #
587
+ # Note this method does not take arguments as in IO.read
588
+ #
589
+ # @param **kwd [Hash] ext_enc, int_enc
590
+ # @return [self]
591
+ # @yieldparam str [String]
592
+ # @yieldreturn [String] to be written back to the original file
593
+ # @raise [FileOverwriteError] if a block is given and nil or Boolean is returned.
594
+ def read(**kwd, &bloc)
595
+ if :first == normalize_status(:@outstr)
596
+ adjust_input_encoding(**kwd){ |f| # @fname
597
+ @outstr = File.read f
598
+ }
599
+ end
600
+
601
+ @outstr = yield(@outstr) if block_given?
602
+ raise FileOverwriteError, 'ERROR: The returned value from the block in read() can not be nil or Boolean.' if !@outstr || true == @outstr
603
+ self
604
+ end
605
+
606
+
607
+ # Alias to self.{#read}.{#run!}
608
+ #
609
+ # @param **kwd [Hash] ext_enc, int_enc
610
+ # @return [self]
611
+ # @yield Should return string
612
+ def read!(**kwd, &bloc)
613
+ read(**kwd, &bloc).run!(**kwd)
614
+ end
615
+
616
+
617
+ # Similar to {String#replace} but the original is not modified
618
+ #
619
+ # This method can be chained.
620
+ #
621
+ # @param str [String] the content will be replaced with this
622
+ # @return [self]
623
+ def replace_with(str)
624
+ read
625
+ @outstr = str.to_s
626
+ self
627
+ end
628
+
629
+
630
+ # Alias to self.{#replace_with}.{#run!}
631
+ #
632
+ # @return [self]
633
+ # @yield the same as {String#gsub!}
634
+ def replace_with!(str, **kwd)
635
+ replace_with(str).run!(**kwd)
636
+ end
637
+
638
+
639
+ # Similar to String#sub
640
+ #
641
+ # This method can be chained.
642
+ # This method never returns an Enumerator.
643
+ #
644
+ # WARNING: Do not use the local variables like $1, $2, $', and Regexp.last_match
645
+ # inside the block supplied. They would NOT be interpreted in the context of
646
+ # this method, but that in the caller, which is most likely not to be what you want.
647
+ #
648
+ # Instead, this method supplies the MatchData of the match as the second block parameter
649
+ # in addition to the matched string as in String#sub.
650
+ #
651
+ # @param *rest [Array<Regexp,String>]
652
+ # @param max: [Integer] the number of the maximum matches. If it is not 1, {#gsub} is called, instead. See {#gsub} for detail.
653
+ # @param **kwd [Hash] ext_enc, int_enc
654
+ # @return [self]
655
+ # @yield the same as String#sub
656
+ def sub(*rest, max: 1, **kwd, &bloc)
657
+ return self if sub_gsub_args_only(*rest, max: max, **kwd)
658
+
659
+ if !block_given?
660
+ raise ArgumentError, full_method_name+' does not support the format to return an enumerator.'
661
+ end
662
+
663
+ if max.to_i != 1
664
+ return gsub(*rest, max: max, **kwd, &bloc)
665
+ end
666
+
667
+ begin
668
+ m = rest[0].match(@outstr)
669
+ # Returning nil, Integer etc is accepted in the block of sub/gsub
670
+ @outstr = m.pre_match + yield(m[0], m).to_s + m.post_match if m
671
+ # Not to break the specification of sub(), but just to extend.
672
+ return self
673
+ rescue NoMethodError => err
674
+ warn_for_sub_gsub(err)
675
+ raise
676
+ end
677
+ end
678
+
679
+
680
+ # Alias to self.{#sub}.{#run!}
681
+ #
682
+ # @param *rest [Array<Regexp,String>]
683
+ # @param **kwd [Hash] setsize: etc
684
+ # @return [self]
685
+ # @yield the same as {String#sub!}
686
+ def sub!(*rest, **kwd, &bloc)
687
+ sub(*rest, &bloc).run!(**kwd)
688
+ end
689
+
690
+
691
+ # Similar to String#gsub
692
+ #
693
+ # This method can be chained.
694
+ # This method never returns an Enumerator.
695
+ #
696
+ # This method supplies the MatchData of the match as the second block parameter
697
+ # in addition to the matched string as in String#sub.
698
+ #
699
+ # Being different from the standard Srrint#gsub, this method accepts
700
+ # the optional parameter max, which specifies the maximum number of times
701
+ # of the matches and is valid ONLY WHEN a block is given.
702
+ #
703
+ # @note Disclaimer
704
+ # When a block is not given but arguments only (and not expecting Enumerator to return),
705
+ # this method simply calls String#gsub . However, when only 1 argument
706
+ # and a block is given, this method must iterate on its own, which is implemented.
707
+ # I am not 100% confident if this method works in the completely same way
708
+ # as String#gsub in every single situation (except the local variables like $1, $2, etc
709
+ # are not on the {#gsub} context; see {#sub}), given the regular expression
710
+ # has so many possibilities; I have made the best effort and so far
711
+ # I have not found any cases where this method breaks.
712
+ # This method is more inefficient and slower than the original String#gsub
713
+ # as this method scans/matches the string twice as many times as String#gsub
714
+ # (which is unavoidable to implement it properly, I think), and the implementation
715
+ # is in pure Ruby.
716
+ #
717
+ # @param *rest [Array<Regexp,String>]
718
+ # @param max: [Integer] the number of the maximum matches. 0 means no limit (as in String#gsub). Valid only if a block is given.
719
+ # @param **kwd [Hash] ext_enc, int_enc
720
+ # @return [self]
721
+ # @yield the same as String#gsub
722
+ # @see #sub
723
+ def gsub(*rest, max: 0, **kwd, &bloc)
724
+ return sub(*rest, max: 1, **kwd, &bloc) if 1 == max # Note: Error message would be labelled as 'sub'
725
+ return self if sub_gsub_args_only(*rest, max: max, **kwd)
726
+
727
+ if !block_given?
728
+ raise ArgumentError, full_method_name+' does not support the format to return an enumerator.'
729
+ end
730
+
731
+ max = 5.0/0 if max.to_i <= 0
732
+
733
+ scans = @outstr.scan(rest[0])
734
+ return self if scans.empty? # no matches
735
+
736
+ scans.map!{|i| [i].flatten} # Originally, it can be a double array.
737
+ regbase_str = rest[0].to_s
738
+ prematch = ''
739
+ ret = ''
740
+ imatch = 0 # Number of matches
741
+ begin
742
+ scans.each do |ea_sc|
743
+ str_matched = ea_sc[0]
744
+ imatch += 1
745
+ pre_size = prematch.size
746
+ pos_end_p1 = @outstr.index(str_matched, pre_size) # End+1
747
+ str_between = @outstr[pre_size...pos_end_p1]
748
+ prematch << str_between
749
+ ret << str_between
750
+ regex = Regexp.new( sprintf('(?<=\A%s)%s', Regexp.quote(prematch), regbase_str) )
751
+ #regex = rest[0] if prematch.empty? # The first run
752
+ m = regex.match(@outstr)
753
+ prematch << str_matched
754
+ # Not to break the specification of sub(), but just to extend.
755
+ ret << yield(m[0], m).to_s
756
+ break if imatch >= max
757
+ end
758
+ ret << Regexp.last_match.post_match # Guaranteed to be non-nil.
759
+
760
+ @outstr = ret
761
+ return self
762
+ rescue NoMethodError => err
763
+ warn_for_sub_gsub(err)
764
+ raise
765
+ end
766
+ end
767
+
768
+
769
+ # Alias to self.{#gsub}.{#run!}
770
+ #
771
+ # @return [self]
772
+ # @yield the same as {String#gsub!}
773
+ def gsub!(*rest, **kwd, &bloc)
774
+ gsub(*rest, &bloc).run!(**kwd)
775
+ end
776
+
777
+
778
+ # Similar to {String#tr}
779
+ #
780
+ # This method can be chained.
781
+ #
782
+ # @param *rest [Array] replacers etc
783
+ # @param **kwd [Hash] ext_enc, int_enc
784
+ # @return [self]
785
+ def tr(*rest, **kwd)
786
+ read(**kwd){ |outstr|
787
+ outstr.tr!(*rest) || outstr
788
+ }
789
+ end
790
+
791
+ # Alias to self.{#tr}.{#run!}
792
+ #
793
+ # @return [self]
794
+ def tr!(*rest, **kwd)
795
+ tr(*rest, **kwd).run!(**kwd)
796
+ end
797
+
798
+ # Similar to {String#tr_s}
799
+ #
800
+ # This method can be chained.
801
+ #
802
+ # @param *rest [Array] replacers etc
803
+ # @param **kwd [Hash] ext_enc, int_enc
804
+ # @return [self]
805
+ def tr_s(*rest, **kwd)
806
+ read(**kwd){ |outstr|
807
+ outstr.tr_s!(*rest) || outstr
808
+ }
809
+ end
810
+
811
+ # Alias to self.{#tr}.{#run!}
812
+ #
813
+ # @return [self]
814
+ def tr_s!(*rest, **kwd)
815
+ tr_s(*rest, **kwd).run!(**kwd)
816
+ end
817
+
818
+
819
+ ########################################################
820
+ # Class methods
821
+ ########################################################
822
+
823
+ # Shorthand of {FileOverwrite#initialize}.{#readlines}, taking parameters for both
824
+ #
825
+ # @param fname [String] Input and overwriting filename
826
+ # @param *rest [Array] (see {#initialize} and {#readlines})
827
+ # @param **kwd [Hash] (see {#initialize})
828
+ # @return [FileOverwrite]
829
+ # @yield refer to {#readlines}
830
+ # @see #readlines
831
+ def self.readlines(fname, *rest, **kwd, &bloc)
832
+ new(fname, *rest, **kwd).send(__method__, *rest, **kwd, &bloc)
833
+ end
834
+
835
+
836
+ # Shorthand of {FileOverwrite.readlines}.{#run!}
837
+ #
838
+ # @param (see FileOverwrite.readlines and #run)
839
+ # @return [FileOverwrite]
840
+ # @yield refer to {#readlines}
841
+ # @see #readlines
842
+ def self.readlines!(*rest, **kwd, &bloc)
843
+ readlines(*rest, **kwd, &bloc).run!(**kwd)
844
+ end
845
+
846
+
847
+ ########################################################
848
+ private
849
+ ########################################################
850
+
851
+ # Core routine to adjust the encoding of the input String (or Array)
852
+ #
853
+ # @return [Array, String]
854
+ # @yieldparam fname [String]
855
+ # @yieldreturn [Array, String] @outstr or @outary
856
+ def adjust_input_encoding(**kwd, &bloc)
857
+ raise ArgumentError, 'Block must be given.' if !block_given?
858
+ obj = yield(@fname)
859
+
860
+ kwd_enc = {}
861
+ kwd_enc[:ext_enc] = @ext_enc_old if @ext_enc_old
862
+ kwd_enc[:ext_enc] = kwd[:ext_enc] if kwd[:ext_enc]
863
+ kwd_enc[:int_enc] = @int_enc if @int_enc
864
+ kwd_enc[:int_enc] = kwd[:int_enc] if kwd[:int_enc]
865
+ if kwd_enc[:ext_enc]
866
+ force_encoding kwd_enc[:ext_enc]
867
+ end
868
+ if kwd_enc[:int_enc]
869
+ if defined? obj.map!
870
+ obj.map!{|i| i.encode(kwd_enc[:int_enc])}
871
+ elsif defined? obj.encode
872
+ obj.encode kwd_enc[:int_enc]
873
+ else
874
+ raise 'Should not happen. Contact the code developper.'
875
+ end
876
+ end
877
+ end
878
+ private :adjust_input_encoding
879
+
880
+
881
+ # Returns a path of the filename constructed with the supplied suffix
882
+ #
883
+ # @param suffix [String, TrueClass] Suffix of the backup file. True for Def, or false if no backup.
884
+ # @return [String, NilClass]
885
+ def backup_from_suffix(suffix)
886
+ raise 'Should not happen. Contact the code developper.' if !suffix
887
+
888
+ @fname + ((suffix == true) ? Time.now.strftime(".%Y%m%d%H%M%S.bak") : suffix)
889
+ end
890
+ private :backup_from_suffix
891
+
892
+
893
+ # Deletes the temporary file if exists
894
+ #
895
+ # @return [String, NilClass] Filename if deleted, else nil
896
+ def close_iotmp
897
+ return if !@iotmp
898
+ fn = @iotmp.path
899
+ @iotmp.close(true) if @iotmp # immediate deletion of the temporary file
900
+ @iotmp = nil
901
+ fn
902
+ end
903
+ private :close_iotmp
904
+
905
+ # Returns a String "FileOverwrite#MY_METHOD_NAME"
906
+ #
907
+ # @param nested_level [Integer] 0 (Def) if the caller wants the name of itself.
908
+ # @return [String]
909
+ def full_method_name(nested_level=0)
910
+ # Note: caller_locations() is equivalent to caller_locations(1).
911
+ # caller_locations(0) from this method would also contain the information of
912
+ # this method full_method_name() itself, which is totally irrelevant.
913
+ sprintf("%s#%s", self.class.to_s, caller_locations()[nested_level].label)
914
+ end
915
+ private :full_method_name
916
+
917
+
918
+ # Gets a path of the filename for backup and checks out clobber
919
+ #
920
+ # @param backup_l [String, NilClass] File name to which the original file is backed up. If non-Nil, suffix is ignored.
921
+ # @param suffix [String, TrueClass, FalseClass, NilClass] Suffix of the backup file. True for Def, or false if no backup.
922
+ # @param noop [Boolean]
923
+ # @param verbose [Boolean]
924
+ # @param clobber [Boolean] raise Exception if false(Def) and fname exists and suffix is non-null.
925
+ # @return [String, NilClass]
926
+ def get_bkupname(backup_l, suffix, noop, verbose, clobber)
927
+ bkupname = backup(suffix, backupfile: backup_l)
928
+ return nil if !bkupname
929
+
930
+ if File.exist?(bkupname)
931
+ raise "File(#{@fname}) exists." if !clobber
932
+ fu_output_message sprintf("%Backup File %s is overwritten.", prefix(noop), bkupname) if verbose
933
+ end
934
+
935
+ bkupname
936
+ end
937
+ private :get_bkupname
938
+
939
+
940
+ # Returns joined string of @outary as it is to output
941
+ #
942
+ # @return [String]
943
+ def join_outary(ary=@outary)
944
+ ary.join ''
945
+ end
946
+ private :join_outary
947
+
948
+
949
+ # Changes the status of a set of instance variables
950
+ #
951
+ # Returns :first if this is the first process, else :continuation (ie, chained)
952
+ # For IO-style, this returns always :first
953
+ #
954
+ # @param inst_var [Symbol, String] '@is_edit_finished', :@outary (do not forget '@')
955
+ # @return [Symbol] :first or :continuation
956
+ def normalize_status(inst_var)
957
+ errmsg = "WARNING: The file (#{@fname}) is reread from the beginning."
958
+
959
+ case inst_var
960
+ when :@is_edit_finished, '@is_edit_finished'
961
+ warn errmsg if @outstr || @outary
962
+ reset
963
+ @is_edit_finished = true
964
+ return :first
965
+
966
+ when :@outary, '@outary'
967
+ warn errmsg if @outstr || @is_edit_finished
968
+ @is_edit_finished = false
969
+ close_iotmp # @iotmp=nil; immediate deletion of the temporary file
970
+ @outstr = nil
971
+ return :continuation if @outary
972
+ @outary ||= []
973
+ return :first
974
+
975
+ when :@outstr, '@outstr'
976
+ # For String-type processing, it is allowed if the previous processing
977
+ # is not String-type but Array-type.
978
+ warn errmsg if @is_edit_finished || (@outstr && @outary)
979
+ @is_edit_finished = false
980
+ close_iotmp # @iotmp=nil; immediate deletion of the temporary file
981
+ if @outary
982
+ @outstr = join_outary()
983
+ @outary = nil
984
+ return :continuation
985
+ else
986
+ return :continuation if @outstr
987
+ @outstr ||= ''
988
+ return :first
989
+ end
990
+ else
991
+ raise
992
+ end
993
+ end
994
+ private :normalize_status
995
+
996
+
997
+ # Returns the prefix for message for noop option
998
+ #
999
+ # @return [self]
1000
+ # @yield Should return String or Array (which will be simply joined)
1001
+ def prefix(noop=@noop)
1002
+ (noop ? '[Dryrun]' : '')
1003
+ end
1004
+ private :prefix
1005
+
1006
+
1007
+ # Common routine to process String#sub and String#gsub
1008
+ #
1009
+ # handling the case where no block is given.
1010
+ #
1011
+ # @param *rest [Array<Regexp,String>]
1012
+ # @param max: [Integer] the number of the maximum matches. 0 means no limit (as in String#gsub)
1013
+ # @param **kwd [Hash] ext_enc, int_enc
1014
+ # @return [String, NilClass] nil if not processed because a block is supplied (or error).
1015
+ # @yield the same as String#sub
1016
+ def sub_gsub_args_only(*rest, max: 1, **kwd)
1017
+ read(**kwd)
1018
+ return if 1 == rest.size
1019
+
1020
+ method = caller_locations()[0].label
1021
+ if (max != 1 && 'sub' == method) || (max != 0 && 'gsub' == method)
1022
+ msg = sprintf "WARNING: max option (%s) is given and not 1, but ignored in %s(). Give a block to take it into account..", max, method
1023
+ warn msg
1024
+ end
1025
+
1026
+ # Note: When 2 arguments are given, the block is simply ignored in default (in Ruby 2.5).
1027
+ @outstr.send(method+'!', *rest) # sub! or gsub! => String|nil
1028
+ @outstr
1029
+ end
1030
+ private :sub_gsub_args_only
1031
+
1032
+
1033
+ # Gets an IO of a temporary file (in the same directory as the source file)
1034
+ #
1035
+ # @return [IO]
1036
+ def tempfile_io(**kwd)
1037
+ kwd_def = {}
1038
+ kwd_def[:ext_enc] = @ext_enc_new if @ext_enc_new
1039
+ kwd_def[:int_enc] = @int_enc if @int_enc
1040
+ kwd = kwd_def.merge kwd
1041
+
1042
+ iot = Tempfile.open(File.basename(@fname) + '.' + self.class.to_s, File.dirname(@fname), **kwd)
1043
+ iot.sync=true # Essential!
1044
+ iot
1045
+ end
1046
+ private :tempfile_io
1047
+
1048
+
1049
+ # Issues an warning for {#sub}/{#gsub} and {#sub!}/{#gsub!}
1050
+ #
1051
+ # @return [NilClass]
1052
+ def warn_for_sub_gsub(err)
1053
+ return if !err.message.include?('for nil:NilClass') # and raise-d
1054
+ warn 'WARNING: The variables $1, $2, etc (and $& and Regexp.last_match) are NOT passed to the block in '+full_method_name(1)+' (if that is the cause of this Exception). Use the second block parameter instead, which is the MatchData.'
1055
+ nil
1056
+ end
1057
+ private :warn_for_sub_gsub
1058
+
1059
+
1060
+ # Write a temporary new file
1061
+ #
1062
+ # The Tempfile IO for the new file is set to be @iotmp (so @iotmp.path gives the filename).
1063
+ #
1064
+ # Returns either nil (if no further process is needed) or Hash.
1065
+ # The Hash would be empty if not verbose or !setsize.
1066
+ # Else it would contains the filesizes for :old and :new files.
1067
+ #
1068
+ # @param verbose [Boolean, NilClass]
1069
+ # @param setsize [Boolean] If true, @sizes is set.
1070
+ # @return [Hash, NilClass]
1071
+ def write_new(verbose, setsize=true)
1072
+ if @outstr || @outary
1073
+ @iotmp.close(true) if @iotmp # should be redundant, but to play safe
1074
+ @iotmp = tempfile_io
1075
+ @iotmp.print (@outstr || join_outary())
1076
+ @outstr = nil
1077
+ @outary = nil
1078
+ elsif !@is_edit_finished
1079
+ warn "Input file (#{@fname}) is not opened, and hence is not modified." if !verbose.nil?
1080
+ return
1081
+ end
1082
+
1083
+ return Hash.new if !verbose && !setsize
1084
+
1085
+ @sizes = {
1086
+ :old => File.size(@fname),
1087
+ :new => File.size(@iotmp.path),
1088
+ }
1089
+ if @sizes[:new] == 0
1090
+ warn "The revised file (#{@fname}) is empty." if !verbose.nil?
1091
+ end
1092
+ @sizes
1093
+ end
1094
+ private :write_new
1095
+
1096
+
1097
+ end # class FileOverwrite
1098
+
1099
+ ### Future A/Is
1100
+ #
1101
+ # def backup_base=(filename)
1102
+ # end
1103
+