tty-file 0.6.0 → 0.10.0

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.
@@ -1,3 +1 @@
1
- # encoding: utf-8
2
-
3
- require_relative 'tty/file'
1
+ require_relative "tty/file"
@@ -1,17 +1,16 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- require 'pastel'
4
- require 'tty-prompt'
5
- require 'erb'
6
- require 'tempfile'
7
- require 'pathname'
3
+ require "pastel"
4
+ require "erb"
5
+ require "tempfile"
6
+ require "pathname"
8
7
 
9
- require_relative 'file/create_file'
10
- require_relative 'file/digest_file'
11
- require_relative 'file/download_file'
12
- require_relative 'file/differ'
13
- require_relative 'file/read_backward_file'
14
- require_relative 'file/version'
8
+ require_relative "file/compare_files"
9
+ require_relative "file/create_file"
10
+ require_relative "file/digest_file"
11
+ require_relative "file/download_file"
12
+ require_relative "file/read_backward_file"
13
+ require_relative "file/version"
15
14
 
16
15
  module TTY
17
16
  module File
@@ -38,14 +37,14 @@ module TTY
38
37
 
39
38
  # Check if file is binary
40
39
  #
41
- # @param [String] relative_path
40
+ # @param [String, Pathname] relative_path
42
41
  # the path to file to check
43
42
  #
44
43
  # @example
45
- # binary?('Gemfile') # => false
44
+ # binary?("Gemfile") # => false
46
45
  #
47
46
  # @example
48
- # binary?('image.jpg') # => true
47
+ # binary?("image.jpg") # => true
49
48
  #
50
49
  # @return [Boolean]
51
50
  # Returns `true` if the file is binary, `false` otherwise
@@ -54,10 +53,10 @@ module TTY
54
53
  def binary?(relative_path)
55
54
  bytes = ::File.new(relative_path).size
56
55
  bytes = 2**12 if bytes > 2**12
57
- buffer = ::File.read(relative_path, bytes, 0) || ''
58
- buffer = buffer.force_encoding(Encoding.default_external)
56
+ buffer = read_to_char(relative_path, bytes, 0)
57
+
59
58
  begin
60
- return buffer !~ /\A[\s[[:print:]]]*\z/m
59
+ buffer !~ /\A[\s[[:print:]]]*\z/m
61
60
  rescue ArgumentError => error
62
61
  return true if error.message =~ /invalid byte sequence/
63
62
  raise
@@ -65,88 +64,119 @@ module TTY
65
64
  end
66
65
  module_function :binary?
67
66
 
67
+ # Read bytes from a file up to valid character
68
+ #
69
+ # @param [String, Pathname] relative_path
70
+ # the path to file
71
+ #
72
+ # @param [Integer] bytes
73
+ #
74
+ # @example
75
+ # TTY::File.read_to_char()
76
+ #
77
+ # @return [String]
78
+ #
79
+ # @api public
80
+ def read_to_char(relative_path, bytes = nil, offset = nil)
81
+ buffer = ""
82
+ ::File.open(relative_path) do |file|
83
+ buffer = file.read(bytes) || ""
84
+ buffer = buffer.dup.force_encoding(Encoding.default_external)
85
+
86
+ while !file.eof? && !buffer.valid_encoding? &&
87
+ (buffer.bytesize < bytes + 10)
88
+
89
+ buffer += file.read(1).force_encoding(Encoding.default_external)
90
+ end
91
+ end
92
+ buffer
93
+ end
94
+ module_function :read_to_char
95
+
68
96
  # Create checksum for a file, io or string objects
69
97
  #
70
- # @param [File,IO,String] source
98
+ # @param [File, IO, String, Pathname] source
71
99
  # the source to generate checksum for
72
100
  # @param [String] mode
73
- # @param [Hash[Symbol]] options
74
- # @option options [String] :noop
75
- # No operation
101
+ # @param [Boolean] noop
102
+ # when true skip this action
76
103
  #
77
104
  # @example
78
- # checksum_file('/path/to/file')
105
+ # checksum_file("/path/to/file")
79
106
  #
80
107
  # @example
81
- # checksum_file('Some string content', 'md5')
108
+ # checksum_file("Some string content", "md5")
82
109
  #
83
110
  # @return [String]
84
111
  # the generated hex value
85
112
  #
86
113
  # @api public
87
- def checksum_file(source, *args, **options)
88
- mode = args.size.zero? ? 'sha256' : args.pop
89
- digester = DigestFile.new(source, mode, options)
90
- digester.call unless options[:noop]
114
+ def checksum_file(source, *args, noop: false)
115
+ mode = args.size.zero? ? "sha256" : args.pop
116
+ digester = DigestFile.new(source, mode)
117
+ digester.call unless noop
91
118
  end
92
119
  module_function :checksum_file
93
120
 
94
121
  # Change file permissions
95
122
  #
96
- # @param [String] relative_path
123
+ # @param [String, Pathname] relative_path
124
+ # the string or path to a file
97
125
  # @param [Integer,String] permisssions
