class FileOverwrite

Controller class to backup a file and overwrite it

Ruby iterators and chaining-methods are fully exploited to edit the file.

Examples

f1 = FileOverwrite.new('a.txt', noop: true, verbose: true)
  # Treat the content as String
f1.sub(/abc/, 'xyz').gsub(/(.)ef/){|i| $1}.run!
f1.sizes   # => { :old => 40, :new => 50 }
f1.backup  # => 'a.txt.20180915.bak'
           # However, the file has not been created
           # and the original file has not been modified, either,
           # due to the noop option

f2 = FileOverwrite.new('a.txt', suffix: '~')
f2.backup  # => 'a.txt~'
f2.completed?  # => false
  # Treat the content as String inside the block
f2.read{ |str| "\n" + i + "\n" }.gsub(/a\nb/m, '').run!
f2.completed?  # => true
FileOverwrite.new('a.txt', suffix: '~').sub(/a/, '').run!
  # => RuntimeError, because the backup file 'a.txt~' exists.
FileOverwrite.new('a.txt', suffix: '~').sub(/a/, '').run!(clobber: true)
  # => The backup file is overwritten.

f3 = FileOverwrite.new('a.txt', backup: '/tmp/b.txt')
  # Backup file can be explicitly specified.
f3.backup  # => '/tmp/b.txt'
f3.backup = 'original.txt'
f3.backup  # => 'original.txt'
  # Treat the file as IO inside the block
f3.open{ |ior, iow| i + "XYZ" }
f3.reset   # The modification is discarded
f3.reset?  # => true
f3.open{ |ior, iow| i + "XYZ"; raise FileOverwriteError, 'I stop.' }
           # To discard the modification inside the block
f3.reset?  # => true
f3.open{ |ior, iow| "\n" + i + "\n" }
f3.run!(noop: true, verbose: true)  # Dryrun
f3.completed?  # => true
f3.backup = 'change.d'  # => FrozenError (the state can not be modified after run!(), including dryrun)

f4 = FileOverwrite.new('a.txt', suffix: nil)
f4.backup  # => nil (No backup file is created.)
f4.readlines{|ary| ary+["last\n"]}.each{|i| 'XX'+i}.run!
IO.readlines('a.txt')[-1]   # => "XXlast\n"

f5 = FileOverwrite.new('a.txt', suffix: '.bak')
f5.backup  # => 'a.txt.bak'
f5.read{|i| i}.run!
FileUtils.identical? 'a.txt', 'a.txt.bak'       # => true
File.mtime('a.txt') == File.mtime('a.txt.bak')  # => true
  # To forcibly update the Timestamp, give touch option as true
  # either in new() or run!(), ie., run!(touch: true)

@author Masa Sakano

Attributes

backup[W]

