sup 0.19.0 → 0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -1
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +12 -6
  5. data/CONTRIBUTORS +28 -14
  6. data/Gemfile +5 -0
  7. data/History.txt +92 -0
  8. data/README.md +26 -5
  9. data/Rakefile +41 -1
  10. data/ReleaseNotes +17 -0
  11. data/bin/sup +12 -23
  12. data/bin/sup-add +15 -16
  13. data/bin/sup-config +30 -45
  14. data/bin/sup-dump +2 -3
  15. data/bin/sup-import-dump +5 -6
  16. data/bin/sup-sync +3 -4
  17. data/bin/sup-sync-back-maildir +3 -4
  18. data/bin/sup-tweak-labels +6 -7
  19. data/contrib/colorpicker.rb +0 -2
  20. data/contrib/completion/_sup.bash +102 -0
  21. data/devel/profile.rb +0 -1
  22. data/ext/mkrf_conf_xapian.rb +47 -0
  23. data/lib/sup.rb +10 -8
  24. data/lib/sup/buffer.rb +12 -0
  25. data/lib/sup/colormap.rb +5 -2
  26. data/lib/sup/contact.rb +4 -2
  27. data/lib/sup/crypto.rb +58 -16
  28. data/lib/sup/draft.rb +8 -8
  29. data/lib/sup/hook.rb +9 -9
  30. data/lib/sup/index.rb +20 -7
  31. data/lib/sup/label.rb +1 -1
  32. data/lib/sup/logger.rb +1 -1
  33. data/lib/sup/maildir.rb +16 -5
  34. data/lib/sup/mbox.rb +13 -5
  35. data/lib/sup/message.rb +36 -12
  36. data/lib/sup/message_chunks.rb +13 -4
  37. data/lib/sup/mode.rb +34 -28
  38. data/lib/sup/modes/contact_list_mode.rb +1 -0
  39. data/lib/sup/modes/edit_message_mode.rb +3 -2
  40. data/lib/sup/modes/forward_mode.rb +22 -3
  41. data/lib/sup/modes/line_cursor_mode.rb +1 -1
  42. data/lib/sup/modes/reply_mode.rb +3 -1
  43. data/lib/sup/modes/text_mode.rb +6 -1
  44. data/lib/sup/modes/thread_index_mode.rb +12 -2
  45. data/lib/sup/modes/thread_view_mode.rb +111 -14
  46. data/lib/sup/person.rb +68 -61
  47. data/lib/sup/search.rb +1 -1
  48. data/lib/sup/sent.rb +1 -1
  49. data/lib/sup/source.rb +1 -1
  50. data/lib/sup/util.rb +15 -94
  51. data/lib/sup/util/axe.rb +17 -0
  52. data/lib/sup/util/locale_fiddler.rb +24 -0
  53. data/lib/sup/util/ncurses.rb +3 -3
  54. data/lib/sup/version.rb +10 -1
  55. data/sup.gemspec +29 -11
  56. data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
  57. data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
  58. data/test/fixtures/blank-header-fields.eml +71 -0
  59. data/test/fixtures/contacts.txt +1 -0
  60. data/test/fixtures/mailing-list-header.eml +80 -0
  61. data/test/fixtures/malicious-attachment-names.eml +55 -0
  62. data/test/fixtures/missing-from-to.eml +18 -0
  63. data/test/{messages → fixtures}/missing-line.eml +0 -0
  64. data/test/fixtures/multi-part-2.eml +72 -0
  65. data/test/fixtures/multi-part.eml +61 -0
  66. data/test/fixtures/no-body.eml +18 -0
  67. data/test/fixtures/simple-message.eml +29 -0
  68. data/test/fixtures/text-attachments-with-charset.eml +46 -0
  69. data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
  70. data/test/gnupg_test_home/gpg.conf +3 -1
  71. data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
  72. data/test/gnupg_test_home/pubring.gpg +0 -0
  73. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  74. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  75. data/test/gnupg_test_home/regen_keys.sh +89 -0
  76. data/test/gnupg_test_home/secring.gpg +0 -0
  77. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -17
  78. data/test/integration/test_maildir.rb +75 -0
  79. data/test/integration/test_mbox.rb +69 -0
  80. data/test/test_crypto.rb +14 -2
  81. data/test/test_header_parsing.rb +1 -1
  82. data/test/test_helper.rb +6 -3
  83. data/test/test_message.rb +115 -341
  84. data/test/test_messages_dir.rb +4 -28
  85. data/test/test_yaml_regressions.rb +1 -1
  86. data/test/unit/test_contact.rb +33 -0
  87. data/test/unit/test_locale_fiddler.rb +15 -0
  88. data/test/unit/test_person.rb +37 -0
  89. data/test/unit/util/test_query.rb +10 -4
  90. data/test/unit/util/test_string.rb +6 -0
  91. metadata +137 -53
  92. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  93. data/test/gnupg_test_home/trustdb.gpg +0 -0
