iostreams 1.2.1 → 1.6.2

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -4
  3. data/lib/io_streams/builder.rb +27 -10
  4. data/lib/io_streams/bzip2/reader.rb +3 -3
  5. data/lib/io_streams/bzip2/writer.rb +3 -3
  6. data/lib/io_streams/deprecated.rb +1 -1
  7. data/lib/io_streams/encode/reader.rb +1 -3
  8. data/lib/io_streams/encode/writer.rb +1 -1
  9. data/lib/io_streams/errors.rb +22 -0
  10. data/lib/io_streams/io_streams.rb +1 -5
  11. data/lib/io_streams/line/reader.rb +28 -16
  12. data/lib/io_streams/path.rb +3 -1
  13. data/lib/io_streams/paths/file.rb +4 -4
  14. data/lib/io_streams/paths/http.rb +6 -3
  15. data/lib/io_streams/paths/s3.rb +30 -8
  16. data/lib/io_streams/paths/sftp.rb +34 -13
  17. data/lib/io_streams/pgp.rb +84 -71
  18. data/lib/io_streams/stream.rb +78 -12
  19. data/lib/io_streams/tabular.rb +28 -27
  20. data/lib/io_streams/tabular/header.rb +14 -12
  21. data/lib/io_streams/tabular/parser/csv.rb +4 -2
  22. data/lib/io_streams/tabular/parser/fixed.rb +166 -26
  23. data/lib/io_streams/tabular/utility/csv_row.rb +1 -4
  24. data/lib/io_streams/utils.rb +4 -4
  25. data/lib/io_streams/version.rb +1 -1
  26. data/lib/io_streams/zip/reader.rb +1 -1
  27. data/test/builder_test.rb +29 -0
  28. data/test/bzip2_writer_test.rb +6 -4
  29. data/test/deprecated_test.rb +2 -0
  30. data/test/files/test.psv +4 -0
  31. data/test/files/unclosed_quote_large_test.csv +1658 -0
  32. data/test/files/unclosed_quote_test2.csv +3 -0
  33. data/test/io_streams_test.rb +2 -2
  34. data/test/line_reader_test.rb +30 -4
  35. data/test/paths/file_test.rb +1 -1
  36. data/test/paths/s3_test.rb +3 -3
  37. data/test/paths/sftp_test.rb +4 -4
  38. data/test/pgp_test.rb +54 -4
  39. data/test/pgp_writer_test.rb +3 -3
  40. data/test/stream_test.rb +174 -8
  41. data/test/tabular_test.rb +100 -40
  42. data/test/test_helper.rb +1 -1
  43. metadata +47 -42
@@ -26,12 +26,13 @@ module IOStreams
26
26
  include SemanticLogger::Loggable if defined?(SemanticLogger)
27
27
 
28
28
  class << self
29
- attr_accessor :sshpass_bin, :sftp_bin, :sshpass_wait_seconds
29
+ attr_accessor :sshpass_bin, :sftp_bin, :sshpass_wait_seconds, :before_password_wait_seconds
30
30
  end
31
31
 
32
- @sftp_bin = "sftp"
33
- @sshpass_bin = "sshpass"
34
- @sshpass_wait_seconds = 5
32
+ @sftp_bin = "sftp"
33
+ @sshpass_bin = "sshpass"
34
+ @before_password_wait_seconds = 2
35
+ @sshpass_wait_seconds = 5
35
36
 
36
37
  attr_reader :hostname, :username, :ssh_options, :url, :port
37
38
 
@@ -71,7 +72,9 @@ module IOStreams
71
72
  # end
72
73
  #
73
74
  # # When using the sftp executable use an identity file instead of a password to authenticate:
74
- # IOStreams.path("sftp://test.com/path/file_name.csv", username: "jack", ssh_options: {IdentityFile: "~/.ssh/private_key"}).reader do |io|
75
+ # IOStreams.path("sftp://test.com/path/file_name.csv",
76
+ # username: "jack",
77
+ # ssh_options: {IdentityFile: "~/.ssh/private_key"}).reader do |io|
75
78
  # puts io.read
76
79
  # end
77
80
  def initialize(url, username: nil, password: nil, ssh_options: {})
