package argimport (scalar)// path represents a sequence of steps to find the output location for an// argument or subcommand in the final destination structtype path struct { root int// index of the destination struct fields []reflect.StructField// sequence of struct fields to traverse}// String gets a string representation of the given pathfunc ( path) () string { := "args"for , := range .fields { += "." + .Name }return}// Child gets a new path representing a child of this path.func ( path) ( reflect.StructField) path {// copy the entire slice of fields to avoid possible slice overwrite := make([]reflect.StructField, len(.fields)+1)copy(, .fields) [len()-1] = returnpath{root: .root,fields: , }}// spec represents a command line optiontype spec struct { dest path field reflect.StructField// the struct field from which this option was created long string// the --long form for this option, or empty if none short string// the -s short form for this option, or empty if none cardinality cardinality// determines how many tokens will be present (possible values: zero, one, multiple) required bool// if true, this option must be present on the command line positional bool// if true, this option will be looked for in the positional flags separate bool// if true, each slice and map entry will have its own --flag help string// the help text for this option env string// the name of the environment variable for this option, or empty for none defaultValue reflect.Value// default value for this option defaultString string// default value for this option, in string form to be displayed in help text placeholder string// placeholder string in help}// command represents a named subcommand, or the top-level commandtype command struct { name string aliases []string help string dest path specs []*spec subcommands []*command parent *command}// ErrHelp indicates that the builtin -h or --help were providedvarErrHelp = errors.New("help requested by user")// ErrVersion indicates that the builtin --version was providedvarErrVersion = errors.New("version requested by user")// for monkey patching in example and test codevar mustParseExit = os.Exitvar mustParseOut io.Writer = os.Stdout// MustParse processes command line arguments and exits upon failurefunc ( ...interface{}) *Parser {returnmustParse(Config{Exit: mustParseExit, Out: mustParseOut}, ...)}// mustParse is a helper that facilitates testingfunc mustParse( Config, ...interface{}) *Parser { , := NewParser(, ...)if != nil {fmt.Fprintln(.Out, ) .Exit(2)returnnil } .MustParse(flags())return}// Parse processes command line arguments and stores them in destfunc ( ...interface{}) error { , := NewParser(Config{}, ...)if != nil {return }return .Parse(flags())}// flags gets all command line arguments other than the first (program name)func flags() []string {iflen(os.Args) == 0 { // os.Args could be emptyreturnnil }returnos.Args[1:]}// Config represents configuration options for an argument parsertypeConfigstruct {// Program is the name of the program used in the help text Program string// IgnoreEnv instructs the library not to read environment variables IgnoreEnv bool// IgnoreDefault instructs the library not to reset the variables to the // default values, including pointers to sub commands IgnoreDefault bool// StrictSubcommands intructs the library not to allow global commands after // subcommand StrictSubcommands bool// EnvPrefix instructs the library to use a name prefix when reading environment variables. EnvPrefix string// Exit is called to terminate the process with an error code (defaults to os.Exit) Exit func(int)// Out is where help text, usage text, and failure messages are printed (defaults to os.Stdout) Out io.Writer}// Parser represents a set of command line options with destination valuestypeParserstruct { cmd *command roots []reflect.Value config Config version string description string epilogue string// the following field changes during processing of command line arguments subcommand []string}// Versioned is the interface that the destination struct should implement to// make a version string appear at the top of the help message.typeVersionedinterface {// Version returns the version string that will be printed on a line by itself // at the top of the help message.Version() string}// Described is the interface that the destination struct should implement to// make a description string appear at the top of the help message.typeDescribedinterface {// Description returns the string that will be printed on a line by itself // at the top of the help message.Description() string}// Epilogued is the interface that the destination struct should implement to// add an epilogue string at the bottom of the help message.typeEpiloguedinterface {// Epilogue returns the string that will be printed on a line by itself // at the end of the help message.Epilogue() string}// walkFields calls a function for each field of a struct, recursively expanding struct fields.func walkFields( reflect.Type, func( reflect.StructField, reflect.Type) bool) {walkFieldsImpl(, , nil)}func walkFieldsImpl( reflect.Type, func( reflect.StructField, reflect.Type) bool, []int) {for := 0; < .NumField(); ++ { := .Field() .Index = make([]int, len()+1)copy(.Index, append(, )) := (, )if && .Type.Kind() == reflect.Struct {var []intif .Anonymous { = append(, ) } (.Type, , ) } }}// NewParser constructs a parser from a list of destination structsfunc ( Config, ...interface{}) (*Parser, error) {// fill in defaultsif .Exit == nil { .Exit = os.Exit }if .Out == nil { .Out = os.Stdout }// first pick a name for the command for use in the usage textvarstringswitch {case .Program != "": = .Programcaselen(os.Args) > 0: = filepath.Base(os.Args[0])default: = "program" }// construct a parser := Parser{cmd: &command{name: },config: , }// make a list of rootsfor , := range { .roots = append(.roots, reflect.ValueOf()) }// process each of the destination valuesfor , := range { := reflect.TypeOf()if .Kind() != reflect.Ptr {panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", )) } , := cmdFromStruct(, path{root: }, , .EnvPrefix)if != nil {returnnil, }// for backwards compatibility, add nonzero field values as defaults // this applies only to the top-level command, not to subcommands (this inconsistency // is the reason that this method for setting default values was deprecated)for , := range .specs {// get the value := .val(.dest)// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignoreifisZero() {continue }// store as a default .defaultValue = // we need a string to display in help text // if MarshalText is implemented then use thatif , := .Interface().(encoding.TextMarshaler); { , := .MarshalText()if != nil {returnnil, fmt.Errorf("%v: error marshaling default value to string: %v", .dest, ) } .defaultString = string() } else { .defaultString = fmt.Sprintf("%v", ) } } .cmd.specs = append(.cmd.specs, .specs...) .cmd.subcommands = append(.cmd.subcommands, .subcommands...)if , := .(Versioned); { .version = .Version() }if , := .(Described); { .description = .Description() }if , := .(Epilogued); { .epilogue = .Epilogue() } }// Set the parent of the subcommands to be the top-level command // to make sure that global options work when there is more than one // dest supplied.for , := range .cmd.subcommands { .parent = .cmd }return &, nil}func cmdFromStruct( string, path, reflect.Type, string) (*command, error) {// commands can only be created from pointers to structsif .Kind() != reflect.Ptr {returnnil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s", , .Kind()) } = .Elem()if .Kind() != reflect.Struct {returnnil, fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s", , .Kind()) } := command{name: ,dest: , }var []stringwalkFields(, func( reflect.StructField, reflect.Type) bool {// check for the ignore switch in the tag := .Tag.Get("arg")if == "-" {returnfalse }// if this is an embedded struct then recurse into its fields, even if // it is unexported, because exported fields on unexported embedded // structs are still writableif .Anonymous && .Type.Kind() == reflect.Struct {returntrue }// ignore any other unexported fieldif !isExported(.Name) {returnfalse }// duplicate the entire path to avoid slice overwrites := .Child() := spec{dest: ,field: ,long: strings.ToLower(.Name), } , := .Tag.Lookup("help")if { .help = }// process each comma-separated part of the tagvarboolfor , := rangestrings.Split(, ",") {if == "" {continue } = strings.TrimLeft(, " ")varstringif := strings.Index(, ":"); != -1 { = [+1:] = [:] }switch {casestrings.HasPrefix(, "---"): = append(, fmt.Sprintf("%s.%s: too many hyphens", .Name(), .Name))casestrings.HasPrefix(, "--"): .long = [2:]casestrings.HasPrefix(, "-"):iflen() > 2 { = append(, fmt.Sprintf("%s.%s: short arguments must be one character only", .Name(), .Name))returnfalse } .short = [1:]case == "required": .required = truecase == "positional": .positional = truecase == "separate": .separate = truecase == "help": // deprecated .help = case == "env":// Use override name if providedif != "" { .env = + } else { .env = + strings.ToUpper(.Name) }case == "subcommand":// decide on a name for the subcommandvar []stringif == "" { = []string{strings.ToLower(.Name)} } else { = strings.Split(, "|") }for := range { [] = strings.TrimSpace([]) }// parse the subcommand recursively , := ([0], , .Type, )if != nil { = append(, .Error())returnfalse } .aliases = [1:] .parent = & .help = .Tag.Get("help") .subcommands = append(.subcommands, ) = truedefault: = append(, fmt.Sprintf("unrecognized tag '%s' on field %s", , ))returnfalse } }// placeholder is the string used in the help text like this: "--somearg PLACEHOLDER" , := .Tag.Lookup("placeholder")if { .placeholder = } elseif .long != "" { .placeholder = strings.ToUpper(.long) } else { .placeholder = strings.ToUpper(.field.Name) }// if this is a subcommand then we've done everything we need to doif {returnfalse }// check whether this field is supported. It's good to do this here rather than // wait until ParseValue because it means that a program with invalid argument // fields will always fail regardless of whether the arguments it received // exercised those fields.varerror .cardinality, = cardinalityOf(.Type)if != nil { = append(, fmt.Sprintf("%s.%s: %s fields are not supported", .Name(), .Name, .Type.String()))returnfalse } , := .Tag.Lookup("default")if {// we do not support default values for maps and slicesif .cardinality == multiple { = append(, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields", .Name(), .Name))returnfalse }// a required field cannot also have a default valueif .required { = append(, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified", .Name(), .Name))returnfalse }// parse the default value .defaultString = if .Type.Kind() == reflect.Ptr {// here we have a field of type *T and we create a new T, no need to dereference // in order for the value to be settable .defaultValue = reflect.New(.Type.Elem()) } else {// here we have a field of type T and we create a new T and then dereference it // so that the resulting value is settable .defaultValue = reflect.New(.Type).Elem() } := scalar.ParseValue(.defaultValue, )if != nil { = append(, fmt.Sprintf("%s.%s: error processing default value: %v", .Name(), .Name, ))returnfalse } }// add the spec to the list of specs .specs = append(.specs, &)// if this was an embedded field then we already returned true up abovereturnfalse })iflen() > 0 {returnnil, errors.New(strings.Join(, "\n")) }// check that we don't have both positionals and subcommandsvarboolfor , := range .specs {if .positional { = true } }if && len(.subcommands) > 0 {returnnil, fmt.Errorf("%s cannot have both subcommands and positional arguments", ) }return &, nil}// Parse processes the given command line option, storing the results in the fields// of the structs from which NewParser was constructed.//// It returns ErrHelp if "--help" is one of the command line args and ErrVersion if// "--version" is one of the command line args (the latter only applies if the// destination struct passed to NewParser implements Versioned.)//// To respond to --help and --version in the way that MustParse does, see examples// in the README under "Custom handling of --help and --version".func ( *Parser) ( []string) error { := .process()if != nil {// If -h or --help were specified then make sure help text supercedes other errorsfor , := range {if == "-h" || == "--help" {returnErrHelp }if == "--" {break } } }return}func ( *Parser) ( []string) { := .Parse()switch {case == ErrHelp: .WriteHelpForSubcommand(.config.Out, .subcommand...) .config.Exit(0)case == ErrVersion:fmt.Fprintln(.config.Out, .version) .config.Exit(0)case != nil: .FailSubcommand(.Error(), .subcommand...) }}// process environment vars for the given argumentsfunc ( *Parser) ( []*spec, map[*spec]bool) error {for , := range {if .env == "" {continue } , := os.LookupEnv(.env)if ! {continue }if .cardinality == multiple {// expect a CSV string in an environment // variable in the case of multiple valuesvar []stringvarerroriflen(strings.TrimSpace()) > 0 { , = csv.NewReader(strings.NewReader()).Read()if != nil {returnfmt.Errorf("error reading a CSV string from environment variable %s with multiple values: %v", .env, , ) } }if = setSliceOrMap(.val(.dest), , !.separate); != nil {returnfmt.Errorf("error processing environment variable %s with multiple values: %v", .env, , ) } } else {if := scalar.ParseValue(.val(.dest), ); != nil {returnfmt.Errorf("error processing environment variable %s: %v", .env, ) } } [] = true }returnnil}// process goes through arguments one-by-one, parses them, and assigns the result to// the underlying struct fieldfunc ( *Parser) ( []string) error {// track the options we have seen := make(map[*spec]bool)// union of specs for the chain of subcommands encountered so far := .cmd .subcommand = nil// make a copy of the specs because we will add to this list each time we expand a subcommand := make([]*spec, len(.specs))copy(, .specs)// deal with environment varsif !.config.IgnoreEnv { := .captureEnvVars(, )if != nil {return } }// determine if the current command has a version option specvarboolfor , := range .specs {if .long == "version" { = truebreak } }// process each string from the command linevarboolvar []string// must use explicit for loop, not range, because we manipulate i inside the loopfor := 0; < len(); ++ { := []if == "--" && ! { = truecontinue }if !isFlag() || {// each subcommand can have either subcommands or positionals, but not bothiflen(.subcommands) == 0 { = append(, )continue }// if we have a subcommand then make sure it is valid for the current context := findSubcommand(.subcommands, )if == nil {returnfmt.Errorf("invalid subcommand: %s", ) }// instantiate the field to point to a new struct := .val(.dest)if .IsNil() { .Set(reflect.New(.Type().Elem())) // we already checked that all subcommands are struct pointers }// add the new options to the set of allowed optionsif .config.StrictSubcommands { = make([]*spec, len(.specs))copy(, .specs) } else { = append(, .specs...) }// capture environment vars for these new optionsif !.config.IgnoreEnv { := .captureEnvVars(.specs, )if != nil {return } } = .subcommand = append(.subcommand, )continue }// check for special --help and --version flagsswitch {case"-h", "--help":returnErrHelpcase"--version":if ! && .version != "" {returnErrVersion } }// check for an equals sign, as in "--foo=bar"varstring := strings.TrimLeft(, "-")if := strings.Index(, "="); != -1 { = [+1:] = [:] }// lookup the spec for this option (note that the "specs" slice changes as // we expand subcommands so it is better not to use a map) := findOption(, )if == nil || == "" {returnfmt.Errorf("unknown argument %s", ) } [] = true// deal with the case of multiple valuesif .cardinality == multiple {var []stringif == "" {for +1 < len() && isValue([+1], .field.Type, ) && [+1] != "--" { = append(, [+1]) ++if .separate {break } } } else { = append(, ) } := setSliceOrMap(.val(.dest), , !.separate)if != nil {returnfmt.Errorf("error processing %s: %v", , ) }continue }// if it's a flag and it has no value then set the value to true // use boolean because this takes account of TextUnmarshalerif .cardinality == zero && == "" { = "true" }// if we have something like "--foo" then the value is the next argumentif == "" {if +1 == len() {returnfmt.Errorf("missing value for %s", ) }if !isValue([+1], .field.Type, ) {returnfmt.Errorf("missing value for %s", ) } = [+1] ++ } := scalar.ParseValue(.val(.dest), )if != nil {returnfmt.Errorf("error processing %s: %v", , ) } }// process positionalsfor , := range {if !.positional {continue }iflen() == 0 {break } [] = trueif .cardinality == multiple { := setSliceOrMap(.val(.dest), , true)if != nil {returnfmt.Errorf("error processing %s: %v", .placeholder, ) } = nil } else { := scalar.ParseValue(.val(.dest), [0])if != nil {returnfmt.Errorf("error processing %s: %v", .placeholder, ) } = [1:] } }iflen() > 0 {returnfmt.Errorf("too many positional arguments at '%s'", [0]) }// fill in defaults and check that all the required args were providedfor , := range {if [] {continue }if .required {if .short == "" && .long == "" { := fmt.Sprintf("environment variable %s is required", .env)returnerrors.New() } := fmt.Sprintf("%s is required", .placeholder)if .env != "" { += " (or environment variable " + .env + ")" }returnerrors.New() }if .defaultValue.IsValid() && !.config.IgnoreDefault {// One issue here is that if the user now modifies the value then // the default value stored in the spec will be corrupted. There // is no general way to "deep-copy" values in Go, and we still // support the old-style method for specifying defaults as // Go values assigned directly to the struct field, so we are stuck. .val(.dest).Set(.defaultValue) } }returnnil}// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"func isFlag( string) bool {returnstrings.HasPrefix(, "-") && strings.TrimLeft(, "-") != ""}// isValue returns true if a token should be consumed as a value for a flag of type t. This// is almost always the inverse of isFlag. The one exception is for negative numbers, in which// case we check the list of active options and return true if its not present there.func isValue( string, reflect.Type, []*spec) bool {switch .Kind() {casereflect.Ptr, reflect.Slice:return (, .Elem(), )casereflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: := reflect.New() := scalar.ParseValue(, )// if value can be parsed and is not an explicit option declared elsewhere, then use it as a valueif == nil && (!strings.HasPrefix(, "-") || findOption(, strings.TrimPrefix(, "-")) == nil) {returntrue } }// default case that is used in all cases other than negative numbers: inverse of isFlagreturn !isFlag()}// val returns a reflect.Value corresponding to the current value for the// given pathfunc ( *Parser) ( path) reflect.Value { := .roots[.root]for , := range .fields {if .Kind() == reflect.Ptr {if .IsNil() {returnreflect.Value{} } = .Elem() } = .FieldByIndex(.Index) }return}// findOption finds an option from its name, or returns null if no spec is foundfunc findOption( []*spec, string) *spec {for , := range {if .positional {continue }if .long == || .short == {return } }returnnil}// findSubcommand finds a subcommand using its name, or returns null if no subcommand is foundfunc findSubcommand( []*command, string) *command {for , := range {if .name == {return }for , := range .aliases {if == {return } } }returnnil}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.