package main import ( "bytes" "flag" "fmt" "io" "maps" "slices" "strings" "github.com/lixenwraith/logwisp/internal/config" ) // compFlag is a flag as a shell completes it: value is its row in a shell's // values, "" for a switch; repeat lets a shell offer it again. type compFlag struct { long, short, value, help string repeat bool } type compCommand struct { name, summary string subs []compSub } type compSub struct { name, summary string flags []compFlag } // shell is a completion script make completion writes under // deploy/package/completion: values completes a flag's value by its kind, // "text" as the shell would without lw's script, PRESETS the preset names. type shell struct { file string values map[string]string write func(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) } var shells = []shell{ {"lw.bash", map[string]string{ "text": "COMPREPLY=()", "dir": "_lw_reply -d", "preset": "mapfile -t COMPREPLY < <(compgen -W 'PRESETS' -- \"$cur\")", }, writeBash}, {"_lw", map[string]string{ "text": ":value:_default", "dir": ":directory:_files -/", "preset": ":preset:(PRESETS)", }, writeZsh}, {"lw.fish", map[string]string{ "text": "-r -F", "dir": "-x -a '(__fish_complete_directories)'", "preset": "-x -a 'PRESETS'", }, writeFish}, } // valueOf is the kind of value an option of lw's own takes when it is not // text: "" for none, as a bare --color means always. var valueOf = map[string]string{"preset": "preset", "logging.file.directory": "dir", "color": "", "help": ""} const generated = "Generated by make completion from lw's own tables: edit cmd/lw, not this file." // script is the shell's completion for lw as its tables stand func (s shell) script() []byte { top, cmds, presets := completionModel() values := map[string]string{} for kind, action := range s.values { values[kind] = strings.ReplaceAll(action, "PRESETS", presets) } var b bytes.Buffer s.write(&b, top, cmds, values) return b.Bytes() } // options maps each long option lw itself takes to its short letter, if any func options() map[string]string { options := map[string]string{} for _, long := range config.SpecFlags() { options[long] = "" } for long := range config.Settings() { options[long] = "" } for letter, short := range shorts { if short.top { options[short.long] = letter } } return options } // completionModel reads the parser's tables: lw's options, then each // command's subcommands with the flags their define registers. func completionModel() (top []compFlag, cmds []compCommand, presets string) { settings, specFlags, shortOf := config.Settings(), config.SpecFlags(), options() for _, long := range slices.Sorted(maps.Keys(shortOf)) { f := compFlag{long: long, short: shortOf[long], value: "text", repeat: slices.Contains(specFlags, long)} if v, ok := valueOf[long]; ok { f.value = v } else if settings[long] { f.value = "" } top = append(top, f) } for _, c := range commands { cmd := compCommand{name: c.name, summary: c.summary} for _, s := range c.subcommands { fs := flag.NewFlagSet(s.name, flag.ContinueOnError) s.define(fs) sub := compSub{name: s.name, summary: s.summary} fs.VisitAll(func(f *flag.Flag) { if f.Usage == "" { return // an alias, unlisted like a short } arg, help := flag.UnquoteUsage(f) value := arg // "" for a switch; a backquoted `dir` names its row if _, known := shells[0].values[arg]; arg != "" && !known { value = "text" } sub.flags = append(sub.flags, compFlag{long: f.Name, short: shortFor(f.Name), value: value, help: help}) }) // flag answers -h and --help itself sub.flags = append(sub.flags, compFlag{long: "help", short: shortFor("help"), help: "print the flags"}) cmd.subs = append(cmd.subs, sub) } cmds = append(cmds, cmd) } var names []string for _, p := range config.Presets() { names = append(names, p.Name) } return top, cmds, strings.Join(names, " ") } func shortFor(long string) string { for letter, short := range shorts { if short.long == long { return letter } } return "" } // words lists the flags as typed: --long, then -s for a short func words(flags []compFlag, dashes ...string) string { var w []string for _, f := range flags { for _, d := range dashes { w = append(w, d+f.long) } if f.short != "" { w = append(w, "-"+f.short) } } return strings.Join(w, " ") } // bashValues is a case statement completing each valued flag's argument func bashValues(w io.Writer, indent string, flags []compFlag, values map[string]string, dashes ...string) { byValue := map[string][]string{} for _, f := range flags { if f.value != "" { byValue[f.value] = append(byValue[f.value], strings.Fields(words([]compFlag{f}, dashes...))...) } } fmt.Fprintf(w, "%scase $prev in\n", indent) for _, v := range slices.Sorted(maps.Keys(byValue)) { fmt.Fprintf(w, "%s%s) %s; return ;;\n", indent, strings.Join(byValue[v], "|"), values[v]) } fmt.Fprintf(w, "%sesac\n", indent) } func writeBash(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) { var names []string for _, c := range cmds { names = append(names, c.name) } fmt.Fprintf(w, "# bash completion for lw. %s\n\n", generated) fmt.Fprintf(w, "_lw_reply() { compopt -o filenames 2>/dev/null; mapfile -t COMPREPLY < <(compgen \"$@\" -- \"$cur\"); }\n\n") fmt.Fprintf(w, "_lw() {\n") fmt.Fprintf(w, "\tlocal cur=${COMP_WORDS[COMP_CWORD]} prev='' i=$COMP_CWORD words\n") fmt.Fprintf(w, "\t((i > 0)) && prev=${COMP_WORDS[i-1]}\n") fmt.Fprintf(w, "\t# bash splits --key=value at '=': complete the value of the key before it\n") fmt.Fprintf(w, "\tif [[ $cur == = ]]; then cur=''\n\telif [[ $prev == = ]]; then prev=${COMP_WORDS[i-2]}\n\tfi\n") fmt.Fprintf(w, "\tcase ${COMP_WORDS[1]}:$i in\n") fmt.Fprintf(w, "\t*:1) words='%s %s help' ;;\n", words(top, "--"), strings.Join(names, " ")) for _, c := range cmds { var subs []string for _, s := range c.subs { subs = append(subs, s.name) } fmt.Fprintf(w, "\t%s:2) words='%s' ;;\n", c.name, strings.Join(subs, " ")) fmt.Fprintf(w, "\t%s:*)\n\t\tcase ${COMP_WORDS[2]} in\n", c.name) for _, s := range c.subs { fmt.Fprintf(w, "\t\t%s)\n", s.name) bashValues(w, "\t\t\t", s.flags, values, "--", "-") fmt.Fprintf(w, "\t\t\twords='%s' ;;\n", words(s.flags, "--")) } fmt.Fprintf(w, "\t\tesac ;;\n") } fmt.Fprintf(w, "\t*)\n") bashValues(w, "\t\t", top, values, "--") fmt.Fprintf(w, "\t\twords='%s' ;;\n", words(top, "--")) fmt.Fprintf(w, "\tesac\n") fmt.Fprintf(w, "\tmapfile -t COMPREPLY < <(compgen -W \"$words\" -- \"$cur\")\n}\n\n") fmt.Fprintf(w, "# An empty reply falls back to file names, as without this script\n") fmt.Fprintf(w, "complete -o bashdefault -o default -F _lw lw\n") } // zshQuote makes help text safe inside a single-quoted _arguments spec func zshQuote(s string) string { return strings.NewReplacer("'", `'\''`, "]", `\]`).Replace(s) } // zshSpecs are _arguments specs for flags; a value follows '=' or as the next // word, never attached to a short, the forms lw parses. func zshSpecs(flags []compFlag, values map[string]string) []string { var specs []string for _, f := range flags { help, action, eq := "", "", "" if f.help != "" { help = "[" + zshQuote(f.help) + "]" } if f.value != "" { action, eq = values[f.value], "=" } names, repeat := fmt.Sprintf("'(-%s --%s)'{-%s%s,--%s%s}", f.short, f.long, f.short, eq, f.long, eq), "" if f.repeat { names, repeat = fmt.Sprintf("'*'{-%s%s,--%s%s}", f.short, eq, f.long, eq), "*" } switch tail := help + action; { case f.short == "": specs = append(specs, fmt.Sprintf("'%s--%s%s%s'", repeat, f.long, eq, tail)) case tail == "": specs = append(specs, names) default: specs = append(specs, names+"'"+tail+"'") } } return specs } func writeZsh(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) { fmt.Fprintf(w, "#compdef lw\n# zsh completion for lw. %s\n\n", generated) fmt.Fprintf(w, "local -a described\n") fmt.Fprintf(w, "case ${words[2]}:$CURRENT in\n") for _, c := range cmds { fmt.Fprintf(w, "%s:2) ;;\n%s:3)\n\tdescribed=(\n", c.name, c.name) for _, s := range c.subs { fmt.Fprintf(w, "\t\t'%s:%s'\n", s.name, zshQuote(s.summary)) } fmt.Fprintf(w, "\t)\n\t_describe -t commands 'lw %s command' described\n\treturn ;;\n", c.name) // _arguments reads words from the second: make that the subcommand's first flag fmt.Fprintf(w, "%s:*)\n\tshift 2 words\n\t(( CURRENT -= 2 ))\n\tcase ${words[1]} in\n", c.name) for _, s := range c.subs { fmt.Fprintf(w, "\t%s) _arguments -S \\\n\t\t%s ;;\n", s.name, strings.Join(zshSpecs(s.flags, values), " \\\n\t\t")) } fmt.Fprintf(w, "\tesac\n\treturn ;;\n") } fmt.Fprintf(w, "esac\n") fmt.Fprintf(w, "if (( CURRENT == 2 )) && [[ $PREFIX != -* ]]; then\n\tdescribed=(\n") for _, c := range cmds { fmt.Fprintf(w, "\t\t'%s:%s'\n", c.name, zshQuote(c.summary)) } fmt.Fprintf(w, "\t\t'help:Print the help and exit'\n\t)\n") fmt.Fprintf(w, "\t_describe -t commands 'lw command' described\n\treturn\nfi\n") fmt.Fprintf(w, "_arguments -S \\\n\t%s\n", strings.Join(zshSpecs(top, values), " \\\n\t")) } // fishQuote makes text safe inside a single-quoted fish string func fishQuote(s string) string { return strings.NewReplacer(`\`, `\\`, "'", `\'`).Replace(s) } // fishFlags completes a short as an old-style option (-o): lw takes no // bundled shorts and no value attached to one. func fishFlags(w io.Writer, cond string, flags []compFlag, values map[string]string) { for _, f := range flags { line := fmt.Sprintf("complete -c lw -n '%s' -l %s", cond, f.long) if f.short != "" { line += " -o " + f.short } if f.value != "" { line += " " + values[f.value] } if f.help != "" { line += " -d '" + fishQuote(f.help) + "'" } fmt.Fprintln(w, line) } } func writeFish(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) { fmt.Fprintf(w, "# fish completion for lw. %s\n\n", generated) fmt.Fprintf(w, "# __lw_in WORD...: the command line so far is lw WORD...\n") fmt.Fprintf(w, "function __lw_in\n\tset -l w (commandline -opc)\n\tset -l n (math (count $argv) + 1)\n") fmt.Fprintf(w, "\ttest (count $w) -ge $n; and test \"$w[2..$n]\" = \"$argv\"\nend\n\n") fmt.Fprintf(w, "complete -c lw -f\n") fmt.Fprintf(w, "set -l __lw_first 'test (count (commandline -opc)) -eq 1'\n") for _, c := range cmds { fmt.Fprintf(w, "complete -c lw -n $__lw_first -a %s -d '%s'\n", c.name, fishQuote(c.summary)) } fmt.Fprintf(w, "complete -c lw -n $__lw_first -a help -d 'Print the help and exit'\n") var cond []string for _, c := range cmds { cond = append(cond, "__lw_in "+c.name) } fishFlags(w, "not "+strings.Join(cond, "; and not "), top, values) for _, c := range cmds { fmt.Fprintf(w, "\n") for _, s := range c.subs { fmt.Fprintf(w, "complete -c lw -n '__lw_in %s; and test (count (commandline -opc)) -eq 2' -a %s -d '%s'\n", c.name, s.name, fishQuote(s.summary)) } for _, s := range c.subs { fishFlags(w, "__lw_in "+c.name+" "+s.name, s.flags, values) } } }