@@ -122,7 +125,8 @@ module IOStreams
122
125
  # end
123
126
  #
124
127
  # Example Output:
125
- # sftp://sftp.example.org/a/b/c/test.txt {:type=>1, :size=>37, :owner=>"test_owner", :group=>"test_group", :permissions=>420, :atime=>1572378136, :mtime=>1572378136, :link_count=>1, :extended=>{}}
128
+ # sftp://sftp.example.org/a/b/c/test.txt {:type=>1, :size=>37, :owner=>"test_owner", :group=>"test_group",
129
+ # :permissions=>420, :atime=>1572378136, :mtime=>1572378136, :link_count=>1, :extended=>{}}
126
130
  def each_child(pattern = "*", case_sensitive: true, directories: false, hidden: false)
127
131
  Utils.load_soft_dependency("net-sftp", "SFTP glob capability", "net/sftp") unless defined?(Net::SFTP)
128
132
 
@@ -165,15 +169,23 @@ module IOStreams
165
169
  with_sftp_args do |args|
166
170
  Open3.popen2e(*args) do |writer, reader, waith_thr|
167
171
  begin
172
+ # Give time for remote sftp server to get ready to accept the password.
173
+ sleep self.class.before_password_wait_seconds
174
+
168
175
  writer.puts password
176
+
169
177
  # Give time for password to be processed and stdin to be passed to sftp process.
170
178
  sleep self.class.sshpass_wait_seconds
179
+
171
180
  writer.puts "get #{remote_file_name} #{local_file_name}"
172
181
  writer.puts "bye"
173
182
  writer.close
174
183
  out = reader.read.chomp
175
184
  unless waith_thr.value.success?