98
- # @param [Hash[Symbol]] options
99
- # @option options [Symbol] :noop
100
- # @option options [Symbol] :verbose
101
- # @option options [Symbol] :force
126
+ # the string or octal number for permissoins
127
+ # @param [Boolean] noop
128
+ # when true skips this action
129
+ # @param [Boolean] verbose
130
+ # when true displays logging information
131
+ # @param [Symbol] color
132
+ # the name for the color to format display message, :green by default
102
133
  #
103
134
  # @example
104
- # chmod('Gemfile', 0755)
135
+ # chmod("Gemfile", 0755)
105
136
  #
106
137
  # @example
107
- # chmod('Gemilfe', TTY::File::U_R | TTY::File::U_W)
138
+ # chmod("Gemilfe", TTY::File::U_R | TTY::File::U_W)
108
139
  #
109
140
  # @example
110
- # chmod('Gemfile', 'u+x,g+x')
141
+ # chmod("Gemfile", "u+x,g+x")
111
142
  #
112
143
  # @api public
113
- def chmod(relative_path, permissions, **options)
114
- mode = ::File.lstat(relative_path).mode
115
- if permissions.to_s =~ /\d+/
116
- mode = permissions
117
- else
118
- permissions.scan(/[ugoa][+-=][rwx]+/) do |setting|
119
- who, action = setting[0], setting[1]
120
- setting[2..setting.size].each_byte do |perm|
121
- mask = const_get("#{who.upcase}_#{perm.chr.upcase}")
122
- (action == '+') ? mode |= mask : mode ^= mask
123
- end
124
- end
125
- end
126
- log_status(:chmod, relative_path, options.fetch(:verbose, true),
127
- options.fetch(:color, :green))
128
- ::FileUtils.chmod_R(mode, relative_path) unless options[:noop]
144
+ def chmod(relative_path, permissions, verbose: true, color: :green, noop: false)
145
+ log_status(:chmod, relative_path, verbose: verbose, color: color)
146
+ ::FileUtils.chmod_R(permissions, relative_path) unless noop
129
147
  end
130
148
  module_function :chmod
131
149
 
132
150
  # Create directory structure
133
151
  #
134
- # @param [String, Hash] destination
152
+ # @param [String, Pathname, Hash] destination
135
153
  # the path or data structure describing directory tree
154
+ # @param [Object] context
155
+ # the context for template evaluation
156
+ # @param [Boolean] quiet
157
+ # when true leaves prompt output, otherwise clears
158
+ # @param [Boolean] force
159
+ # when true overwrites existing files, false by default
160
+ # @param [Boolean] noop
161
+ # when true skips this action
162
+ # @param [Boolean] verbose
163
+ # when true displays logging information
164
+ # @param [Symbol] color
165
+ # the name for the color to format display message, :green by default
136
166
  #
137
167
  # @example
138
- # create_directory('/path/to/dir')
168
+ # create_directory("/path/to/dir")
139
169
  #
140
170
  # @example
141
171
  # tree =
142
- # 'app' => [
143
- # 'README.md',
144
- # ['Gemfile', "gem 'tty-file'"],
145
- # 'lib' => [
146
- # 'cli.rb',
147
- # ['file_utils.rb', "require 'tty-file'"]
172
+ # "app" => [
173
+ # "README.md",
174
+ # ["Gemfile", "gem "tty-file""],
175
+ # "lib" => [
176
+ # "cli.rb",
177
+ # ["file_utils.rb", "require "tty-file""]
148
178
  # ]
149
- # 'spec' => []
179
+ # "spec" => []
150
180
  # ]
151
181
  #
152
182
  # create_directory(tree)
@@ -154,25 +184,30 @@ module TTY
154
184
  # @return [void]
155
185
  #
156
186
  # @api public
157
- def create_directory(destination, *args, **options)
187
+ def create_directory(destination, *args, context: nil, verbose: true,
188
+ color: :green, noop: false, force: false, skip: false,
189
+ quiet: true)
158
190
  parent = args.size.nonzero? ? args.pop : nil
159
- if destination.is_a?(String)
160
- destination = { destination => [] }
191
+ if destination.is_a?(String) || destination.is_a?(Pathname)
192
+ destination = { destination.to_s => [] }
161
193
  end
162
194
 
163
195
  destination.each do |dir, files|
164
196
  path = parent.nil? ? dir : ::File.join(parent, dir)
165
197
  unless ::File.exist?(path)
166
198
  ::FileUtils.mkdir_p(path)
167
- log_status(:create, path, options.fetch(:verbose, true),
168
- options.fetch(:color, :green))
199
+ log_status(:create, path, verbose: verbose, color: color)
169
200
  end
170
201
 
171
202
  files.each do |filename, contents|
172
203
  if filename.respond_to?(:each_pair)
173
- create_directory(filename, path, options)
204
+ create_directory(filename, path, context: context,
205
+ verbose: verbose, color: color, noop: noop,
206
+ force: force, skip: skip, quiet: quiet)
174
207
  else
175
- create_file(::File.join(path, filename), contents, options)
208
+ create_file(::File.join(path, filename), contents, context: context,
209
+ verbose: verbose, color: color, noop: noop, force: force,
210
+ skip: skip, quiet: quiet)
176
211
  end
