tty-file 0.6.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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