176
- raise(Errors::CommunicationsFailure, "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
185
+ raise(
186
+ Errors::CommunicationsFailure,
187
+ "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
188
+ )
177
189
  end
178
190
 
179
191
  out
@@ -183,7 +195,10 @@ module IOStreams
183
195
  rescue StandardError
184
196
  nil
185
197
  end
186
- raise(Errors::CommunicationsFailure, "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
198
+ raise(
199
+ Errors::CommunicationsFailure,
200
+ "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
201
+ )
187
202
  end
188
203
  end
189
204
  end
@@ -201,7 +216,10 @@ module IOStreams
201
216
  writer.close
202
217
  out = reader.read.chomp
203
218
  unless waith_thr.value.success?
204
- raise(Errors::CommunicationsFailure, "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
219
+ raise(
220
+ Errors::CommunicationsFailure,
221
+ "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
222
+ )
205
223
  end
206
224
 
207
225
  out
@@ -211,7 +229,10 @@ module IOStreams
211
229
  rescue StandardError
212
230
  nil
213
231
  end
214
- raise(Errors::CommunicationsFailure, "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
232
+ raise(
233
+ Errors::CommunicationsFailure,
234
+ "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
235
+ )
215
236
  end
216
237
  end
217
238
  end
@@ -256,9 +277,9 @@ module IOStreams
256
277
  end
257
278
 
258
279
  def build_ssh_options
259
- options = ssh_options.dup
260
- options[:logger] ||= logger if defined?(SemanticLogger)
261
- options[:port] ||= port
280
+ options = ssh_options.dup
281
+ options[:logger] ||= logger if defined?(SemanticLogger)
282
+ options[:port] ||= port
262
283
  options[:max_pkt_size] ||= 65_536
263
284
  options[:password] ||= @password
264
285
  options
@@ -46,7 +46,15 @@ module IOStreams
46
46
  # `SecureRandom.urlsafe_base64(128)`
47
47
  #
48
48
  # See `man gpg` for the remaining options
49
- def self.generate_key(name:, email:, comment: nil, passphrase:, key_type: "RSA", key_length: 4096, subkey_type: "RSA", subkey_length: key_length, expire_date: nil)
49
+ def self.generate_key(name:,
50
+ email:,
51
+ comment: nil,
52
+ passphrase:,
53
+ key_type: "RSA",
54
+ key_length: 4096,
55
+ subkey_type: "RSA",
56
+ subkey_length: key_length,
57
+ expire_date: nil)
50
58
  version_check
51
59
  params = ""
52
60
  params << "Key-Type: #{key_type}\n" if key_type
@@ -63,13 +71,13 @@ module IOStreams
63
71
 
64
72
  out, err, status = Open3.capture3(command, binmode: true, stdin_data: params)
65
73
  logger&.debug { "IOStreams::Pgp.generate_key: #{command}\n#{params}\n#{err}#{out}" }
66
- if status.success?
67
- if match = err.match(/gpg: key ([0-9A-F]+)\s+/)
68
- match[1]
69
- end
70
- else
71
- raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}")
72
- end
74
+
75
+ raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}") unless status.success?
76
+
77
+ match = err.match(/gpg: key ([0-9A-F]+)\s+/)
78
+ return unless match
79
+
80
+ match[1]
73
81
  end
74
82
 
75
83
  # Delete all private and public keys for a particular email.
@@ -77,7 +85,8 @@ module IOStreams
77
85
  # Returns false if no key was found.
78
86
  # Raises an exception if it fails to delete the key.
79
87
  #
80
- # email: [String] Email address for the key.
88
+ # email: [String] Optional email address for the key.
89
+ # key_id: [String] Optional id for the key.
81
90
  #
82
91
  # public: [true|false]
83
92
  # Whether to delete the public key
@@ -86,12 +95,12 @@ module IOStreams
86
95
  # private: [true|false]
87
96
  # Whether to delete the private key
88
97
  # Default: false
89
- def self.delete_keys(email:, public: true, private: false)
98
+ def self.delete_keys(email: nil, key_id: nil, public: true, private: false)
90
99
  version_check
91
100
  method_name = pgp_version.to_f >= 2.2 ? :delete_public_or_private_keys : :delete_public_or_private_keys_v1
92
101
  status = false
93
- status = send(method_name, email: email, private: true) if private
94
- status = send(method_name, email: email, private: false) if public
102
+ status = send(method_name, email: email, key_id: key_id, private: true) if private
103
+ status = send(method_name, email: email, key_id: key_id, private: false) if public
95
104
  status
96
105
  end
97
106
 
@@ -102,11 +111,6 @@ module IOStreams
102
111
  !list_keys(email: email, key_id: key_id, private: private).empty?
103
112
  end
104
113
 
105
- # Deprecated
106
- def self.has_key?(**args)
107
- key?(**args)
108
- end
109
-
110
114
  # Returns [Array<Hash>] the list of keys.
111
115
  # Each Hash consists of:
112
116
  # key_length: [Integer]
@@ -150,16 +154,15 @@ module IOStreams
150
154
 
151
155
  out, err, status = Open3.capture3(command, binmode: true, stdin_data: key)
152
156
  logger&.debug { "IOStreams::Pgp.key_info: #{command}\n#{err}#{out}" }
153
- if status.success? && out.length.positive?
154
- # Sample Output:
155
- #
156
- # pub 4096R/3A5456F5 2017-06-07
157
- # uid Joe Bloggs <j@bloggs.net>
158
- # sub 4096R/2C9B240B 2017-06-07
159
- parse_list_output(out)
160
- else
161
- raise(Pgp::Failure, "GPG Failed extracting key details: #{err} #{out}")
162
- end
157
+
158
+ raise(Pgp::Failure, "GPG Failed extracting key details: #{err} #{out}") unless status.success? && out.length.positive?
159
+
160
+ # Sample Output:
161
+ #
162
+ # pub 4096R/3A5456F5 2017-06-07
163
+ # uid Joe Bloggs <j@bloggs.net>
164
+ # sub 4096R/2C9B240B 2017-06-07
165
+ parse_list_output(out)
163
166
  end
164
167
 
165
168
  # Returns [String] containing all the public keys for the supplied email address.
@@ -181,11 +184,10 @@ module IOStreams
181
184
 
182
185
  out, err, status = Open3.capture3(command, binmode: true)
183
186
  logger&.debug { "IOStreams::Pgp.export: #{command}\n#{err}" }
184
- if status.success? && out.length.positive?
185
- out
186
- else
187
- raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err}")
188
- end
187
+
188
+ raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err}") unless status.success? && out.length.positive?
189
+
190
+ out
189
191
  end
