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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +63 -2
- data/README.md +191 -80
- data/lib/tty-file.rb +1 -3
- data/lib/tty/file.rb +383 -242
- data/lib/tty/file/compare_files.rb +70 -0
- data/lib/tty/file/create_file.rb +35 -19
- data/lib/tty/file/differ.rb +47 -25
- data/lib/tty/file/digest_file.rb +5 -5
- data/lib/tty/file/download_file.rb +10 -10
- data/lib/tty/file/read_backward_file.rb +0 -1
- data/lib/tty/file/version.rb +2 -2
- metadata +32 -50
- data/.gitignore +0 -10
- data/.rspec +0 -3
- data/.travis.yml +0 -26
- data/CODE_OF_CONDUCT.md +0 -49
- data/Gemfile +0 -9
- data/Rakefile +0 -10
- data/appveyor.yml +0 -26
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/tasks/console.rake +0 -10
- data/tasks/coverage.rake +0 -11
- data/tasks/spec.rake +0 -29
- data/tty-file.gemspec +0 -32
data/lib/tty-file.rb
CHANGED
data/lib/tty/file.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require 'pathname'
|
3
|
+
require "pastel"
|
4
|
+
require "erb"
|
5
|
+
require "tempfile"
|
6
|
+
require "pathname"
|
8
7
|
|
9
|
-
require_relative
|
10
|
-
require_relative
|
11
|
-
require_relative
|
12
|
-
require_relative
|
13
|
-
require_relative
|
14
|
-
require_relative
|
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?(
|
44
|
+
# binary?("Gemfile") # => false
|
46
45
|
#
|
47
46
|
# @example
|
48
|
-
# binary?(
|
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 =
|
58
|
-
|
56
|
+
buffer = read_to_char(relative_path, bytes, 0)
|
57
|
+
|
59
58
|
begin
|
60
|
-
|
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 [
|
74
|
-
#
|
75
|
-
# No operation
|
101
|
+
# @param [Boolean] noop
|
102
|
+
# when true skip this action
|
76
103
|
#
|
77
104
|
# @example
|
78
|
-
# checksum_file(
|
105
|
+
# checksum_file("/path/to/file")
|
79
106
|
#
|
80
107
|
# @example
|
81
|
-
# checksum_file(
|
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,
|
88
|
-
mode = args.size.zero? ?
|
89
|
-
digester = DigestFile.new(source, mode
|
90
|
-
digester.call unless
|
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
|
-
#
|
99
|
-
# @
|
100
|
-
#
|
101
|
-
# @
|
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(
|
135
|
+
# chmod("Gemfile", 0755)
|
105
136
|
#
|
106
137
|
# @example
|
107
|
-
# chmod(
|
138
|
+
# chmod("Gemilfe", TTY::File::U_R | TTY::File::U_W)
|
108
139
|
#
|
109
140
|
# @example
|
110
|
-
# chmod(
|
141
|
+
# chmod("Gemfile", "u+x,g+x")
|
111
142
|
#
|
112
143
|
# @api public
|
113
|
-
def chmod(relative_path, permissions,
|
114
|
-
|
115
|
-
|
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(
|
168
|
+
# create_directory("/path/to/dir")
|
139
169
|
#
|
140
170
|
# @example
|
141
171
|
# tree =
|
142
|
-
#
|
143
|
-
#
|
144
|
-
# [
|
145
|
-
#
|
146
|
-
#
|
147
|
-
# [
|
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
|
-
#
|
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,
|
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,
|
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,
|
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,
|
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 [
|
191
|
-
#
|
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(
|
241
|
+
# create_file("doc/README.md", "# Title header")
|
196
242
|
#
|
197
243
|
# @example
|
198
|
-
# create_file
|
199
|
-
#
|
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,
|
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,
|
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
|
267
|
+
# copy_file "templates/test.rb", "app/test.rb"
|
218
268
|
#
|
219
269
|
# @example
|
220
270
|
# vars = OpenStruct.new
|
221
|
-
# vars[:name] =
|
222
|
-
# copy_file
|
271
|
+
# vars[:name] = "foo"
|
272
|
+
# copy_file "templates/%name%.rb", "app/%name%.rb", context: vars
|
223
273
|
#
|
224
|
-
# @param [
|
225
|
-
#
|
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
|
-
# @
|
228
|
-
#
|
229
|
-
# are preserved on the copied file, defaults to false
|
230
|
-
# @
|
231
|
-
#
|
232
|
-
# @
|
233
|
-
#
|
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,
|
237
|
-
|
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
|
240
|
-
|
294
|
+
ctx = if context
|
295
|
+
context.instance_eval("binding")
|
241
296
|
else
|
242
|
-
instance_eval(
|
297
|
+
instance_eval("binding")
|
243
298
|
end
|
244
299
|
|
245
|
-
create_file(dest_path,
|
246
|
-
|
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
|
252
|
-
|
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
|
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,
|
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
|
-
|
311
|
-
|
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
|
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,
|
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
|
-
#
|
333
|
-
# @param [
|
334
|
-
#
|
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
|
-
# @
|
407
|
+
# @param [Intger] lines
|
337
408
|
# the number of extra lines for the context
|
338
|
-
# @
|
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,
|
346
|
-
|
347
|
-
|
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
|
-
|
350
|
-
|
351
|
-
|
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
|
-
|
365
|
-
|
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
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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 [
|
393
|
-
#
|
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
|
-
|
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(
|
535
|
+
# prepend_to_file("Gemfile", "gem "tty"")
|
435
536
|
#
|
436
537
|
# @example
|
437
|
-
# prepend_to_file(
|
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,
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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(
|
566
|
+
# append_to_file("Gemfile", "gem 'tty'")
|
458
567
|
#
|
459
568
|
# @example
|
460
|
-
# append_to_file(
|
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,
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
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
|
-
#
|
585
|
+
# Safely append to file checking if content is not already present
|
477
586
|
#
|
478
|
-
# @
|
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 [
|
481
|
-
# @
|
595
|
+
# @param [String, Pathname] relative_path
|
596
|
+
# @param [String] before
|
482
597
|
# the matching line to insert content before
|
483
|
-
# @
|
598
|
+
# @param [String] after
|
484
599
|
# the matching line to insert content after
|
485
|
-
# @
|
600
|
+
# @param [Boolean] force
|
486
601
|
# insert content more than once
|
487
|
-
# @
|
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(
|
610
|
+
# inject_into_file("Gemfile", "gem 'tty'", after: "gem 'rack'\n")
|
492
611
|
#
|
493
612
|
# @example
|
494
|
-
# inject_into_file(
|
613
|
+
# inject_into_file("Gemfile", "gem 'tty'\n", "gem 'loaf'", after: "gem 'rack'\n")
|
495
614
|
#
|
496
615
|
# @example
|
497
|
-
# inject_into_file(
|
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,
|
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 =
|
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,
|
520
|
-
|
521
|
-
|
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
|
-
# @
|
533
|
-
# @
|
655
|
+
# @param [String, Pathname] relative_path
|
656
|
+
# @param [Boolean] force
|
534
657
|
# replace content even if present
|
535
|
-
# @
|
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(
|
666
|
+
# replace_in_file("Gemfile", /gem 'rails'/, "gem 'hanami'")
|
540
667
|
#
|
541
668
|
# @example
|
542
|
-
# replace_in_file(
|
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,
|
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 =
|
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,
|
558
|
-
|
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)
|
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,
|
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 [
|
579
|
-
# @
|
580
|
-
# pretend
|
581
|
-
# @
|
582
|
-
# remove file ignoring errors
|
583
|
-
# @
|
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
|
716
|
+
# remove_file "doc/README.md"
|
588
717
|
#
|
589
718
|
# @api public
|
590
|
-
def remove_file(relative_path, *args,
|
591
|
-
|
592
|
-
|
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
|
724
|
+
return if noop || !::File.exist?(relative_path)
|
595
725
|
|
596
|
-
::FileUtils.rm_r(relative_path, force:
|
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
|
610
|
-
# # => [
|
740
|
+
# tail_file "filename"
|
741
|
+
# # => ["line 19", "line20", ... ]
|
611
742
|
#
|
612
743
|
# @example
|
613
|
-
# tail_file
|
614
|
-
# # => [
|
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,
|
620
|
-
file
|
621
|
-
|
622
|
-
|
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 >
|
760
|
+
break if newline_count > lines || nl_index.zero?
|
631
761
|
end
|
632
762
|
|
633
|
-
if newline_count >
|
634
|
-
|
763
|
+
if newline_count > lines
|
764
|
+
output.insert(0, chunk[(nl_index + 1)..-1])
|
635
765
|
break
|
636
766
|
else
|
637
|
-
|
767
|
+
output.insert(0, chunk)
|
638
768
|
end
|
639
769
|
end
|
640
770
|
|
641
|
-
|
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
|
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(
|
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
|