file_overwrite 0.1 → 1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6552bc81a8cfaf8164a2f1913c03bdb6d72d6df4138fe912b7336e355f8d495
4
- data.tar.gz: 1feabdf441fdb9f6ecb148257c1bb03ac38b653d6ef528f966367b21bedd412a
3
+ metadata.gz: 6db0d19a4e47ac84f622ac13f773b5eebe35187dd988e45e378ff60ad5062f48
4
+ data.tar.gz: 506a41b185811a8ac654141785e7b0022e54f9fcec30b6a59fd6b5fc3d432324
5
5
  SHA512:
6
- metadata.gz: 25f57ff0bd31eab7acba8265309a0a47fa230c758396cd1eb04cb398ab30713197c54cdfd80e6ed7d0a1578e9185ffbd3c4e13ec886afaadfc80990cf0570693
7
- data.tar.gz: a9db5e53923388d9f0710034806d639d03a44a9a287d7d66f3308f14cc27017421f2f156f108fc94c8f1acdaecf576c8dbb050cb27dd3e4cb694a8906e1df163
6
+ metadata.gz: dbc80fba6539d7257e7c8bf8ce48ba56191d0083de67dddc1b5c8b678e7ed5dab0ea8459946310a78e9732181ca184107d90ee43855a7d40af4bb06762b0018a
7
+ data.tar.gz: b4a8bfc7e2c31ba80000f937ed04c77a5e1374aeeae3a4ed102073ff74f5c467cf054597807b7149957910275c365b0104cd95cd07d39a72d9bd521cda02770a
data/ChangeLog CHANGED
@@ -1,4 +1,19 @@
1
1
  -----
2
+ (Version: 1.0)
3
+ 2018-10-13 Masa Sakano
4
+ * Major upgrade. sub and gsub now accept local-scope variables like $1.
5
+
6
+ * A bug in dump is fixed.
7
+ * Added alias save, the methods path and `last_match`, class methods modify! (open!), read, `each_line`.
8
+ * Added `.github/README.md`
9
+ * Major update in README
10
+ * Tests added accordingly.
11
+
12
+ -----
13
+ 2018-10-12 Masa Sakano
14
+
15
+ * mv lib/file_overwrite/file_overwrite.rb lib/file_overwrite.rb
16
+ -----
2
17
  (Version: 0.1)
3
18
  2018-09-19 Masa Sakano
4
19
 
data/Makefile CHANGED
@@ -18,5 +18,5 @@ test:
18
18
  rake test
19
19
 
20
20
  doc:
21
- yard doc
21
+ yard doc; ruby -r rdoc -e 'puts RDoc::Markup::ToMarkdown.new.convert File.read("README.en.rdoc")' > .github/README.md; ls -lF doc/file.README.en.html .github/README.md
22
22
 
data/README.en.rdoc CHANGED
@@ -1,14 +1,23 @@
1
1
 
2
- = FileOverwrite - Controller class to backup a file and overwrite it
2
+ = FileOverwrite - Controller class to overwrite a file with/without a backup file
3
3
 
4
4
  == Summary
5
5
 
6
- This class provides a Ruby-oriented scheme to safely overwrite an existing file, leaving a backup file unless specified otherwise. It writes a temporary file first, which is renamed to the original file in one action. It accepts a block like some IO class-methods (e.g., each_line) and chaining like String methods (e.g., sub and gsub).
6
+ This class provides a Ruby-oriented scheme to safely overwrite an existing file,
7
+ leaving a backup file unless specified otherwise. It writes a temporary file
8
+ first, which is renamed to the original file in one action. It accepts a block
9
+ like some IO or Array class-methods (e.g., +each_line+ and +readlines+) and
10
+ chaining like String methods (e.g., +read+, +sub+ and +gsub+).
11
+
7
12
 
8
13
  This library realises a simple and secure handling to overwrite a file
9
14
  on the spot like "-i" command-line option in Ruby, but inside a Ruby
10
15
  script (rather than globally as in "-i" option) and with much more flexibility.
11
16
 
