322 lines
11 KiB
Go
322 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 _, 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)
|
|
}
|
|
}
|
|
}
|