@@ -105,7 +105,7 @@ EOS
105
105
 
106
106
  def save
107
107
  debug "saving index and sources..."
108
- FileUtils.mkdir_p @dir unless File.exists? @dir
108
+ FileUtils.mkdir_p @dir unless File.exist? @dir
109
109
  SourceManager.save_sources
110
110
  save_index
111
111
  end
@@ -116,7 +116,7 @@ EOS
116
116
 
117
117
  def load_index failsafe=false
118
118
  path = File.join(@dir, 'xapian')
119
- if File.exists? path
119
+ if File.exist? path
120
120
  @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
121
121
  db_version = @xapian.get_metadata 'version'
122
122
  db_version = '0' if db_version.empty?
@@ -516,19 +516,32 @@ EOS
516
516
  qp.stemmer = Xapian::Stem.new($config[:stem_language])
517
517
  qp.stemming_strategy = Xapian::QueryParser::STEM_SOME
518
518
  qp.default_op = Xapian::Query::OP_AND
519
- qp.add_valuerangeprocessor(Xapian::NumberValueRangeProcessor.new(DATE_VALUENO, 'date:', true))
520
- NORMAL_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_prefix k, v } }
521
- BOOLEAN_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_boolean_prefix k, v, info[:exclusive] } }
519
+ valuerangeprocessor = Xapian::NumberValueRangeProcessor.new(DATE_VALUENO,
520
+ 'date:', true)
521
+ qp.add_valuerangeprocessor(valuerangeprocessor)
522
+ NORMAL_PREFIX.each { |k,info| info[:prefix].each {
523
+ |v| qp.add_prefix k, v }
524
+ }
525
+ BOOLEAN_PREFIX.each { |k,info| info[:prefix].each {
526
+ |v| qp.add_boolean_prefix k, v, info[:exclusive] }
527
+ }
522
528
 
523
529
  begin
524
- xapian_query = qp.parse_query(subs, Xapian::QueryParser::FLAG_PHRASE|Xapian::QueryParser::FLAG_BOOLEAN|Xapian::QueryParser::FLAG_LOVEHATE|Xapian::QueryParser::FLAG_WILDCARD)
530
+ xapian_query = qp.parse_query(subs, Xapian::QueryParser::FLAG_PHRASE |
531
+ Xapian::QueryParser::FLAG_BOOLEAN |
532
+ Xapian::QueryParser::FLAG_LOVEHATE |
533
+ Xapian::QueryParser::FLAG_WILDCARD)
525
534
  rescue RuntimeError => e
526
535
  raise ParseError, "xapian query parser error: #{e}"
527
536
  end
528
537
 
529
538
  debug "parsed xapian query: #{Util::Query.describe(xapian_query, subs)}"
530
539
 
531
- raise ParseError if xapian_query.nil? or xapian_query.empty?
540
+ if xapian_query.nil? or xapian_query.empty?
541
+ raise ParseError, "couldn't parse \"#{s}\" as xapian query " \
542
+ "(special characters aren't indexed)"
543
+ end
544
+
532
545
  query[:qobj] = xapian_query
533
546
  query[:text] = s
534
547
  query
@@ -15,7 +15,7 @@ class LabelManager
15
15
  def initialize fn
16
16
  @fn = fn
17
17
  labels =
18
- if File.exists? fn
18
+ if File.exist? fn
19
19
  IO.readlines(fn).map { |x| x.chomp.intern }
20
20
  else
21
21
  []
@@ -71,7 +71,7 @@ end
71
71
 
72
72
  ## include me to have top-level #debug, #info, etc. methods.
73
73
  module LogsStuff
