Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go: ["1.21", "1.22", "1.23"]
go: ["1.12", "1.21", "1.22", "1.23"]

steps:
- name: Checkout repository
Expand Down
110 changes: 110 additions & 0 deletions bool_func_go1.21_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//go:build go1.21
// +build go1.21

package pflag

import (
"errors"
"flag"
"io"
"strings"
"testing"
)

func TestBoolFuncCompat(t *testing.T) {
// compare behavior with the stdlib 'flag' package
type BoolFuncFlagSet interface {
BoolFunc(name string, usage string, fn func(string) error)
Parse([]string) error
}

unitTestErr := errors.New("unit test error")
runCase := func(f BoolFuncFlagSet, name string, args []string) (values []string, err error) {
fn := func(s string) error {
values = append(values, s)
if s == "err" {
return unitTestErr
}
return nil
}
f.BoolFunc(name, "Callback function", fn)

err = f.Parse(args)
return values, err
}

t.Run("regular parsing", func(t *testing.T) {
flagName := "bflag"
args := []string{"--bflag", "--bflag=false", "--bflag=1", "--bflag=bar", "--bflag="}

// It turns out that, even though the function is called "BoolFunc",
// the standard flag package does not try to parse the value assigned to
// that cli flag as a boolean. The string provided on the command line is
// passed as is to the callback.
// e.g: with "--bflag=not_a_bool" on the command line, the FlagSet does not
// generate an error stating "invalid boolean value", and `fn` will be called
// with "not_a_bool" as an argument.

stdFSet := flag.NewFlagSet("std test", flag.ContinueOnError)
stdValues, err := runCase(stdFSet, flagName, args)
if err != nil {
t.Fatalf("std flag: expected no error, got %v", err)
}
expected := []string{"true", "false", "1", "bar", ""}
if !cmpLists(expected, stdValues) {
t.Fatalf("std flag: expected %v, got %v", expected, stdValues)
}

fset := NewFlagSet("pflag test", ContinueOnError)
pflagValues, err := runCase(fset, flagName, args)
if err != nil {
t.Fatalf("pflag: expected no error, got %v", err)
}
if !cmpLists(stdValues, pflagValues) {
t.Fatalf("pflag: expected %v, got %v", stdValues, pflagValues)
}
})

t.Run("error triggered by callback", func(t *testing.T) {
flagName := "bflag"
args := []string{"--bflag", "--bflag=err", "--bflag=after"}

// test behavior of standard flag.Fset with an error triggered by the callback:
// (note: as can be seen in 'runCase()', if the callback sees "err" as a value
// for the bool flag, it will return an error)
stdFSet := flag.NewFlagSet("std test", flag.ContinueOnError)
stdFSet.SetOutput(io.Discard) // suppress output

// run test case with standard flag.Fset
stdValues, err := runCase(stdFSet, flagName, args)

// double check the standard behavior:
// - .Parse() should return an error, which contains the error message
if err == nil {
t.Fatalf("std flag: expected an error triggered by callback, got no error instead")
}
if !strings.HasSuffix(err.Error(), unitTestErr.Error()) {
t.Fatalf("std flag: expected unittest error, got unexpected error value: %T %v", err, err)
}
// - the function should have been called twice, with the first two values,
// the final "=after" should not be recorded
expected := []string{"true", "err"}
if !cmpLists(expected, stdValues) {
t.Fatalf("std flag: expected %v, got %v", expected, stdValues)
}

// now run the test case on a pflag FlagSet:
fset := NewFlagSet("pflag test", ContinueOnError)
pflagValues, err := runCase(fset, flagName, args)

// check that there is a similar error (note: pflag will _wrap_ the error, while the stdlib
// currently keeps the original message but creates a flat errors.Error)
if !errors.Is(err, unitTestErr) {
t.Fatalf("pflag: got unexpected error value: %T %v", err, err)
}
// the callback should be called the same number of times, with the same values:
if !cmpLists(stdValues, pflagValues) {
t.Fatalf("pflag: expected %v, got %v", stdValues, pflagValues)
}
})
}
101 changes: 0 additions & 101 deletions bool_func_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package pflag

