import { type CommandFlag, type CommandSpec, flagTokens, } from 'lib/commands/spec.js' export const renderBashCompletion = (spec: CommandSpec): string => { const globalTokens = spec.globalFlags.flatMap(flagTokens).sort() const valuelessTokens = spec.globalFlags .filter(({ takesValue }) => !takesValue) .flatMap(flagTokens) .sort() return `${[ header, `_seam_global_flags='${globalTokens.join(' ')}'`, `_seam_valueless_flags=' ${valuelessTokens.join(' ')} '`, renderCase('_seam_subcommands', subcommandBranches(spec)), renderCase('_seam_flags', flagBranches(spec)), renderCase('_seam_flag_values', flagValueBranches(spec)), completionFunction, 'complete -F _seam_completion seam', ].join('\n\n')}\n` } const header = `# bash completion for the seam command. # # Generated by @seamapi/cli from the Seam API schema. # Do not edit: regenerate with 'seam completion bash'. # # Load it for the current shell with # # source <(seam completion bash) # # or install it for every shell with # # seam completion bash > /usr/share/bash-completion/completions/seam` const completionFunction = `_seam_completion() { local current previous word command subcommands local -i index current="\${COMP_WORDS[COMP_CWORD]}" previous='' if (( COMP_CWORD > 0 )); then previous="\${COMP_WORDS[COMP_CWORD - 1]}" fi # --flag=value splits into three words under the default word breaks. if [[ "$previous" == '=' ]] && (( COMP_CWORD > 1 )); then previous="\${COMP_WORDS[COMP_CWORD - 2]}" fi # The command path is the run of words before the first flag. command='' for (( index = 1; index < COMP_CWORD; index++ )); do word="\${COMP_WORDS[index]}" if [[ "$word" == -* ]]; then break fi command="\${command:+$command }$word" done # Completing the value of a flag that takes one. if [[ "$previous" == -* && "$_seam_valueless_flags" != *" $previous "* ]]; then COMPREPLY=( $(compgen -W "$(_seam_flag_values "$command $previous")" -- "$current") ) return 0 fi if [[ "$current" == -* ]]; then COMPREPLY=( $(compgen -W "$(_seam_flags "$command") $_seam_global_flags" -- "$current") ) return 0 fi subcommands="$(_seam_subcommands "$command")" if [[ -z "$subcommands" ]]; then COMPREPLY=( $(compgen -W "$(_seam_flags "$command") $_seam_global_flags" -- "$current") ) return 0 fi COMPREPLY=( $(compgen -W "$subcommands" -- "$current") ) return 0 }` interface Branch { pattern: string words: string[] } const subcommandBranches = (spec: CommandSpec): Branch[] => spec.groups.map((group) => ({ pattern: group.path.join(' '), words: group.subcommands.map(({ name }) => name), })) const flagBranches = (spec: CommandSpec): Branch[] => spec.commands .filter(({ flags }) => flags.length > 0) .map((command) => ({ pattern: command.path.join(' '), words: command.flags.flatMap(flagTokens), })) const flagValueBranches = (spec: CommandSpec): Branch[] => spec.commands.flatMap((command) => command.flags.filter(hasValues).flatMap((flag) => flagTokens(flag).map((token) => ({ pattern: `${command.path.join(' ')} ${token}`, words: flag.values, })), ), ) const hasValues = (flag: CommandFlag): boolean => flag.values.length > 0 const renderCase = (name: string, branches: Branch[]): string => [ `${name}() {`, ` case "$1" in`, ...branches.map( ({ pattern, words }) => ` '${pattern}') echo '${words.join(' ')}' ;;`, ), ` esac`, `}`, ].join('\n')