74
- Logger::LEVELS.each { |l| define_method(l) { |s| Logger.instance.send(l, s) } }
74
+ Logger::LEVELS.each { |l| define_method(l) { |s, uplevel = 0| Logger.instance.send(l, s) } }
75
75
  end
76
76
 
77
77
  end
@@ -12,7 +12,15 @@ class Maildir < Source
12
12
  def initialize uri, usual=true, archived=false, sync_back=true, id=nil, labels=[]
13
13
  super uri, usual, archived, id
14
14
  @expanded_uri = Source.expand_filesystem_uri(uri)
15
- uri = URI(@expanded_uri)
15
+ parts = @expanded_uri.match /^([a-zA-Z0-9]*:(\/\/)?)(.*)/
16
+ if parts
17
+ prefix = parts[1]
18
+ @path = parts[3]
19
+ uri = URI(prefix + URI.encode(@path, URI_ENCODE_CHARS))
20
+ else
21
+ uri = URI(URI.encode @expanded_uri, URI_ENCODE_CHARS)
22
+ @path = uri.path
23
+ end
16
24
 
17
25
  raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
18
26
  raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
@@ -22,7 +30,7 @@ class Maildir < Source
22
30
  # sync by default if not specified
23
31
  @sync_back = true if @sync_back.nil?
24
32
 
25
- @dir = uri.path
33
+ @dir = URI.decode uri.path
26
34
  @labels = Set.new(labels || [])
27
35
  @mutex = Mutex.new
28
36
  @ctimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
@@ -60,7 +68,7 @@ class Maildir < Source
60
68
  File.safe_link tmp_path, new_path
61
69
  stored = true
62
70
  ensure
63
- File.unlink tmp_path if File.exists? tmp_path
71
+ File.unlink tmp_path if File.exist? tmp_path
64
72
  end
65
73
  end #rescue Errno...
66
74
  end #Dir.chdir
@@ -120,7 +128,10 @@ class Maildir < Source
120
128
  @ctimes[d] = ctime
121
129
 
122
130
  old_ids = benchmark(:maildir_read_index) { Index.instance.enum_for(:each_source_info, self.id, "#{d}/").to_a }
123
- new_ids = benchmark(:maildir_read_dir) { Dir.glob("#{subdir}/*").map { |x| File.join(d,File.basename(x)) }.sort }
131
+ new_ids = benchmark(:maildir_read_dir) {
132
+ Dir.open(subdir).select {
133
+ |f| !File.directory? f}.map {
134
+ |x| File.join(d,File.basename(x)) }.sort }
124
135
  added += new_ids - old_ids
125
136
  deleted += old_ids - new_ids
126
137
  debug "#{old_ids.size} in index, #{new_ids.size} in filesystem"
@@ -190,7 +201,7 @@ class Maildir < Source
190
201
  def trashed? id; maildir_data(id)[2].include? "T"; end
191
202
 
192
203
  def valid? id
193
- File.exists? File.join(@dir, id)
204
+ File.exist? File.join(@dir, id)
194
205
  end
195
206
 
196
207
  private
@@ -19,16 +19,24 @@ class MBox < Source
19
19
  case uri_or_fp
20
20
  when String
21
21
  @expanded_uri = Source.expand_filesystem_uri(uri_or_fp)
22
- uri = URI(@expanded_uri)
22
+ parts = @expanded_uri.match /^([a-zA-Z0-9]*:(\/\/)?)(.*)/
23
+ if parts
24
+ prefix = parts[1]
25
+ @path = parts[3]
26
+ uri = URI(prefix + URI.encode(@path, URI_ENCODE_CHARS))
27
+ else
28
+ uri = URI(URI.encode @expanded_uri, URI_ENCODE_CHARS)
29
+ @path = uri.path
30
+ end
31
+
23
32
  raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
24
33
  raise ArgumentError, "mbox URI ('#{uri}') cannot have a host: #{uri.host}" if uri.host
25
34
  raise ArgumentError, "mbox URI must have a path component" unless uri.path
26
35
  @f = nil
27
- @path = uri.path
28
36
  else
29
37
  @f = uri_or_fp
30
38
  @path = uri_or_fp.path
31
- @expanded_uri = "mbox://#{@path}"
39
+ @expanded_uri = "mbox://#{URI.encode @path, URI_ENCODE_CHARS}"
32
40
  end
33
41
 
34
42
  super uri_or_fp, usual, archived, id
