philiprehberger-cli_kit 0.1.2 → 0.2.1

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: 42b4dfe8a2da9a5c1836a7c3454ba89f602a03ecbf3f498d2c59b78346211698
4
+ data.tar.gz: c17082808b7bbcbec95514bf206e2cd56e4401d72ef053c0913fe2074ddb67aa
5
5
  SHA512:
6
- metadata.gz: ae7fd1b9fcbe52efea0db053243c449b2eee5f23875cd5f7d5ea61a8b5614bf46535575b82af11376f3f91e11b56057cbe131a367fccac218997155627b24c63
7
- data.tar.gz: 9d81fa407958b8cb11b565634c2995366f43bae342dd32229dc8b9f06035aea4164cab743c0c8c281025cd49397796db5da287b5d7f15bad6d742450bd546749
6
+ metadata.gz: 2108606cbd61a7d7edab01f14f29d2961f580a01231bd03990fb520fb670055f18c53a16317fcee78712034cfd0ae4335c56caf8bdba5b3d0048e1bcc8ed562e
7
+ data.tar.gz: ff65e678ecf0b55b1bdcab3a035bf499dd4d8bd154f33695987ae003dd068d1b0b3543c47a5dbf0b95a265bd24c370dc00b831287d5c7645dcb16a6f528e015a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.1] - 2026-03-31
11
+
12
+ ### Changed
13
+ - Standardize README badges, support section, and license format
14
+
15
+ ## [0.2.0] - 2026-03-30
16
+
17
+ ### Added
18
+ - Subcommands with isolated flags and options via `command(:name) { ... }` DSL
19
+ - `result.command` returns matched subcommand name as symbol or nil
20
+ - Auto-generated help text with `desc:` parameter on flags and options
21
+ - `--help` / `-h` prints formatted usage and exits
22
+ - `result.help_text` returns formatted help string without printing
23
+ - `result.help_requested?` indicates whether help was requested
24
+ - Menu/selection prompt via `CliKit.select(message, choices)` with numbered options
25
+ - `default:` option for pre-selected menu choice
26
+ - `input:` and `output:` IO parameters on `select` for testability
27
+
10
28
  ## [0.1.2] - 2026-03-22
11
29
 
12
30
  ### Added
data/README.md CHANGED
@@ -2,7 +2,7 @@
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
- [![License](https://img.shields.io/github/license/philiprehberger/rb-cli-kit)](LICENSE)
5
+ [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-cli-kit)](https://github.com/philiprehberger/rb-cli-kit/commits/main)
6
6
 
7
7
  All-in-one CLI toolkit with argument parsing, prompts, and spinners
8
8
 
@@ -39,6 +39,42 @@ result.options[:output] # => 'out.txt' or user-provided value
39
39
  result.arguments # => remaining positional args
40
40
  ```
41
41
 
42
+ ### Subcommands
43
+
44
+ ```ruby
45
+ result = Philiprehberger::CliKit.parse(ARGV) do
46
+ command(:deploy) do
47
+ flag :force, short: :f
48
+ option :env, short: :e
49
+ end
50
+ command(:test) do
51
+ flag :coverage
52
+ end
53
+ end
54
+
55
+ result.command # => :deploy or :test or nil
56
+ result.flags[:force] # => true/false (within matched command)
57
+ result.options[:env] # => user-provided value
58
+ ```
59
+
60
+ ### Auto-generated Help
61
+
62
+ ```ruby
63
+ result = Philiprehberger::CliKit.parse(ARGV) do
64
+ flag :verbose, short: :v, desc: 'Enable verbose output'
65
+ option :output, short: :o, desc: 'Output file path'
66
+ end
67
+
68
+ # Passing --help or -h prints formatted usage and exits:
69
+ # Usage: command [options]
70
+ #
71
+ # Options:
72
+ # -v, --verbose Enable verbose output
73
+ # -o, --output VALUE Output file path
74
+
75
+ result.help_text # => formatted help string without printing
76
+ ```
77
+
42
78
  ### Prompts
43
79
 
44
80
  ```ruby
@@ -49,40 +85,48 @@ confirmed = Philiprehberger::CliKit.confirm('Continue?')
49
85
  # Continue? [y/n] _
50
86
  ```
51
87
 
52
- ### Spinners
88
+ ### Menu Selection
53
89
 
54
90
  ```ruby
55
- data = Philiprehberger::CliKit.spinner('Loading data...') do
56
- # long-running operation
57
- fetch_remote_data
58
- end
91
+ env = Philiprehberger::CliKit.select('Choose env:', %w[dev staging prod])
92
+ # Choose env:
93
+ # 1) dev
94
+ # 2) staging
95
+ # 3) prod
96
+ # Choose: _
97
+
98
+ env = Philiprehberger::CliKit.select('Choose env:', %w[dev staging prod], default: 'staging')
99
+ # Choose env:
100
+ # 1) dev
101
+ # * 2) staging
102
+ # 3) prod
103
+ # Choose [2]: _
59
104
  ```
60
105
 
61
- ### Argument Parsing Details
106
+ ### Spinners
62
107
 
63
108
  ```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'
109
+ data = Philiprehberger::CliKit.spinner('Loading data...') do
110
+ # long-running operation
111
+ fetch_remote_data
68
112
  end
69
-
70
- result.flags[:verbose] # => true
71
- result.options[:output] # => 'report.csv'
72
- result.arguments # => ['input.txt']
73
113
  ```
74
114
 
75
115
  ## API
76
116
 
77
117
  | Method | Description |
78
118
  |--------|-------------|
79
- | `.parse(args) { ... }` | Parse arguments with flag/option DSL |
119
+ | `.parse(args) { ... }` | Parse arguments with flag/option/command DSL |
80
120
  | `.prompt(message)` | Display prompt and read input |
81
121
  | `.confirm(message)` | Display yes/no confirmation |
122
+ | `.select(message, choices)` | Present numbered menu and return selection |
82
123
  | `.spinner(message) { ... }` | Show spinner during block execution |
83
124
  | `Parser#flags` | Hash of boolean flag values |
84
125
  | `Parser#options` | Hash of option values |
85
126
  | `Parser#arguments` | Array of positional arguments |
127
+ | `Parser#command` | Matched subcommand name or nil |
128
+ | `Parser#help_text` | Formatted help string |
129
+ | `Parser#help_requested?` | Whether --help or -h was passed |
86
130
 
87
131
  ## Development
88
132
 
@@ -92,6 +136,24 @@ bundle exec rspec
92
136
  bundle exec rubocop
93
137
  ```
94
138
 
139
+ ## Support
140
+
141
+ If you find this project useful:
142
+
143
+ ⭐ [Star the repo](https://github.com/philiprehberger/rb-cli-kit)
144
+
145
+ 🐛 [Report issues](https://github.com/philiprehberger/rb-cli-kit/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
146
+
147
+ 💡 [Suggest features](https://github.com/philiprehberger/rb-cli-kit/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
148
+
149
+ ❤️ [Sponsor development](https://github.com/sponsors/philiprehberger)
150
+
151
+ 🌐 [All Open Source Projects](https://philiprehberger.com/open-source-packages)
152
+
153
+ 💻 [GitHub Profile](https://github.com/philiprehberger)
154
+
155
+ 🔗 [LinkedIn Profile](https://www.linkedin.com/in/philiprehberger)
156
+
95
157
  ## License
96
158
 
97
- MIT
159
+ [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.1'
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.1
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-31 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