17
+ The master of this README file, as well as the document for all the methods, is found in
18
+ {RubyGems/file_overwrite}[https://rubygems.org/gems/file_overwrite] where all
19
+ the hyperlinks are active.
20
+
12
21
  == Install
13
22
 
14
23
  This script requires {Ruby}[http://www.ruby-lang.org] Version 2.0
@@ -23,18 +32,70 @@ some error messages can be less helpful in that case.
23
32
 
24
33
  == Examples
25
34
 
35
+ === Basic examples (short-hand versions)
36
+
37
+ The following six examples perform the identical operations.
38
+
39
+ f1 = FileOverwrite.new('a.txt'); f1.open{|ior, iow| iow.print ior.read.upcase}.save!
40
+ f2 = FileOverwrite.open!('b.txt'){|ior, iow| iow.print ior.read.upcase}
41
+ f3 = FileOverwrite.read!('c.txt'){|s| s.upcase}
42
+ f4 = FileOverwrite.read('d.txt').sub!(/.*/m){$&.upcase}
43
+ f5 = FileOverwrite.each_line!('e.txt'){|s| s.upcase}
44
+ f6 = FileOverwrite.readlines!('f.txt'){|a| a.map{|i| i.upcase}}
45
+
46
+ f6.path # => "f.txt"
47
+ f6.backup # => "f.txt.20180915.bak"
48
+ f6.sizes # => { :old => 40, :new => 40 }
49
+
50
+ The first example (f1) is the basic form of the use of this class. You create a class instance by the
51
+ constructor, perform a manipulataion(s) to modify the content, and *save* it,
52
+ which means *run* the overwriting operation of the original file. Until you
53
+ *run*, the original file unchanges, while any modifications are stored either in
54
+ the memory or a temporary file, either of which would be garbage-collected if
55
+ the process is not *run* in the end. A backup file is automatically
56
+ created, unless explicity suppressed, whose filename or suffix is either
57
+ automatically chosen or specified explicitly by the caller.
58
+ Any parts of the operations are chainable or separatable, as you like.
59
+
60
+ In this example, the main manipuration of modification is *IO-type*, or
61
+ File.open-type. You are responsible to manually read the content of the
62
+ existing file and write the updated content. This is the least memory-heavy
63
+ manipulation and so is most suitable when the original file is huge.
64
+
65
+ The second example (f2) is just a short hand of it.
66
+
67
+ The third and fourth examples (f3 and f4) are *String-type*, or IO.read-like.
68
+ You handle the entire content of the file as a single String. The fourth
69
+ example demonstrates an example of chaining manipulations.
70
+
71
+ The fifth example (f5) is another *String-type* manipulation. You manipulate
72
+ the content of the file line by line as a String in the iterator (aka block), as in +IO.each_line+.
73
+ Although technically {FileOverwrite#read} is all you need for the String-type
74
+ manipulation, given you can do any manipulation you like, including
75
+ String#sub and the equivalent one to {FileOverwrite#each_line}, in the block, a
76
+ few more String-type manipulations are defined for convenience (see below).
77
+
78
+ The sixth example (f6) is *Array-type* manipulation. You get the entire
79
+ content of the file as an Array, each element of which corresponds to a line in
80
+ the file just as +IO.readlines+. You manipulate it and return an Array, which
81
+ will be then joined and output to the original file to overwrite it when you save.
82
+
83
+ Any methods with the name ending with a bang sign '!' implies it automatically
84
+ +run!+ (or +save!+) the final overwriting process, as soon as the manipulation
85
+ of content modification finishes.
86
+
26
87
  === Chaining example (with noop, meaning dryrun)
27
88
 
28
89
  f1 = FileOverwrite.new('a.txt', noop: true, verbose: true)
29
90
  # Treat the content as String
30
- f1.sub(/abc/, 'xyz').gsub(/(.)ef/){|i| $1}.run!
91
+ f1.sub(/abc/, 'xyz').gsub(/(.)ef/){$1}.run!
31
92
  f1.completed? # => true
32
93
  f1.sizes # => { :old => 40, :new => 50 }
33
94
  f1.backup # => 'a.txt.20180915.bak'
34
95
  # However, the file has not been created
35
96
  # and the original file has not been modified, either,
36
97
  # due to the noop option
37
-
98
+
38
99
  === IO.read type manipulation (String-based block)
39
100
 
40
101
  f2 = FileOverwrite.new('a.txt', suffix: '~')
@@ -99,36 +160,41 @@ Note if the input file is not touched at all, the file is never
99
160
 
100
161
  == Description
101
162
 
163
+ Output is via +FileUtils#fu_output_message+ and in practice messages are output to STDERR.
164
+
102
165
  === Content manipulation
103
166
 
104
167
  Three types of manipulation for the content of the file to update are allowed: IO, String, and Array.
105
168
 
106
169
  ==== IO-type manipulation
107
170
 
108
- IO-type manipulation includes
171
+ The only IO-type manipulation is
109
172
 
110
- * open (or modify)
173
+ * +open+ (or +modify+)
111
174
 
112
175
  Two block parameters of IO instances are given (read and write in this order).
113
176
  What is output (i.e., IO#print method) with the write-descriptor
114
177
  inside the block will be the content of the overwritten file.
115
178
 
116
179
  This method can *not* be chained. Once you call the method for manipulation,
117
- you have to either {FileOverwrite#run!}, or {FileOverwrite#reset} and do the manipulation from
180
+ you have to either {FileOverwrite#run!}, or {FileOverwrite#reset} and do manipulation from
118
181
  the beginning. For convenience, the same method names with the
119
182
  trailing '!' are defined ({FileOverwrite#open!} and {FileOverwrite#modify!}), with which {FileOverwrite#run!} will be performed automatically.
120
183
 
184
+ The class method {FileOverwrite.open!} (or {FileOverwrite.modify!}) is also available
185
+ to skip the constructor and perform {FileOverwrite.run!} (or +save!+) straightaway.
186
+
121
187
  ==== String-type manipulation
122
188
 
123
189
  String-type manipulation includes
124
190
 
125
- * read (with block)
126
- * sub (with or without block)
127
- * gsub (with or without block)
128
- * tr
129
- * tr_s
130
- * replace_with (same as String#replace, but can be chained)
131
- * each_line (with block)
191
+ * +read+ (with block; like IO.read)
192
+ * +sub+ (with or without block)
193
+ * +gsub+ (with or without block; nb., as an extension from String#gsub the maximum number of matches can be specified with the optional argument max.)
194
+ * +tr+
195
+ * +tr_s+
196
+ * +replace_with+ (same as String#replace, but can be chained)
197
+ * +each_line+ (with block)
132
198
 
133
199
  These methods must return String (or its equivalent) that will be written in the updated file.
134
200
  Those that take block never return Enumerator (like String#sub).
@@ -155,21 +221,26 @@ content of the input file is stored in the memory until {FileOverwrite#run!} is
155
221
  called and the object is GC-ed. For a very large file, IO-type
156
222
  manipulation is more appropriate.
157
223
 
224
+ For +read+ (+read!+) and +each_line+ (+each_line!+), the class methods of the same names are is available to skip the constructor.
225
+
158
226
  ==== Array-type manipulation
159
227
 
160
228
  Similarly, an Array-type manipulation is defined:
161
229
 
162
- * readlines
230
+ * +readlines+
163
231
 
164
- The "readlines" method must return Array (or its equivalent) that will be
165
- written in the updated file, where all the elements are simply concatenated.
166
-
167
- This method can be concatenated with this method only, and can not be
168
- combined with manipulations in the other types.
232
+ The block for the +readlines+ method must return either Array (or its
233
+ equivalent) or String. If an Array returned, another +readlines+ method can be
234
+ called again (or chained), and in the end the elements are simply concatnated
235
+ when they are written in the updated file. If a String is returned, any
236
+ subsequent methods of manipulation must be String-type manipulations before
237
+ {FileOverwrite#run!}.
169
238
 
170
239
  Just like IO-type methods, this can be called with a trailing '!' to
171
240
  perform {FileOverwrite#run!} straightaway.
172
241
 
242
+ The class method {FileOverwrite.readlines} (and +readlines!+) is available to skip the constructor.
243
+
173
244
  === Other methods
174
245
 
175
246
  ==== Those related to the state
@@ -196,6 +267,7 @@ Note none of the write-methods are available once {FileOverwrite#completed?}
196
267
  {FileOverwrite#verbose}:: (read/write) The default value is set in the constructor, which can be overwritten any time with this method, and for temporarily in {FileOverwrite#run!}
197
268
  {FileOverwrite#ext_enc_old}, {FileOverwrite#ext_enc_new}:: (read/write) Character-encoding of the file to read and write, respectively. The former can be overwritten with the {FileOverwrite#force_encoding}(enc) method, too.
198
269
  {FileOverwrite#int_enc}:: (read/write) If set, Character-encoding of the String read from the file is (attempted to be) converted into this before user's processing. This can be overwritten with the {FileOverwrite#encode}() method, too.
270
+ {FileOverwrite#last_match}:: (read/write) To read (and write if need be) Regexp.last_match after {FileOverwrite#sub} or {FileOverwrite#gsub}. If a block is given to them, +Regexp.last_match+ in the caller's scope reflects the result. However, if a block is not given, it does not, and hence this method is the only way to access the last match. See Section "Known bugs" for detail.
199
271
 
200
272
  == Developer's note
201
273
 
@@ -204,12 +276,13 @@ You can view it in {RubyGems/file_overwrite}[https://rubygems.org/gems/file_over
204
276
 
205
277
  === Algorithm
206
278
 
207
- (1) It first writes a temporary file according to your manipulation.
208
- (2) Then when you perform {FileOverwrite#run!}, the following is done in one go:
209
- * the original file is backed up to the specified backup file,
210
- * the temporary file is renamed to the original file,
211
- * if it is instructed to leave no backup file, the backup file is deleted.
212
- (3) After {FileOverwrite#run!}, the instance of this class is still accessible but frozen.
279
+ 1. The manipulated results are held either in the memory or in a temporary file if IO-based manipulation.
280
+ 2. Then when you perform {FileOverwrite#run!}, the following is done in one go:
281
+ 1. the manipulated results are output to a temporary file if not done so, yet (String- or Array-based manipulations),
282
+ 2. the original file is backed up to the specified backup file,
283
+ 3. the temporary file is renamed to the original file,
284
+ 4. if it is instructed to leave no backup file, the backup file is deleted.
285
+ 3. After {FileOverwrite#run!}, the instance of this class is still accessible but frozen.
213
286
 
214
287
  If {FileOverwrite#run!} or its equivalent is not performed, the temporary file will be
215
288
  deleted by GC.
@@ -223,7 +296,12 @@ or simply run <tt>make test</tt>.
223
296
 
224
297
  == Known bugs
225
298
 
226
- None.
299
+ 1. After every execution of {FileOverwrite#sub} and {FileOverwrite#gsub},
300
+ if a block is given to them, +Regexp.last_match+ (or +$~+) in the caller's scope
301
+ reflects the result. However, if a block is not given, it does not, and they
302
+ retain the values before calling {FileOverwrite#sub} etc in the local scope.
303
+ The value of MatchData of the last match in such cases is accessible via
304
+ {FileOverwrite#last_match}.
227
305
 
228
306
 
229
307
  == Copyright
@@ -231,7 +309,3 @@ None.
231
309
  Author:: Masa Sakano < info a_t wisebabel dot com >
232
310
  Versions:: The versions of this package follow Semantic Versioning (2.0.0) http://semver.org/
233
311
 
234
-
235
- LocalWords: FileOverwrite gsub RUBYLIB rb noop dryrun txt ior iow
236
- LocalWords: RuntimeError XYZ FileOverwriteError FrozenError ary
237
- LocalWords: readlines FileUtils mtime allstr outstr chainable
@@ -4,7 +4,7 @@ require 'rake'
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = %q{file_overwrite}
7
- s.version = "0.1"
7
+ s.version = "1.0"
8
8
  # s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
9
  # s.bindir = 'bin'
10
10
  s.authors = ["Masa Sakano"]
@@ -105,14 +105,26 @@ class FileOverwrite
105
105
  # @return [Encoding]
106
106
  attr_accessor :int_enc
107
107
 
108
+ # last_match from {#sub} ({#sub!}) and {#gsub}
109
+ #
110
+ # This values is false when uninitialised.
111
+ # To set this value is for user's convenience only, and
112
+ # has no effect on processing or any of the methods.
113
+ # Every time a user runs {#sub} or {#gsub}, this value is reset.
114
+ #
115
+ # @!attribute [rw] last_match
116
+ # @return [MatchData]
117
+ attr_accessor :last_match
118
+
108
119
  # @param fname [String] Input filename
109
120
  # @param backup: [String, NilClass] File name to which the original file is backed up. If non-Nil, suffix is ignored.
110
121
  # @param suffix: [String, TrueClass, FalseClass, NilClass] Suffix of the backup file. True for Def, or false if no backup.
111
122
  # @param noop: [Boolean] no-operationor dryrun
112
123
  # @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
124
  # @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)
125
+ # @param touch: [Boolean] if true (non-Def), even when the file content does not change, the timestamp is updated, as long as the file is attempted to be save (#{run!} or #{save})
126
+ # @param last_match: [MatchData, NilClass, FalseClass] To pass Regexp.last_match in Caller's scope.
127
+ def initialize(fname, backup: nil, suffix: true, noop: false, verbose: $VERBOSE, clobber: false, touch: false, last_match: false)
116
128
  @fname = fname
117
129
  @backup = backup
118
130
  @suffix = (backup ? true : suffix)
@@ -120,6 +132,7 @@ class FileOverwrite
120
132
  @verbose = $DEBUG || verbose
121
133
  @clobber = clobber
122
134
  @touch = touch
135
+ @last_match = last_match
123
136
 
124
137
  @ext_enc_old = nil
125
138
  @ext_enc_new = nil
@@ -144,7 +157,7 @@ class FileOverwrite
144
157
  # (so you can tell what the backup filename would be if the suffix was set).
145
158
  #
146
159
  # @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.
160
+ # @param backupfile: [String, NilClass] Explicilty specify the backup filename, when suffix is nil. (For internal use)
148
161
  # @return [String, NilClass]
149
162
  def backup(suffix=nil, backupfile: nil)
150
163
  return backup_from_suffix(suffix) if suffix # non-nil suffix explicitly given
@@ -187,7 +200,7 @@ class FileOverwrite
187
200
  def dump
188
201
  return @outstr.dup if @outstr
189
202
  return join_outary() if @outary
190
- return File.read(@iotmp.path) if @is_edit_finished
203
+ return File.read(@iotmp.path) if @is_edit_finished && !completed?
191
204
  File.read(@fname)
192
205
  end
193
206
 
@@ -266,6 +279,16 @@ class FileOverwrite
266
279
  alias_method :reset?, :fresh? if ! self.method_defined?(:reset?)
267
280
 
268
281
 
282
+ # Returns the (duplicate of the) filename to be (or to have been) updated.
283
+ #
284
+ # To destructively modify this value would affect nothing in the parent object.
285
+ #
286
+ # @return [String]
287
+ def path
288
+ @fname.dup
289
+ end
290
+
291
+
269
292
  # Returns true if the instance is ready to run (to execute overwriting the file).
270
293
  def ready?
271
294
  !fresh? && !completed?
@@ -314,7 +337,7 @@ class FileOverwrite
314
337
 
315
338
  # String#valid_encoding?()
316
339
  #
317
- # returns nil if the process has been already completed.
340
+ # @note returns nil if the process has been already completed.
318
341
  #
319
342
  # @return [Boolean, NilClass]
320
343
  def valid_encoding?()
@@ -422,7 +445,9 @@ class FileOverwrite
422
445
 
423
446
  return self
424
447
  end
425
- alias_method :run, :run! if ! self.method_defined?(:run)
448
+ alias_method :run, :run! if ! self.method_defined?(:run)
449
+ alias_method :save, :run! if ! self.method_defined?(:save)
450
+ alias_method :save!, :run! if ! self.method_defined?(:save!)
426
451
 
427
452
 
428
453
  ########################################################
@@ -459,13 +484,18 @@ class FileOverwrite
459
484
  raise ArgumentError, 'Block must be given.' if !block_given?
460
485
  normalize_status(:@is_edit_finished)
461
486
 
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
487
+ kwd_open = {}
488
+ kwd_open[:external_encoding] = @ext_enc_old if @ext_enc_old
489
+ kwd_open[:internal_encoding] = @int_enc if @int_enc
490
+ kwd_open[:external_encoding] = (kwd[:ext_enc] || kwd_open[:external_encoding])
491
+ kwd_open[:internal_encoding] = (kwd[:int_enc] || kwd_open[:internal_encoding])
492
+ [:mode, :flags, :encoding, :textmode, :binmode, :autoclose].each do |es|
493
+ # Method list from https://ruby-doc.org/core-2.5.1/IO.html#method-c-new
494
+ kwd_open[es] = kwd[es] if kwd.key?(es)
495
+ end
466
496
 
467
497
  begin
468
- File.open(@fname, **kwd) { |ioin|
498
+ File.open(@fname, **kwd_open) { |ioin|
469
499
  @iotmp = tempfile_io
470
500
  yield(ioin, @iotmp)
471
501
  }
@@ -577,14 +607,15 @@ class FileOverwrite
577
607
 
578
608
  # Handler to process the entire string of the file (or current content)
579
609
  #
580
- # If block is not given, just sets the state as String
610
+ # If block is not given, just sets the processing-state as String.
581
611
  #
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.
612
+ # Else, IO.read(infile) is given to the block. No other options, such as length,
613
+ # as in IO.read are accepted. Then, the returned value is held as a String,
614
+ # while self is returned; hence this method can be chained.
584
615
  # If the block returns nil (or Boolean), {FileOverwriteError} is raised.
585
- # Make sure to return a String (whether an empty string or "true")
616
+ # Make sure for the block to return a String.
586
617
  #
587
- # Note this method does not take arguments as in IO.read
618
+ # Note this method does not take arguments as in IO.read .
588
619
  #
589
620
  # @param **kwd [Hash] ext_enc, int_enc
590
621
  # @return [self]
@@ -599,7 +630,8 @@ class FileOverwrite
599
630
  end
600
631
 
601
632
  @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
633
+ raise FileOverwriteError, 'ERROR: The returned value from the block in read() has to be String.' if !defined?(@outstr.gsub)
634
+ warn "WARNING: Empty string returned from a block in #{__method__}" if !@verbose.nil? && @outstr.empty?
603
635
  self
604
636
  end
605
637
 
@@ -614,7 +646,7 @@ class FileOverwrite
614
646
  end
615
647
 
616
648
 
617
- # Similar to {String#replace} but the original is not modified
649
+ # Replaces the file content with the given argument like {String#replace}
618
650
  #
619
651
  # This method can be chained.
620
652
  #
@@ -641,18 +673,22 @@ class FileOverwrite
641
673
  # This method can be chained.
642
674
  # This method never returns an Enumerator.
643
675
  #
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.
676
+ # @note Algorithm
677
+ # To realise the local-scope variables like $~, $1, and Regexp.last_match to
678
+ # be usable inside the block as in String#sub, it overwrites them when a block
679
+ # is given (See the linked article for the phylosophy of how to do it).
680
+ # Once a block is read, those variables remain as updated values even after the block
681
+ # in the caller's scope, in the same way as String#sub. However, when a block is not given,
682
+ # those variables are *NOT* updated, which is different from String#sub.
683
+ # You can retrieve the MatchData by this method via {#last_match} after {#sub}
684
+ # is called, if need be.
650
685
  #
651
686
  # @param *rest [Array<Regexp,String>]
652
687
  # @param max: [Integer] the number of the maximum matches. If it is not 1, {#gsub} is called, instead. See {#gsub} for detail.
653
688
  # @param **kwd [Hash] ext_enc, int_enc
654
689
  # @return [self]
655
690
  # @yield the same as String#sub
691
+ # @see https://stackoverflow.com/questions/52359278/how-to-pass-regexp-last-match-to-a-block-in-ruby/52385870#52385870
656
692
  def sub(*rest, max: 1, **kwd, &bloc)
657
693
  return self if sub_gsub_args_only(*rest, max: max, **kwd)
658
694
 
@@ -664,16 +700,20 @@ class FileOverwrite
664
700
  return gsub(*rest, max: max, **kwd, &bloc)
665
701
  end
666
702
 
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
703
+ @last_match = rest[0].match(@outstr)
704
+ return self if !@last_match
705
+
706
+ # Sets $~ (Regexp.last_match) in the given block.
707
+ # @see https://stackoverflow.com/questions/52359278/how-to-pass-regexp-last-match-to-a-block-in-ruby/52385870#52385870
708
+ bloc.binding.tap do |b|
709
+ b.local_variable_set(:_, $~)
710
+ b.eval("$~=_")
676
711
  end
712
+
713
+ # The first (and only) argument for the block is $& .
714
+ # Returning nil, Integer etc is accepted in the block of sub/gsub
715
+ @outstr = @last_match.pre_match + yield(@last_match[0]).to_s + @last_match.post_match
716
+ return self
677
717
  end
678
718
 
679
719
 
@@ -693,26 +733,33 @@ class FileOverwrite
693
733
  # This method can be chained.
694
734
  # This method never returns an Enumerator.
695
735
  #
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
736
  # Being different from the standard Srrint#gsub, this method accepts
700
737
  # the optional parameter max, which specifies the maximum number of times
701
738
  # of the matches and is valid ONLY WHEN a block is given.
702
739
  #
740
+ # @note Algorithm
741
+ # See {#sub} for the basic algorithm.
742
+ # This method emulates String#gsub as much as possible (duck-typing).
743
+ # In String#gsub, the variable $~ after the method has the last matched characters
744
+ # as the matched string and the original string before the last matched characters
745
+ # as pre_match. For example,
746
+ # 'abc'.gsub(/./){$1.upcase}
747
+ # returns
748
+ # 'ABC'
749
+ # and leaves
750
+ # $& == 'c'
751
+ # Regexp.pre_match == 'ab'
752
+ # It is the same in this method.
753
+ #
703
754
  # @note Disclaimer
704
755
  # When a block is not given but arguments only (and not expecting Enumerator to return),
705
756
  # this method simply calls String#gsub . However, when only 1 argument
706
757
  # and a block is given, this method must iterate on its own, which is implemented.
707
758
  # 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.
759
+ # as String#gsub in every single situation, given the regular expression
760
+ # has so many possibilities; so far I have not found any cases where this method breaks.
712
761
  # 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.
762
+ # as the iteration is implemented in pure Ruby.
716
763
  #
717
764
  # @param *rest [Array<Regexp,String>]
718
765
  # @param max: [Integer] the number of the maximum matches. 0 means no limit (as in String#gsub). Valid only if a block is given.
@@ -720,6 +767,7 @@ class FileOverwrite
720
767
  # @return [self]
721
768
  # @yield the same as String#gsub
722
769
  # @see #sub
770
+ # @see https://stackoverflow.com/questions/52359278/how-to-pass-regexp-last-match-to-a-block-in-ruby/52385870#52385870
723
771
  def gsub(*rest, max: 0, **kwd, &bloc)
724
772
  return sub(*rest, max: 1, **kwd, &bloc) if 1 == max # Note: Error message would be labelled as 'sub'
725
773
  return self if sub_gsub_args_only(*rest, max: max, **kwd)
@@ -730,39 +778,45 @@ class FileOverwrite
730
778
 
731
779
  max = 5.0/0 if max.to_i <= 0
732
780
 
733
- scans = @outstr.scan(rest[0])
781
+ regbase_str = rest[0].to_s
782
+ regex = Regexp.new( sprintf('(%s)', regbase_str) ) # to guarantee the entire string is picked up by String#scan
783
+ scans = @outstr.scan(regex)
734
784
  return self if scans.empty? # no matches
735
785
 
736
786
  scans.map!{|i| [i].flatten} # Originally, it can be a double array.
737
- regbase_str = rest[0].to_s
738
787
  prematch = ''
739
788
  ret = ''
740
789
  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
790
+ scans.each do |ea_sc|
791
+ str_matched = ea_sc[0]
792
+ imatch += 1
793
+ pre_size = prematch.size
794
+ pos_end_p1 = @outstr.index(str_matched, pre_size) # End+1
795
+ str_between = @outstr[pre_size...pos_end_p1]
796
+ prematch << str_between
797
+ ret << str_between
798
+ regex = Regexp.new( sprintf('(?<=\A%s)%s', Regexp.quote(prematch), regbase_str) )
799
+ #regex = rest[0] if prematch.empty? # The first run
800
+ @last_match = regex.match(@outstr)
801
+ prematch << str_matched
802
+
803
+ # Sets $~ (Regexp.last_match) in the given block.
804
+ # @see https://stackoverflow.com/questions/52359278/how-to-pass-regexp-last-match-to-a-block-in-ruby/52385870#52385870
805
+ bloc.binding.tap do |b|
806
+ b.local_variable_set(:_, $~)
807
+ b.eval("$~=_")
757
808
  end
758
- ret << Regexp.last_match.post_match # Guaranteed to be non-nil.
759
809
 
760
- @outstr = ret
761
- return self
762
- rescue NoMethodError => err
763
- warn_for_sub_gsub(err)
764
- raise
810
+ # The first (and only) argument for the block is $& .
811
+ # Returning nil, Integer etc is accepted in the block of sub/gsub
812
+ ret << yield(@last_match[0]).to_s
813
+
814
+ break if imatch >= max
765
815
  end
816
+ ret << Regexp.last_match.post_match # Guaranteed to be non-nil.
817
+
818
+ @outstr = ret
819
+ return self
766
820
  end
767
821
 
768
822
 
@@ -820,6 +874,16 @@ class FileOverwrite
820
874
  # Class methods
821
875
  ########################################################
822
876
 
877
+ # Class method for {FileOverwrite#initialize}.{#modify!}
878
+ #
879
+ # @see #initialize
880
+ # @see #modify
881
+ def self.modify!(*rest, **kwd, &bloc)
882
+ new(*rest, **kwd).modify!(**kwd, &bloc)
883
+ end
884
+ singleton_class.send(:alias_method, :open!, :modify!)
885
+
886
+
823
887
  # Shorthand of {FileOverwrite#initialize}.{#readlines}, taking parameters for both
824
888
  #
825
889
  # @param fname [String] Input and overwriting filename
@@ -844,6 +908,39 @@ class FileOverwrite
844
908
  end
845
909
 
846
910
 
911
+ # Class method for {FileOverwrite#initialize}.{#read}
912
+ #
913
+ # @see #initialize
914
+ # @see #read
915
+ def self.read(*rest, **kwd, &bloc)
916
+ new(*rest, **kwd).send(__method__, **kwd, &bloc)
917
+ end
918
+
919
+ # Class method for {FileOverwrite#initialize}.{#read!}
920
+ #
921
+ # @see #initialize
922
+ # @see #read!
923
+ def self.read!(*rest, **kwd, &bloc)
924
+ new(*rest, **kwd).send(__method__, **kwd, &bloc)
925
+ end
926
+
927
+ # Class method for {FileOverwrite#initialize}.{#each_line}
928
+ #
929
+ # @see #initialize
930
+ # @see #each_line
931
+ def self.each_line(fname, *rest, **kwd, &bloc)
932
+ new(fname, **kwd).send(__method__, *rest, **kwd, &bloc)
933
+ end
934
+
935
+ # Class method for {FileOverwrite#initialize}.{#each_line!}
936
+ #
937
+ # @see #initialize
938
+ # @see #each_line!
939
+ def self.each_line!(fname, *rest, **kwd, &bloc)
940
+ new(fname, **kwd).send(__method__, *rest, **kwd, &bloc)
941
+ end
942
+
943
+
847
944
  ########################################################
848
945
  private
849
946
  ########################################################
@@ -1018,13 +1115,14 @@ class FileOverwrite
1018
1115
  return if 1 == rest.size
1019
1116
 
1020
1117
  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
1118
+ if !@verbose.nil? && ((max != 1 && 'sub' == method) || (max != 0 && 'gsub' == method))
1119
+ msg = sprintf "WARNING: max option (%s) of neither 0 nor 1 is given. It is ignored in %s(). Give a block (instead of just arguments) for the max option to be taken into account.", max, method
1023
1120
  warn msg
1024
1121
  end
1025
1122
 
1026
1123
  # Note: When 2 arguments are given, the block is simply ignored in default (in Ruby 2.5).
1027
1124
  @outstr.send(method+'!', *rest) # sub! or gsub! => String|nil
1125
+ @last_match = Regexp.last_match # $~
1028
1126
  @outstr
1029
1127
  end
1030
1128
  private :sub_gsub_args_only
@@ -5,7 +5,7 @@
5
5
 
6
6
  # require 'tempfile'
7
7
  # require 'fileutils'
8
- require 'file_overwrite/file_overwrite'
8
+ require 'file_overwrite'
9
9
 
10
10
  $stdout.sync=true
11
11
  $stderr.sync=true
@@ -13,6 +13,11 @@ $stderr.sync=true
13
13
 
14
14
  #################################################
15
15
  # Unit Test
16
+ #
17
+ # Note: $VERBOSE is set true in default in Testing.
18
+ # Then, the VERBOSE option in FileOverwite.new is
19
+ # also set TRUE in the tests in default, being different
20
+ # from the Ruby default environment ($VERBOSE==false).
16
21
  #################################################
17
22
 
18
23
  #if $0 == __FILE__
@@ -33,7 +38,7 @@ $stderr.sync=true
33
38
 
34
39
  @orig_path = @tmpioin.path
35
40
  @orig_content = "1 line A\n2 line B\n3 line C\n"
36
- @orig_mtime = Time.now - 86400 # A day earlier
41
+ @orig_mtime = Time.now.round - 86400 # A day earlier
37
42
 
38
43
  @tmpioin.print @orig_content
39
44
  reset_mtime
@@ -77,6 +82,7 @@ $stderr.sync=true
77
82
  assert_equal @tmpioin.path, m[1]
78
83
  end
79
84
 
85
+ # Testing backup file names
80
86
  def test_backup
81
87
  file1 = create_template_file
82
88
  fpath = file1.path
@@ -91,12 +97,16 @@ $stderr.sync=true
91
97
  assert_equal fpath+suffix1, f1.backup # Reverted back
92
98
  end
93
99
 
94
- def test_open # modify
100
+ # FileOverwrite#modify (or #open as an alias)
101
+ def test_open
95
102
  file1 = create_template_file
96
103
  fpath = file1.path
97
104
  f1 = FO.new(fpath)
98
105
  s = 'tekito'
99
106
 
107
+ # Testing non-block
108
+ assert_raises(ArgumentError){ f1.modify }
109
+
100
110
  # Testing open
101
111
  f1.open{|ior, iow|
102
112
  # Line 1
@@ -143,10 +153,33 @@ $stderr.sync=true
143
153
  assert_nil f1.temporary_filename
144
154
  end
145
155
 
156
+
157
+ # FileOverwrite.#open! (an alias of #modify!)
158
+ def test_open_classmethod
159
+ file1 = create_template_file
160
+ fpath = file1.path
161
+ s = 'tekito'
162
+
163
+ # Testing FileOverwrite.open!
164
+ f1 = nil
165
+ assert_output('', /File.+updated/){
166
+ f1 = FO.open!(fpath){|ior, iow|
167
+ line = ior.gets
168
+ ior.gets line
169
+ iow.print s+s
170
+ }
171
+ }
172
+ assert_equal fpath, f1.path
173
+ assert_equal s+s, f1.dump # Testing dump after completed.
174
+ assert_equal s+s, File.read(f1.path)
175
+ end
176
+
177
+
178
+ # Tests of read, read!, replace_with
146
179
  def test_read
147
180
  file1 = create_template_file
148
181
  fpath = file1.path
149
- f1 = FO.new(fpath)
182
+ f1 = FO.new(fpath, verbose: true)
150
183
 
151
184
  s = 'tekito'
152
185
  f1.read{|i| s}
@@ -167,6 +200,20 @@ $stderr.sync=true
167
200
  assert_output('', / not opened,/){ f1.run!(verbose: true) }
168
201
  assert_equal @orig_content, File.read(fpath)
169
202
  assert_in_delta(@orig_mtime, File.mtime(fpath), 1) # 1 sec allowance
203
+
204
+ # Test of invalid returns from the block
205
+ f1.reset
206
+ assert_raises(FileOverwriteError){ f1.read{} }
207
+ f1.reset
208
+ assert_raises(FileOverwriteError){ f1.read{nil} }
209
+ f1.reset
210
+ assert_raises(FileOverwriteError){ f1.read{5} }
211
+ f1.reset
212
+ assert_output('', /Empty string[^\n]*\n.*File .+updated.+Size.* => 0 bytes/im){ f1.read!(){''} }
213
+ # Warning is issued if an empty string is returned, but it is saved nonetheless.
214
+ assert_equal 0, f1.sizes[:new] # The new size is zero.
215
+
216
+ assert_raises(FrozenError){ f1.reset } # Prohibited to reset, once saved.
170
217
  end
171
218
 
172
219
  def test_open_run_noop # modify
@@ -289,13 +336,30 @@ $stderr.sync=true
289
336
  assert_equal @orig_content+s, File.read(fpath)
290
337
  assert_nil f1.sizes
291
338
  end
292
-
339
+
340
+ # FileOverwrite.#read!
341
+ def test_read_classmethod
342
+ file1 = create_template_file
343
+ fpath = file1.path
344
+ s = 'tekito'
345
+
346
+ # Testing FileOverwrite.read!
347
+ f1 = nil
348
+ assert_output('', /File.+updated/){
349
+ f1 = FO.read!(fpath){|i| s+s}
350
+ }
351
+ assert_equal fpath, f1.path
352
+ assert_equal s+s, f1.dump # Testing dump after completed.
353
+ assert_equal s+s, File.read(f1.path)
354
+ end
355
+
293
356
  def test_sub
294
357
  file1 = create_template_file
295
358
  fpath = file1.path
296
359
  f1 = FO.new(fpath, suffix: nil, verbose: true)
297
360
 
298
361
  f1.sub(/line/, 'xyz')
362
+ assert_equal 'line', f1.last_match[0]
299
363
  assert_match(/xyz/, f1.dump.split(/\n/)[0])
300
364
  refute_match(/xyz/, f1.dump.split(/\n/)[1])
301
365
  f1.reset
@@ -308,6 +372,9 @@ $stderr.sync=true
308
372
  refute_match(/xyz/, f1.dump.split(/\n/)[0])
309
373
 
310
374
  f1.sub(/(li)(n)/, '\1' + 'k')
375
+ assert_equal 'lin', f1.last_match[0]
376
+ assert_equal 'li', f1.last_match[1]
377
+ assert_equal 'n', f1.last_match[2]
311
378
  assert_match(/like/, f1.dump.split(/\n/)[0])
312
379
  refute_match(/like/, f1.dump.split(/\n/)[1])
313
380
  f1.reset
@@ -315,16 +382,17 @@ $stderr.sync=true
315
382
 
316
383
  # Failed-match case
317
384
  f1.sub(/naiyo(li)(n)/, '\1' + 'k')
385
+ assert_nil f1.last_match
318
386
  assert_match(/line/, f1.dump.split(/\n/)[0])
319
387
  assert_equal @orig_content, f1.instance_eval{@outstr}
320
388
  f1.reset
321
389
 
322
- ### The following does NOT work!
323
- # f1.sub(/(li)(ne)/){printf "and=(%s)(%s)", $&, $1; 'LIne'} # $1.upcase + $2
324
-
325
- ### MatchData extension in this method!
326
- f1.sub(/(li)(n)/){|_,m| m[1].upcase+m[2]} # $1.upcase + $2
327
-
390
+ # With a block
391
+ f1.sub(/(li)(n)/){$1.upcase+$2}
392
+ # f1.sub(/(li)(n)/){|_,m| m[1].upcase+m[2]} # $1.upcase + $2 # Old version
393
+ assert_equal 'lin', f1.last_match[0]
394
+ assert_equal 'li', f1.last_match[1]
395
+ assert_equal 'n', f1.last_match[2]
328
396
  assert_match(/LIne/, f1.dump.split(/\n/)[0], "DEBUG: "+f1.dump)
329
397
  refute_match(/LIne/, f1.dump.split(/\n/)[1])
330
398
  f1.sub(/I/){nil} # nil.to_s (as in String#sub)
@@ -335,8 +403,19 @@ $stderr.sync=true
335
403
  refute_match(/LI?ne/, f1.dump.split(/\n/)[0])
336
404
  assert_nil f1.instance_eval{@outstr}
337
405
 
406
+ # With a block with an argument
407
+ f1.sub(/(li)(n)/){|ms| ms.upcase} # ms == $&
408
+ assert_equal 'lin', f1.last_match[0]
409
+ assert_equal 'li', f1.last_match[1]
410
+ assert_equal 'n', f1.last_match[2]
411
+ assert_match(/LINe/, f1.dump.split(/\n/)[0], "DEBUG: "+f1.dump)
412
+ f1.reset
413
+ refute_match(/LI?ne/, f1.dump.split(/\n/)[0])
414
+ assert_nil f1.instance_eval{@outstr}
415
+
338
416
  # Failed-match case
339
417
  f1.sub(/naiyo(li)(n)/){|_,m| m[1].upcase+m[2]} # $1.upcase + $2
418
+ assert_nil f1.last_match
340
419
  assert_match(/line/, f1.dump.split(/\n/)[0], "DEBUG: "+f1.dump)
341
420
  f1.reset
342
421
  end
@@ -347,7 +426,10 @@ $stderr.sync=true
347
426
  fpath = file1.path
348
427
  f1 = FO.new(fpath, suffix: nil, verbose: true)
349
428
 
429
+ f1.gsub(/naiyo/, 'xyz') # Failed match
430
+ assert_nil f1.last_match
350
431
  assert_output('', ''){ f1.gsub(/line/, 'xyz') }
432
+ assert_equal 'line', f1.last_match[0]
351
433
  assert_match(/xyz/, f1.dump.split(/\n/)[0])
352
434
  assert_match(/xyz/, f1.dump.split(/\n/)[1])
353
435
  assert_match(/xyz/, f1.dump.split(/\n/)[2])
@@ -355,6 +437,7 @@ $stderr.sync=true
355
437
  refute_match(/xyz/, f1.dump.split(/\n/)[0])
356
438
 
357
439
  assert_output('', ''){ f1.gsub(/line/, 'xyz', max: 1) }
440
+ assert_equal 'line', f1.last_match[0]
358
441
  assert_match(/xyz/, f1.dump.split(/\n/)[0])
359
442
  refute_match(/xyz/, f1.dump.split(/\n/)[1])
360
443
  f1.reset
@@ -364,6 +447,7 @@ $stderr.sync=true
364
447
  assert_output('', /WARNING\b.+\bmax/i){ f1.gsub(/line/, 'xyz', max: 2) }
365
448
  f1.reset
366
449
 
450
+ # With a block
367
451
  assert_output(nil){ f1.gsub(/line/){ 'xyz' } }
368
452
  assert_match(/xyz/, f1.dump.split(/\n/)[0])
369
453
  assert_match(/xyz/, f1.dump.split(/\n/)[1])
@@ -371,12 +455,44 @@ $stderr.sync=true
371
455
  f1.reset
372
456
  refute_match(/xyz/, f1.dump.split(/\n/)[0])
373
457
 
458
+ # With a block with an argument and $1 etc
459
+ mat=nil
460
+ f1.gsub(/lin(e)/){ |ms| ms+$1.upcase } # lineE
461
+ mat=$~
462
+ assert_equal mat, f1.last_match
463
+ assert_match(/lineE/, f1.dump.split(/\n/)[0])
464
+ assert_match(/lineE/, f1.dump.split(/\n/)[1])
465
+ assert_match(/lineE/, f1.dump.split(/\n/)[2])
466
+ assert_equal 'e', mat[1]
467
+ assert_equal 2, mat.pre_match.scan(/\bline\b/).size # "1 line A\n2 line B\n3 "
468
+ assert_match(/[^\n]+\n$/m, mat.post_match) # " C\n"
469
+ f1.reset
470
+ refute_match(/lineE/, f1.dump.split(/\n/)[0])
471
+
472
+ # With a block with max option
374
473
  assert_output('', ''){ f1.gsub(/line/, max: 2){ 'xyz' } }
474
+ assert_equal 'line', f1.last_match[0]
375
475
  assert_match(/xyz/, f1.dump.split(/\n/)[0])
376
476
  assert_match(/xyz/, f1.dump.split(/\n/)[1])
377
477
  refute_match(/xyz/, f1.dump.split(/\n/)[2])
378
478
  f1.reset
379
479
  refute_match(/xyz/, f1.dump.split(/\n/)[0])
480
+
481
+ # With a block with an argument and $1 etc
482
+ # assert_output(nil){ f1.gsub(/lin(e)/){ |ms| ms+$1.upcase } } # lineE
483
+ mat=nil
484
+ f1.gsub(/lin(e)/, max: 2){ |ms| ms+$1.upcase } # lineE
485
+ mat=$~
486
+ assert_equal mat, f1.last_match
487
+ assert_match(/lineE/, f1.dump.split(/\n/)[0])
488
+ assert_match(/lineE/, f1.dump.split(/\n/)[1])
489
+ refute_match(/lineE/, f1.dump.split(/\n/)[2])
490
+ assert_equal 'e', mat[1]
491
+ assert_equal 1, mat.pre_match.scan(/\bline\b/).size # "1 line A\n2 "
492
+ assert_equal " B\n3 line C\n", mat.post_match
493
+ assert_match(/[^\n]+\n[^\n]+\n$/m, mat.post_match) # " B\n3 line C\n"
494
+ f1.reset
495
+ refute_match(/lineE/, f1.dump.split(/\n/)[0])
380
496
  end
381
497
 
382
498
 
@@ -437,6 +553,25 @@ $stderr.sync=true
437
553
  end
438
554
 
439
555
 
556
+ # FileOverwrite.#each_line
557
+ def test_each_line_classmethod
558
+ file1 = create_template_file
559
+ fpath = file1.path
560
+ s = 'tekito'
561
+ nlines = File.read(fpath).count("\n") # == 3
562
+
563
+ # Testing FileOverwrite.each_line
564
+ f1 = nil
565
+ f1 = FO.each_line(fpath){|i| s}
566
+ assert_equal fpath, f1.path
567
+ assert_equal s*nlines, f1.dump # Testing dump after completed.
568
+ refute_equal s*nlines, File.read(f1.path)
569
+ assert_output('', /File.+updated/){
570
+ f1.save
571
+ }
572
+ assert_equal s*nlines, File.read(f1.path)
573
+ end
574
+
440
575
  def test_readlines
441
576
  file1 = create_template_file
442
577
  fpath = file1.path
@@ -462,6 +597,24 @@ $stderr.sync=true
462
597
  assert_equal s.size, sizes[:new]
463
598
  end
464
599
 
600
+ def test_touch
601
+ file1 = create_template_file
602
+ fpath = file1.path
603
+ f1 = FO.new(fpath, suffix: nil, verbose: true, touch: true)
604
+
605
+ assert_equal fpath, f1.path
606
+ assert_equal @orig_mtime.to_s, File.mtime(f1.path).to_s
607
+ f1.path.replace('naiyo')
608
+ assert_equal fpath, f1.path
609
+ Time.stub :now, Time.now do
610
+ assert_output('', /^(\s*warning:?\s*)?No change\b.+timestamp is updated/i){ f1.read!{|i| i} }
611
+ assert_equal fpath, f1.path
612
+
613
+ refute_equal @orig_mtime.to_s, File.mtime(f1.path).to_s # b/c touched
614
+ assert_equal Time.now.to_s, File.mtime(f1.path).to_s # nb., Time#round would add a second.
615
+ end
616
+ end
617
+
465
618
  end # class TestUnitFileOverwrite < MiniTest::Test
466
619
 
467
620
  #end # if $0 == __FILE__
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: file_overwrite
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '1.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masa Sakano
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-19 00:00:00.000000000 Z
11
+ date: 2018-10-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: This class provides a Ruby-oriented scheme to safely overwrite an existing
14
14
  file, leaving a backup file unless specified otherwise. It writes a temporary file
@@ -27,7 +27,7 @@ files:
27
27
  - README.en.rdoc
28
28
  - Rakefile
29
29
  - file_overwrite.gemspec
30
- - lib/file_overwrite/file_overwrite.rb
30
+ - lib/file_overwrite.rb
31
31
  - lib/file_overwrite/file_overwrite_error.rb
32
32
  - test/test_file_overwrite.rb
33
33
  homepage: https://www.wisebabel.com