do quoting in INI and help
diff --git a/convert.go b/convert.go
index be8de39..e805e1f 100644
--- a/convert.go
+++ b/convert.go
@@ -294,6 +294,14 @@
return nil
}
+func quoteIfNeeded(s string) string {
+ if strconv.CanBackquote(s) {
+ return s
+ }
+
+ return strconv.Quote(s)
+}
+
func wrapText(s string, l int, prefix string) string {
// Basic text wrapping of s at spaces to fit in l
var ret string
diff --git a/help.go b/help.go
index d16f3b1..208e936 100644
--- a/help.go
+++ b/help.go
@@ -171,7 +171,17 @@
def, _ = convertToString(option.value, option.tag)
}
} else if len(defs) != 0 {
- def = strings.Join(defs, ", ")
+ if option.field.Type.Kind() == reflect.String {
+ l := len(defs) - 1
+
+ for i := 0; i < l; i++ {
+ def += quoteIfNeeded(defs[i]) + ", "
+ }
+
+ def += quoteIfNeeded(defs[l])
+ } else {
+ def = strings.Join(defs, ", ")
+ }
}
var envDef string
@@ -188,8 +198,7 @@
var desc string
if def != "" {
- desc = fmt.Sprintf("%s (%v)%s", option.Description, def,
- envDef)
+ desc = fmt.Sprintf("%s (%v)%s", option.Description, def, envDef)
} else {
desc = option.Description + envDef
}
diff --git a/help_test.go b/help_test.go
index def3043..6ce55ed 100644
--- a/help_test.go
+++ b/help_test.go
@@ -15,7 +15,7 @@
PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"`
EmptyDescription bool `long:"empty-description"`
- Default string `long:"default" default:"Some value" description:"Test default value"`
+ Default string `long:"default" default:"Some\nvalue" description:"Test default value"`
DefaultArray []string `long:"default-array" default:"Some value" default:"Another value" description:"Test default array value"`
DefaultMap map[string]string `long:"default-map" default:"some:value" default:"another:value" description:"Testdefault map value"`
EnvDefault1 string `long:"env-default1" default:"Some value" env:"ENV_DEFAULT" description:"Test env-default1 value"`
@@ -79,7 +79,7 @@
/c: Call phone number
/ptrslice: A slice of pointers to string
/empty-description
- /default: Test default value (Some value)
+ /default: Test default value ("Some\nvalue")
/default-array: Test default array value (Some value, Another value)
/default-map: Testdefault map value (some:value, another:value)
/env-default1: Test env-default1 value (Some value) [%ENV_DEFAULT%]
@@ -115,7 +115,7 @@
-c= Call phone number
--ptrslice= A slice of pointers to string
--empty-description
- --default= Test default value (Some value)
+ --default= Test default value ("Some\nvalue")
--default-array= Test default array value (Some value, Another value)
--default-map= Testdefault map value (some:value, another:value)
--env-default1= Test env-default1 value (Some value) [$ENV_DEFAULT]
diff --git a/ini_private.go b/ini_private.go
index 2e72b87..b35cffb 100644
--- a/ini_private.go
+++ b/ini_private.go
@@ -7,16 +7,21 @@
"os"
"reflect"
"sort"
+ "strconv"
"strings"
)
type iniValue struct {
- Name string
- Value string
+ Name string
+ Value string
+ LineNumber uint
}
type iniSection []iniValue
-type ini map[string]iniSection
+type ini struct {
+ File string
+ Sections map[string]iniSection
+}
func readFullLine(reader *bufio.Reader) (string, error) {
var line []byte
@@ -76,6 +81,18 @@
sectionwritten := false
comments := (options & IniIncludeComments) != IniNone
+ printOption := func(commentOption string, optionName string, optionType reflect.Kind, optionKey string, optionValue string) {
+ if optionType == reflect.String {
+ optionValue = quoteIfNeeded(optionValue)
+ }
+
+ if optionKey == "" {
+ fmt.Fprintf(writer, "%s%s = %s\n", commentOption, optionName, optionValue)
+ } else {
+ fmt.Fprintf(writer, "%s%s = %s:%s\n", commentOption, optionName, optionKey, optionValue)
+ }
+ }
+
for _, option := range group.options {
if option.isFunc() {
continue
@@ -107,11 +124,12 @@
commentOption = "; "
}
- switch val.Type().Kind() {
+ kind := val.Type().Kind()
+ switch kind {
case reflect.Slice:
for idx := 0; idx < val.Len(); idx++ {
v, _ := convertToString(val.Index(idx), option.tag)
- fmt.Fprintf(writer, "%s%s = %s\n", commentOption, oname, v)
+ printOption(commentOption, oname, val.Type().Elem().Kind(), "", v)
}
if val.Len() == 0 {
@@ -132,7 +150,7 @@
for _, k := range keys {
v, _ := convertToString(val.MapIndex(kkmap[k]), option.tag)
- fmt.Fprintf(writer, "%s%s = %s:%s\n", commentOption, oname, k, v)
+ printOption(commentOption, oname, val.Type().Elem().Kind(), k, v)
}
if val.Len() == 0 {
@@ -142,7 +160,7 @@
v, _ := convertToString(val, option.tag)
if len(v) != 0 {
- fmt.Fprintf(writer, "%s%s = %s\n", commentOption, oname, v)
+ printOption(commentOption, oname, kind, "", v)
} else {
fmt.Fprintf(writer, "%s%s =\n", commentOption, oname)
}
@@ -194,7 +212,7 @@
return nil
}
-func readIniFromFile(filename string) (ini, error) {
+func readIniFromFile(filename string) (*ini, error) {
file, err := os.Open(filename)
if err != nil {
@@ -206,8 +224,11 @@
return readIni(file, filename)
}
-func readIni(contents io.Reader, filename string) (ini, error) {
- ret := make(ini)
+func readIni(contents io.Reader, filename string) (*ini, error) {
+ ret := &ini{
+ File: filename,
+ Sections: make(map[string]iniSection),
+ }
reader := bufio.NewReader(contents)
@@ -215,7 +236,7 @@
section := make(iniSection, 0, 10)
sectionname := ""
- ret[sectionname] = section
+ ret.Sections[sectionname] = section
var lineno uint
@@ -256,11 +277,11 @@
}
sectionname = name
- section = ret[name]
+ section = ret.Sections[name]
if section == nil {
section = make(iniSection, 0, 10)
- ret[name] = section
+ ret.Sections[name] = section
}
continue
@@ -280,12 +301,25 @@
name := strings.TrimSpace(keyval[0])
value := strings.TrimSpace(keyval[1])
+ if len(value) != 0 && value[0] == '"' {
+ if v, err := strconv.Unquote(value); err == nil {
+ value = v
+ } else {
+ return nil, &IniError{
+ Message: err.Error(),
+ File: filename,
+ LineNumber: lineno,
+ }
+ }
+ }
+
section = append(section, iniValue{
- Name: name,
- Value: value,
+ Name: name,
+ Value: value,
+ LineNumber: lineno,
})
- ret[sectionname] = section
+ ret.Sections[sectionname] = section
}
return ret, nil
@@ -311,10 +345,10 @@
return nil
}
-func (i *IniParser) parse(ini ini) error {
+func (i *IniParser) parse(ini *ini) error {
p := i.parser
- for name, section := range ini {
+ for name, section := range ini.Sections {
groups := i.matchingGroups(name)
if len(groups) == 0 {
@@ -343,10 +377,11 @@
if opt == nil {
if (p.Options & IgnoreUnknown) == None {
- return newError(
- ErrUnknownFlag,
- fmt.Sprintf("unknown option: %s", inival.Name),
- )
+ return &IniError{
+ Message: fmt.Sprintf("unknown option: %s", inival.Name),
+ File: ini.File,
+ LineNumber: inival.LineNumber,
+ }
}
continue
@@ -356,10 +391,35 @@
if !opt.canArgument() && len(inival.Value) == 0 {
pval = nil
+ } else {
+ if opt.value.Type().Kind() == reflect.Map {
+ parts := strings.SplitN(inival.Value, ":", 2)
+
+ // only handle unquoting
+ if len(parts) == 2 && parts[1][0] == '"' {
+ if v, err := strconv.Unquote(parts[1]); err == nil {
+ parts[1] = v
+ } else {
+ return &IniError{
+ Message: err.Error(),
+ File: ini.File,
+ LineNumber: inival.LineNumber,
+ }
+ }
+
+ s := parts[0] + ":" + parts[1]
+
+ pval = &s
+ }
+ }
}
if err := opt.set(pval); err != nil {
- return wrapError(err)
+ return &IniError{
+ Message: err.Error(),
+ File: ini.File,
+ LineNumber: inival.LineNumber,
+ }
}
opt.tag.Set("_read-ini-name", inival.Name)
diff --git a/ini_test.go b/ini_test.go
index e2b5cdc..888e56c 100644
--- a/ini_test.go
+++ b/ini_test.go
@@ -5,6 +5,7 @@
"fmt"
"io/ioutil"
"os"
+ "reflect"
"strings"
"testing"
)
@@ -54,7 +55,7 @@
EmptyDescription = false
; Test default value
-Default = Some value
+Default = "Some\nvalue"
; Test default array value
DefaultArray = Some value
@@ -109,7 +110,7 @@
; EmptyDescription = false
; Test default value
-; Default = Some value
+; Default = "Some\nvalue"
; Test default array value
; DefaultArray = Some value
@@ -242,16 +243,23 @@
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 = Some value
+Default = "New\nvalue"
+
+; Test env-default1 value
+EnvDefault1 = New value
[Other Options]
# A slice of strings
-# StringSlice =
+StringSlice = "some\nvalue"
+StringSlice = another value
; A map from string to int
int-map = a:2
@@ -268,6 +276,16 @@
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 {
@@ -281,6 +299,60 @@
}
}
+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"`
@@ -354,7 +426,15 @@
t.Fatalf("Expected error")
}
- assertError(t, err, ErrUnknownFlag, "unknown option: value")
+ 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: value"; iniError.Message != v {
+ t.Errorf("Expected opts.Add.Name to be %s, but got %s", v, iniError.Message)
+ }
}
func TestIniParse(t *testing.T) {