Skip to content

Commit 17530cc

Browse files
committed
rfunctionality for completions and manpages remove test files accidently committed
tweaking
1 parent abbaf90 commit 17530cc

File tree

12 files changed

+2537
-19
lines changed

12 files changed

+2537
-19
lines changed

Library/Homebrew/commands.rb

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,64 @@ def self.named_args_type(command)
251251
cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path)
252252
return if cmd_parser.blank?
253253

254-
Array(cmd_parser.named_args_type)
254+
named_args = Array(cmd_parser.named_args_type)
255+
if path.file? && path.extname == ".rb"
256+
begin
257+
content = path.read
258+
if content.include?("include AbstractSubcommandMod")
259+
named_args_match = content.match(/named_args\s+%w\[(.*?)\]/m)
260+
if named_args_match && named_args_match[1].present?
261+
subcommands = named_args_match[1].split(/\s+/).uniq
262+
named_args.concat(subcommands)
263+
named_args.uniq!
264+
end
265+
end
266+
rescue
267+
end
268+
end
269+
270+
named_args
271+
end
272+
273+
def self.subcommand_options(command, subcommand)
274+
path = self.path(command)
275+
return {} if path.blank?
276+
277+
return {} unless path.file?
278+
return {} if path.extname != ".rb"
279+
280+
begin
281+
content = path.read
282+
return {} unless content.include?("include AbstractSubcommandMod")
283+
284+
pattern = /(?:^|[-_])([a-z])/
285+
subcommand_class = subcommand.gsub(pattern) { T.must(::Regexp.last_match(1)).upcase }.gsub(/[-_]/, "")
286+
subcommand_class_match = content.match(
287+
/class\s+#{subcommand_class}Subcommand\s+<\s+AbstractSubcommand(.*?)end\s+end/m,
288+
)
289+
return {} unless subcommand_class_match
290+
291+
subcommand_def = subcommand_class_match[0]
292+
cmd_args_match = subcommand_def.match(/cmd_args\s+do(.*?)end/m)
293+
return {} unless cmd_args_match
294+
295+
cmd_args_block = cmd_args_match[1]
296+
options = {}
297+
flag_matches = cmd_args_block.scan(/flag\s+["']([^"']+)["'],?\s*(?:description:|desc:)\s*["']([^"']+)["']/m)
298+
flag_matches.each do |flag, desc|
299+
options[flag] = desc
300+
end
301+
switch_matches = cmd_args_block.scan(/switch\s+["']([^"']+)["'],?\s*(?:description:|desc:)\s*["']([^"']+)["']/m)
302+
switch_matches.each do |switch, desc|
303+
options[switch] = desc
304+
end
305+
306+
options
307+
rescue
308+
{}
309+
end
255310
end
256311

257-
# Returns the conflicts of a given `option` for `command`.
258312
def self.option_conflicts(command, option)
259313
path = self.path(command)
260314
return if path.blank?

Library/Homebrew/completions.rb

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,10 @@ def self.format_description(description, fish: false)
144144
description.gsub(/[<>]/, "").tr("\n", " ").chomp(".")
145145
end
146146

147-
sig { params(command: String).returns(T::Hash[String, String]) }
148-
def self.command_options(command)
147+
sig { params(command: String, subcommand: T.nilable(String)).returns(T::Hash[String, String]) }
148+
def self.command_options(command, subcommand = nil)
149149
options = {}
150+
150151
Commands.command_options(command)&.each do |option|
151152
next if option.blank?
152153

@@ -159,6 +160,19 @@ def self.command_options(command)
159160
options[name] = desc
160161
end
161162
end
163+
164+
if subcommand.present?
165+
subcommand_options = Commands.subcommand_options(command, subcommand)
166+
subcommand_options.each do |name, desc|
167+
if name.start_with? "--[no-]"
168+
options[name.gsub("[no-]", "")] = desc
169+
options[name.sub("[no-]", "no-")] = desc
170+
else
171+
options[name] = desc
172+
end
173+
end
174+
end
175+
162176
options
163177
end
164178

@@ -176,7 +190,67 @@ def self.generate_bash_subcommand_completion(command)
176190
named_completion_string += "\n #{BASH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING[type]}"
177191
end
178192

