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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b81a21c1714aef7854a2d71fa19dfda96b549e63d76a235afe555fa491ddf3a
4
- data.tar.gz: 86c9be91443b2d34502557bbefcbca284f9141a0f28d263125b207356f9c184e
3
+ metadata.gz: c09f7a9f76769c7ecf089942c9594ba1f6d09ce3a992c4def928fe68551eeee3
4
+ data.tar.gz: c9cf6608c8fb9da1ea27ea7d8afa19654f1323ea0a4ed97f6a31ed18c4a49e6b
5
5
  SHA512:
6
- metadata.gz: ae7fd1b9fcbe52efea0db053243c449b2eee5f23875cd5f7d5ea61a8b5614bf46535575b82af11376f3f91e11b56057cbe131a367fccac218997155627b24c63
7
- data.tar.gz: 9d81fa407958b8cb11b565634c2995366f43bae342dd32229dc8b9f06035aea4164cab743c0c8c281025cd49397796db5da287b5d7f15bad6d742450bd546749
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
  [![Tests](https://github.com/philiprehberger/rb-cli-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-cli-kit/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/philiprehberger-cli_kit.svg)](https://rubygems.org/gems/philiprehberger-cli_kit)
5
+ [![GitHub release](https://img.shields.io/github/v/release/philiprehberger/rb-cli-kit)](https://github.com/philiprehberger/rb-cli-kit/releases)
6
+ [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-cli-kit)](https://github.com/philiprehberger/rb-cli-kit/commits/main)
5
7
  [![License](https://img.shields.io/github/license/philiprehberger/rb-cli-kit)](LICENSE)
8
+ [![Bug Reports](https://img.shields.io/github/issues/philiprehberger/rb-cli-kit/bug)](https://github.com/philiprehberger/rb-cli-kit/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
9
+ [![Feature Requests](https://img.shields.io/github/issues/philiprehberger/rb-cli-kit/enhancement)](https://github.com/philiprehberger/rb-cli-kit/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
10
+ [![Sponsor](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-ec6cb9)](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
- ### Spinners
93
+ ### Menu Selection
53
94
 
54
95
  ```ruby
55
- data = Philiprehberger::CliKit.spinner('Loading data...') do
56
- # long-running operation
57
- fetch_remote_data
58
- end
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
- ### Argument Parsing Details
111
+ ### Spinners
62
112
 
63
113
  ```ruby
64
- # Given: mytool --verbose -o report.csv input.txt
65
- result = Philiprehberger::CliKit.parse(%w[--verbose -o report.csv input.txt]) do
66
- flag :verbose, short: :v
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
+ [![LinkedIn](https://img.shields.io/badge/Philip%20Rehberger-LinkedIn-0A66C2?logo=linkedin)](https://www.linkedin.com/in/philiprehberger)
149
+ [![More packages](https://img.shields.io/badge/more-open%20source%20packages-blue)](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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module CliKit
5
- VERSION = '0.1.2'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  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
- # @yield [Parser] the parser for defining flags and options
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, &block)
19
+ def self.parse(args, output: $stdout, &)
18
20
  parser = Parser.new
19
- parser.instance_eval(&block)
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.1.2
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-23 00:00:00.000000000 Z
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