177
212
  end
178
213
  end
@@ -184,26 +219,41 @@ module TTY
184
219
 
185
220
  # Create new file if doesn't exist
186
221
  #
187
- # @param [String] relative_path
222
+ # @param [String, Pathname] relative_path
188
223
  # @param [String|nil] content
189
224
  # the content to add to file
190
- # @param [Hash] options
191
- # @option options [Symbol] :force
225
+ # @param [Object] context
226
+ # the binding to use for the template
227
+ # @param [Symbol] color
228
+ # the color name to use for logging
229
+ # @param [Boolean] force
192
230
  # forces ovewrite if conflict present
231
+ # @param [Boolean] verbose
232
+ # when true log the action status to stdout
233
+ # @param [Boolean] noop
234
+ # when true do not execute the action
235
+ # @param [Boolean] skip
236
+ # when true skip the action
237
+ # @param [Boolean] quiet
238
+ # when true leaves prompt output, otherwise clears
193
239
  #
194
240
  # @example
195
- # create_file('doc/README.md', '# Title header')
241
+ # create_file("doc/README.md", "# Title header")
196
242
  #
197
243
  # @example
198
- # create_file 'doc/README.md' do
199
- # '# Title Header'
244
+ # create_file "doc/README.md" do
245
+ # "# Title Header"
200
246
  # end
201
247
  #
202
248
  # @api public
203
- def create_file(relative_path, *args, **options, &block)
249
+ def create_file(relative_path, *args, context: nil, force: false, skip: false,
250
+ verbose: true, color: :green, noop: false, quiet: true, &block)
251
+ relative_path = relative_path.to_s
204
252
  content = block_given? ? block[] : args.join
205
253
 
206
- CreateFile.new(self, relative_path, content, options).call
254
+ CreateFile.new(self, relative_path, content, context: context, force: force,
255
+ skip: skip, verbose: verbose, color: color, noop: noop,
256
+ quiet: quiet).call
207
257
  end
208
258
  module_function :create_file
209
259
 
@@ -214,42 +264,55 @@ module TTY
214
264
  # destination running it through ERB.
215
265
  #
216
266
  # @example
217
- # copy_file 'templates/test.rb', 'app/test.rb'
267
+ # copy_file "templates/test.rb", "app/test.rb"
218
268
  #
219
269
  # @example
220
270
  # vars = OpenStruct.new
221
- # vars[:name] = 'foo'
222
- # copy_file 'templates/%name%.rb', 'app/%name%.rb', context: vars
271
+ # vars[:name] = "foo"
272
+ # copy_file "templates/%name%.rb", "app/%name%.rb", context: vars
223
273
  #
224
- # @param [Hash] options
225
- # @option options [Symbol] :context
274
+ # @param [String, Pathname] source_path
275
+ # the file path to copy file from
276
+ # @param [Object] context
226
277
  # the binding to use for the template
227
- # @option options [Symbol] :preserve
228
- # If true, the owner, group, permissions and modified time
229
- # are preserved on the copied file, defaults to false.
230
- # @option options [Symbol] :noop
231
- # If true do not execute the action.
232
- # @option options [Symbol] :verbose
233
- # If true log the action status to stdout
278
+ # @param [Boolean] preserve
279
+ # when true, the owner, group, permissions and modified time
280
+ # are preserved on the copied file, defaults to false
281
+ # @param [Boolean] noop
282
+ # when true does not execute the action
283
+ # @param [Boolean] verbose
284
+ # when true log the action status to stdout
285
+ # @param [Symbol] color
286
+ # the color name to use for logging
234
287
  #
235
288
  # @api public
236
- def copy_file(source_path, *args, **options, &block)
237
- dest_path = (args.first || source_path).sub(/\.erb$/, '')
289
+ def copy_file(source_path, *args, context: nil, force: false, skip: false,
290
+ verbose: true, color: :green, noop: false, preserve: nil, &block)
291
+ source_path = source_path.to_s
292
+ dest_path = (args.first || source_path).to_s.sub(/\.erb$/, "")
238
293
 
239
- ctx = if (vars = options[:context])
240
- vars.instance_eval('binding')
294
+ ctx = if context
295
+ context.instance_eval("binding")
241
296
  else
242
- instance_eval('binding')
297
+ instance_eval("binding")
243
298
  end
244
299
 
245
- create_file(dest_path, options) do
246
- template = ERB.new(::File.binread(source_path), nil, "-", "@output_buffer")
300
+ create_file(dest_path, context: context, force: force, skip: skip,
301
+ verbose: verbose, color: color, noop: noop) do
302
+ version = ERB.version.scan(/\d+\.\d+\.\d+/)[0]
303
+ template = if version.to_f >= 2.2
304
+ ERB.new(::File.binread(source_path), trim_mode: "-", eoutvar: "@output_buffer")
305
+ else
306
+ ERB.new(::File.binread(source_path), nil, "-", "@output_buffer")
307
+ end
247
308
  content = template.result(ctx)
248
309
  content = block[content] if block
249
310
  content
250
311
  end
