Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions commands/commands.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"context"
"encoding/json"
"fmt"
"os"
Expand All @@ -13,7 +14,9 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/pkg/version"
"github.com/fatih/color"
"github.com/urfave/cli/v2"

// "github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

// categories of commands
Expand All @@ -34,19 +37,22 @@ func cmd(cat string, c *cli.Command) bool {
var _ = cmd(catDebug, &cli.Command{
Name: "version",
Usage: "Print version information",
Action: func(c *cli.Context) error {
Action: func(ctx context.Context, c *cli.Command) error {
_, err := fmt.Println(version.Version())
return err
},
})

// Run will execute the CLI
func Run(v string) int {
app := cli.NewApp()
app.Version = v
app.Name = "dnscontrol"
app.HideVersion = true
app.Usage = "DNSControl is a compiler and DSL for managing dns zones"
// app := cli.NewApp()
// In v3, there's no cli.App, everything is a cli.Command
app := &cli.Command{
Name: "dnscontrol",
Usage: "DNSControl is a compiler and DSL for managing dns zones",
Version: v,
// HideVersion: true, // Not available in v3
}
app.Flags = []cli.Flag{
&cli.BoolFlag{
Name: "debug",
Expand All @@ -63,7 +69,7 @@ func Run(v string) int {
Name: "diff2",
Usage: "Obsolete flag. Will be removed in v5 or later",
Hidden: true,
Action: func(ctx *cli.Context, v bool) error {
Action: func(ctx context.Context, c *cli.Command, v bool) error {
pobsoleteDiff2FlagUsed = true
return nil
},
Expand All @@ -79,11 +85,31 @@ func Run(v string) int {
Destination: &color.NoColor,
Value: false,
},
&cli.BoolFlag{
Name: "generate-bash-completion",
Usage: "Generate bash completion",
Hidden: true,
},
}
app.Before = func(ctx context.Context, c *cli.Command) (context.Context, error) {
// In v2, EnableBashCompletion would automatically add this flag and trigger completion.
// In v3, we need to handle it manually.
// Only handle at root level - subcommands like shell-completion will handle their own.
if c.Bool("generate-bash-completion") && c.Root() == c {
if c.Root().ShellComplete != nil {
c.Root().ShellComplete(ctx, c)
}
return ctx, cli.Exit("", 0)
}
return ctx, nil
}
sort.Sort(cli.CommandsByName(commands))
sort.Slice(commands, func(i, j int) bool {
return commands[i].Name < commands[j].Name
})
app.Commands = commands
app.EnableBashCompletion = true
app.BashComplete = func(cCtx *cli.Context) {
// app.EnableBashCompletion = true // Removed in v3
// app.BashComplete = func(cCtx *cli.Context) { // BashComplete renamed to ShellComplete
app.ShellComplete = func(ctx context.Context, c *cli.Command) {
// ripped from cli.DefaultCompleteWithFlags
var lastArg string

Expand All @@ -94,14 +120,14 @@ func Run(v string) int {
if lastArg != "" {
if strings.HasPrefix(lastArg, "-") {
if !islastFlagComplete(lastArg, app.Flags) {
dnscontrolPrintFlagSuggestions(lastArg, app.Flags, cCtx.App.Writer)
dnscontrolPrintFlagSuggestions(lastArg, app.Flags, c.Writer)
return
}
}
}
dnscontrolPrintCommandSuggestions(app.Commands, cCtx.App.Writer)
dnscontrolPrintCommandSuggestions(app.Commands, c.Writer)
}
if err := app.Run(os.Args); err != nil {
if err := app.Run(context.Background(), os.Args); err != nil {
return 1
}
return 0
Expand Down Expand Up @@ -212,7 +238,8 @@ type ExecuteDSLArgs struct {
JSFile string
JSONFile string
DevMode bool
Variable cli.StringSlice
// Variable cli.StringSlice // v2 syntax
Variable []string // v3: Destination is now *[]string
}

func (args *ExecuteDSLArgs) flags() []cli.Flag {
Expand Down
48 changes: 39 additions & 9 deletions commands/completion.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"context"
"embed"
"errors"
"fmt"
Expand All @@ -12,7 +13,8 @@ import (
"text/template"
"unicode/utf8"

"github.com/urfave/cli/v2"
// "github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

//go:embed completion-scripts/completion.*.gotmpl
Expand All @@ -28,32 +30,60 @@ func shellCompletionCommand() *cli.Command {
Usage: "generate shell completion scripts",
ArgsUsage: fmt.Sprintf("[ %s ]", strings.Join(supportedShells, " | ")),
Description: fmt.Sprintf("Generate shell completion script for [ %s ]", strings.Join(supportedShells, " | ")),
BashComplete: func(ctx *cli.Context) {
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "generate-bash-completion",
Usage: "Generate bash completion",
Hidden: true,
},
},
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
// In v2, EnableBashCompletion would automatically add this flag and trigger completion.
// In v3, we need to handle it manually.
// This runs before Action, so we intercept the flag here.
if cmd.Bool("generate-bash-completion") {
if cmd.ShellComplete != nil {
cmd.ShellComplete(ctx, cmd)
}
// Mark that we handled completion so Action can skip
return context.WithValue(ctx, "completionHandled", true), nil
}
return ctx, nil
},
// BashComplete: func(ctx *cli.Context) { // BashComplete renamed to ShellComplete in v3
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
for _, shell := range supportedShells {
if strings.HasPrefix(shell, ctx.Args().First()) {
if _, err := ctx.App.Writer.Write([]byte(shell + "\n")); err != nil {
if strings.HasPrefix(shell, cmd.Args().First()) {
if _, err := cmd.Root().Writer.Write([]byte(shell + "\n")); err != nil {
panic(err)
}
}
}
},
Action: func(ctx *cli.Context) error {
Action: func(ctx context.Context, cmd *cli.Command) error {
// If completion was handled in Before, skip normal action
if ctx.Value("completionHandled") != nil {
return nil
}

var inputShell string
if inputShell = ctx.Args().First(); inputShell == "" {
if inputShell = cmd.Args().First(); inputShell == "" {
//fmt.Printf("DEBUG: no shell argument, checking $SHELL\n")
if inputShell = os.Getenv("SHELL"); inputShell == "" {
return cli.Exit(errors.New("shell not specified"), 1)
}
}
shellName := path.Base(inputShell) // necessary if using $SHELL, noop otherwise
//fmt.Printf("DEBUG: shellName = %q\n", shellName)

template := templates[shellName]
if template == nil {
return cli.Exit(fmt.Errorf("unknown shell: %s", inputShell), 1)
}

err = template.Execute(ctx.App.Writer, struct {
App *cli.App
}{ctx.App})
err = template.Execute(cmd.Root().Writer, struct {
App *cli.Command
}{cmd.Root()})
if err != nil {
return cli.Exit(fmt.Errorf("failed to print completion script: %w", err), 1)
}
Expand Down
49 changes: 34 additions & 15 deletions commands/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package commands

import (
"bytes"
"context"
"fmt"
"slices"
"strings"
"testing"
"text/template"

"github.com/google/go-cmp/cmp"
"github.com/urfave/cli/v2"
// "github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

type shellTestDataItem struct {
Expand All @@ -19,15 +21,17 @@ type shellTestDataItem struct {
}

// setupTestShellCompletionCommand resets the buffers used to capture output and errors from the app.
func setupTestShellCompletionCommand(app *cli.App) func(t *testing.T) {
func setupTestShellCompletionCommand(app *cli.Command) func(t *testing.T) {
// func setupTestShellCompletionCommand(app *cli.App) func(t *testing.T) { // v2 syntax
return func(t *testing.T) {
app.Writer.(*bytes.Buffer).Reset()
cli.ErrWriter.(*bytes.Buffer).Reset()
}
}

func TestShellCompletionCommand(t *testing.T) {
app := cli.NewApp()
// app := cli.NewApp() // v2 syntax
app := &cli.Command{}
app.Name = "testing"

var appWriterBuffer bytes.Buffer
Expand Down Expand Up @@ -67,7 +71,7 @@ func TestShellCompletionCommand(t *testing.T) {
tearDownTest := setupTestShellCompletionCommand(app)
defer tearDownTest(t)

err := app.Run([]string{app.Name, "shell-completion", tt.shellName})
err := app.Run(context.Background(), []string{app.Name, "shell-completion", tt.shellName})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -92,7 +96,7 @@ func TestShellCompletionCommand(t *testing.T) {
tearDownTest := setupTestShellCompletionCommand(app)
defer tearDownTest(t)

err := app.Run([]string{app.Name, "shell-completion", "invalid"})
err := app.Run(context.Background(), []string{app.Name, "shell-completion", "invalid"})

if err == nil {
t.Fatal("expected error, but didn't get one")
Expand Down Expand Up @@ -120,7 +124,7 @@ func TestShellCompletionCommand(t *testing.T) {

t.Setenv("SHELL", tt.shellPath)

err := app.Run([]string{app.Name, "shell-completion"})
err := app.Run(context.Background(), []string{app.Name, "shell-completion"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -147,7 +151,7 @@ func TestShellCompletionCommand(t *testing.T) {

t.Setenv("SHELL", invalidShellTestDataItem.shellPath)

err := app.Run([]string{app.Name, "shell-completion"})
err := app.Run(context.Background(), []string{app.Name, "shell-completion"})
if err == nil {
t.Fatal("expected error, but didn't get one")
}
Expand Down Expand Up @@ -190,12 +194,25 @@ func TestShellCompletionCommand(t *testing.T) {
t.Run(tC.shellArg, func(t *testing.T) {
tearDownTest := setupTestShellCompletionCommand(app)
defer tearDownTest(t)
app.EnableBashCompletion = true
defer func() {
app.EnableBashCompletion = false
}()

err := app.Run([]string{app.Name, "shell-completion", tC.shellArg, "--generate-bash-completion"})
// app.EnableBashCompletion = true // v2: Not available in v3
// defer func() {
// app.EnableBashCompletion = false
// }()

cmdargs := []string{app.Name,
"shell-completion",
tC.shellArg,
"--generate-bash-completion",
}
if tC.shellArg == "" {
// remove empty argument to simulate user not providing it
cmdargs = []string{app.Name,
"shell-completion",
"--generate-bash-completion",
}
}
//fmt.Printf("DEBUG: app.Run(%v)\n", cmdargs)
err := app.Run(context.Background(), cmdargs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down Expand Up @@ -239,10 +256,12 @@ func testHelperGetShellsAndCompletionScripts() ([]shellTestDataItem, error) {

// testHelperRenderTemplateFromApp renders a given template with a given app.
// This is used to test the output of the CLI command against a 'known good' value.
func testHelperRenderTemplateFromApp(app *cli.App, scriptTemplate *template.Template) (string, error) {
func testHelperRenderTemplateFromApp(app *cli.Command, scriptTemplate *template.Template) (string, error) {
// func testHelperRenderTemplateFromApp(app *cli.App, scriptTemplate *template.Template) (string, error) { // v2 syntax
var scriptBytes bytes.Buffer
err := scriptTemplate.Execute(&scriptBytes, struct {
App *cli.App
App *cli.Command
// App *cli.App // v2 syntax
}{app})

return scriptBytes.String(), err
Expand Down
11 changes: 7 additions & 4 deletions commands/createDomains.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
package commands

import (
"context"
"fmt"

"github.com/StackExchange/dnscontrol/v4/pkg/credsfile"
"github.com/StackExchange/dnscontrol/v4/pkg/providers"
"github.com/urfave/cli/v2"

// "github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var _ = cmd(catUtils, func() *cli.Command {
var args CreateDomainsArgs
return &cli.Command{
Name: "create-domains",
Usage: "DEPRECATED: Ensures that all domains in your configuration are activated at their Domain Service Provider (This does not purchase the domain or otherwise interact with Registrars.)",
Action: func(ctx *cli.Context) error {
Action: func(ctx context.Context, c *cli.Command) error {
return exit(CreateDomains(args))
},
Flags: args.flags(),
Before: func(context *cli.Context) error {
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
fmt.Println("DEPRECATED: This command is deprecated. The domain is automatically created at the Domain Service Provider during the push command.")
fmt.Println("DEPRECATED: To prevent disable auto-creating, use --no-populate with the push command.")
return nil
return ctx, nil
},
}
}())
Expand Down
6 changes: 4 additions & 2 deletions commands/fmt.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package commands

import (
"context"
"fmt"
"io"
"os"
"strings"

"github.com/ditashi/jsbeautifier-go/jsbeautifier"
"github.com/urfave/cli/v2"
// "github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var _ = cmd(catUtils, func() *cli.Command {
var args FmtArgs
return &cli.Command{
Name: "fmt",
Usage: "[BETA] Format and prettify a given file",
Action: func(c *cli.Context) error {
Action: func(ctx context.Context, c *cli.Command) error {
return exit(FmtFile(args))
},
Flags: args.flags(),
Expand Down
Loading
Loading