import (
"errors"
"flag"
"io"
"strings"
"testing"
)
Expand Down Expand Up @@ -48,104 +45,6 @@ func TestBoolFuncP(t *testing.T) {
}
}

func TestBoolFuncCompat(t *testing.T) {
// compare behavior with the stdlib 'flag' package
type BoolFuncFlagSet interface {
BoolFunc(name string, usage string, fn func(string) error)
Parse([]string) error
}

unitTestErr := errors.New("unit test error")
runCase := func(f BoolFuncFlagSet, name string, args []string) (values []string, err error) {
fn := func(s string) error {
values = append(values, s)
if s == "err" {
return unitTestErr
}
return nil
}
f.BoolFunc(name, "Callback function", fn)

err = f.Parse(args)
return values, err
}

t.Run("regular parsing", func(t *testing.T) {
flagName := "bflag"
args := []string{"--bflag", "--bflag=false", "--bflag=1", "--bflag=bar", "--bflag="}

// It turns out that, even though the function is called "BoolFunc",
// the standard flag package does not try to parse the value assigned to
// that cli flag as a boolean. The string provided on the command line is
// passed as is to the callback.
// e.g: with "--bflag=not_a_bool" on the command line, the FlagSet does not
// generate an error stating "invalid boolean value", and `fn` will be called
// with "not_a_bool" as an argument.

stdFSet := flag.NewFlagSet("std test", flag.ContinueOnError)
stdValues, err := runCase(stdFSet, flagName, args)
if err != nil {
t.Fatalf("std flag: expected no error, got %v", err)
}
expected := []string{"true", "false", "1", "bar", ""}
if !cmpLists(expected, stdValues) {
t.Fatalf("std flag: expected %v, got %v", expected, stdValues)
}

fset := NewFlagSet("pflag test", ContinueOnError)
pflagValues, err := runCase(fset, flagName, args)
if err != nil {
t.Fatalf("pflag: expected no error, got %v", err)
}
if !cmpLists(stdValues, pflagValues) {
t.Fatalf("pflag: expected %v, got %v", stdValues, pflagValues)
}
})

t.Run("error triggered by callback", func(t *testing.T) {
flagName := "bflag"
args := []string{"--bflag", "--bflag=err", "--bflag=after"}

// test behavior of standard flag.Fset with an error triggered by the callback:
// (note: as can be seen in 'runCase()', if the callback sees "err" as a value
// for the bool flag, it will return an error)
stdFSet := flag.NewFlagSet("std test", flag.ContinueOnError)
stdFSet.SetOutput(io.Discard) // suppress output

// run test case with standard flag.Fset
stdValues, err := runCase(stdFSet, flagName, args)

// double check the standard behavior:
// - .Parse() should return an error, which contains the error message
if err == nil {
t.Fatalf("std flag: expected an error triggered by callback, got no error instead")
}
if !strings.HasSuffix(err.Error(), unitTestErr.Error()) {
t.Fatalf("std flag: expected unittest error, got unexpected error value: %T %v", err, err)
}
// - the function should have been called twice, with the first two values,
// the final "=after" should not be recorded
expected := []string{"true", "err"}
if !cmpLists(expected, stdValues) {
t.Fatalf("std flag: expected %v, got %v", expected, stdValues)
}

// now run the test case on a pflag FlagSet:
fset := NewFlagSet("pflag test", ContinueOnError)
pflagValues, err := runCase(fset, flagName, args)

// check that there is a similar error (note: pflag will _wrap_ the error, while the stdlib
// currently keeps the original message but creates a flat errors.Error)
if !errors.Is(err, unitTestErr) {
t.Fatalf("pflag: got unexpected error value: %T %v", err, err)
}
// the callback should be called the same number of times, with the same values:
if !cmpLists(stdValues, pflagValues) {
t.Fatalf("pflag: expected %v, got %v", stdValues, pflagValues)
}
})
}

