Skip to content

Commit

Permalink
Operand support and custom errors
Browse files Browse the repository at this point in the history
  • Loading branch information
ibraimgm committed Sep 26, 2019
1 parent 69d8b6f commit 9c2f1ed
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 17 deletions.
8 changes: 6 additions & 2 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,19 @@ type Options struct {
// help flag is set
SupressPrintHelpWhenSet bool

// when true, do not print the help automatically when a command with
// When true, do not print the help automatically when a command with
// subcommands and without a Run callback is executed
SuppressPrintHelpPartialCommand bool

// When true, returns an parsing error when the number of operands does not match
// the expected number of required operands (optional and repeated operands are ignored).
StrictOperands bool

// When set, redirect the help output to the specified writer.
// When it is nil, the help text will be printed to Stdout
HelpOutput io.Writer

// function that overrides the auto-generated help text.
// Function that overrides the auto-generated help text.
OnHelp HelpCallback
}

Expand Down
38 changes: 37 additions & 1 deletion cmd.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package libcmd

type operand struct {
name string
modifier string
}

// Cmd defines a (sub)command of the application.
// Since commands cannot do much by themselves, you should create
// your commands by calling the Command method in the App instance.
Expand All @@ -19,6 +24,9 @@ type Cmd struct {
// Set this value to "-" to omit the usage line
Usage string

// Options for this command
Options Options

args []string
optentries []*optEntry
shortopt map[string]*optEntry
Expand All @@ -30,7 +38,7 @@ type Cmd struct {
breadcrumbs string
commands map[string]*Cmd
parentCmd *Cmd
Options Options
operands []operand
}

func newCmd() *Cmd {
Expand Down Expand Up @@ -98,6 +106,34 @@ func (cmd *Cmd) CommandRun(name, brief string, callback RunCallback) {
}
}

// AddOperand documents an expected operand.
// The modifier parameter can be either '?' for optional operands or '*'
// for repeating ones. The documentation is printed in the order that
// was used to add the operands, so it is advisable to put them in an order
// that makes sense for the user (required, optional and repeating, in this order).
//
// Note that this modifier is used only for documentation purposes; no special validation
// is done, except by the one documented in Options.StrictOperands.
func (cmd *Cmd) AddOperand(name string, modifer string) {
cmd.operands = append(cmd.operands, operand{name: name, modifier: modifer})
}

// Operand returns the value of the named operand, if any.
// When specified using AddOperand, each unparsed arg is considered an operand and it's
// value is fetched - but not consumed - from the Args() method.
//
// The behavior of this function is only guaranteed when used in a 'leaf' command or
// and Run() callback.
func (cmd *Cmd) Operand(name string) string {
for i, op := range cmd.operands {
if op.name == name && i < len(cmd.args) {
return cmd.args[i]
}
}

return ""
}

func (cmd *Cmd) setupHelp() {
// no automatic '-h' flag
if cmd.Options.SuppressHelpFlag {
Expand Down
2 changes: 1 addition & 1 deletion cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func TestCommandArgReuse(t *testing.T) {
})

err := app.RunArgs(test.cmd)
if test.expectError && err == nil {
if test.expectError && !libcmd.IsParserErr(err) {
t.Errorf("Case %d, should have returned error", i)
continue
} else if !test.expectError && err != nil {
Expand Down
2 changes: 1 addition & 1 deletion custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ func (c *choiceString) Set(value string) error {
}
}

return fmt.Errorf("'%s' is not a valid value (possible values: %s)", value, strings.Join(c.choices, ","))
return parserError{err: fmt.Errorf("'%s' is not a valid value (possible values: %s)", value, strings.Join(c.choices, ","))}
}
99 changes: 99 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package libcmd

import (
"fmt"
)

// parsing: generic parser error
type parserError struct {
arg string
err error
}

func (e parserError) Error() string {
if e.arg != "" {
return fmt.Sprintf("error parsing argument '%s': %v", e.arg, e.err)
}

return fmt.Sprintf("parsing error: %v", e.err)
}

// parsing: unknown argument
type unknownArgErr struct {
arg string
}

func (e unknownArgErr) Error() string {
return fmt.Sprintf("unknown argument: %s", e.arg)
}

// parsing: no value for argument
type noValueErr struct {
arg string
}

func (e noValueErr) Error() string {
return fmt.Sprintf("no value for argument: %s", e.arg)
}

// parsing: conversion error
type conversionErr struct {
value interface{}
typeName string
}

func (e conversionErr) Error() string {
return fmt.Sprintf("'%v' is not a valid %s value", e.value, e.typeName)
}

// parsing: unsupported type
type unsupportedErr struct {
value interface{}
typeName string
}

func (e unsupportedErr) Error() string {
return fmt.Sprintf("unsupported type '%s' for value '%s'", e.typeName, e.value)
}

// parsing: wrong number of operands
type operandRequiredErr struct {
required int
got int
exact bool
}

func (e operandRequiredErr) Error() string {
if e.exact {
return fmt.Sprintf("wrong number of operands, exactly %d required (got %d)", e.required, e.got)
}
return fmt.Sprintf("wrong number of operands, at least %d required (got %d)", e.required, e.got)
}

// IsParserErr returns true is the error is an error
// generated by the parsing process itself.
func IsParserErr(err error) bool {
if err == nil {
return false
}

switch err.(type) {
case parserError:
return true

case unknownArgErr:
return true

case noValueErr:
return true

case conversionErr:
return true

case operandRequiredErr:
return true

default:
return false
}
}
26 changes: 26 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package libcmd_test

import (
"errors"
"testing"

"github.com/ibraimgm/libcmd"
)

func TestIsParserErr(t *testing.T) {
tests := []struct {
err error
expected bool
}{
{err: nil, expected: false},
{err: errors.New("my error"), expected: false},
}

for i, test := range tests {
actual := libcmd.IsParserErr(test.err)

if actual != test.expected {
t.Errorf("Case %d, expected '%v', but comparison returned '%v'", i, test.expected, actual)
}
}
}
2 changes: 1 addition & 1 deletion get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func TestGetChoice(t *testing.T) {
continue
}

if test.expectErr && err == nil {
if test.expectErr && !libcmd.IsParserErr(err) {
t.Errorf("Case %d, expected error but none received", i)
continue
}
Expand Down
46 changes: 41 additions & 5 deletions opt.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package libcmd

import (
"fmt"
"strings"
)

Expand Down Expand Up @@ -86,11 +85,11 @@ func (entry *optEntry) setValue(arg *optArg) error {
// the option '--string=' is the only case where
// an empty value should be accepted
if arg.value == "" && !(entry.val.isStr && arg.isEq) {
return fmt.Errorf("no value for argument: %s", arg.name)
return noValueErr{arg: arg.name}
}

if err := entry.val.setValue(arg.value); err != nil {
return fmt.Errorf("error parsing argument '%s': %v", arg.name, err)
return parserError{arg: arg.name, err: err}
}

if entry.val.isBool && arg.isNeg && arg.isEq {
Expand Down Expand Up @@ -148,7 +147,7 @@ func (cmd *Cmd) doParse(args []string) error {
// if no entry exists, this argument is unknown
entry := cmd.findOpt(arg.name)
if entry == nil {
return fmt.Errorf("unknown argument: %s", arg.name)
return unknownArgErr{arg: arg.name}
}

// some argument types have autmatic values in certain cases
Expand All @@ -159,7 +158,7 @@ func (cmd *Cmd) doParse(args []string) error {
// long params with '=' should not be considered
if !arg.isEq && arg.value == "" {
if i+1 == len(args) {
return fmt.Errorf("no value for argument: %s", arg.name)
return noValueErr{arg: arg.name}
}

arg.value = args[i+1]
Expand Down Expand Up @@ -236,6 +235,11 @@ func (cmd *Cmd) runLeafCommand() error {
return nil
}

// check for operands
if err := cmd.checkOperands(); err != nil {
return err
}

// actual command, as defined by the user
if cmd.run != nil {
return cmd.run(cmd)
Expand All @@ -255,6 +259,38 @@ func (cmd *Cmd) runLeafCommand() error {
return nil
}

func (cmd *Cmd) checkOperands() error {
// if we're permissive, there's nothing to do
if !cmd.Options.StrictOperands {
return nil
}

// consider only the required ones
var need int
var hasOptionals bool
for _, op := range cmd.operands {
if op.modifier == "" {
need++
} else {
hasOptionals = true
}
}

// if at least one is optional, no need for an exact number of
// arguments
if hasOptionals && need > len(cmd.args) {
return operandRequiredErr{required: need, got: len(cmd.args)}
}

// if e do not have optional arguments, we need an exact number
if !hasOptionals && need != len(cmd.args) {
return operandRequiredErr{required: need, got: len(cmd.args), exact: true}
}

// we should be good to go now
return nil
}

// Args returns the remaining non-parsed command line arguments.
func (cmd *Cmd) Args() []string {
return cmd.args
Expand Down
Loading

0 comments on commit 9c2f1ed

Please sign in to comment.