251
- return unless options[:preserve]
252
- copy_metadata(source_path, dest_path, options)
312
+ return unless preserve
313
+
314
+ copy_metadata(source_path, dest_path, verbose: verbose, noop: noop,
315
+ color: color)
253
316
  end
254
317
  module_function :copy_file
255
318
 
@@ -264,7 +327,7 @@ module TTY
264
327
  def copy_metadata(src_path, dest_path, **options)
265
328
  stats = ::File.lstat(src_path)
266
329
  ::File.utime(stats.atime, stats.mtime, dest_path)
267
- chmod(dest_path, stats.mode, options)
330
+ chmod(dest_path, stats.mode, **options)
268
331
  end
269
332
  module_function :copy_metadata
270
333
 
@@ -282,43 +345,50 @@ module TTY
282
345
  # Invoking:
283
346
  # copy_directory("app", "new_app")
284
347
  # The following directory structure should be created where
285
- # name resolves to 'cli' value:
348
+ # name resolves to "cli" value:
286
349
  #
287
350
  # new_app/
288
351
  # cli.rb
289
352
  # command.rb
290
353
  # README
291
354
  #
292
- # @param [Hash[Symbol]] options
293
- # @option options [Symbol] :preserve
294
- # If true, the owner, group, permissions and modified time
295
- # are preserved on the copied file, defaults to false.
296
- # @option options [Symbol] :recursive
297
- # If false, copies only top level files, defaults to true.
298
- # @option options [Symbol] :exclude
299
- # A regex that specifies files to ignore when copying.
300
- #
301
355
  # @example
302
356
  # copy_directory("app", "new_app", recursive: false)
357
+ #
358
+ # @example
303
359
  # copy_directory("app", "new_app", exclude: /docs/)
304
360
  #
361
+ # @param [String, Pathname] source_path
362
+ # the source directory to copy files from
363
+ # @param [Boolean] preserve
364
+ # when true, the owner, group, permissions and modified time
365
+ # are preserved on the copied file, defaults to false.
366
+ # @param [Boolean] recursive
367
+ # when false, copies only top level files, defaults to true
368
+ # @param [Regexp] exclude
369
+ # a regex that specifies files to ignore when copying
370
+ #
305
371
  # @api public
306
- def copy_directory(source_path, *args, **options, &block)
372
+ def copy_directory(source_path, *args, context: nil, force: false, skip: false,
373
+ verbose: true, color: :green, noop: false, preserve: nil,
374
+ recursive: true, exclude: nil, &block)
375
+ source_path = source_path.to_s
307
376
  check_path(source_path)
308
377
  source = escape_glob_path(source_path)
309
- dest_path = args.first || source
310
- opts = {recursive: true}.merge(options)
311
- pattern = opts[:recursive] ? ::File.join(source, '**') : source
312
- glob_pattern = ::File.join(pattern, '*')
378
+ dest_path = (args.first || source).to_s
379
+ pattern = recursive ? ::File.join(source, "**") : source
380
+ glob_pattern = ::File.join(pattern, "*")
313
381
 
314
382
  Dir.glob(glob_pattern, ::File::FNM_DOTMATCH).sort.each do |file_source|
315
383
  next if ::File.directory?(file_source)
316
- next if opts[:exclude] && file_source.match(opts[:exclude])
384
+ next if exclude && file_source.match(exclude)
317
385
 
318
- dest = ::File.join(dest_path, file_source.gsub(source_path, '.'))
386
+ dest = ::File.join(dest_path, file_source.gsub(source_path, "."))
319
387
  file_dest = ::Pathname.new(dest).cleanpath.to_s
320
388
 
321
- copy_file(file_source, file_dest, **options, &block)
389
+ copy_file(file_source, file_dest, context: context, force: force,
390
+ skip: skip, verbose: verbose, color: color, noop: noop,
391
+ preserve: preserve, &block)
322
392
  end
323
393
  end
324
394
  module_function :copy_directory
@@ -328,70 +398,100 @@ module TTY
328
398
 
329
399
  # Diff files line by line
330
400
  #
331
- # @param [String] path_a
332
- # @param [String] path_b
333
- # @param [Hash[Symbol]] options
334
- # @option options [Symbol] :format
401
+ # @param [String, Pathname] path_a
402
+ # the path to the original file
403
+ # @param [String, Pathname] path_b
404
+ # the path to a new file
405
+ # @param [Symbol] format
335
406
  # the diffining output format
336
- # @option options [Symbol] :context_lines
407
+ # @param [Intger] lines
337
408
  # the number of extra lines for the context
338
- # @option options [Symbol] :threshold
409
+ # @param [Integer] threshold
339
410
  # maximum file size in bytes
340
411
  #
341
412
  # @example
342
413
  # diff(file_a, file_b, format: :old)
343
414
  #
344
415
  # @api public
345
- def diff(path_a, path_b, **options)
346
- threshold = options[:threshold] || 10_000_000
347
- output = ''
416
+ def diff(path_a, path_b, threshold: 10_000_000, format: :unified, lines: 3,
417
+ header: true, verbose: true, color: :green, noop: false)
418
+ open_tempfile_if_missing(path_a) do |file_a, temp_a|
419
+ message = check_binary_or_large(file_a, threshold)
420
+ return message if message
348
421
 
