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.
- 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
|