190
192
 
191
193
  # Imports the supplied public/private key
@@ -226,7 +228,7 @@ module IOStreams
226
228
  err.each_line do |line|
227
229
  if line =~ /secret key imported/
228
230
  secret = true
229
- elsif match = line.match(/key\s+(\w+):\s+(\w+).+\"(.*)<(.*)>\"/)
231
+ elsif (match = line.match(/key\s+(\w+):\s+(\w+).+\"(.*)<(.*)>\"/))
230
232
  results << {
231
233
  key_id: match[1].to_s.strip,
232
234
  private: secret,
@@ -251,12 +253,15 @@ module IOStreams
251
253
  def self.import_and_trust(key:)
252
254
  raise(ArgumentError, "Key cannot be empty") if key.nil? || (key == "")
253
255
 
254
- email = key_info(key: key).last.fetch(:email)
255
- raise(ArgumentError, "Recipient email cannot be extracted from supplied key") unless email
256
+ key_info = key_info(key: key).last
257
+
258
+ email = key_info.fetch(:email, nil)
259
+ key_id = key_info.fetch(:key_id, nil)
260
+ raise(ArgumentError, "Recipient email or key id cannot be extracted from supplied key") unless email || key_id
256
261
 
257
262
  import(key: key)
258
- set_trust(email: email)
259
- email
263
+ set_trust(email: email, key_id: key_id)
264
+ email || key_id
260
265
  end
261
266
 
262
267
  # Set the trust level for an existing key.
@@ -266,20 +271,19 @@ module IOStreams
266
271
  #
267
272
  # After importing keys, they are not trusted and the relevant trust level must be set.
268
273
  # Default: 5 : Ultimate
269
- def self.set_trust(email:, level: 5)
274
+ def self.set_trust(email: nil, key_id: nil, level: 5)
270
275
  version_check
271
- fingerprint = fingerprint(email: email)
276
+ fingerprint = key_id || fingerprint(email: email)
272
277
  return unless fingerprint
273
278
 
274
279
  command = "#{executable} --import-ownertrust"
275
280
  trust = "#{fingerprint}:#{level + 1}:\n"
276
281
  out, err, status = Open3.capture3(command, stdin_data: trust)
277
282
  logger&.debug { "IOStreams::Pgp.set_trust: #{command}\n#{err}#{out}" }
278
- if status.success?
279
- err
280
- else
281
- raise(Pgp::Failure, "GPG Failed trusting key: #{err} #{out}")
282
- end
283
+
284
+ raise(Pgp::Failure, "GPG Failed trusting key: #{err} #{out}") unless status.success?
285
+
286
+ err
283
287
  end
284
288
 
285
289
  # DEPRECATED - Use key_ids instead of fingerprints
@@ -287,18 +291,18 @@ module IOStreams
287
291
  version_check
288
292
  Open3.popen2e("#{executable} --list-keys --fingerprint --with-colons #{email}") do |_stdin, out, waith_thr|
289
293
  output = out.read.chomp
290
- if waith_thr.value.success?
291
- output.each_line do |line|
292
- if match = line.match(/\Afpr.*::([^\:]*):\Z/)
293
- return match[1]
294
- end
294
+ unless waith_thr.value.success?
295
+ unless output =~ /(public key not found|No public key)/i
296
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}")
295
297
  end
296
- nil
297
- else
298
- return if output =~ /(public key not found|No public key)/i
298
+ end
299
299
 
300
- raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}")
300
+ output.each_line do |line|
301
+ if (match = line.match(/\Afpr.*::([^\:]*):\Z/))
302
+ return match[1]
303
+ end
301
304
  end
305
+ nil
302
306
  end
303
307
  end
304
308
 
@@ -328,7 +332,7 @@ module IOStreams
328
332
  # CAMELLIA128, CAMELLIA192, CAMELLIA256
329
333
  # Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
330
334
  # Compression: Uncompressed, ZIP, ZLIB, BZIP2
331
- if match = out.lines.first.match(/(\d+\.\d+.\d+)/)
335
+ if (match = out.lines.first.match(/(\d+\.\d+.\d+)/))
332
336
  match[1]
333
337
  end
334
338
  else
@@ -339,8 +343,6 @@ module IOStreams
339
343
  end
340
344
  end
341
345
 
342
- private
343
-
344
346
  @logger = nil
345
347
 
346
348
  def self.logger
@@ -348,9 +350,12 @@ module IOStreams
348
350
  end
349
351
 
350
352
  def self.version_check
351
- if pgp_version.to_f >= 2.3
352
- raise(Pgp::UnsupportedVersion, "Version #{pgp_version} of #{executable} is not yet supported. You are welcome to submit a Pull Request.")
353
- end
353
+ return unless pgp_version.to_f >= 2.4
354
+
355
+ raise(
356
+ Pgp::UnsupportedVersion,
357
+ "Version #{pgp_version} of #{executable} is not yet supported. Please submit a Pull Request to support it."
358
+ )
354
359
  end
355
360
 
356
361
  # v2.2.1 output:
@@ -370,7 +375,7 @@ module IOStreams
370
375
  results = []
371
376
  hash = {}
372
377
  out.each_line do |line|
373
- if match = line.match(/(pub|sec)\s+(\D+)(\d+)\s+(\d+-\d+-\d+)\s+(.*)/)
378
+ if (match = line.match(/(pub|sec)\s+(\D+)(\d+)\s+(\d+-\d+-\d+)\s+(.*)/))
374
379
  # v2.2: pub rsa1024 2017-10-24 [SCEA]
375
380
  hash = {
376
381
  private: match[1] == "sec",
@@ -382,7 +387,7 @@ module IOStreams
382
387
  match[4]
383
388
  end)
384
389
  }
385
- elsif match = line.match(%r{(pub|sec)\s+(\d+)(.*)/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?})
390
+ elsif (match = line.match(%r{(pub|sec)\s+(\d+)(.*)/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?}))
386
391
  # Matches: pub 2048R/C7F9D9CB 2016-10-26
387
392
  # Or: pub 2048R/C7F9D9CB 2016-10-26 Receiver <receiver@example.org>
388
393
  hash = {
@@ -403,7 +408,7 @@ module IOStreams
403
408
  results << hash
404
409
  hash = {}
405
410
  end
406
- elsif match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)<(.+)>/)
411
+ elsif (match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)<(.+)>/))
407
412
  # Matches: uid [ unknown] Joe Bloggs <j@bloggs.net>
