blob: 374f21cc50fdb85664dafaaf5af57bb60551a2e7 [file] [log] [blame]
package flags
import (
"fmt"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
"time"
)
type defaultOptions struct {
Int int `long:"i"`
IntDefault int `long:"id" default:"1"`
Float64 float64 `long:"f"`
Float64Default float64 `long:"fd" default:"-3.14"`
NumericFlag bool `short:"3"`
String string `long:"str"`
StringDefault string `long:"strd" default:"abc"`
StringNotUnquoted string `long:"strnot" unquote:"false"`
Time time.Duration `long:"t"`
TimeDefault time.Duration `long:"td" default:"1m"`
Map map[string]int `long:"m"`
MapDefault map[string]int `long:"md" default:"a:1"`
Slice []int `long:"s"`
SliceDefault []int `long:"sd" default:"1" default:"2"`
}
func TestDefaults(t *testing.T) {
var tests = []struct {
msg string
args []string
expected defaultOptions
}{
{
msg: "no arguments, expecting default values",
args: []string{},
expected: defaultOptions{
Int: 0,
IntDefault: 1,
Float64: 0.0,
Float64Default: -3.14,
NumericFlag: false,
String: "",
StringDefault: "abc",
Time: 0,
TimeDefault: time.Minute,
Map: map[string]int{},
MapDefault: map[string]int{"a": 1},
Slice: []int{},
SliceDefault: []int{1, 2},
},
},
{
msg: "non-zero value arguments, expecting overwritten arguments",
args: []string{"--i=3", "--id=3", "--f=-2.71", "--fd=2.71", "-3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"},
expected: defaultOptions{
Int: 3,
IntDefault: 3,
Float64: -2.71,
Float64Default: 2.71,
NumericFlag: true,
String: "def",
StringDefault: "def",
Time: 3 * time.Millisecond,
TimeDefault: 3 * time.Millisecond,
Map: map[string]int{"c": 3},
MapDefault: map[string]int{"c": 3},
Slice: []int{3},
SliceDefault: []int{3},
},
},
{
msg: "zero value arguments, expecting overwritten arguments",
args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"},
expected: defaultOptions{
Int: 0,
IntDefault: 0,
Float64: 0,
Float64Default: 0,
String: "",
StringDefault: "",
Time: 0,
TimeDefault: 0,
Map: map[string]int{"": 0},
MapDefault: map[string]int{"": 0},
Slice: []int{0},
SliceDefault: []int{0},
},
},
}
for _, test := range tests {
var opts defaultOptions
_, err := ParseArgs(&opts, test.args)
if err != nil {
t.Fatalf("%s:\nUnexpected error: %v", test.msg, err)
}
if opts.Slice == nil {
opts.Slice = []int{}
}
if !reflect.DeepEqual(opts, test.expected) {
t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts)
}
}
}
func TestNoDefaultsForBools(t *testing.T) {
var opts struct {
DefaultBool bool `short:"d" default:"true"`
}
if runtime.GOOS == "windows" {
assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned on", &opts)
} else {
assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned on", &opts)
}
}
func TestUnquoting(t *testing.T) {
var tests = []struct {
arg string
err error
value string
}{
{
arg: "\"abc",
err: strconv.ErrSyntax,
value: "",
},
{
arg: "\"\"abc\"",
err: strconv.ErrSyntax,
value: "",
},
{
arg: "\"abc\"",
err: nil,
value: "abc",
},
{
arg: "\"\\\"abc\\\"\"",
err: nil,
value: "\"abc\"",
},
{
arg: "\"\\\"abc\"",
err: nil,
value: "\"abc",
},
}
for _, test := range tests {
var opts defaultOptions
for _, delimiter := range []bool{false, true} {
p := NewParser(&opts, None)
var err error
if delimiter {
_, err = p.ParseArgs([]string{"--str=" + test.arg, "--strnot=" + test.arg})
} else {
_, err = p.ParseArgs([]string{"--str", test.arg, "--strnot", test.arg})
}
if test.err == nil {
if err != nil {
t.Fatalf("Expected no error but got: %v", err)
}
if test.value != opts.String {
t.Fatalf("Expected String to be %q but got %q", test.value, opts.String)
}
if q := strconv.Quote(test.value); q != opts.StringNotUnquoted {
t.Fatalf("Expected StringDefault to be %q but got %q", q, opts.StringNotUnquoted)
}
} else {
if err == nil {
t.Fatalf("Expected error")
} else if e, ok := err.(*Error); ok {
if strings.HasPrefix(e.Message, test.err.Error()) {
t.Fatalf("Expected error message to end with %q but got %v", test.err.Error(), e.Message)
}
}
}
}
}
}
// EnvRestorer keeps a copy of a set of env variables and can restore the env from them
type EnvRestorer struct {
env map[string]string
}
func (r *EnvRestorer) Restore() {
os.Clearenv()
for k, v := range r.env {
os.Setenv(k, v)
}
}
// EnvSnapshot returns a snapshot of the currently set env variables
func EnvSnapshot() *EnvRestorer {
r := EnvRestorer{make(map[string]string)}
for _, kv := range os.Environ() {
parts := strings.SplitN(kv, "=", 2)
if len(parts) != 2 {
panic("got a weird env variable: " + kv)
}
r.env[parts[0]] = parts[1]
}
return &r
}
type envDefaultOptions struct {
Int int `long:"i" default:"1" env:"TEST_I"`
Time time.Duration `long:"t" default:"1m" env:"TEST_T"`
Map map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"`
Slice []int `long:"s" default:"1" default:"2" env:"TEST_S" env-delim:","`
}
func TestEnvDefaults(t *testing.T) {
var tests = []struct {
msg string
args []string
expected envDefaultOptions
env map[string]string
}{
{
msg: "no arguments, no env, expecting default values",
args: []string{},
expected: envDefaultOptions{
Int: 1,
Time: time.Minute,
Map: map[string]int{"a": 1},
Slice: []int{1, 2},
},
},
{
msg: "no arguments, env defaults, expecting env default values",
args: []string{},
expected: envDefaultOptions{
Int: 2,
Time: 2 * time.Minute,
Map: map[string]int{"a": 2, "b": 3},
Slice: []int{4, 5, 6},
},
env: map[string]string{
"TEST_I": "2",
"TEST_T": "2m",
"TEST_M": "a:2;b:3",
"TEST_S": "4,5,6",
},
},
{
msg: "non-zero value arguments, expecting overwritten arguments",
args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3"},
expected: envDefaultOptions{
Int: 3,
Time: 3 * time.Millisecond,
Map: map[string]int{"c": 3},
Slice: []int{3},
},
env: map[string]string{
"TEST_I": "2",
"TEST_T": "2m",
"TEST_M": "a:2;b:3",
"TEST_S": "4,5,6",
},
},
{
msg: "zero value arguments, expecting overwritten arguments",
args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0"},
expected: envDefaultOptions{
Int: 0,
Time: 0,
Map: map[string]int{"": 0},
Slice: []int{0},
},
env: map[string]string{
"TEST_I": "2",
"TEST_T": "2m",
"TEST_M": "a:2;b:3",
"TEST_S": "4,5,6",
},
},
}
oldEnv := EnvSnapshot()
defer oldEnv.Restore()
for _, test := range tests {
var opts envDefaultOptions
oldEnv.Restore()
for envKey, envValue := range test.env {
os.Setenv(envKey, envValue)
}
_, err := ParseArgs(&opts, test.args)
if err != nil {
t.Fatalf("%s:\nUnexpected error: %v", test.msg, err)
}
if opts.Slice == nil {
opts.Slice = []int{}
}
if !reflect.DeepEqual(opts, test.expected) {
t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts)
}
}
}
func TestOptionAsArgument(t *testing.T) {
var tests = []struct {
args []string
expectError bool
errType ErrorType
errMsg string
rest []string
}{
{
// short option must not be accepted as argument
args: []string{"--string-slice", "foobar", "--string-slice", "-o"},
expectError: true,
errType: ErrExpectedArgument,
errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-o'",
},
{
// long option must not be accepted as argument
args: []string{"--string-slice", "foobar", "--string-slice", "--other-option"},
expectError: true,
errType: ErrExpectedArgument,
errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `--other-option'",
},
{
// long option must not be accepted as argument
args: []string{"--string-slice", "--"},
expectError: true,
errType: ErrExpectedArgument,
errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got double dash `--'",
},
{
// quoted and appended option should be accepted as argument (even if it looks like an option)
args: []string{"--string-slice", "foobar", "--string-slice=\"--other-option\""},
},
{
// Accept any single character arguments including '-'
args: []string{"--string-slice", "-"},
},
{
// Do not accept arguments which start with '-' even if the next character is a digit
args: []string{"--string-slice", "-3.14"},
expectError: true,
errType: ErrExpectedArgument,
errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-3.14'",
},
{
// Do not accept arguments which start with '-' if the next character is not a digit
args: []string{"--string-slice", "-character"},
expectError: true,
errType: ErrExpectedArgument,
errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-character'",
},
{
args: []string{"-o", "-", "-"},
rest: []string{"-", "-"},
},
{
// Accept arguments which start with '-' if the next character is a digit, for number options only
args: []string{"--int-slice", "-3"},
},
{
// Accept arguments which start with '-' if the next character is a digit, for number options only
args: []string{"--int16", "-3"},
},
{
// Accept arguments which start with '-' if the next character is a digit, for number options only
args: []string{"--float32", "-3.2"},
},
{
// Accept arguments which start with '-' if the next character is a digit, for number options only
args: []string{"--float32ptr", "-3.2"},
},
}
var opts struct {
StringSlice []string `long:"string-slice"`
IntSlice []int `long:"int-slice"`
Int16 int16 `long:"int16"`
Float32 float32 `long:"float32"`
Float32Ptr *float32 `long:"float32ptr"`
OtherOption bool `long:"other-option" short:"o"`
}
for _, test := range tests {
if test.expectError {
assertParseFail(t, test.errType, test.errMsg, &opts, test.args...)
} else {
args := assertParseSuccess(t, &opts, test.args...)
assertStringArray(t, args, test.rest)
}
}
}
func TestUnknownFlagHandler(t *testing.T) {
var opts struct {
Flag1 string `long:"flag1"`
Flag2 string `long:"flag2"`
}
p := NewParser(&opts, None)
var unknownFlag1 string
var unknownFlag2 bool
var unknownFlag3 string
// Set up a callback to intercept unknown options during parsing
p.UnknownOptionHandler = func(option string, arg SplitArgument, args []string) ([]string, error) {
if option == "unknownFlag1" {
if argValue, ok := arg.Value(); ok {
unknownFlag1 = argValue
return args, nil
}
// consume a value from remaining args list
unknownFlag1 = args[0]
return args[1:], nil
} else if option == "unknownFlag2" {
// treat this one as a bool switch, don't consume any args
unknownFlag2 = true
return args, nil
} else if option == "unknownFlag3" {
if argValue, ok := arg.Value(); ok {
unknownFlag3 = argValue
return args, nil
}
// consume a value from remaining args list
unknownFlag3 = args[0]
return args[1:], nil
}
return args, fmt.Errorf("Unknown flag: %v", option)
}
// Parse args containing some unknown flags, verify that
// our callback can handle all of them
_, err := p.ParseArgs([]string{"--flag1=stuff", "--unknownFlag1", "blah", "--unknownFlag2", "--unknownFlag3=baz", "--flag2=foo"})
if err != nil {
assertErrorf(t, "Parser returned unexpected error %v", err)
}
assertString(t, opts.Flag1, "stuff")
assertString(t, opts.Flag2, "foo")
assertString(t, unknownFlag1, "blah")
assertString(t, unknownFlag3, "baz")
if !unknownFlag2 {
assertErrorf(t, "Flag should have been set by unknown handler, but had value: %v", unknownFlag2)
}
// Parse args with unknown flags that callback doesn't handle, verify it returns error
_, err = p.ParseArgs([]string{"--flag1=stuff", "--unknownFlagX", "blah", "--flag2=foo"})
if err == nil {
assertErrorf(t, "Parser should have returned error, but returned nil")
}
}
func TestChoices(t *testing.T) {
var opts struct {
Choice string `long:"choose" choice:"v1" choice:"v2"`
}
assertParseFail(t, ErrInvalidChoice, "Invalid value `invalid' for option `"+defaultLongOptDelimiter+"choose'. Allowed values are: v1 or v2", &opts, "--choose", "invalid")
assertParseSuccess(t, &opts, "--choose", "v2")
assertString(t, opts.Choice, "v2")
}
func TestEmbedded(t *testing.T) {
type embedded struct {
V bool `short:"v"`
}
var opts struct {
embedded
}
assertParseSuccess(t, &opts, "-v")
if !opts.V {
t.Errorf("Expected V to be true")
}
}
type command struct {
}
func (c *command) Execute(args []string) error {
return nil
}
func TestCommandHandlerNoCommand(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
}{}
parser := NewParser(&opts, Default&^PrintErrors)
var executedCommand Commander
var executedArgs []string
executed := false
parser.CommandHandler = func(command Commander, args []string) error {
executed = true
executedCommand = command
executedArgs = args
return nil
}
_, err := parser.ParseArgs([]string{"arg1", "arg2"})
if err != nil {
t.Fatalf("Unexpected parse error: %s", err)
}
if !executed {
t.Errorf("Expected command handler to be executed")
}
if executedCommand != nil {
t.Errorf("Did not exect an executed command")
}
assertStringArray(t, executedArgs, []string{"arg1", "arg2"})
}
func TestCommandHandler(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Command command `command:"cmd"`
}{}
parser := NewParser(&opts, Default&^PrintErrors)
var executedCommand Commander
var executedArgs []string
executed := false
parser.CommandHandler = func(command Commander, args []string) error {
executed = true
executedCommand = command
executedArgs = args
return nil
}
_, err := parser.ParseArgs([]string{"cmd", "arg1", "arg2"})
if err != nil {
t.Fatalf("Unexpected parse error: %s", err)
}
if !executed {
t.Errorf("Expected command handler to be executed")
}
if executedCommand == nil {
t.Errorf("Expected command handler to be executed")
}
assertStringArray(t, executedArgs, []string{"arg1", "arg2"})
}