Compare commits

...

8 Commits

Author SHA1 Message Date
Jeff Carr 8ecad2c6af go test ? 2025-08-29 10:39:34 -05:00
Jeff Carr 8b32c8d316 change namespace 2025-08-29 07:28:16 -05:00
Jeff Carr 6cc03a124c allows GO libraries to have common args 2025-08-29 07:09:32 -05:00
Alex Flint e778224db2 improve handling of negative numbers 2025-05-23 11:01:34 -04:00
Alex Flint 7155e7e986
Merge pull request #278 from Areson/ioberst-global-help-options
Fix Missing Global Options
2025-03-07 17:08:54 -05:00
Ian Oberst a1a65e29cf Fix Missing Global Options 2025-02-11 09:26:24 -08:00
Alex Flint f21878956c
Merge pull request #276 from zanvd/make-placeholder-output-consistent
Make field name output consistent when using placeholders
2024-12-11 17:25:19 +11:00
Žan V. Dragan a7ee83cf5d Use common output for usage and errors during parsing. 2024-12-05 21:24:09 +01:00
4 changed files with 149 additions and 37 deletions

21
Makefile Normal file
View File

@ -0,0 +1,21 @@
all:
@echo
@echo
clean:
rm -f go.*
redomod:
rm -f go.*
GO111MODULE= go mod init
GO111MODULE= go mod tidy
test:
go test
vet:
@GO111MODULE=off go vet
@echo this go binary package builds okay
goimports:
goimports -w *.go

2
go.mod
View File