Sets the backup filename. Read method ({#backup}) is provided separately. @!attribute [w] backup

@return [String]  Keys: :old and :new
ext_enc_new[RW]

Encoding of the content of the output file. Default is nil (unspecified). @!attribute [rw] ext_enc

@return [Encoding]
ext_enc_old[RW]

Encoding of the content of the input file. Default is nil (unspecified). @!attribute [rw] ext_enc

@return [Encoding]
int_enc[RW]

Encoding of the content (String, Array) of the file or IO to be passed to the user. @!attribute [rw] int_enc

@return [Encoding]
last_match[RW]

last_match from {#sub} ({#sub!}) and {#gsub}

This values is false when uninitialised. To set this value is for user's convenience only, and has no effect on processing or any of the methods. Every time a user runs {#sub} or {#gsub}, this value is reset.

@!attribute [rw] last_match

@return [MatchData]
sizes[R]

Hash of the file sizes of before (:old) and after (:new).

This is set after {#run!} and if setsize option in {#run!} is given true (Default) or if verbose option is true (Def: false). Else, nil is returned.

@!attribute [r] sizes

@return [Hash, NilClass]  Keys: :old and :new
verbose[RW]

Verbose flag can be read or set any time, except after the process is completed @!attribute [rw] verbose

@return [Boolean]

Public Class Methods

each_line(fname, *rest, **kwd, &bloc) click to toggle source

Class method for {FileOverwrite#initialize}.{#each_line}

@see initialize @see each_line

# File lib/file_overwrite.rb, line 930
def self.each_line(fname, *rest, **kwd, &bloc)
  new(fname, **kwd).send(__method__, *rest, **kwd, &bloc)
end
each_line!(fname, *rest, **kwd, &bloc) click to toggle source

Class method for {FileOverwrite#initialize}.{#each_line!}

@see initialize @see each_line!

# File lib/file_overwrite.rb, line 938
def self.each_line!(fname, *rest, **kwd, &bloc)
  new(fname, **kwd).send(__method__, *rest, **kwd, &bloc)
end
modify!(*rest, **kwd, &bloc) click to toggle source

Class method for {FileOverwrite#initialize}.{#modify!}

@see initialize @see modify

# File lib/file_overwrite.rb, line 880
def self.modify!(*rest, **kwd, &bloc)
  new(*rest, **kwd).modify!(**kwd, &bloc)
end
new(fname, backup: nil, suffix: true, noop: false, verbose: $VERBOSE, clobber: false, touch: false, last_match: false) click to toggle source

@param fname [String] Input filename @param backup: [String, NilClass] File name to which the original file is backed up. If non-Nil, suffix is ignored. @param suffix: [String, TrueClass, FalseClass, NilClass] Suffix of the backup file. True for Def, or false if no backup. @param noop: [Boolean] no-operationor dryrun @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 @param clobber: [Boolean] raise Exception if false(Def) and fname exists and suffix is non-null. @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}) @param last_match: [MatchData, NilClass, FalseClass] To pass Regexp.last_match in Caller's scope.

# File lib/file_overwrite.rb, line 126
def initialize(fname, backup: nil, suffix: true, noop: false, verbose: $VERBOSE, clobber: false, touch: false, last_match: false)
  @fname = fname
  @backup = backup
  @suffix = (backup ? true : suffix)
  @noop  = noop
  @verbose = $DEBUG || verbose
  @clobber = clobber
  @touch   = touch
  @last_match = last_match

  @ext_enc_old = nil
  @ext_enc_new = nil
  @int_enc     = nil

  @outstr = nil  # String to write.  This is nil if the temporary file was already created with modify().
  @outary = nil  # or Array to write
  @iotmp  = nil  # Temporary file IO to replace the original
  @is_edit_finished = false  # true if the file modification is finished.
  @is_completed = false      # true after all the process has been completed.
  @sizes = nil
end
read(*rest, **kwd, &bloc) click to toggle source

Class method for {FileOverwrite#initialize}.{#read}

@see initialize @see read

# File lib/file_overwrite.rb, line 914
def self.read(*rest, **kwd, &bloc)
  new(*rest, **kwd).send(__method__, **kwd, &bloc)
end
read!(*rest, **kwd, &bloc) click to toggle source

Class method for {FileOverwrite#initialize}.{#read!}

@see initialize @see read!

# File lib/file_overwrite.rb, line 922
def self.read!(*rest, **kwd, &bloc)
  new(*rest, **kwd).send(__method__, **kwd, &bloc)
end
readlines(fname, *rest, **kwd, &bloc) click to toggle source

Shorthand of {FileOverwrite#initialize}.{#readlines}, taking parameters for both

@param fname [String] Input and overwriting filename @param *rest [Array] (see {#initialize} and {#readlines}) @param **kwd [Hash] (see {#initialize}) @return [FileOverwrite] @yield refer to {#readlines} @see readlines

# File lib/file_overwrite.rb, line 894
def self.readlines(fname, *rest, **kwd, &bloc)
  new(fname, *rest, **kwd).send(__method__, *rest, **kwd, &bloc)
end
readlines!(*rest, **kwd, &bloc) click to toggle source

Shorthand of {FileOverwrite.readlines}.{#run!}

@param (see FileOverwrite.readlines and run) @return [FileOverwrite] @yield refer to {#readlines} @see readlines

# File lib/file_overwrite.rb, line 905
def self.readlines!(*rest, **kwd, &bloc)
  readlines(*rest, **kwd, &bloc).run!(**kwd)
end

Public Instance Methods

backup(suffix=nil, backupfile: nil) click to toggle source

Gets a path of the filename for backup

If suffix is given, the default suffix and backup filename are ignored, and the (backup) filename with the given suffix is returned (so you can tell what the backup filename would be if the suffix was set).

@param suffix [String, TrueClass, FalseClass, NilClass] Suffix of the backup file. True for Def, or false if no backup. @param backupfile: [String, NilClass] Explicilty specify the backup filename, when suffix is nil. (For internal use) @return [String, NilClass]

# File lib/file_overwrite.rb, line 161
def backup(suffix=nil, backupfile: nil)
  return backup_from_suffix(suffix)  if suffix   # non-nil suffix explicitly given
  return backupfile                  if backupfile
  return @backup                     if @backup
  return backup_from_suffix(@suffix) if @suffix
  nil
end
chainable?() click to toggle source

Returns true if the instance is chainable.

In other words, whether a further process like {#gsub} can be run. This returns nil if {#fresh?} is true.

@return [Boolean, NilClass]

# File lib/file_overwrite.rb, line 176
def chainable?
  return nil if fresh?
  return false if completed?
  return !@is_edit_finished   # ie., (@outary || @outstr) b/c one of the three must be non-false after the 2 clauses above.
end
completed?() click to toggle source

Returns true if the process has been completed.

# File lib/file_overwrite.rb, line 184
def completed?
  @is_completed
end
dump() click to toggle source

Returns the (current) content as String to supercede the input file

If the file has been already overwritten, this returns the content of the new one. Note it would be impossible to return the old one anyway, if no backup is left, as the user chooses.

Even if the returned string is destructively modified, it has no effect on the final output to the overwritten file.

@return [String]

# File lib/file_overwrite.rb, line 199
def dump
  return @outstr.dup   if @outstr
  return join_outary() if @outary
  return File.read(@iotmp.path) if @is_edit_finished && !completed?
  File.read(@fname)
end
each_line(*rest, **kwd) { |i| ... } click to toggle source

Takes a block in which each line of the file (or current content) is passed.

In the block each line as String is given as a block argument. Each iterator must return a String (or an object having to_s method), which replaces the input String to be output to the overwritten file later.

This method can be chained, as String-type processing.

@param *rest [Array] separator etc @param **kwd [Hash] ext_enc, int_enc @return [self] @yieldparam str [String] @yieldreturn [String] to be written back to the original file @raise [ArgumentError] if a block is not given

# File lib/file_overwrite.rb, line 573
def each_line(*rest, **kwd, &bloc)
  raise ArgumentError, 'Block must be given.' if !block_given?
  read(**kwd){ |outstr|
    outstr.each_line(*rest).map{|i| yield(i).to_s}.join('')
  }
end
each_line!(*rest, **kwd, &bloc) click to toggle source

Alias to self.{#sub}.{#run!}

@param *rest [Array<Regexp,String>] @param **kwd [Hash] setsize: etc @return [self] @yield the same as {String#sub!}

# File lib/file_overwrite.rb, line 586
def each_line!(*rest, **kwd, &bloc)
  send(__method__.to_s.chop, *rest, **kwd, &bloc).run!(**kwd)
end
empty?() click to toggle source

True if the (current) content to supercede the input file is empty.

@return [String]

# File lib/file_overwrite.rb, line 210
def empty?
  dump.empty?
end
encode(*rest, **kwd) click to toggle source

Implement String#encode

If it is in the middle of the process, the internal encoding for String (or Array) changes. Note if the current proces is in the IO-mode, everything has been already written in a temporary file, and hence there is no effect.

Once this is called, @int_enc is overwritten (or set), and it remains so even after reset() is called.

It is advisable to call {#force_encoding} or {#ext_enc_old=} before this is called to set the encoding of the input file.

@param *rest [Array] @param **kwd [Hash] @return [String] @see ruby-doc.org/core-2.5.1/String.html#method-i-encode

# File lib/file_overwrite.rb, line 232
def encode(*rest, **kwd)
  enc = (rest[0] || Encoding.default_internal)
  @int_enc = enc  # raises an Exception if called after "completed"
  return enc if @is_edit_finished || fresh?
  return @outstr.encode(*rest, **kwd) if @outstr
  if @outary
    @outary.map!{|i| i.encode(*rest, **kwd)}
    return enc
  end
  raise 'Should not happen.  Contact the code developer.'
end
end_with?(*rest) click to toggle source

True if the (current) content to supercede the input file end with the specified.

Wrapper of String#end_with?

@return [String]

# File lib/file_overwrite.rb, line 250
def end_with?(*rest)
  dump.end_with?(*rest)
end
force_encoding(enc) click to toggle source

Implement String#force_encoding

Once this is called, @ext_enc_old is overwritten (or set), and it remains so even after reset() is called.

@return [Encoding] @see ruby-doc.org/core-2.5.1/String.html#method-i-force_encoding

# File lib/file_overwrite.rb, line 262
def force_encoding(enc)
  @ext_enc_old = enc  # raises an Exception if called after "completed"
  return enc if @is_edit_finished || fresh?
  return @outstr.force_encoding(enc) if @outstr
  if @outary
    @outary.map!{|i| i.force_encoding(enc)}
    return enc
  end
  raise 'Should not happen.  Contact the code developer.'
end
fresh?() click to toggle source

Returns true if the process has not yet started.

# File lib/file_overwrite.rb, line 275
def fresh?
  !state
end
Also aliased as: reset?, reset?
gsub(*rest, max: 0, **kwd) { |last_match| ... } click to toggle source

Similar to String#gsub

This method can be chained. This method never returns an Enumerator.

Being different from the standard Srrint#gsub, this method accepts the optional parameter max, which specifies the maximum number of times of the matches and is valid ONLY WHEN a block is given.

@note Algorithm

See {#sub} for the basic algorithm.
This method emulates String#gsub as much as possible (duck-typing).
In String#gsub, the variable $~ after the method has the last matched characters
as the matched string and the original string before the last matched characters
as pre_match.  For example,
  'abc'.gsub(/./){$1.upcase}
returns
  'ABC'
and leaves
  $& == 'c'
  Regexp.pre_match == 'ab'
It is the same in this method.

@note Disclaimer

When a block is not given but arguments only (and not expecting Enumerator to return),
this method simply calls String#gsub .  However, when only 1 argument
and a block is given, this method must iterate on its own, which is implemented.
I am not 100% confident if this method works in the completely same way
as String#gsub in every single situation, given the regular expression
has so many possibilities; so far I have not found any cases where this method breaks.
This method is more inefficient and slower than the original String#gsub
as the iteration is implemented in pure Ruby.

@param *rest [Array<Regexp,String>] @param max: [Integer] the number of the maximum matches. 0 means no limit (as in String#gsub). Valid only if a block is given. @param **kwd [Hash] ext_enc, int_enc @return [self] @yield the same as String#gsub @see sub @see stackoverflow.com/questions/52359278/how-to-pass-regexp-last-match-to-a-block-in-ruby/52385870#52385870

# File lib/file_overwrite.rb, line 770
def gsub(*rest, max: 0, **kwd, &bloc)
  return sub(*rest, max: 1, **kwd, &bloc) if 1 == max  # Note: Error message would be labelled as 'sub'
  return self if sub_gsub_args_only(*rest, max: max, **kwd)

  if !block_given?
    raise ArgumentError, full_method_name+' does not support the format to return an enumerator.'
  end

  max = 5.0/0 if max.to_i <= 0

  regbase_str = rest[0].to_s
  regex = Regexp.new( sprintf('(%s)', regbase_str) ) # to guarantee the entire string is picked up by String#scan
  scans = @outstr.scan(regex)
  return self if scans.empty?  # no matches

  scans.map!{|i| [i].flatten}  # Originally, it can be a double array.
  prematch = ''
  ret = ''
  imatch = 0  # Number of matches
  scans.each do |ea_sc|
    str_matched = ea_sc[0]
    imatch += 1
    pre_size = prematch.size
    pos_end_p1 = @outstr.index(str_matched, pre_size) # End+1
    str_between = @outstr[pre_size...pos_end_p1]
    prematch << str_between
    ret      << str_between
    regex = Regexp.new( sprintf('(?<=\A%s)%s', Regexp.quote(prematch), regbase_str) )
    #regex = rest[0] if prematch.empty?  # The first run
    @last_match = regex.match(@outstr)
    prematch << str_matched

    # Sets $~ (Regexp.last_match) in the given block.
    # @see https://stackoverflow.com/questions/52359278/how-to-pass-regexp-last-match-to-a-block-in-ruby/52385870#52385870
    bloc.binding.tap do |b|
      b.local_variable_set(:_, $~)
      b.eval("$~=_")
    end

    # The first (and only) argument for the block is $& .
    # Returning nil, Integer etc is accepted in the block of sub/gsub
    ret << yield(@last_match[0]).to_s

    break if imatch >= max
  end
  ret << Regexp.last_match.post_match  # Guaranteed to be non-nil.

  @outstr = ret
  return self
end
gsub!(*rest, **kwd, &bloc) click to toggle source

Alias to self.{#gsub}.{#run!}

@return [self] @yield the same as {String#gsub!}

# File lib/file_overwrite.rb, line 826
def gsub!(*rest, **kwd, &bloc)
  gsub(*rest, &bloc).run!(**kwd)
end
modify(**kwd) { |ioin, iotmp| ... } click to toggle source

Modify the content in the block (though not committed, yet)

Two parameters are passed to the block: io_r and io_w. The former is the read-descriptor to read from the original file and the latter is the write-descriptor to write whatever to the temporary file, which is later moved back to the original file when you {#run!}.

Note the IO pointer for the input file is reset after this method. Hence, chaining this method makes no effect (warning is issued), but only the last one is taken into account.

@example

fo.modify do |io_r, io_w|
  io_w.print( "\n" + io_r.read + "\n" )
end

If you want to halt, undo and reset your modification process in the middle, issue

raise FileOverwriteError [Your_Message]

and it will be rescued. Your_Message is printed to STDERR if verbose was specified in {#initialize} or $DEBUG

@param **kwd [Hash] keyword parameters passed to File.open. Notably, ext_enc and int_enc . @return [self] @yieldparam ioin [IO] Read IO instance from the original file @yieldparam @iotmp [IO] Write IO instance to the temporary file @yieldreturn [Object] ignored @raise [ArgumentError] if a block is not given

# File lib/file_overwrite.rb, line 482
def modify(**kwd)
  raise ArgumentError, 'Block must be given.' if !block_given?
  normalize_status(:@is_edit_finished)

  kwd_open = {}
  kwd_open[:external_encoding] = @ext_enc_old if @ext_enc_old
  kwd_open[:internal_encoding] = @int_enc     if @int_enc
  kwd_open[:external_encoding] = (kwd[:ext_enc] || kwd_open[:external_encoding])
  kwd_open[:internal_encoding] = (kwd[:int_enc] || kwd_open[:internal_encoding])
  [:mode, :flags, :encoding, :textmode, :binmode, :autoclose].each do |es|
    # Method list from https://ruby-doc.org/core-2.5.1/IO.html#method-c-new
    kwd_open[es] = kwd[es] if kwd.key?(es)
  end

  begin
    File.open(@fname, **kwd_open) { |ioin|
      @iotmp = tempfile_io
      yield(ioin, @iotmp)
    }
  rescue FileOverwriteError => err
    warn err.message if @verbose
    reset
  end
  self
end
Also aliased as: open
modify!(**kwd, &bloc) click to toggle source

Alias to self.{#modify}.{#run!}

@return [self] @yieldparam ioin [IO] Read IO instance from the original file @yieldparam @iotmp [IO] Write IO instance to the temporary file @yieldreturn [Object] ignored @raise [ArgumentError] if a block is not given

# File lib/file_overwrite.rb, line 517
def modify!(**kwd, &bloc)
  modify(&bloc).run!(**kwd)
end
Also aliased as: open!
open(**kwd)
Alias for: modify
open!(**kwd, &bloc)
Alias for: modify!
path() click to toggle source

Returns the (duplicate of the) filename to be (or to have been) updated.

To destructively modify this value would affect nothing in the parent object.

@return [String]

# File lib/file_overwrite.rb, line 286
def path
  @fname.dup
end
read(**kwd) { |outstr| ... } click to toggle source

Handler to process the entire string of the file (or current content)

If block is not given, just sets the processing-state as String.

Else, IO.read(infile) is given to the block. No other options, such as length, as in IO.read are accepted. Then, the returned value is held as a String, while self is returned; hence this method can be chained. If the block returns nil (or Boolean), {FileOverwriteError} is raised. Make sure for the block to return a String.

Note this method does not take arguments as in IO.read .

@param **kwd [Hash] ext_enc, int_enc @return [self] @yieldparam str [String] @yieldreturn [String] to be written back to the original file @raise [FileOverwriteError] if a block is given and nil or Boolean is returned.

# File lib/file_overwrite.rb, line 624
def read(**kwd, &bloc)
  if :first == normalize_status(:@outstr)
    adjust_input_encoding(**kwd){ |f|  # @fname
      @outstr = File.read f
    }
  end
    
  @outstr = yield(@outstr) if block_given?
  raise FileOverwriteError, 'ERROR: The returned value from the block in read() has to be String.' if !defined?(@outstr.gsub)
  warn "WARNING: Empty string returned from a block in #{__method__}" if !@verbose.nil? && @outstr.empty?
  self
end
read!(**kwd, &bloc) click to toggle source

Alias to self.{#read}.{#run!}

@param **kwd [Hash] ext_enc, int_enc @return [self] @yield Should return string

# File lib/file_overwrite.rb, line 643
def read!(**kwd, &bloc)
  read(**kwd, &bloc).run!(**kwd)
end
readlines(*rest, **kwd) { |outary| ... } click to toggle source

Takes a block in which the entire String of the file is passed.

IO.readlines(infile) is given to the block, where Encode may be taken into account if specified already.

The block must return an Array, the number of the elements of which can be allowed to differ from the input. The elements of the Array will be joined to output to the overwritten file in the end.

@param *rest [Array] separator etc @param **kwd [Hash] ext_enc, int_enc @return [self] @yieldparam str [String] @yieldreturn [String] to be written back to the original file

# File lib/file_overwrite.rb, line 541
def readlines(*rest, **kwd, &bloc)
  raise ArgumentError, 'Block must be given.' if !block_given?

  if :first == normalize_status(:@outary)
    adjust_input_encoding(**kwd){ |f|  # @fname
      @outary = IO.readlines(f, *rest)
    }
  end

  @outary = yield(@outary)
  self
end
ready?() click to toggle source

Returns true if the instance is ready to run (to execute overwriting the file).

# File lib/file_overwrite.rb, line 292
def ready?
  !fresh? && !completed?
end
replace_with(str) click to toggle source

Replaces the file content with the given argument like {String#replace}

This method can be chained.

@param str [String] the content will be replaced with this @return [self]

# File lib/file_overwrite.rb, line 654
def replace_with(str)
  read
  @outstr = str.to_s
  self
end
replace_with!(str, **kwd) click to toggle source

Alias to self.{#replace_with}.{#run!}

@return [self] @yield the same as {String#gsub!}

# File lib/file_overwrite.rb, line 665
def replace_with!(str, **kwd)
  replace_with(str).run!(**kwd)
end
reset() click to toggle source

Reset all the modification which is to be applied

@return [NilClass]

# File lib/file_overwrite.rb, line 301
def reset
  @outstr = nil
  @outary = nil
  @is_edit_finished = nil
  close_iotmp  # @iotmp=nil; immediate deletion of the temporary file
  warn "The modification process is reset." if $DEBUG
  nil
end
reset?()
Alias for: fresh?
run(backup: @backup, suffix: @suffix, noop: @noop, verbose: @verbose, clobber: @clobber, touch: @touch, setsize: true, **kwd)
Alias for: run!
run!(backup: @backup, suffix: @suffix, noop: @noop, verbose: @verbose, clobber: @clobber, touch: @touch, setsize: true, **kwd) click to toggle source

Actually performs the file modification

If setsize option is true (Default) or verbose, method {#sizes} is activated after this method, which returns a hash of file sizes in bytes before and after, so you can chain it. Note this method returns nil if the input file is not opened at all.

@example With setsize option

fo.run!(setsize: true).sizes
  # => { :old => 40, :new => 50 }

@example One case where this returns nil

fo.new('test.f').run!  # => nil

The folloing optional parameters are taken into account. Any other options are ignored.

@param backup: [String, NilClass] File name to which the original file is backed up. If non-Nil, suffix is ignored. @param suffix: [String, TrueClass, FalseClass, NilClass] Suffix of the backup file. True for Def, or false if no backup. @param noop: [Boolean] @param verbose: [Boolean] @param clobber: [Boolean] raise Exception if false(Def) and fname exists and suffix is non-null. @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. @param setsize: [Boolean] @return [NilClass, self] If the input file is not touched, nil is returned, else self. @raise [FileOverwriteError] if the process has been already completed.

# File lib/file_overwrite.rb, line 405
def run!(backup: @backup, suffix: @suffix, noop: @noop, verbose: @verbose, clobber: @clobber, touch: @touch, setsize: true, **kwd)
  raise FileOverwriteError, 'The process has been already completed.' if completed?

  bkupname = get_bkupname(backup, suffix, noop, verbose, clobber)
  sizes = write_new(verbose, setsize)
  return nil if !sizes

  return self if run_identical?(noop, verbose, touch)

  if bkupname
    msg4bkup = ", Backup: " + bkupname if verbose
  else
    io2del = tempfile_io
    io2delname = io2del.path
  end

  fname_to = (bkupname || io2delname)
  mv(  @fname,    fname_to, noop: noop, verbose: $DEBUG) # defined in FileUtils
  begin
    mv(@iotmp.path, @fname, noop: noop, verbose: $DEBUG) # defined in FileUtils
  rescue
    msg = sprintf("Process halted! File system error in renaming the temporary file %s back to the original %s", @iotmp.path, @fname) 
    warn msg
    raise
  end

  # @iotmp.close(true)  # to immediate delete the temporary file
                        # If commented out, GC looks after it.

  File.unlink io2delname if io2delname && !noop
  # if noop, GC will delete it.

  if verbose
    msg = sprintf("%sFile %s updated (Size: %d => %d bytes%s)\n", prefix(noop), @fname, sizes[:old], sizes[:new], msg4bkup)
    fu_output_message msg
  end

  @is_completed = true
  self.freeze

  return self
end
Also aliased as: run, save, save!
save(backup: @backup, suffix: @suffix, noop: @noop, verbose: @verbose, clobber: @clobber, touch: @touch, setsize: true, **kwd)
Alias for: run!
save!(backup: @backup, suffix: @suffix, noop: @noop, verbose: @verbose, clobber: @clobber, touch: @touch, setsize: true, **kwd)
Alias for: run!
state() click to toggle source

Returns the current state

nil if no modification has been attempted. IO if the modification has been made and it is wating to run. String or Array (or their equivalent), depending how it has been chained so far. true if the process has been completed.

@return [Class, TrueClass, NilClass]

# File lib/file_overwrite.rb, line 329
def state
  return true     if completed?
  return IO            if @is_edit_finished
  return @outstr.class if @outstr
  return @outary.class if @outary
  nil
end
sub(*rest, max: 1, **kwd) { |last_match| ... } click to toggle source

Similar to String#sub

This method can be chained. This method never returns an Enumerator.

@note Algorithm

To realise the local-scope variables like $~, $1, and Regexp.last_match to
be usable inside the block as in String#sub, it overwrites them when a block
is given (See the linked article for the phylosophy of how to do it).
Once a block is read, those variables remain as updated values even after the block
in the caller's scope, in the same way as String#sub.  However, when a block is not given,
those variables are *NOT* updated, which is different from String#sub.
You can retrieve the MatchData by this method via {#last_match} after {#sub}
is called, if need be.

@param *rest [Array<Regexp,String>] @param max: [Integer] the number of the maximum matches. If it is not 1, {#gsub} is called, instead. See {#gsub} for detail. @param **kwd [Hash] ext_enc, int_enc @return [self] @yield the same as String#sub @see stackoverflow.com/questions/52359278/how-to-pass-regexp-last-match-to-a-block-in-ruby/52385870#52385870

# File lib/file_overwrite.rb, line 691
def sub(*rest, max: 1, **kwd, &bloc)
  return self if sub_gsub_args_only(*rest, max: max, **kwd)

  if !block_given?
    raise ArgumentError, full_method_name+' does not support the format to return an enumerator.'
  end

  if max.to_i != 1
    return gsub(*rest, max: max, **kwd, &bloc)
  end

  @last_match = rest[0].match(@outstr)
  return self if !@last_match

  # Sets $~ (Regexp.last_match) in the given block.
  # @see https://stackoverflow.com/questions/52359278/how-to-pass-regexp-last-match-to-a-block-in-ruby/52385870#52385870
  bloc.binding.tap do |b|
    b.local_variable_set(:_, $~)
    b.eval("$~=_")
  end

  # The first (and only) argument for the block is $& .
  # Returning nil, Integer etc is accepted in the block of sub/gsub
  @outstr = @last_match.pre_match + yield(@last_match[0]).to_s + @last_match.post_match
  return self
end
sub!(*rest, **kwd, &bloc) click to toggle source

Alias to self.{#sub}.{#run!}

@param *rest [Array<Regexp,String>] @param **kwd [Hash] setsize: etc @return [self] @yield the same as {String#sub!}

# File lib/file_overwrite.rb, line 725
def sub!(*rest, **kwd, &bloc)
  sub(*rest, &bloc).run!(**kwd)
end
temporary_filename() click to toggle source

Returns the temporary filename (or nil), maybe for debugging

It may not be open?

@return [String, NilClass] Filename if exists, else nil

# File lib/file_overwrite.rb, line 316
def temporary_filename
  @iotmp ? @iotmp.path : nil
end
tr(*rest, **kwd) click to toggle source

Similar to {String#tr}

This method can be chained.

@param *rest [Array] replacers etc @param **kwd [Hash] ext_enc, int_enc @return [self]

# File lib/file_overwrite.rb, line 838
def tr(*rest, **kwd)
  read(**kwd){ |outstr|
    outstr.tr!(*rest) || outstr
  }
end
tr!(*rest, **kwd) click to toggle source

Alias to self.{#tr}.{#run!}

@return [self]

# File lib/file_overwrite.rb, line 847
def tr!(*rest, **kwd)
  tr(*rest, **kwd).run!(**kwd)
end
tr_s(*rest, **kwd) click to toggle source

Similar to {String#tr_s}

This method can be chained.

@param *rest [Array] replacers etc @param **kwd [Hash] ext_enc, int_enc @return [self]

# File lib/file_overwrite.rb, line 858
def tr_s(*rest, **kwd)
  read(**kwd){ |outstr|
    outstr.tr_s!(*rest) || outstr
  }
end
tr_s!(*rest, **kwd) click to toggle source

Alias to self.{#tr}.{#run!}

@return [self]

# File lib/file_overwrite.rb, line 867
def tr_s!(*rest, **kwd)
  tr_s(*rest, **kwd).run!(**kwd)
end
valid_encoding?() click to toggle source

String#valid_encoding?()

@note returns nil if the process has been already completed.

@return [Boolean, NilClass]

# File lib/file_overwrite.rb, line 342
def valid_encoding?()
  return nil if completed?
  dump.valid_encoding?
end

Private Instance Methods

adjust_input_encoding(**kwd) { |fname| ... } click to toggle source

Core routine to adjust the encoding of the input String (or Array)

@return [Array, String] @yieldparam fname [String] @yieldreturn [Array, String] @outstr or @outary

# File lib/file_overwrite.rb, line 952
def adjust_input_encoding(**kwd, &bloc)
  raise ArgumentError, 'Block must be given.' if !block_given?
  obj = yield(@fname)

  kwd_enc = {}
  kwd_enc[:ext_enc] = @ext_enc_old  if @ext_enc_old
  kwd_enc[:ext_enc] = kwd[:ext_enc] if kwd[:ext_enc]
  kwd_enc[:int_enc] = @int_enc      if @int_enc
  kwd_enc[:int_enc] = kwd[:int_enc] if kwd[:int_enc]
  if kwd_enc[:ext_enc]
    force_encoding kwd_enc[:ext_enc]
  end
  if kwd_enc[:int_enc]
    if defined? obj.map!
      obj.map!{|i| i.encode(kwd_enc[:int_enc])}
    elsif defined? obj.encode
      obj.encode kwd_enc[:int_enc]
    else
      raise 'Should not happen. Contact the code developper.'
    end
  end
end
backup_from_suffix(suffix) click to toggle source

Returns a path of the filename constructed with the supplied suffix

@param suffix [String, TrueClass] Suffix of the backup file. True for Def, or false if no backup. @return [String, NilClass]

# File lib/file_overwrite.rb, line 981
def backup_from_suffix(suffix)
  raise 'Should not happen. Contact the code developper.' if !suffix

  @fname + ((suffix == true) ? Time.now.strftime(".%Y%m%d%H%M%S.bak") : suffix)
end
close_iotmp() click to toggle source

Deletes the temporary file if exists

@return [String, NilClass] Filename if deleted, else nil

# File lib/file_overwrite.rb, line 992
def close_iotmp
  return if !@iotmp
  fn = @iotmp.path
  @iotmp.close(true) if @iotmp # immediate deletion of the temporary file
  @iotmp = nil
  fn
end
full_method_name(nested_level=0) click to toggle source

Returns a String “FileOverwrite#MY_METHOD_NAME”

@param nested_level [Integer] 0 (Def) if the caller wants the name of itself. @return [String]

# File lib/file_overwrite.rb, line 1005
def full_method_name(nested_level=0)
  # Note: caller_locations() is equivalent to caller_locations(1).
  #       caller_locations(0) from this method would also contain the information of
  #       this method full_method_name() itself, which is totally irrelevant.
  sprintf("%s#%s", self.class.to_s, caller_locations()[nested_level].label)
end
get_bkupname(backup_l, suffix, noop, verbose, clobber) click to toggle source

Gets a path of the filename for backup and checks out clobber

@param backup_l [String, NilClass] File name to which the original file is backed up. If non-Nil, suffix is ignored. @param suffix [String, TrueClass, FalseClass, NilClass] Suffix of the backup file. True for Def, or false if no backup. @param noop [Boolean] @param verbose [Boolean] @param clobber [Boolean] raise Exception if false(Def) and fname exists and suffix is non-null. @return [String, NilClass]

# File lib/file_overwrite.rb, line 1022
def get_bkupname(backup_l, suffix, noop, verbose, clobber)
  bkupname = backup(suffix, backupfile: backup_l)
  return nil if !bkupname

  if File.exist?(bkupname)
    raise "File(#{@fname}) exists." if !clobber
    fu_output_message sprintf("%Backup File %s is overwritten.", prefix(noop), bkupname) if verbose
  end

  bkupname
end
join_outary(ary=@outary) click to toggle source

Returns joined string of @outary as it is to output

@return [String]

# File lib/file_overwrite.rb, line 1039
def join_outary(ary=@outary)
  ary.join ''
end
normalize_status(inst_var) click to toggle source

Changes the status of a set of instance variables

Returns :first if this is the first process, else :continuation (ie, chained) For IO-style, this returns always :first

@param inst_var [Symbol, String] '@is_edit_finished', :@outary (do not forget '@') @return [Symbol] :first or :continuation

# File lib/file_overwrite.rb, line 1052
def normalize_status(inst_var)
  errmsg = "WARNING: The file (#{@fname}) is reread from the beginning."

  case inst_var
  when :@is_edit_finished, '@is_edit_finished'
    warn errmsg if @outstr || @outary
    reset
    @is_edit_finished = true
    return :first

  when :@outary, '@outary'
    warn errmsg if @outstr || @is_edit_finished
    @is_edit_finished = false
    close_iotmp  # @iotmp=nil; immediate deletion of the temporary file
    @outstr = nil
    return :continuation if @outary 
    @outary ||= []
    return :first

  when :@outstr, '@outstr'
    # For String-type processing, it is allowed if the previous processing
    # is not String-type but Array-type.
    warn errmsg if @is_edit_finished || (@outstr && @outary)
    @is_edit_finished = false
    close_iotmp  # @iotmp=nil; immediate deletion of the temporary file
    if @outary
      @outstr = join_outary()
      @outary = nil
      return :continuation
    else
      return :continuation if @outstr
      @outstr ||= ''
      return :first
    end
  else
    raise
  end
end
prefix(noop=@noop) click to toggle source

Returns the prefix for message for noop option

@return [self] @yield Should return String or Array (which will be simply joined)

# File lib/file_overwrite.rb, line 1097
def prefix(noop=@noop)
  (noop ? '[Dryrun]' : '')
end
run_identical?(noop, verbose, touch) click to toggle source

If identical, just touch (if specified) and returns true

@param noop [Boolean] @param verbose [Boolean] @param touch [Boolean] if true (non-Def), when the file content does not change, the timestamp is updated @return [Boolean]

# File lib/file_overwrite.rb, line 358
def run_identical?(noop, verbose, touch)
  if !identical?(@iotmp.path, @fname) # defined in FileUtils
    return false
  end

  @iotmp.close(true)  # immediate deletion of the temporary file

  msg = sprintf("%sNo change in (%s).", prefix(noop), @fname)
  if touch
    touch(@fname, noop: noop)          # defined in FileUtils
    msg.chop!  # chop a full stop.
    msg << " but timestamp is updated to " << File.mtime(@fname).to_s << '.'
  end
  fu_output_message msg if verbose

  @is_completed = true
  self.freeze
  true
end
sub_gsub_args_only(*rest, max: 1, **kwd) click to toggle source

Common routine to process String#sub and String#gsub

handling the case where no block is given.

@param *rest [Array<Regexp,String>] @param max: [Integer] the number of the maximum matches. 0 means no limit (as in String#gsub) @param **kwd [Hash] ext_enc, int_enc @return [String, NilClass] nil if not processed because a block is supplied (or error). @yield the same as String#sub

# File lib/file_overwrite.rb, line 1112
def sub_gsub_args_only(*rest, max: 1, **kwd)
  read(**kwd) 
  return if 1 == rest.size

  method = caller_locations()[0].label
  if !@verbose.nil? && ((max != 1 && 'sub' == method) ||  (max != 0 && 'gsub' == method))
    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
    warn msg
  end

  # Note: When 2 arguments are given, the block is simply ignored in default (in Ruby 2.5).
  @outstr.send(method+'!', *rest) # sub! or gsub! => String|nil
  @last_match = Regexp.last_match # $~
  @outstr
end
tempfile_io(**kwd) click to toggle source

Gets an IO of a temporary file (in the same directory as the source file)

@return [IO]

# File lib/file_overwrite.rb, line 1133
def tempfile_io(**kwd)
  kwd_def = {}
  kwd_def[:ext_enc] = @ext_enc_new if @ext_enc_new
  kwd_def[:int_enc] = @int_enc     if @int_enc
  kwd = kwd_def.merge kwd

  iot = Tempfile.open(File.basename(@fname) + '.' + self.class.to_s, File.dirname(@fname), **kwd)
  iot.sync=true    # Essential!
  iot
end
warn_for_sub_gsub(err) click to toggle source

Issues an warning for {#sub}/{#gsub} and {#sub!}/{#gsub!}

@return [NilClass]

# File lib/file_overwrite.rb, line 1149
def warn_for_sub_gsub(err)
  return if !err.message.include?('for nil:NilClass')  # and raise-d
  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.'
  nil
end
write_new(verbose, setsize=true) click to toggle source

Write a temporary new file

The Tempfile IO for the new file is set to be @iotmp (so @iotmp.path gives the filename).

Returns either nil (if no further process is needed) or Hash. The Hash would be empty if not verbose or !setsize. Else it would contains the filesizes for :old and :new files.

@param verbose [Boolean, NilClass] @param setsize [Boolean] If true, @sizes is set. @return [Hash, NilClass]

# File lib/file_overwrite.rb, line 1168
def write_new(verbose, setsize=true)
  if @outstr || @outary
    @iotmp.close(true) if @iotmp  # should be redundant, but to play safe
    @iotmp = tempfile_io
    @iotmp.print (@outstr || join_outary())
    @outstr = nil
    @outary = nil
  elsif !@is_edit_finished
    warn "Input file (#{@fname}) is not opened, and hence is not modified." if !verbose.nil?
    return
  end

  return Hash.new if !verbose && !setsize

  @sizes = {
    :old => File.size(@fname),
    :new => File.size(@iotmp.path),
  }
  if @sizes[:new] == 0
    warn "The revised file (#{@fname}) is empty." if !verbose.nil?
  end
  @sizes
end