philiprehberger-cli_kit 0.1.2 → 0.2.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/CHANGELOG.md +13 -0
- data/README.md +72 -16
- data/lib/philiprehberger/cli_kit/menu.rb +48 -0
- data/lib/philiprehberger/cli_kit/parser.rb +120 -4
- data/lib/philiprehberger/cli_kit/version.rb +1 -1
- data/lib/philiprehberger/cli_kit.rb +24 -3
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c09f7a9f76769c7ecf089942c9594ba1f6d09ce3a992c4def928fe68551eeee3
|
|
4
|
+
data.tar.gz: c9cf6608c8fb9da1ea27ea7d8afa19654f1323ea0a4ed97f6a31ed18c4a49e6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c8fd3284baffd888ab79d3c39264e2e7cb742e51ab5de8411526e21dbc282321d1f2c937e58646a2230b24cfb4cc4219f4fbb9ff10755a4de6a3980c02a00ac
|
|
7
|
+
data.tar.gz: 7029600e08a0001ca9ca5b0fed28150ce33cf09bfbbe799685d736d92f30695ba23095a86a9e06458e16c0a842bfc10a0690c08faf08797568f3bf74b8828c3b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-03-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Subcommands with isolated flags and options via `command(:name) { ... }` DSL
|
|
14
|
+
- `result.command` returns matched subcommand name as symbol or nil
|
|
15
|
+
- Auto-generated help text with `desc:` parameter on flags and options
|
|
16
|
+
- `--help` / `-h` prints formatted usage and exits
|
|
17
|
+
- `result.help_text` returns formatted help string without printing
|
|
18
|
+
- `result.help_requested?` indicates whether help was requested
|
|
19
|
+
- Menu/selection prompt via `CliKit.select(message, choices)` with numbered options
|
|
20
|
+
- `default:` option for pre-selected menu choice
|
|
21
|
+
- `input:` and `output:` IO parameters on `select` for testability
|
|
22
|
+
|
|
10
23
|
## [0.1.2] - 2026-03-22
|
|
11
24
|
|
|
12
25
|
### Added
|
data/README.md
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/philiprehberger/rb-cli-kit/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/philiprehberger-cli_kit)
|
|
5
|
+
[](https://github.com/philiprehberger/rb-cli-kit/releases)
|
|
6
|
+
[](https://github.com/philiprehberger/rb-cli-kit/commits/main)
|
|
5
7
|
[](LICENSE)
|
|
8
|
+
[](https://github.com/philiprehberger/rb-cli-kit/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
|
9
|
+
[](https://github.com/philiprehberger/rb-cli-kit/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
|
|
10
|
+
[](https://github.com/sponsors/philiprehberger)
|
|
6
11
|
|
|
7
12
|
All-in-one CLI toolkit with argument parsing, prompts, and spinners
|
|
8
13
|
|
|
@@ -39,6 +44,42 @@ result.options[:output] # => 'out.txt' or user-provided value
|
|
|
39
44
|
result.arguments # => remaining positional args
|
|
40
45
|
```
|
|
41
46
|
|
|
47
|
+
### Subcommands
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
result = Philiprehberger::CliKit.parse(ARGV) do
|
|
51
|
+
command(:deploy) do
|
|
52
|
+
flag :force, short: :f
|
|
53
|
+
option :env, short: :e
|
|
54
|
+
end
|
|
55
|
+
command(:test) do
|
|
56
|
+
flag :coverage
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result.command # => :deploy or :test or nil
|
|
61
|
+
result.flags[:force] # => true/false (within matched command)
|
|
62
|
+
result.options[:env] # => user-provided value
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Auto-generated Help
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
result = Philiprehberger::CliKit.parse(ARGV) do
|
|
69
|
+
flag :verbose, short: :v, desc: 'Enable verbose output'
|
|
70
|
+
option :output, short: :o, desc: 'Output file path'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Passing --help or -h prints formatted usage and exits:
|
|
74
|
+
# Usage: command [options]
|
|
75
|
+
#
|
|
76
|
+
# Options:
|
|
77
|
+
# -v, --verbose Enable verbose output
|
|
78
|
+
# -o, --output VALUE Output file path
|
|
79
|
+
|
|
80
|
+
result.help_text # => formatted help string without printing
|
|
81
|
+
```
|
|
82
|
+
|
|
42
83
|
### Prompts
|
|
43
84
|
|
|
44
85
|
```ruby
|
|
@@ -49,40 +90,48 @@ confirmed = Philiprehberger::CliKit.confirm('Continue?')
|
|
|
49
90
|
# Continue? [y/n] _
|
|
50
91
|
```
|
|
51
92
|
|
|
52
|
-
###
|
|
93
|
+
### Menu Selection
|
|
53
94
|
|
|
54
95
|
```ruby
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
96
|
+
env = Philiprehberger::CliKit.select('Choose env:', %w[dev staging prod])
|
|
97
|
+
# Choose env:
|
|
98
|
+
# 1) dev
|
|
99
|
+
# 2) staging
|
|
100
|
+
# 3) prod
|
|
101
|
+
# Choose: _
|
|
102
|
+
|
|
103
|
+
env = Philiprehberger::CliKit.select('Choose env:', %w[dev staging prod], default: 'staging')
|
|
104
|
+
# Choose env:
|
|
105
|
+
# 1) dev
|
|
106
|
+
# * 2) staging
|
|
107
|
+
# 3) prod
|
|
108
|
+
# Choose [2]: _
|
|
59
109
|
```
|
|
60
110
|
|
|
61
|
-
###
|
|
111
|
+
### Spinners
|
|
62
112
|
|
|
63
113
|
```ruby
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
option :output, short: :o, default: 'out.txt'
|
|
114
|
+
data = Philiprehberger::CliKit.spinner('Loading data...') do
|
|
115
|
+
# long-running operation
|
|
116
|
+
fetch_remote_data
|
|
68
117
|
end
|
|
69
|
-
|
|
70
|
-
result.flags[:verbose] # => true
|
|
71
|
-
result.options[:output] # => 'report.csv'
|
|
72
|
-
result.arguments # => ['input.txt']
|
|
73
118
|
```
|
|
74
119
|
|
|
75
120
|
## API
|
|
76
121
|
|
|
77
122
|
| Method | Description |
|
|
78
123
|
|--------|-------------|
|
|
79
|
-
| `.parse(args) { ... }` | Parse arguments with flag/option DSL |
|
|
124
|
+
| `.parse(args) { ... }` | Parse arguments with flag/option/command DSL |
|
|
80
125
|
| `.prompt(message)` | Display prompt and read input |
|
|
81
126
|
| `.confirm(message)` | Display yes/no confirmation |
|
|
127
|
+
| `.select(message, choices)` | Present numbered menu and return selection |
|
|
82
128
|
| `.spinner(message) { ... }` | Show spinner during block execution |
|
|
83
129
|
| `Parser#flags` | Hash of boolean flag values |
|
|
84
130
|
| `Parser#options` | Hash of option values |
|
|
85
131
|
| `Parser#arguments` | Array of positional arguments |
|
|
132
|
+
| `Parser#command` | Matched subcommand name or nil |
|
|
133
|
+
| `Parser#help_text` | Formatted help string |
|
|
134
|
+
| `Parser#help_requested?` | Whether --help or -h was passed |
|
|
86
135
|
|
|
87
136
|
## Development
|
|
88
137
|
|
|
@@ -92,6 +141,13 @@ bundle exec rspec
|
|
|
92
141
|
bundle exec rubocop
|
|
93
142
|
```
|
|
94
143
|
|
|
144
|
+
## Support
|
|
145
|
+
|
|
146
|
+
If you find this package useful, consider giving it a star on GitHub — it helps motivate continued maintenance and development.
|
|
147
|
+
|
|
148
|
+
[](https://www.linkedin.com/in/philiprehberger)
|
|
149
|
+
[](https://philiprehberger.com/open-source-packages)
|
|
150
|
+
|
|
95
151
|
## License
|
|
96
152
|
|
|
97
|
-
MIT
|
|
153
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module CliKit
|
|
5
|
+
# Numbered menu selection for CLI applications.
|
|
6
|
+
module Menu
|
|
7
|
+
# Present a numbered menu and return the selected value.
|
|
8
|
+
#
|
|
9
|
+
# @param message [String] the prompt message
|
|
10
|
+
# @param choices [Array<String>] the list of choices
|
|
11
|
+
# @param default [String, nil] pre-selected default choice
|
|
12
|
+
# @param input [IO] input stream (default: $stdin)
|
|
13
|
+
# @param output [IO] output stream (default: $stdout)
|
|
14
|
+
# @return [String] the selected value
|
|
15
|
+
# @raise [ArgumentError] if choices is empty
|
|
16
|
+
def self.select(message, choices, default: nil, input: $stdin, output: $stdout)
|
|
17
|
+
raise ArgumentError, 'choices must not be empty' if choices.empty?
|
|
18
|
+
|
|
19
|
+
default_index = default ? choices.index(default) : nil
|
|
20
|
+
|
|
21
|
+
output.puts message
|
|
22
|
+
choices.each_with_index do |choice, idx|
|
|
23
|
+
marker = default_index == idx ? '*' : ' '
|
|
24
|
+
output.puts " #{marker} #{idx + 1}) #{choice}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
prompt_text = default ? "Choose [#{default_index + 1}]: " : 'Choose: '
|
|
28
|
+
output.print prompt_text
|
|
29
|
+
output.flush
|
|
30
|
+
|
|
31
|
+
answer = input.gets&.strip || ''
|
|
32
|
+
|
|
33
|
+
if answer.empty? && default
|
|
34
|
+
return default
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
index = answer.to_i - 1
|
|
38
|
+
if index >= 0 && index < choices.length
|
|
39
|
+
choices[index]
|
|
40
|
+
elsif default
|
|
41
|
+
default
|
|
42
|
+
else
|
|
43
|
+
choices.first
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -9,18 +9,22 @@ module Philiprehberger
|
|
|
9
9
|
def initialize
|
|
10
10
|
@flag_definitions = {}
|
|
11
11
|
@option_definitions = {}
|
|
12
|
+
@command_definitions = {}
|
|
12
13
|
@flags = {}
|
|
13
14
|
@options = {}
|
|
14
15
|
@arguments = []
|
|
16
|
+
@command_name = nil
|
|
17
|
+
@program_name = nil
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
# Define a boolean flag.
|
|
18
21
|
#
|
|
19
22
|
# @param name [Symbol] the flag name
|
|
20
23
|
# @param short [Symbol, nil] short alias (single character)
|
|
24
|
+
# @param desc [String, nil] description for help text
|
|
21
25
|
# @return [void]
|
|
22
|
-
def flag(name, short: nil)
|
|
23
|
-
@flag_definitions[name] = { short: short }
|
|
26
|
+
def flag(name, short: nil, desc: nil)
|
|
27
|
+
@flag_definitions[name] = { short: short, desc: desc }
|
|
24
28
|
@flags[name] = false
|
|
25
29
|
end
|
|
26
30
|
|
|
@@ -29,18 +33,93 @@ module Philiprehberger
|
|
|
29
33
|
# @param name [Symbol] the option name
|
|
30
34
|
# @param short [Symbol, nil] short alias (single character)
|
|
31
35
|
# @param default [Object, nil] default value
|
|
36
|
+
# @param desc [String, nil] description for help text
|
|
32
37
|
# @return [void]
|
|
33
|
-
def option(name, short: nil, default: nil)
|
|
34
|
-
@option_definitions[name] = { short: short, default: default }
|
|
38
|
+
def option(name, short: nil, default: nil, desc: nil)
|
|
39
|
+
@option_definitions[name] = { short: short, default: default, desc: desc }
|
|
35
40
|
@options[name] = default
|
|
36
41
|
end
|
|
37
42
|
|
|
43
|
+
# Define a subcommand or return the matched command name.
|
|
44
|
+
#
|
|
45
|
+
# When called with a name and block, defines a subcommand.
|
|
46
|
+
# When called with no arguments, returns the matched command name.
|
|
47
|
+
#
|
|
48
|
+
# @param name [Symbol, nil] the command name (nil to query)
|
|
49
|
+
# @yield [Parser] the command parser for defining command-specific flags and options
|
|
50
|
+
# @return [Symbol, nil, void]
|
|
51
|
+
def command(name = nil, &block)
|
|
52
|
+
if name.nil?
|
|
53
|
+
@command_name
|
|
54
|
+
else
|
|
55
|
+
cmd_parser = Parser.new
|
|
56
|
+
cmd_parser.instance_eval(&block) if block
|
|
57
|
+
@command_definitions[name] = cmd_parser
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return the matched command name, or nil if no command matched.
|
|
62
|
+
#
|
|
63
|
+
# @return [Symbol, nil]
|
|
64
|
+
attr_reader :command_name
|
|
65
|
+
|
|
66
|
+
# Return the formatted help text without printing.
|
|
67
|
+
#
|
|
68
|
+
# @return [String]
|
|
69
|
+
def help_text
|
|
70
|
+
lines = []
|
|
71
|
+
lines << "Usage: #{@program_name || 'command'} [options]"
|
|
72
|
+
lines << ''
|
|
73
|
+
|
|
74
|
+
unless @flag_definitions.empty? && @option_definitions.empty?
|
|
75
|
+
lines << 'Options:'
|
|
76
|
+
@flag_definitions.each do |name, defn|
|
|
77
|
+
lines << format_flag_help(name, defn)
|
|
78
|
+
end
|
|
79
|
+
@option_definitions.each do |name, defn|
|
|
80
|
+
lines << format_option_help(name, defn)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
unless @command_definitions.empty?
|
|
85
|
+
lines << '' unless @flag_definitions.empty? && @option_definitions.empty?
|
|
86
|
+
lines << 'Commands:'
|
|
87
|
+
@command_definitions.each_key do |name|
|
|
88
|
+
lines << " #{name}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
lines.join("\n")
|
|
93
|
+
end
|
|
94
|
+
|
|
38
95
|
# Parse the given argument array.
|
|
39
96
|
#
|
|
40
97
|
# @param args [Array<String>] command-line arguments
|
|
41
98
|
# @return [self]
|
|
42
99
|
def parse(args)
|
|
43
100
|
args = args.dup
|
|
101
|
+
|
|
102
|
+
# Check for --help / -h before anything else
|
|
103
|
+
if args.include?('--help') || args.include?('-h')
|
|
104
|
+
@help_requested = true
|
|
105
|
+
return self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Try to match a subcommand
|
|
109
|
+
if @command_definitions.any? && args.any?
|
|
110
|
+
potential_cmd = args.first.to_sym
|
|
111
|
+
if @command_definitions.key?(potential_cmd)
|
|
112
|
+
@command_name = potential_cmd
|
|
113
|
+
cmd_parser = @command_definitions[potential_cmd]
|
|
114
|
+
args.shift
|
|
115
|
+
cmd_parser.parse(args)
|
|
116
|
+
@flags = cmd_parser.flags
|
|
117
|
+
@options = cmd_parser.options
|
|
118
|
+
@arguments = cmd_parser.arguments
|
|
119
|
+
return self
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
44
123
|
while args.any?
|
|
45
124
|
arg = args.shift
|
|
46
125
|
if arg.start_with?('--')
|
|
@@ -54,6 +133,13 @@ module Philiprehberger
|
|
|
54
133
|
self
|
|
55
134
|
end
|
|
56
135
|
|
|
136
|
+
# Check if help was requested.
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
def help_requested?
|
|
140
|
+
@help_requested || false
|
|
141
|
+
end
|
|
142
|
+
|
|
57
143
|
private
|
|
58
144
|
|
|
59
145
|
def parse_long(arg, args)
|
|
@@ -82,6 +168,36 @@ module Philiprehberger
|
|
|
82
168
|
end
|
|
83
169
|
end
|
|
84
170
|
end
|
|
171
|
+
|
|
172
|
+
def format_flag_help(name, defn)
|
|
173
|
+
long = "--#{name.to_s.tr('_', '-')}"
|
|
174
|
+
if defn[:short]
|
|
175
|
+
short = "-#{defn[:short]}"
|
|
176
|
+
label = " #{short}, #{long}"
|
|
177
|
+
else
|
|
178
|
+
label = " #{long}"
|
|
179
|
+
end
|
|
180
|
+
if defn[:desc]
|
|
181
|
+
"#{label.ljust(24)}#{defn[:desc]}"
|
|
182
|
+
else
|
|
183
|
+
label
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def format_option_help(name, defn)
|
|
188
|
+
long = "--#{name.to_s.tr('_', '-')} VALUE"
|
|
189
|
+
if defn[:short]
|
|
190
|
+
short = "-#{defn[:short]}"
|
|
191
|
+
label = " #{short}, #{long}"
|
|
192
|
+
else
|
|
193
|
+
label = " #{long}"
|
|
194
|
+
end
|
|
195
|
+
if defn[:desc]
|
|
196
|
+
"#{label.ljust(24)}#{defn[:desc]}"
|
|
197
|
+
else
|
|
198
|
+
label
|
|
199
|
+
end
|
|
200
|
+
end
|
|
85
201
|
end
|
|
86
202
|
end
|
|
87
203
|
end
|
|
@@ -4,6 +4,7 @@ require_relative 'cli_kit/version'
|
|
|
4
4
|
require_relative 'cli_kit/parser'
|
|
5
5
|
require_relative 'cli_kit/prompt'
|
|
6
6
|
require_relative 'cli_kit/spinner'
|
|
7
|
+
require_relative 'cli_kit/menu'
|
|
7
8
|
|
|
8
9
|
module Philiprehberger
|
|
9
10
|
module CliKit
|
|
@@ -12,12 +13,20 @@ module Philiprehberger
|
|
|
12
13
|
# Parse command-line arguments using a DSL block.
|
|
13
14
|
#
|
|
14
15
|
# @param args [Array<String>] command-line arguments
|
|
15
|
-
# @
|
|
16
|
+
# @param output [IO] output stream for help text (default: $stdout)
|
|
17
|
+
# @yield [Parser] the parser for defining flags, options, and commands
|
|
16
18
|
# @return [Parser] the parsed result with flags, options, and arguments
|
|
17
|
-
def self.parse(args, &
|
|
19
|
+
def self.parse(args, output: $stdout, &)
|
|
18
20
|
parser = Parser.new
|
|
19
|
-
parser.instance_eval(&
|
|
21
|
+
parser.instance_eval(&)
|
|
20
22
|
parser.parse(args)
|
|
23
|
+
|
|
24
|
+
if parser.help_requested?
|
|
25
|
+
output.puts parser.help_text
|
|
26
|
+
exit 0 unless output.is_a?(StringIO)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
parser
|
|
21
30
|
end
|
|
22
31
|
|
|
23
32
|
# Display a prompt and read user input.
|
|
@@ -49,5 +58,17 @@ module Philiprehberger
|
|
|
49
58
|
def self.spinner(message, output: $stderr, &block)
|
|
50
59
|
Spinner.spinner(message, output: output, &block)
|
|
51
60
|
end
|
|
61
|
+
|
|
62
|
+
# Present a numbered menu and return the selected value.
|
|
63
|
+
#
|
|
64
|
+
# @param message [String] the prompt message
|
|
65
|
+
# @param choices [Array<String>] the list of choices
|
|
66
|
+
# @param default [String, nil] pre-selected default choice
|
|
67
|
+
# @param input [IO] input stream
|
|
68
|
+
# @param output [IO] output stream
|
|
69
|
+
# @return [String] the selected value
|
|
70
|
+
def self.select(message, choices, default: nil, input: $stdin, output: $stdout)
|
|
71
|
+
Menu.select(message, choices, default: default, input: input, output: output)
|
|
72
|
+
end
|
|
52
73
|
end
|
|
53
74
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-cli_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Lightweight CLI toolkit combining argument parsing with flags and options,
|
|
14
14
|
interactive prompts with confirmation, and animated spinners for long-running operations.
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- LICENSE
|
|
23
23
|
- README.md
|
|
24
24
|
- lib/philiprehberger/cli_kit.rb
|
|
25
|
+
- lib/philiprehberger/cli_kit/menu.rb
|
|
25
26
|
- lib/philiprehberger/cli_kit/parser.rb
|
|
26
27
|
- lib/philiprehberger/cli_kit/prompt.rb
|
|
27
28
|
- lib/philiprehberger/cli_kit/spinner.rb
|