349
- open_tempfile_if_missing(path_a) do |file_a|
350
- if ::File.size(file_a) > threshold
351
- raise ArgumentError, "(file size of #{file_a.path} exceeds #{threshold} bytes, diff output suppressed)"
352
- end
353
- if binary?(file_a)
354
- raise ArgumentError, "(#{file_a.path} is binary, diff output suppressed)"
355
- end
356
- open_tempfile_if_missing(path_b) do |file_b|
357
- if binary?(file_b)
358
- raise ArgumentError, "(#{file_a.path} is binary, diff output suppressed)"
359
- end
360
- if ::File.size(file_b) > threshold
361
- return "(file size of #{file_b.path} exceeds #{threshold} bytes, diff output suppressed)"
362
- end
422
+ open_tempfile_if_missing(path_b) do |file_b, temp_b|
423
+ message = check_binary_or_large(file_b, threshold)
424
+ return message if message
363
425
 
364
- log_status(:diff, "#{file_a.path} - #{file_b.path}",
365
- options.fetch(:verbose, true), options.fetch(:color, :green))
366
- return output if options[:noop]
426
+ file_a_path, file_b_path = *diff_paths(file_a, file_b, temp_a, temp_b)
427
+ .map { |path| ::File.join(*path) }
367
428
 
368
- block_size = file_a.lstat.blksize
369
- while !file_a.eof? && !file_b.eof?
370
- output << Differ.new(file_a.read(block_size),
371
- file_b.read(block_size),
372
- options).call
373
- end
429
+ log_status(:diff, "#{file_a_path} and #{file_b_path}",
430
+ verbose: verbose, color: color)
431
+
432
+ return "" if noop
433
+
434
+ diff_files = CompareFiles.new(format: format, context_lines: lines,
435
+ header: header, verbose: verbose,
436
+ color: color, noop: noop,
437
+ diff_colors: diff_colors)
438
+
439
+ return diff_files.call(file_a, file_b, file_a_path, file_b_path)
374
440
  end
375
441
  end
376
- output
377
442
  end
378
443
  module_function :diff
379
444
 
380
445
  alias diff_files diff
381
446
  module_function :diff_files
382
447
 
448
+ # @api private
449
+ def diff_paths(file_a, file_b, temp_a, temp_b)
450
+ if temp_a && !temp_b
451
+ [["a", file_b.path], ["b", file_b.path]]
452
+ elsif !temp_a && temp_b
453
+ [["a", file_a.path], ["b", file_a.path]]
454
+ elsif temp_a && temp_b
455
+ [["a"], ["b"]]
456
+ else
457
+ [file_a.path, file_b.path]
458
+ end
459
+ end
460
+ private_module_function :diff_paths
461
+
462
+ def diff_colors
463
+ {
464
+ green: @pastel.green.detach,
465
+ red: @pastel.red.detach,
466
+ cyan: @pastel.cyan.detach
467
+ }
468
+ end
469
+ private_module_function :diff_colors
470
+
471
+ # Check if file is binary or exceeds threshold size
472
+ #
473
+ # @api private
474
+ def check_binary_or_large(file, threshold)
475
+ if binary?(file)
476
+ "#{file.path} is binary, diff output suppressed"
477
+ elsif ::File.size(file) > threshold
478
+ "file size of #{file.path} exceeds #{threshold} bytes, " \
479
+ " diff output suppressed"
480
+ end
481
+ end
482
+ private_module_function :check_binary_or_large
483
+
383
484
  # Download the content from a given address and
384
485
  # save at the given relative destination. If block
385
486
  # is provided in place of destination, the content of
386
487
  # of the uri is yielded.
387
488
  #
388
- # @param [String] uri
489
+ # @param [String, Pathname] uri
389
490
  # the URI address
390
- # @param [String] dest
491
+ # @param [String, Pathname] dest
391
492
  # the relative path to save
392
- # @param [Hash[Symbol]] options
393
- # @param options [Symbol] :limit
394
- # the limit of redirects
493
+ # @param [Integer] limit
494
+ # the number of maximium redirects
395
495
  #
396
496
  # @example
397
497
  # download_file("https://gist.github.com/4701967",
@@ -404,20 +504,21 @@ module TTY
404
504
  #
405
505
  # @api public
406
506
  def download_file(uri, *args, **options, &block)
407
- dest_path = args.first || ::File.basename(uri)
507
+ uri = uri.to_s
508
+ dest_path = (args.first || ::File.basename(uri)).to_s
408
509
 