408
413
  # Or: uid Joe Bloggs <j@bloggs.net>
409
414
  # v2.2: uid [ultimate] Joe Bloggs <pgp_test@iostreams.net>
@@ -412,7 +417,15 @@ module IOStreams
412
417
  hash[:trust] = match[2].to_s.strip if match[1]
413
418
  results << hash
414
419
  hash = {}
415
- elsif match = line.match(/([A-Z0-9]+)/)
420
+ elsif (match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)/))
421
+ # Matches: uid [ unknown] Joe Bloggs
422
+ # Or: uid Joe Bloggs
423
+ # v2.2: uid [ultimate] Joe Bloggs
424
+ hash[:name] = match[3].to_s.strip
425
+ hash[:trust] = match[2].to_s.strip if match[1]
426
+ results << hash
427
+ hash = {}
428
+ elsif (match = line.match(/([A-Z0-9]+)/))
416
429
  # v2.2 18A0FC1C09C0D8AE34CE659257DC4AE323C7368C
417
430
  hash[:key_id] ||= match[1]
418
431
  end
@@ -420,10 +433,10 @@ module IOStreams
420
433
  results
421
434
  end
422
435
 
423
- def self.delete_public_or_private_keys(email:, private: false)
436
+ def self.delete_public_or_private_keys(email: nil, key_id: nil, private: false)
424
437
  keys = private ? "secret-keys" : "keys"
425
438
 
426
- list = list_keys(email: email, private: private)
439
+ list = email ? list_keys(email: email, private: private) : list_keys(key_id: key_id)
427
440
  return false if list.empty?
428
441
 
429
442
  list.each do |key_info|
