cli-kit 3.0.0 → 4.0.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/.github/CODEOWNERS +1 -0
- data/.github/dependabot.yml +10 -0
- data/.github/probots.yml +2 -0
- data/.github/workflows/ruby.yml +32 -0
- data/.rubocop.yml +13 -15
- data/Gemfile +3 -2
- data/Gemfile.lock +35 -30
- data/README.md +102 -4
- data/Rakefile +27 -0
- data/TODO.md +5 -0
- data/bin/console +3 -3
- data/bin/test_gen +31 -0
- data/bin/testunit +2 -2
- data/cli-kit.gemspec +8 -7
- data/dev.yml +1 -1
- data/examples/README.md +21 -0
- data/examples/minimal/example.rb +22 -0
- data/examples/single-file/example.rb +71 -0
- data/examples/todo-list/README.md +1 -0
- data/gen/lib/gen/commands/help.rb +4 -4
- data/gen/lib/gen/generator.rb +12 -10
- data/gen/lib/gen.rb +5 -1
- data/gen/template/Gemfile +6 -0
- data/gen/template/bin/testunit +23 -0
- data/gen/template/bin/update-deps +2 -1
- data/gen/template/lib/__app__/commands/example.rb +0 -1
- data/gen/template/test/example_test.rb +17 -0
- data/gen/template/test/test_helper.rb +22 -0
- data/lib/cli/kit/base_command.rb +14 -8
- data/lib/cli/kit/command_registry.rb +1 -1
- data/lib/cli/kit/config.rb +39 -4
- data/lib/cli/kit/error_handler.rb +56 -36
- data/lib/cli/kit/executor.rb +39 -10
- data/lib/cli/kit/ini.rb +16 -7
- data/lib/cli/kit/logger.rb +82 -0
- data/lib/cli/kit/resolver.rb +2 -2
- data/lib/cli/kit/support/test_helper.rb +244 -0
- data/lib/cli/kit/support.rb +9 -0
- data/lib/cli/kit/system.rb +61 -22
- data/lib/cli/kit/util.rb +189 -0
- data/lib/cli/kit/version.rb +1 -1
- data/lib/cli/kit.rb +4 -1
- metadata +36 -20
- data/.travis.yml +0 -5
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
module CLI
|
|
2
|
+
module Kit
|
|
3
|
+
module Support
|
|
4
|
+
module TestHelper
|
|
5
|
+
def setup
|
|
6
|
+
super
|
|
7
|
+
CLI::Kit::System.reset!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def assert_all_commands_run(should_raise: true)
|
|
11
|
+
errors = CLI::Kit::System.error_message
|
|
12
|
+
CLI::Kit::System.reset!
|
|
13
|
+
assert(false, errors) if should_raise && !errors.nil?
|
|
14
|
+
errors
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def teardown
|
|
18
|
+
super
|
|
19
|
+
assert_all_commands_run
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module FakeConfig
|
|
23
|
+
require 'tmpdir'
|
|
24
|
+
require 'fileutils'
|
|
25
|
+
|
|
26
|
+
def setup
|
|
27
|
+
super
|
|
28
|
+
@tmpdir = Dir.mktmpdir
|
|
29
|
+
@prev_xdg = ENV['XDG_CONFIG_HOME']
|
|
30
|
+
ENV['XDG_CONFIG_HOME'] = @tmpdir
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def teardown
|
|
34
|
+
FileUtils.rm_rf(@tmpdir)
|
|
35
|
+
ENV['XDG_CONFIG_HOME'] = @prev_xdg
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class FakeSuccess
|
|
41
|
+
def initialize(success)
|
|
42
|
+
@success = success
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def success?
|
|
46
|
+
@success
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module ::CLI
|
|
51
|
+
module Kit
|
|
52
|
+
module System
|
|
53
|
+
class << self
|
|
54
|
+
alias_method :original_system, :system
|
|
55
|
+
def system(*a, sudo: false, env: {}, **kwargs)
|
|
56
|
+
expected_command = expected_command(*a, sudo: sudo, env: env)
|
|
57
|
+
|
|
58
|
+
# In the case of an unexpected command, expected_command will be nil
|
|
59
|
+
return FakeSuccess.new(false) if expected_command.nil?
|
|
60
|
+
|
|
61
|
+
# Otherwise handle the command
|
|
62
|
+
if expected_command[:allow]
|
|
63
|
+
original_system(*a, sudo: sudo, env: env, **kwargs)
|
|
64
|
+
else
|
|
65
|
+
FakeSuccess.new(expected_command[:success])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
alias_method :original_capture2, :capture2
|
|
70
|
+
def capture2(*a, sudo: false, env: {}, **kwargs)
|
|
71
|
+
expected_command = expected_command(*a, sudo: sudo, env: env)
|
|
72
|
+
|
|
73
|
+
# In the case of an unexpected command, expected_command will be nil
|
|
74
|
+
return [nil, FakeSuccess.new(false)] if expected_command.nil?
|
|
75
|
+
|
|
76
|
+
# Otherwise handle the command
|
|
77
|
+
if expected_command[:allow]
|
|
78
|
+
original_capture2(*a, sudo: sudo, env: env, **kwargs)
|
|
79
|
+
else
|
|
80
|
+
[
|
|
81
|
+
expected_command[:stdout],
|
|
82
|
+
FakeSuccess.new(expected_command[:success]),
|
|
83
|
+
]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
alias_method :original_capture2e, :capture2e
|
|
88
|
+
def capture2e(*a, sudo: false, env: {}, **kwargs)
|
|
89
|
+
expected_command = expected_command(*a, sudo: sudo, env: env)
|
|
90
|
+
|
|
91
|
+
# In the case of an unexpected command, expected_command will be nil
|
|
92
|
+
return [nil, FakeSuccess.new(false)] if expected_command.nil?
|
|
93
|
+
|
|
94
|
+
# Otherwise handle the command
|
|
95
|
+
if expected_command[:allow]
|
|
96
|
+
original_capture2ecapture2e(*a, sudo: sudo, env: env, **kwargs)
|
|
97
|
+
else
|
|
98
|
+
[
|
|
99
|
+
expected_command[:stdout],
|
|
100
|
+
FakeSuccess.new(expected_command[:success]),
|
|
101
|
+
]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
alias_method :original_capture3, :capture3
|
|
106
|
+
def capture3(*a, sudo: false, env: {}, **kwargs)
|
|
107
|
+
expected_command = expected_command(*a, sudo: sudo, env: env)
|
|
108
|
+
|
|
109
|
+
# In the case of an unexpected command, expected_command will be nil
|
|
110
|
+
return [nil, nil, FakeSuccess.new(false)] if expected_command.nil?
|
|
111
|
+
|
|
112
|
+
# Otherwise handle the command
|
|
113
|
+
if expected_command[:allow]
|
|
114
|
+
original_capture3(*a, sudo: sudo, env: env, **kwargs)
|
|
115
|
+
else
|
|
116
|
+
[
|
|
117
|
+
expected_command[:stdout],
|
|
118
|
+
expected_command[:stderr],
|
|
119
|
+
FakeSuccess.new(expected_command[:success]),
|
|
120
|
+
]
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Sets up an expectation for a command and stubs out the call (unless allow is true)
|
|
125
|
+
#
|
|
126
|
+
# #### Parameters
|
|
127
|
+
# `*a` : the command, represented as a splat
|
|
128
|
+
# `stdout` : stdout to stub the command with (defaults to empty string)
|
|
129
|
+
# `stderr` : stderr to stub the command with (defaults to empty string)
|
|
130
|
+
# `allow` : allow determines if the command will be actually run, or stubbed. Defaults to nil (stub)
|
|
131
|
+
# `success` : success status to stub the command with (Defaults to nil)
|
|
132
|
+
# `sudo` : expectation of sudo being set or not (defaults to false)
|
|
133
|
+
# `env` : expectation of env being set or not (defaults to {})
|
|
134
|
+
#
|
|
135
|
+
# Note: Must set allow or success
|
|
136
|
+
#
|
|
137
|
+
def fake(*a, stdout: '', stderr: '', allow: nil, success: nil, sudo: false, env: {})
|
|
138
|
+
raise ArgumentError, 'success or allow must be set' if success.nil? && allow.nil?
|
|
139
|
+
|
|
140
|
+
@delegate_open3 ||= {}
|
|
141
|
+
@delegate_open3[a.join(' ')] = {
|
|
142
|
+
expected: {
|
|
143
|
+
sudo: sudo,
|
|
144
|
+
env: env,
|
|
145
|
+
},
|
|
146
|
+
actual: {
|
|
147
|
+
sudo: nil,
|
|
148
|
+
env: nil,
|
|
149
|
+
},
|
|
150
|
+
stdout: stdout,
|
|
151
|
+
stderr: stderr,
|
|
152
|
+
allow: allow,
|
|
153
|
+
success: success,
|
|
154
|
+
run: false,
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Resets the faked commands
|
|
159
|
+
#
|
|
160
|
+
def reset!
|
|
161
|
+
@delegate_open3 = {}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Returns the errors associated to a test run
|
|
165
|
+
#
|
|
166
|
+
# #### Returns
|
|
167
|
+
# `errors` (String) a string representing errors found on this run, nil if none
|
|
168
|
+
def error_message
|
|
169
|
+
errors = {
|
|
170
|
+
unexpected: [],
|
|
171
|
+
not_run: [],
|
|
172
|
+
other: {},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@delegate_open3.each do |cmd, opts|
|
|
176
|
+
if opts[:unexpected]
|
|
177
|
+
errors[:unexpected] << cmd
|
|
178
|
+
elsif opts[:run]
|
|
179
|
+
error = []
|
|
180
|
+
|
|
181
|
+
if opts[:expected][:sudo] != opts[:actual][:sudo]
|
|
182
|
+
error << "- sudo was supposed to be #{opts[:expected][:sudo]} but was #{opts[:actual][:sudo]}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if opts[:expected][:env] != opts[:actual][:env]
|
|
186
|
+
error << "- env was supposed to be #{opts[:expected][:env]} but was #{opts[:actual][:env]}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
errors[:other][cmd] = error.join("\n") unless error.empty?
|
|
190
|
+
else
|
|
191
|
+
errors[:not_run] << cmd
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
final_error = []
|
|
196
|
+
|
|
197
|
+
unless errors[:unexpected].empty?
|
|
198
|
+
final_error << CLI::UI.fmt(<<~EOF)
|
|
199
|
+
{{bold:Unexpected command invocations:}}
|
|
200
|
+
{{command:#{errors[:unexpected].join("\n")}}}
|
|
201
|
+
EOF
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
unless errors[:not_run].empty?
|
|
205
|
+
final_error << CLI::UI.fmt(<<~EOF)
|
|
206
|
+
{{bold:Expected commands were not run:}}
|
|
207
|
+
{{command:#{errors[:not_run].join("\n")}}}
|
|
208
|
+
EOF
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
unless errors[:other].empty?
|
|
212
|
+
final_error << CLI::UI.fmt(<<~EOF)
|
|
213
|
+
{{bold:Commands were not run as expected:}}
|
|
214
|
+
#{errors[:other].map { |cmd, msg| "{{command:#{cmd}}}\n#{msg}" }.join("\n\n")}
|
|
215
|
+
EOF
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
return nil if final_error.empty?
|
|
219
|
+
"\n" + final_error.join("\n") # Initial new line for formatting reasons
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
def expected_command(*a, sudo: raise, env: raise)
|
|
225
|
+
expected_cmd = @delegate_open3[a.join(' ')]
|
|
226
|
+
|
|
227
|
+
if expected_cmd.nil?
|
|
228
|
+
@delegate_open3[a.join(' ')] = { unexpected: true }
|
|
229
|
+
return nil
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
expected_cmd[:run] = true
|
|
233
|
+
expected_cmd[:actual][:sudo] = sudo
|
|
234
|
+
expected_cmd[:actual][:env] = env
|
|
235
|
+
expected_cmd
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
data/lib/cli/kit/system.rb
CHANGED
|
@@ -6,7 +6,7 @@ require 'English'
|
|
|
6
6
|
module CLI
|
|
7
7
|
module Kit
|
|
8
8
|
module System
|
|
9
|
-
SUDO_PROMPT = CLI::UI.fmt(
|
|
9
|
+
SUDO_PROMPT = CLI::UI.fmt('{{info:(sudo)}} Password: ')
|
|
10
10
|
class << self
|
|
11
11
|
# Ask for sudo access with a message explaning the need for it
|
|
12
12
|
# Will make subsequent commands capable of running with sudo for a period of time
|
|
@@ -19,7 +19,7 @@ module CLI
|
|
|
19
19
|
#
|
|
20
20
|
def sudo_reason(msg)
|
|
21
21
|
# See if sudo has a cached password
|
|
22
|
-
|
|
22
|
+
%x(env SUDO_ASKPASS=/usr/bin/false sudo -A true)
|
|
23
23
|
return if $CHILD_STATUS.success?
|
|
24
24
|
CLI::UI.with_frame_color(:blue) do
|
|
25
25
|
puts(CLI::UI.fmt("{{i}} #{msg}"))
|
|
@@ -90,6 +90,18 @@ module CLI
|
|
|
90
90
|
delegate_open3(*a, sudo: sudo, env: env, method: :capture3, **kwargs)
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
def popen2(*a, sudo: false, env: ENV, **kwargs, &block)
|
|
94
|
+
delegate_open3(*a, sudo: sudo, env: env, method: :popen2, **kwargs, &block)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def popen2e(*a, sudo: false, env: ENV, **kwargs, &block)
|
|
98
|
+
delegate_open3(*a, sudo: sudo, env: env, method: :popen2e, **kwargs, &block)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def popen3(*a, sudo: false, env: ENV, **kwargs, &block)
|
|
102
|
+
delegate_open3(*a, sudo: sudo, env: env, method: :popen3, **kwargs, &block)
|
|
103
|
+
end
|
|
104
|
+
|
|
93
105
|
# Execute a command in the user's environment
|
|
94
106
|
# Outputs result of the command without capturing it
|
|
95
107
|
#
|
|
@@ -100,7 +112,7 @@ module CLI
|
|
|
100
112
|
# - `**kwargs`: additional keyword arguments to pass to Process.spawn
|
|
101
113
|
#
|
|
102
114
|
# #### Returns
|
|
103
|
-
# - `status`:
|
|
115
|
+
# - `status`: The `Process:Status` result for the command execution
|
|
104
116
|
#
|
|
105
117
|
# #### Usage
|
|
106
118
|
# `stat = CLI::Kit::System.system('ls', 'a_folder')`
|
|
@@ -116,11 +128,15 @@ module CLI
|
|
|
116
128
|
err_w.close
|
|
117
129
|
|
|
118
130
|
handlers = if block_given?
|
|
119
|
-
{
|
|
120
|
-
|
|
131
|
+
{
|
|
132
|
+
out_r => ->(data) { yield(data.force_encoding(Encoding::UTF_8), '') },
|
|
133
|
+
err_r => ->(data) { yield('', data.force_encoding(Encoding::UTF_8)) },
|
|
134
|
+
}
|
|
121
135
|
else
|
|
122
|
-
{
|
|
123
|
-
|
|
136
|
+
{
|
|
137
|
+
out_r => ->(data) { STDOUT.write(data) },
|
|
138
|
+
err_r => ->(data) { STDOUT.write(data) },
|
|
139
|
+
}
|
|
124
140
|
end
|
|
125
141
|
|
|
126
142
|
previous_trailing = Hash.new('')
|
|
@@ -130,13 +146,11 @@ module CLI
|
|
|
130
146
|
|
|
131
147
|
readers, = IO.select(ios)
|
|
132
148
|
readers.each do |io|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
io.close
|
|
139
|
-
end
|
|
149
|
+
data, trailing = split_partial_characters(io.readpartial(4096))
|
|
150
|
+
handlers[io].call(previous_trailing[io] + data)
|
|
151
|
+
previous_trailing[io] = trailing
|
|
152
|
+
rescue IOError
|
|
153
|
+
io.close
|
|
140
154
|
end
|
|
141
155
|
end
|
|
142
156
|
|
|
@@ -163,6 +177,14 @@ module CLI
|
|
|
163
177
|
[data.byteslice(0...partial_character_index), data.byteslice(partial_character_index..-1)]
|
|
164
178
|
end
|
|
165
179
|
|
|
180
|
+
def os
|
|
181
|
+
return :mac if /darwin/.match(RUBY_PLATFORM)
|
|
182
|
+
return :linux if /linux/.match(RUBY_PLATFORM)
|
|
183
|
+
return :windows if /mingw32/.match(RUBY_PLATFORM)
|
|
184
|
+
|
|
185
|
+
raise "Could not determine OS from platform #{RUBY_PLATFORM}"
|
|
186
|
+
end
|
|
187
|
+
|
|
166
188
|
private
|
|
167
189
|
|
|
168
190
|
def apply_sudo(*a, sudo)
|
|
@@ -171,11 +193,11 @@ module CLI
|
|
|
171
193
|
a
|
|
172
194
|
end
|
|
173
195
|
|
|
174
|
-
def delegate_open3(*a, sudo: raise, env: raise, method: raise, **kwargs)
|
|
196
|
+
def delegate_open3(*a, sudo: raise, env: raise, method: raise, **kwargs, &block)
|
|
175
197
|
a = apply_sudo(*a, sudo)
|
|
176
|
-
Open3.send(method, env, *resolve_path(a, env), **kwargs)
|
|
198
|
+
Open3.send(method, env, *resolve_path(a, env), **kwargs, &block)
|
|
177
199
|
rescue Errno::EINTR
|
|
178
|
-
raise(Errno::EINTR, "command interrupted: #{a.join(
|
|
200
|
+
raise(Errno::EINTR, "command interrupted: #{a.join(" ")}")
|
|
179
201
|
end
|
|
180
202
|
|
|
181
203
|
# Ruby resolves the program to execute using its own PATH, but we want it to
|
|
@@ -189,14 +211,31 @@ module CLI
|
|
|
189
211
|
# See https://github.com/Shopify/dev/pull/625 for more details.
|
|
190
212
|
def resolve_path(a, env)
|
|
191
213
|
# If only one argument was provided, make sure it's interpreted by a shell.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
214
|
+
if a.size == 1
|
|
215
|
+
if os == :windows
|
|
216
|
+
return ['break && ' + a[0]]
|
|
217
|
+
else
|
|
218
|
+
return ['true ; ' + a[0]]
|
|
219
|
+
end
|
|
196
220
|
end
|
|
197
|
-
a
|
|
221
|
+
return a if a.first.include?('/')
|
|
222
|
+
|
|
223
|
+
item = which(a.first, env)
|
|
224
|
+
a[0] = item if item
|
|
198
225
|
a
|
|
199
226
|
end
|
|
227
|
+
|
|
228
|
+
def which(cmd, env)
|
|
229
|
+
exts = os == :windows ? env.fetch('PATHEXT').split(';') : ['']
|
|
230
|
+
env.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |path|
|
|
231
|
+
exts.each do |ext|
|
|
232
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
|
233
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
200
239
|
end
|
|
201
240
|
end
|
|
202
241
|
end
|
data/lib/cli/kit/util.rb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
module CLI
|
|
2
|
+
module Kit
|
|
3
|
+
module Util
|
|
4
|
+
class << self
|
|
5
|
+
def snake_case(camel_case, seperator = '_')
|
|
6
|
+
camel_case.to_s # MyCoolThing::MyAPIModule
|
|
7
|
+
.gsub(/::/, '/') # MyCoolThing/MyAPIModule
|
|
8
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, "\\1#{seperator}\\2") # MyCoolThing::MyAPI_Module
|
|
9
|
+
.gsub(/([a-z\d])([A-Z])/, "\\1#{seperator}\\2") # My_Cool_Thing::My_API_Module
|
|
10
|
+
.downcase # my_cool_thing/my_api_module
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def dash_case(camel_case)
|
|
14
|
+
snake_case(camel_case, '-')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The following methods is taken from activesupport
|
|
18
|
+
# All credit for this method goes to the original authors.
|
|
19
|
+
# https://github.com/rails/rails/blob/d66e7835bea9505f7003e5038aa19b6ea95ceea1/activesupport/lib/active_support/core_ext/string/strip.rb
|
|
20
|
+
#
|
|
21
|
+
# Copyright (c) 2005-2018 David Heinemeier Hansson
|
|
22
|
+
#
|
|
23
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
24
|
+
# a copy of this software and associated documentation files (the
|
|
25
|
+
# "Software"), to deal in the Software without restriction, including
|
|
26
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
27
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
28
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
29
|
+
# the following conditions:
|
|
30
|
+
#
|
|
31
|
+
# The above copyright notice and this permission notice shall be
|
|
32
|
+
# included in all copies or substantial portions of the Software.
|
|
33
|
+
#
|
|
34
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
35
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
36
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
37
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
38
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
39
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
40
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
41
|
+
#
|
|
42
|
+
# Strips indentation by removing the amount of leading whitespace in the least indented
|
|
43
|
+
# non-empty line in the whole string
|
|
44
|
+
#
|
|
45
|
+
def strip_heredoc(str)
|
|
46
|
+
str.gsub(/^#{str.scan(/^[ \t]*(?=\S)/).min}/, ''.freeze)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Joins an array with commas and "and", using the Oxford comma.
|
|
50
|
+
def english_join(array)
|
|
51
|
+
return '' if array.nil?
|
|
52
|
+
return array.join(' and ') if array.length < 3
|
|
53
|
+
|
|
54
|
+
"#{array[0..-2].join(", ")}, and #{array[-1]}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Execute a block within the context of a variable enviroment
|
|
58
|
+
#
|
|
59
|
+
def with_environment(environment, value)
|
|
60
|
+
return yield unless environment
|
|
61
|
+
|
|
62
|
+
old_env = ENV[environment]
|
|
63
|
+
begin
|
|
64
|
+
ENV[environment] = value
|
|
65
|
+
yield
|
|
66
|
+
ensure
|
|
67
|
+
old_env ? ENV[environment] = old_env : ENV.delete(environment)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Converts an integer representing bytes into a human readable format
|
|
72
|
+
#
|
|
73
|
+
def to_filesize(bytes, precision: 2, space: false)
|
|
74
|
+
to_si_scale(bytes, 'B', precision: precision, space: space, factor: 1024)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Converts a number to a human readable format on the SI scale
|
|
78
|
+
#
|
|
79
|
+
def to_si_scale(number, unit = '', factor: 1000, precision: 2, space: false)
|
|
80
|
+
raise ArgumentError, 'factor should only be 1000 or 1024' unless [1000, 1024].include?(factor)
|
|
81
|
+
|
|
82
|
+
small_scale = ['m', 'µ', 'n', 'p', 'f', 'a', 'z', 'y']
|
|
83
|
+
big_scale = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
|
|
84
|
+
negative = number < 0
|
|
85
|
+
number = number.abs.to_f
|
|
86
|
+
|
|
87
|
+
if number == 0 || number.between?(1, factor)
|
|
88
|
+
prefix = ''
|
|
89
|
+
scale = 0
|
|
90
|
+
else
|
|
91
|
+
scale = Math.log(number, factor).floor
|
|
92
|
+
if number < 1
|
|
93
|
+
index = [-scale - 1, small_scale.length].min
|
|
94
|
+
scale = -(index + 1)
|
|
95
|
+
prefix = small_scale[index]
|
|
96
|
+
else
|
|
97
|
+
index = [scale - 1, big_scale.length].min
|
|
98
|
+
scale = index + 1
|
|
99
|
+
prefix = big_scale[index]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
divider = (factor**scale)
|
|
104
|
+
fnum = (number / divider).round(precision)
|
|
105
|
+
|
|
106
|
+
# Trim useless decimal
|
|
107
|
+
fnum = fnum.to_i if (fnum.to_i.to_f * divider) == number
|
|
108
|
+
|
|
109
|
+
fnum = -fnum if negative
|
|
110
|
+
prefix = ' ' + prefix if space
|
|
111
|
+
|
|
112
|
+
"#{fnum}#{prefix}#{unit}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Dir.chdir, when invoked in block form, complains when we call chdir
|
|
116
|
+
# again recursively. There's no apparent good reason for this, so we
|
|
117
|
+
# simply implement our own block form of Dir.chdir here.
|
|
118
|
+
def with_dir(dir)
|
|
119
|
+
prev = Dir.pwd
|
|
120
|
+
Dir.chdir(dir)
|
|
121
|
+
yield
|
|
122
|
+
ensure
|
|
123
|
+
Dir.chdir(prev)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def with_tmp_dir
|
|
127
|
+
require 'fileutils'
|
|
128
|
+
dir = Dir.mktmpdir
|
|
129
|
+
with_dir(dir) do
|
|
130
|
+
yield(dir)
|
|
131
|
+
end
|
|
132
|
+
ensure
|
|
133
|
+
FileUtils.remove_entry(dir)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Standard way of checking for CI / Tests
|
|
137
|
+
def testing?
|
|
138
|
+
ci? || ENV['TEST']
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Set only in IntegrationTest#session; indicates that the process was
|
|
142
|
+
# called by `session.execute` from an IntegrationTest subclass.
|
|
143
|
+
def integration_test_session?
|
|
144
|
+
ENV['INTEGRATION_TEST_SESSION']
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Standard way of checking for CI
|
|
148
|
+
def ci?
|
|
149
|
+
ENV['CI']
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Must call retry_after on the result in order to execute the block
|
|
153
|
+
#
|
|
154
|
+
# Example usage:
|
|
155
|
+
#
|
|
156
|
+
# CLI::Kit::Util.begin do
|
|
157
|
+
# might_raise_if_costly_prep_not_done()
|
|
158
|
+
# end.retry_after(ExpectedError) do
|
|
159
|
+
# costly_prep()
|
|
160
|
+
# end
|
|
161
|
+
def begin(&block_that_might_raise)
|
|
162
|
+
Retrier.new(block_that_might_raise)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
class Retrier
|
|
167
|
+
def initialize(block_that_might_raise)
|
|
168
|
+
@block_that_might_raise = block_that_might_raise
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def retry_after(exception = StandardError, retries: 1, &before_retry)
|
|
172
|
+
@block_that_might_raise.call
|
|
173
|
+
rescue exception => e
|
|
174
|
+
raise if (retries -= 1) < 0
|
|
175
|
+
if before_retry
|
|
176
|
+
if before_retry.arity == 0
|
|
177
|
+
yield
|
|
178
|
+
else
|
|
179
|
+
yield e
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
retry
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private_constant :Retrier
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
data/lib/cli/kit/version.rb
CHANGED
data/lib/cli/kit.rb
CHANGED
|
@@ -11,8 +11,11 @@ module CLI
|
|
|
11
11
|
autoload :Executor, 'cli/kit/executor'
|
|
12
12
|
autoload :Ini, 'cli/kit/ini'
|
|
13
13
|
autoload :Levenshtein, 'cli/kit/levenshtein'
|
|
14
|
+
autoload :Logger, 'cli/kit/logger'
|
|
14
15
|
autoload :Resolver, 'cli/kit/resolver'
|
|
16
|
+
autoload :Support, 'cli/kit/support'
|
|
15
17
|
autoload :System, 'cli/kit/system'
|
|
18
|
+
autoload :Util, 'cli/kit/util'
|
|
16
19
|
|
|
17
20
|
EXIT_FAILURE_BUT_NOT_BUG = 30
|
|
18
21
|
EXIT_BUG = 1
|
|
@@ -48,7 +51,7 @@ module CLI
|
|
|
48
51
|
# 1. rescue Abort or Bug
|
|
49
52
|
# 2. Print a contextualized error message
|
|
50
53
|
# 3. Re-raise AbortSilent or BugSilent respectively.
|
|
51
|
-
GenericAbort = Class.new(Exception)
|
|
54
|
+
GenericAbort = Class.new(Exception) # rubocop:disable Lint/InheritException
|
|
52
55
|
Abort = Class.new(GenericAbort)
|
|
53
56
|
Bug = Class.new(GenericAbort)
|
|
54
57
|
BugSilent = Class.new(GenericAbort)
|