409
510
  unless uri =~ %r{^https?\://}
410
- copy_file(uri, dest_path, options)
511
+ copy_file(uri, dest_path, **options)
411
512
  return
412
513
  end
413
514
 
414
- content = DownloadFile.new(uri, dest_path, options).call
515
+ content = DownloadFile.new(uri, dest_path, limit: options[:limit]).call
415
516
 
416
517
  if block_given?
417
518
  content = (block.arity.nonzero? ? block[content] : block[])
418
519
  end
419
520
 
420
- create_file(dest_path, content, options)
521
+ create_file(dest_path, content, **options)
421
522
  end
422
523
  module_function :download_file
423
524
 
@@ -426,88 +527,103 @@ module TTY
426
527
 
427
528
  # Prepend to a file
428
529
  #
429
- # @param [String] relative_path
530
+ # @param [String, Pathname] relative_path
430
531
  # @param [Array[String]] content
431
532
  # the content to preped to file
432
533
  #
433
534
  # @example
434
- # prepend_to_file('Gemfile', "gem 'tty'")
535
+ # prepend_to_file("Gemfile", "gem "tty"")
435
536
  #
436
537
  # @example
437
- # prepend_to_file('Gemfile') do
538
+ # prepend_to_file("Gemfile") do
438
539
  # "gem 'tty'"
439
540
  # end
440
541
  #
441
542
  # @api public
442
- def prepend_to_file(relative_path, *args, **options, &block)
443
- log_status(:prepend, relative_path, options.fetch(:verbose, true),
444
- options.fetch(:color, :green))
445
- options.merge!(before: /\A/, verbose: false)
446
- inject_into_file(relative_path, *(args << options), &block)
543
+ def prepend_to_file(relative_path, *args, verbose: true, color: :green,
544
+ force: true, noop: false, &block)
545
+ log_status(:prepend, relative_path, verbose: verbose, color: color)
546
+ inject_into_file(relative_path, *args, before: /\A/, verbose: false,
547
+ color: color, force: force, noop: noop, &block)
447
548
  end
448
549
  module_function :prepend_to_file
449
550
 
551
+ # Safely prepend to file checking if content is not already present
552
+ #
553
+ # @api public
554
+ def safe_prepend_to_file(relative_path, *args, **options, &block)
555
+ prepend_to_file(relative_path, *args, **(options.merge(force: false)), &block)
556
+ end
557
+ module_function :safe_prepend_to_file
558
+
450
559
  # Append to a file
451
560
  #
452
- # @param [String] relative_path
561
+ # @param [String, Pathname] relative_path
453
562
  # @param [Array[String]] content
454
563
  # the content to append to file
455
564
  #
456
565
  # @example
457
- # append_to_file('Gemfile', "gem 'tty'")
566
+ # append_to_file("Gemfile", "gem 'tty'")
458
567
  #
459
568
  # @example
460
- # append_to_file('Gemfile') do
569
+ # append_to_file("Gemfile") do
461
570
  # "gem 'tty'"
462
571
  # end
463
572
  #
464
573
  # @api public
465
- def append_to_file(relative_path, *args, **options, &block)
466
- log_status(:append, relative_path, options.fetch(:verbose, true),
467
- options.fetch(:color, :green))
468
- options.merge!(after: /\z/, verbose: false)
469
- inject_into_file(relative_path, *(args << options), &block)
574
+ def append_to_file(relative_path, *args, verbose: true, color: :green,
575
+ force: true, noop: false, &block)
576
+ log_status(:append, relative_path, verbose: verbose, color: color)
577
+ inject_into_file(relative_path, *args, after: /\z/, verbose: false,
578
+ force: force, noop: noop, color: color, &block)
470
579
  end
471
580
  module_function :append_to_file
472
581
 
473
582
  alias add_to_file append_to_file
474
583
  module_function :add_to_file
475
584
 
476
- # Inject content into file at a given location
585
+ # Safely append to file checking if content is not already present
477
586
  #
478
- # @param [String] relative_path
587
+ # @api public
588
+ def safe_append_to_file(relative_path, *args, **options, &block)
589
+ append_to_file(relative_path, *args, **(options.merge(force: false)), &block)
590
+ end
591
+ module_function :safe_append_to_file
592
+
593
+ # Inject content into file at a given location
479
594
  #
480
- # @param [Hash] options
481
- # @option options [Symbol] :before
595
+ # @param [String, Pathname] relative_path
596
+ # @param [String] before
482
597
  # the matching line to insert content before
483
- # @option options [Symbol] :after
598
+ # @param [String] after
484
599
  # the matching line to insert content after
485
- # @option options [Symbol] :force
600
+ # @param [Boolean] force
486
601
  # insert content more than once
487
- # @option options [Symbol] :verbose
488
- # log status
602
+ # @param [Boolean] verbose
603
+ # when true log status
604
+ # @param [Symbol] color
605
+ # the color name used in displaying this action
606
+ # @param [Boolean] noop
607
+ # when true skip perfomring this action
489
608
  #
490
609
  # @example
491
- # inject_into_file('Gemfile', "gem 'tty'", after: "gem 'rack'\n")
610
+ # inject_into_file("Gemfile", "gem 'tty'", after: "gem 'rack'\n")
492
611
  #
493
612
  # @example
494
- # inject_into_file('Gemfile', "gem 'tty'\n", "gem 'loaf'", after: "gem 'rack'\n")
613
+ # inject_into_file("Gemfile", "gem 'tty'\n", "gem 'loaf'", after: "gem 'rack'\n")
495
614
  #
496
615
  # @example
497
- # inject_into_file('Gemfile', after: "gem 'rack'\n") do
616
+ # inject_into_file("Gemfile", after: "gem 'rack'\n") do
498
617
  # "gem 'tty'\n"
499
618
  # end
500
619
  #
501
620
  # @api public
502
- def inject_into_file(relative_path, *args, **options, &block)
621
+ def inject_into_file(relative_path, *args, verbose: true, color: :green,
622
+ after: nil, before: nil, force: true, noop: false, &block)
503
623
  check_path(relative_path)
504
624
  replacement = block_given? ? block[] : args.join
505
625
 
506
- flag, match = if options.key?(:after)
507
- [:after, options.delete(:after)]
508
- else
509
- [:before, options.delete(:before)]
510
- end
626
+ flag, match = after ? [:after, after] : [:before, before]
511
627
 
512
628
  match = match.is_a?(Regexp) ? match : Regexp.escape(match)
513
629
  content = if flag == :after
@@ -516,30 +632,41 @@ module TTY
516
632
  replacement + '\0'
517
633
  end
518
634
 
519
- log_status(:inject, relative_path, options.fetch(:verbose, true),
520
- options.fetch(:color, :green))
521
- replace_in_file(relative_path, /#{match}/, content,
522
- options.merge(verbose: false))
635
+ log_status(:inject, relative_path, verbose: verbose, color: color)
636
+ replace_in_file(relative_path, /#{match}/, content, verbose: false,
637
+ color: color, force: force, noop: noop)
523
638
  end
524
639
  module_function :inject_into_file
525
640
 
526
641
  alias insert_into_file inject_into_file
527
642
  module_function :insert_into_file
528
643
 
644
+ # Safely prepend to file checking if content is not already present
645
+ #
646
+ # @api public
647
+ def safe_inject_into_file(relative_path, *args, **options, &block)
648
+ inject_into_file(relative_path, *args, **(options.merge(force: false)), &block)
649
+ end
650
+ module_function :safe_inject_into_file
651
+
529
652
  # Replace content of a file matching string, returning false
530
653
  # when no substitutions were performed, true otherwise.
531
654
  #
532
- # @options [Hash[String]] options
533
- # @option options [Symbol] :force
655
+ # @param [String, Pathname] relative_path
656
+ # @param [Boolean] force
534
657
  # replace content even if present
535
- # @option options [Symbol] :verbose
536
- # log status
658
+ # @param [Boolean] verbose
659
+ # when true log status to stdout
660
+ # @param [Boolean] noop
661
+ # when true skip executing this action
662
+ # @param [Symbol] color
663
+ # the name of the color used for displaying action
537
664
  #
538
665
  # @example
539
- # replace_in_file('Gemfile', /gem 'rails'/, "gem 'hanami'")
666
+ # replace_in_file("Gemfile", /gem 'rails'/, "gem 'hanami'")
540
667
  #
541
668
  # @example
542
- # replace_in_file('Gemfile', /gem 'rails'/) do |match|
669
+ # replace_in_file("Gemfile", /gem 'rails'/) do |match|
543
670
  # match = "gem 'hanami'"
544
671
  # end
545
672
  #
@@ -547,21 +674,21 @@ module TTY
547
674
  # true when replaced content, false otherwise
548
675
  #
549
676
  # @api public
550
- def replace_in_file(relative_path, *args, **options, &block)
677
+ def replace_in_file(relative_path, *args, verbose: true, color: :green,
678
+ noop: false, force: true, &block)
551
679
  check_path(relative_path)
552
- contents = IO.read(relative_path)
553
- replacement = (block ? block[] : args[1..-1].join).gsub('\0', '')
680
+ contents = ::File.read(relative_path)
681
+ replacement = (block ? block[] : args[1..-1].join).gsub('\0', "")
554
682
  match = Regexp.escape(replacement)
555
683
  status = nil
556
684
 
557
- log_status(:replace, relative_path, options.fetch(:verbose, true),
558
- options.fetch(:color, :green))
559
- return false if options[:noop]
685
+ log_status(:replace, relative_path, verbose: verbose, color: color)
686
+ return false if noop
560
687
 
561
- if !(contents =~ /^#{match}(\r?\n)*/m) || options[:force]
688
+ if force || !(contents =~ /^#{match}(\r?\n)*/m)
562
689
  status = contents.gsub!(*args, &block)
563
690
  if !status.nil?
564
- ::File.open(relative_path, 'wb') do |file|
691
+ ::File.open(relative_path, "w") do |file|
565
692
  file.write(contents)
566
693
  end
567
694
  end
@@ -575,70 +702,73 @@ module TTY
575
702
 
576
703
  # Remove a file or a directory at specified relative path.
577
704
  #
578
- # @param [Hash[:Symbol]] options
579
- # @option options [Symbol] :noop
580
- # pretend removing file
581
- # @option options [Symbol] :force
582
- # remove file ignoring errors
583
- # @option options [Symbol] :verbose
584
- # log status
705
+ # @param [String, Pathname] relative_path
706
+ # @param [Boolean] noop
707
+ # when true pretend to remove file
708
+ # @param [Boolean] force
709
+ # when true remove file ignoring errors
710
+ # @param [Boolean] verbose
711
+ # when true log status
712
+ # @param [Boolean] secure
713
+ # when true check for secure removing
585
714
  #
586
715
  # @example
587
- # remove_file 'doc/README.md'
716
+ # remove_file "doc/README.md"
588
717
  #
589
718
  # @api public
590
- def remove_file(relative_path, *args, **options)
591
- log_status(:remove, relative_path, options.fetch(:verbose, true),
592
- options.fetch(:color, :red))
719
+ def remove_file(relative_path, *args, verbose: true, color: :red, noop: false,
720
+ force: nil, secure: true)
721
+ relative_path = relative_path.to_s
722
+ log_status(:remove, relative_path, verbose: verbose, color: color)
593
723
 
594
- return if options[:noop]
724
+ return if noop || !::File.exist?(relative_path)
595
725
 
596
- ::FileUtils.rm_r(relative_path, force: options[:force], secure: true)
726
+ ::FileUtils.rm_r(relative_path, force: force, secure: secure)
597
727
  end
598
728
  module_function :remove_file
599
729
 
600
730
  # Provide the last number of lines from a file
601
731
  #
602
- # @param [String] relative_path
732
+ # @param [String, Pathname] relative_path
603
733
  # the relative path to a file
604
- #
605
- # @param [Integer] num_lines
734
+ # @param [Integer] lines
606
735
  # the number of lines to return from file
736
+ # @param [Integer] chunk_size
737
+ # the size of the chunk to read
607
738
  #
608
739
  # @example
609
- # tail_file 'filename'
610
- # # => ['line 19', 'line20', ... ]
740
+ # tail_file "filename"
741
+ # # => ["line 19", "line20", ... ]
611
742
  #
612
743
  # @example
613
- # tail_file 'filename', 15
614
- # # => ['line 19', 'line20', ... ]
744
+ # tail_file "filename", lines: 15
745
+ # # => ["line 19", "line20", ... ]
615
746
  #
616
747
  # @return [Array[String]]
617
748
  #
618
749
  # @api public
619
- def tail_file(relative_path, num_lines = 10, **options, &block)
620
- file = ::File.open(relative_path)
621
- chunk_size = options.fetch(:chunk_size, 512)
622
- line_sep = $/
623
- lines = []
750
+ def tail_file(relative_path, lines: 10, chunk_size: 512, &block)
751
+ file = ::File.open(relative_path)
752
+ line_sep = $/
753
+ output = []
624
754
  newline_count = 0
625
755
 
626
756
  ReadBackwardFile.new(file, chunk_size).each_chunk do |chunk|
627
757
  # look for newline index counting from right of chunk
628
758
  while (nl_index = chunk.rindex(line_sep, (nl_index || chunk.size) - 1))
629
759
  newline_count += 1
630
- break if newline_count > num_lines || nl_index.zero?
760
+ break if newline_count > lines || nl_index.zero?
631
761
  end
632
762
 
633
- if newline_count > num_lines
634
- lines.insert(0, chunk[(nl_index + 1)..-1])
763
+ if newline_count > lines
764
+ output.insert(0, chunk[(nl_index + 1)..-1])
635
765
  break
636
766
  else
637
- lines.insert(0, chunk)
767
+ output.insert(0, chunk)
638
768
  end
639
769
  end
640
770
 
641
- lines.join.split(line_sep).each(&block).to_a
771
+ output.join.split(line_sep).each(&block).to_a
642
772
  end
643
773
  module_function :tail_file
644
774
 
@@ -667,6 +797,7 @@ module TTY
667
797
  # @api private
668
798
  def check_path(path)
669
799
  return if ::File.exist?(path)
800
+
670
801
  raise InvalidPathError, "File path \"#{path}\" does not exist."
671
802
  end
672
803
  private_module_function :check_path
@@ -682,7 +813,7 @@ module TTY
682
813
  # Log file operation
683
814
  #
684
815
  # @api private
685
- def log_status(cmd, message, verbose, color = false)
816
+ def log_status(cmd, message, verbose: true, color: false)
686
817
  return unless verbose
687
818
 
688
819
  cmd = cmd.to_s.rjust(12)
@@ -710,11 +841,11 @@ module TTY
710
841
  if ::FileTest.file?(object)
711
842
  ::File.open(object, &block)
712
843
  else
713
- tempfile = Tempfile.new('tty-file-diff')
844
+ tempfile = Tempfile.new("tty-file-diff")
714
845
  tempfile << object
715
846
  tempfile.rewind
716
847
 
717
- block[tempfile]
848
+ block[tempfile, ::File.basename(tempfile)]
718
849
 
719
850
  unless tempfile.nil?
720
851
  tempfile.close
@@ -723,5 +854,15 @@ module TTY
723
854
  end
724
855
  end
725
856
  private_module_function :open_tempfile_if_missing
857
+
858
+ # Check if IO is attached to a terminal
859
+ #
860
+ # return [Boolean]
861
+ #
862
+ # @api public
863
+ def tty?
864
+ @output.respond_to?(:tty?) && @output.tty?
865
+ end
866
+ private_module_function :tty?
726
867
  end # File
727
868
  end # TTY