parse/parse.go (295 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package parse
import (
"errors"
"fmt"
"strconv"
"strings"
"unicode"
)
// Config allows enabling and disabling parser features.
type Config struct {
// Enables parsing arrays from values enclosed in [].
Array bool
// Enables parsing objects enclosed in {}.
Object bool
// Enables parsing double quoted strings, where values are escaped.
StringDQuote bool
// Enables parsing single quotes strings, where no values are escaped.
StringSQuote bool
// Enables ignoring commas as a shortcut for building arrays: a,b parses to [a,b].
// The comma array syntax is enabled by default for backwards compatibility.
IgnoreCommas bool
}
// DefaultConfig is the default config with all parser features enabled.
var DefaultConfig = Config{
Array: true,
Object: true,
StringDQuote: true,
StringSQuote: true,
}
// EnvConfig is configuration for parser when the value comes from environmental variable.
var EnvConfig = Config{
Array: true,
Object: false,
StringDQuote: true,
StringSQuote: true,
}
// NoopConfig is configuration for parser that disables all options.
var NoopConfig = Config{
Array: false,
Object: false,
StringDQuote: false,
StringSQuote: false,
IgnoreCommas: true,
}
type flagParser struct {
input string
cfg Config
}
// stopSet definitions for handling unquoted strings
const (
toplevelStopSet = ","
arrayElemStopSet = ",]"
objKeyStopSet = ":"
objValueStopSet = ",}"
)
// Value parses command line arguments, supporting
// boolean, numbers, strings, arrays, objects.
//
// The parser implements a superset of JSON, but only a subset of YAML by
// allowing for arrays and objects having a trailing comma. In addition 3
// string types are supported:
//
// 1. single quoted string (no unescaping of any characters)
// 2. double quoted strings (characters are escaped)
// 3. strings without quotes. String parsing stops at
// special characters like '[]{},:'
//
// In addition, top-level values can be separated by ',' to build arrays
// without having to use [].
func Value(content string) (interface{}, error) {
return ValueWithConfig(content, DefaultConfig)
}
// ValueWithConfig parses command line arguments, supporting
// boolean, numbers, strings, arrays, objects when enabled.
//
// The parser implements a superset of JSON, but only a subset of YAML by
// allowing for arrays and objects having a trailing comma. In addition 3
// string types are supported:
//
// 1. single quoted string (no unescaping of any characters)
// 2. double quoted strings (characters are escaped)
// 3. strings without quotes. String parsing stops at
// special characters like '[]{},:'
//
// In addition, top-level values can be separated by ',' to build arrays
// without having to use [].
func ValueWithConfig(content string, cfg Config) (interface{}, error) {
p := &flagParser{strings.TrimSpace(content), cfg}
if err := p.validateConfig(); err != nil {
return nil, err
}
v, err := p.parse()
if err != nil {
return nil, fmt.Errorf("%v when parsing '%v'", err.Error(), content)
}
return v, nil
}
func (p *flagParser) validateConfig() error {
if !p.cfg.Array && p.cfg.Object {
return fmt.Errorf("cfg.Array cannot be disabled when cfg.Object is enabled")
}
return nil
}
func (p *flagParser) parse() (interface{}, error) {
var values []interface{}
for {
// Enable building arrays when commas separate top level elements by default.
stopSet := toplevelStopSet
if p.cfg.IgnoreCommas {
stopSet = ""
}
v, err := p.parseValue(stopSet)
if err != nil {
return nil, err
}
values = append(values, v)
p.ignoreWhitespace()
if p.input == "" {
break
}
if err := p.expectChar(','); err != nil {
return nil, err
}
}
switch len(values) {
case 0:
return nil, nil
case 1:
return values[0], nil
}
return values, nil
}
func (p *flagParser) parseValue(stopSet string) (interface{}, error) {
p.ignoreWhitespace()
in := p.input
if in == "" {
return nil, nil
}
switch in[0] {
case '[':
if p.cfg.Array {
return p.parseArray()
}
return p.parsePrimitive(stopSet)
case '{':
if p.cfg.Object {
return p.parseObj()
}
return p.parsePrimitive(stopSet)
case '"':
if p.cfg.StringDQuote {
return p.parseStringDQuote()
}
return p.parsePrimitive(stopSet)
case '\'':
if p.cfg.StringSQuote {
return p.parseStringSQuote()
}
return p.parsePrimitive(stopSet)
default:
return p.parsePrimitive(stopSet)
}
}
func (p *flagParser) ignoreWhitespace() {
p.input = strings.TrimLeftFunc(p.input, unicode.IsSpace)
}
func (p *flagParser) parseArray() (interface{}, error) {
p.input = p.input[1:]
var values []interface{}
loop:
for {
p.ignoreWhitespace()
if p.input[0] == ']' {
p.input = p.input[1:]
break
}
v, err := p.parseValue(arrayElemStopSet)
if err != nil {
return nil, err
}
values = append(values, v)
p.ignoreWhitespace()
if p.input == "" {
return nil, errors.New("array closing ']' missing")
}
next := p.input[0]
p.input = p.input[1:]
switch next {
case ']':
break loop
case ',':
continue
default:
return nil, errors.New("array expected ',' or ']'")
}
}
if len(values) == 0 {
return nil, nil
}
return values, nil
}
func (p *flagParser) parseObj() (interface{}, error) {
p.input = p.input[1:]
O := map[string]interface{}{}
loop:
for {
p.ignoreWhitespace()
if p.input[0] == '}' {
p.input = p.input[1:]
break
}
k, err := p.parseKey()
if err != nil {
return nil, err
}
p.ignoreWhitespace()
if err := p.expectChar(':'); err != nil {
return nil, err
}
v, err := p.parseValue(objValueStopSet)
if err != nil {
return nil, err
}
if p.input == "" {
return nil, errors.New("dictionary expected ',' or '}'")
}
O[k] = v
next := p.input[0]
p.input = p.input[1:]
switch next {
case '}':
break loop
case ',':
continue
default:
return nil, errors.New("dictionary expected ',' or '}'")
}
}
// empty object
if len(O) == 0 {
return nil, nil
}
return O, nil
}
func (p *flagParser) parseKey() (string, error) {
in := p.input
if in == "" {
return "", errors.New("expected key")
}
switch in[0] {
case '"':
return p.parseStringDQuote()
case '\'':
return p.parseStringSQuote()
default:
return p.parseNonQuotedString(objKeyStopSet)
}
}
func (p *flagParser) parseStringDQuote() (string, error) {
in := p.input
off := 1
var i int
for {
i = strings.IndexByte(in[off:], '"')
if i < 0 {
return "", errors.New("Missing \" to close string ")
}
i += off
if in[i-1] != '\\' {
break
}
off = i + 1
}
p.input = in[i+1:]
return strconv.Unquote(in[:i+1])
}
func (p *flagParser) parseStringSQuote() (string, error) {
in := p.input
i := strings.IndexByte(in[1:], '\'')
if i < 0 {
return "", errors.New("missing ' to close string")
}
p.input = in[i+2:]
return in[1 : 1+i], nil
}
func (p *flagParser) parseNonQuotedString(stopSet string) (string, error) {
in := p.input
idx := strings.IndexAny(in, stopSet)
if idx == 0 {
return "", fmt.Errorf("unexpected '%v'", string(in[idx]))
}
content, in := in, ""
if idx > 0 {
content, in = content[:idx], content[idx:]
}
p.input = in
return strings.TrimSpace(content), nil
}
func (p *flagParser) parsePrimitive(stopSet string) (interface{}, error) {
content, err := p.parseNonQuotedString(stopSet)
if err != nil {
return nil, err
}
if content == "null" {
return nil, nil
}
if b, ok := parseBoolValue(content); ok {
return b, nil
}
if n, err := strconv.ParseUint(content, 0, 64); err == nil {
return n, nil
}
if n, err := strconv.ParseInt(content, 0, 64); err == nil {
return n, nil
}
if n, err := strconv.ParseFloat(content, 64); err == nil {
return n, nil
}
return content, nil
}
func (p *flagParser) expectChar(c byte) error {
if p.input == "" || p.input[0] != c {
return fmt.Errorf("expected '%v'", string(c))
}
p.input = p.input[1:]
return nil
}
func parseBoolValue(str string) (value bool, ok bool) {
switch str {
case "t", "T", "true", "TRUE", "True", "on", "ON":
return true, true
case "f", "F", "false", "FALSE", "False", "off", "OFF":
return false, true
}
return false, false
}