179-
named_completion_string += "\n __brewcomp \"#{named_args_strings.join(" ")}\"" if named_args_strings.any?
193+
if named_args_strings.any?
194+
named_completion_string += "\n __brewcomp \"#{named_args_strings.join(" ")}\""
195+
subcommand_functions = []
196+
named_args_strings.each do |subcommand|
197+
subcommand_options = command_options(command, subcommand)
198+
next if subcommand_options.empty?
199+
subcommand_functions << <<~SUBCOMPLETION
200+
_brew_#{Commands.method_name command}_#{subcommand.tr("-", "_")}() {
201+
local cur="${COMP_WORDS[COMP_CWORD]}"
202+
case "${cur}" in
203+
-*)
204+
__brewcomp "
205+
#{subcommand_options.keys.sort.join("\n ")}
206+
"
207+
return
208+
;;
209+
*) ;;
210+
esac
211+
}
212+
SUBCOMPLETION
213+
end
214+
215+
if subcommand_functions.any?
216+
named_completion_string += "\n\n # Handle subcommands"
217+
named_completion_string += "\n local subcmd_idx=1"
218+
named_completion_string += "\n for ((; subcmd_idx < COMP_CWORD; subcmd_idx++))"
219+
named_completion_string += "\n do"
220+
named_completion_string += "\n local subcmd=\"${COMP_WORDS[subcmd_idx]}\""
221+
named_completion_string += "\n case \"${subcmd}\" in"
222+
223+
named_args_strings.each do |subcommand|
224+
next if command_options(command, subcommand).empty?
225+
226+
named_completion_string += "\n #{subcommand})"
227+
named_completion_string += "\n _brew_#{Commands.method_name command}_#{subcommand.tr("-", "_")}"
228+
named_completion_string += "\n return"
229+
named_completion_string += "\n ;;"
230+
end
231+
232+
named_completion_string += "\n *) ;; # Default case"
233+
named_completion_string += "\n esac"
234+
named_completion_string += "\n done"
235+
236+
return <<~COMPLETION
237+
#{subcommand_functions.join("\n")}
238+
239+
_brew_#{Commands.method_name command}() {
240+
local cur="${COMP_WORDS[COMP_CWORD]}"
241+
case "${cur}" in
242+
-*)
243+
__brewcomp "
244+
#{command_options(command).keys.sort.join("\n ")}
245+
"
246+
return
247+
;;
248+
*) ;;
249+
esac#{named_completion_string}
250+
}
251+
COMPLETION
252+
end
253+
end
180254
end
181255

182256
<<~COMPLETION
@@ -218,6 +292,8 @@ def self.generate_zsh_subcommand_completion(command)
218292
options = command_options(command)
219293

220294
args_options = []
295+
subcommand_completions = []
296+
221297
if (types = Commands.named_args_type(command))
222298
named_args_strings, named_args_types = types.partition { |type| type.is_a? String }
223299

@@ -244,6 +320,25 @@ def self.generate_zsh_subcommand_completion(command)
244320
if named_args_strings.any?
245321
args_options << "- subcommand"
246322
args_options << "*::subcommand:(#{named_args_strings.join(" ")})"
323+
named_args_strings.each do |subcommand|
324+
subcommand_opts = command_options(command, subcommand)
325+
next if subcommand_opts.empty?
326+
327+
formatted_opts = subcommand_opts.sort.map do |opt, desc|
328+
next opt if desc.blank?
329+
330+
conflicts = generate_zsh_option_exclusions(command, opt)
331+
"#{conflicts}#{opt}[#{format_description desc}]"
332+
end
333+
334+
subcommand_completions << <<~SUBCOMPLETION
335+
# brew #{command} #{subcommand}
336+
_brew_#{Commands.method_name command}_#{subcommand.tr("-", "_")}() {
337+
_arguments \\
338+
#{formatted_opts.map { |opt| "'#{opt}'" }.join(" \\\n ")}
339+
}
340+
SUBCOMPLETION
341+
end
247342
end
248343
end
249344

@@ -255,11 +350,33 @@ def self.generate_zsh_subcommand_completion(command)
255350
end
256351
options += args_options
257352

