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.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/ChangeLog +5 -0
- data/Makefile +22 -0
- data/README.en.rdoc +237 -0
- data/Rakefile +9 -0
- data/file_overwrite.gemspec +45 -0
- data/lib/file_overwrite/file_overwrite.rb +1103 -0
- data/lib/file_overwrite/file_overwrite_error.rb +4 -0
- data/test/test_file_overwrite.rb +469 -0
- metadata +60 -0
@@ -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
|
+
|