class Fned::EditList

Public Class Methods

new(options = {}) click to toggle source
# File lib/fned/edit_list.rb, line 37
def initialize(options = {})
  @options = {
    :separator => ' ',
  }.merge(options)

  # Do not use characters in lower and upper case, the line numbers
  # are case insensitive.
  @digits = ('0'..'9').to_a + ('A'..'Z').to_a
  @digits_upcase = @digits.map { |s| s.upcase }

  @escape = {
    "\r" => "\\r",
    "\n" => "\\n",
    "\\" => "\\\\",
  }
end

Public Instance Methods

bin_dup(str) click to toggle source

return dup of string with binary encoding

# File lib/fned/edit_list.rb, line 174
def bin_dup(str)
  str = str.to_s.dup
  str.force_encoding "binary"
  str
end
edit(items, comments) click to toggle source

start editor to edit items, returns new list of items

# File lib/fned/edit_list.rb, line 181
def edit(items, comments)
  # Ensure all strings are in binary encoding as filenames may have
  # invalid encodings.
  items = items.map { |item| bin_dup(item) }
  comments = comments.map { |comment| bin_dup(comment) if comment }

  Tempfile.open(File.basename($0), :encoding => "binary") do |fh|
    write_file(fh, items, comments)
    fh.close
    begin
      # TODO: return code of editor meaningful?
      system(editor, fh.path)
      File.open(fh.path, "r", :encoding => "binary") do |io|
        return read_file(io, items.length)
      end
    rescue InvalidLine => e
      warn e.message
      if retry?
        retry
      else
        raise UserAbort
      end
    end
  end
end
editor() click to toggle source

editor to run from environment or

# File lib/fned/edit_list.rb, line 55
def editor
  # TODO: check for existence of editor, vim, emacs?
  ENV["VISUAL"] || ENV["EDITOR"] || "vi"
end
escape(str) click to toggle source

escape string using @escape

# File lib/fned/edit_list.rb, line 67
def escape(str)
  replace(@escape, str)
end
number_decode(str) click to toggle source

decode number using @digits

# File lib/fned/edit_list.rb, line 90
def number_decode(str)
  str.upcase.chars.map do |char|
    n = @digits_upcase.index(char)
    return nil unless n
    n
  end.inject(0) do |m, e|
    m * @digits.length + e
  end
end
number_encode(n, padding = 1) click to toggle source

encode number using @digits, padding is minimum number of digits

# File lib/fned/edit_list.rb, line 77
def number_encode(n, padding = 1)
  result = []
  raise ArgumentError if n < 0
  raise ArgumentError if padding < 1
  until n == 0
    n, k = n.divmod(@digits.length)
    result << @digits[k]
  end
  result.fill(@digits[0], result.length, padding - result.length)
  result.reverse.join
end
read_file(io, count) click to toggle source

read from io and parse content

# File lib/fned/edit_list.rb, line 124
def read_file(io, count)
  @result = Array.new(count)
  line_number = 0
  io.each do |line|
    line_number += 1
    line = line.chomp
    if line =~ /\A\s*(?:#|\z)/
      next
    end

    key, value = line.split(@options[:separator], 2)
    index = number_decode(key)
    value = unescape(value.to_s)

    if index.nil?
      raise InvalidLine.new("index #{key.inspect} contains invalid " +
                            "characters", line_number)
    end
    if index >= count
      raise InvalidLine.new("index #{key.inspect} too large", line_number)
    end
    if @result[index]
      raise InvalidLine.new("index #{key.inspect} used multiple times",
                            line_number)
    end
    if value.empty?
      raise InvalidLine.new("value for #{key.inspect} empty",
                            line_number)
    end

    @result[index] = value
  end
  @result
end
replace(replacements, str) click to toggle source

replace according to a hash

# File lib/fned/edit_list.rb, line 61
def replace(replacements, str)
  r = Regexp.new(replacements.keys.map { |s| Regexp.quote(s) }.join("|"))
  str.gsub(r) { |s| replacements[s] }
end
retry?() click to toggle source

ask user for retry

# File lib/fned/edit_list.rb, line 160
def retry?
  loop do
    $stderr.print "Edit / Abort? [Ea] "
    $stderr.flush
    case $stdin.readline.strip
    when "", /\Ae/i
      return true
    when /\Aa/i
      return false
    end
  end
end
unescape(str) click to toggle source

unescape string using @escape

# File lib/fned/edit_list.rb, line 72
def unescape(str)
  replace(@escape.invert, str)
end
write_comment(io, comments) click to toggle source

write comment to io

# File lib/fned/edit_list.rb, line 101
def write_comment(io, comments)
  comments.lines.map(&:chomp).each do |s|
    io.puts "# #{escape(s.to_s)}"
  end
end
write_file(io, items, comments) click to toggle source

write items and comments to io

# File lib/fned/edit_list.rb, line 114
def write_file(io, items, comments)
  padding = number_encode([items.length - 1, 0].max).length
  items.each_with_index do |item, index|
    write_comment(io, comments[index]) if comments[index]
    write_item(io, index, padding, item)
  end
  write_comment(io, comments[items.length]) if comments[items.length]
end
write_item(io, index, padding, str) click to toggle source

write number and item to io

# File lib/fned/edit_list.rb, line 108
def write_item(io, index, padding, str)
  io.puts number_encode(index, padding) + @options[:separator] +
    escape(str.to_s)
end