353+
return <<~COMPLETION if subcommand_completions.any?
354+
355+
#{subcommand_completions.join("\n\n")}
356+
357+
# brew #{command}
358+
_brew_#{Commands.method_name command}() {
359+
if (( CURRENT > 2 )); then
360+
local subcmd=${words[2]}
361+
case "$subcmd" in
362+
#{named_args_strings.reject { |s| command_options(command, s).empty? }
363+
.map { |s| "#{s}) _brew_#{Commands.method_name command}_#{s.tr("-", "_")} ;;" }
364+
.join("\n ")}
365+
*) _arguments \\
366+
#{options.map { |opt| opt.start_with?("- ") ? opt : "'#{opt}'" }.join(" \\\n ")} ;;
367+
esac
368+
else
369+
_arguments \\
370+
#{options.map { |opt| opt.start_with?("- ") ? opt : "'#{opt}'" }.join(" \\\n ")}
371+
fi
372+
}
373+
COMPLETION
374+
258375
<<~COMPLETION
259376
# brew #{command}
260377
_brew_#{Commands.method_name command}() {
261378
_arguments \\
262-
#{options.map! { |opt| opt.start_with?("- ") ? opt : "'#{opt}'" }.join(" \\\n ")}
379+
#{options.map { |opt| opt.start_with?("- ") ? opt : "'#{opt}'" }.join(" \\\n ")}
263380
}
264381
COMPLETION
265382
end
@@ -318,6 +435,8 @@ def self.generate_fish_subcommand_completion(command)
318435

319436
subcommands = []
320437
named_args = []
438+
subcommand_options = []
439+
321440
if (types = Commands.named_args_type(command))
322441
named_args_strings, named_args_types = types.partition { |type| type.is_a? String }
323442

@@ -341,10 +460,22 @@ def self.generate_fish_subcommand_completion(command)
341460

342461
named_args_strings.each do |subcommand|
343462
subcommands << "__fish_brew_complete_sub_cmd '#{command}' '#{subcommand}'"
463+
464+
subcmd_options = command_options(command, subcommand)
465+
next if subcmd_options.empty?
466+
467+
subcmd_options.sort.each do |opt, desc|
468+
arg_line = "# #{subcommand} subcommand options"
469+
subcommand_options << arg_line unless subcommand_options.include?(arg_line)
470+
471+
arg_line = "__fish_brew_complete_sub_arg '#{command}' '#{subcommand}' -l #{opt.sub(/^-+/, "")}"
472+
arg_line += " -d '#{format_description desc, fish: true}'" if desc.present?
473+
subcommand_options << arg_line
474+
end
344475
end
345476
end
346477

347-
lines += subcommands + options + named_args
478+
lines += subcommands + options + named_args + subcommand_options
348479
<<~COMPLETION
349480
#{lines.join("\n").chomp}
350481
COMPLETION

Library/Homebrew/completions/fish.erb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ end
191191
function __fish_brew_complete_sub_arg -a cmd sub -d "A shortcut for defining brew subcommand arguments completions"
192192
set -e argv[1..2]
193193
# NOTE: $sub can be just a name of a subcommand (or several) or additionally any other condition
194-
complete -f -c brew -n "__fish_brew_subcommand $cmd $sub" $argv
194+
# This supports both traditional subcommands and AbstractSubcommand framework
195+
complete -f -c brew -n "begin; __fish_brew_subcommand $cmd $sub; or begin; __fish_brew_command $cmd; and string match -q -- '$sub' (__fish_brew_args)[2]; end; end" $argv
195196
end
196197

197198

Library/Homebrew/manpages.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,61 @@ def self.cmd_parser_manpage_lines(cmd_parser)
112112