@@ -107,7 +115,7 @@ class MBox < Source
107
115
  end
108
116
 
109
117
  def store_message date, from_email, &block
110
- need_blank = File.exists?(@path) && !File.zero?(@path)
118
+ need_blank = File.exist?(@path) && !File.zero?(@path)
111
119
  File.open(@path, "ab") do |f|
112
120
  f.puts if need_blank
113
121
  f.puts "From #{from_email} #{date.asctime}"
@@ -172,7 +180,7 @@ class MBox < Source
172
180
  time = $1
173
181
  begin
174
182
  ## hack -- make Time.parse fail when trying to substitute values from Time.now
175
- Time.parse time, 0
183
+ Time.parse time, Time.at(0)
176
184
  true
177
185
  rescue NoMethodError, ArgumentError
178
186
  warn "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
@@ -136,6 +136,11 @@ class Message
136
136
  header["list-post"] # just try the whole fucking thing
137
137
  end
138
138
  address && Person.from_address(address)
139
+ elsif header["mailing-list"]
140
+ address = if header["mailing-list"] =~ /list (.*?);/
141
+ $1
142
+ end
143
+ address && Person.from_address(address)
139
144
  elsif header["x-mailing-list"]
140
145
  Person.from_address header["x-mailing-list"]
141
146
  end
@@ -264,13 +269,27 @@ class Message
264
269
  parse_header rmsg.header
265
270
  message_to_chunks rmsg
266
271
  rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e
267
- warn "problem reading message #{id}"
272
+ warn_with_location "problem reading message #{id}"
268
273
  debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
269
274
 
270
275
  [Chunk::Text.new(error_message.split("\n"))]
276
+
277
+ rescue Exception => e
278
+
279
+ warn_with_location "problem reading message #{id}"
280
+ debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
281
+
282
+ raise e
283
+
271
284
  end
272
285
  end
273
286
 
287
+ def reload_from_source!
288
+ @chunks = nil
289
+ load_from_source!
290
+ end
291
+
292
+
274
293
  def error_message
275
294
  <<EOS
276
295
  #@snippet...
@@ -327,17 +346,17 @@ EOS
327
346
  to.map { |p| p.indexable_content },
328
347
  cc.map { |p| p.indexable_content },
329
348
  bcc.map { |p| p.indexable_content },
330
- indexable_chunks.map { |c| c.lines },
349
+ indexable_chunks.map { |c| c.lines.map { |l| l.fix_encoding! } },
331
350
  indexable_subject,
332
351
  ].flatten.compact.join " "
333
352
  end
334
353
 
335
354
  def indexable_body
336
- indexable_chunks.map { |c| c.lines }.flatten.compact.join " "
355
+ indexable_chunks.map { |c| c.lines }.flatten.compact.map { |l| l.fix_encoding! }.join " "
337
356
  end
338
357
 
339
358
  def indexable_chunks
340
- chunks.select { |c| c.is_a? Chunk::Text } || []
359
+ chunks.select { |c| c.indexable? } || []
341
360
  end
342
361
 
343
362
  def indexable_subject
@@ -390,19 +409,19 @@ private
390
409
 
391
410
  def multipart_signed_to_chunks m
392
411
  if m.body.size != 2
393
- warn "multipart/signed with #{m.body.size} parts (expecting 2)"
412
+ warn_with_location "multipart/signed with #{m.body.size} parts (expecting 2)"
394
413
  return
395
414
  end
396
415
 
397
416
  payload, signature = m.body
398
417
  if signature.multipart?
399
- warn "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
418
+ warn_with_location "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
400
419
  return
401
420
  end
402
421
 
403
422
  ## this probably will never happen
404
423
  if payload.header.content_type && payload.header.content_type.downcase == "application/pgp-signature"
405
- warn "multipart/signed with payload content type #{payload.header.content_type}"
424
+ warn_with_location "multipart/signed with payload content type #{payload.header.content_type}"
406
425
  return
407
426
  end
408
427
 
@@ -417,23 +436,23 @@ private
417
436
 
418
437
  def multipart_encrypted_to_chunks m
419
438
  if m.body.size != 2
420
- warn "multipart/encrypted with #{m.body.size} parts (expecting 2)"
439
+ warn_with_location "multipart/encrypted with #{m.body.size} parts (expecting 2)"
421
440
  return
422
441
  end