@ -1,4 +1,4 @@
module github.com/alexflint/go-arg
module go.wit.com/dev/alexflint/arg
require (
github.com/alexflint/go-scalar v1.2.0

View File

@ -80,9 +80,26 @@ var ErrVersion = errors.New("version requested by user")
var mustParseExit = os.Exit
var mustParseOut io.Writer = os.Stdout
/*
This allows you to have common arg values defined in a GO package
package 'foo'
function init() {
args.Register(&argsFoo)
}
*/
// This stores the args sent from the GO packages
var register []interface{}
func Register(dest ...interface{}) {
register = append(register, dest...)
}
// MustParse processes command line arguments and exits upon failure
func MustParse(dest ...interface{}) *Parser {
return mustParse(Config{Exit: mustParseExit, Out: mustParseOut}, dest...)
register = append(register, dest...)
return mustParse(Config{Exit: mustParseExit, Out: mustParseOut}, register...)
}
// mustParse is a helper that facilitates testing
@ -285,6 +302,13 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
}
}
// Set the parent of the subcommands to be the top-level command
// to make sure that global options work when there is more than one
// dest supplied.
for _, subcommand := range p.cmd.subcommands {
subcommand.parent = p.cmd
}
return &p, nil
}
@ -693,7 +717,7 @@ func (p *Parser) process(args []string) error {
if spec.cardinality == multiple {
var values []string
if value == "" {
for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" {
for i+1 < len(args) && isValue(args[i+1], spec.field.Type, specs) && args[i+1] != "--" {
values = append(values, args[i+1])
i++
if spec.separate {
@ -721,7 +745,7 @@ func (p *Parser) process(args []string) error {
if i+1 == len(args) {
return fmt.Errorf("missing value for %s", arg)
}
if !nextIsNumeric(spec.field.Type, args[i+1]) && isFlag(args[i+1]) {
if !isValue(args[i+1], spec.field.Type, specs) {
return fmt.Errorf("missing value for %s", arg)
}
value = args[i+1]
@ -746,13 +770,13 @@ func (p *Parser) process(args []string) error {
if spec.cardinality == multiple {
err := setSliceOrMap(p.val(spec.dest), positionals, true)
if err != nil {
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
}
positionals = nil
} else {
err := scalar.ParseValue(p.val(spec.dest), positionals[0])
if err != nil {
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
}
positionals = positionals[1:]
}
@ -767,18 +791,13 @@ func (p *Parser) process(args []string) error {
continue
}
name := strings.ToLower(spec.field.Name)
if spec.long != "" && !spec.positional {
name = "--" + spec.long
}
if spec.required {
if spec.short == "" && spec.long == "" {
msg := fmt.Sprintf("environment variable %s is required", spec.env)
return errors.New(msg)
}
msg := fmt.Sprintf("%s is required", name)
msg := fmt.Sprintf("%s is required", spec.placeholder)
if spec.env != "" {
msg += " (or environment variable " + spec.env + ")"
}
@ -799,24 +818,31 @@ func (p *Parser) process(args []string) error {
return nil
}
func nextIsNumeric(t reflect.Type, s string) bool {
switch t.Kind() {
case reflect.Ptr:
return nextIsNumeric(t.Elem(), s)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
v := reflect.New(t)
err := scalar.ParseValue(v, s)
return err == nil
default:
return false
}
}
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
func isFlag(s string) bool {
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
}
// isValue returns true if a token should be consumed as a value for a flag of type t. This
// is almost always the inverse of isFlag. The one exception is for negative numbers, in which
// case we check the list of active options and return true if its not present there.
func isValue(s string, t reflect.Type, specs []*spec) bool {
switch t.Kind() {
case reflect.Ptr, reflect.Slice:
return isValue(s, t.Elem(), specs)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
v := reflect.New(t)
err := scalar.ParseValue(v, s)
// if value can be parsed and is not an explicit option declared elsewhere, then use it as a value
if err == nil && (!strings.HasPrefix(s, "-") || findOption(specs, strings.TrimPrefix(s, "-")) == nil) {
return true
}
}
// default case that is used in all cases other than negative numbers: inverse of isFlag
return !isFlag(s)
}
// val returns a reflect.Value corresponding to the current value for the
// given path
func (p *Parser) val(dest path) reflect.Value {

View File

@ -120,17 +120,91 @@ func TestNegativeInt(t *testing.T) {
assert.EqualValues(t, args.Foo, -100)
}
func TestNegativeFloat(t *testing.T) {
var args struct {
Foo float64
}
err := parse("-foo -99", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Foo, -99)
}
func TestNumericFlag(t *testing.T) {
var args struct {
UseIPv6 bool `arg:"-6"`
Foo int
}
err := parse("-6", &args)
require.NoError(t, err)
assert.EqualValues(t, args.UseIPv6, true)
}
func TestNumericFlagTakesPrecedence(t *testing.T) {
var args struct {
UseIPv6 bool `arg:"-6"`
Foo int
}
err := parse("-foo -6", &args)
require.Error(t, err)
}
func TestRepeatedNegativeInts(t *testing.T) {
var args struct {
Ints []int `arg:"--numbers"`
}
err := parse("--numbers -1 -2 -6", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Ints, []int{-1, -2, -6})
}
func TestRepeatedNegativeFloats(t *testing.T) {
var args struct {
Floats []float32 `arg:"--numbers"`
}
err := parse("--numbers -1 -2 -6", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Floats, []float32{-1, -2, -6})
}
func TestRepeatedNegativeFloatsThenNumericFlag(t *testing.T) {
var args struct {
Floats []float32 `arg:"--numbers"`
UseIPv6 bool `arg:"-6"`
}
err := parse("--numbers -1 -2 -6", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Floats, []float32{-1, -2})
assert.True(t, args.UseIPv6)
}
func TestRepeatedNegativeFloatsThenNonexistentFlag(t *testing.T) {
var args struct {
Floats []float32 `arg:"--numbers"`
UseIPv6 bool `arg:"-6"`
}
err := parse("--numbers -1 -2 -n", &args)
require.Error(t, err, "unknown argument -n")
}
func TestRepeatedNegativeIntsThenFloat(t *testing.T) {
var args struct {
Ints []int `arg:"--numbers"`
}
err := parse("--numbers -1 -2 -0.1", &args)
require.Error(t, err, "unknown argument -0.1")
}
func TestNegativeIntAndFloatAndTricks(t *testing.T) {
var args struct {
Foo int
Bar float64
N int `arg:"--100"`
}
err := parse("-foo -100 -bar -60.14 -100 -100", &args)
err := parse("-foo -99 -bar -60.14 -100 -101", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Foo, -100)
assert.EqualValues(t, args.Foo, -99)
assert.EqualValues(t, args.Bar, -60.14)
assert.EqualValues(t, args.N, -100)
assert.EqualValues(t, args.N, -101)
}
func TestUint(t *testing.T) {
@ -525,15 +599,6 @@ func TestMissingValueInMiddle(t *testing.T) {
assert.Error(t, err)
}
func TestNegativeValue(t *testing.T) {
var args struct {
Foo int
}
err := parse("--foo -123", &args)
require.NoError(t, err)
assert.Equal(t, -123, args.Foo)
}
func TestInvalidInt(t *testing.T) {
var args struct {
Foo int