@@ -435,17 +448,17 @@ module IOStreams
435
448
  logger&.debug { "IOStreams::Pgp.delete_keys: #{command}\n#{err}#{out}" }
436
449
 
437
450
  unless status.success?
438
- raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}")
451
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email || key_id}: #{err}: #{out}")
439
452
  end
440
- raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}:#{out}") if out.include?("error")
453
+ raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}:#{out}") if out.include?("error")
441
454
  end
442
455
  true
443
456
  end
444
457
 
445
- def self.delete_public_or_private_keys_v1(email:, private: false)
458
+ def self.delete_public_or_private_keys_v1(email: nil, key_id: nil, private: false)
446
459
  keys = private ? "secret-keys" : "keys"
447
460
 
448
- command = "for i in `#{executable} --list-#{keys} --with-colons --fingerprint #{email} | grep \"^fpr\" | cut -d: -f10`; do\n"
461
+ command = "for i in `#{executable} --list-#{keys} --with-colons --fingerprint #{email || key_id} | grep \"^fpr\" | cut -d: -f10`; do\n"
449
462
  command << "#{executable} --batch --no-tty --yes --delete-#{keys} \"$i\" ;\n"
450
463
  command << "done"
451
464
 
@@ -454,9 +467,9 @@ module IOStreams
454
467
 
455
468
  return false if err =~ /(not found|no public key)/i
456
469
  unless status.success?
457
- raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}")
470
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email || key_id}: #{err}: #{out}")
458
471
  end
459
- raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}: #{out}") if out.include?("error")
472
+ raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}: #{out}") if out.include?("error")
460
473
 
461
474
  true
462
475
  end
@@ -17,7 +17,7 @@ module IOStreams
17
17
  #
18
18
  # Example:
19
19
  #
20
- # IOStreams.path('tempfile2527').stream(:zip).stream(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
20
+ # IOStreams.path("tempfile2527").stream(:zip).stream(:pgp, passphrase: "receiver_passphrase").read
21
21
  def stream(stream, **options)
22
22
  builder.stream(stream, **options)
23
23
  self
@@ -27,12 +27,12 @@ module IOStreams
27
27
  # If the relevant stream is not found for this file it is ignored.
28
28
  # For example, if the file does not have a pgp extension then the pgp option is not relevant.
29
29
  #
30
- # IOStreams.path('keep_safe.pgp').option(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
30
+ # IOStreams.path("keep_safe.pgp").option(:pgp, passphrase: "receiver_passphrase").read
31
31
  #
32
32
  # # In this case the file is not pgp so the `passphrase` option is ignored.
33
- # IOStreams.path('keep_safe.enc').option(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
33
+ # IOStreams.path("keep_safe.enc").option(:pgp, passphrase: "receiver_passphrase").read
34
34
  #
35
- # IOStreams.path(output_file_name).option(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
35
+ # IOStreams.path(output_file_name).option(:pgp, passphrase: "receiver_passphrase").read
36
36
  def option(stream, **options)
37
37
  builder.option(stream, **options)
38
38
  self
@@ -177,7 +177,7 @@ module IOStreams
177
177
  end
178
178
 
179
179
  def copy_to(target, convert: true)
180
- target = IOStreams.path(target) unless target.is_a?(Stream)
180
+ target = IOStreams.new(target)
181
181
  target.copy_from(self, convert: convert)
182
182
  end
183
183
 
@@ -191,11 +191,41 @@ module IOStreams
191
191
  end
192
192
  end
193
193
 
194
- # Set/get the original file_name
194
+ # Set the original file_name
195
195
  def file_name=(file_name)
196
196
  builder.file_name = file_name
197
197
  end
198
198
 
199
+ # Set/get the tabular format_options
200
+ def format(format = :none)
201
+ if format == :none
202
+ builder.format
203
+ else
204
+ builder.format = format
205
+ self
206
+ end
207
+ end
208
+
209
+ # Set the tabular format
210
+ def format=(format)
211
+ builder.format = format
212
+ end
213
+
214
+ # Set/get the tabular format options
215
+ def format_options(format_options = :none)
216
+ if format_options == :none
217
+ builder.format_options
218
+ else
219
+ builder.format_options = format_options
220
+ self
221
+ end
222
+ end
223
+
224
+ # Set the tabular format_options
225
+ def format_options=(format_options)
226
+ builder.format_options = format_options
227
+ end
228
+
199
229
  # Returns [String] the last component of this path.
