Skip to content
Browse files

Doc updates and a couple more minor features

  • Loading branch information...
1 parent b1ae2f4 commit b5da7c8c10915e1094c04472ad4fb5b033bd4143 @gaal committed Jan 22, 2012
Showing with 206 additions and 10 deletions.
  1. +19 −0 LICENSE
  2. +18 −0 README.markdown
  3. +5 −0 TODO.markdown
  4. +147 −6 options.go
  5. +17 −4 options_test.go
View
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Google, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
View
18 README.markdown
@@ -0,0 +1,18 @@
+go-options - command line parsing library for Go.
+
+* Easy to use - no boilerplate
+* Self-documenting - the spec turns into the usage string
+* Powerful - doesn't do everything you ever dreamed of, but comes close
+
+This design is inspired by `git revparse --parseopt` and the discussion of
+`bup.options` here: <http://apenwarr.ca/log/?m=201111#02>. There are some
+minor deviations.
+
+* On the code side, you must access the opt structure with canonical option
+names only. This is intended to reduce programmer errors.
+* When I support negated options, I will not support unnegated aliases
+for them as that can lead to more confusion than I deem worth it.
+
+This package is distributed under the MIT/X license.
+
+Comments? Please write me at <gaal@forum2.org>.
View
5 TODO.markdown
@@ -0,0 +1,5 @@
+* Simple clustering of short opts, e.g., `-abc`
+* Smooshing with short opts, e.g., `-abcfoo` == `-a -b -c foo` magically
+* Negated args, e.g., `--no-foo`. (Note, I'm not sure "no" needs to be
+allowed as part of the name and even if so, all aliases should have the
+same negation value. Probably just allow it on the user side.)
View
153 options.go
@@ -1,3 +1,85 @@
+// Copyright 2012 Google Inc. All rights reserved.
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file.
+
+// Package options provides a command line option parser.
+//
+// This package is meant as an alternative to the core flag package. It
+// is more powerful without attempting to support every possible feature
+// some parsing library ever introduced. It is arguably easier to use.
+//
+// Usage:
+//
+// Create an OptionsSpec that documents your program's allowed flags. This
+// begins with a free-text synopsis of your command line interface, then
+// a line containing only two dashes, then a series of option specifications:
+//
+// import "options"
+// s := options.NewOptions(`
+// cat - concatenate files to standard input
+// Usage: cat [OPTIONS] file...
+// This version of cat supports character set conversion.
+// Fancifully, you can say "-r 3" and have everything told you three times.
+// --
+// n,numerate,number number input lines
+// e,escape escape nonprintable characters
+// i,input-encoding= charset input is encoded in [utf-8]
+// o,output-encoding= charset output is encoded in [utf-8]
+// r,repeat= repeat every line some number of times [1]
+// v,verbose be verbose
+// `)
+//
+// Then parse the command line:
+//
+// opt, flags, extra := s.Parse(os.Args)
+//
+// Options may have any number of aliases; the last one is the "canonical"
+// name and the one your program must use when reading values.
+//
+// opt.Get("input-encoding") // Returns "utf-8", or whatever user set.
+// opt.Get("i") // Error! No option with that canonical name.
+// opt.Get("number") // Returns "" if the user didn't specify it.
+//
+// Get returns a string. Several very simple conversions are provided but you
+// are encouraged to write your own if you need more.
+//
+// opt.GetBool("escape") // false (by default)
+// opt.GetBool("number") // false (by default)
+// opt.GetInt("repeat") // 1 (by default)
+//
+// Options either take a required argument or take no argument. Non-argument
+// options have useful values exposed as bool and ints.
+//
+// // cat -v -v -v
+// opt.GetBool("verbose") // true
+// opt.GetInt("verbose") // 3
+//
+// The user can say either "--foo=bar" or "--foo bar".
+//
+// Parsing stops if "--" is given on the command line.
+//
+// The "extra" return value of Parse contains all non-option command line
+// input. In the case of a cat command, this would be the filenames to concat.
+//
+// By default, options permits such extra values. Setting UnknownValuesFatal
+// causes it to panic when it enconters them instead.
+//
+// The "flags" return value of Parse contains the series of flags as given on
+// the command line, including repeated ones (which are suppressed in opt --
+// it only contains the last value). This allows you to do your own handling
+// of repeated options easily.
+//
+// By default, options does not permit unknown flags. Setting
+// UnknownOptionsFatal to false causes them to be recorded in "flags" instead.
+// Note that since they have no canonical name, they cannot be accessed via
+// opt. Also note that since options does not know about the meaning of these
+// flags, it has to guess whether they consume the next argument or not. This
+// is currently done naively by peeking at the first character of the next
+// argument.
+//
+// BUG(gaal): Clustering of short options ("cat -vvv") is not yet supported.
+// BUG(gaal): Negated options ("--no-frobulate") are not yet supported.
+// BUG(gaal): Option groups are not yet supported.
package options
import (
@@ -6,11 +88,16 @@ import (
"strings"
)
+// Options represents the known formal options provided on the command line.
type Options struct {
opts map[string]string
known map[string]bool
}
+// Get returns the value of an option, which must be known to this parse.
+// Options that take an argument return the argument. Options with no argument
+// return values hinting whether they were specified or not; GetInt or GetBool
+// may be more suited for looking them up.
func (o *Options) Get(flag string) string {
val, ok := o.opts[flag]
if !ok {
@@ -21,6 +108,8 @@ func (o *Options) Get(flag string) string {
return val
}
+// GetInt returns the value of an option as an integer. The empty string is
+// treated as zero, but otherwise the option must parse or a panic occurs.
func (o *Options) GetInt(flag string) int {
val := o.Get(flag)
if val == "" {
@@ -33,17 +122,47 @@ func (o *Options) GetInt(flag string) int {
return num
}
+// GetBool returns the value of an option as a bool. All values are treated
+// as true except for the following which yield false:
+// "" (empty), "0", "false", "off", "nil", "null", "no"
func (o *Options) GetBool(flag string) bool {
val := o.Get(flag)
if val == "" || val == "0" || val == "false" ||
- val == "off" || val == "nil" || val == "no" {
+ val == "off" || val == "nil" || val == "null" || val == "no" {
return false
}
return true
}
+// Have returns false when an option has no default value and was not given
+// on the command line, or true otherwise.
+func (o *Options) Have(flag string) bool {
+ if !o.known[flag] {
+ panic(fmt.Sprintf("[Programmer error] Unknown option: %s\ndump: %+v", flag, *o))
+ }
+ _, ok := o.opts[flag]
+ return ok
+}
+
+// GetAll is a convenience function which scans the "flags" return value of
+// OptionSpec.Parse, and gathers all the values of a given option. This must
+// be a required-argument option.
+func GetAll(flag string, flags [][]string) []string {
+ out := make([]string, 0)
+ for _, val := range flags {
+ if (val[0] == flag) {
+ if (len(val) != 2) {
+ panic("[Programmer error] Option does not appear to take arguments: " + flag)
+ }
+ out = append(out, val[1])
+ }
+ }
+ return out
+}
+
+// OptionSpec represents the specification of a command line interface.
type OptionSpec struct {
- Usage string
+ Usage string // Formatted usage string
UnknownOptionsFatal bool // Whether to die on unknown flags [true]
UnknownValuesFatal bool // Whether to die un extra nonflags [false]
aliases map[string]string
@@ -52,6 +171,22 @@ type OptionSpec struct {
requiresArg map[string]bool
}
+// SetUnknownOptionsFatal is a conveience function designed to be chained
+// after NewOptions.
+func (s *OptionSpec) SetUnknownOptionsFatal(val bool) *OptionSpec {
+ s.UnknownOptionsFatal = val;
+ return s
+}
+
+// SetUnknownValuesFatal is a conveience function designed to be chained
+// after NewOptions.
+func (s *OptionSpec) SetUnknownValuesFatal(val bool) *OptionSpec {
+ s.UnknownValuesFatal = val;
+ return s
+}
+
+// NewOptions takes a string speficiation of a command line interface and
+// returns an OptionSpec for you to call Parse on.
func NewOptions(spec string) *OptionSpec {
// TODO(gaal): move to constant
flagSpec := regexp.MustCompile(`^([-\w,]+)(=?)\s+(.*)$`)
@@ -114,6 +249,12 @@ func NewOptions(spec string) *OptionSpec {
return s
}
+// Parse performs the actual parsing of a command line according to an
+// OptionSpec.
+// It returns three values: opt, flags, extra; see the package description
+// for an overview of what they mean and how they are used.
+// In case of parse error, a panic is thrown.
+// TODO(gaal): decide if gentler error signalling is more useful.
func (s *OptionSpec) Parse(args []string) (Options, [][]string, []string) {
// TODO(gaal): extract to constant.
flagRe := regexp.MustCompile(`^((--?)([-\w]+))(=(.*))?$`)
@@ -139,7 +280,7 @@ func (s *OptionSpec) Parse(args []string) (Options, [][]string, []string) {
flagParts := flagRe.FindStringSubmatch(val)
if flagParts == nil { // This is not a flag.
if s.UnknownValuesFatal {
- panic("Unexpected argument: " + val)
+ panic("Unexpected argument: " + val + "\n" + s.Usage)
}
extra = append(extra, val)
continue
@@ -175,20 +316,20 @@ func (s *OptionSpec) Parse(args []string) (Options, [][]string, []string) {
recordOptionValue(*nextArg)
i++
} else {
- panic("Option requires argument: " + canonical)
+ panic("Option requires argument: " + canonical + "\n" + s.Usage)
}
} else {
// TODO(gaal): decide what to do: we were given an argument to
// an option that doesn't take one. Do we treat this as an
// optional argument and just record it? Panic?
if haveSelfValue {
- panic("Option does not take argument: " + canonical)
+ panic("Option does not take argument: " + canonical + "\n" + s.Usage)
}
recordOptionNoValue()
}
} else { // Unknown option: try to do the right thing.
if s.UnknownOptionsFatal {
- panic("Unexpected option argument: " + val)
+ panic("Unexpected option argument: " + val + "\n" + s.Usage)
}
if haveSelfValue {
recordOptionValue(selfValue)
View
21 options_test.go
@@ -63,7 +63,7 @@ func TestParse_extra(t *testing.T) {
ExpectEquals(t, [][]string{[]string{"--ccc", "myval"}}, flags, "flags specified")
ExpectEquals(t, []string{"extra1", "extra2"}, extra, "extra args given")
- s.UnknownValuesFatal = true
+ s.SetUnknownValuesFatal(true)
ExpectDies(t, func() {
s.Parse([]string{"extra1", "--ccc", "myval", "extra2"})
}, "dies on extras when asked to")
@@ -76,7 +76,7 @@ func TestParse_unknownFlags(t *testing.T) {
s.Parse([]string{"--ccc", "myval", "--unk"})
}, "dies on unknown options unless asked not to")
- s.UnknownOptionsFatal = false
+ s.SetUnknownOptionsFatal(false)
opt, flags, extra := s.Parse([]string{"--unk1", "--ccc", "myval", "--unk2", "val2", "--unk3"})
ExpectEquals(t, "myval", opt.Get("ccc"), "Get")
ExpectEquals(t, [][]string{
@@ -111,7 +111,20 @@ d,bbb,eee an option with dupe`
ExpectDies(t, func() { NewOptions(spec) })
}
-// These are small little testing utilities that I like. May move it to a separate module one day.
+func TestGetAll(t *testing.T) {
+ ExpectEquals(
+ t,
+ []string{},
+ GetAll("elk", [][]string{[]string{"foo", "aaa"}, []string{"bar"}, []string{"foo", "bbb"}}),
+ "GetAll - nothing there")
+ ExpectEquals(
+ t,
+ []string{"aaa", "bbb"},
+ GetAll("foo", [][]string{[]string{"foo", "aaa"}, []string{"bar"}, []string{"foo", "bbb"}}),
+ "GetAll")
+}
+
+// These are little testing utilities that I like. May move to a separate module one day.
func Wrap(vs ...interface{}) interface{} {
return vs
@@ -141,5 +154,5 @@ func TestExpectDies(t *testing.T) {
ExpectDies(t, func() { panic("aaaaahh") }, "simple panic dies")
t1 := new(testing.T)
ExpectDies(t1, func() {}, "doesn't die")
- ExpectEquals(t, Wrap(true), Wrap(t1.Failed()), "ExpectDies on something that doesn't die fails")
+ ExpectEquals(t, true, t1.Failed(), "ExpectDies on something that doesn't die fails")
}

0 comments on commit b5da7c8

Please sign in to comment.
Something went wrong with that request. Please try again.