package flags import ( "bytes" "fmt" "io/ioutil" "os" "reflect" "strings" "testing" ) func TestWriteIni(t *testing.T) { oldEnv := EnvSnapshot() defer oldEnv.Restore() os.Setenv("ENV_DEFAULT", "env-def") var tests = []struct { args []string options IniOptions expected string }{ { []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "3.14", "command"}, IniDefault, `[Application Options] ; Show verbose debug information verbose = true verbose = true ; Test env-default1 value EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def [Other Options] ; A map from string to int int-map = a:2 int-map = b:3 `, }, { []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "3.14", "command"}, IniDefault | IniIncludeDefaults, `[Application Options] ; Show verbose debug information verbose = true verbose = true ; A slice of pointers to string ; PtrSlice = EmptyDescription = false ; Test default value Default = "Some\nvalue" ; Test default array value DefaultArray = Some value DefaultArray = "Other\tvalue" ; Testdefault map value DefaultMap = another:value DefaultMap = some:value ; Test env-default1 value EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def ; Option with named argument OptionWithArgName = ; Option with choices OptionWithChoices = ; Option only available in ini only-ini = [Other Options] ; A slice of strings StringSlice = some StringSlice = value ; A map from string to int int-map = a:2 int-map = b:3 [Subgroup] ; This is a subgroup option Opt = ; Not hidden inside group NotHiddenInsideGroup = [Subsubgroup] ; This is a subsubgroup option Opt = [command] ; Use for extra verbosity ; ExtraVerbose = [parent] ; This is a parent command option Opt = [parent.sub] ; This is a sub command option Opt = `, }, { []string{"filename", "0", "3.14", "command"}, IniDefault | IniIncludeDefaults | IniCommentDefaults, `[Application Options] ; Show verbose debug information ; verbose = ; A slice of pointers to string ; PtrSlice = ; EmptyDescription = false ; Test default value ; Default = "Some\nvalue" ; Test default array value ; DefaultArray = Some value ; DefaultArray = "Other\tvalue" ; Testdefault map value ; DefaultMap = another:value ; DefaultMap = some:value ; Test env-default1 value EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def ; Option with named argument ; OptionWithArgName = ; Option with choices ; OptionWithChoices = ; Option only available in ini ; only-ini = [Other Options] ; A slice of strings ; StringSlice = some ; StringSlice = value ; A map from string to int ; int-map = a:1 [Subgroup] ; This is a subgroup option ; Opt = ; Not hidden inside group ; NotHiddenInsideGroup = [Subsubgroup] ; This is a subsubgroup option ; Opt = [command] ; Use for extra verbosity ; ExtraVerbose = [parent] ; This is a parent command option ; Opt = [parent.sub] ; This is a sub command option ; Opt = `, }, { []string{"--default=New value", "--default-array=New value", "--default-map=new:value", "filename", "0", "3.14", "command"}, IniDefault | IniIncludeDefaults | IniCommentDefaults, `[Application Options] ; Show verbose debug information ; verbose = ; A slice of pointers to string ; PtrSlice = ; EmptyDescription = false ; Test default value Default = New value ; Test default array value DefaultArray = New value ; Testdefault map value DefaultMap = new:value ; Test env-default1 value EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def ; Option with named argument ; OptionWithArgName = ; Option with choices ; OptionWithChoices = ; Option only available in ini ; only-ini = [Other Options] ; A slice of strings ; StringSlice = some ; StringSlice = value ; A map from string to int ; int-map = a:1 [Subgroup] ; This is a subgroup option ; Opt = ; Not hidden inside group ; NotHiddenInsideGroup = [Subsubgroup] ; This is a subsubgroup option ; Opt = [command] ; Use for extra verbosity ; ExtraVerbose = [parent] ; This is a parent command option ; Opt = [parent.sub] ; This is a sub command option ; Opt = `, }, { []string{"-vv", "filename", "0", "3.14", "parent", "--opt=p", "sub", "--opt=s"}, IniDefault, `[Application Options] ; Show verbose debug information verbose = true verbose = true ; Test env-default1 value EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def [parent] ; This is a parent command option Opt = p [parent.sub] ; This is a sub command option Opt = s `, }, } for _, test := range tests { var opts helpOptions p := NewNamedParser("TestIni", Default) p.AddGroup("Application Options", "The application options", &opts) _, err := p.ParseArgs(test.args) if err != nil { t.Fatalf("Unexpected error: %v", err) } inip := NewIniParser(p) var b bytes.Buffer inip.Write(&b, test.options) got := b.String() expected := test.expected msg := fmt.Sprintf("with arguments %+v and ini options %b", test.args, test.options) assertDiff(t, got, expected, msg) } } func TestReadIni_flagEquivalent(t *testing.T) { type options struct { Opt1 bool `long:"opt1"` Group1 struct { Opt2 bool `long:"opt2"` } `group:"group1"` Group2 struct { Opt3 bool `long:"opt3"` } `group:"group2" namespace:"ns1"` Cmd1 struct { Opt4 bool `long:"opt4"` Opt5 bool `long:"foo.opt5"` Group1 struct { Opt6 bool `long:"opt6"` Opt7 bool `long:"foo.opt7"` } `group:"group1"` Group2 struct { Opt8 bool `long:"opt8"` } `group:"group2" namespace:"ns1"` } `command:"cmd1"` } a := ` opt1=true [group1] opt2=true [group2] ns1.opt3=true [cmd1] opt4=true foo.opt5=true [cmd1.group1] opt6=true foo.opt7=true [cmd1.group2] ns1.opt8=true ` b := ` opt1=true opt2=true ns1.opt3=true [cmd1] opt4=true foo.opt5=true opt6=true foo.opt7=true ns1.opt8=true ` parse := func(readIni string) (opts options, writeIni string) { p := NewNamedParser("TestIni", Default) p.AddGroup("Application Options", "The application options", &opts) inip := NewIniParser(p) err := inip.Parse(strings.NewReader(readIni)) if err != nil { t.Fatalf("Unexpected error: %s\n\nFile:\n%s", err, readIni) } var b bytes.Buffer inip.Write(&b, Default) return opts, b.String() } aOpt, aIni := parse(a) bOpt, bIni := parse(b) assertDiff(t, aIni, bIni, "") if !reflect.DeepEqual(aOpt, bOpt) { t.Errorf("not equal") } } func TestReadIni(t *testing.T) { var opts helpOptions p := NewNamedParser("TestIni", Default) p.AddGroup("Application Options", "The application options", &opts) inip := NewIniParser(p) inic := ` ; Show verbose debug information verbose = true verbose = true DefaultMap = another:"value\n1" DefaultMap = some:value 2 [Application Options] ; A slice of pointers to string ; PtrSlice = ; Test default value Default = "New\nvalue" ; Test env-default1 value EnvDefault1 = New value [Other Options] # A slice of strings StringSlice = "some\nvalue" StringSlice = another value ; A map from string to int int-map = a:2 int-map = b:3 ` b := strings.NewReader(inic) err := inip.Parse(b) if err != nil { t.Fatalf("Unexpected error: %s", err) } assertBoolArray(t, opts.Verbose, []bool{true, true}) if v := map[string]string{"another": "value\n1", "some": "value 2"}; !reflect.DeepEqual(opts.DefaultMap, v) { t.Fatalf("Expected %#v for DefaultMap but got %#v", v, opts.DefaultMap) } assertString(t, opts.Default, "New\nvalue") assertString(t, opts.EnvDefault1, "New value") assertStringArray(t, opts.Other.StringSlice, []string{"some\nvalue", "another value"}) if v, ok := opts.Other.IntMap["a"]; !ok { t.Errorf("Expected \"a\" in Other.IntMap") } else if v != 2 { t.Errorf("Expected Other.IntMap[\"a\"] = 2, but got %v", v) } if v, ok := opts.Other.IntMap["b"]; !ok { t.Errorf("Expected \"b\" in Other.IntMap") } else if v != 3 { t.Errorf("Expected Other.IntMap[\"b\"] = 3, but got %v", v) } } func TestReadAndWriteIni(t *testing.T) { var tests = []struct { options IniOptions read string write string }{ { IniIncludeComments, `[Application Options] ; Show verbose debug information verbose = true verbose = true ; Test default value Default = "quote me" ; Test default array value DefaultArray = 1 DefaultArray = "2" DefaultArray = 3 ; Testdefault map value ; DefaultMap = ; Test env-default1 value EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def [Other Options] ; A slice of strings ; StringSlice = ; A map from string to int int-map = a:2 int-map = b:"3" `, `[Application Options] ; Show verbose debug information verbose = true verbose = true ; Test default value Default = "quote me" ; Test default array value DefaultArray = 1 DefaultArray = 2 DefaultArray = 3 ; Testdefault map value ; DefaultMap = ; Test env-default1 value EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def [Other Options] ; A slice of strings ; StringSlice = ; A map from string to int int-map = a:2 int-map = b:3 `, }, { IniIncludeComments, `[Application Options] ; Show verbose debug information verbose = true verbose = true ; Test default value Default = "quote me" ; Test default array value DefaultArray = "1" DefaultArray = "2" DefaultArray = "3" ; Testdefault map value ; DefaultMap = ; Test env-default1 value EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def [Other Options] ; A slice of strings ; StringSlice = ; A map from string to int int-map = a:"2" int-map = b:"3" `, `[Application Options] ; Show verbose debug information verbose = true verbose = true ; Test default value Default = "quote me" ; Test default array value DefaultArray = "1" DefaultArray = "2" DefaultArray = "3" ; Testdefault map value ; DefaultMap = ; Test env-default1 value EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def [Other Options] ; A slice of strings ; StringSlice = ; A map from string to int int-map = a:"2" int-map = b:"3" `, }, } for _, test := range tests { var opts helpOptions p := NewNamedParser("TestIni", Default) p.AddGroup("Application Options", "The application options", &opts) inip := NewIniParser(p) read := strings.NewReader(test.read) err := inip.Parse(read) if err != nil { t.Fatalf("Unexpected error: %s", err) } var write bytes.Buffer inip.Write(&write, test.options) got := write.String() msg := fmt.Sprintf("with ini options %b", test.options) assertDiff(t, got, test.write, msg) } } func TestReadIniWrongQuoting(t *testing.T) { var tests = []struct { iniFile string lineNumber uint }{ { iniFile: `Default = "New\nvalue`, lineNumber: 1, }, { iniFile: `StringSlice = "New\nvalue`, lineNumber: 1, }, { iniFile: `StringSlice = "New\nvalue" StringSlice = "Second\nvalue`, lineNumber: 2, }, { iniFile: `DefaultMap = some:"value`, lineNumber: 1, }, { iniFile: `DefaultMap = some:value DefaultMap = another:"value`, lineNumber: 2, }, } for _, test := range tests { var opts helpOptions p := NewNamedParser("TestIni", Default) p.AddGroup("Application Options", "The application options", &opts) inip := NewIniParser(p) inic := test.iniFile b := strings.NewReader(inic) err := inip.Parse(b) if err == nil { t.Fatalf("Expect error") } iniError := err.(*IniError) if iniError.LineNumber != test.lineNumber { t.Fatalf("Expect error on line %d", test.lineNumber) } } } func TestIniCommands(t *testing.T) { var opts struct { Value string `short:"v" long:"value"` Add struct { Name int `short:"n" long:"name" ini-name:"AliasName"` Other struct { O string `short:"o" long:"other"` } `group:"Other Options"` } `command:"add"` } p := NewNamedParser("TestIni", Default) p.AddGroup("Application Options", "The application options", &opts) inip := NewIniParser(p) inic := `[Application Options] value = some value [add] AliasName = 5 [add.Other Options] other = subgroup ` b := strings.NewReader(inic) err := inip.Parse(b) if err != nil { t.Fatalf("Unexpected error: %s", err) } assertString(t, opts.Value, "some value") if opts.Add.Name != 5 { t.Errorf("Expected opts.Add.Name to be 5, but got %v", opts.Add.Name) } assertString(t, opts.Add.Other.O, "subgroup") // Test writing it back buf := &bytes.Buffer{} inip.Write(buf, IniDefault) assertDiff(t, buf.String(), inic, "ini contents") } func TestIniNoIni(t *testing.T) { var opts struct { NoValue string `short:"n" long:"novalue" no-ini:"yes"` Value string `short:"v" long:"value"` } p := NewNamedParser("TestIni", Default) p.AddGroup("Application Options", "The application options", &opts) inip := NewIniParser(p) // read INI inic := `[Application Options] novalue = some value value = some other value ` b := strings.NewReader(inic) err := inip.Parse(b) if err == nil { t.Fatalf("Expected error") } iniError := err.(*IniError) if v := uint(2); iniError.LineNumber != v { t.Errorf("Expected opts.Add.Name to be %d, but got %d", v, iniError.LineNumber) } if v := "unknown option: novalue"; iniError.Message != v { t.Errorf("Expected opts.Add.Name to be %s, but got %s", v, iniError.Message) } // write INI opts.NoValue = "some value" opts.Value = "some other value" file, err := ioutil.TempFile("", "") if err != nil { t.Fatalf("Cannot create temporary file: %s", err) } defer os.Remove(file.Name()) err = inip.WriteFile(file.Name(), IniIncludeDefaults) if err != nil { t.Fatalf("Could not write ini file: %s", err) } found, err := ioutil.ReadFile(file.Name()) if err != nil { t.Fatalf("Could not read written ini file: %s", err) } expected := "[Application Options]\nValue = some other value\n\n" assertDiff(t, string(found), expected, "ini content") } func TestIniParse(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { t.Fatalf("Cannot create temporary file: %s", err) } defer os.Remove(file.Name()) _, err = file.WriteString("value = 123") if err != nil { t.Fatalf("Cannot write to temporary file: %s", err) } file.Close() var opts struct { Value int `long:"value"` } err = IniParse(file.Name(), &opts) if err != nil { t.Fatalf("Could not parse ini: %s", err) } if opts.Value != 123 { t.Fatalf("Expected Value to be \"123\" but was \"%d\"", opts.Value) } } func TestIniCliOverrides(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { t.Fatalf("Cannot create temporary file: %s", err) } defer os.Remove(file.Name()) _, err = file.WriteString("values = 123\n") _, err = file.WriteString("values = 456\n") if err != nil { t.Fatalf("Cannot write to temporary file: %s", err) } file.Close() var opts struct { Values []int `long:"values"` } p := NewParser(&opts, Default) err = NewIniParser(p).ParseFile(file.Name()) if err != nil { t.Fatalf("Could not parse ini: %s", err) } _, err = p.ParseArgs([]string{"--values", "111", "--values", "222"}) if err != nil { t.Fatalf("Failed to parse arguments: %s", err) } if len(opts.Values) != 2 { t.Fatalf("Expected Values to contain two elements, but got %d", len(opts.Values)) } if opts.Values[0] != 111 { t.Fatalf("Expected Values[0] to be 111, but got '%d'", opts.Values[0]) } if opts.Values[1] != 222 { t.Fatalf("Expected Values[1] to be 222, but got '%d'", opts.Values[1]) } } func TestIniOverrides(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { t.Fatalf("Cannot create temporary file: %s", err) } defer os.Remove(file.Name()) _, err = file.WriteString("value-with-default = \"ini-value\"\n") _, err = file.WriteString("value-with-default-override-cli = \"ini-value\"\n") if err != nil { t.Fatalf("Cannot write to temporary file: %s", err) } file.Close() var opts struct { ValueWithDefault string `long:"value-with-default" default:"value"` ValueWithDefaultOverrideCli string `long:"value-with-default-override-cli" default:"value"` } p := NewParser(&opts, Default) err = NewIniParser(p).ParseFile(file.Name()) if err != nil { t.Fatalf("Could not parse ini: %s", err) } _, err = p.ParseArgs([]string{"--value-with-default-override-cli", "cli-value"}) if err != nil { t.Fatalf("Failed to parse arguments: %s", err) } assertString(t, opts.ValueWithDefault, "ini-value") assertString(t, opts.ValueWithDefaultOverrideCli, "cli-value") } func TestIniOverridesFromConfigFlag(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { t.Fatalf("Cannot create temporary file: %s", err) } defer os.Remove(file.Name()) _, err = file.WriteString("value-with-default = \"ini-value\"\n") _, err = file.WriteString("value-with-default-override-cli = \"ini-value\"\n") if err != nil { t.Fatalf("Cannot write to temporary file: %s", err) } file.Close() var opts struct { Config func(filename string) `long:"config"` ValueWithDefault string `long:"value-with-default" default:"value"` ValueWithDefaultOverrideCli string `long:"value-with-default-override-cli" default:"value"` } p := NewParser(&opts, Default) opt := p.FindOptionByLongName("config") opt.Default = []string{file.Name()} opts.Config = func(filename string) { parser := NewIniParser(p) parser.ParseAsDefaults = true parser.ParseFile(filename) } _, err = p.ParseArgs([]string{"--value-with-default-override-cli", "cli-value"}) if err != nil { t.Fatalf("Failed to parse arguments: %s", err) } assertString(t, opts.ValueWithDefault, "ini-value") assertString(t, opts.ValueWithDefaultOverrideCli, "cli-value") } func TestIniRequired(t *testing.T) { var opts struct { Required string `short:"r" required:"yes" description:"required"` Config func(s string) error `long:"config" default:"no-ini-file" no-ini:"true"` } p := NewParser(&opts, Default) opts.Config = func(s string) error { inip := NewIniParser(p) inip.ParseAsDefaults = true return inip.Parse(strings.NewReader("Required = ini-value\n")) } _, err := p.ParseArgs([]string{"-r", "cli-value"}) if err != nil { t.Fatalf("Failed to parse arguments: %s", err) } assertString(t, opts.Required, "cli-value") } func TestIniRequiredSlice_ShouldNotNeedToBeSpecifiedOnCli(t *testing.T) { type options struct { Items []string `long:"item" required:"true"` } var opts options ini := ` [Application Options] item=abc` args := []string{} parser := NewParser(&opts, Default) inip := NewIniParser(parser) inip.Parse(strings.NewReader(ini)) _, err := parser.ParseArgs(args) if err != nil { t.Fatalf("Unexpected failure: %v", err) } assertString(t, opts.Items[0], "abc") } func TestWriteFile(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { t.Fatalf("Cannot create temporary file: %s", err) } defer os.Remove(file.Name()) var opts struct { Value int `long:"value"` } opts.Value = 123 p := NewParser(&opts, Default) ini := NewIniParser(p) err = ini.WriteFile(file.Name(), IniIncludeDefaults) if err != nil { t.Fatalf("Could not write ini file: %s", err) } found, err := ioutil.ReadFile(file.Name()) if err != nil { t.Fatalf("Could not read written ini file: %s", err) } expected := "[Application Options]\nValue = 123\n\n" assertDiff(t, string(found), expected, "ini content") } func TestOverwriteRequiredOptions(t *testing.T) { var tests = []struct { args []string expected []string }{ { args: []string{"--value", "from CLI"}, expected: []string{ "from CLI", "from default", }, }, { args: []string{"--value", "from CLI", "--default", "from CLI"}, expected: []string{ "from CLI", "from CLI", }, }, { args: []string{"--config", "no file name"}, expected: []string{ "from INI", "from INI", }, }, { args: []string{"--value", "from CLI before", "--default", "from CLI before", "--config", "no file name"}, expected: []string{ "from INI", "from INI", }, }, { args: []string{"--value", "from CLI before", "--default", "from CLI before", "--config", "no file name", "--value", "from CLI after", "--default", "from CLI after"}, expected: []string{ "from CLI after", "from CLI after", }, }, } for _, test := range tests { var opts struct { Config func(s string) error `long:"config" no-ini:"true"` Value string `long:"value" required:"true"` Default string `long:"default" required:"true" default:"from default"` } p := NewParser(&opts, Default) opts.Config = func(s string) error { ini := NewIniParser(p) return ini.Parse(bytes.NewBufferString("value = from INI\ndefault = from INI")) } _, err := p.ParseArgs(test.args) if err != nil { t.Fatalf("Unexpected error %s with args %+v", err, test.args) } if opts.Value != test.expected[0] { t.Fatalf("Expected Value to be \"%s\" but was \"%s\" with args %+v", test.expected[0], opts.Value, test.args) } if opts.Default != test.expected[1] { t.Fatalf("Expected Default to be \"%s\" but was \"%s\" with args %+v", test.expected[1], opts.Default, test.args) } } } func TestIniOverwriteOptions(t *testing.T) { var tests = []struct { args []string expected string toggled bool }{ { args: []string{}, expected: "from default", }, { args: []string{"--value", "from CLI"}, expected: "from CLI", }, { args: []string{"--config", "no file name"}, expected: "from INI", toggled: true, }, { args: []string{"--value", "from CLI before", "--config", "no file name"}, expected: "from CLI before", toggled: true, }, { args: []string{"--config", "no file name", "--value", "from CLI after"}, expected: "from CLI after", toggled: true, }, { args: []string{"--toggle"}, toggled: true, expected: "from default", }, } for _, test := range tests { var opts struct { Config string `long:"config" no-ini:"true"` Value string `long:"value" default:"from default"` Toggle bool `long:"toggle"` } p := NewParser(&opts, Default) _, err := p.ParseArgs(test.args) if err != nil { t.Fatalf("Unexpected error %s with args %+v", err, test.args) } if opts.Config != "" { inip := NewIniParser(p) inip.ParseAsDefaults = true err = inip.Parse(bytes.NewBufferString("value = from INI\ntoggle = true")) if err != nil { t.Fatalf("Unexpected error %s with args %+v", err, test.args) } } if opts.Value != test.expected { t.Fatalf("Expected Value to be \"%s\" but was \"%s\" with args %+v", test.expected, opts.Value, test.args) } if opts.Toggle != test.toggled { t.Fatalf("Expected Toggle to be \"%v\" but was \"%v\" with args %+v", test.toggled, opts.Toggle, test.args) } } }