Skip to content

Commit 7ad4580

Browse files
authored
Merge branch 'main' into chicks/2026-01-06-fix-3965
2 parents b35f263 + 96a7bf3 commit 7ad4580

File tree

12 files changed

+139
-87
lines changed

12 files changed

+139
-87
lines changed

commands/commands.go

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package commands
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"os"
@@ -13,7 +14,7 @@ import (
1314
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
1415
"github.com/StackExchange/dnscontrol/v4/pkg/version"
1516
"github.com/fatih/color"
16-
"github.com/urfave/cli/v2"
17+
"github.com/urfave/cli/v3"
1718
)
1819

1920
// categories of commands
@@ -34,19 +35,19 @@ func cmd(cat string, c *cli.Command) bool {
3435
var _ = cmd(catDebug, &cli.Command{
3536
Name: "version",
3637
Usage: "Print version information",
37-
Action: func(c *cli.Context) error {
38+
Action: func(ctx context.Context, c *cli.Command) error {
3839
_, err := fmt.Println(version.Version())
3940
return err
4041
},
4142
})
4243

4344
// Run will execute the CLI
4445
func Run(v string) int {
45-
app := cli.NewApp()
46-
app.Version = v
47-
app.Name = "dnscontrol"
48-
app.HideVersion = true
49-
app.Usage = "DNSControl is a compiler and DSL for managing dns zones"
46+
app := &cli.Command{
47+
Name: "dnscontrol",
48+
Usage: "DNSControl is a compiler and DSL for managing dns zones",
49+
Version: v,
50+
}
5051
app.Flags = []cli.Flag{
5152
&cli.BoolFlag{
5253
Name: "debug",
@@ -63,7 +64,7 @@ func Run(v string) int {
6364
Name: "diff2",
6465
Usage: "Obsolete flag. Will be removed in v5 or later",
6566
Hidden: true,
66-
Action: func(ctx *cli.Context, v bool) error {
67+
Action: func(ctx context.Context, c *cli.Command, v bool) error {
6768
pobsoleteDiff2FlagUsed = true
6869
return nil
6970
},
@@ -79,11 +80,29 @@ func Run(v string) int {
7980
Destination: &color.NoColor,
8081
Value: false,
8182
},
83+
&cli.BoolFlag{
84+
Name: "generate-bash-completion",
85+
Usage: "Generate bash completion",
86+
Hidden: true,
87+
},
88+
}
89+
app.Before = func(ctx context.Context, c *cli.Command) (context.Context, error) {
90+
// In v2, EnableBashCompletion would automatically add this flag and trigger completion.
91+
// In v3, we need to handle it manually.
92+
// Only handle at root level - subcommands like shell-completion will handle their own.
93+
if c.Bool("generate-bash-completion") && c.Root() == c {
94+
if c.Root().ShellComplete != nil {
95+
c.Root().ShellComplete(ctx, c)
96+
}
97+
return ctx, cli.Exit("", 0)
98+
}
99+
return ctx, nil
82100
}
83-
sort.Sort(cli.CommandsByName(commands))
101+
sort.Slice(commands, func(i, j int) bool {
102+
return commands[i].Name < commands[j].Name
103+
})
84104
app.Commands = commands
85-
app.EnableBashCompletion = true
86-
app.BashComplete = func(cCtx *cli.Context) {
105+
app.ShellComplete = func(ctx context.Context, c *cli.Command) {
87106
// ripped from cli.DefaultCompleteWithFlags
88107
var lastArg string
89108

@@ -94,14 +113,14 @@ func Run(v string) int {
94113
if lastArg != "" {
95114
if strings.HasPrefix(lastArg, "-") {
96115
if !islastFlagComplete(lastArg, app.Flags) {
97-
dnscontrolPrintFlagSuggestions(lastArg, app.Flags, cCtx.App.Writer)
116+
dnscontrolPrintFlagSuggestions(lastArg, app.Flags, c.Writer)
98117
return
99118
}
100119
}
101120
}
102-
dnscontrolPrintCommandSuggestions(app.Commands, cCtx.App.Writer)
121+
dnscontrolPrintCommandSuggestions(app.Commands, c.Writer)
103122
}
104-
if err := app.Run(os.Args); err != nil {
123+
if err := app.Run(context.Background(), os.Args); err != nil {
105124
return 1
106125
}
107126
return 0
@@ -212,7 +231,7 @@ type ExecuteDSLArgs struct {
212231
JSFile string
213232
JSONFile string
214233
DevMode bool
215-
Variable cli.StringSlice
234+
Variable []string
216235
}
217236

218237
func (args *ExecuteDSLArgs) flags() []cli.Flag {

commands/completion.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package commands
22

33
import (
4+
"context"
45
"embed"
56
"errors"
67
"fmt"
@@ -12,7 +13,7 @@ import (
1213
"text/template"
1314
"unicode/utf8"
1415

15-
"github.com/urfave/cli/v2"
16+
"github.com/urfave/cli/v3"
1617
)
1718

1819
//go:embed completion-scripts/completion.*.gotmpl
@@ -28,32 +29,58 @@ func shellCompletionCommand() *cli.Command {
2829
Usage: "generate shell completion scripts",
2930
ArgsUsage: fmt.Sprintf("[ %s ]", strings.Join(supportedShells, " | ")),
3031
Description: fmt.Sprintf("Generate shell completion script for [ %s ]", strings.Join(supportedShells, " | ")),
31-
BashComplete: func(ctx *cli.Context) {
32+
Flags: []cli.Flag{
33+
&cli.BoolFlag{
34+
Name: "generate-bash-completion",
35+
Usage: "Generate bash completion",
36+
Hidden: true,
37+
},
38+
},
39+
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
40+
// In v2, EnableBashCompletion would automatically add this flag and trigger completion.
41+
// In v3, we need to handle it manually.
42+
// This runs before Action, so we intercept the flag here.
43+
if cmd.Bool("generate-bash-completion") {
44+
if cmd.ShellComplete != nil {
45+
cmd.ShellComplete(ctx, cmd)
46+
}
47+
// Mark that we handled completion so Action can skip
48+
return context.WithValue(ctx, "completionHandled", true), nil
49+
}
50+
return ctx, nil
51+
},
52+
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
3253
for _, shell := range supportedShells {
33-
if strings.HasPrefix(shell, ctx.Args().First()) {
34-
if _, err := ctx.App.Writer.Write([]byte(shell + "\n")); err != nil {
54+
if strings.HasPrefix(shell, cmd.Args().First()) {
55+
if _, err := cmd.Root().Writer.Write([]byte(shell + "\n")); err != nil {
3556
panic(err)
3657
}
3758
}
3859
}
3960
},
40-
Action: func(ctx *cli.Context) error {
61+
Action: func(ctx context.Context, cmd *cli.Command) error {
62+
// If completion was handled in Before, skip normal action
63+
if ctx.Value("completionHandled") != nil {
64+
return nil
65+
}
66+
4167
var inputShell string
42-
if inputShell = ctx.Args().First(); inputShell == "" {
68+
if inputShell = cmd.Args().First(); inputShell == "" {
4369
if inputShell = os.Getenv("SHELL"); inputShell == "" {
4470
return cli.Exit(errors.New("shell not specified"), 1)
4571
}
4672
}
4773
shellName := path.Base(inputShell) // necessary if using $SHELL, noop otherwise
74+
//fmt.Printf("DEBUG: shellName = %q\n", shellName)
4875

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

54-
err = template.Execute(ctx.App.Writer, struct {
55-
App *cli.App
56-
}{ctx.App})
81+
err = template.Execute(cmd.Root().Writer, struct {
82+
App *cli.Command
83+
}{cmd.Root()})
5784
if err != nil {
5885
return cli.Exit(fmt.Errorf("failed to print completion script: %w", err), 1)
5986
}

commands/completion_test.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ package commands
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
67
"slices"
78
"strings"
89
"testing"
910
"text/template"
1011

1112
"github.com/google/go-cmp/cmp"
12-
"github.com/urfave/cli/v2"
13+
"github.com/urfave/cli/v3"
1314
)
1415

1516
type shellTestDataItem struct {
@@ -19,15 +20,15 @@ type shellTestDataItem struct {
1920
}
2021

2122
// setupTestShellCompletionCommand resets the buffers used to capture output and errors from the app.
22-
func setupTestShellCompletionCommand(app *cli.App) func(t *testing.T) {
23+
func setupTestShellCompletionCommand(app *cli.Command) func(t *testing.T) {
2324
return func(t *testing.T) {
2425
app.Writer.(*bytes.Buffer).Reset()
2526
cli.ErrWriter.(*bytes.Buffer).Reset()
2627
}
2728
}
2829

2930
func TestShellCompletionCommand(t *testing.T) {
30-
app := cli.NewApp()
31+
app := &cli.Command{}
3132
app.Name = "testing"
3233

3334
var appWriterBuffer bytes.Buffer
@@ -67,7 +68,7 @@ func TestShellCompletionCommand(t *testing.T) {
6768
tearDownTest := setupTestShellCompletionCommand(app)
6869
defer tearDownTest(t)
6970

70-
err := app.Run([]string{app.Name, "shell-completion", tt.shellName})
71+
err := app.Run(context.Background(), []string{app.Name, "shell-completion", tt.shellName})
7172
if err != nil {
7273
t.Fatalf("unexpected error: %v", err)
7374
}
@@ -92,7 +93,7 @@ func TestShellCompletionCommand(t *testing.T) {
9293
tearDownTest := setupTestShellCompletionCommand(app)
9394
defer tearDownTest(t)
9495

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

9798
if err == nil {
9899
t.Fatal("expected error, but didn't get one")
@@ -120,7 +121,7 @@ func TestShellCompletionCommand(t *testing.T) {
120121

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

123-
err := app.Run([]string{app.Name, "shell-completion"})
124+
err := app.Run(context.Background(), []string{app.Name, "shell-completion"})
124125
if err != nil {
125126
t.Fatalf("unexpected error: %v", err)
126127
}
@@ -147,7 +148,7 @@ func TestShellCompletionCommand(t *testing.T) {
147148

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

150-
err := app.Run([]string{app.Name, "shell-completion"})
151+
err := app.Run(context.Background(), []string{app.Name, "shell-completion"})
151152
if err == nil {
152153
t.Fatal("expected error, but didn't get one")
153154
}
@@ -190,12 +191,21 @@ func TestShellCompletionCommand(t *testing.T) {
190191
t.Run(tC.shellArg, func(t *testing.T) {
191192
tearDownTest := setupTestShellCompletionCommand(app)
192193
defer tearDownTest(t)
193-
app.EnableBashCompletion = true
194-
defer func() {
195-
app.EnableBashCompletion = false
196-
}()
197194

198-
err := app.Run([]string{app.Name, "shell-completion", tC.shellArg, "--generate-bash-completion"})
195+
cmdargs := []string{app.Name,
196+
"shell-completion",
197+
tC.shellArg,
198+
"--generate-bash-completion",
199+
}
200+
if tC.shellArg == "" {
201+
// remove empty argument to simulate user not providing it
202+
cmdargs = []string{app.Name,
203+
"shell-completion",
204+
"--generate-bash-completion",
205+
}
206+
}
207+
//fmt.Printf("DEBUG: app.Run(%v)\n", cmdargs)
208+
err := app.Run(context.Background(), cmdargs)
199209
if err != nil {
200210
t.Fatalf("unexpected error: %v", err)
201211
}
@@ -239,10 +249,10 @@ func testHelperGetShellsAndCompletionScripts() ([]shellTestDataItem, error) {
239249

240250
// testHelperRenderTemplateFromApp renders a given template with a given app.
241251
// This is used to test the output of the CLI command against a 'known good' value.
242-
func testHelperRenderTemplateFromApp(app *cli.App, scriptTemplate *template.Template) (string, error) {
252+
func testHelperRenderTemplateFromApp(app *cli.Command, scriptTemplate *template.Template) (string, error) {
243253
var scriptBytes bytes.Buffer
244254
err := scriptTemplate.Execute(&scriptBytes, struct {
245-
App *cli.App
255+
App *cli.Command
246256
}{app})
247257

248258
return scriptBytes.String(), err

commands/createDomains.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
package commands
22

33
import (
4+
"context"
45
"fmt"
56

67
"github.com/StackExchange/dnscontrol/v4/pkg/credsfile"
78
"github.com/StackExchange/dnscontrol/v4/pkg/providers"
8-
"github.com/urfave/cli/v2"
9+
"github.com/urfave/cli/v3"
910
)
1011

1112
var _ = cmd(catUtils, func() *cli.Command {
1213
var args CreateDomainsArgs
1314
return &cli.Command{
1415
Name: "create-domains",
1516
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.)",
16-
Action: func(ctx *cli.Context) error {
17+
Action: func(ctx context.Context, c *cli.Command) error {
1718
return exit(CreateDomains(args))
1819
},
1920
Flags: args.flags(),
20-
Before: func(context *cli.Context) error {
21+
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
2122
fmt.Println("DEPRECATED: This command is deprecated. The domain is automatically created at the Domain Service Provider during the push command.")
2223
fmt.Println("DEPRECATED: To prevent disable auto-creating, use --no-populate with the push command.")
23-
return nil
24+
return ctx, nil
2425
},
2526
}
2627
}())

commands/fmt.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
package commands
22

33
import (
4+
"context"
45
"fmt"
56
"io"
67
"os"
78
"strings"
89

910
"github.com/ditashi/jsbeautifier-go/jsbeautifier"
10-
"github.com/urfave/cli/v2"
11+
"github.com/urfave/cli/v3"
1112
)
1213

1314
var _ = cmd(catUtils, func() *cli.Command {
1415
var args FmtArgs
1516
return &cli.Command{
1617
Name: "fmt",
1718
Usage: "[BETA] Format and prettify a given file",
18-
Action: func(c *cli.Context) error {
19+
Action: func(ctx context.Context, c *cli.Command) error {
1920
return exit(FmtFile(args))
2021
},
2122
Flags: args.flags(),

0 commit comments

Comments
 (0)