423
442
 
424
443
  control, payload = m.body
425
444
  if control.multipart?
426
- warn "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
445
+ warn_with_location "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
427
446
  return
428
447
  end
429
448
 
430
449
  if payload.header.content_type && payload.header.content_type.downcase != "application/octet-stream"
431
- warn "multipart/encrypted with payload content type #{payload.header.content_type}"
450
+ warn_with_location "multipart/encrypted with payload content type #{payload.header.content_type}"
432
451
  return
433
452
  end
434
453
 
435
454
  if control.header.content_type && control.header.content_type.downcase != "application/pgp-encrypted"
436
- warn "multipart/encrypted with control content type #{signature.header.content_type}"
455
+ warn_with_location "multipart/encrypted with control content type #{signature.header.content_type}"
437
456
  return
438
457
  end
439
458
 
@@ -677,7 +696,7 @@ private
677
696
  newstate = :quote
678
697
  elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE && !lines[(i+1)..-1].index { |l| l =~ /^-- $/ }
679
698
  newstate = :sig
680
- elsif line =~ BLOCK_QUOTE_PATTERN
699
+ elsif line =~ BLOCK_QUOTE_PATTERN && nextline !~ QUOTE_PATTERN
681
700
  newstate = :block_quote
682
701
  end
683
702
 
@@ -737,6 +756,11 @@ private
737
756
  end
738
757
  chunks
739
758
  end
759
+
760
+ def warn_with_location msg
761
+ warn msg
762
+ warn "Message is in #{location.source.uri} at #{location.info}"
763
+ end
740
764
  end
741
765
 
742
766
  class Location
@@ -128,7 +128,7 @@ EOS
128
128
 
129
129
  text = case @content_type
130
130
  when /^text\/plain\b/
131
- @raw_content
131
+ @raw_content.force_encoding(encoded_content.charset || 'US-ASCII')
132
132
  else
133
133
  HookManager.run "mime-decode", :content_type => @content_type,
134
134
  :filename => lambda { write_to_disk },
@@ -138,7 +138,7 @@ EOS
138
138
 
139
139
  @lines = nil
140
140
  if text
141
- text = text.transcode(encoded_content.charset || $encoding, text.encoding)
141
+ text = text.encode($encoding, :invalid => :replace, :undef => :replace)
142
142
  begin
143
143
  @lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
144
144
  rescue Encoding::CompatibilityError
@@ -159,11 +159,14 @@ EOS
159
159
  "Attachment: #{filename} (#{content_type}; #{@raw_content.size.to_human_size})"
160
160
  end
161
161
  end
162
+ def safe_filename; Shellwords.escape(@filename).gsub("/", "_") end
163
+ def filesafe_filename; @filename.gsub("/", "_") end
162
164
 
163
165
  ## an attachment is exapndable if we've managed to decode it into
164
166
  ## something we can display inline. otherwise, it's viewable.
165
167
  def inlineable?; false end
166
168
  def expandable?; !viewable? end
169
+ def indexable?; expandable? end
167
170
  def initial_state; :open end
168
171
  def viewable?; @lines.nil? end
169
172
  def view_default! path
@@ -229,6 +232,7 @@ EOS
229
232
  def inlineable?; true end
230
233
  def quotable?; true end
231
234
  def expandable?; false end
235
+ def indexable?; true end
232
236
  def viewable?; false end
233
237
  def color; :text_color end
234
238
  end
@@ -242,6 +246,7 @@ EOS
242
246
  def inlineable?; @lines.length == 1 end
243
247
  def quotable?; true end
244
248
  def expandable?; !inlineable? end
249
+ def indexable?; expandable? end
245
250
  def viewable?; false end
246
251
 
247
252
  def patina_color; :quote_patina_color end
@@ -258,6 +263,7 @@ EOS
258
263
  def inlineable?; @lines.length == 1 end
259
264
  def quotable?; false end
260
265
  def expandable?; !inlineable? end
266
+ def indexable?; expandable? end
261
267
  def viewable?; false end
262
268
 
263
269
  def patina_color; :sig_patina_color end
@@ -291,6 +297,7 @@ EOS
291
297
  def inlineable?; false end
292
298
  def quotable?; false end
293
299
  def expandable?; true end
300
+ def indexable?; true end
294
301
  def initial_state; :closed end
295
302
  def viewable?; false end
