commander-tool 1.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/commander-tool.rb +195 -0
  3. metadata +38 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5fe0b76d088b96fcd3e9a46d540f79ff38d10bb49d1f35c8ed4d2b1f9bd26ddb
4
+ data.tar.gz: 2c349bc20037922113ea90e74a00c6688495f1950bebe67c505d502c6bffff4d
5
+ SHA512:
6
+ metadata.gz: 4c20b7c34bcdb2663ee7f4554ac25b1c41c8910fe62d21e9e91b60b47f651c54fdfc7aa0e7f529db11e6a1e27e26d61ba12bc40c5f08ff8ad8fbd7fe8679b4fa
7
+ data.tar.gz: 3bd9b6f788e3b2bb33e714520db2b57eb299cce3d95670b15dfb6a203cac16acc233a5c5b908647d1f3a2e5d9e2118fc32a4ca60d79a7f095afade07fe90734b
@@ -0,0 +1,195 @@
1
+ require 'did_you_mean'
2
+
3
+ class Commander
4
+ class Command
5
+ attr_reader :name, :description, :options, :block
6
+
7
+ def initialize(name, description = '', &block)
8
+ @name = name.to_s
9
+ @description = description
10
+ @options = []
11
+ @block = block
12
+ end
13
+
14
+ def add_option(switches, description = '')
15
+ @options << Option.new(switches, description)
16
+ self
17
+ end
18
+ end
19
+
20
+ class Option
21
+ attr_reader :switches, :description, :key
22
+
23
+ def initialize(switches, description = '')
24
+ @switches = switches
25
+ @description = description
26
+
27
+ @key =
28
+ switches.find { |s| s.start_with?('--') }&.sub(/^--/, '')&.tr('-', '_') ||
29
+ switches.find { |s| s.start_with?('-') }&.sub(/^-/, '')
30
+ end
31
+
32
+ def matches?(arg)
33
+ @switches.include?(arg)
34
+ end
35
+
36
+ def value_required?
37
+ description.include?('<') ||
38
+ description.include?('argument') ||
39
+ description.include?('value') ||
40
+ description.include?('VALUE')
41
+ end
42
+ end
43
+
44
+ def initialize
45
+ @commands = {}
46
+ @global_options = []
47
+ end
48
+
49
+ def add_command(name, description = '', &block)
50
+ cmd = Command.new(name, description, &block)
51
+ @commands[cmd.name] = cmd
52
+ cmd
53
+ end
54
+
55
+ def add_option(switches, description = '')
56
+ @global_options << Option.new(switches, description)
57
+ self
58
+ end
59
+
60
+ def parse(args = ARGV)
61
+ global_parsed, remaining_args =
62
+ parse_options(args, @global_options)
63
+
64
+ command_name = remaining_args.shift
65
+
66
+ if command_name.nil?
67
+ handle_no_command(global_parsed)
68
+ return
69
+ end
70
+
71
+ if @commands.key?(command_name)
72
+ command = @commands[command_name]
73
+
74
+ cmd_parsed, command_args =
75
+ parse_options(remaining_args, command.options)
76
+
77
+ # Merge global and command options for execution
78
+ combined_options = global_parsed.merge(cmd_parsed)
79
+
80
+ command.block.call(combined_options, command_args) if command.block
81
+ else
82
+ raise_unknown_command(command_name)
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def parse_options(args, available_options)
89
+ parsed = {}
90
+ remaining = []
91
+ i = 0
92
+
93
+ while i < args.length
94
+ arg = args[i]
95
+
96
+ if arg.start_with?('-')
97
+ option = available_options.find { |opt| opt.matches?(arg) }
98
+
99
+ unless option
100
+ raise_unknown_option(arg, available_options)
101
+ end
102
+
103
+ next_arg = args[i + 1]
104
+
105
+ if option.value_required?
106
+ if next_arg.nil? || next_arg.start_with?('-')
107
+ raise_missing_argument(arg)
108
+ end
109
+
110
+ parsed[option.key.to_sym] = next_arg
111
+ i += 2
112
+ elsif next_arg && !next_arg.start_with?('-')
113
+ parsed[option.key.to_sym] = next_arg
114
+ i += 2
115
+ else
116
+ parsed[option.key.to_sym] = true
117
+ i += 1
118
+ end
119
+ else
120
+ remaining << arg
121
+ i += 1
122
+ end
123
+ end
124
+
125
+ [parsed, remaining]
126
+ end
127
+
128
+ def raise_missing_argument(option_name)
129
+ abort "error: flag '#{option_name}' needs an argument"
130
+ end
131
+
132
+ def raise_unknown_command(command_name)
133
+ corrector = DidYouMean::SpellChecker.new(
134
+ dictionary: @commands.keys
135
+ )
136
+
137
+ suggestions = corrector.correct(command_name)
138
+
139
+ msg = "error: unknown command '#{command_name}'"
140
+
141
+ unless suggestions.empty?
142
+ msg += "\n\nDid you mean?\n"
143
+ msg += suggestions.map { |s| "\t#{s}" }.join("\n")
144
+ end
145
+
146
+ abort msg
147
+ end
148
+
149
+ def raise_unknown_option(option_name, available_options)
150
+ all_switches = available_options.flat_map(&:switches)
151
+
152
+ corrector = DidYouMean::SpellChecker.new(
153
+ dictionary: all_switches
154
+ )
155
+
156
+ suggestions = corrector.correct(option_name)
157
+
158
+ msg = "error: unknown option '#{option_name}'"
159
+
160
+ unless suggestions.empty?
161
+ msg += "\n\nDid you mean?\n"
162
+ msg += suggestions.map { |s| "\t#{s}" }.join("\n")
163
+ end
164
+
165
+ abort msg
166
+ end
167
+
168
+ def handle_no_command(options)
169
+ if options[:version] || options[:v]
170
+ puts "Commander CLI v1.0.0"
171
+ return
172
+ end
173
+
174
+ puts "Usage: <command> [options]"
175
+ puts
176
+ puts "Commands:"
177
+
178
+ if @commands.empty?
179
+ puts " No commands available."
180
+ else
181
+ @commands.each_value do |command|
182
+ puts " #{command.name.ljust(20)} #{command.description}"
183
+ end
184
+ end
185
+
186
+ unless @global_options.empty?
187
+ puts
188
+ puts "Options:"
189
+
190
+ @global_options.each do |option|
191
+ puts " #{option.switches.join(', ').ljust(20)} #{option.description}"
192
+ end
193
+ end
194
+ end
195
+ end
metadata ADDED
@@ -0,0 +1,38 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: commander-tool
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jjjm
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-09-17 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Command and option parser with DidYouMean support.
13
+ executables: []
14
+ extensions: []
15
+ extra_rdoc_files: []
16
+ files:
17
+ - lib/commander-tool.rb
18
+ licenses:
19
+ - MIT
20
+ metadata: {}
21
+ rdoc_options: []
22
+ require_paths:
23
+ - lib
24
+ required_ruby_version: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '3.0'
29
+ required_rubygems_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ requirements: []
35
+ rubygems_version: 3.6.2
36
+ specification_version: 4
37
+ summary: Lightweight Ruby command-line parser
38
+ test_files: []