func TestBoolFuncUsage(t *testing.T) {
t.Run("regular func flag", func(t *testing.T) {
// regular boolfunc flag:
Expand Down
4 changes: 2 additions & 2 deletions flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -1185,7 +1185,7 @@ func (f *FlagSet) Parse(arguments []string) error {
case ContinueOnError:
return err
case ExitOnError:
if errors.Is(err, ErrHelp) {
if err == ErrHelp {
os.Exit(0)
}
fmt.Fprintln(f.Output(), err)
Expand Down Expand Up @@ -1214,7 +1214,7 @@ func (f *FlagSet) ParseAll(arguments []string, fn func(flag *Flag, value string)
case ContinueOnError:
return err
case ExitOnError:
if errors.Is(err, ErrHelp) {
if err == ErrHelp {
os.Exit(0)
}
fmt.Fprintln(f.Output(), err)
Expand Down
102 changes: 102 additions & 0 deletions func_go1.21_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//go:build go1.21
// +build go1.21

package pflag

import (
"errors"
"flag"
"io"
"strings"
"testing"
)

func TestFuncCompat(t *testing.T) {
// compare behavior with the stdlib 'flag' package
type FuncFlagSet interface {
Func(name string, usage string, fn func(string) error)
Parse([]string) error
}

unitTestErr := errors.New("unit test error")
runCase := func(f FuncFlagSet, name string, args []string) (values []string, err error) {
fn := func(s string) error {
values = append(values, s)
if s == "err" {
return unitTestErr
}
return nil
}
f.Func(name, "Callback function", fn)

err = f.Parse(args)
return values, err
}

t.Run("regular parsing", func(t *testing.T) {
flagName := "fnflag"
args := []string{"--fnflag=xx", "--fnflag", "yy", "--fnflag=zz"}

stdFSet := flag.NewFlagSet("std test", flag.ContinueOnError)
stdValues, err := runCase(stdFSet, flagName, args)
if err != nil {
t.Fatalf("std flag: expected no error, got %v", err)
}
expected := []string{"xx", "yy", "zz"}
if !cmpLists(expected, stdValues) {
t.Fatalf("std flag: expected %v, got %v", expected, stdValues)
}

fset := NewFlagSet("pflag test", ContinueOnError)
pflagValues, err := runCase(fset, flagName, args)
if err != nil {
t.Fatalf("pflag: expected no error, got %v", err)
}
if !cmpLists(stdValues, pflagValues) {
t.Fatalf("pflag: expected %v, got %v", stdValues, pflagValues)
}
})

t.Run("error triggered by callback", func(t *testing.T) {
flagName := "fnflag"
args := []string{"--fnflag", "before", "--fnflag", "err", "--fnflag", "after"}

// test behavior of standard flag.Fset with an error triggered by the callback:
// (note: as can be seen in 'runCase()', if the callback sees "err" as a value
// for the flag, it will return an error)
stdFSet := flag.NewFlagSet("std test", flag.ContinueOnError)
stdFSet.SetOutput(io.Discard) // suppress output

// run test case with standard flag.Fset
stdValues, err := runCase(stdFSet, flagName, args)

// double check the standard behavior:
// - .Parse() should return an error, which contains the error message
if err == nil {
t.Fatalf("std flag: expected an error triggered by callback, got no error instead")
}
if !strings.HasSuffix(err.Error(), unitTestErr.Error()) {
t.Fatalf("std flag: expected unittest error, got unexpected error value: %T %v", err, err)
}
// - the function should have been called twice, with the first two values,
// the final "=after" should not be recorded
expected := []string{"before", "err"}
if !cmpLists(expected, stdValues) {
t.Fatalf("std flag: expected %v, got %v", expected, stdValues)
}

// now run the test case on a pflag FlagSet:
fset := NewFlagSet("pflag test", ContinueOnError)
pflagValues, err := runCase(fset, flagName, args)

// check that there is a similar error (note: pflag will _wrap_ the error, while the stdlib
// currently keeps the original message but creates a flat errors.Error)
if !errors.Is(err, unitTestErr) {
t.Fatalf("pflag: got unexpected error value: %T %v", err, err)
}
// the callback should be called the same number of times, with the same values:
if !cmpLists(stdValues, pflagValues) {
t.Fatalf("pflag: expected %v, got %v", stdValues, pflagValues)
}
})
}
Loading
Loading