296
303
 
@@ -301,12 +308,13 @@ EOS
301
308
  end
302
309
 
303
310
  class CryptoNotice
304
- attr_reader :lines, :status, :patina_text
311
+ attr_reader :lines, :status, :patina_text, :unknown_fingerprint
305
312
 
306
- def initialize status, description, lines=[]
313
+ def initialize status, description, lines=[], unknown_fingerprint=nil
307
314
  @status = status
308
315
  @patina_text = description
309
316
  @lines = lines
317
+ @unknown_fingerprint = unknown_fingerprint
310
318
  end
311
319
 
312
320
  def patina_color
@@ -322,6 +330,7 @@ EOS
322
330
  def inlineable?; false end
323
331
  def quotable?; false end
324
332
  def expandable?; !@lines.empty? end
333
+ def indexable?; false end
325
334
  def viewable?; false end
326
335
  end
327
336
  end
@@ -46,7 +46,7 @@ class Mode
46
46
  end
47
47
 
48
48
  def resolve_input c
49
- ancestors.each do |klass| # try all keymaps in order of ancestry
49
+ self.class.ancestors.each do |klass| # try all keymaps in order of ancestry
50
50
  next unless @@keymaps.member?(klass)
51
51
  action = BufferManager.resolve_input_with_keymap c, @@keymaps[klass]
52
52
  return action if action
@@ -62,7 +62,7 @@ class Mode
62
62
 
63
63
  def help_text
64
64
  used_keys = {}
65
- ancestors.map do |klass|
65
+ self.class.ancestors.map do |klass|
66
66
  km = @@keymaps[klass] or next
67
67
  title = "Keybindings from #{Mode.make_name klass.name}"
68
68
  s = <<EOS
@@ -83,7 +83,8 @@ EOS
83
83
  ### helper functions
84
84
 
85
85
  def save_to_file fn, talk=true
86
- if File.exists? fn
86
+ FileUtils.mkdir_p File.dirname(fn)
87
+ if File.exist? fn
87
88
  unless BufferManager.ask_yes_or_no "File \"#{fn}\" exists. Overwrite?"
88
89
  info "Not overwriting #{fn}"
89
90
  return
@@ -102,37 +103,42 @@ EOS
102
103
  end
103
104
 
104
105
  def pipe_to_process command
105
- Open3.popen3(command) do |input, output, error|
106
- err, data, * = IO.select [error], [input], nil
107
-
108
- unless err.empty?
109
- message = err.first.read
110
- if message =~ /^\s*$/
111
- warn "error running #{command} (but no error message)"
112
- BufferManager.flash "Error running #{command}!"
113
- else
114
- warn "error running #{command}: #{message}"
115
- BufferManager.flash "Error: #{message}"
106
+ begin
107
+ Open3.popen3(command) do |input, output, error|
108
+ err, data, * = IO.select [error], [input], nil
109
+
110
+ unless err.empty?
111
+ message = err.first.read
112
+ if message =~ /^\s*$/
113
+ warn "error running #{command} (but no error message)"
114
+ BufferManager.flash "Error running #{command}!"
115
+ else
116
+ warn "error running #{command}: #{message}"
117
+ BufferManager.flash "Error: #{message}"
118
+ end
119
+ return nil, false
116
120
  end
117
- return
118
- end
119
121
 
120
- data = data.first
121
- data.sync = false # buffer input
122
+ data = data.first
123
+ data.sync = false # buffer input
122
124
 
123
- yield data
124
- data.close # output will block unless input is closed
125
+ yield data
126
+ data.close # output will block unless input is closed
125
127
 
126
- ## BUG?: shows errors or output but not both....
127
- data, * = IO.select [output, error], nil, nil
128
- data = data.first
128
+ ## BUG?: shows errors or output but not both....
129
+ data, * = IO.select [output, error], nil, nil
130
+ data = data.first
129
131
 
130
- if data.eof
131
- BufferManager.flash "'#{command}' done!"
132
- nil
133
- else
134
- data.read
132
+ if data.eof
133
+ BufferManager.flash "'#{command}' done!"
134
+ return nil, true
135
+ else
136
+ return data.read, true
137
+ end
135
138
  end
139
+ rescue Errno::ENOENT
140
+ # If the command is invalid
141
+ return nil, false
136
142
  end
137
143
  end
138
144
  end