Files
jarl/cmd/jarl/complete.go
T
adamandClaude Opus 5.5 9fc23310bd Add blocking: blocklists and block/allow rules with BIND response policy zones
jarld downloads blocklists (Adblock, hosts or plain domains), checks them
daily and reloads named only when a list changed; your own block and
allow rules take precedence, and the most specific rule wins. Lists and
rules apply per view (default: every view that offers recursion). Web UI
Blocking page with name checks, jarl blocklist and jarl block, the
/blocking API, blocked counts in the statistics. Database schema 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:24:19 +00:00

239 lines
7.0 KiB
Go

package main
// Shell completion. Cobra completes commands and flags by itself
// ('jarl completion bash'); this adds the values: server and group names
// from the configuration file, and zone, view, ACL, TSIG key, user and
// record type names from the server. Lookups on the server use the
// configured token and give up quickly, completing nothing, when the
// server can't be reached.
import (
"os"
"strings"
"time"
"github.com/spf13/cobra"
"jarl/internal/api"
"jarl/internal/model"
"jarl/internal/rr"
)
const completeTimeout = 3 * time.Second
// setupCompletion adds value completion to the command tree.
func setupCompletion(root *cobra.Command) {
root.RegisterFlagCompletionFunc("server", completeServers(true))
root.RegisterFlagCompletionFunc("output", cobra.FixedCompletions([]string{"table", "json", "yaml", "id", "field="}, cobra.ShellCompDirectiveNoFileComp|cobra.ShellCompDirectiveNoSpace))
var walk func(c *cobra.Command)
walk = func(c *cobra.Command) {
for _, name := range []string{"view", "user", "zone"} {
if c.LocalFlags().Lookup(name) != nil {
c.RegisterFlagCompletionFunc(name, completeFromServer(name))
}
}
if c.ValidArgsFunction == nil && c.Runnable() {
if fn := argCompletion(c); fn != nil {
c.ValidArgsFunction = fn
}
}
for _, sub := range c.Commands() {
walk(sub)
}
}
walk(root)
}
// argCompletion completes a command's arguments from the placeholders in
// its usage line ("add ZONE NAME TYPE DATA...").
func argCompletion(c *cobra.Command) cobra.CompletionFunc {
fields := strings.Fields(c.Use)
if len(fields) < 2 {
return nil
}
params := fields[1:]
parent := ""
if c.HasParent() {
parent = c.Parent().Name()
}
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) >= len(params) && !strings.HasSuffix(params[len(params)-1], "...") {
return nil, cobra.ShellCompDirectiveNoFileComp
}
p := params[min(len(args), len(params)-1)]
p = strings.Trim(strings.TrimSuffix(p, "..."), "[]")
if i := strings.Index(p, "|"); i >= 0 {
p = p[i+1:] // "[ID|NAME]": complete names
}
switch {
case p == "FILE":
return nil, cobra.ShellCompDirectiveDefault
case p == "ZONE":
return lookup(cmd, "zone")
case p == "VIEW":
return lookup(cmd, "view")
case p == "COLOR":
return append([]string{"default"}, model.ViewColors...), cobra.ShellCompDirectiveNoFileComp
case p == "ACL":
return lookup(cmd, "acl")
case p == "LIST":
return lookup(cmd, "blocklist")
case p == "USER":
return lookup(cmd, "user")
case p == "KEY" && parent == "tsig":
return lookup(cmd, "tsig")
case p == "TYPE":
var out []cobra.Completion
for _, t := range rr.Types() {
out = append(out, cobra.CompletionWithDesc(t.Type, t.Description))
}
return out, cobra.ShellCompDirectiveNoFileComp
case p == "NAME" && (parent == "server" || parent == "config" && cmd.Name() == "use"):
return completeServers(false)(cmd, args, toComplete)
case p == "NAME" && parent == "group" && len(args) == 0:
return completeGroups(cmd)
case p == "MEMBER":
all, d := completeServers(true)(cmd, args, toComplete)
var out []cobra.Completion
for _, c := range all {
name, _, _ := strings.Cut(c, "\t")
if name != "all" && name != args[0] {
out = append(out, c)
}
}
return out, d
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
// completeServers completes server names (and with groups, group names
// and "all") from the configuration file.
func completeServers(groups bool) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
cfg, err := loadCLIConfig()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
// After a comma, complete the next item of a list.
prefix := ""
if i := strings.LastIndex(toComplete, ","); i >= 0 {
prefix = toComplete[:i+1]
}
var out []cobra.Completion
for _, name := range sortedKeys(cfg.Servers) {
out = append(out, cobra.CompletionWithDesc(prefix+name, cfg.Servers[name].URL))
}
if groups {
for _, name := range sortedKeys(cfg.Groups) {
out = append(out, cobra.CompletionWithDesc(prefix+name, "group: "+strings.Join(cfg.Groups[name], ", ")))
}
if len(cfg.Servers) > 0 {
out = append(out, cobra.CompletionWithDesc(prefix+"all", "every server"))
}
if _, err := os.Stat(socketPath()); err == nil {
out = append(out, cobra.CompletionWithDesc(prefix+localName, "jarld on this machine (local socket)"))
}
}
return out, cobra.ShellCompDirectiveNoFileComp
}
}
func completeGroups(cmd *cobra.Command) ([]cobra.Completion, cobra.ShellCompDirective) {
cfg, err := loadCLIConfig()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return sortedKeys(cfg.Groups), cobra.ShellCompDirectiveNoFileComp
}
func completeFromServer(kind string) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
return lookup(cmd, kind)
}
}
// lookup asks the server for names of a kind of object.
func lookup(cmd *cobra.Command, kind string) ([]cobra.Completion, cobra.ShellCompDirective) {
none := cobra.ShellCompDirectiveNoFileComp
c, err := newClient()
if err != nil {
return nil, none
}
c.HTTP.Timeout = completeTimeout
seen := map[string]bool{}
var out []cobra.Completion
addName := func(name, desc string) {
if name != "" && !seen[name] {
seen[name] = true
out = append(out, cobra.CompletionWithDesc(name, desc))
}
}
switch kind {
case "zone":
var zones []api.Zone
if c.Get(ctx(), "/zones", nil, &zones) != nil {
return nil, none
}
// A zone in several views is one name; its description lists them.
views := map[string][]string{}
for _, z := range zones {
name := dot(z.Name)
if z.View != "" {
views[name] = append(views[name], z.View)
}
addName(name, z.Type)
}
for i, c := range out {
if vs := views[c[:strings.IndexByte(c, '\t')]]; len(vs) > 0 {
label := "view "
if len(vs) > 1 {
label = "views "
}
out[i] = c + ", " + label + strings.Join(vs, ", ")
}
}
case "view":
var views []api.View
if c.Get(ctx(), "/views", nil, &views) != nil {
return nil, none
}
for _, v := range views {
addName(v.Name, v.Comment)
}
case "blocklist":
var lists []api.Blocklist
if c.Get(ctx(), "/blocking/lists", nil, &lists) != nil {
return nil, none
}
for _, b := range lists {
addName(b.Name, b.URL)
}
case "acl":
var acls []api.ACL
if c.Get(ctx(), "/acls", nil, &acls) != nil {
return nil, none
}
for _, a := range acls {
addName(a.Name, a.Comment)
}
case "tsig":
var keys []api.TSIGKey
if c.Get(ctx(), "/tsig-keys", nil, &keys) != nil {
return nil, none
}
for _, k := range keys {
addName(k.Name, k.Algorithm)
}
case "user":
var users []api.User
if c.Get(ctx(), "/users", nil, &users) != nil {
return nil, none
}
for _, u := range users {
addName(u.Username, u.Comment)
}
}
return out, none
}