Class: Fned::EditList

Inherits:
Object
  • Object
show all
Defined in:
lib/fned/edit_list.rb

Defined Under Namespace

Classes: InvalidLine, UserAbort

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ EditList

Returns a new instance of EditList.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 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

Instance Method Details

#bin_dup(str) ⇒ Object

return dup of string with binary encoding



174
175
176
177
178
# 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) ⇒ Object

start editor to edit items, returns new list of items



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# 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

#editorObject

editor to run from environment or



55
56
57
58
# 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) ⇒ Object

escape string using @escape



67
68
69
# File 'lib/fned/edit_list.rb', line 67

def escape(str)
  replace(@escape, str)
end

#number_decode(str) ⇒ Object

decode number using @digits



90
91
92
93
94
95
96
97
98
# 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) ⇒ Object

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

Raises:

  • (ArgumentError)


77
78
79
80
81
82
83
84
85
86
87
# 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) ⇒ Object

read from io and parse content



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# 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) ⇒ Object

replace according to a hash



61
62
63
64
# 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?Boolean

ask user for retry

Returns:

  • (Boolean)


160
161
162
163
164
165
166
167
168
169
170
171
# 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) ⇒ Object

unescape string using @escape



72
73
74
# File 'lib/fned/edit_list.rb', line 72

def unescape(str)
  replace(@escape.invert, str)
end

#write_comment(io, comments) ⇒ Object

write comment to io



101
102
103
104
105
# 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) ⇒ Object

write items and comments to io



114
115
116
117
118
119
120
121
# 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) ⇒ Object

write number and item to io



108
109
110
111
# 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