200
230
  # Returns `nil` if no `file_name` was set.
201
231
  #
@@ -282,20 +312,37 @@ module IOStreams
282
312
  def line_reader(embedded_within: nil, **args)
283
313
  embedded_within = '"' if embedded_within.nil? && builder.file_name&.include?(".csv")
284
314
 
285
- stream_reader { |io| yield IOStreams::Line::Reader.new(io, original_file_name: builder.file_name, embedded_within: embedded_within, **args) }
315
+ stream_reader do |io|
316
+ yield IOStreams::Line::Reader.new(io,
317
+ original_file_name: builder.file_name,
318
+ embedded_within: embedded_within,
319
+ **args)
320
+ end
286
321
  end
287
322
 
288
323
  # Iterate over a file / stream returning each line as an array, one at a time.
289
324
  def row_reader(delimiter: nil, embedded_within: nil, **args)
290
325
  line_reader(delimiter: delimiter, embedded_within: embedded_within) do |io|
291
- yield IOStreams::Row::Reader.new(io, original_file_name: builder.file_name, **args)
326
+ yield IOStreams::Row::Reader.new(
327
+ io,
328
+ original_file_name: builder.file_name,
329
+ format: builder.format,
330
+ format_options: builder.format_options,
331
+ **args
332
+ )
292
333
  end
293
334
  end
294
335
 
295
336
  # Iterate over a file / stream returning each line as a hash, one at a time.
296
337
  def record_reader(delimiter: nil, embedded_within: nil, **args)
297
338
  line_reader(delimiter: delimiter, embedded_within: embedded_within) do |io|
298
- yield IOStreams::Record::Reader.new(io, original_file_name: builder.file_name, **args)
339
+ yield IOStreams::Record::Reader.new(
340
+ io,
341
+ original_file_name: builder.file_name,
342
+ format: builder.format,
343
+ format_options: builder.format_options,
344
+ **args
345
+ )
299
346
  end
300
347
  end
301
348
 
@@ -306,19 +353,38 @@ module IOStreams
306
353
  def line_writer(**args, &block)
307
354
  return block.call(io_stream) if io_stream&.is_a?(IOStreams::Line::Writer)
308
355
 
309
- writer { |io| IOStreams::Line::Writer.stream(io, original_file_name: builder.file_name, **args, &block) }
356
+ writer do |io|
357
+ IOStreams::Line::Writer.stream(io, original_file_name: builder.file_name, **args, &block)
358
+ end
310
359
  end
311
360
 
312
361
  def row_writer(delimiter: $/, **args, &block)
313
362
  return block.call(io_stream) if io_stream&.is_a?(IOStreams::Row::Writer)
314
363
 
315
- line_writer(delimiter: delimiter) { |io| IOStreams::Row::Writer.stream(io, original_file_name: builder.file_name, **args, &block) }
364
+ line_writer(delimiter: delimiter) do |io|
365
+ IOStreams::Row::Writer.stream(
366
+ io,
367
+ original_file_name: builder.file_name,
368
+ format: builder.format,
369
+ format_options: builder.format_options,
370
+ **args,
371
+ &block
372
+ )
373
+ end
316
374
  end
317
375
 
318
376
  def record_writer(delimiter: $/, **args, &block)
319
377
  return block.call(io_stream) if io_stream&.is_a?(IOStreams::Record::Writer)
320
378
 
321
- line_writer(delimiter: delimiter) { |io| IOStreams::Record::Writer.stream(io, original_file_name: builder.file_name, **args, &block) }
379
+ line_writer(delimiter: delimiter) do |io|
380
+ IOStreams::Record::Writer.stream(
381
+ io,
382
+ original_file_name: builder.file_name,
383
+ format: builder.format,
384
+ format_options: builder.format_options,
385
+ **args,
386
+ &block)
387
+ end
322
388
  end
323
389
  end
324
390
  end