Files
logwisp/cmd/lw/completion.go
T

325 lines
11 KiB
Go

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 _, f := range config.SpecFlags() {
options[f.Flag] = ""
}
for long := range config.Settings() {
options[long] = ""
}
for _, long := range switches {
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.ContainsFunc(specFlags, isFlag(long))}
if v, ok := valueOf[long]; ok {
f.value = v
} else if settings[long] || slices.Contains(switches, 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)
}
}
}