113113
generate_option_doc(short, long, desc)
114114
end
115+
116+
command_name = cmd_parser.instance_variable_get(:@command_name)
117+
return lines unless command_name
118+
cmd_path = Commands.path(command_name)
119+
if cmd_path&.file? && cmd_path.extname == ".rb"
120+
begin
121+
content = cmd_path.read
122+
if content.include?("include AbstractSubcommandMod") && content.include?("< AbstractSubcommand")
123+
named_args_match = content.match(/named_args\s+%w\[(.*?)\]/m)
124+
if named_args_match && named_args_match[1].present?
125+
subcommands = named_args_match[1].split(/\s+/).uniq
126+
lines << "\n#### Subcommands\n\n"
127+
subcommands.each do |subcommand|
128+
subcommand_class = subcommand.gsub(/(?:^|[-_])([a-z])/) do
129+
T.must(::Regexp.last_match(1)).upcase
130+
end.gsub(/[-_]/, "")
131+
132+
subcommand_class_match = content.match(
133+
/class\s+#{subcommand_class}Subcommand\s+<\s+AbstractSubcommand(.*?)end\s+class/m,
134+
)
135+
next unless subcommand_class_match
136+
subcommand_def = subcommand_class_match[0]
137+
cmd_args_match = subcommand_def.match(/cmd_args\s+do(.*?)end/m)
138+
next unless cmd_args_match
139+
140+
cmd_args_block = cmd_args_match[1]
141+
subcommand_lines = ["**`#{subcommand}`**"]
142+
flag_matches = cmd_args_block.scan(
143+
/flag\s+["']([^"']+)["'],?\s*(?:description:|desc:)\s*["']([^"']+)["']/m,
144+
)
145+
switch_matches = cmd_args_block.scan(
146+
/switch\s+["']([^"']+)["'],?\s*(?:description:|desc:)\s*["']([^"']+)["']/m,
147+
)
148+
149+
option_docs = flag_matches.map do |flag, desc|
150+
generate_option_doc(nil, flag, desc)
151+
end
152+
153+
switch_matches.each do |switch, desc|
154+
option_docs << generate_option_doc(nil, switch, desc)
155+
end
156+
157+
if option_docs.any?
158+
subcommand_lines << "\n*Subcommand options:*\n\n"
159+
subcommand_lines.concat(option_docs)
160+
end
161+
162+
lines << subcommand_lines.join
163+
end
164+
end
165+
end
166+
rescue
167+
end
168+
end
169+
115170
lines
116171
end
117172

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2025-04-05 10:45:08.599 [info] CLI main {"_":[],"diff":false,"merge":false,"add":false,"goto":false,"new-window":false,"reuse-window":false,"wait":false,"help":false,"list-extensions":true,"show-versions":false,"pre-release":false,"update-extensions":false,"version":false,"verbose":false,"status":false,"prof-startup":false,"no-cached-data":false,"prof-v8-extensions":false,"disable-extensions":false,"disable-lcd-text":false,"disable-gpu":false,"disable-chromium-sandbox":false,"sandbox":false,"telemetry":false,"debugRenderer":false,"enable-smoke-test-driver":false,"logExtensionHostCommunication":false,"skip-release-notes":false,"skip-welcome":false,"skip-onboarding":false,"disable-telemetry":false,"disable-updates":false,"use-inmemory-secretstorage":false,"disable-workspace-trust":false,"disable-crash-reporter":false,"skip-add-to-recently-opened":false,"open-url":false,"file-write":false,"file-chmod":false,"force":false,"do-not-sync":false,"trace":false,"trace-memory-infra":false,"preserve-env":false,"force-user-env":false,"force-disable-user-env":false,"open-devtools":false,"disable-gpu-sandbox":false,"__enable-file-policy":false,"enable-coi":false,"no-proxy-server":false,"no-sandbox":false,"nolazy":false,"force-renderer-accessibility":false,"ignore-certificate-errors":false,"allow-insecure-localhost":false,"disable-dev-shm-usage":false,"profile-temp":false,"hmr":false,"logsPath":"/opt/homebrew/Library/Homebrew/test/Library/Application Support/Cursor/logs/20250405T104508"}
2+
2025-04-05 10:45:08.607 [info] Started initializing default profile extensions in extensions installation folder. file:///opt/homebrew/Library/Homebrew/test/.cursor/extensions
3+
2025-04-05 10:45:08.639 [info] Completed initializing default profile extensions in extensions installation folder. file:///opt/homebrew/Library/Homebrew/test/.cursor/extensions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
e68d460a-022e-4fb4-9e74-489c10addb6c
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
e6fb08ab-c726-4029-b5cd-dad9134c4739

0 commit comments

Comments
 (0)