Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to a parser-based approach #1

Merged
merged 1 commit into from
Dec 28, 2024
Merged
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
50 changes: 50 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Run Tests
on:
push:
branches:
- main
paths-ignore:
- "*.md"
pull_request:
paths-ignore:
- "*.md"

jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
go:
- 'stable'
- 'oldstable'

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}

- id: go-cache-paths
run: |
echo "::set-output name=go-build::$(go env GOCACHE)"
echo "::set-output name=go-mod::$(go env GOMODCACHE)"

- name: Go Build Cache
uses: actions/[email protected]
with:
path: ${{ steps.go-cache-paths.outputs.go-build }}
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}

- name: Go Mod Cache
uses: actions/[email protected]
with:
path: ${{ steps.go-cache-paths.outputs.go-mod }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}

- name: Run Tests
run: go test ./... -race -v -count=1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage.out
54 changes: 50 additions & 4 deletions application.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ package clif

import (
"context"
"errors"
"fmt"
"os"
"strings"
)

var (
// ErrAppHasNoCommands is returned when an application does not define
// any commands, which is invalid.
ErrAppHasNoCommands = errors.New("application does not define any commands")
)

// Application is the root definition of a CLI.
type Application struct {
// Commands are the commands that the application supports.
Expand All @@ -15,11 +22,9 @@ type Application struct {
// Flags are the definitions for any global flags the application
// supports.
Flags []FlagDef
}

func (Application) argsAccepted() bool { return false }
func (app Application) subcommands() []Command { return app.Commands }
func (app Application) flags() []FlagDef { return app.Flags }
Handler HandlerBuilder
}

// Run executes the invoked command. It routes the input to the appropriate
// [Command], parses it with the [HandlerBuilder], and executes the [Handler].
Expand Down Expand Up @@ -62,3 +67,44 @@ func (app Application) Run(ctx context.Context, opts ...RunOption) int {
handler.Handle(ctx, resp)
return resp.Code
}

// Validate determines whether an [Application] has a valid definition or not.
func (app Application) Validate(ctx context.Context) error {
var errs error
if len(app.Commands) < 1 {
errs = errors.Join(errs, ErrAppHasNoCommands)
}
flagKeys := map[string]struct{}{}
for _, flagDef := range app.Flags {
if _, ok := flagKeys[flagDef.Name]; ok {
errs = errors.Join(errs, DuplicateFlagNameError(flagDef.Name))
}
flagKeys[flagDef.Name] = struct{}{}
for _, alias := range flagDef.Aliases {
if _, ok := flagKeys[alias]; ok {
errs = errors.Join(errs, DuplicateFlagNameError(alias))
}
}
}
cmdNames := map[string]struct{}{}
for pos, cmd := range app.Commands {
if cmd.Name == "" {
errs = errors.Join(errs, CommandMissingNameError{Path: []string{}, Pos: pos})
continue
}
if _, ok := cmdNames[cmd.Name]; ok {
errs = errors.Join(errs, DuplicateCommandError{Path: []string{}, Command: cmd.Name})
}
for _, alias := range cmd.Aliases {
if alias == "" {
errs = errors.Join(errs, CommandAliasEmptyError{Path: []string{}, Command: cmd.Name})
} else if alias == cmd.Name {
errs = errors.Join(errs, CommandDuplicatesNameAsAliasError{Path: []string{}, Command: cmd.Name})
} else if _, ok := cmdNames[alias]; ok {
errs = errors.Join(errs, DuplicateCommandError{Path: []string{}, Command: alias})
}
}
errs = errors.Join(errs, cmd.Validate(ctx, []string{cmd.Name}, flagKeys))
}
return errs
}
34 changes: 15 additions & 19 deletions application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import (
"fmt"

"impractical.co/clif"
"impractical.co/clif/flagtypes"
)

type funcCommandHandler func(ctx context.Context, resp *clif.Response)

func (f funcCommandHandler) Build(_ context.Context, _ map[string]clif.Flag, _ []string, _ *clif.Response) clif.Handler { //nolint:ireturn // filling an interface
func (f funcCommandHandler) Build(_ context.Context, _ clif.FlagSet, _ []string, _ *clif.Response) clif.Handler { //nolint:ireturn // filling an interface
return f
}

Expand All @@ -19,12 +18,12 @@ func (f funcCommandHandler) Handle(ctx context.Context, resp *clif.Response) {
}

type flagCommandHandler struct {
flags map[string]clif.Flag
flags clif.FlagSet
args []string
f func(ctx context.Context, flags map[string]clif.Flag, args []string, resp *clif.Response)
f func(ctx context.Context, flags clif.FlagSet, args []string, resp *clif.Response)
}

func (f flagCommandHandler) Build(_ context.Context, flags map[string]clif.Flag, args []string, _ *clif.Response) clif.Handler { //nolint:ireturn // filling an interface
func (f flagCommandHandler) Build(_ context.Context, flags clif.FlagSet, args []string, _ *clif.Response) clif.Handler { //nolint:ireturn // filling an interface
f.flags = flags
f.args = args
return f
Expand Down Expand Up @@ -52,14 +51,11 @@ func ExampleApplication() {
Name: "bar",
Flags: []clif.FlagDef{
{
Name: "quux",
ValueAccepted: true,
OnlyAfterCommandName: false,
Parser: flagtypes.StringParser{},
Name: "--quux",
},
},
Handler: flagCommandHandler{
f: func(_ context.Context, flags map[string]clif.Flag, args []string, resp *clif.Response) {
f: func(_ context.Context, flags clif.FlagSet, args []string, resp *clif.Response) {
fmt.Fprintln(resp.Output, flags, args)
},
},
Expand All @@ -69,8 +65,8 @@ func ExampleApplication() {
},
Flags: []clif.FlagDef{
{
Name: "baaz",
Parser: flagtypes.BoolParser{},
Name: "--baaz",
IsToggle: true,
},
},
}
Expand All @@ -93,18 +89,18 @@ func ExampleApplication() {
// output:
// this is help information
// 0
// map[quux:{quux hello hello}] []
// map[quux:[{true hello}]] []
// 0
// map[quux:{quux hello hello}] []
// map[quux:[{true hello}]] []
// 0
// map[quux:{quux hello hello}] []
// map[quux:[{true hello}]] []
// 0
// map[quux:{quux hello hello}] []
// map[quux:[{true hello}]] []
// 0
// map[quux:{quux hello hello}] []
// map[quux:[{true hello}]] []
// 0
// map[quux:{quux hello hello}] []
// map[quux:[{true hello}]] []
// 0
// map[baaz:{baaz true} quux:{quux hello hello}] []
// map[baaz:[{false }] quux:[{true hello}]] []
// 0
}
Loading
Loading