ini.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. package flags
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "os"
  7. "reflect"
  8. "sort"
  9. "strconv"
  10. "strings"
  11. )
  12. // IniError contains location information on where an error occurred.
  13. type IniError struct {
  14. // The error message.
  15. Message string
  16. // The filename of the file in which the error occurred.
  17. File string
  18. // The line number at which the error occurred.
  19. LineNumber uint
  20. }
  21. // Error provides a "file:line: message" formatted message of the ini error.
  22. func (x *IniError) Error() string {
  23. return fmt.Sprintf(
  24. "%s:%d: %s",
  25. x.File,
  26. x.LineNumber,
  27. x.Message,
  28. )
  29. }
  30. // IniOptions for writing
  31. type IniOptions uint
  32. const (
  33. // IniNone indicates no options.
  34. IniNone IniOptions = 0
  35. // IniIncludeDefaults indicates that default values should be written.
  36. IniIncludeDefaults = 1 << iota
  37. // IniCommentDefaults indicates that if IniIncludeDefaults is used
  38. // options with default values are written but commented out.
  39. IniCommentDefaults
  40. // IniIncludeComments indicates that comments containing the description
  41. // of an option should be written.
  42. IniIncludeComments
  43. // IniDefault provides a default set of options.
  44. IniDefault = IniIncludeComments
  45. )
  46. // IniParser is a utility to read and write flags options from and to ini
  47. // formatted strings.
  48. type IniParser struct {
  49. ParseAsDefaults bool // override default flags
  50. parser *Parser
  51. }
  52. type iniValue struct {
  53. Name string
  54. Value string
  55. Quoted bool
  56. LineNumber uint
  57. }
  58. type iniSection []iniValue
  59. type ini struct {
  60. File string
  61. Sections map[string]iniSection
  62. }
  63. // NewIniParser creates a new ini parser for a given Parser.
  64. func NewIniParser(p *Parser) *IniParser {
  65. return &IniParser{
  66. parser: p,
  67. }
  68. }
  69. // IniParse is a convenience function to parse command line options with default
  70. // settings from an ini formatted file. The provided data is a pointer to a struct
  71. // representing the default option group (named "Application Options"). For
  72. // more control, use flags.NewParser.
  73. func IniParse(filename string, data interface{}) error {
  74. p := NewParser(data, Default)
  75. return NewIniParser(p).ParseFile(filename)
  76. }
  77. // ParseFile parses flags from an ini formatted file. See Parse for more
  78. // information on the ini file format. The returned errors can be of the type
  79. // flags.Error or flags.IniError.
  80. func (i *IniParser) ParseFile(filename string) error {
  81. ini, err := readIniFromFile(filename)
  82. if err != nil {
  83. return err
  84. }
  85. return i.parse(ini)
  86. }
  87. // Parse parses flags from an ini format. You can use ParseFile as a
  88. // convenience function to parse from a filename instead of a general
  89. // io.Reader.
  90. //
  91. // The format of the ini file is as follows:
  92. //
  93. // [Option group name]
  94. // option = value
  95. //
  96. // Each section in the ini file represents an option group or command in the
  97. // flags parser. The default flags parser option group (i.e. when using
  98. // flags.Parse) is named 'Application Options'. The ini option name is matched
  99. // in the following order:
  100. //
  101. // 1. Compared to the ini-name tag on the option struct field (if present)
  102. // 2. Compared to the struct field name
  103. // 3. Compared to the option long name (if present)
  104. // 4. Compared to the option short name (if present)
  105. //
  106. // Sections for nested groups and commands can be addressed using a dot `.'
  107. // namespacing notation (i.e [subcommand.Options]). Group section names are
  108. // matched case insensitive.
  109. //
  110. // The returned errors can be of the type flags.Error or flags.IniError.
  111. func (i *IniParser) Parse(reader io.Reader) error {
  112. ini, err := readIni(reader, "")
  113. if err != nil {
  114. return err
  115. }
  116. return i.parse(ini)
  117. }
  118. // WriteFile writes the flags as ini format into a file. See Write
  119. // for more information. The returned error occurs when the specified file
  120. // could not be opened for writing.
  121. func (i *IniParser) WriteFile(filename string, options IniOptions) error {
  122. return writeIniToFile(i, filename, options)
  123. }
  124. // Write writes the current values of all the flags to an ini format.
  125. // See Parse for more information on the ini file format. You typically
  126. // call this only after settings have been parsed since the default values of each
  127. // option are stored just before parsing the flags (this is only relevant when
  128. // IniIncludeDefaults is _not_ set in options).
  129. func (i *IniParser) Write(writer io.Writer, options IniOptions) {
  130. writeIni(i, writer, options)
  131. }
  132. func readFullLine(reader *bufio.Reader) (string, error) {
  133. var line []byte
  134. for {
  135. l, more, err := reader.ReadLine()
  136. if err != nil {
  137. return "", err
  138. }
  139. if line == nil && !more {
  140. return string(l), nil
  141. }
  142. line = append(line, l...)
  143. if !more {
  144. break
  145. }
  146. }
  147. return string(line), nil
  148. }
  149. func optionIniName(option *Option) string {
  150. name := option.tag.Get("_read-ini-name")
  151. if len(name) != 0 {
  152. return name
  153. }
  154. name = option.tag.Get("ini-name")
  155. if len(name) != 0 {
  156. return name
  157. }
  158. return option.field.Name
  159. }
  160. func writeGroupIni(cmd *Command, group *Group, namespace string, writer io.Writer, options IniOptions) {
  161. var sname string
  162. if len(namespace) != 0 {
  163. sname = namespace
  164. }
  165. if cmd.Group != group && len(group.ShortDescription) != 0 {
  166. if len(sname) != 0 {
  167. sname += "."
  168. }
  169. sname += group.ShortDescription
  170. }
  171. sectionwritten := false
  172. comments := (options & IniIncludeComments) != IniNone
  173. for _, option := range group.options {
  174. if option.isFunc() || option.Hidden {
  175. continue
  176. }
  177. if len(option.tag.Get("no-ini")) != 0 {
  178. continue
  179. }
  180. val := option.value
  181. if (options&IniIncludeDefaults) == IniNone && option.valueIsDefault() {
  182. continue
  183. }
  184. if !sectionwritten {
  185. fmt.Fprintf(writer, "[%s]\n", sname)
  186. sectionwritten = true
  187. }
  188. if comments && len(option.Description) != 0 {
  189. fmt.Fprintf(writer, "; %s\n", option.Description)
  190. }
  191. oname := optionIniName(option)
  192. commentOption := (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && option.valueIsDefault()
  193. kind := val.Type().Kind()
  194. switch kind {
  195. case reflect.Slice:
  196. kind = val.Type().Elem().Kind()
  197. if val.Len() == 0 {
  198. writeOption(writer, oname, kind, "", "", true, option.iniQuote)
  199. } else {
  200. for idx := 0; idx < val.Len(); idx++ {
  201. v, _ := convertToString(val.Index(idx), option.tag)
  202. writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote)
  203. }
  204. }
  205. case reflect.Map:
  206. kind = val.Type().Elem().Kind()
  207. if val.Len() == 0 {
  208. writeOption(writer, oname, kind, "", "", true, option.iniQuote)
  209. } else {
  210. mkeys := val.MapKeys()
  211. keys := make([]string, len(val.MapKeys()))
  212. kkmap := make(map[string]reflect.Value)
  213. for i, k := range mkeys {
  214. keys[i], _ = convertToString(k, option.tag)
  215. kkmap[keys[i]] = k
  216. }
  217. sort.Strings(keys)
  218. for _, k := range keys {
  219. v, _ := convertToString(val.MapIndex(kkmap[k]), option.tag)
  220. writeOption(writer, oname, kind, k, v, commentOption, option.iniQuote)
  221. }
  222. }
  223. default:
  224. v, _ := convertToString(val, option.tag)
  225. writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote)
  226. }
  227. if comments {
  228. fmt.Fprintln(writer)
  229. }
  230. }
  231. if sectionwritten && !comments {
  232. fmt.Fprintln(writer)
  233. }
  234. }
  235. func writeOption(writer io.Writer, optionName string, optionType reflect.Kind, optionKey string, optionValue string, commentOption bool, forceQuote bool) {
  236. if forceQuote || (optionType == reflect.String && !isPrint(optionValue)) {
  237. optionValue = strconv.Quote(optionValue)
  238. }
  239. comment := ""
  240. if commentOption {
  241. comment = "; "
  242. }
  243. fmt.Fprintf(writer, "%s%s =", comment, optionName)
  244. if optionKey != "" {
  245. fmt.Fprintf(writer, " %s:%s", optionKey, optionValue)
  246. } else if optionValue != "" {
  247. fmt.Fprintf(writer, " %s", optionValue)
  248. }
  249. fmt.Fprintln(writer)
  250. }
  251. func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) {
  252. command.eachGroup(func(group *Group) {
  253. if !group.Hidden {
  254. writeGroupIni(command, group, namespace, writer, options)
  255. }
  256. })
  257. for _, c := range command.commands {
  258. var nns string
  259. if c.Hidden {
  260. continue
  261. }
  262. if len(namespace) != 0 {
  263. nns = c.Name + "." + nns
  264. } else {
  265. nns = c.Name
  266. }
  267. writeCommandIni(c, nns, writer, options)
  268. }
  269. }
  270. func writeIni(parser *IniParser, writer io.Writer, options IniOptions) {
  271. writeCommandIni(parser.parser.Command, "", writer, options)
  272. }
  273. func writeIniToFile(parser *IniParser, filename string, options IniOptions) error {
  274. file, err := os.Create(filename)
  275. if err != nil {
  276. return err
  277. }
  278. defer file.Close()
  279. writeIni(parser, file, options)
  280. return nil
  281. }
  282. func readIniFromFile(filename string) (*ini, error) {
  283. file, err := os.Open(filename)
  284. if err != nil {
  285. return nil, err
  286. }
  287. defer file.Close()
  288. return readIni(file, filename)
  289. }
  290. func readIni(contents io.Reader, filename string) (*ini, error) {
  291. ret := &ini{
  292. File: filename,
  293. Sections: make(map[string]iniSection),
  294. }
  295. reader := bufio.NewReader(contents)
  296. // Empty global section
  297. section := make(iniSection, 0, 10)
  298. sectionname := ""
  299. ret.Sections[sectionname] = section
  300. var lineno uint
  301. for {
  302. line, err := readFullLine(reader)
  303. if err == io.EOF {
  304. break
  305. } else if err != nil {
  306. return nil, err
  307. }
  308. lineno++
  309. line = strings.TrimSpace(line)
  310. // Skip empty lines and lines starting with ; (comments)
  311. if len(line) == 0 || line[0] == ';' || line[0] == '#' {
  312. continue
  313. }
  314. if line[0] == '[' {
  315. if line[0] != '[' || line[len(line)-1] != ']' {
  316. return nil, &IniError{
  317. Message: "malformed section header",
  318. File: filename,
  319. LineNumber: lineno,
  320. }
  321. }
  322. name := strings.TrimSpace(line[1 : len(line)-1])
  323. if len(name) == 0 {
  324. return nil, &IniError{
  325. Message: "empty section name",
  326. File: filename,
  327. LineNumber: lineno,
  328. }
  329. }
  330. sectionname = name
  331. section = ret.Sections[name]
  332. if section == nil {
  333. section = make(iniSection, 0, 10)
  334. ret.Sections[name] = section
  335. }
  336. continue
  337. }
  338. // Parse option here
  339. keyval := strings.SplitN(line, "=", 2)
  340. if len(keyval) != 2 {
  341. return nil, &IniError{
  342. Message: fmt.Sprintf("malformed key=value (%s)", line),
  343. File: filename,
  344. LineNumber: lineno,
  345. }
  346. }
  347. name := strings.TrimSpace(keyval[0])
  348. value := strings.TrimSpace(keyval[1])
  349. quoted := false
  350. if len(value) != 0 && value[0] == '"' {
  351. if v, err := strconv.Unquote(value); err == nil {
  352. value = v
  353. quoted = true
  354. } else {
  355. return nil, &IniError{
  356. Message: err.Error(),
  357. File: filename,
  358. LineNumber: lineno,
  359. }
  360. }
  361. }
  362. section = append(section, iniValue{
  363. Name: name,
  364. Value: value,
  365. Quoted: quoted,
  366. LineNumber: lineno,
  367. })
  368. ret.Sections[sectionname] = section
  369. }
  370. return ret, nil
  371. }
  372. func (i *IniParser) matchingGroups(name string) []*Group {
  373. if len(name) == 0 {
  374. var ret []*Group
  375. i.parser.eachGroup(func(g *Group) {
  376. ret = append(ret, g)
  377. })
  378. return ret
  379. }
  380. g := i.parser.groupByName(name)
  381. if g != nil {
  382. return []*Group{g}
  383. }
  384. return nil
  385. }
  386. func (i *IniParser) parse(ini *ini) error {
  387. p := i.parser
  388. var quotesLookup = make(map[*Option]bool)
  389. for name, section := range ini.Sections {
  390. groups := i.matchingGroups(name)
  391. if len(groups) == 0 {
  392. return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name)
  393. }
  394. for _, inival := range section {
  395. var opt *Option
  396. for _, group := range groups {
  397. opt = group.optionByName(inival.Name, func(o *Option, n string) bool {
  398. return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n)
  399. })
  400. if opt != nil && len(opt.tag.Get("no-ini")) != 0 {
  401. opt = nil
  402. }
  403. if opt != nil {
  404. break
  405. }
  406. }
  407. if opt == nil {
  408. if (p.Options & IgnoreUnknown) == None {
  409. return &IniError{
  410. Message: fmt.Sprintf("unknown option: %s", inival.Name),
  411. File: ini.File,
  412. LineNumber: inival.LineNumber,
  413. }
  414. }
  415. continue
  416. }
  417. // ini value is ignored if override is set and
  418. // value was previously set from non default
  419. if i.ParseAsDefaults && !opt.isSetDefault {
  420. continue
  421. }
  422. pval := &inival.Value
  423. if !opt.canArgument() && len(inival.Value) == 0 {
  424. pval = nil
  425. } else {
  426. if opt.value.Type().Kind() == reflect.Map {
  427. parts := strings.SplitN(inival.Value, ":", 2)
  428. // only handle unquoting
  429. if len(parts) == 2 && parts[1][0] == '"' {
  430. if v, err := strconv.Unquote(parts[1]); err == nil {
  431. parts[1] = v
  432. inival.Quoted = true
  433. } else {
  434. return &IniError{
  435. Message: err.Error(),
  436. File: ini.File,
  437. LineNumber: inival.LineNumber,
  438. }
  439. }
  440. s := parts[0] + ":" + parts[1]
  441. pval = &s
  442. }
  443. }
  444. }
  445. if err := opt.set(pval); err != nil {
  446. return &IniError{
  447. Message: err.Error(),
  448. File: ini.File,
  449. LineNumber: inival.LineNumber,
  450. }
  451. }
  452. // either all INI values are quoted or only values who need quoting
  453. if _, ok := quotesLookup[opt]; !inival.Quoted || !ok {
  454. quotesLookup[opt] = inival.Quoted
  455. }
  456. opt.tag.Set("_read-ini-name", inival.Name)
  457. }
  458. }
  459. for opt, quoted := range quotesLookup {
  460. opt.iniQuote = quoted
  461. }
  462. return nil
  463. }