cli-kit 4.0.0 → 5.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 +4 -4
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/ruby.yml +34 -2
- data/.gitignore +2 -0
- data/.rubocop.sorbet.yml +47 -0
- data/.rubocop.yml +16 -1
- data/Gemfile +10 -1
- data/Gemfile.lock +94 -18
- data/README.md +46 -3
- data/Rakefile +1 -0
- data/bin/onchange +30 -0
- data/bin/tapioca +29 -0
- data/bin/testunit +1 -0
- data/cli-kit.gemspec +2 -2
- data/dev.yml +35 -3
- data/examples/minimal/example.rb +3 -1
- data/examples/single-file/example.rb +25 -35
- data/gen/lib/gen/commands/help.rb +8 -10
- data/gen/lib/gen/commands/new.rb +23 -9
- data/gen/lib/gen/commands.rb +21 -9
- data/gen/lib/gen/entry_point.rb +12 -3
- data/gen/lib/gen/generator.rb +28 -7
- data/gen/lib/gen/help.rb +63 -0
- data/gen/lib/gen.rb +18 -23
- data/gen/template/bin/update-deps +2 -2
- data/gen/template/lib/__app__/commands.rb +1 -4
- data/gen/template/lib/__app__.rb +8 -17
- data/gen/template/test/example_test.rb +1 -1
- data/lib/cli/kit/args/definition.rb +344 -0
- data/lib/cli/kit/args/evaluation.rb +245 -0
- data/lib/cli/kit/args/parser/node.rb +132 -0
- data/lib/cli/kit/args/parser.rb +129 -0
- data/lib/cli/kit/args/tokenizer.rb +133 -0
- data/lib/cli/kit/args.rb +16 -0
- data/lib/cli/kit/base_command.rb +17 -32
- data/lib/cli/kit/command_help.rb +271 -0
- data/lib/cli/kit/command_registry.rb +69 -17
- data/lib/cli/kit/config.rb +25 -22
- data/lib/cli/kit/core_ext.rb +30 -0
- data/lib/cli/kit/error_handler.rb +131 -70
- data/lib/cli/kit/executor.rb +19 -3
- data/lib/cli/kit/ini.rb +31 -38
- data/lib/cli/kit/levenshtein.rb +12 -4
- data/lib/cli/kit/logger.rb +16 -2
- data/lib/cli/kit/opts.rb +301 -0
- data/lib/cli/kit/resolver.rb +8 -0
- data/lib/cli/kit/sorbet_runtime_stub.rb +156 -0
- data/lib/cli/kit/support/test_helper.rb +23 -14
- data/lib/cli/kit/support.rb +2 -0
- data/lib/cli/kit/system.rb +188 -54
- data/lib/cli/kit/util.rb +48 -103
- data/lib/cli/kit/version.rb +3 -1
- data/lib/cli/kit.rb +103 -7
- metadata +22 -10
- data/.github/probots.yml +0 -2
- data/lib/cli/kit/autocall.rb +0 -21
- data/lib/cli/kit/ruby_backports/enumerable.rb +0 -6
@@ -0,0 +1,129 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'cli/kit'
|
4
|
+
|
5
|
+
module CLI
|
6
|
+
module Kit
|
7
|
+
module Args
|
8
|
+
class Parser
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
autoload :Node, 'cli/kit/args/parser/node'
|
12
|
+
|
13
|
+
Error = Class.new(Args::Error)
|
14
|
+
|
15
|
+
class InvalidOptionError < Error
|
16
|
+
extend T::Sig
|
17
|
+
sig { params(option: String).void }
|
18
|
+
def initialize(option)
|
19
|
+
super("invalid option -- '#{option}'")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class OptionRequiresAnArgumentError < Error
|
24
|
+
extend T::Sig
|
25
|
+
sig { params(option: String).void }
|
26
|
+
def initialize(option)
|
27
|
+
super("option requires an argument -- '#{option}'")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
sig { params(tokens: T::Array[Tokenizer::Token]).returns(T::Array[Node]) }
|
32
|
+
def parse(tokens)
|
33
|
+
nodes = T.let([], T::Array[Node])
|
34
|
+
args = T.let(tokens, T::Array[T.nilable(Tokenizer::Token)])
|
35
|
+
args << nil # to make each_cons pass (args.last, nil) on the final round.
|
36
|
+
state = :init
|
37
|
+
# TODO: test that "--height -- 3" is parsed correctly.
|
38
|
+
args.each_cons(2) do |(arg, next_arg)|
|
39
|
+
case state
|
40
|
+
when :skip
|
41
|
+
state = :init
|
42
|
+
when :init
|
43
|
+
state, val = parse_token(T.must(arg), next_arg)
|
44
|
+
nodes << val
|
45
|
+
when :unparsed
|
46
|
+
unless arg.is_a?(Tokenizer::Token::UnparsedArgument)
|
47
|
+
raise(Error, 'bug: non-unparsed argument after unparsed argument')
|
48
|
+
end
|
49
|
+
|
50
|
+
unparsed = nodes.last
|
51
|
+
unless unparsed.is_a?(Node::Unparsed)
|
52
|
+
# :nocov: not actually possible, in theory
|
53
|
+
raise(Error, 'bug: parser failed to recognize first unparsed argument')
|
54
|
+
# :nocov:
|
55
|
+
end
|
56
|
+
|
57
|
+
unparsed.value << arg.value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
nodes
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { params(definition: Definition).void }
|
64
|
+
def initialize(definition)
|
65
|
+
@defn = definition
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
sig do
|
71
|
+
params(token: Tokenizer::Token, next_token: T.nilable(Tokenizer::Token))
|
72
|
+
.returns([Symbol, Parser::Node])
|
73
|
+
end
|
74
|
+
def parse_token(token, next_token)
|
75
|
+
case token
|
76
|
+
when Tokenizer::Token::LongOptionName
|
77
|
+
case @defn.lookup_long(token.value)
|
78
|
+
when Definition::Option
|
79
|
+
[:skip, parse_option(token, next_token)]
|
80
|
+
when Definition::Flag
|
81
|
+
[:init, Node::LongFlag.new(token.value)]
|
82
|
+
else
|
83
|
+
raise(InvalidOptionError, token.value)
|
84
|
+
end
|
85
|
+
when Tokenizer::Token::ShortOptionName
|
86
|
+
case @defn.lookup_short(token.value)
|
87
|
+
when Definition::Option
|
88
|
+
[:skip, parse_option(token, next_token)]
|
89
|
+
when Definition::Flag
|
90
|
+
[:init, Node::ShortFlag.new(token.value)]
|
91
|
+
else
|
92
|
+
raise(InvalidOptionError, token.value)
|
93
|
+
end
|
94
|
+
when Tokenizer::Token::OptionValue
|
95
|
+
raise(Error, "bug: unexpected option value in argument parse sequence: #{token.value}")
|
96
|
+
when Tokenizer::Token::PositionalArgument
|
97
|
+
[:init, Node::Argument.new(token.value)]
|
98
|
+
when Tokenizer::Token::OptionValueOrPositionalArgument
|
99
|
+
[:init, Node::Argument.new(token.value)]
|
100
|
+
when Tokenizer::Token::UnparsedArgument
|
101
|
+
[:unparsed, Node::Unparsed.new([token.value])]
|
102
|
+
else
|
103
|
+
raise(Error, "bug: unexpected token type: #{token.class}")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
sig { params(arg: Tokenizer::Token::OptionName, next_arg: T.nilable(Tokenizer::Token)).returns(Node) }
|
108
|
+
def parse_option(arg, next_arg)
|
109
|
+
case next_arg
|
110
|
+
when nil, Tokenizer::Token::LongOptionName,
|
111
|
+
Tokenizer::Token::ShortOptionName, Tokenizer::Token::PositionalArgument
|
112
|
+
raise(OptionRequiresAnArgumentError, arg.value)
|
113
|
+
when Tokenizer::Token::OptionValue, Tokenizer::Token::OptionValueOrPositionalArgument
|
114
|
+
case arg
|
115
|
+
when Tokenizer::Token::LongOptionName
|
116
|
+
Node::LongOption.new(arg.value, next_arg.value)
|
117
|
+
when Tokenizer::Token::ShortOptionName
|
118
|
+
Node::ShortOption.new(arg.value, next_arg.value)
|
119
|
+
else
|
120
|
+
raise(Error, "bug: unexpected token type: #{arg.class}")
|
121
|
+
end
|
122
|
+
else
|
123
|
+
raise(Error, "bug: unexpected argument type: #{next_arg.class}")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'cli/kit'
|
4
|
+
|
5
|
+
module CLI
|
6
|
+
module Kit
|
7
|
+
module Args
|
8
|
+
module Tokenizer
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
Error = Class.new(Args::Error)
|
12
|
+
|
13
|
+
class InvalidShortOption < Error
|
14
|
+
extend T::Sig
|
15
|
+
sig { params(short_option: String).void }
|
16
|
+
def initialize(short_option)
|
17
|
+
super("invalid short option: '-#{short_option}'")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class InvalidCharInShortOption < Error
|
22
|
+
extend T::Sig
|
23
|
+
sig { params(short_option: String, char: String).void }
|
24
|
+
def initialize(short_option, char)
|
25
|
+
super("invalid character '#{char}' in short option: '-#{short_option}'")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Token
|
30
|
+
extend T::Sig
|
31
|
+
|
32
|
+
sig { returns(String) }
|
33
|
+
attr_reader :value
|
34
|
+
|
35
|
+
sig { params(value: String).void }
|
36
|
+
def initialize(value)
|
37
|
+
@value = value
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { returns(String) }
|
41
|
+
def inspect
|
42
|
+
"#<#{self.class.name} #{@value}>"
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
46
|
+
def ==(other)
|
47
|
+
self.class == other.class && @value == other.value
|
48
|
+
end
|
49
|
+
|
50
|
+
OptionName = Class.new(Token)
|
51
|
+
LongOptionName = Class.new(OptionName)
|
52
|
+
ShortOptionName = Class.new(OptionName)
|
53
|
+
|
54
|
+
OptionValue = Class.new(Token)
|
55
|
+
PositionalArgument = Class.new(Token)
|
56
|
+
OptionValueOrPositionalArgument = Class.new(Token)
|
57
|
+
UnparsedArgument = Class.new(Token)
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
extend T::Sig
|
62
|
+
|
63
|
+
sig { params(raw_args: T::Array[String]).returns(T::Array[Token]) }
|
64
|
+
def tokenize(raw_args)
|
65
|
+
args = []
|
66
|
+
|
67
|
+
mode = :init
|
68
|
+
|
69
|
+
raw_args.each do |arg|
|
70
|
+
case mode
|
71
|
+
when :unparsed
|
72
|
+
args << Token::UnparsedArgument.new(arg)
|
73
|
+
when :init
|
74
|
+
case arg
|
75
|
+
when '--'
|
76
|
+
mode = :unparsed
|
77
|
+
when /\A--./
|
78
|
+
name, value = arg.split('=', 2)
|
79
|
+
args << Token::LongOptionName.new(T.must(T.must(name)[2..-1]))
|
80
|
+
if value
|
81
|
+
args << Token::OptionValue.new(value)
|
82
|
+
end
|
83
|
+
when /\A-./
|
84
|
+
args.concat(tokenize_short_option(T.must(arg[1..-1])))
|
85
|
+
else
|
86
|
+
args << if args.last.is_a?(Token::OptionName)
|
87
|
+
Token::OptionValueOrPositionalArgument.new(arg)
|
88
|
+
else
|
89
|
+
Token::PositionalArgument.new(arg)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
args
|
96
|
+
end
|
97
|
+
|
98
|
+
sig { params(arg: String).returns(T::Array[Token]) }
|
99
|
+
def tokenize_short_option(arg)
|
100
|
+
args = []
|
101
|
+
mode = :init
|
102
|
+
number = +''
|
103
|
+
arg.each_char do |char|
|
104
|
+
case mode
|
105
|
+
when :numeric
|
106
|
+
case char
|
107
|
+
when /[0-9]/
|
108
|
+
number << char
|
109
|
+
else
|
110
|
+
raise(InvalidShortOption, arg)
|
111
|
+
end
|
112
|
+
when :init
|
113
|
+
case char
|
114
|
+
when /[a-zA-Z]/
|
115
|
+
args << Token::ShortOptionName.new(char)
|
116
|
+
when /[0-9]/
|
117
|
+
mode = :numeric
|
118
|
+
number << char
|
119
|
+
else
|
120
|
+
raise(InvalidCharInShortOption.new(arg, char))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
if number != ''
|
125
|
+
args << Token::OptionValue.new(number)
|
126
|
+
end
|
127
|
+
args
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/cli/kit/args.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'cli/kit'
|
4
|
+
|
5
|
+
module CLI
|
6
|
+
module Kit
|
7
|
+
module Args
|
8
|
+
Error = Class.new(StandardError)
|
9
|
+
|
10
|
+
autoload :Definition, 'cli/kit/args/definition'
|
11
|
+
autoload :Parser, 'cli/kit/args/parser'
|
12
|
+
autoload :Evaluation, 'cli/kit/args/evaluation'
|
13
|
+
autoload :Tokenizer, 'cli/kit/args/tokenizer'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/cli/kit/base_command.rb
CHANGED
@@ -1,46 +1,31 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/kit'
|
2
4
|
|
3
5
|
module CLI
|
4
6
|
module Kit
|
5
7
|
class BaseCommand
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
include CLI::Kit::CommandHelp
|
11
|
+
extend CLI::Kit::CommandHelp::ClassMethods
|
12
|
+
abstract!
|
9
13
|
|
10
|
-
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.statsd_time(_metric, **_kwargs)
|
15
|
-
yield
|
16
|
-
end
|
14
|
+
class << self
|
15
|
+
extend T::Sig
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
begin
|
22
|
-
statsd_increment('cli.command.invoked', tags: stats_tags)
|
23
|
-
statsd_time('cli.command.time', tags: stats_tags) do
|
24
|
-
cmd.call(args, command_name)
|
25
|
-
end
|
26
|
-
statsd_increment('cli.command.success', tags: stats_tags)
|
27
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
28
|
-
statsd_increment('cli.command.exception', tags: stats_tags + ["exception:#{e.class}"])
|
29
|
-
raise e
|
17
|
+
sig { returns(T::Boolean) }
|
18
|
+
def defined?
|
19
|
+
true
|
30
20
|
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def stats_tags(args, command_name)
|
34
|
-
tags = ["task:#{self.class}"]
|
35
|
-
tags << "command:#{command_name}" if command_name
|
36
|
-
tags << "subcommand:#{args.first}" if args&.first && has_subcommands?
|
37
|
-
tags
|
38
|
-
end
|
39
21
|
|
40
|
-
|
41
|
-
|
22
|
+
sig { params(args: T::Array[String], command_name: String).void }
|
23
|
+
def call(args, command_name)
|
24
|
+
new.call(args, command_name)
|
25
|
+
end
|
42
26
|
end
|
43
27
|
|
28
|
+
sig { returns(T::Boolean) }
|
44
29
|
def has_subcommands?
|
45
30
|
false
|
46
31
|
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'cli/kit'
|
4
|
+
|
5
|
+
module CLI
|
6
|
+
module Kit
|
7
|
+
module CommandHelp
|
8
|
+
extend T::Sig
|
9
|
+
include Kernel # for sorbet
|
10
|
+
|
11
|
+
sig { params(args: T::Array[String], name: String).void }
|
12
|
+
def call(args, name)
|
13
|
+
begin
|
14
|
+
opts = self.class.opts_class
|
15
|
+
opts_inst = opts.new
|
16
|
+
defn = Args::Definition.new
|
17
|
+
opts_inst.define!(defn)
|
18
|
+
tokens = Args::Tokenizer.tokenize(args)
|
19
|
+
parse = Args::Parser.new(defn).parse(tokens)
|
20
|
+
evl = Args::Evaluation.new(defn, parse)
|
21
|
+
opts_inst.evaluate!(evl)
|
22
|
+
rescue Args::Evaluation::TooManyPositions, Args::Evaluation::MissingRequiredPosition => e
|
23
|
+
STDERR.puts CLI::UI.fmt("{{red:{{bold:Error: #{e.message}}}}}")
|
24
|
+
STDERR.puts
|
25
|
+
STDERR.puts self.class.build_help
|
26
|
+
raise(AbortSilent)
|
27
|
+
rescue Args::Error => e
|
28
|
+
raise(Abort, e)
|
29
|
+
end
|
30
|
+
|
31
|
+
if opts_inst.helpflag
|
32
|
+
puts self.class.build_help
|
33
|
+
else
|
34
|
+
invoke_wrapper(opts_inst, name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# use to implement error handling
|
39
|
+
sig { params(op: T.untyped, name: String).void }
|
40
|
+
def invoke_wrapper(op, name)
|
41
|
+
invoke(op, name)
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { params(op: T.untyped, name: String).void }
|
45
|
+
def invoke(op, name)
|
46
|
+
raise(NotImplementedError, '#invoke must be implemented, or #call overridden')
|
47
|
+
end
|
48
|
+
|
49
|
+
class << self
|
50
|
+
extend T::Sig
|
51
|
+
|
52
|
+
sig { params(tool_name: String).void }
|
53
|
+
attr_writer :tool_name
|
54
|
+
|
55
|
+
sig { params(max_desc_length: Integer).void }
|
56
|
+
attr_writer :max_desc_length
|
57
|
+
|
58
|
+
sig { returns(String) }
|
59
|
+
def _tool_name
|
60
|
+
unless @tool_name
|
61
|
+
raise 'You must set CLI::Kit::CommandHelp.tool_name='
|
62
|
+
end
|
63
|
+
|
64
|
+
@tool_name
|
65
|
+
end
|
66
|
+
|
67
|
+
sig { returns(Integer) }
|
68
|
+
def _max_desc_length
|
69
|
+
@max_desc_length || 80
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
module ClassMethods
|
74
|
+
extend T::Sig
|
75
|
+
include Kernel # for sorbet
|
76
|
+
|
77
|
+
DEFAULT_HELP_SECTIONS = [
|
78
|
+
:desc,
|
79
|
+
:long_desc,
|
80
|
+
:usage,
|
81
|
+
:examples,
|
82
|
+
:options,
|
83
|
+
]
|
84
|
+
|
85
|
+
sig { returns(String) }
|
86
|
+
def build_help
|
87
|
+
h = (@help_sections || DEFAULT_HELP_SECTIONS).map do |section|
|
88
|
+
case section
|
89
|
+
when :desc
|
90
|
+
build_desc
|
91
|
+
when :long_desc
|
92
|
+
@long_desc
|
93
|
+
when :usage
|
94
|
+
@usage_section ||= build_usage
|
95
|
+
when :examples
|
96
|
+
@examples_section ||= build_examples
|
97
|
+
when :options
|
98
|
+
@options_section ||= build_options
|
99
|
+
else
|
100
|
+
raise "Unknown help section: #{section}"
|
101
|
+
end
|
102
|
+
end.compact.map(&:chomp).join("\n\n") + "\n"
|
103
|
+
CLI::UI.fmt(h)
|
104
|
+
end
|
105
|
+
|
106
|
+
sig { returns(String) }
|
107
|
+
def _command_name
|
108
|
+
return @command_name if @command_name
|
109
|
+
|
110
|
+
last_camel = send(:name).split('::').last
|
111
|
+
last_camel.gsub(/([a-z])([A-Z])/, '\1-\2').downcase
|
112
|
+
end
|
113
|
+
|
114
|
+
sig { returns(String) }
|
115
|
+
def _desc
|
116
|
+
@desc
|
117
|
+
end
|
118
|
+
|
119
|
+
sig { returns(String) }
|
120
|
+
def build_desc
|
121
|
+
out = +"{{command:#{CommandHelp._tool_name} #{_command_name}}}"
|
122
|
+
if @desc
|
123
|
+
out << ": #{@desc}"
|
124
|
+
end
|
125
|
+
"{{bold:#{out}}}"
|
126
|
+
end
|
127
|
+
|
128
|
+
sig { returns(T.untyped) }
|
129
|
+
def opts_class
|
130
|
+
T.unsafe(self).const_get(:Opts) # rubocop:disable Sorbet/ConstantsFromStrings
|
131
|
+
rescue NameError
|
132
|
+
Class.new(CLI::Kit::Opts)
|
133
|
+
end
|
134
|
+
|
135
|
+
sig { returns(T.nilable(String)) }
|
136
|
+
def build_options
|
137
|
+
opts = opts_class
|
138
|
+
return(nil) unless opts
|
139
|
+
|
140
|
+
methods = []
|
141
|
+
loop do
|
142
|
+
methods.concat(opts.public_instance_methods(false))
|
143
|
+
break if opts.superclass == CLI::Kit::Opts
|
144
|
+
|
145
|
+
opts = opts.superclass
|
146
|
+
end
|
147
|
+
|
148
|
+
@defn = Args::Definition.new
|
149
|
+
o = opts.new
|
150
|
+
o.define!(@defn)
|
151
|
+
|
152
|
+
return nil if @defn.options.empty? && @defn.flags.empty?
|
153
|
+
|
154
|
+
merged = T.let(@defn.options, T::Array[T.any(Args::Definition::Option, Args::Definition::Flag)])
|
155
|
+
merged += @defn.flags
|
156
|
+
merged.sort_by!(&:name)
|
157
|
+
"{{bold:Options:}}\n" + merged.map do |o|
|
158
|
+
if o.is_a?(Args::Definition::Option)
|
159
|
+
z = ' ' + [o.short&.prepend('-'), o.long&.prepend('--')].compact.join(', ') + ' VALUE'
|
160
|
+
default = if o.dynamic_default?
|
161
|
+
'(generated default)'
|
162
|
+
elsif o.default.nil?
|
163
|
+
'(no default)'
|
164
|
+
else
|
165
|
+
"(default: #{o.default.inspect})"
|
166
|
+
end
|
167
|
+
z << if o.desc
|
168
|
+
" {{italic:{{gray:# #{o.desc} #{default}}}}}"
|
169
|
+
else
|
170
|
+
" {{italic:{{gray:# #{default}}}}}"
|
171
|
+
end
|
172
|
+
else
|
173
|
+
z = ' ' + [o.short&.prepend('-'), o.long&.prepend('--')].compact.join(', ')
|
174
|
+
if o.desc
|
175
|
+
z << " {{italic:{{gray:# #{o.desc}}}}}"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
z
|
179
|
+
end.join("\n")
|
180
|
+
end
|
181
|
+
|
182
|
+
sig { params(sections: T::Array[Symbol]).void }
|
183
|
+
def help_sections(sections)
|
184
|
+
@help_sections = sections
|
185
|
+
end
|
186
|
+
|
187
|
+
sig { params(command_name: String).void }
|
188
|
+
def command_name(command_name)
|
189
|
+
if @command_name
|
190
|
+
raise(ArgumentError, "Command name already set to #{@command_name}")
|
191
|
+
end
|
192
|
+
|
193
|
+
@command_name = command_name
|
194
|
+
end
|
195
|
+
|
196
|
+
sig { params(desc: String).void }
|
197
|
+
def desc(desc)
|
198
|
+
# A limit of 80 characters has been chosen to fit on standard terminal configurations. `long_desc` is
|
199
|
+
# available when descriptions don't fit nicely in that space. If you're using CLI::Kit for an application
|
200
|
+
# where you control the runtime environments and know that terminals will have more than 80 columns
|
201
|
+
# available, you can use
|
202
|
+
#
|
203
|
+
# CLI::Kit::CommandHelp.max_desc_length =
|
204
|
+
#
|
205
|
+
# to increase this limit.
|
206
|
+
if desc.size > CommandHelp._max_desc_length
|
207
|
+
raise(ArgumentError, "description must be #{CommandHelp._max_desc_length} characters or less")
|
208
|
+
end
|
209
|
+
if @desc
|
210
|
+
raise(ArgumentError, 'description already set')
|
211
|
+
end
|
212
|
+
|
213
|
+
@desc = desc
|
214
|
+
end
|
215
|
+
|
216
|
+
sig { params(long_desc: String).void }
|
217
|
+
def long_desc(long_desc)
|
218
|
+
if @long_desc
|
219
|
+
raise(ArgumentError, 'long description already set')
|
220
|
+
end
|
221
|
+
|
222
|
+
@long_desc = long_desc
|
223
|
+
end
|
224
|
+
|
225
|
+
sig { returns(String) }
|
226
|
+
def build_usage
|
227
|
+
'{{bold:Usage:}}' + case (@usage || []).size
|
228
|
+
when 0
|
229
|
+
" {{command:#{CommandHelp._tool_name} #{_command_name}}} [options]\n"
|
230
|
+
when 1
|
231
|
+
" {{command:#{CommandHelp._tool_name} #{_command_name}}} #{@usage.first}\n"
|
232
|
+
else
|
233
|
+
"\n" + @usage.map do |usage|
|
234
|
+
" {{command:#{CommandHelp._tool_name} #{_command_name}}} #{usage}\n"
|
235
|
+
end.join
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
sig { returns(T.nilable(String)) }
|
240
|
+
def build_examples
|
241
|
+
return nil unless @examples
|
242
|
+
|
243
|
+
cmd_prefix = " {{command:#{CommandHelp._tool_name} #{_command_name}}}"
|
244
|
+
"{{bold:Examples:}}\n" + @examples.map do |command, explanation|
|
245
|
+
cmd = "#{cmd_prefix} #{command}"
|
246
|
+
exp = "{{italic:{{gray:# #{explanation}}}}}"
|
247
|
+
|
248
|
+
width = CLI::UI::ANSI.printing_width(CLI::UI.fmt("#{cmd} #{exp}"))
|
249
|
+
if width > CLI::UI::Terminal.width
|
250
|
+
" #{exp}\n#{cmd}"
|
251
|
+
else
|
252
|
+
"#{cmd} #{exp}"
|
253
|
+
end
|
254
|
+
end.join("\n\n")
|
255
|
+
end
|
256
|
+
|
257
|
+
sig { params(usage: String).void }
|
258
|
+
def usage(usage)
|
259
|
+
@usage ||= []
|
260
|
+
@usage << usage
|
261
|
+
end
|
262
|
+
|
263
|
+
sig { params(command: String, explanation: T.nilable(String)).void }
|
264
|
+
def example(command, explanation)
|
265
|
+
@examples ||= []
|
266
|
+
@examples << [command, explanation]
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|