From c564903a2cbae0a625803d1b11d95f295f918b08 Mon Sep 17 00:00:00 2001 From: spolisar <22416070+spolisar@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:48:16 -0400 Subject: [PATCH] feat: add completion shell script generation for bash, zsh, and fish --- cmd/codecrafters/main.go | 8 ++ internal/commands/completions.go | 230 +++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 internal/commands/completions.go diff --git a/cmd/codecrafters/main.go b/cmd/codecrafters/main.go index a95f447..8a07dbe 100644 --- a/cmd/codecrafters/main.go +++ b/cmd/codecrafters/main.go @@ -28,6 +28,7 @@ EXAMPLES $ codecrafters submit -m "msg" # Commit changes & run tests with a custom commit message $ codecrafters test # Run tests without committing changes $ codecrafters test --previous # Run tests for all previous stages and the current stage without committing changes + $ codecrafters completion zsh # Generate shell completions for zsh COMMANDS submit: Commit changes & run tests @@ -35,6 +36,7 @@ COMMANDS task: View current stage instructions update-buildpack: Update language version ping: Test the connection to a CodeCrafters repository + completion: Generate shell completions for bash, zsh, or fish help: Show usage instructions VERSION @@ -111,6 +113,12 @@ func run() error { return commands.UpdateBuildpackCommand() case "ping": return commands.PingCommand() + case "completion": + if flag.NArg() != 2 { + fmt.Print(commands.CompletionUsage()) + return nil + } + return commands.CompletionCommand(flag.Arg(1)) case "help", "": // no argument flag.Usage() diff --git a/internal/commands/completions.go b/internal/commands/completions.go new file mode 100644 index 0000000..42efcfc --- /dev/null +++ b/internal/commands/completions.go @@ -0,0 +1,230 @@ +package commands + +import ( + "fmt" + "strings" +) + +const ( + ShellBash = "bash" + ShellZsh = "zsh" + ShellFish = "fish" +) + +func CompletionCommand(shell string) (err error) { + var completionScript string + switch shell { + case ShellBash: + completionScript = bashCompletion + case ShellZsh: + completionScript = zshCompletion + case ShellFish: + completionScript = fishCompletion + case "-help": + fallthrough + case "--help": + fmt.Print(CompletionUsage()) + return nil + default: + return fmt.Errorf("unsupported shell %q; supported shells: %s", shell, strings.Join(supportedShells, ", ")) + } + fmt.Print(completionScript) + + return nil +} + +var supportedShells = []string{ShellBash, ShellZsh, ShellFish} + +func CompletionUsage() string { + return fmt.Sprintf(`Generate shell completions for codecrafters. + +USAGE + $ codecrafters completion <%s> + +INSTALL + Bash: + $ mkdir -p ~/.local/share/bash-completion/completions + $ codecrafters completion bash > ~/.local/share/bash-completion/completions/codecrafters + $ echo "source ~/.local/share/bash-completion/completions/codecrafters" >> ~/.bashrc + + Zsh: + $ mkdir -p ~/.zsh/completions/ + $ codecrafters completion zsh > ~/.zsh/completions/_codecrafters + $ echo "fpath=(~/.zsh/completions $fpath)" >> ~/.zshrc + $ echo "autoload -Uz compinit && compinit" >> ~/.zshrc + + Fish: + $ codecrafters completion fish > ~/.config/fish/completions/codecrafters.fish +`, strings.Join(supportedShells, "|")) +} + +const bashCompletion = `# bash completion for codecrafters + +_codecrafters() +{ + local cur prev words cword command + COMPREPLY=() + + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion || return + else + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + words=("${COMP_WORDS[@]}") + cword=$COMP_CWORD + fi + + local commands="submit test task update-buildpack ping completion help" + local global_flags="--help --version" + local shells="bash zsh fish" + + command="" + for word in "${words[@]:1:cword-1}"; do + case "$word" in + submit|test|task|update-buildpack|ping|completion|help) + command="$word" + break + ;; + esac + done + + case "$prev" in + -m|--message|--stage|bash|zsh|fish) + return 0 + ;; + esac + + if [[ -z "$command" ]]; then + if [[ "$cur" == -* ]]; then + COMPREPLY=( $(compgen -W "$global_flags" -- "$cur") ) + else + COMPREPLY=( $(compgen -W "$commands" -- "$cur") ) + fi + return 0 + fi + + case "$command" in + submit) + COMPREPLY=( $(compgen -W "-m --message --help" -- "$cur") ) + ;; + test) + COMPREPLY=( $(compgen -W "--previous --help" -- "$cur") ) + ;; + task) + COMPREPLY=( $(compgen -W "--stage --raw --help" -- "$cur") ) + ;; + completion) + COMPREPLY=( $(compgen -W "$shells" -- "$cur") ) + ;; + esac +} + +complete -F _codecrafters codecrafters +` + +const zshCompletion = `#compdef codecrafters + +_codecrafters() { + local -a commands global_flags submit_flags test_flags test_previous_flag task_flags no_arg_command_flags shells + + commands=( + 'submit:Commit changes and run tests' + 'test:Run tests without committing changes' + 'task:View current stage instructions' + 'update-buildpack:Update language version' + 'ping:Test the connection to a CodeCrafters repository' + 'completion:Generate shell completion script' + 'help:Show usage instructions' + ) + global_flags=( + '--help:show usage instructions' + '--version:print version and exit' + ) + + submit_flags=( + '-m:Commit changes and run tests with a custom commit message' + '--message:Commit changes and run tests with a custom commit message' + '--help:show usage instructions' + '-help:show usage instructions' + ) + test_flags=( + '--help:show usage instructions' + '-help:show usage instructions' + ) + task_flags=( + '--stage:view instructions for a specific stage (slug, +N, or -N)' + '--raw:print instructions without pretty-printing' + '--help:show usage instructions' + '-help:show usage instructions' + ) + no_arg_command_flags=( + '--help' + '-help' + ) + shells=(bash zsh fish) + + if (( CURRENT == 2 )); then + if [[ "${words[CURRENT]}" == -* ]]; then + _describe -t options 'codecrafters option' global_flags + return + fi + + _describe -t commands 'codecrafters command' commands + return + fi + + + case "${words[2]}" in + submit) + if [[ "${words[(Ie)-m]}" -ne 0 || "${words[(Ie)--message]}" -ne 0 || "${words[(Ie)-help]}" -ne 0 || "${words[(Ie)--help]}" -ne 0 ]]; then + return + fi + _describe -t options 'submit option' submit_flags + ;; + test) + if [[ "${words[(Ie)--previous]}" -ne 0 || "${words[(Ie)-help]}" -ne 0 || "${words[(Ie)--help]}" -ne 0 ]]; then + return + fi + _describe -t options 'test option' test_flags + ;; + task) + _describe -t options 'task option' task_flags + ;; + update-buildpack|ping|help) + return + ;; + completion) + if [[ "${words[(Ie)bash]}" -ne 0 || "${words[(Ie)zsh]}" -ne 0 || "${words[(Ie)fish]}" -ne 0 ]]; then + return + fi + + if [[ "${words[CURRENT]}" == -* ]]; then + _describe -t options 'completion option' no_arg_command_flags + else + _describe -t shells 'shell' shells + fi + ;; + esac +} + +compdef _codecrafters codecrafters +` + +const fishCompletion = `# fish completion for codecrafters + +complete -c codecrafters -f +complete -c codecrafters -n __fish_use_subcommand -a submit -d 'Commit changes and run tests' +complete -c codecrafters -n __fish_use_subcommand -a test -d 'Run tests without committing changes' +complete -c codecrafters -n __fish_use_subcommand -a task -d 'View current stage instructions' +complete -c codecrafters -n __fish_use_subcommand -a update-buildpack -d 'Update language version' +complete -c codecrafters -n __fish_use_subcommand -a ping -d 'Test the connection to a CodeCrafters repository' +complete -c codecrafters -n __fish_use_subcommand -a completion -d 'Generate shell completion script' +complete -c codecrafters -n __fish_use_subcommand -a help -d 'Show usage instructions' +complete -c codecrafters -n __fish_use_subcommand -l help -d 'show usage instructions' +complete -c codecrafters -n __fish_use_subcommand -l version -d 'print version and exit' +complete -c codecrafters -n '__fish_seen_subcommand_from submit' -s m -l message -r -d 'Commit changes and run tests with a custom commit message' +complete -c codecrafters -n '__fish_seen_subcommand_from test; and not __fish_seen_argument -l previous' -l previous -d 'Run tests for all previous stages and the current stage without committing changes' +complete -c codecrafters -n '__fish_seen_subcommand_from task' -l stage -r -d 'view instructions for a specific stage (slug, +N, or -N)' +complete -c codecrafters -n '__fish_seen_subcommand_from task' -l raw -d 'print instructions without pretty-printing' +complete -c codecrafters -n '__fish_seen_subcommand_from completion; and not __fish_seen_argument -a bash zsh fish' -a 'bash zsh fish' +`