Merge lib/netconfig from release.go.x.ref into release.go.x.lib.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..53191fd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/.v23
diff --git a/cmdline/cmdline.go b/cmdline/cmdline.go
new file mode 100644
index 0000000..d10c76a
--- /dev/null
+++ b/cmdline/cmdline.go
@@ -0,0 +1,526 @@
+// Package cmdline provides a data-driven framework to simplify writing
+// command-line programs. It includes built-in support for formatted help.
+//
+// Commands may be linked together to form a command tree. Since commands may
+// be arbitrarily nested within other commands, it's easy to create wrapper
+// programs that invoke existing commands.
+//
+// The syntax for each command-line program is:
+//
+// command [flags] [subcommand [flags]]* [args]
+//
+// Each sequence of flags on the command-line is associated with the command
+// that immediately precedes them. Global flags registered with the standard
+// flags package are allowed anywhere a command-specific flag is allowed.
+package cmdline
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "strconv"
+ "strings"
+
+ "v.io/x/lib/textutil"
+)
+
+// ErrExitCode may be returned by the Run function of a Command to cause the
+// program to exit with a specific error code.
+type ErrExitCode int
+
+func (x ErrExitCode) Error() string {
+ return fmt.Sprintf("exit code %d", x)
+}
+
+// ErrUsage is returned to indicate an error in command usage; e.g. unknown
+// flags, subcommands or args. It corresponds to exit code 1.
+const ErrUsage = ErrExitCode(1)
+
+// Command represents a single command in a command-line program. A program
+// with subcommands is represented as a root Command with children representing
+// each subcommand. The command graph must be a tree; each command may either
+// have exactly one parent (a sub-command), or no parent (the root), and cycles
+// are not allowed. This makes it easier to display the usage for subcommands.
+type Command struct {
+ Name string // Name of the command.
+ Short string // Short description, shown in help called on parent.
+ Long string // Long description, shown in help called on itself.
+
+ // WARNING: If this Command is the root of the command tree, specifying
+ // flags this way will interfere with attempts to parse flag args in
+ // other code that may be linked in (since the other code's flag set --
+ // typically, the global flag set -- will likely not contain the flags
+ // defined here). In such cases, it's recommended to just use the
+ // global flag set for all flags and avoid defining flags on the root
+ // command.
+ Flags flag.FlagSet // Flags for the command.
+ ArgsName string // Name of the args, shown in usage line.
+ ArgsLong string // Long description of the args, shown in help.
+
+ // Children of the command. The framework will match args[0] against each
+ // child's name, and call Run on the first matching child.
+ Children []*Command
+
+ // Topics that provide additional info via the default help command.
+ Topics []Topic
+
+ // Run is a function that runs cmd with args. If both Children and Run are
+ // specified, Run will only be called if none of the children match. It is an
+ // error if neither is specified. The special ErrExitCode error may be
+ // returned to indicate the command should exit with a specific exit code.
+ Run func(cmd *Command, args []string) error
+
+ // parent holds the parent of this Command, or nil if this is the root.
+ parent *Command
+
+ // stdout and stderr are set through Init.
+ stdout, stderr io.Writer
+
+ // parseFlags holds the merged flags used for parsing. Each command starts
+ // with its own Flags, and we merge in all global flags. If the same flag is
+ // specified in both sets, the command's own flag wins.
+ parseFlags *flag.FlagSet
+
+ // isDefaultHelp indicates whether this is the the default help command
+ // provided by the framework.
+ isDefaultHelp bool
+
+ // TODO(toddw): If necessary we can add alias support, e.g. for abbreviations.
+ // Alias map[string]string
+}
+
+// Topic represents an additional help topic that is accessed via the default
+// help command.
+type Topic struct {
+ Name string // Name of the topic.
+ Short string // Short description, shown in help for the command.
+ Long string // Long description, shown in help for this topic.
+}
+
+// style describes the formatting style for usage descriptions.
+type style int
+
+const (
+ styleText style = iota // Default style, good for cmdline output.
+ styleGoDoc // Style good for godoc processing.
+)
+
+// String returns the human-readable representation of the style.
+func (s *style) String() string {
+ switch *s {
+ case styleText:
+ return "text"
+ case styleGoDoc:
+ return "godoc"
+ default:
+ panic(fmt.Errorf("Unhandled style %d", *s))
+ }
+}
+
+// Set implements the flag.Value interface method.
+func (s *style) Set(value string) error {
+ switch value {
+ case "text":
+ *s = styleText
+ case "godoc":
+ *s = styleGoDoc
+ default:
+ return fmt.Errorf("Unknown style %q", value)
+ }
+ return nil
+}
+
+// Stdout is where output goes. Typically os.Stdout.
+func (cmd *Command) Stdout() io.Writer {
+ return cmd.stdout
+}
+
+// Stderr is where error messages go. Typically os.Stderr
+func (cmd *Command) Stderr() io.Writer {
+ return cmd.stderr
+}
+
+// UsageErrorf prints the error message represented by the printf-style format
+// string and args, followed by the usage description of cmd. Returns ErrUsage
+// to make it easy to use from within the cmd.Run function.
+func (cmd *Command) UsageErrorf(format string, v ...interface{}) error {
+ fmt.Fprint(cmd.stderr, "ERROR: ")
+ fmt.Fprintf(cmd.stderr, format, v...)
+ fmt.Fprint(cmd.stderr, "\n\n")
+ cmd.writeUsage(cmd.stderr)
+ return ErrUsage
+}
+
+// Have a reasonable default for the output width in runes.
+const defaultWidth = 80
+
+func outputWidth() int {
+ if width, err := strconv.Atoi(os.Getenv("CMDLINE_WIDTH")); err == nil && width != 0 {
+ return width
+ }
+ if _, width, err := textutil.TerminalSize(); err == nil && width != 0 {
+ return width
+ }
+ return defaultWidth
+}
+
+func (cmd *Command) writeUsage(w io.Writer) {
+ lineWriter := textutil.NewUTF8LineWriter(w, outputWidth())
+ cmd.usage(lineWriter, true)
+ lineWriter.Flush()
+}
+
+// usage prints the usage of cmd to the writer. The firstCall boolean is set to
+// false when printing usage for multiple commands, and is used to avoid
+// printing redundant information (e.g. help command, global flags).
+func (cmd *Command) usage(w *textutil.LineWriter, firstCall bool) {
+ fmt.Fprintln(w, cmd.Long)
+ fmt.Fprintln(w)
+ // Usage line.
+ hasFlags := numFlags(&cmd.Flags) > 0
+ fmt.Fprintln(w, "Usage:")
+ path := cmd.namePath()
+ pathf := " " + path
+ if hasFlags {
+ pathf += " [flags]"
+ }
+ if len(cmd.Children) > 0 {
+ fmt.Fprintln(w, pathf, "<command>")
+ }
+ if cmd.Run != nil {
+ if cmd.ArgsName != "" {
+ fmt.Fprintln(w, pathf, cmd.ArgsName)
+ } else {
+ fmt.Fprintln(w, pathf)
+ }
+ }
+ if len(cmd.Children) == 0 && cmd.Run == nil {
+ // This is a specification error.
+ fmt.Fprintln(w, pathf, "[ERROR: neither Children nor Run is specified]")
+ }
+ // Commands.
+ const minNameWidth = 11
+ if len(cmd.Children) > 0 {
+ fmt.Fprintln(w)
+ fmt.Fprintln(w, "The", path, "commands are:")
+ nameWidth := minNameWidth
+ for _, child := range cmd.Children {
+ if len(child.Name) > nameWidth {
+ nameWidth = len(child.Name)
+ }
+ }
+ // Print as a table with aligned columns Name and Short.
+ w.SetIndents(spaces(3), spaces(3+nameWidth+1))
+ for _, child := range cmd.Children {
+ // Don't repeatedly list default help command.
+ if !child.isDefaultHelp || firstCall {
+ fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, child.Name, child.Short)
+ w.Flush()
+ }
+ }
+ w.SetIndents()
+ if firstCall {
+ fmt.Fprintf(w, "Run \"%s help [command]\" for command usage.\n", path)
+ }
+ }
+ // Args.
+ if cmd.Run != nil && cmd.ArgsLong != "" {
+ fmt.Fprintln(w)
+ fmt.Fprintln(w, cmd.ArgsLong)
+ }
+ // Help topics.
+ if len(cmd.Topics) > 0 {
+ fmt.Fprintln(w)
+ fmt.Fprintln(w, "The", path, "additional help topics are:")
+ nameWidth := minNameWidth
+ for _, topic := range cmd.Topics {
+ if len(topic.Name) > nameWidth {
+ nameWidth = len(topic.Name)
+ }
+ }
+ // Print as a table with aligned columns Name and Short.
+ w.SetIndents(spaces(3), spaces(3+nameWidth+1))
+ for _, topic := range cmd.Topics {
+ fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, topic.Name, topic.Short)
+ w.Flush()
+ }
+ w.SetIndents()
+ if firstCall {
+ fmt.Fprintf(w, "Run \"%s help [topic]\" for topic details.\n", path)
+ }
+ }
+ // Flags.
+ if hasFlags {
+ fmt.Fprintln(w)
+ fmt.Fprintln(w, "The", path, "flags are:")
+ printFlags(w, &cmd.Flags)
+ }
+ // Global flags.
+ if numFlags(flag.CommandLine) > 0 && firstCall {
+ fmt.Fprintln(w)
+ fmt.Fprintln(w, "The global flags are:")
+ printFlags(w, flag.CommandLine)
+ }
+}
+
+// namePath returns the path of command names up to cmd.
+func (cmd *Command) namePath() string {
+ var path []string
+ for ; cmd != nil; cmd = cmd.parent {
+ path = append([]string{cmd.Name}, path...)
+ }
+ return strings.Join(path, " ")
+}
+
+func numFlags(set *flag.FlagSet) (num int) {
+ set.VisitAll(func(*flag.Flag) {
+ num++
+ })
+ return
+}
+
+func printFlags(w *textutil.LineWriter, set *flag.FlagSet) {
+ set.VisitAll(func(f *flag.Flag) {
+ fmt.Fprintf(w, " -%s=%s", f.Name, f.DefValue)
+ w.SetIndents(spaces(3))
+ fmt.Fprintln(w, f.Usage)
+ w.SetIndents()
+ })
+}
+
+func spaces(count int) string {
+ return strings.Repeat(" ", count)
+}
+
+// newDefaultHelp creates a new default help command. We need to create new
+// instances since the parent for each help command is different.
+func newDefaultHelp() *Command {
+ helpStyle := styleText
+ help := &Command{
+ Name: helpName,
+ Short: "Display help for commands or topics",
+ Long: `
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+`,
+ ArgsName: "[command/topic ...]",
+ ArgsLong: `
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+`,
+ Run: func(cmd *Command, args []string) error {
+ // Help applies to its parent - e.g. "foo help" applies to the foo command.
+ lineWriter := textutil.NewUTF8LineWriter(cmd.stdout, outputWidth())
+ defer lineWriter.Flush()
+ return runHelp(lineWriter, cmd.parent, args, helpStyle)
+ },
+ isDefaultHelp: true,
+ }
+ help.Flags.Var(&helpStyle, "style", `The formatting style for help output, either "text" or "godoc".`)
+ return help
+}
+
+const helpName = "help"
+
+// runHelp runs the "help" command.
+func runHelp(w *textutil.LineWriter, cmd *Command, args []string, style style) error {
+ if len(args) == 0 {
+ cmd.usage(w, true)
+ return nil
+ }
+ if args[0] == "..." {
+ recursiveHelp(w, cmd, style, true)
+ return nil
+ }
+ // Try to display help for the subcommand.
+ subName, subArgs := args[0], args[1:]
+ for _, child := range cmd.Children {
+ if child.Name == subName {
+ return runHelp(w, child, subArgs, style)
+ }
+ }
+ // Try to display help for the help topic.
+ for _, topic := range cmd.Topics {
+ if topic.Name == subName {
+ fmt.Fprintln(w, topic.Long)
+ return nil
+ }
+ }
+ return cmd.UsageErrorf("%s: unknown command or topic %q", cmd.namePath(), subName)
+}
+
+// recursiveHelp prints help recursively via DFS from this cmd onward.
+func recursiveHelp(w *textutil.LineWriter, cmd *Command, style style, firstCall bool) {
+ if !firstCall {
+ lineBreak(w, style)
+ // Title-case required for godoc to recognize this as a section header.
+ header := strings.Title(cmd.namePath())
+ fmt.Fprintln(w, header)
+ fmt.Fprintln(w)
+ }
+ cmd.usage(w, firstCall)
+ for _, child := range cmd.Children {
+ // Don't repeatedly print default help command.
+ if !child.isDefaultHelp || firstCall {
+ recursiveHelp(w, child, style, false)
+ }
+ }
+ for _, topic := range cmd.Topics {
+ lineBreak(w, style)
+ // Title-case required for godoc to recognize this as a section header.
+ header := strings.Title(cmd.namePath()+" "+topic.Name) + " - help topic"
+ fmt.Fprintln(w, header)
+ fmt.Fprintln(w)
+ fmt.Fprintln(w, topic.Long)
+ }
+}
+
+func lineBreak(w *textutil.LineWriter, style style) {
+ w.Flush()
+ switch style {
+ case styleText:
+ width := w.Width()
+ if width < 0 {
+ // If the user has chosen an "unlimited" word-wrapping width, we still
+ // need a reasonable width for our visual line break.
+ width = defaultWidth
+ }
+ fmt.Fprintln(w, strings.Repeat("=", width))
+ case styleGoDoc:
+ fmt.Fprintln(w)
+ }
+ w.Flush()
+}
+
+func trimNewlines(s *string) { *s = strings.Trim(*s, "\n") }
+
+// Init initializes all nodes in the command tree rooted at cmd. Init must be
+// called before Execute.
+func (cmd *Command) Init(parent *Command, stdout, stderr io.Writer) {
+ cmd.parent = parent
+ cmd.stdout = stdout
+ cmd.stderr = stderr
+ trimNewlines(&cmd.Short)
+ trimNewlines(&cmd.Long)
+ trimNewlines(&cmd.ArgsLong)
+ for tx := range cmd.Topics {
+ trimNewlines(&cmd.Topics[tx].Short)
+ trimNewlines(&cmd.Topics[tx].Long)
+ }
+ // Add help command, if it doesn't already exist.
+ hasHelp := false
+ for _, child := range cmd.Children {
+ if child.Name == helpName {
+ hasHelp = true
+ break
+ }
+ }
+ if !hasHelp && cmd.Name != helpName && len(cmd.Children) > 0 {
+ cmd.Children = append(cmd.Children, newDefaultHelp())
+ }
+ // Merge command-specific and global flags into parseFlags. We want to handle
+ // all error output ourselves, so we:
+ // 1) Set flag.ContinueOnError so that Parse() doesn't exit or panic.
+ // 2) Discard all output (can't be nil, that means stderr).
+ // 3) Set an empty Usage function (can't be nil, that means default).
+ cmd.parseFlags = flag.NewFlagSet(cmd.Name, flag.ContinueOnError)
+ cmd.parseFlags.SetOutput(ioutil.Discard)
+ cmd.parseFlags.Usage = emptyUsage
+ mergeFlags(cmd.parseFlags, &cmd.Flags)
+ mergeFlags(cmd.parseFlags, flag.CommandLine)
+ // Call children recursively.
+ for _, child := range cmd.Children {
+ child.Init(cmd, stdout, stderr)
+ }
+}
+
+func mergeFlags(dst, src *flag.FlagSet) {
+ src.VisitAll(func(f *flag.Flag) {
+ trimNewlines(&f.Usage)
+ if dst.Lookup(f.Name) == nil {
+ dst.Var(f.Value, f.Name, f.Usage)
+ }
+ })
+}
+
+func emptyUsage() {}
+
+// Execute the command with the given args. The returned error is ErrUsage if
+// there are usage errors, otherwise it is whatever the leaf command returns
+// from its Run function.
+func (cmd *Command) Execute(args []string) error {
+ path := cmd.namePath()
+ // Parse the merged flags.
+ if err := cmd.parseFlags.Parse(args); err != nil {
+ if err == flag.ErrHelp {
+ cmd.writeUsage(cmd.stdout)
+ return nil
+ }
+ return cmd.UsageErrorf("%s: %v", path, err)
+ }
+ args = cmd.parseFlags.Args()
+ // Look for matching children.
+ if len(args) > 0 {
+ subName, subArgs := args[0], args[1:]
+ for _, child := range cmd.Children {
+ if child.Name == subName {
+ return child.Execute(subArgs)
+ }
+ }
+ }
+ // No matching children, try Run.
+ if cmd.Run != nil {
+ if cmd.ArgsName == "" && len(args) > 0 {
+ if len(cmd.Children) > 0 {
+ return cmd.UsageErrorf("%s: unknown command %q", path, args[0])
+ }
+ return cmd.UsageErrorf("%s doesn't take any arguments", path)
+ }
+ return cmd.Run(cmd, args)
+ }
+ switch {
+ case len(cmd.Children) == 0:
+ return cmd.UsageErrorf("%s: neither Children nor Run is specified", path)
+ case len(args) > 0:
+ return cmd.UsageErrorf("%s: unknown command %q", path, args[0])
+ default:
+ return cmd.UsageErrorf("%s: no command specified", path)
+ }
+}
+
+// Main executes the command tree rooted at cmd, writing output to os.Stdout,
+// writing errors to os.Stderr, and getting args from os.Args. We return
+// an appropriate exit code depending on whether there were errors or not.
+// Users should call os.Exit(exitCode).
+//
+// Many main packages can use this simple pattern:
+//
+// var cmd := &cmdline.Command{
+// ...
+// }
+//
+// func main() {
+// os.Exit(cmd.Main())
+// }
+//
+func (cmd *Command) Main() (exitCode int) {
+ cmd.Init(nil, os.Stdout, os.Stderr)
+ if err := cmd.Execute(os.Args[1:]); err != nil {
+ if code, ok := err.(ErrExitCode); ok {
+ return int(code)
+ }
+ fmt.Fprintln(os.Stderr, "ERROR:", err)
+ return 2
+ }
+ return 0
+}
diff --git a/cmdline/cmdline_test.go b/cmdline/cmdline_test.go
new file mode 100644
index 0000000..6cf6b0e
--- /dev/null
+++ b/cmdline/cmdline_test.go
@@ -0,0 +1,2238 @@
+package cmdline
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "regexp"
+ "strings"
+ "testing"
+)
+
+var (
+ errEcho = errors.New("echo error")
+ flagExtra bool
+ optNoNewline bool
+ flagTopLevelExtra bool
+ globalFlag1 string
+ globalFlag2 *int64
+)
+
+// runEcho is used to implement commands for our tests.
+func runEcho(cmd *Command, args []string) error {
+ if len(args) == 1 {
+ if args[0] == "error" {
+ return errEcho
+ } else if args[0] == "bad_arg" {
+ return cmd.UsageErrorf("Invalid argument %v", args[0])
+ }
+ }
+ if flagExtra {
+ args = append(args, "extra")
+ }
+ if flagTopLevelExtra {
+ args = append(args, "tlextra")
+ }
+ if optNoNewline {
+ fmt.Fprint(cmd.Stdout(), args)
+ } else {
+ fmt.Fprintln(cmd.Stdout(), args)
+ }
+ return nil
+}
+
+// runHello is another function for test commands.
+func runHello(cmd *Command, args []string) error {
+ if flagTopLevelExtra {
+ args = append(args, "tlextra")
+ }
+ fmt.Fprintln(cmd.Stdout(), strings.Join(append([]string{"Hello"}, args...), " "))
+ return nil
+}
+
+type testCase struct {
+ Args []string
+ Err error
+ Stdout string
+ Stderr string
+ GlobalFlag1 string
+ GlobalFlag2 int64
+}
+
+func init() {
+ os.Setenv("CMDLINE_WIDTH", "80") // make sure the formatting stays the same.
+ flag.StringVar(&globalFlag1, "global1", "", "global test flag 1")
+ globalFlag2 = flag.Int64("global2", 0, "global test flag 2")
+}
+
+func stripOutput(got string) string {
+ // The global flags include the flags from the testing package, so strip them
+ // out before the comparison.
+ re := regexp.MustCompile(" -test[^\n]+\n(?: [^\n]+\n)+")
+ return re.ReplaceAllLiteralString(got, "")
+}
+
+func runTestCases(t *testing.T, cmd *Command, tests []testCase) {
+ for _, test := range tests {
+ // Reset global variables before running each test case.
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ flagExtra = false
+ flagTopLevelExtra = false
+ optNoNewline = false
+ globalFlag1 = ""
+ *globalFlag2 = 0
+
+ // Run the execute function and check against expected results.
+ cmd.Init(nil, &stdout, &stderr)
+ if err := cmd.Execute(test.Args); err != test.Err {
+ t.Errorf("Ran with args %q\n GOT error:\n%q\nWANT error:\n%q", test.Args, err, test.Err)
+ }
+ if got, want := stripOutput(stdout.String()), test.Stdout; got != want {
+ t.Errorf("Ran with args %q\n GOT stdout:\n%q\nWANT stdout:\n%q", test.Args, got, want)
+ }
+ if got, want := stripOutput(stderr.String()), test.Stderr; got != want {
+ t.Errorf("Ran with args %q\n GOT stderr:\n%q\nWANT stderr:\n%q", test.Args, got, want)
+ }
+ if got, want := globalFlag1, test.GlobalFlag1; got != want {
+ t.Errorf("global1 flag got %q, want %q", got, want)
+ }
+ if got, want := *globalFlag2, test.GlobalFlag2; got != want {
+ t.Errorf("global2 flag got %q, want %q", got, want)
+ }
+ }
+}
+
+func TestNoCommands(t *testing.T) {
+ cmd := &Command{
+ Name: "nocmds",
+ Short: "Nocmds is invalid.",
+ Long: "Nocmds has no commands and no run function.",
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: nocmds: neither Children nor Run is specified
+
+Nocmds has no commands and no run function.
+
+Usage:
+ nocmds [ERROR: neither Children nor Run is specified]
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: nocmds: neither Children nor Run is specified
+
+Nocmds has no commands and no run function.
+
+Usage:
+ nocmds [ERROR: neither Children nor Run is specified]
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ }
+ runTestCases(t, cmd, tests)
+}
+
+func TestOneCommand(t *testing.T) {
+ cmdEcho := &Command{
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ Run: runEcho,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+
+ prog := &Command{
+ Name: "onecmd",
+ Short: "Onecmd program.",
+ Long: "Onecmd only has the echo command.",
+ Children: []*Command{cmdEcho},
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: onecmd: no command specified
+
+Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "onecmd help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: onecmd: unknown command "foo"
+
+Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "onecmd help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "onecmd help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "echo"},
+ Stdout: `Echo prints any strings passed in to stdout.
+
+Usage:
+ onecmd echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "help"},
+ Stdout: `Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ onecmd help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The onecmd help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "onecmd help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Onecmd Echo
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ onecmd echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Onecmd Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ onecmd help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The onecmd help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: onecmd: unknown command or topic "foo"
+
+Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "onecmd help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echo", "bad_arg"},
+ Err: ErrUsage,
+ Stderr: `ERROR: Invalid argument bad_arg
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ onecmd echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
+
+func TestMultiCommands(t *testing.T) {
+ cmdEcho := &Command{
+ Run: runEcho,
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+ var cmdEchoOpt = &Command{
+ Run: runEcho,
+ Name: "echoopt",
+ Short: "Print strings on stdout, with opts",
+ // Try varying number of header/trailer newlines around the long description.
+ Long: `Echoopt prints any args passed in to stdout.
+
+
+`,
+ ArgsName: "[args]",
+ ArgsLong: "[args] are arbitrary strings that will be echoed.",
+ }
+ cmdEchoOpt.Flags.BoolVar(&optNoNewline, "n", false, "Do not output trailing newline")
+
+ prog := &Command{
+ Name: "multi",
+ Short: "Multi test command",
+ Long: "Multi has two variants of echo.",
+ Children: []*Command{cmdEcho, cmdEchoOpt},
+ }
+ prog.Flags.BoolVar(&flagExtra, "extra", false, "Print an extra arg")
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: multi: no command specified
+
+Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands or topics
+Run "multi help [command]" for command usage.
+
+The multi flags are:
+ -extra=false
+ Print an extra arg
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands or topics
+Run "multi help [command]" for command usage.
+
+The multi flags are:
+ -extra=false
+ Print an extra arg
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands or topics
+Run "multi help [command]" for command usage.
+
+The multi flags are:
+ -extra=false
+ Print an extra arg
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Multi Echo
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ multi echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Multi Echoopt
+
+Echoopt prints any args passed in to stdout.
+
+Usage:
+ multi echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The multi echoopt flags are:
+ -n=false
+ Do not output trailing newline
+================================================================================
+Multi Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ multi help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The multi help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+`,
+ },
+ {
+ Args: []string{"help", "echo"},
+ Stdout: `Echo prints any strings passed in to stdout.
+
+Usage:
+ multi echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "echoopt"},
+ Stdout: `Echoopt prints any args passed in to stdout.
+
+Usage:
+ multi echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The multi echoopt flags are:
+ -n=false
+ Do not output trailing newline
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: multi: unknown command or topic "foo"
+
+Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands or topics
+Run "multi help [command]" for command usage.
+
+The multi flags are:
+ -extra=false
+ Print an extra arg
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"-extra", "echo", "foo", "bar"},
+ Stdout: "[foo bar extra]\n",
+ },
+ {
+ Args: []string{"echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echoopt", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"-extra", "echoopt", "foo", "bar"},
+ Stdout: "[foo bar extra]\n",
+ },
+ {
+ Args: []string{"echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar]",
+ },
+ {
+ Args: []string{"-extra", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar extra]",
+ },
+ {
+ Args: []string{"-global1=globalStringValue", "-extra", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar extra]",
+ GlobalFlag1: "globalStringValue",
+ },
+ {
+ Args: []string{"-global2=42", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar]",
+ GlobalFlag2: 42,
+ },
+ {
+ Args: []string{"-global1=globalStringOtherValue", "-global2=43", "-extra", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar extra]",
+ GlobalFlag1: "globalStringOtherValue",
+ GlobalFlag2: 43,
+ },
+ {
+ Args: []string{"echoopt", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echo", "-n", "foo", "bar"},
+ Err: ErrUsage,
+ Stderr: `ERROR: multi echo: flag provided but not defined: -n
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ multi echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"-nosuchflag", "echo", "foo", "bar"},
+ Err: ErrUsage,
+ Stderr: `ERROR: multi: flag provided but not defined: -nosuchflag
+
+Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands or topics
+Run "multi help [command]" for command usage.
+
+The multi flags are:
+ -extra=false
+ Print an extra arg
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
+
+func TestMultiLevelCommands(t *testing.T) {
+ cmdEcho := &Command{
+ Run: runEcho,
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+ cmdEchoOpt := &Command{
+ Run: runEcho,
+ Name: "echoopt",
+ Short: "Print strings on stdout, with opts",
+ // Try varying number of header/trailer newlines around the long description.
+ Long: `Echoopt prints any args passed in to stdout.
+
+
+`,
+ ArgsName: "[args]",
+ ArgsLong: "[args] are arbitrary strings that will be echoed.",
+ }
+ cmdEchoOpt.Flags.BoolVar(&optNoNewline, "n", false, "Do not output trailing newline")
+ cmdHello := &Command{
+ Run: runHello,
+ Name: "hello",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ }
+ echoProg := &Command{
+ Name: "echoprog",
+ Short: "Set of echo commands",
+ Long: "Echoprog has two variants of echo.",
+ Children: []*Command{cmdEcho, cmdEchoOpt},
+ Topics: []Topic{
+ {Name: "topic3", Short: "Help topic 3 short", Long: "Help topic 3 long."},
+ },
+ }
+ echoProg.Flags.BoolVar(&flagExtra, "extra", false, "Print an extra arg")
+ prog := &Command{
+ Name: "toplevelprog",
+ Short: "Top level prog",
+ Long: "Toplevelprog has the echo subprogram and the hello command.",
+ Children: []*Command{echoProg, cmdHello},
+ Topics: []Topic{
+ {Name: "topic1", Short: "Help topic 1 short", Long: "Help topic 1 long."},
+ {Name: "topic2", Short: "Help topic 2 short", Long: "Help topic 2 long."},
+ },
+ }
+ prog.Flags.BoolVar(&flagTopLevelExtra, "tlextra", false, "Print an extra arg for all commands")
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: toplevelprog: no command specified
+
+Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands or topics
+Run "toplevelprog help [command]" for command usage.
+
+The toplevelprog additional help topics are:
+ topic1 Help topic 1 short
+ topic2 Help topic 2 short
+Run "toplevelprog help [topic]" for topic details.
+
+The toplevelprog flags are:
+ -tlextra=false
+ Print an extra arg for all commands
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands or topics
+Run "toplevelprog help [command]" for command usage.
+
+The toplevelprog additional help topics are:
+ topic1 Help topic 1 short
+ topic2 Help topic 2 short
+Run "toplevelprog help [topic]" for topic details.
+
+The toplevelprog flags are:
+ -tlextra=false
+ Print an extra arg for all commands
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands or topics
+Run "toplevelprog help [command]" for command usage.
+
+The toplevelprog additional help topics are:
+ topic1 Help topic 1 short
+ topic2 Help topic 2 short
+Run "toplevelprog help [topic]" for topic details.
+
+The toplevelprog flags are:
+ -tlextra=false
+ Print an extra arg for all commands
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Toplevelprog Echoprog
+
+Echoprog has two variants of echo.
+
+Usage:
+ toplevelprog echoprog [flags] <command>
+
+The toplevelprog echoprog commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+
+The toplevelprog echoprog additional help topics are:
+ topic3 Help topic 3 short
+
+The toplevelprog echoprog flags are:
+ -extra=false
+ Print an extra arg
+================================================================================
+Toplevelprog Echoprog Echo
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Toplevelprog Echoprog Echoopt
+
+Echoopt prints any args passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The toplevelprog echoprog echoopt flags are:
+ -n=false
+ Do not output trailing newline
+================================================================================
+Toplevelprog Echoprog Topic3 - help topic
+
+Help topic 3 long.
+================================================================================
+Toplevelprog Hello
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ toplevelprog hello [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Toplevelprog Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ toplevelprog help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The toplevelprog help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+================================================================================
+Toplevelprog Topic1 - help topic
+
+Help topic 1 long.
+================================================================================
+Toplevelprog Topic2 - help topic
+
+Help topic 2 long.
+`,
+ },
+ {
+ Args: []string{"help", "echoprog"},
+ Stdout: `Echoprog has two variants of echo.
+
+Usage:
+ toplevelprog echoprog [flags] <command>
+
+The toplevelprog echoprog commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands or topics
+Run "toplevelprog echoprog help [command]" for command usage.
+
+The toplevelprog echoprog additional help topics are:
+ topic3 Help topic 3 short
+Run "toplevelprog echoprog help [topic]" for topic details.
+
+The toplevelprog echoprog flags are:
+ -extra=false
+ Print an extra arg
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "topic1"},
+ Stdout: `Help topic 1 long.
+`,
+ },
+ {
+ Args: []string{"help", "topic2"},
+ Stdout: `Help topic 2 long.
+`,
+ },
+ {
+ Args: []string{"echoprog", "help", "..."},
+ Stdout: `Echoprog has two variants of echo.
+
+Usage:
+ toplevelprog echoprog [flags] <command>
+
+The toplevelprog echoprog commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands or topics
+Run "toplevelprog echoprog help [command]" for command usage.
+
+The toplevelprog echoprog additional help topics are:
+ topic3 Help topic 3 short
+Run "toplevelprog echoprog help [topic]" for topic details.
+
+The toplevelprog echoprog flags are:
+ -extra=false
+ Print an extra arg
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Toplevelprog Echoprog Echo
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Toplevelprog Echoprog Echoopt
+
+Echoopt prints any args passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The toplevelprog echoprog echoopt flags are:
+ -n=false
+ Do not output trailing newline
+================================================================================
+Toplevelprog Echoprog Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ toplevelprog echoprog help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The toplevelprog echoprog help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+================================================================================
+Toplevelprog Echoprog Topic3 - help topic
+
+Help topic 3 long.
+`,
+ },
+ {
+ Args: []string{"echoprog", "help", "echoopt"},
+ Stdout: `Echoopt prints any args passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The toplevelprog echoprog echoopt flags are:
+ -n=false
+ Do not output trailing newline
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "echoprog", "topic3"},
+ Stdout: `Help topic 3 long.
+`,
+ },
+ {
+ Args: []string{"echoprog", "help", "topic3"},
+ Stdout: `Help topic 3 long.
+`,
+ },
+ {
+ Args: []string{"help", "hello"},
+ Stdout: `Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ toplevelprog hello [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: toplevelprog: unknown command or topic "foo"
+
+Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands or topics
+Run "toplevelprog help [command]" for command usage.
+
+The toplevelprog additional help topics are:
+ topic1 Help topic 1 short
+ topic2 Help topic 2 short
+Run "toplevelprog help [topic]" for topic details.
+
+The toplevelprog flags are:
+ -tlextra=false
+ Print an extra arg for all commands
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"echoprog", "echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echoprog", "-extra", "echo", "foo", "bar"},
+ Stdout: "[foo bar extra]\n",
+ },
+ {
+ Args: []string{"echoprog", "echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echoprog", "echoopt", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echoprog", "-extra", "echoopt", "foo", "bar"},
+ Stdout: "[foo bar extra]\n",
+ },
+ {
+ Args: []string{"echoprog", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar]",
+ },
+ {
+ Args: []string{"echoprog", "-extra", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar extra]",
+ },
+ {
+ Args: []string{"echoprog", "echoopt", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"--tlextra", "echoprog", "-extra", "echoopt", "foo", "bar"},
+ Stdout: "[foo bar extra tlextra]\n",
+ },
+ {
+ Args: []string{"hello", "foo", "bar"},
+ Stdout: "Hello foo bar\n",
+ },
+ {
+ Args: []string{"--tlextra", "hello", "foo", "bar"},
+ Stdout: "Hello foo bar tlextra\n",
+ },
+ {
+ Args: []string{"hello", "--extra", "foo", "bar"},
+ Err: ErrUsage,
+ Stderr: `ERROR: toplevelprog hello: flag provided but not defined: -extra
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ toplevelprog hello [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"-extra", "echoprog", "echoopt", "foo", "bar"},
+ Err: ErrUsage,
+ Stderr: `ERROR: toplevelprog: flag provided but not defined: -extra
+
+Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands or topics
+Run "toplevelprog help [command]" for command usage.
+
+The toplevelprog additional help topics are:
+ topic1 Help topic 1 short
+ topic2 Help topic 2 short
+Run "toplevelprog help [topic]" for topic details.
+
+The toplevelprog flags are:
+ -tlextra=false
+ Print an extra arg for all commands
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
+
+func TestMultiLevelCommandsOrdering(t *testing.T) {
+ cmdHello11 := &Command{
+ Name: "hello11",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello12 := &Command{
+ Name: "hello12",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello21 := &Command{
+ Name: "hello21",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello22 := &Command{
+ Name: "hello22",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello31 := &Command{
+ Name: "hello31",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello32 := &Command{
+ Name: "hello32",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ progHello3 := &Command{
+ Name: "prog3",
+ Short: "Set of hello commands",
+ Long: "Prog3 has two variants of hello.",
+ Children: []*Command{cmdHello31, cmdHello32},
+ }
+ progHello2 := &Command{
+ Name: "prog2",
+ Short: "Set of hello commands",
+ Long: "Prog2 has two variants of hello and a subprogram prog3.",
+ Children: []*Command{cmdHello21, progHello3, cmdHello22},
+ }
+ progHello1 := &Command{
+ Name: "prog1",
+ Short: "Set of hello commands",
+ Long: "Prog1 has two variants of hello and a subprogram prog2.",
+ Children: []*Command{cmdHello11, cmdHello12, progHello2},
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: prog1: no command specified
+
+Prog1 has two variants of hello and a subprogram prog2.
+
+Usage:
+ prog1 <command>
+
+The prog1 commands are:
+ hello11 Print strings on stdout preceded by "Hello"
+ hello12 Print strings on stdout preceded by "Hello"
+ prog2 Set of hello commands
+ help Display help for commands or topics
+Run "prog1 help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Prog1 has two variants of hello and a subprogram prog2.
+
+Usage:
+ prog1 <command>
+
+The prog1 commands are:
+ hello11 Print strings on stdout preceded by "Hello"
+ hello12 Print strings on stdout preceded by "Hello"
+ prog2 Set of hello commands
+ help Display help for commands or topics
+Run "prog1 help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Prog1 has two variants of hello and a subprogram prog2.
+
+Usage:
+ prog1 <command>
+
+The prog1 commands are:
+ hello11 Print strings on stdout preceded by "Hello"
+ hello12 Print strings on stdout preceded by "Hello"
+ prog2 Set of hello commands
+ help Display help for commands or topics
+Run "prog1 help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Prog1 Hello11
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 hello11 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Hello12
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 hello12 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2
+
+Prog2 has two variants of hello and a subprogram prog3.
+
+Usage:
+ prog1 prog2 <command>
+
+The prog1 prog2 commands are:
+ hello21 Print strings on stdout preceded by "Hello"
+ prog3 Set of hello commands
+ hello22 Print strings on stdout preceded by "Hello"
+================================================================================
+Prog1 Prog2 Hello21
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello21 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Prog3
+
+Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog1 prog2 prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+================================================================================
+Prog1 Prog2 Prog3 Hello31
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Prog3 Hello32
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Hello22
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello22 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ prog1 help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The prog1 help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+`,
+ },
+ {
+ Args: []string{"prog2", "help", "..."},
+ Stdout: `Prog2 has two variants of hello and a subprogram prog3.
+
+Usage:
+ prog1 prog2 <command>
+
+The prog1 prog2 commands are:
+ hello21 Print strings on stdout preceded by "Hello"
+ prog3 Set of hello commands
+ hello22 Print strings on stdout preceded by "Hello"
+ help Display help for commands or topics
+Run "prog1 prog2 help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Prog1 Prog2 Hello21
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello21 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Prog3
+
+Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog1 prog2 prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+================================================================================
+Prog1 Prog2 Prog3 Hello31
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Prog3 Hello32
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Hello22
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello22 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ prog1 prog2 help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The prog1 prog2 help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+`,
+ },
+ {
+ Args: []string{"prog2", "prog3", "help", "..."},
+ Stdout: `Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog1 prog2 prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+ help Display help for commands or topics
+Run "prog1 prog2 prog3 help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Prog1 Prog2 Prog3 Hello31
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Prog3 Hello32
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Prog3 Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ prog1 prog2 prog3 help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The prog1 prog2 prog3 help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+`,
+ },
+ {
+ Args: []string{"help", "prog2", "prog3", "..."},
+ Stdout: `Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog1 prog2 prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+ help Display help for commands or topics
+Run "prog1 prog2 prog3 help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Prog1 Prog2 Prog3 Hello31
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Prog3 Hello32
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog1 Prog2 Prog3 Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ prog1 prog2 prog3 help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The prog1 prog2 prog3 help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+`,
+ },
+ {
+ Args: []string{"help", "-style=godoc", "..."},
+ Stdout: `Prog1 has two variants of hello and a subprogram prog2.
+
+Usage:
+ prog1 <command>
+
+The prog1 commands are:
+ hello11 Print strings on stdout preceded by "Hello"
+ hello12 Print strings on stdout preceded by "Hello"
+ prog2 Set of hello commands
+ help Display help for commands or topics
+Run "prog1 help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+
+Prog1 Hello11
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 hello11 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Hello12
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 hello12 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Prog2
+
+Prog2 has two variants of hello and a subprogram prog3.
+
+Usage:
+ prog1 prog2 <command>
+
+The prog1 prog2 commands are:
+ hello21 Print strings on stdout preceded by "Hello"
+ prog3 Set of hello commands
+ hello22 Print strings on stdout preceded by "Hello"
+
+Prog1 Prog2 Hello21
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello21 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Prog2 Prog3
+
+Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog1 prog2 prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+
+Prog1 Prog2 Prog3 Hello31
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Prog2 Prog3 Hello32
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Prog2 Hello22
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello22 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ prog1 help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The prog1 help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+`,
+ },
+ }
+
+ runTestCases(t, progHello1, tests)
+}
+
+func TestCommandAndArgs(t *testing.T) {
+ cmdEcho := &Command{
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ Run: runEcho,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+
+ prog := &Command{
+ Name: "cmdargs",
+ Short: "Cmdargs program.",
+ Long: "Cmdargs has the echo command and a Run function with args.",
+ Children: []*Command{cmdEcho},
+ Run: runHello,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Stdout: "Hello\n",
+ },
+ {
+ Args: []string{"foo"},
+ Stdout: "Hello foo\n",
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Cmdargs has the echo command and a Run function with args.
+
+Usage:
+ cmdargs <command>
+ cmdargs [strings]
+
+The cmdargs commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "cmdargs help [command]" for command usage.
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "echo"},
+ Stdout: `Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdargs echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Cmdargs has the echo command and a Run function with args.
+
+Usage:
+ cmdargs <command>
+ cmdargs [strings]
+
+The cmdargs commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "cmdargs help [command]" for command usage.
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Cmdargs Echo
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdargs echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Cmdargs Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ cmdargs help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The cmdargs help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: cmdargs: unknown command or topic "foo"
+
+Cmdargs has the echo command and a Run function with args.
+
+Usage:
+ cmdargs <command>
+ cmdargs [strings]
+
+The cmdargs commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "cmdargs help [command]" for command usage.
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echo", "bad_arg"},
+ Err: ErrUsage,
+ Stderr: `ERROR: Invalid argument bad_arg
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdargs echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
+
+func TestCommandAndRunNoArgs(t *testing.T) {
+ cmdEcho := &Command{
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ Run: runEcho,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+
+ prog := &Command{
+ Name: "cmdrun",
+ Short: "Cmdrun program.",
+ Long: "Cmdrun has the echo command and a Run function with no args.",
+ Children: []*Command{cmdEcho},
+ Run: runHello,
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Stdout: "Hello\n",
+ },
+ {
+ Args: []string{"foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: cmdrun: unknown command "foo"
+
+Cmdrun has the echo command and a Run function with no args.
+
+Usage:
+ cmdrun <command>
+ cmdrun
+
+The cmdrun commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "cmdrun help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Cmdrun has the echo command and a Run function with no args.
+
+Usage:
+ cmdrun <command>
+ cmdrun
+
+The cmdrun commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "cmdrun help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "echo"},
+ Stdout: `Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdrun echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Cmdrun has the echo command and a Run function with no args.
+
+Usage:
+ cmdrun <command>
+ cmdrun
+
+The cmdrun commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "cmdrun help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+================================================================================
+Cmdrun Echo
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdrun echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Cmdrun Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes. The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars. By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+ cmdrun help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The cmdrun help flags are:
+ -style=text
+ The formatting style for help output, either "text" or "godoc".
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: cmdrun: unknown command or topic "foo"
+
+Cmdrun has the echo command and a Run function with no args.
+
+Usage:
+ cmdrun <command>
+ cmdrun
+
+The cmdrun commands are:
+ echo Print strings on stdout
+ help Display help for commands or topics
+Run "cmdrun help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echo", "bad_arg"},
+ Err: ErrUsage,
+ Stderr: `ERROR: Invalid argument bad_arg
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdrun echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
+
+func TestLongCommandsHelp(t *testing.T) {
+ cmdLong := &Command{
+ Name: "thisisaverylongcommand",
+ Short: "the short description of the very long command is very long, and will have to be wrapped",
+ Long: "The long description of the very long command is also very long, and will similarly have to be wrapped",
+ Run: runEcho,
+ }
+ cmdShort := &Command{
+ Name: "x",
+ Short: "description of short command.",
+ Long: "blah blah blah",
+ Run: runEcho,
+ }
+ prog := &Command{
+ Name: "program",
+ Short: "Test help strings when there are long commands.",
+ Long: "Test help strings when there are long commands.",
+ Children: []*Command{cmdShort, cmdLong},
+ }
+ var tests = []testCase{
+ {
+ Args: []string{"help"},
+ Stdout: `Test help strings when there are long commands.
+
+Usage:
+ program <command>
+
+The program commands are:
+ x description of short command.
+ thisisaverylongcommand the short description of the very long command is very
+ long, and will have to be wrapped
+ help Display help for commands or topics
+Run "program help [command]" for command usage.
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "thisisaverylongcommand"},
+ Stdout: `The long description of the very long command is also very long, and will
+similarly have to be wrapped
+
+Usage:
+ program thisisaverylongcommand
+
+The global flags are:
+ -global1=
+ global test flag 1
+ -global2=0
+ global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
diff --git a/cmdline/testdata/gendoc.go b/cmdline/testdata/gendoc.go
new file mode 100644
index 0000000..02ecc5c
--- /dev/null
+++ b/cmdline/testdata/gendoc.go
@@ -0,0 +1,83 @@
+// Command gendoc can be used for generating detailed godoc comments
+// for cmdline-based tools. The user specifies the cmdline-based tool
+// source file directory <dir> using the first command-line argument
+// and gendoc executes the tool with flags that generate detailed
+// godoc comment and output it to <dir>/doc.go. If more than one
+// command-line argument is provided, they are passed through to the
+// tool the gendoc executes.
+//
+// NOTE: The reason this command is located in under a testdata
+// directory is to enforce its idiomatic use through "go run
+// <path>/testdata/gendoc.go <dir> [args]".
+//
+// NOTE: The gendoc command itself is not based on the cmdline library
+// to avoid non-trivial bootstrapping. In particular, if the
+// compilation of gendoc requires GOPATH to contain the vanadium Go
+// workspaces, then running the gendoc command requires the v23 tool,
+// which in turn my depend on the gendoc command.
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+)
+
+func main() {
+ if err := generate(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+func generate() error {
+ if got, want := len(os.Args[1:]), 1; got < want {
+ return fmt.Errorf("gendoc requires at least one argument\nusage: gendoc <dir> [args]")
+ }
+ pkg := os.Args[1]
+
+ // Build the gendoc binary in a temporary folder.
+ tmpDir, err := ioutil.TempDir("", "")
+ if err != nil {
+ return fmt.Errorf("TempDir() failed: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+ gendocBin := filepath.Join(tmpDir, "gendoc")
+ args := []string{"go", "build", "-o", gendocBin}
+ args = append(args, pkg)
+ buildCmd := exec.Command("v23", args...)
+ if err := buildCmd.Run(); err != nil {
+ return fmt.Errorf("%q failed: %v\n", strings.Join(buildCmd.Args, " "), err)
+ }
+
+ // Use it to generate the documentation.
+ var out bytes.Buffer
+ if len(os.Args) == 2 {
+ args = []string{"help", "-style=godoc", "..."}
+ } else {
+ args = os.Args[2:]
+ }
+ runCmd := exec.Command(gendocBin, args...)
+ runCmd.Stdout = &out
+ if err := runCmd.Run(); err != nil {
+ return fmt.Errorf("%q failed: %v\n%v\n", strings.Join(runCmd.Args, " "), err)
+ }
+ doc := fmt.Sprintf(`// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+%s*/
+package main
+`, out.String())
+
+ // Write the result to doc.go.
+ path, perm := filepath.Join(pkg, "doc.go"), os.FileMode(0644)
+ if err := ioutil.WriteFile(path, []byte(doc), perm); err != nil {
+ return fmt.Errorf("WriteFile(%v, %v) failed: %v\n", path, perm, err)
+ }
+ return nil
+}
diff --git a/dbutil/mysql.go b/dbutil/mysql.go
new file mode 100644
index 0000000..a1e4eee
--- /dev/null
+++ b/dbutil/mysql.go
@@ -0,0 +1,246 @@
+// Utility functions for opening and configuring a connection to a MySQL-like
+// database, with optional TLS support.
+//
+// Functions in this file are not thread-safe. However, the returned *sql.DB is.
+// Sane defaults are assumed: utf8mb4 encoding, UTC timezone, parsing date/time
+// into time.Time.
+
+package dbutil
+
+import (
+ "crypto/sha256"
+ "crypto/tls"
+ "crypto/x509"
+ "database/sql"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/url"
+ "path/filepath"
+ "strings"
+
+ "github.com/go-sql-driver/mysql"
+)
+
+// SQL statement suffix to be appended when creating tables.
+const SqlCreateTableSuffix = "CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"
+
+// Description of the SQL configuration file format.
+const SqlConfigFileDescription = `File must contain a JSON object of the following form:
+ {
+ "dataSourceName": "[username[:password]@][protocol[(address)]]/dbname", (the connection string required by go-sql-driver; database name must be specified, query parameters are not supported)
+ "tlsDisable": "false|true", (defaults to false; if set to true, uses an unencrypted connection; otherwise, the following fields are mandatory)
+ "tlsServerName": "serverName", (the domain name of the SQL server for TLS)
+ "rootCertPath": "[/]path/server-ca.pem", (the root certificate of the SQL server for TLS)
+ "clientCertPath": "[/]path/client-cert.pem", (the client certificate for TLS)
+ "clientKeyPath": "[/]path/client-key.pem" (the client private key for TLS)
+ }
+Paths must be either absolute or relative to the configuration file directory.`
+
+// SqlConfig holds the fields needed to connect to a SQL instance and to
+// configure TLS encryption of the information sent over the wire. It must be
+// activated via Activate() before use.
+type SqlConfig struct {
+ // DataSourceName is the connection string as required by go-sql-driver:
+ // "[username[:password]@][protocol[(address)]]/dbname";
+ // database name must be specified, query parameters are not supported.
+ DataSourceName string `json:"dataSourceName"`
+ // TLSDisable, if set to true, uses an unencrypted connection;
+ // otherwise, the following fields are mandatory.
+ TLSDisable bool `json:"tlsDisable"`
+ // TLSServerName is the domain name of the SQL server for TLS.
+ TLSServerName string `json:"tlsServerName"`
+ // RootCertPath is the root certificate of the SQL server for TLS.
+ RootCertPath string `json:"rootCertPath"`
+ // ClientCertPath is the client certificate for TLS.
+ ClientCertPath string `json:"clientCertPath"`
+ // ClientKeyPath is the client private key for TLS.
+ ClientKeyPath string `json:"clientKeyPath"`
+}
+
+// ActiveSqlConfig represents a SQL configuration that has been activated
+// by registering the TLS configuration (if applicable). It can be used for
+// opening SQL database connections.
+type ActiveSqlConfig struct {
+ // cfg is a copy of the SqlConfig that was activated.
+ cfg *SqlConfig
+ // tlsConfigIdentifier is the identifier under which the TLS configuration
+ // is registered with go-sql-driver. It is computed as a secure hash of the
+ // SqlConfig after resolving any relative paths.
+ tlsConfigIdentifier string `json:"-"`
+}
+
+// Parses the SQL configuration file pointed to by sqlConfigFile (format
+// described in SqlConfigFileDescription; also see links below).
+// https://github.com/go-sql-driver/mysql/#dsn-data-source-name
+// https://github.com/go-sql-driver/mysql/#tls
+func ParseSqlConfigFromFile(sqlConfigFile string) (*SqlConfig, error) {
+ configJSON, err := ioutil.ReadFile(sqlConfigFile)
+ if err != nil {
+ return nil, fmt.Errorf("failed reading SQL config file %q: %v", sqlConfigFile, err)
+ }
+ var config SqlConfig
+ if err = json.Unmarshal(configJSON, &config); err != nil {
+ // TODO(ivanpi): Parsing errors might leak the SQL password into error
+ // logs, depending on standard library implementation.
+ return nil, fmt.Errorf("failed parsing SQL config file %q: %v", sqlConfigFile, err)
+ }
+ return &config, nil
+}
+
+// Activates the SQL configuration by registering the TLS configuration with
+// go-mysql-driver (if TLSDisable is not set).
+// Certificate paths from SqlConfig that aren't absolute are interpreted relative
+// to certBaseDir.
+// For more information see https://github.com/go-sql-driver/mysql/#tls
+func (sc *SqlConfig) Activate(certBaseDir string) (*ActiveSqlConfig, error) {
+ if sc.TLSDisable {
+ return &ActiveSqlConfig{
+ cfg: sc.normalizePaths(""),
+ }, nil
+ }
+ cbdAbs, err := filepath.Abs(certBaseDir)
+ if err != nil {
+ return nil, fmt.Errorf("failed resolving certificate base directory %q: %v", certBaseDir, err)
+ }
+ scn := sc.normalizePaths(cbdAbs)
+ configId := scn.hash()
+ if err = registerSqlTLSConfig(scn, configId); err != nil {
+ return nil, fmt.Errorf("failed registering TLS config: %v", err)
+ }
+ return &ActiveSqlConfig{
+ cfg: scn,
+ tlsConfigIdentifier: configId,
+ }, nil
+}
+
+// Convenience function to parse and activate the SQL configuration file.
+// Certificate paths that aren't absolute are interpreted relative to the
+// directory containing sqlConfigFile.
+func ActivateSqlConfigFromFile(sqlConfigFile string) (*ActiveSqlConfig, error) {
+ cfg, err := ParseSqlConfigFromFile(sqlConfigFile)
+ if err != nil {
+ return nil, err
+ }
+ activeCfg, err := cfg.Activate(filepath.Dir(sqlConfigFile))
+ if err != nil {
+ return nil, fmt.Errorf("failed activating SQL config from file %q: %v", sqlConfigFile, err)
+ }
+ return activeCfg, nil
+}
+
+// Opens a connection to the SQL database using the provided configuration.
+// Sets the specified transaction isolation (see link below).
+// https://dev.mysql.com/doc/refman/5.5/en/server-system-variables.html#sysvar_tx_isolation
+func (sqlConfig *ActiveSqlConfig) NewSqlDBConn(txIsolation string) (*sql.DB, error) {
+ return openSqlDBConn(configureSqlDBConn(sqlConfig, txIsolation))
+}
+
+// Convenience function to parse and activate the configuration file and open
+// a connection to the SQL database. If multiple connections with the same
+// configuration are needed, a single ActivateSqlConfigFromFile() and multiple
+// NewSqlDbConn() calls are recommended instead.
+func NewSqlDBConnFromFile(sqlConfigFile, txIsolation string) (*sql.DB, error) {
+ config, err := ActivateSqlConfigFromFile(sqlConfigFile)
+ if err != nil {
+ return nil, err
+ }
+ return config.NewSqlDBConn(txIsolation)
+}
+
+func configureSqlDBConn(sqlConfig *ActiveSqlConfig, txIsolation string) string {
+ params := url.Values{}
+ // Setting charset is unneccessary when collation is set, according to
+ // https://github.com/go-sql-driver/mysql/#charset
+ params.Set("collation", "utf8mb4_general_ci")
+ // Maps SQL date/time values into time.Time instead of strings.
+ params.Set("parseTime", "true")
+ params.Set("loc", "UTC")
+ params.Set("time_zone", "'+00:00'")
+ if !sqlConfig.cfg.TLSDisable {
+ params.Set("tls", sqlConfig.tlsConfigIdentifier)
+ }
+ params.Set("tx_isolation", "'"+txIsolation+"'")
+ return sqlConfig.cfg.DataSourceName + "?" + params.Encode()
+}
+
+func openSqlDBConn(dataSrcName string) (*sql.DB, error) {
+ // Prevent leaking the SQL password into error logs.
+ sanitizedDSN := dataSrcName[strings.LastIndex(dataSrcName, "@")+1:]
+ db, err := sql.Open("mysql", dataSrcName)
+ if err != nil {
+ return nil, fmt.Errorf("failed opening database connection at %q: %v", sanitizedDSN, err)
+ }
+ if err := db.Ping(); err != nil {
+ return nil, fmt.Errorf("failed connecting to database at %q: %v", sanitizedDSN, err)
+ }
+ return db, nil
+}
+
+// registerSqlTLSConfig sets up the SQL connection to use TLS encryption
+// and registers the configuration under configId.
+// For more information see https://github.com/go-sql-driver/mysql/#tls
+func registerSqlTLSConfig(cfg *SqlConfig, configId string) error {
+ rootCertPool := x509.NewCertPool()
+ pem, err := ioutil.ReadFile(cfg.RootCertPath)
+ if err != nil {
+ return fmt.Errorf("failed reading root certificate: %v", err)
+ }
+ if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
+ return fmt.Errorf("failed to append PEM to cert pool")
+ }
+ ckpair, err := tls.LoadX509KeyPair(cfg.ClientCertPath, cfg.ClientKeyPath)
+ if err != nil {
+ return fmt.Errorf("failed loading client key pair: %v", err)
+ }
+ clientCert := []tls.Certificate{ckpair}
+ return mysql.RegisterTLSConfig(configId, &tls.Config{
+ RootCAs: rootCertPool,
+ Certificates: clientCert,
+ ServerName: cfg.TLSServerName,
+ // SSLv3 is more vulnerable than TLSv1.0, see https://en.wikipedia.org/wiki/POODLE
+ // TODO(ivanpi): Increase when Cloud SQL starts supporting higher TLS versions.
+ MinVersion: tls.VersionTLS10,
+ ClientAuth: tls.RequireAndVerifyClientCert,
+ })
+}
+
+// Computes a secure hash of the SqlConfig. Paths are canonicalized before
+// hashing.
+func (sc *SqlConfig) hash() string {
+ scn := sc.normalizePaths("")
+ fieldsToHash := []interface{}{
+ scn.DataSourceName, scn.TLSDisable, scn.TLSServerName,
+ scn.RootCertPath, scn.ClientCertPath, scn.ClientKeyPath,
+ }
+ hashAcc := make([]byte, 0, len(fieldsToHash)*sha256.Size)
+ for _, field := range fieldsToHash {
+ fieldHash := sha256.Sum256([]byte(fmt.Sprintf("%v", field)))
+ hashAcc = append(hashAcc, fieldHash[:]...)
+ }
+ structHash := sha256.Sum256(hashAcc)
+ return hex.EncodeToString(structHash[:])
+}
+
+// Returns a copy of the SqlConfig with certificate paths canonicalized and
+// resolved relative to certBaseDir. Blank paths remain blank.
+func (sc *SqlConfig) normalizePaths(certBaseDir string) *SqlConfig {
+ scn := *sc
+ for _, path := range []*string{&scn.RootCertPath, &scn.ClientCertPath, &scn.ClientKeyPath} {
+ *path = normalizePath(*path, certBaseDir)
+ }
+ return &scn
+}
+
+// If path is not absolute, resolves path relative to baseDir. Otherwise,
+// canonicalizes path. Blank paths are not resolved or canonicalized.
+func normalizePath(path, baseDir string) string {
+ if strings.TrimSpace(path) == "" {
+ return ""
+ }
+ if filepath.IsAbs(path) {
+ return filepath.Clean(path)
+ }
+ return filepath.Join(baseDir, path)
+}
diff --git a/netstate/isgloballyroutable.go b/netstate/isgloballyroutable.go
new file mode 100644
index 0000000..ee6bae5
--- /dev/null
+++ b/netstate/isgloballyroutable.go
@@ -0,0 +1,29 @@
+package netstate
+
+import (
+ "net"
+)
+
+var privateCIDRs = []net.IPNet{
+ net.IPNet{IP: net.IPv4(10, 0, 0, 0), Mask: net.IPv4Mask(0xff, 0, 0, 0)},
+ net.IPNet{IP: net.IPv4(172, 16, 0, 0), Mask: net.IPv4Mask(0xff, 0xf0, 0, 0)},
+ net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(0xff, 0xff, 0, 0)},
+}
+
+// IsGloballyRoutable returns true if the argument is a globally routable IP address.
+func IsGloballyRoutableIP(ip net.IP) bool {
+ if !ip.IsGlobalUnicast() {
+ return false
+ }
+ if ip4 := ip.To4(); ip4 != nil {
+ for _, cidr := range privateCIDRs {
+ if cidr.Contains(ip4) {
+ return false
+ }
+ }
+ if ip4.Equal(net.IPv4bcast) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/netstate/isgloballyroutable_test.go b/netstate/isgloballyroutable_test.go
new file mode 100644
index 0000000..dd0885a
--- /dev/null
+++ b/netstate/isgloballyroutable_test.go
@@ -0,0 +1,31 @@
+package netstate
+
+import (
+ "net"
+ "testing"
+)
+
+func TestIsGloballyRoutable(t *testing.T) {
+ tests := []struct {
+ ip string
+ want bool
+ }{
+ {"192.168.1.1", false},
+ {"192.169.0.3", true},
+ {"10.1.1.1", false},
+ {"172.17.100.255", false},
+ {"172.32.0.1", true},
+ {"255.255.255.255", false},
+ {"127.0.0.1", false},
+ {"224.0.0.1", false},
+ {"FF02::FB", false},
+ {"fe80::be30:5bff:fed3:843f", false},
+ {"2620:0:1000:8400:be30:5bff:fed3:843f", true},
+ }
+ for _, test := range tests {
+ ip := net.ParseIP(test.ip)
+ if got := IsGloballyRoutableIP(ip); got != test.want {
+ t.Fatalf("%s: want %v got %v", test.ip, test.want, got)
+ }
+ }
+}
diff --git a/netstate/netstate.go b/netstate/netstate.go
new file mode 100644
index 0000000..623e7b2
--- /dev/null
+++ b/netstate/netstate.go
@@ -0,0 +1,398 @@
+// Package netstate provides routines to obtain the available set of
+// of network addresess, for determining changes to those addresses and for
+// selecting from amongst them according to some set of policies that are
+// implemented by applying simple predicates (functions with names of the form
+// Is<condition>) to filter or find the first matching address from a list
+// of addresses. The intent is to make it easy to create policies that do
+// things like 'find the first IPv4 unicast address that is globally routable,
+// failing that use a private IPv4 address, and failing that, an IPv6 address'.
+//
+// A simple usage would be:
+//
+// state, _ := netstate.GetAccessibleIPs()
+// ipv4 := state.Filter(netstate.IsPublicUnicastIPv4)
+// // ipv4 will contain all of the public IPv4 addresses, if any.
+//
+// The example policy described above would be implemented using a
+// series of calls to Filter with appropriate predicates.
+//
+// In some cases, it may be necessary to take IP routing information
+// into account and hence interface hosting the address. The interface
+// hosting each address is provided in the AddrIfc structure used to represent
+// addresses and the IP routing information is provided by the GetAccessibleIPs
+// function which will typically be used to obtain the available IP addresses.
+//
+// Although most commercial networking hardware supports IPv6, some consumer
+// devices and more importantly many ISPs do not, so routines are provided
+// to allow developers to easily distinguish between the two and to use
+// whichever is appropriate for their product/situation.
+//
+// The term 'accessible' is used to refer to any non-loopback IP address.
+// The term 'public' is used to refer to any globally routable IP address.
+//
+// All IPv6 addresses are intended to be 'public', but any starting with
+// fc00::/7 (RFC4193) are reserved for private use, but the go
+// net libraries do not appear to recognise this. Similarly fe80::/10
+// (RFC 4291) are reserved for 'site-local' usage, but again this is not
+// implemented in the go libraries. Any developer who needs to distinguish
+// these cases will need to write their own routines to test for them.
+//
+// When using the go net package it is important to remember that IPv6
+// addresses subsume IPv4 and hence in many cases the same internal
+// representation is used for both, thus testing for the length of the IP
+// address is unreliable. The reliable test is to use the net.To4() which
+// will return a non-nil result if can be used as an IPv4 one. Any address
+// can be used as an IPv6 and hence the only reliable way to test for an IPv6
+// address that is not an IPv4 one also is for the To4 call to return nil for
+// it.
+package netstate
+
+import (
+ "fmt"
+ "net"
+ "strings"
+
+ "v.io/v23/ipc"
+
+ "v.io/x/ref/lib/netconfig"
+)
+
+// AddrIfc represents a network address and the network interface that
+// hosts it.
+type AddrIfc struct {
+ // Network address
+ Addr net.Addr
+
+ // The name of the network interface this address is hosted on, empty
+ // if this information is not available.
+ Name string
+
+ // The IPRoutes of the network interface this address is hosted on,
+ // nil if this information is not available.
+ IPRoutes []*netconfig.IPRoute
+}
+
+func (a *AddrIfc) String() string {
+ if a.IPRoutes != nil {
+ r := fmt.Sprintf("%s: %s[", a.Addr, a.Name)
+ for _, rt := range a.IPRoutes {
+ src := ""
+ if rt.PreferredSource != nil {
+ src = ", src: " + rt.PreferredSource.String()
+ }
+ r += fmt.Sprintf("{%d: net: %s, gw: %s%s}, ", rt.IfcIndex, rt.Net, rt.Gateway, src)
+ }
+ r = strings.TrimSuffix(r, ", ")
+ r += "]"
+ return r
+ }
+ return a.Addr.String()
+}
+
+func (a *AddrIfc) Address() net.Addr {
+ return a.Addr
+}
+
+func (a *AddrIfc) InterfaceIndex() int {
+ if len(a.IPRoutes) == 0 {
+ return -1
+ }
+ return a.IPRoutes[0].IfcIndex
+}
+
+func (a *AddrIfc) InterfaceName() string {
+ return a.Name
+}
+
+func (a *AddrIfc) Networks() []net.Addr {
+ nets := []net.Addr{}
+ for _, r := range a.IPRoutes {
+ nets = append(nets, &r.Net)
+ }
+ return nets
+}
+
+type AddrList []ipc.Address
+
+func (al AddrList) String() string {
+ r := ""
+ for _, v := range al {
+ r += fmt.Sprintf("(%s) ", v)
+ }
+ return strings.TrimRight(r, " ")
+}
+
+// GetAll gets all of the available addresses on the device, including
+// loopback addresses, non-IP protocols etc.
+func GetAll() (AddrList, error) {
+ interfaces, err := net.Interfaces()
+ if err != nil {
+ return nil, err
+ }
+ routes := netconfig.GetIPRoutes(false)
+ routeTable := make(map[int][]*netconfig.IPRoute)
+ for _, r := range routes {
+ routeTable[r.IfcIndex] = append(routeTable[r.IfcIndex], r)
+ }
+ var all AddrList
+ for _, ifc := range interfaces {
+ addrs, err := ifc.Addrs()
+ if err != nil {
+ continue
+ }
+ for _, a := range addrs {
+ all = append(all, &AddrIfc{a, ifc.Name, routeTable[ifc.Index]})
+ }
+ }
+ return all, nil
+}
+
+// GetAccessibleIPs returns all of the accessible IP addresses on the device
+// - i.e. excluding loopback and unspecified addresses.
+// The IP addresses returned will be host addresses.
+func GetAccessibleIPs() (AddrList, error) {
+ all, err := GetAll()
+ if err != nil {
+ return nil, err
+ }
+ return all.Map(ConvertAccessibleIPHost), nil
+}
+
+// AddressPredicate defines the function signature for predicate functions
+// to be used with AddrList
+type AddressPredicate func(a ipc.Address) bool
+
+// Filter returns all of the addresses for which the predicate
+// function is true.
+func (al AddrList) Filter(predicate AddressPredicate) AddrList {
+ r := AddrList{}
+ for _, a := range al {
+ if predicate(a) {
+ r = append(r, a)
+ }
+ }
+ return r
+}
+
+type Mapper func(a ipc.Address) ipc.Address
+
+// Map will apply the Mapper function to all of the items in its receiver
+// and return a new AddrList containing all of the non-nil results from
+// said calls.
+func (al AddrList) Map(mapper Mapper) AddrList {
+ var ral AddrList
+ for _, a := range al {
+ if na := mapper(a); na != nil {
+ ral = append(ral, na)
+ }
+ }
+ return ral
+}
+
+// ConvertToIPHost converts the network address component of an ipc.Address into
+// an instance with a net.Addr that contains an IP host address (as opposed to a
+// network CIDR for example).
+func ConvertToIPHost(a ipc.Address) ipc.Address {
+ aifc, ok := a.(*AddrIfc)
+ if !ok {
+ return nil
+ }
+ aifc.Addr = AsIPAddr(aifc.Addr)
+ return aifc
+}
+
+// ConvertAccessibleIPHost converts the network address component of an ipc.Address
+// into an instance with a net.Addr that contains an IP host address (as opposed to a
+// network CIDR for example) with filtering out a loopback or non-accessible IPs.
+func ConvertAccessibleIPHost(a ipc.Address) ipc.Address {
+ if !IsAccessibleIP(a) {
+ return nil
+ }
+ aifc, ok := a.(*AddrIfc)
+ if !ok {
+ return nil
+ }
+ if ip := AsIPAddr(aifc.Addr); ip != nil {
+ aifc.Addr = ip
+ }
+ return aifc
+}
+
+// IsIPProtocol returns true if its parameter is one of the allowed
+// network/protocol values for IP.
+func IsIPProtocol(n string) bool {
+ // Removed the training IP version number.
+ n = strings.TrimRightFunc(n, func(r rune) bool { return r == '4' || r == '6' })
+ switch n {
+ case "ip+net", "ip", "tcp", "udp", "ws", "wsh":
+ return true
+ default:
+ return false
+ }
+}
+
+// AsIPAddr returns its argument as a net.IPAddr if that's possible.
+func AsIPAddr(a net.Addr) *net.IPAddr {
+ if v, ok := a.(*net.IPAddr); ok {
+ return v
+ }
+ if ipn, ok := a.(*net.IPNet); ok {
+ return &net.IPAddr{IP: ipn.IP}
+ }
+ if IsIPProtocol(a.Network()) {
+ if r := net.ParseIP(a.String()); r != nil {
+ return &net.IPAddr{IP: r}
+ }
+ }
+ return nil
+}
+
+// AsIP returns its argument as a net.IP if that's possible.
+func AsIP(a net.Addr) net.IP {
+ ipAddr := AsIPAddr(a)
+ if ipAddr == nil {
+ return nil
+ }
+ return ipAddr.IP
+}
+
+// IsUnspecified returns true if its argument is an unspecified IP address
+func IsUnspecifiedIP(a ipc.Address) bool {
+ if ip := AsIP(a.Address()); ip != nil {
+ return ip.IsUnspecified()
+ }
+ return false
+}
+
+// IsLoopback returns true if its argument is a loopback IP address
+func IsLoopbackIP(a ipc.Address) bool {
+ if ip := AsIP(a.Address()); ip != nil && !ip.IsUnspecified() {
+ return ip.IsLoopback()
+ }
+ return false
+}
+
+// IsAccessible returns true if its argument is an accessible (non-loopback)
+// IP address.
+func IsAccessibleIP(a ipc.Address) bool {
+ if ip := AsIP(a.Address()); ip != nil && !ip.IsUnspecified() {
+ return !ip.IsLoopback()
+ }
+ return false
+}
+
+// IsUnicastIP returns true if its argument is a unicast IP address.
+func IsUnicastIP(a ipc.Address) bool {
+ if ip := AsIP(a.Address()); ip != nil && !ip.IsUnspecified() {
+ // ipv4 or v6
+ return !(ip.IsMulticast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast())
+ }
+ return false
+}
+
+// IsUnicastIPv4 returns true if its argument is a unicast IP4 address
+func IsUnicastIPv4(a ipc.Address) bool {
+ if ip := AsIP(a.Address()); ip != nil && ip.To4() != nil {
+ return !ip.IsUnspecified() && !ip.IsMulticast()
+ }
+ return false
+}
+
+// IsPublicUnicastIPv4 returns true if its argument is a globally routable,
+// public IPv4 unicast address.
+func IsPublicUnicastIPv4(a ipc.Address) bool {
+ if ip := AsIP(a.Address()); ip != nil && !ip.IsUnspecified() {
+ if t := ip.To4(); t != nil && IsGloballyRoutableIP(t) {
+ return !ip.IsMulticast()
+ }
+ }
+ return false
+}
+
+// IsUnicastIPv6 returns true if its argument is a unicast IPv6 address
+func IsUnicastIPv6(a ipc.Address) bool {
+ if ip := AsIP(a.Address()); ip != nil && ip.To4() == nil {
+ return !ip.IsUnspecified() && !(ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast())
+ }
+ return false
+}
+
+// IsUnicastIPv6 returns true if its argument is a globally routable IP6
+// address
+func IsPublicUnicastIPv6(a ipc.Address) bool {
+ if ip := AsIP(a.Address()); ip != nil && ip.To4() == nil {
+ if t := ip.To16(); t != nil && IsGloballyRoutableIP(t) {
+ return true
+ }
+ }
+ return false
+}
+
+// IsPublicUnicastIP returns true if its argument is a global routable IPv4
+// or 6 address.
+func IsPublicUnicastIP(a ipc.Address) bool {
+ if ip := AsIP(a.Address()); ip != nil {
+ if t := ip.To4(); t != nil && IsGloballyRoutableIP(t) {
+ return true
+ }
+ if t := ip.To16(); t != nil && IsGloballyRoutableIP(t) {
+ return true
+ }
+ }
+ return false
+}
+
+func diffAB(a, b AddrList) AddrList {
+ diff := AddrList{}
+ for _, av := range a {
+ found := false
+ for _, bv := range b {
+ if av.Address().Network() == bv.Address().Network() &&
+ av.Address().String() == bv.Address().String() {
+ found = true
+ break
+ }
+ }
+ if !found {
+ diff = append(diff, av)
+ }
+ }
+ return diff
+}
+
+// FindAdded returns the set addresses that are present in b, but not
+// in a - i.e. have been added.
+func FindAdded(a, b AddrList) AddrList {
+ return diffAB(b, a)
+}
+
+// FindRemoved returns the set of addresses that are present in a, but not
+// in b - i.e. have been removed.
+func FindRemoved(a, b AddrList) AddrList {
+ return diffAB(a, b)
+}
+
+// SameMachine returns true if the provided addr is on the device executing this
+// function.
+func SameMachine(addr net.Addr) (bool, error) {
+ // The available interfaces may change between calls.
+ addrs, err := GetAll()
+ if err != nil {
+ return false, err
+ }
+ ips := make(map[string]struct{})
+ for _, a := range addrs {
+ ip, _, err := net.ParseCIDR(a.Address().String())
+ if err != nil {
+ return false, err
+ }
+ ips[ip.String()] = struct{}{}
+ }
+
+ client, _, err := net.SplitHostPort(addr.String())
+ if err != nil {
+ return false, err
+ }
+ _, islocal := ips[client]
+ return islocal, nil
+}
diff --git a/netstate/netstate_test.go b/netstate/netstate_test.go
new file mode 100644
index 0000000..954b9d4
--- /dev/null
+++ b/netstate/netstate_test.go
@@ -0,0 +1,448 @@
+package netstate_test
+
+import (
+ "net"
+ "reflect"
+ "testing"
+
+ "v.io/v23/ipc"
+
+ "v.io/x/ref/lib/netconfig"
+ "v.io/x/ref/lib/netstate"
+)
+
+func TestGet(t *testing.T) {
+ // We assume that this machine running this test has at least
+ // one non-loopback interface.
+ all, err := netstate.GetAll()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ all = all.Map(netstate.ConvertToIPHost)
+ accessible, err := netstate.GetAccessibleIPs()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if len(all) == 0 || len(accessible) == 0 {
+ t.Errorf("expected non zero lengths, not %d and %d", len(all), len(accessible))
+ }
+ if len(accessible) > len(all) {
+ t.Errorf("should never be more accessible addresses than 'all' addresses")
+ }
+ loopback := netstate.FindAdded(accessible, all)
+ if got, want := loopback.Filter(netstate.IsLoopbackIP), loopback; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+type ma struct {
+ n, a string
+}
+
+func (a *ma) Network() string {
+ return a.n
+}
+
+func (a *ma) String() string {
+ return a.a
+}
+
+func mkAddr(n, a string) net.Addr {
+ ip := net.ParseIP(a)
+ return &ma{n: n, a: ip.String()}
+}
+
+func TestAsIP(t *testing.T) {
+ lh := net.ParseIP("127.0.0.1")
+ if got, want := netstate.AsIP(&net.IPAddr{IP: lh}), "127.0.0.1"; got == nil || got.String() != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ if got, want := netstate.AsIP(&net.IPNet{IP: lh}), "127.0.0.1"; got == nil || got.String() != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ if got, want := netstate.AsIP(&ma{"tcp", lh.String()}), "127.0.0.1"; got == nil || got.String() != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestRoutes(t *testing.T) {
+ accessible, err := netstate.GetAccessibleIPs()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ ifcl, err := netstate.GetInterfaces()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+
+ if len(ifcl) == 0 || len(accessible) == 0 {
+ t.Errorf("expected non zero lengths, not %d and %d", len(ifcl), len(accessible))
+ }
+
+ routes := netstate.GetRoutes()
+ // Make sure that the routes refer to valid interfaces
+ for _, r := range routes {
+ found := false
+ for _, ifc := range ifcl {
+ if r.IfcIndex == ifc.Index {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("failed to find ifc index %d", r.IfcIndex)
+ }
+ }
+}
+
+func mkAddrIfc(n, a string) *netstate.AddrIfc {
+ ip := net.ParseIP(a)
+ return &netstate.AddrIfc{
+ Addr: &ma{n: n, a: ip.String()},
+ }
+}
+
+type hw struct{}
+
+func (*hw) Network() string { return "mac" }
+func (*hw) String() string { return "01:23:45:67:89:ab:cd:ef" }
+
+func TestPredicates(t *testing.T) {
+ hwifc := &netstate.AddrIfc{Addr: &hw{}}
+ if got, want := netstate.IsUnicastIP(hwifc), false; got != want {
+ t.Errorf("got %t, want %t", got, want)
+
+ }
+ cases := []struct {
+ f func(a ipc.Address) bool
+ a string
+ r bool
+ }{
+ {netstate.IsUnspecifiedIP, "0.0.0.0", true},
+ {netstate.IsUnspecifiedIP, "::", true},
+ {netstate.IsUnspecifiedIP, "127.0.0.1", false},
+ {netstate.IsUnspecifiedIP, "::1", false},
+
+ {netstate.IsLoopbackIP, "0.0.0.0", false},
+ {netstate.IsLoopbackIP, "::", false},
+ {netstate.IsLoopbackIP, "127.0.0.1", true},
+ {netstate.IsLoopbackIP, "::1", true},
+
+ {netstate.IsAccessibleIP, "0.0.0.0", false},
+ {netstate.IsAccessibleIP, "::", false},
+ {netstate.IsAccessibleIP, "127.0.0.1", false},
+ {netstate.IsAccessibleIP, "::1", false},
+ {netstate.IsAccessibleIP, "224.0.0.2", true},
+ {netstate.IsAccessibleIP, "fc00:1234::", true},
+ {netstate.IsAccessibleIP, "192.168.1.1", true},
+ {netstate.IsAccessibleIP, "2001:4860:0:2001::68", true},
+
+ {netstate.IsUnicastIP, "0.0.0.0", false},
+ {netstate.IsUnicastIP, "::", false},
+ {netstate.IsUnicastIP, "127.0.0.1", true},
+ {netstate.IsUnicastIP, "::1", true},
+ {netstate.IsUnicastIP, "192.168.1.2", true},
+ {netstate.IsUnicastIP, "74.125.239.36", true},
+ {netstate.IsUnicastIP, "224.0.0.2", false},
+ {netstate.IsUnicastIP, "fc00:1235::", true},
+ {netstate.IsUnicastIP, "ff01::01", false},
+ {netstate.IsUnicastIP, "2001:4860:0:2001::69", true},
+
+ {netstate.IsUnicastIPv4, "0.0.0.0", false},
+ {netstate.IsUnicastIPv4, "::", false},
+ {netstate.IsUnicastIPv4, "127.0.0.1", true},
+ {netstate.IsUnicastIPv4, "::1", false},
+ {netstate.IsUnicastIPv4, "192.168.1.3", true},
+ {netstate.IsUnicastIPv6, "74.125.239.37", false},
+ {netstate.IsUnicastIPv4, "224.0.0.2", false},
+ {netstate.IsUnicastIPv4, "fc00:1236::", false},
+ {netstate.IsUnicastIPv4, "ff01::02", false},
+ {netstate.IsUnicastIPv4, "2001:4860:0:2001::6a", false},
+
+ {netstate.IsUnicastIPv6, "0.0.0.0", false},
+ {netstate.IsUnicastIPv6, "::", false},
+ {netstate.IsUnicastIPv6, "127.0.0.1", false},
+ {netstate.IsUnicastIPv6, "::1", true},
+ {netstate.IsUnicastIPv6, "192.168.1.4", false},
+ {netstate.IsUnicastIPv6, "74.125.239.38", false},
+ {netstate.IsUnicastIPv6, "224.0.0.2", false},
+ {netstate.IsUnicastIPv6, "fc00:1237::", true},
+ {netstate.IsUnicastIPv6, "ff01::03", false},
+ {netstate.IsUnicastIPv6, "2607:f8b0:4003:c00::6b", true},
+
+ {netstate.IsPublicUnicastIP, "0.0.0.0", false},
+ {netstate.IsPublicUnicastIP, "::", false},
+ {netstate.IsPublicUnicastIP, "127.0.0.1", false},
+ {netstate.IsPublicUnicastIP, "::1", false},
+ {netstate.IsPublicUnicastIP, "192.168.1.2", false},
+ {netstate.IsPublicUnicastIP, "74.125.239.39", true},
+ {netstate.IsPublicUnicastIP, "224.0.0.2", false},
+ // Arguably this is buggy, the fc00:/7 prefix is supposed to be
+ // non-routable.
+ {netstate.IsPublicUnicastIP, "fc00:1238::", true},
+ {netstate.IsPublicUnicastIP, "ff01::01", false},
+ {netstate.IsPublicUnicastIP, "2001:4860:0:2001::69", true},
+
+ {netstate.IsPublicUnicastIPv4, "0.0.0.0", false},
+ {netstate.IsPublicUnicastIPv4, "::", false},
+ {netstate.IsPublicUnicastIPv4, "127.0.0.1", false},
+ {netstate.IsPublicUnicastIPv4, "::1", false},
+ {netstate.IsPublicUnicastIPv4, "192.168.1.3", false},
+ {netstate.IsPublicUnicastIPv4, "74.125.239.40", true},
+ {netstate.IsPublicUnicastIPv4, "224.0.0.2", false},
+ {netstate.IsPublicUnicastIPv4, "fc00:1239::", false},
+ {netstate.IsPublicUnicastIPv4, "ff01::02", false},
+ {netstate.IsPublicUnicastIPv4, "2001:4860:0:2001::6a", false},
+
+ {netstate.IsPublicUnicastIPv6, "0.0.0.0", false},
+ {netstate.IsPublicUnicastIPv6, "::", false},
+ {netstate.IsPublicUnicastIPv6, "127.0.0.1", false},
+ {netstate.IsPublicUnicastIPv6, "::1", false},
+ {netstate.IsPublicUnicastIPv6, "192.168.1.4", false},
+ {netstate.IsPublicUnicastIPv6, "74.125.239.41", false},
+ {netstate.IsPublicUnicastIPv6, "224.0.0.2", false},
+ // Arguably this is buggy, the fc00:/7 prefix is supposed to be
+ // non-routable.
+ {netstate.IsPublicUnicastIPv6, "fc00:123a::", true},
+ {netstate.IsPublicUnicastIPv6, "ff01::03", false},
+ {netstate.IsPublicUnicastIPv6, "2607:f8b0:4003:c00::6b", true},
+ }
+ for i, c := range cases {
+ net := "tcp"
+ if got, want := c.f(mkAddrIfc(net, c.a)), c.r; got != want {
+ t.Errorf("#%d: %s %s: got %t, want %t", i+1, net, c.a, got, want)
+ }
+ }
+}
+
+func TestRoutePredicate(t *testing.T) {
+ net1_ip, net1, _ := net.ParseCIDR("192.168.1.10/24")
+ net2_ip, net2, _ := net.ParseCIDR("172.16.1.11/24")
+ net3_ip, net3, _ := net.ParseCIDR("172.16.2.12/24")
+ // net4 and net5 are on the same interface.
+ net4_ip, net4, _ := net.ParseCIDR("172.19.39.142/23")
+ net5_ip, net5, _ := net.ParseCIDR("2620::1000:5e01:56e4:3aff:fef1:1383/64")
+
+ rt1 := []*netconfig.IPRoute{{*net1, net.ParseIP("192.168.1.1"), nil, 1}}
+ rt2 := []*netconfig.IPRoute{{*net2, net.ParseIP("172.16.1.1"), nil, 2}}
+ rt3 := []*netconfig.IPRoute{{*net3, net.ParseIP("172.16.2.1"), nil, 3}}
+ rt4_0 := &netconfig.IPRoute{*net4, net.ParseIP("172.19.39.142"), nil, 6}
+ rt4_1 := &netconfig.IPRoute{*net5, net.ParseIP("fe80::5:73ff:fea0:fb"), nil, 6}
+ rt4 := []*netconfig.IPRoute{rt4_0, rt4_1}
+
+ net1_addr := &netstate.AddrIfc{&net.IPAddr{IP: net1_ip}, "eth0", rt1}
+ net2_addr := &netstate.AddrIfc{&net.IPAddr{IP: net2_ip}, "eth1", rt2}
+ net3_addr := &netstate.AddrIfc{&net.IPAddr{IP: net3_ip}, "eth2", rt3}
+ net4_addr := &netstate.AddrIfc{&net.IPAddr{IP: net4_ip}, "wn0", rt4}
+ net5_addr := &netstate.AddrIfc{&net.IPAddr{IP: net5_ip}, "wn0", rt4}
+
+ al := netstate.AddrList{}
+ if got, want := al.Filter(netstate.IsOnDefaultRoute), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ al = netstate.AddrList{net1_addr, net2_addr, net3_addr, net4_addr, net5_addr}
+ if got, want := al.Filter(netstate.IsOnDefaultRoute), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ defaultRoute := net.IPNet{net.IPv4zero, make([]byte, net.IPv4len)}
+ // Make eth1 a default route.
+ rt2[0].Net = defaultRoute
+ if got, want := al.Filter(netstate.IsOnDefaultRoute), (netstate.AddrList{net2_addr}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ // Make wn0 a default route also.
+ rt3[0].Net = defaultRoute
+ if got, want := al.Filter(netstate.IsOnDefaultRoute), (netstate.AddrList{net2_addr, net3_addr}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ // Restore the original route.
+ rt2[0].Net = *net2
+ rt4_0.Net = defaultRoute
+ if got, want := al.Filter(netstate.IsOnDefaultRoute), (netstate.AddrList{net3_addr, net4_addr}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ // Shouldn't return the IPv6 default route so long as al doesn't
+ // contain any IPv6 default routes.
+ rt4_0.Net = *net4
+ rt4_1.Net = defaultRoute
+ if got, want := al.Filter(netstate.IsOnDefaultRoute), (netstate.AddrList{net3_addr}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ // Now that we have an IPv6 default route that matches an IPv6 gateway
+ // we can expect to find the IPv6 host address
+ rt4_1.Net = net.IPNet{net.IPv6zero, make([]byte, net.IPv6len)}
+ if got, want := al.Filter(netstate.IsOnDefaultRoute), (netstate.AddrList{net3_addr, net5_addr}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+var (
+ a = mkAddrIfc("tcp4", "1.2.3.4")
+ b = mkAddrIfc("tcp4", "1.2.3.5")
+ c = mkAddrIfc("tcp4", "1.2.3.6")
+ d = mkAddrIfc("tcp4", "1.2.3.7")
+ a6 = mkAddrIfc("tcp6", "2001:4860:0:2001::68")
+ b6 = mkAddrIfc("tcp6", "2001:4860:0:2001::69")
+ c6 = mkAddrIfc("tcp6", "2001:4860:0:2001::70")
+ d6 = mkAddrIfc("tcp6", "2001:4860:0:2001::71")
+)
+
+func TestRemoved(t *testing.T) {
+ al := netstate.AddrList{a, b, c, a6, b6, c6}
+ bl := netstate.AddrList{}
+
+ // no changes.
+ got, want := netstate.FindRemoved(al, al), netstate.AddrList{}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+
+ // missing everything
+ if got, want := netstate.FindRemoved(al, bl), al; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // missing nothing
+ if got, want := netstate.FindRemoved(bl, al), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // remove some addresses
+ bl = netstate.AddrList{a, b, a6, b6}
+ if got, want := netstate.FindRemoved(al, bl), (netstate.AddrList{c, c6}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // add some addresses
+ bl = netstate.AddrList{a, b, c, a6, b6, c6, d6}
+ if got, want := netstate.FindRemoved(al, bl), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // change some addresses
+ bl = netstate.AddrList{a, b, d, a6, d6, c6}
+ if got, want := netstate.FindRemoved(al, bl), (netstate.AddrList{c, b6}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+}
+
+func TestAdded(t *testing.T) {
+ al := netstate.AddrList{a, b, c, a6, b6, c6}
+ bl := netstate.AddrList{}
+
+ // no changes.
+ if got, want := netstate.FindAdded(al, al), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // add nothing
+ if got, want := netstate.FindAdded(al, bl), bl; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // add everything
+ if got, want := netstate.FindAdded(bl, al), al; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // add some addresses
+ bl = netstate.AddrList{a, b, c, d, a6, b6, c6, d6}
+ if got, want := netstate.FindAdded(al, bl), (netstate.AddrList{d, d6}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // remove some addresses
+ bl = netstate.AddrList{a, b, c, b6}
+ if got, want := netstate.FindAdded(al, bl), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // change some addresses
+ bl = netstate.AddrList{a, d, c, a6, d6, c6}
+ if got, want := netstate.FindAdded(al, bl), (netstate.AddrList{d, d6}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+}
+
+// buildNonLocalhostTestAddress constructs a selection of test addresses
+// that are local.
+func buildNonLocalhostTestAddress(t *testing.T) []string {
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ t.Errorf("InterfaceAddrs() failed: %v\n", err)
+ }
+
+ ips := make([]string, 0, len(addrs))
+ for _, a := range addrs {
+ ip, _, err := net.ParseCIDR(a.String())
+ if err != nil {
+ t.Errorf("ParseCIDR() failed: %v\n", err)
+ }
+ ips = append(ips, net.JoinHostPort(ip.String(), "111"))
+ }
+ return ips
+}
+
+func TestSameMachine(t *testing.T) {
+ cases := []struct {
+ Addr *ma
+ Same bool
+ Err error
+ }{
+ {
+ Addr: &ma{
+ n: "tcp",
+ a: "batman.com:4444",
+ },
+ Same: false,
+ Err: nil,
+ },
+ {
+ Addr: &ma{
+ n: "tcp",
+ a: "127.0.0.1:1000",
+ },
+ Same: true,
+ Err: nil,
+ },
+ {
+ Addr: &ma{
+ n: "tcp",
+ a: "::1/128",
+ },
+ Same: false,
+ Err: &net.AddrError{Err: "too many colons in address", Addr: "::1/128"},
+ },
+ }
+
+ for _, a := range buildNonLocalhostTestAddress(t) {
+ cases = append(cases, struct {
+ Addr *ma
+ Same bool
+ Err error
+ }{
+ Addr: &ma{
+ n: "tcp",
+ a: a,
+ },
+ Same: true,
+ Err: nil,
+ })
+ }
+
+ for _, v := range cases {
+ issame, err := netstate.SameMachine(v.Addr)
+ if !reflect.DeepEqual(err, v.Err) {
+ t.Errorf("Bad error: got %#v, expected %#v\n", err, v.Err)
+ }
+ if issame != v.Same {
+ t.Errorf("for Endpoint address %v: got %v, expected %v\n", v.Addr.a, issame, v.Same)
+ }
+ }
+}
diff --git a/netstate/route.go b/netstate/route.go
new file mode 100644
index 0000000..906dc9e
--- /dev/null
+++ b/netstate/route.go
@@ -0,0 +1,105 @@
+package netstate
+
+import (
+ "fmt"
+ "net"
+ "strings"
+
+ "v.io/v23/ipc"
+
+ "v.io/x/ref/lib/netconfig"
+)
+
+// Interface represents a network interface.
+type Interface struct {
+ Index int
+ Name string
+}
+type InterfaceList []*Interface
+
+// GetInterfaces returns a list of all of the network interfaces on this
+// device.
+func GetInterfaces() (InterfaceList, error) {
+ ifcl := InterfaceList{}
+ interfaces, err := net.Interfaces()
+ if err != nil {
+ return nil, err
+ }
+ for _, ifc := range interfaces {
+ ifcl = append(ifcl, &Interface{ifc.Index, ifc.Name})
+ }
+ return ifcl, nil
+}
+
+func (ifcl InterfaceList) String() string {
+ r := ""
+ for _, ifc := range ifcl {
+ r += fmt.Sprintf("(%d: %s) ", ifc.Index, ifc.Name)
+ }
+ return strings.TrimRight(r, " ")
+}
+
+// IPRouteList is a slice of IPRoutes as returned by the netconfig package.
+type IPRouteList []*netconfig.IPRoute
+
+func (rl IPRouteList) String() string {
+ r := ""
+ for _, rt := range rl {
+ src := ""
+ if len(rt.PreferredSource) > 0 {
+ src = ", src: " + rt.PreferredSource.String()
+ }
+ r += fmt.Sprintf("(%d: net: %s, gw: %s%s) ", rt.IfcIndex, rt.Net, rt.Gateway, src)
+ }
+ return strings.TrimRight(r, " ")
+}
+
+func GetRoutes() IPRouteList {
+ return netconfig.GetIPRoutes(false)
+}
+
+// RoutePredicate defines the function signature for predicate functions
+// to be used with RouteList
+type RoutePredicate func(r *netconfig.IPRoute) bool
+
+// Filter returns all of the routes for which the predicate
+// function is true.
+func (rl IPRouteList) Filter(predicate RoutePredicate) IPRouteList {
+ r := IPRouteList{}
+ for _, rt := range rl {
+ if predicate(rt) {
+ r = append(r, rt)
+ }
+ }
+ return r
+}
+
+// IsDefaultRoute returns true if the supplied IPRoute is a default route.
+func IsDefaultRoute(r *netconfig.IPRoute) bool {
+ return netconfig.IsDefaultIPRoute(r)
+}
+
+// IsOnDefaultRoute returns true for addresses that are on an interface that
+// has a default route set for the supplied address.
+func IsOnDefaultRoute(a ipc.Address) bool {
+ aifc, ok := a.(*AddrIfc)
+ if !ok || len(aifc.IPRoutes) == 0 {
+ return false
+ }
+ ipv4 := IsUnicastIPv4(a)
+ for _, r := range aifc.IPRoutes {
+ // Ignore entries with a nil gateway.
+ if r.Gateway == nil {
+ continue
+ }
+ // We have a default route, so we check the gateway to make sure
+ // it matches the format of the default route.
+ if ipv4 {
+ return netconfig.IsDefaultIPv4Route(r) && r.Gateway.To4() != nil
+ }
+ if netconfig.IsDefaultIPv6Route(r) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/textutil/doc.go b/textutil/doc.go
new file mode 100644
index 0000000..fed643b
--- /dev/null
+++ b/textutil/doc.go
@@ -0,0 +1,8 @@
+// Package textutil implements utilities for handling human-readable text.
+//
+// This package includes a combination of low-level and high-level utilities.
+// The main high-level utilities are:
+// NewUTF8LineWriter: Line-based text formatter.
+// PrefixWriter: Add prefix to output.
+// ByteReplaceWriter: Replace single byte with bytes in output.
+package textutil
diff --git a/textutil/line_writer.go b/textutil/line_writer.go
new file mode 100644
index 0000000..c7df3a5
--- /dev/null
+++ b/textutil/line_writer.go
@@ -0,0 +1,445 @@
+package textutil
+
+import (
+ "fmt"
+ "io"
+ "unicode"
+)
+
+// LineWriter implements an io.Writer filter that formats input text into output
+// lines with a given target width in runes.
+//
+// Each input rune is classified into one of three kinds:
+// EOL: end-of-line, consisting of \f, \n, \r, \v, U+2028 or U+2029
+// Space: defined by unicode.IsSpace
+// Letter: everything else
+//
+// The input text is expected to consist of words, defined as sequences of
+// letters. Sequences of words form paragraphs, where paragraphs are separated
+// by either blank lines (that contain no letters), or an explicit U+2029
+// ParagraphSeparator. Input lines with leading spaces are treated verbatim.
+//
+// Paragraphs are output as word-wrapped lines; line breaks only occur at word
+// boundaries. Output lines are usually no longer than the target width. The
+// exceptions are single words longer than the target width, which are output on
+// their own line, and verbatim lines, which may be arbitrarily longer or
+// shorter than the width.
+//
+// Output lines never contain trailing spaces. Only verbatim output lines may
+// contain leading spaces. Spaces separating input words are output verbatim,
+// unless it would result in a line with leading or trailing spaces.
+//
+// EOL runes within the input text are never written to the output; the output
+// line terminator and paragraph separator may be configured, and some EOL may
+// be output as a single space ' ' to maintain word separation.
+//
+// The algorithm greedily fills each output line with as many words as it can,
+// assuming that all Unicode code points have the same width. Invalid UTF-8 is
+// silently transformed to the replacement character U+FFFD and treated as a
+// single rune.
+//
+// Flush must be called after the last call to Write; the input is buffered.
+//
+// Implementation note: line breaking is a complicated topic. This approach
+// attempts to be simple and useful; a full implementation conforming to
+// Unicode Standard Annex #14 would be complicated, and is not implemented.
+// Languages that don't use spaces to separate words (e.g. CJK) won't work
+// well under the current approach.
+//
+// http://www.unicode.org/reports/tr14 [Unicode Line Breaking Algorithm]
+// http://www.unicode.org/versions/Unicode4.0.0/ch05.pdf [5.8 Newline Guidelines]
+type LineWriter struct {
+ // State configured by the user.
+ w io.Writer
+ runeDecoder RuneChunkDecoder
+ width runePos
+ lineTerm []byte
+ paragraphSep string
+ indents []string
+
+ // The buffer contains a single output line.
+ lineBuf byteRuneBuffer
+
+ // Keep track of the previous state and rune.
+ prevState state
+ prevRune rune
+
+ // Keep track of blank input lines.
+ inputLineHasLetter bool
+
+ // lineBuf positions where the line starts (after separators and indents), a
+ // new word has started and the last word has ended.
+ lineStart bytePos
+ newWordStart bytePos
+ lastWordEnd bytePos
+
+ // Keep track of paragraph terminations and line indices, so we can output the
+ // paragraph separator and indents correctly.
+ terminateParagraph bool
+ paragraphLineIndex int
+ wroteFirstLine bool
+}
+
+type state int
+
+const (
+ stateWordWrap state = iota // Perform word-wrapping [start state]
+ stateVerbatim // Verbatim output-line, no word-wrapping
+ stateSkipSpace // Skip spaces in input line.
+)
+
+// NewLineWriter returns a new LineWriter with the given target width in runes,
+// producing output on the underlying writer w. The dec and enc are used to
+// respectively decode runes from Write calls, and encode runes to w.
+func NewLineWriter(w io.Writer, width int, dec RuneChunkDecoder, enc RuneEncoder) *LineWriter {
+ ret := &LineWriter{
+ w: w,
+ runeDecoder: dec,
+ width: runePos(width),
+ lineTerm: []byte("\n"),
+ paragraphSep: "\n",
+ prevState: stateWordWrap,
+ prevRune: LineSeparator,
+ lineBuf: byteRuneBuffer{enc: enc},
+ }
+ ret.resetLine()
+ return ret
+}
+
+// NewUTF8LineWriter returns a new LineWriter filter that implements io.Writer,
+// and decodes and encodes runes in UTF-8.
+func NewUTF8LineWriter(w io.Writer, width int) *LineWriter {
+ return NewLineWriter(w, width, &UTF8ChunkDecoder{}, UTF8Encoder{})
+}
+
+// Width returns the target width in runes. If width < 0 the width is
+// unlimited; each paragraph is output as a single line.
+func (w *LineWriter) Width() int { return int(w.width) }
+
+// SetLineTerminator sets the line terminator for subsequent Write calls. Every
+// output line is terminated with term; EOL runes from the input are never
+// written to the output. A new LineWriter instance uses "\n" as the default
+// line terminator.
+//
+// Calls Flush internally, and returns any Flush error.
+func (w *LineWriter) SetLineTerminator(term string) error {
+ if err := w.Flush(); err != nil {
+ return err
+ }
+ w.lineTerm = []byte(term)
+ w.resetLine()
+ return nil
+}
+
+// SetParagraphSeparator sets the paragraph separator for subsequent Write
+// calls. Every consecutive pair of non-empty paragraphs is separated with sep;
+// EOL runes from the input are never written to the output. A new LineWriter
+// instance uses "\n" as the default paragraph separator.
+//
+// Calls Flush internally, and returns any Flush error.
+func (w *LineWriter) SetParagraphSeparator(sep string) error {
+ if err := w.Flush(); err != nil {
+ return err
+ }
+ w.paragraphSep = sep
+ w.resetLine()
+ return nil
+}
+
+// SetIndents sets the indentation for subsequent Write calls. Multiple indents
+// may be set, corresponding to the indent to use for the corresponding
+// paragraph line. E.g. SetIndents("AA", "BBB", C") means the first line in
+// each paragraph is indented with "AA", the second line in each paragraph is
+// indented with "BBB", and all subsequent lines in each paragraph are indented
+// with "C".
+//
+// SetIndents() is equivalent to SetIndents(""), SetIndents("", ""), etc.
+//
+// A new LineWriter instance has no indents by default.
+//
+// Calls Flush internally, and returns any Flush error.
+func (w *LineWriter) SetIndents(indents ...string) error {
+ if err := w.Flush(); err != nil {
+ return err
+ }
+ // Copy indents in case the user passed the slice via SetIndents(p...), and
+ // canonicalize the all empty case to nil.
+ allEmpty := true
+ w.indents = make([]string, len(indents))
+ for ix, indent := range indents {
+ w.indents[ix] = indent
+ if indent != "" {
+ allEmpty = false
+ }
+ }
+ if allEmpty {
+ w.indents = nil
+ }
+ w.resetLine()
+ return nil
+}
+
+// Write implements io.Writer by buffering data into the LineWriter w. Actual
+// writes to the underlying writer may occur, and may include data buffered in
+// either this Write call or previous Write calls.
+//
+// Flush must be called after the last call to Write.
+func (w *LineWriter) Write(data []byte) (int, error) {
+ return RuneChunkWrite(w.runeDecoder, w.addRune, data)
+}
+
+// Flush flushes any remaining buffered text, and resets the paragraph line
+// count back to 0, so that indents will be applied starting from the first
+// line. It does not imply a paragraph separator; repeated calls to Flush with
+// no intervening calls to other methods is equivalent to a single Flush.
+//
+// Flush must be called after the last call to Write, and may be called an
+// arbitrary number of times before the last Write.
+func (w *LineWriter) Flush() error {
+ if err := RuneChunkFlush(w.runeDecoder, w.addRune); err != nil {
+ return err
+ }
+ // Add U+2028 to force the last line (if any) to be written.
+ if err := w.addRune(LineSeparator); err != nil {
+ return err
+ }
+ // Reset the paragraph line count.
+ w.paragraphLineIndex = 0
+ w.resetLine()
+ return nil
+}
+
+// addRune is called every time w.runeDecoder decodes a full rune.
+func (w *LineWriter) addRune(r rune) error {
+ state, lineBreak := w.nextState(r, w.updateRune(r))
+ if lineBreak {
+ if err := w.writeLine(); err != nil {
+ return err
+ }
+ }
+ w.bufferRune(r, state, lineBreak)
+ w.prevState = state
+ w.prevRune = r
+ return nil
+}
+
+// We classify each incoming rune into three kinds for easier handling.
+type kind int
+
+const (
+ kindEOL kind = iota
+ kindSpace
+ kindLetter
+)
+
+func runeKind(r rune) kind {
+ switch r {
+ case '\f', '\n', '\r', '\v', LineSeparator, ParagraphSeparator:
+ return kindEOL
+ }
+ if unicode.IsSpace(r) {
+ return kindSpace
+ }
+ return kindLetter
+}
+
+func (w *LineWriter) updateRune(r rune) bool {
+ forceLineBreak := false
+ switch kind := runeKind(r); kind {
+ case kindEOL:
+ // Update lastWordEnd if the last word just ended.
+ if w.newWordStart != -1 {
+ w.newWordStart = -1
+ w.lastWordEnd = w.lineBuf.ByteLen()
+ }
+ switch {
+ case w.prevRune == '\r' && r == '\n':
+ // Treat "\r\n" as a single EOL; we've already handled the logic for '\r',
+ // so there's nothing to do when we see '\n'.
+ case r == LineSeparator:
+ // Treat U+2028 as a pure line break; it's never a paragraph break.
+ forceLineBreak = true
+ case r == ParagraphSeparator || !w.inputLineHasLetter:
+ // The paragraph has just been terminated if we see an explicit U+2029, or
+ // if we see a blank line, which may contain spaces.
+ forceLineBreak = true
+ w.terminateParagraph = true
+ }
+ w.inputLineHasLetter = false
+ case kindSpace:
+ // Update lastWordEnd if the last word just ended.
+ if w.newWordStart != -1 {
+ w.newWordStart = -1
+ w.lastWordEnd = w.lineBuf.ByteLen()
+ }
+ case kindLetter:
+ // Update newWordStart if a new word just started.
+ if w.newWordStart == -1 {
+ w.newWordStart = w.lineBuf.ByteLen()
+ }
+ w.inputLineHasLetter = true
+ w.terminateParagraph = false
+ default:
+ panic(fmt.Errorf("textutil: updateRune unhandled kind %d", kind))
+ }
+ return forceLineBreak
+}
+
+// nextState returns the next state and whether we should break the line.
+//
+// Here's a handy table that describes all the scenarios in which we will line
+// break input text, grouped by the reason for the break. The current position
+// is the last non-* rune in each pattern, which is where we decide to break.
+//
+// w.prevState Next state Buffer reset
+// ----------- ---------- ------------
+// ===== Force line break (U+2028 / U+2029, blank line) =====
+// a..*|*** * wordWrap empty
+// a._.|*** * wordWrap empty
+// a+**|*** * wordWrap empty
+//
+// ===== verbatim: wait for any EOL =====
+// _*.*|*** verbatim wordWrap empty
+//
+// ===== wordWrap: switch to verbatim =====
+// a._*|*** wordWrap verbatim empty
+//
+// ===== wordWrap: line is too wide =====
+// abc.|*** wordWrap wordWrap empty
+// abcd|.** wordWrap wordWrap empty
+// abcd|e.* wordWrap wordWrap empty
+// a_cd|.** wordWrap wordWrap empty
+//
+// abc_|*** wordWrap skipSpace empty
+// abcd|_** wordWrap skipSpace empty
+// abcd|e_* wordWrap skipSpace empty
+// a_cd|_** wordWrap skipSpace empty
+//
+// a_cd|e** wordWrap start newWordStart
+//
+// LEGEND
+// abcde Letter
+// . End-of-line
+// + End-of-line (only U+2028 / U+2029)
+// _ Space
+// * Any rune (letter, line-end or space)
+// | Visual indication of width=4, has no width itself.
+//
+// Note that Flush calls behave exactly as if an explicit U+2028 line separator
+// were added to the end of all buffered data.
+func (w *LineWriter) nextState(r rune, forceLineBreak bool) (state, bool) {
+ if forceLineBreak {
+ return stateWordWrap, true
+ }
+ kind := runeKind(r)
+ // Handle non word-wrap states, which are easy.
+ switch w.prevState {
+ case stateVerbatim:
+ if kind == kindEOL {
+ return stateWordWrap, true
+ }
+ return stateVerbatim, false
+ case stateSkipSpace:
+ if kind == kindSpace {
+ return stateSkipSpace, false
+ }
+ return stateWordWrap, false
+ }
+ // Handle stateWordWrap, which is more complicated.
+
+ // Switch to the verbatim state when we see a space right after an EOL.
+ if runeKind(w.prevRune) == kindEOL && kind == kindSpace {
+ return stateVerbatim, true
+ }
+ // Break on EOL or space when the line is too wide. See above table.
+ if w.width >= 0 && w.width <= w.lineBuf.RuneLen()+1 {
+ switch kind {
+ case kindEOL:
+ return stateWordWrap, true
+ case kindSpace:
+ return stateSkipSpace, true
+ }
+ // case kindLetter falls through
+ }
+ // Handle the newWordStart case in the above table.
+ if w.width >= 0 && w.width < w.lineBuf.RuneLen()+1 && w.newWordStart != w.lineStart {
+ return stateWordWrap, true
+ }
+ // Stay in the wordWrap state and don't break the line.
+ return stateWordWrap, false
+}
+
+func (w *LineWriter) writeLine() error {
+ if w.lastWordEnd == -1 {
+ // Don't write blank lines, but we must reset the line in case the paragraph
+ // has just been terminated.
+ w.resetLine()
+ return nil
+ }
+ // Write the line (without trailing spaces) followed by the line terminator.
+ line := w.lineBuf.Bytes()[:w.lastWordEnd]
+ if _, err := w.w.Write(line); err != nil {
+ return err
+ }
+ if _, err := w.w.Write(w.lineTerm); err != nil {
+ return err
+ }
+ // Reset the line buffer.
+ w.wroteFirstLine = true
+ w.paragraphLineIndex++
+ if w.newWordStart != -1 {
+ // If we have an unterminated new word, we must be in the newWordStart case
+ // in the table above. Handle the special buffer reset here.
+ newWord := string(w.lineBuf.Bytes()[w.newWordStart:])
+ w.resetLine()
+ w.newWordStart = w.lineBuf.ByteLen()
+ w.lineBuf.WriteString(newWord)
+ } else {
+ w.resetLine()
+ }
+ return nil
+}
+
+func (w *LineWriter) resetLine() {
+ w.lineBuf.Reset()
+ w.newWordStart = -1
+ w.lastWordEnd = -1
+ // Write the paragraph separator if the previous paragraph has terminated.
+ // This consumes no runes from the line width.
+ if w.wroteFirstLine && w.terminateParagraph {
+ w.lineBuf.WriteString0Runes(w.paragraphSep)
+ w.paragraphLineIndex = 0
+ }
+ // Add indent; a non-empty indent consumes runes from the line width.
+ var indent string
+ switch {
+ case w.paragraphLineIndex < len(w.indents):
+ indent = w.indents[w.paragraphLineIndex]
+ case len(w.indents) > 0:
+ indent = w.indents[len(w.indents)-1]
+ }
+ w.lineBuf.WriteString(indent)
+ w.lineStart = w.lineBuf.ByteLen()
+}
+
+func (w *LineWriter) bufferRune(r rune, state state, lineBreak bool) {
+ // Never add leading spaces to the buffer in the wordWrap state.
+ wordWrapNoLeadingSpaces := state == stateWordWrap && !lineBreak
+ switch kind := runeKind(r); kind {
+ case kindEOL:
+ // When we're word-wrapping and we see a letter followed by EOL, we convert
+ // the EOL into a single space in the buffer, to break the previous word
+ // from the next word.
+ if wordWrapNoLeadingSpaces && runeKind(w.prevRune) == kindLetter {
+ w.lineBuf.WriteRune(' ')
+ }
+ case kindSpace:
+ if wordWrapNoLeadingSpaces || state == stateVerbatim {
+ w.lineBuf.WriteRune(r)
+ }
+ case kindLetter:
+ w.lineBuf.WriteRune(r)
+ default:
+ panic(fmt.Errorf("textutil: bufferRune unhandled kind %d", kind))
+ }
+}
diff --git a/textutil/line_writer_test.go b/textutil/line_writer_test.go
new file mode 100644
index 0000000..29db23f
--- /dev/null
+++ b/textutil/line_writer_test.go
@@ -0,0 +1,268 @@
+package textutil
+
+import (
+ "bytes"
+ "io"
+ "strings"
+ "testing"
+)
+
+type lp struct {
+ line, para string
+}
+
+var (
+ allIndents = [][]int{nil, {}, {1}, {2}, {1, 2}, {2, 1}}
+ allIndents1 = [][]int{{1}, {2}, {1, 2}, {2, 1}}
+)
+
+func TestLineWriter(t *testing.T) {
+ tests := []struct {
+ Width int
+ Indents [][]int
+ In string // See xlateIn for details on the format
+ Want string // See xlateWant for details on the format
+ }{
+ // Completely blank input yields empty output.
+ {4, allIndents, "", ""},
+ {4, allIndents, " ", ""},
+ {4, allIndents, " ", ""},
+ {4, allIndents, " ", ""},
+ {4, allIndents, " ", ""},
+ {4, allIndents, " ", ""},
+ {4, allIndents, " ", ""},
+ {4, allIndents, "F N R V L P ", ""},
+ // Single words never get word-wrapped, even if they're long.
+ {4, allIndents, "a", "0a."},
+ {4, allIndents, "ab", "0ab."},
+ {4, allIndents, "abc", "0abc."},
+ {4, allIndents, "abcd", "0abcd."},
+ {4, allIndents, "abcde", "0abcde."},
+ {4, allIndents, "abcdef", "0abcdef."},
+ // Word-wrapping boundary conditions.
+ {4, allIndents, "abc ", "0abc."},
+ {4, allIndents, "abc ", "0abc."},
+ {4, allIndents, "abcN", "0abc."},
+ {4, allIndents, "abcN ", "0abc."},
+ {4, allIndents, "abcd ", "0abcd."},
+ {4, allIndents, "abcd ", "0abcd."},
+ {4, allIndents, "abcdN", "0abcd."},
+ {4, allIndents, "abcdN ", "0abcd."},
+ {4, [][]int{nil}, "a cd", "0a cd."},
+ {4, [][]int{nil}, "a cd ", "0a cd."},
+ {4, [][]int{nil}, "a cdN", "0a cd."},
+ {4, allIndents1, "a cd", "0a.1cd."},
+ {4, allIndents1, "a cd ", "0a.1cd."},
+ {4, allIndents1, "a cdN", "0a.1cd."},
+ {4, allIndents, "a cde", "0a.1cde."},
+ {4, allIndents, "a cde ", "0a.1cde."},
+ {4, allIndents, "a cdeN", "0a.1cde."},
+ {4, [][]int{nil}, "a d", "0a d."},
+ {4, [][]int{nil}, "a d ", "0a d."},
+ {4, [][]int{nil}, "a dN", "0a d."},
+ {4, allIndents1, "a d", "0a.1d."},
+ {4, allIndents1, "a d ", "0a.1d."},
+ {4, allIndents1, "a dN", "0a.1d."},
+ {4, allIndents, "a de", "0a.1de."},
+ {4, allIndents, "a de ", "0a.1de."},
+ {4, allIndents, "a deN", "0a.1de."},
+ // Multi-line word-wrapping boundary conditions.
+ {4, allIndents, "abc e", "0abc.1e."},
+ {4, allIndents, "abc.e", "0abc.1e."},
+ {4, allIndents, "abc efgh", "0abc.1efgh."},
+ {4, allIndents, "abc.efgh", "0abc.1efgh."},
+ {4, allIndents, "abc efghi", "0abc.1efghi."},
+ {4, allIndents, "abc.efghi", "0abc.1efghi."},
+ {4, [][]int{nil}, "abc e gh", "0abc.1e gh."},
+ {4, [][]int{nil}, "abc.e.gh", "0abc.1e gh."},
+ {4, allIndents1, "abc e gh", "0abc.1e.2gh."},
+ {4, allIndents1, "abc.e.gh", "0abc.1e.2gh."},
+ {4, allIndents, "abc e ghijk", "0abc.1e.2ghijk."},
+ {4, allIndents, "abc.e.ghijk", "0abc.1e.2ghijk."},
+ // Verbatim lines.
+ {4, allIndents, " b", "0 b."},
+ {4, allIndents, " bc", "0 bc."},
+ {4, allIndents, " bcd", "0 bcd."},
+ {4, allIndents, " bcde", "0 bcde."},
+ {4, allIndents, " bcdef", "0 bcdef."},
+ {4, allIndents, " bcdefg", "0 bcdefg."},
+ {4, allIndents, " b de ghijk", "0 b de ghijk."},
+ // Verbatim lines before word-wrapped lines.
+ {4, allIndents, " b.vw yz", "0 b.1vw.2yz."},
+ {4, allIndents, " bc.vw yz", "0 bc.1vw.2yz."},
+ {4, allIndents, " bcd.vw yz", "0 bcd.1vw.2yz."},
+ {4, allIndents, " bcde.vw yz", "0 bcde.1vw.2yz."},
+ {4, allIndents, " bcdef.vw yz", "0 bcdef.1vw.2yz."},
+ {4, allIndents, " bcdefg.vw yz", "0 bcdefg.1vw.2yz."},
+ {4, allIndents, " b de ghijk.vw yz", "0 b de ghijk.1vw.2yz."},
+ // Verbatim lines after word-wrapped lines.
+ {4, allIndents, "vw yz. b", "0vw.1yz.2 b."},
+ {4, allIndents, "vw yz. bc", "0vw.1yz.2 bc."},
+ {4, allIndents, "vw yz. bcd", "0vw.1yz.2 bcd."},
+ {4, allIndents, "vw yz. bcde", "0vw.1yz.2 bcde."},
+ {4, allIndents, "vw yz. bcdef", "0vw.1yz.2 bcdef."},
+ {4, allIndents, "vw yz. bcdefg", "0vw.1yz.2 bcdefg."},
+ {4, allIndents, "vw yz. b de ghijk", "0vw.1yz.2 b de ghijk."},
+ // Verbatim lines between word-wrapped lines.
+ {4, allIndents, "vw yz. b.mn pq", "0vw.1yz.2 b.2mn.2pq."},
+ {4, allIndents, "vw yz. bc.mn pq", "0vw.1yz.2 bc.2mn.2pq."},
+ {4, allIndents, "vw yz. bcd.mn pq", "0vw.1yz.2 bcd.2mn.2pq."},
+ {4, allIndents, "vw yz. bcde.mn pq", "0vw.1yz.2 bcde.2mn.2pq."},
+ {4, allIndents, "vw yz. bcdef.mn pq", "0vw.1yz.2 bcdef.2mn.2pq."},
+ {4, allIndents, "vw yz. bcdefg.mn pq", "0vw.1yz.2 bcdefg.2mn.2pq."},
+ {4, allIndents, "vw yz. b de ghijk.mn pq", "0vw.1yz.2 b de ghijk.2mn.2pq."},
+ // Multi-paragraphs via explicit U+2029, and multi-newline.
+ {4, allIndents, "ab de ghPij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab.de.ghPij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab de gh Pij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab.de.gh Pij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab de ghNNij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab.de.ghNNij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab de ghNNNij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab.de.ghNNNij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab de gh N Nij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab.de.gh N Nij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab de gh N N Nij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab.de.gh N N Nij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ // Special-case /r/n is a single EOL, but may be combined.
+ {4, allIndents, "ab de ghRNij lm op", "0ab.1de.2gh.2ij.2lm.2op."},
+ {4, allIndents, "ab.de.ghRNij.lm.op", "0ab.1de.2gh.2ij.2lm.2op."},
+ {4, allIndents, "ab de gh RNij lm op", "0ab.1de.2gh.2ij.2lm.2op."},
+ {4, allIndents, "ab.de.gh RNij.lm.op", "0ab.1de.2gh.2ij.2lm.2op."},
+ {4, allIndents, "ab de ghRNRNij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab.de.ghRNRNij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab de gh RN RNij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab.de.gh RN RNij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab de ghR Nij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ {4, allIndents, "ab.de.ghR Nij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+ // Line separator via explicit U+2028 ends lines, but not paragraphs.
+ {4, allIndents, "aLcd", "0a.1cd."},
+ {4, allIndents, "a Lcd", "0a.1cd."},
+ {4, allIndents, "aLLcd", "0a.1cd."},
+ {4, allIndents, "a LLcd", "0a.1cd."},
+ // 0 width ends up with one word per line, except verbatim lines.
+ {0, allIndents, "a c e", "0a.1c.2e."},
+ {0, allIndents, "a cd fghij", "0a.1cd.2fghij."},
+ {0, allIndents, "a. cd fghij.l n", "0a.1 cd fghij.2l.2n."},
+ // -1 width ends up with all words on same line, except verbatim lines.
+ {-1, allIndents, "a c e", "0a c e."},
+ {-1, allIndents, "a cd fghij", "0a cd fghij."},
+ {-1, allIndents, "a. cd fghij.l n", "0a.1 cd fghij.2l n."},
+ }
+ for _, test := range tests {
+ // Run with a variety of chunk sizes.
+ for _, sizes := range [][]int{nil, {1}, {2}, {1, 2}, {2, 1}} {
+ // Run with a variety of line terminators and paragraph separators.
+ for _, lp := range []lp{{}, {"\n", "\n"}, {"L", "P"}, {"LLL", "PPP"}} {
+ // Run with a variety of indents.
+ if len(test.Indents) == 0 {
+ t.Errorf("%d %q %q has no indents, use [][]int{nil} rather than nil", test.Width, test.In, test.Want)
+ }
+ for _, indents := range test.Indents {
+ var buf bytes.Buffer
+ w := newUTF8LineWriter(t, &buf, test.Width, lp, indents)
+ lineWriterWriteFlush(t, w, xlateIn(test.In), sizes)
+ if got, want := buf.String(), xlateWant(test.Want, lp, indents); got != want {
+ t.Errorf("%q sizes:%v lp:%q indents:%v got %q, want %q", test.In, sizes, lp, indents, got, want)
+ }
+ }
+ }
+ }
+ }
+}
+
+// xlateIn translates our test.In pattern into an actual input string to feed
+// into the writer. The point is to make it easy to specify the various control
+// sequences in a single character, so it's easier to understand.
+func xlateIn(text string) string {
+ text = strings.Replace(text, "F", "\f", -1)
+ text = strings.Replace(text, "N", "\n", -1)
+ text = strings.Replace(text, ".", "\n", -1) // Also allow . for easier reading
+ text = strings.Replace(text, "R", "\r", -1)
+ text = strings.Replace(text, "V", "\v", -1)
+ text = strings.Replace(text, "L", "\u2028", -1)
+ text = strings.Replace(text, "P", "\u2029", -1)
+ return text
+}
+
+// xlateWant translates our test.Want pattern into an actual expected string to
+// compare against the output. The point is to make it easy to read and write
+// the expected patterns, and to make it easy to test various indents.
+func xlateWant(text string, lp lp, indents []int) string {
+ // Dot "." and colon ":" in the want string indicate line terminators and
+ // paragraph separators, respectively.
+ line := lp.line
+ if line == "" {
+ line = "\n"
+ }
+ text = strings.Replace(text, ".", line, -1)
+ para := lp.para
+ if para == "" {
+ para = "\n"
+ }
+ text = strings.Replace(text, ":", para, -1)
+ // The numbers in the want string indicate paragraph line numbers, to make it
+ // easier to automatically replace for various indent configurations.
+ switch len(indents) {
+ case 0:
+ text = strings.Replace(text, "0", "", -1)
+ text = strings.Replace(text, "1", "", -1)
+ text = strings.Replace(text, "2", "", -1)
+ case 1:
+ text = strings.Replace(text, "0", spaces(indents[0]), -1)
+ text = strings.Replace(text, "1", spaces(indents[0]), -1)
+ text = strings.Replace(text, "2", spaces(indents[0]), -1)
+ case 2:
+ text = strings.Replace(text, "0", spaces(indents[0]), -1)
+ text = strings.Replace(text, "1", spaces(indents[1]), -1)
+ text = strings.Replace(text, "2", spaces(indents[1]), -1)
+ case 3:
+ text = strings.Replace(text, "0", spaces(indents[0]), -1)
+ text = strings.Replace(text, "1", spaces(indents[1]), -1)
+ text = strings.Replace(text, "2", spaces(indents[2]), -1)
+ }
+ return text
+}
+
+func spaces(count int) string {
+ return strings.Repeat(" ", count)
+}
+
+func newUTF8LineWriter(t *testing.T, buf io.Writer, width int, lp lp, indents []int) *LineWriter {
+ w := NewUTF8LineWriter(buf, width)
+ if lp.line != "" || lp.para != "" {
+ if err := w.SetLineTerminator(lp.line); err != nil {
+ t.Errorf("SetLineTerminator(%q) got %v, want nil", lp.line, err)
+ }
+ if err := w.SetParagraphSeparator(lp.para); err != nil {
+ t.Errorf("SetParagraphSeparator(%q) got %v, want nil", lp.para, err)
+ }
+ }
+ if indents != nil {
+ indentStrs := make([]string, len(indents))
+ for ix, indent := range indents {
+ indentStrs[ix] = spaces(indent)
+ }
+ if err := w.SetIndents(indentStrs...); err != nil {
+ t.Errorf("SetIndents(%v) got %v, want nil", indentStrs, err)
+ }
+ }
+ return w
+}
+
+func lineWriterWriteFlush(t *testing.T, w *LineWriter, text string, sizes []int) {
+ // Write chunks of different sizes until we've exhausted the input.
+ remain := text
+ for ix := 0; len(remain) > 0; ix++ {
+ var chunk []byte
+ chunk, remain = nextChunk(remain, sizes, ix)
+ got, err := w.Write(chunk)
+ if want := len(chunk); got != want || err != nil {
+ t.Errorf("%q Write(%q) got (%d,%v), want (%d,nil)", text, chunk, got, err, want)
+ }
+ }
+ // Flush the writer.
+ if err := w.Flush(); err != nil {
+ t.Errorf("%q Flush() got %v, want nil", text, err)
+ }
+}
diff --git a/textutil/rune.go b/textutil/rune.go
new file mode 100644
index 0000000..57db02e
--- /dev/null
+++ b/textutil/rune.go
@@ -0,0 +1,119 @@
+package textutil
+
+import (
+ "bytes"
+)
+
+// TODO(toddw): Add UTF16 support.
+
+const (
+ EOF = rune(-1) // Indicates the end of a rune stream.
+ LineSeparator = '\u2028' // Unicode line separator rune.
+ ParagraphSeparator = '\u2029' // Unicode paragraph separator rune.
+)
+
+// RuneEncoder is the interface to an encoder of a stream of runes into
+// bytes.Buffer.
+type RuneEncoder interface {
+ // Encode encodes r into buf.
+ Encode(r rune, buf *bytes.Buffer)
+}
+
+// RuneStreamDecoder is the interface to a decoder of a contiguous stream of
+// runes.
+type RuneStreamDecoder interface {
+ // Next returns the next rune. Invalid encodings are returned as U+FFFD.
+ // Returns EOF at the end of the stream.
+ Next() rune
+ // BytePos returns the current byte position in the original data buffer.
+ BytePos() int
+}
+
+// RuneChunkDecoder is the interface to a decoder of a stream of encoded runes
+// that may be arbitrarily chunked.
+//
+// Implementations of RuneChunkDecoder are commonly used to implement io.Writer
+// wrappers, to handle buffering when chunk boundaries may occur in the middle
+// of an encoded rune.
+type RuneChunkDecoder interface {
+ // Decode returns a RuneStreamDecoder that decodes the data chunk. Call Next
+ // repeatedly on the returned stream until it returns EOF to decode the chunk.
+ Decode(chunk []byte) RuneStreamDecoder
+ // DecodeLeftover returns a RuneStreamDecoder that decodes leftover buffered
+ // data. Call Next repeatedly on the returned stream until it returns EOF to
+ // ensure all buffered data is processed.
+ DecodeLeftover() RuneStreamDecoder
+}
+
+// RuneChunkWrite is a helper that calls d.Decode(data) and repeatedly calls
+// Next in a loop, calling fn for every rune that is decoded. Returns the
+// number of bytes in data that were successfully processed. If fn returns an
+// error, Write will return with that error, without processing any more data.
+//
+// This is a convenience for implementing io.Writer, given a RuneChunkDecoder.
+func RuneChunkWrite(d RuneChunkDecoder, fn func(rune) error, data []byte) (int, error) {
+ stream := d.Decode(data)
+ for r := stream.Next(); r != EOF; r = stream.Next() {
+ if err := fn(r); err != nil {
+ return stream.BytePos(), err
+ }
+ }
+ return stream.BytePos(), nil
+}
+
+// RuneChunkFlush is a helper that calls d.DecodeLeftover and repeatedly calls
+// Next in a loop, calling fn for every rune that is decoded. If fn returns an
+// error, Flush will return with that error, without processing any more data.
+//
+// This is a convenience for implementing an additional Flush() call on an
+// implementation of io.Writer, given a RuneChunkDecoder.
+func RuneChunkFlush(d RuneChunkDecoder, fn func(rune) error) error {
+ stream := d.DecodeLeftover()
+ for r := stream.Next(); r != EOF; r = stream.Next() {
+ if err := fn(r); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// bytePos and runePos distinguish positions that are used in either domain;
+// we're trying to avoid silly mistakes like adding a bytePos to a runePos.
+type bytePos int
+type runePos int
+
+// byteRuneBuffer maintains a buffer with both byte and rune based positions.
+type byteRuneBuffer struct {
+ enc RuneEncoder
+ buf bytes.Buffer
+ runeLen runePos
+}
+
+func (b *byteRuneBuffer) ByteLen() bytePos { return bytePos(b.buf.Len()) }
+func (b *byteRuneBuffer) RuneLen() runePos { return b.runeLen }
+func (b *byteRuneBuffer) Bytes() []byte { return b.buf.Bytes() }
+
+func (b *byteRuneBuffer) Reset() {
+ b.buf.Reset()
+ b.runeLen = 0
+}
+
+// WriteRune writes r into b.
+func (b *byteRuneBuffer) WriteRune(r rune) {
+ b.enc.Encode(r, &b.buf)
+ b.runeLen++
+}
+
+// WriteString writes str into b.
+func (b *byteRuneBuffer) WriteString(str string) {
+ for _, r := range str {
+ b.WriteRune(r)
+ }
+}
+
+// WriteString0Runes writes str into b, not incrementing the rune length.
+func (b *byteRuneBuffer) WriteString0Runes(str string) {
+ for _, r := range str {
+ b.enc.Encode(r, &b.buf)
+ }
+}
diff --git a/textutil/utf8.go b/textutil/utf8.go
new file mode 100644
index 0000000..349033e
--- /dev/null
+++ b/textutil/utf8.go
@@ -0,0 +1,167 @@
+package textutil
+
+import (
+ "bytes"
+ "fmt"
+ "unicode/utf8"
+)
+
+// UTF8Encoder implements RuneEncoder for the UTF-8 encoding.
+type UTF8Encoder struct{}
+
+var _ RuneEncoder = UTF8Encoder{}
+
+// Encode encodes r into buf in the UTF-8 encoding.
+func (UTF8Encoder) Encode(r rune, buf *bytes.Buffer) { buf.WriteRune(r) }
+
+// UTF8ChunkDecoder implements RuneChunkDecoder for a stream of UTF-8 data that
+// is arbitrarily chunked.
+//
+// UTF-8 is a byte-wise encoding that may use multiple bytes to encode a single
+// rune. This decoder buffers partial runes that have been split across chunks,
+// so that a full rune is returned when the subsequent data chunk is provided.
+//
+// This is commonly used to implement an io.Writer wrapper over UTF-8 text. It
+// is useful since the data provided to Write calls may be arbitrarily chunked.
+//
+// The zero UTF8ChunkDecoder is a decoder with an empty buffer.
+type UTF8ChunkDecoder struct {
+ // The only state we keep is the last partial rune we've encountered.
+ partial [utf8.UTFMax]byte
+ partialLen int
+}
+
+var _ RuneChunkDecoder = (*UTF8ChunkDecoder)(nil)
+
+// Decode returns a RuneStreamDecoder that decodes the data chunk. Call Next
+// repeatedly on the returned stream until it returns EOF to decode the chunk.
+//
+// If the data is chunked in the middle of an encoded rune, the final partial
+// rune in the chunk will be buffered, and the next call to Decode will continue
+// by combining the buffered data with the next chunk.
+//
+// Invalid encodings are transformed into U+FFFD, one byte at a time. See
+// unicode/utf8.DecodeRune for details.
+func (d *UTF8ChunkDecoder) Decode(chunk []byte) RuneStreamDecoder {
+ return &utf8Stream{d, chunk, 0}
+}
+
+// DecodeLeftover returns a RuneStreamDecoder that decodes leftover buffered
+// data. Call Next repeatedly on the returned stream until it returns EOF to
+// ensure all buffered data is processed.
+//
+// Since the only data that is buffered is the final partial rune, the returned
+// RuneStreamDecoder will only contain U+FFFD or EOF.
+func (d *UTF8ChunkDecoder) DecodeLeftover() RuneStreamDecoder {
+ return &utf8LeftoverStream{d, 0}
+}
+
+// nextRune decodes the next rune, logically combining any previously buffered
+// data with the data chunk. It returns the decoded rune and the byte size of
+// the data that was used for the decoding.
+//
+// The returned size may be > 0 even if the returned rune == EOF, if a partial
+// rune was detected and buffered. The returned size may be 0 even if the
+// returned rune != EOF, if previously buffered data was decoded.
+func (d *UTF8ChunkDecoder) nextRune(data []byte) (rune, int) {
+ if d.partialLen > 0 {
+ return d.nextRunePartial(data)
+ }
+ r, size := utf8.DecodeRune(data)
+ if r == utf8.RuneError && !utf8.FullRune(data) {
+ // Initialize the partial rune buffer with remaining data.
+ d.partialLen = copy(d.partial[:], data)
+ return d.verifyPartial(d.partialLen, data)
+ }
+ return r, size
+}
+
+// nextRunePartial implements nextRune when there is a previously buffered
+// partial rune.
+func (d *UTF8ChunkDecoder) nextRunePartial(data []byte) (rune, int) {
+ // Append as much data as we can to the partial rune, and see if it's full.
+ oldLen := d.partialLen
+ d.partialLen += copy(d.partial[oldLen:], data)
+ if !utf8.FullRune(d.partial[:d.partialLen]) {
+ // We still don't have a full rune - keep waiting.
+ return d.verifyPartial(d.partialLen-oldLen, data)
+ }
+ // We finally have a full rune.
+ r, size := utf8.DecodeRune(d.partial[:d.partialLen])
+ if size < oldLen {
+ // This occurs when we have a multi-byte rune that has the right number of
+ // bytes, but is an invalid code point.
+ //
+ // Say oldLen=2, and we just received the third byte of a 3-byte rune which
+ // isn't a UTF-8 trailing byte. In this case utf8.DecodeRune returns U+FFFD
+ // and size=1, to indicate we should skip the first byte.
+ //
+ // We shift the unread portion of the old partial data forward, and update
+ // the partial len so that it's strictly decreasing. The strictly
+ // decreasing property isn't necessary for correctness, but helps avoid
+ // repeatedly copying data into the partial buffer unecessarily.
+ copy(d.partial[:], d.partial[size:oldLen])
+ d.partialLen = oldLen - size
+ return r, 0
+ }
+ // We've used all the old buffered data; start decoding directly from data.
+ d.partialLen = 0
+ return r, size - oldLen
+}
+
+// verifyPartial is called when we don't have a full rune, and ncopy bytes have
+// been copied from data into the decoder partial rune buffer. We expect that
+// all data has been buffered and we return EOF and the total size of the data.
+func (d *UTF8ChunkDecoder) verifyPartial(ncopy int, data []byte) (rune, int) {
+ if ncopy < len(data) {
+ // Something's very wrong if we managed to fill d.partial without copying
+ // all the data; any sequence of utf8.UTFMax bytes must be a full rune.
+ panic(fmt.Errorf("UTF8ChunkDecoder: partial rune %v with leftover data %v", d.partial[:d.partialLen], data[ncopy:]))
+ }
+ return EOF, len(data)
+}
+
+// utf8Stream implements UTF8ChunkDecoder.Decode.
+type utf8Stream struct {
+ d *UTF8ChunkDecoder
+ data []byte
+ pos int
+}
+
+var _ RuneStreamDecoder = (*utf8Stream)(nil)
+
+func (s *utf8Stream) Next() rune {
+ if s.pos == len(s.data) {
+ return EOF
+ }
+ r, size := s.d.nextRune(s.data[s.pos:])
+ s.pos += size
+ return r
+}
+
+func (s *utf8Stream) BytePos() int {
+ return s.pos
+}
+
+// utf8LeftoverStream implements UTF8ChunkDecoder.DecodeLeftover.
+type utf8LeftoverStream struct {
+ d *UTF8ChunkDecoder
+ pos int
+}
+
+var _ RuneStreamDecoder = (*utf8LeftoverStream)(nil)
+
+func (s *utf8LeftoverStream) Next() rune {
+ if s.d.partialLen == 0 {
+ return EOF
+ }
+ r, size := utf8.DecodeRune(s.d.partial[:s.d.partialLen])
+ copy(s.d.partial[:], s.d.partial[size:])
+ s.d.partialLen -= size
+ s.pos += size
+ return r
+}
+
+func (s *utf8LeftoverStream) BytePos() int {
+ return s.pos
+}
diff --git a/textutil/utf8_test.go b/textutil/utf8_test.go
new file mode 100644
index 0000000..7517bef
--- /dev/null
+++ b/textutil/utf8_test.go
@@ -0,0 +1,110 @@
+package textutil
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestUTF8ChunkDecoder(t *testing.T) {
+ r2 := "Δ"
+ r3 := "王"
+ r4 := "\U0001F680"
+ tests := []struct {
+ Text string
+ Want []rune
+ }{
+ {"", nil},
+ {"a", []rune{'a'}},
+ {"abc", []rune{'a', 'b', 'c'}},
+ {"abc def ghi", []rune{'a', 'b', 'c', ' ', 'd', 'e', 'f', ' ', 'g', 'h', 'i'}},
+ // 2-byte runes.
+ {"ΔΘΠΣΦ", []rune{'Δ', 'Θ', 'Π', 'Σ', 'Φ'}},
+ // 3-byte runes.
+ {"王普澤世界", []rune{'王', '普', '澤', '世', '界'}},
+ // 4-byte runes.
+ {"\U0001F680\U0001F681\U0001F682\U0001F683", []rune{'\U0001F680', '\U0001F681', '\U0001F682', '\U0001F683'}},
+ // Mixed-bytes.
+ {"aΔ王\U0001F680æ™®Θb", []rune{'a', 'Δ', '王', '\U0001F680', 'æ™®', 'Θ', 'b'}},
+ // Error runes translated to U+FFFD.
+ {"\uFFFD", []rune{'\uFFFD'}},
+ {"a\uFFFDb", []rune{'a', '\uFFFD', 'b'}},
+ {"\xFF", []rune{'\uFFFD'}},
+ {"a\xFFb", []rune{'a', '\uFFFD', 'b'}},
+ // Multi-byte full runes.
+ {r2, []rune{[]rune(r2)[0]}},
+ {r3, []rune{[]rune(r3)[0]}},
+ {r4, []rune{[]rune(r4)[0]}},
+ // Partial runes translated to U+FFFD.
+ {r2[:1], []rune{'\uFFFD'}},
+ {r3[:1], []rune{'\uFFFD'}},
+ {r3[:2], []rune{'\uFFFD', '\uFFFD'}},
+ {r4[:1], []rune{'\uFFFD'}},
+ {r4[:2], []rune{'\uFFFD', '\uFFFD'}},
+ {r4[:3], []rune{'\uFFFD', '\uFFFD', '\uFFFD'}},
+ // Leading partial runes translated to U+FFFD.
+ {r2[:1] + "b", []rune{'\uFFFD', 'b'}},
+ {r3[:1] + "b", []rune{'\uFFFD', 'b'}},
+ {r3[:2] + "b", []rune{'\uFFFD', '\uFFFD', 'b'}},
+ {r4[:1] + "b", []rune{'\uFFFD', 'b'}},
+ {r4[:2] + "b", []rune{'\uFFFD', '\uFFFD', 'b'}},
+ {r4[:3] + "b", []rune{'\uFFFD', '\uFFFD', '\uFFFD', 'b'}},
+ // Trailing partial runes translated to U+FFFD.
+ {"a" + r2[:1], []rune{'a', '\uFFFD'}},
+ {"a" + r3[:1], []rune{'a', '\uFFFD'}},
+ {"a" + r3[:2], []rune{'a', '\uFFFD', '\uFFFD'}},
+ {"a" + r4[:1], []rune{'a', '\uFFFD'}},
+ {"a" + r4[:2], []rune{'a', '\uFFFD', '\uFFFD'}},
+ {"a" + r4[:3], []rune{'a', '\uFFFD', '\uFFFD', '\uFFFD'}},
+ // Bracketed partial runes translated to U+FFFD.
+ {"a" + r2[:1] + "b", []rune{'a', '\uFFFD', 'b'}},
+ {"a" + r3[:1] + "b", []rune{'a', '\uFFFD', 'b'}},
+ {"a" + r3[:2] + "b", []rune{'a', '\uFFFD', '\uFFFD', 'b'}},
+ {"a" + r4[:1] + "b", []rune{'a', '\uFFFD', 'b'}},
+ {"a" + r4[:2] + "b", []rune{'a', '\uFFFD', '\uFFFD', 'b'}},
+ {"a" + r4[:3] + "b", []rune{'a', '\uFFFD', '\uFFFD', '\uFFFD', 'b'}},
+ }
+ for _, test := range tests {
+ // Run with a variety of chunk sizes.
+ for _, sizes := range [][]int{nil, {1}, {2}, {1, 2}, {2, 1}, {3}, {1, 2, 3}} {
+ got := runeChunkWriteFlush(t, test.Text, sizes)
+ if want := test.Want; !reflect.DeepEqual(got, want) {
+ t.Errorf("%q got %v, want %v", test.Text, got, want)
+ }
+ }
+ }
+}
+
+func runeChunkWriteFlush(t *testing.T, text string, sizes []int) []rune {
+ var dec UTF8ChunkDecoder
+ var runes []rune
+ addRune := func(r rune) error {
+ runes = append(runes, r)
+ return nil
+ }
+ // Write chunks of different sizes until we've exhausted the input text.
+ remain := text
+ for ix := 0; len(remain) > 0; ix++ {
+ var chunk []byte
+ chunk, remain = nextChunk(remain, sizes, ix)
+ got, err := RuneChunkWrite(&dec, addRune, chunk)
+ if want := len(chunk); got != want || err != nil {
+ t.Errorf("%q RuneChunkWrite(%q) got (%d,%v), want (%d,nil)", text, chunk, got, err, want)
+ }
+ }
+ // Flush the decoder.
+ if err := RuneChunkFlush(&dec, addRune); err != nil {
+ t.Errorf("%q RuneChunkFlush got %v, want nil", text, err)
+ }
+ return runes
+}
+
+func nextChunk(text string, sizes []int, index int) (chunk []byte, remain string) {
+ if len(sizes) == 0 {
+ return []byte(text), ""
+ }
+ size := sizes[index%len(sizes)]
+ if size >= len(text) {
+ return []byte(text), ""
+ }
+ return []byte(text[:size]), text[size:]
+}
diff --git a/textutil/util.go b/textutil/util.go
new file mode 100644
index 0000000..85402de
--- /dev/null
+++ b/textutil/util.go
@@ -0,0 +1,40 @@
+package textutil
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+// TerminalSize returns the dimensions of the terminal, if it's available from
+// the OS, otherwise returns an error.
+func TerminalSize() (row, col int, _ error) {
+ // Try getting the terminal size from stdout, stderr and stdin respectively.
+ // We try each of these in turn because the mechanism we're using fails if any
+ // of the fds is redirected on the command line. E.g. "tool | less" redirects
+ // the stdout of tool to the stdin of less, and will mean tool cannot retrieve
+ // the terminal size from stdout.
+ //
+ // TODO(toddw): This probably only works on some linux / unix variants; add
+ // build tags and support different platforms.
+ if row, col, err := terminalSize(syscall.Stdout); err == nil {
+ return row, col, err
+ }
+ if row, col, err := terminalSize(syscall.Stderr); err == nil {
+ return row, col, err
+ }
+ return terminalSize(syscall.Stdin)
+}
+
+func terminalSize(fd int) (int, int, error) {
+ var ws winsize
+ if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&ws))); err != 0 {
+ return 0, 0, err
+ }
+ return int(ws.row), int(ws.col), nil
+}
+
+// winsize must correspond to the struct defined in "sys/ioctl.h". Do not
+// export this struct; it's a platform-specific implementation detail.
+type winsize struct {
+ row, col, xpixel, ypixel uint16
+}
diff --git a/textutil/writer.go b/textutil/writer.go
new file mode 100644
index 0000000..59a5efe
--- /dev/null
+++ b/textutil/writer.go
@@ -0,0 +1,54 @@
+package textutil
+
+import (
+ "bytes"
+ "io"
+)
+
+// PrefixWriter returns an io.Writer that wraps w, where the prefix is written
+// out immediately before the first non-empty Write call.
+func PrefixWriter(w io.Writer, prefix string) io.Writer {
+ return &prefixWriter{w, []byte(prefix)}
+}
+
+type prefixWriter struct {
+ w io.Writer
+ prefix []byte
+}
+
+func (w *prefixWriter) Write(data []byte) (int, error) {
+ if w.prefix != nil && len(data) > 0 {
+ w.w.Write(w.prefix)
+ w.prefix = nil
+ }
+ return w.w.Write(data)
+}
+
+// ByteReplaceWriter returns an io.Writer that wraps w, where all occurrences of
+// the old byte are replaced with the new string on Write calls.
+func ByteReplaceWriter(w io.Writer, old byte, new string) io.Writer {
+ return &byteReplaceWriter{w, []byte{old}, []byte(new)}
+}
+
+type byteReplaceWriter struct {
+ w io.Writer
+ old, new []byte
+}
+
+func (w *byteReplaceWriter) Write(data []byte) (int, error) {
+ replaced := bytes.Replace(data, w.old, w.new, -1)
+ if len(replaced) == 0 {
+ return len(data), nil
+ }
+ // Write the replaced data, and return the number of bytes in data that were
+ // written out, based on the proportion of replaced data written. The
+ // important boundary cases are:
+ // If all replaced data was written, we return n=len(data).
+ // If not all replaced data was written, we return n<len(data).
+ n, err := w.w.Write(replaced)
+ return n * len(data) / len(replaced), err
+}
+
+// TODO(toddw): Add ReplaceWriter, which performs arbitrary string replacements.
+// This will need to buffer data and have an extra Flush() method, since the old
+// string may match across successive Write calls.
diff --git a/textutil/writer_test.go b/textutil/writer_test.go
new file mode 100644
index 0000000..37c5f1d
--- /dev/null
+++ b/textutil/writer_test.go
@@ -0,0 +1,90 @@
+package textutil
+
+import (
+ "bytes"
+ "fmt"
+ "testing"
+)
+
+func TestPrefixWriter(t *testing.T) {
+ tests := []struct {
+ Prefix string
+ Writes []string
+ Want string
+ }{
+ {"", nil, ""},
+ {"", []string{""}, ""},
+ {"", []string{"a"}, "a"},
+ {"", []string{"a", ""}, "a"},
+ {"", []string{"", "a"}, "a"},
+ {"", []string{"a", "b"}, "ab"},
+ {"PRE", nil, ""},
+ {"PRE", []string{""}, ""},
+ {"PRE", []string{"a"}, "PREa"},
+ {"PRE", []string{"a", ""}, "PREa"},
+ {"PRE", []string{"", "a"}, "PREa"},
+ {"PRE", []string{"a", "b"}, "PREab"},
+ }
+ for _, test := range tests {
+ var buf bytes.Buffer
+ w := PrefixWriter(&buf, test.Prefix)
+ for _, write := range test.Writes {
+ name := fmt.Sprintf("(%v, %v)", test.Want, write)
+ n, err := w.Write([]byte(write))
+ if got, want := n, len(write); got != want {
+ t.Errorf("%s got len %d, want %d", name, got, want)
+ }
+ if err != nil {
+ t.Errorf("%s got error: %v", name, err)
+ }
+ }
+ if got, want := buf.String(), test.Want; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ }
+}
+
+func TestByteReplaceWriter(t *testing.T) {
+ tests := []struct {
+ Old byte
+ New string
+ Writes []string
+ Want string
+ }{
+ {'a', "", nil, ""},
+ {'a', "", []string{""}, ""},
+ {'a', "", []string{"a"}, ""},
+ {'a', "", []string{"b"}, "b"},
+ {'a', "", []string{"aba"}, "b"},
+ {'a', "", []string{"aba", "bab"}, "bbb"},
+ {'a', "X", nil, ""},
+ {'a', "X", []string{""}, ""},
+ {'a', "X", []string{"a"}, "X"},
+ {'a', "X", []string{"b"}, "b"},
+ {'a', "X", []string{"aba"}, "XbX"},
+ {'a', "X", []string{"aba", "bab"}, "XbXbXb"},
+ {'a', "ZZZ", nil, ""},
+ {'a', "ZZZ", []string{""}, ""},
+ {'a', "ZZZ", []string{"a"}, "ZZZ"},
+ {'a', "ZZZ", []string{"b"}, "b"},
+ {'a', "ZZZ", []string{"aba"}, "ZZZbZZZ"},
+ {'a', "ZZZ", []string{"aba", "bab"}, "ZZZbZZZbZZZb"},
+ }
+ for _, test := range tests {
+ var buf bytes.Buffer
+ w := ByteReplaceWriter(&buf, test.Old, test.New)
+ for _, write := range test.Writes {
+ name := fmt.Sprintf("(%v, %v, %v, %v)", test.Old, test.New, test.Want, write)
+ n, err := w.Write([]byte(write))
+ if got, want := n, len(write); got != want {
+ t.Errorf("%s got len %d, want %d", name, got, want)
+ }
+ if err != nil {
+ t.Errorf("%s got error: %v", name, err)
+ }
+ }
+ if got, want := buf.String(), test.Want; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ }
+}
diff --git a/toposort/sort.go b/toposort/sort.go
new file mode 100644
index 0000000..26fdfa9
--- /dev/null
+++ b/toposort/sort.go
@@ -0,0 +1,132 @@
+// Package toposort implements topological sort. For details see:
+// http://en.wikipedia.org/wiki/Topological_sorting
+package toposort
+
+// Sorter implements a topological sorter. Add nodes and edges to the sorter to
+// describe the graph, and call Sort to retrieve topologically-sorted nodes.
+// The zero Sorter describes an empty graph.
+type Sorter struct {
+ values map[interface{}]int // maps from user-provided value to index in nodes
+ nodes []*node // the graph to sort
+}
+
+// node represents a node in the graph.
+type node struct {
+ value interface{}
+ children []*node
+}
+
+func (s *Sorter) getOrAddNode(value interface{}) *node {
+ if s.values == nil {
+ s.values = make(map[interface{}]int)
+ }
+ if index, ok := s.values[value]; ok {
+ return s.nodes[index]
+ }
+ s.values[value] = len(s.nodes)
+ newNode := &node{value: value}
+ s.nodes = append(s.nodes, newNode)
+ return newNode
+}
+
+// AddNode adds a node. Arbitrary value types are supported, but the values
+// must be comparable; they'll be used as map keys. Typically this is only used
+// to add nodes with no incoming or outgoing edges.
+func (s *Sorter) AddNode(value interface{}) {
+ s.getOrAddNode(value)
+}
+
+// AddEdge adds nodes from and to, and adds an edge from -> to. You don't need
+// to call AddNode first; the nodes will be implicitly added if they don't
+// already exist. The direction means that from depends on to; i.e. to will
+// appear before from in the sorted output. Cycles are allowed.
+func (s *Sorter) AddEdge(from interface{}, to interface{}) {
+ fromN, toN := s.getOrAddNode(from), s.getOrAddNode(to)
+ fromN.children = append(fromN.children, toN)
+}
+
+// Sort returns the topologically sorted nodes, along with some of the cycles
+// (if any) that were encountered. You're guaranteed that len(cycles)==0 iff
+// there are no cycles in the graph, otherwise an arbitrary (but non-empty) list
+// of cycles is returned.
+//
+// If there are cycles the sorting is best-effort; portions of the graph that
+// are acyclic will still be ordered correctly, and the cyclic portions have an
+// arbitrary ordering.
+//
+// Sort is deterministic; given the same sequence of inputs it always returns
+// the same output, even if the inputs are only partially ordered.
+func (s *Sorter) Sort() (sorted []interface{}, cycles [][]interface{}) {
+ // The strategy is the standard simple approach of performing DFS on the
+ // graph. Details are outlined in the above wikipedia article.
+ done := make(map[*node]bool)
+ for _, n := range s.nodes {
+ cycles = appendCycles(cycles, n.visit(done, make(map[*node]bool), &sorted))
+ }
+ return
+}
+
+// visit performs DFS on the graph, and fills in sorted and cycles as it
+// traverses. We use done to indicate a node has been fully explored, and
+// visiting to indicate a node is currently being explored.
+//
+// The cycle collection strategy is to wait until we've hit a repeated node in
+// visiting, and add that node to cycles and return. Thereafter as the
+// recursive stack is unwound, nodes append themselves to the end of each cycle,
+// until we're back at the repeated node. This guarantees that if the graph is
+// cyclic we'll return at least one of the cycles.
+func (n *node) visit(done, visiting map[*node]bool, sorted *[]interface{}) (cycles [][]interface{}) {
+ if done[n] {
+ return
+ }
+ if visiting[n] {
+ cycles = [][]interface{}{{n.value}}
+ return
+ }
+ visiting[n] = true
+ for _, child := range n.children {
+ cycles = appendCycles(cycles, child.visit(done, visiting, sorted))
+ }
+ done[n] = true
+ *sorted = append(*sorted, n.value)
+ // Update cycles. If it's empty none of our children detected any cycles, and
+ // there's nothing to do. Otherwise we append ourselves to the cycle, iff the
+ // cycle hasn't completed yet. We know the cycle has completed if the first
+ // and last item in the cycle are the same, with an exception for the single
+ // item case; self-cycles are represented as the same node appearing twice.
+ for cx := range cycles {
+ len := len(cycles[cx])
+ if len == 1 || cycles[cx][0] != cycles[cx][len-1] {
+ cycles[cx] = append(cycles[cx], n.value)
+ }
+ }
+ return
+}
+
+// appendCycles returns the combined cycles in a and b.
+func appendCycles(a [][]interface{}, b [][]interface{}) [][]interface{} {
+ for _, bcycle := range b {
+ a = append(a, bcycle)
+ }
+ return a
+}
+
+// DumpCycles dumps the cycles returned from Sorter.Sort, using toString to
+// convert each node into a string.
+func DumpCycles(cycles [][]interface{}, toString func(n interface{}) string) string {
+ var str string
+ for cyclex, cycle := range cycles {
+ if cyclex > 0 {
+ str += " "
+ }
+ str += "["
+ for nodex, node := range cycle {
+ if nodex > 0 {
+ str += " <= "
+ }
+ str += toString(node)
+ }
+ str += "]"
+ }
+ return str
+}
diff --git a/toposort/sort_test.go b/toposort/sort_test.go
new file mode 100644
index 0000000..f33fabc
--- /dev/null
+++ b/toposort/sort_test.go
@@ -0,0 +1,215 @@
+package toposort
+
+import (
+ "reflect"
+ "testing"
+)
+
+func toStringSlice(input []interface{}) (output []string) {
+ output = make([]string, len(input))
+ for ix, ival := range input {
+ output[ix] = ival.(string)
+ }
+ return
+}
+
+func toStringCycles(input [][]interface{}) (output [][]string) {
+ output = make([][]string, len(input))
+ for ix, islice := range input {
+ output[ix] = toStringSlice(islice)
+ }
+ return
+}
+
+type orderChecker struct {
+ t *testing.T
+ original []string
+ orderMap map[string]int
+}
+
+func makeOrderChecker(t *testing.T, slice []interface{}) orderChecker {
+ result := orderChecker{t, toStringSlice(slice), make(map[string]int)}
+ for ix, val := range result.original {
+ result.orderMap[val] = ix
+ }
+ return result
+}
+
+func (oc *orderChecker) findValue(val string) int {
+ if index, ok := oc.orderMap[val]; ok {
+ return index
+ }
+ oc.t.Errorf("Couldn't find val %v in slice %v", val, oc.original)
+ return -1
+}
+
+func (oc *orderChecker) expectOrder(before, after string) {
+ if oc.findValue(before) >= oc.findValue(after) {
+ oc.t.Errorf("Expected %v before %v, slice %v", before, after, oc.original)
+ }
+}
+
+// Since sort is deterministic we can expect a particular total order, in
+// addition to the partial order checks.
+func (oc *orderChecker) expectTotalOrder(expect ...string) {
+ if !reflect.DeepEqual(oc.original, expect) {
+ oc.t.Errorf("Expected order %v, actual %v", expect, oc.original)
+ }
+}
+
+func expectCycles(t *testing.T, actual [][]interface{}, expect [][]string) {
+ actualStr := toStringCycles(actual)
+ if !reflect.DeepEqual(actualStr, expect) {
+ t.Errorf("Expected cycles %v, actual %v", expect, actualStr)
+ }
+}
+
+func TestSortDag(t *testing.T) {
+ // This is the graph:
+ // ,-->B
+ // |
+ // A-->C---->D
+ // | \
+ // | `-->E--.
+ // `-------------`-->F
+ var sorter Sorter
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("A", "C")
+ sorter.AddEdge("A", "F")
+ sorter.AddEdge("C", "D")
+ sorter.AddEdge("C", "E")
+ sorter.AddEdge("E", "F")
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectOrder("B", "A")
+ oc.expectOrder("C", "A")
+ oc.expectOrder("D", "A")
+ oc.expectOrder("E", "A")
+ oc.expectOrder("F", "A")
+ oc.expectOrder("D", "C")
+ oc.expectOrder("E", "C")
+ oc.expectOrder("F", "C")
+ oc.expectOrder("F", "E")
+ oc.expectTotalOrder("B", "D", "F", "E", "C", "A")
+ expectCycles(t, cycles, [][]string{})
+}
+
+func TestSortSelfCycle(t *testing.T) {
+ // This is the graph:
+ // ,---.
+ // | |
+ // A<--'
+ var sorter Sorter
+ sorter.AddEdge("A", "A")
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectTotalOrder("A")
+ expectCycles(t, cycles, [][]string{{"A", "A"}})
+}
+
+func TestSortCycle(t *testing.T) {
+ // This is the graph:
+ // ,-->B-->C
+ // | |
+ // A<------'
+ var sorter Sorter
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("B", "C")
+ sorter.AddEdge("C", "A")
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectTotalOrder("C", "B", "A")
+ expectCycles(t, cycles, [][]string{{"A", "C", "B", "A"}})
+}
+
+func TestSortContainsCycle1(t *testing.T) {
+ // This is the graph:
+ // ,-->B
+ // | ,-----.
+ // | v |
+ // A-->C---->D
+ // | \
+ // | `-->E--.
+ // `-------------`-->F
+ var sorter Sorter
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("A", "C")
+ sorter.AddEdge("A", "F")
+ sorter.AddEdge("C", "D")
+ sorter.AddEdge("C", "E")
+ sorter.AddEdge("D", "C") // creates the cycle
+ sorter.AddEdge("E", "F")
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectOrder("B", "A")
+ oc.expectOrder("C", "A")
+ oc.expectOrder("D", "A")
+ oc.expectOrder("E", "A")
+ oc.expectOrder("F", "A")
+ // The difference with the dag is C, D may be in either order.
+ oc.expectOrder("E", "C")
+ oc.expectOrder("F", "C")
+ oc.expectOrder("F", "E")
+ oc.expectTotalOrder("B", "D", "F", "E", "C", "A")
+ expectCycles(t, cycles, [][]string{{"C", "D", "C"}})
+}
+
+func TestSortContainsCycle2(t *testing.T) {
+ // This is the graph:
+ // ,-->B
+ // | ,-------------.
+ // | v |
+ // A-->C---->D |
+ // | \ |
+ // | `-->E--. |
+ // `-------------`-->F
+ var sorter Sorter
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("A", "C")
+ sorter.AddEdge("A", "F")
+ sorter.AddEdge("C", "D")
+ sorter.AddEdge("C", "E")
+ sorter.AddEdge("E", "F")
+ sorter.AddEdge("F", "C") // creates the cycle
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectOrder("B", "A")
+ oc.expectOrder("C", "A")
+ oc.expectOrder("D", "A")
+ oc.expectOrder("E", "A")
+ oc.expectOrder("F", "A")
+ oc.expectOrder("D", "C")
+ // The difference with the dag is C, E, F may be in any order.
+ oc.expectTotalOrder("B", "D", "F", "E", "C", "A")
+ expectCycles(t, cycles, [][]string{{"C", "F", "E", "C"}})
+}
+
+func TestSortMultiCycles(t *testing.T) {
+ // This is the graph:
+ // ,-->B
+ // | ,------------.
+ // | v |
+ // .--A-->C---->D |
+ // | ^ \ |
+ // | | `-->E--. |
+ // | | | | |
+ // | `---------' | |
+ // `---------------`-->F
+ var sorter Sorter
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("A", "C")
+ sorter.AddEdge("A", "F")
+ sorter.AddEdge("C", "D")
+ sorter.AddEdge("C", "E")
+ sorter.AddEdge("E", "A") // creates a cycle
+ sorter.AddEdge("E", "F")
+ sorter.AddEdge("F", "C") // creates a cycle
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectOrder("B", "A")
+ oc.expectOrder("D", "A")
+ oc.expectOrder("F", "A")
+ oc.expectOrder("D", "C")
+ oc.expectTotalOrder("B", "D", "F", "E", "C", "A")
+ expectCycles(t, cycles, [][]string{{"A", "E", "C", "A"}, {"C", "F", "E", "C"}})
+}
diff --git a/vlog/GO.PACKAGE b/vlog/GO.PACKAGE
new file mode 100644
index 0000000..0d3f6ae
--- /dev/null
+++ b/vlog/GO.PACKAGE
@@ -0,0 +1,7 @@
+{
+ "dependencies": {
+ "outgoing": [
+ {"allow": "github.com/cosmosnicolaou/llog"}
+ ]
+ }
+}
diff --git a/vlog/doc.go b/vlog/doc.go
new file mode 100644
index 0000000..cf0fb3d
--- /dev/null
+++ b/vlog/doc.go
@@ -0,0 +1,69 @@
+// Package vlog defines and implements the veyron2 logging interfaces and
+// command line parsing. vlog is modeled on google3 and glog;
+// the differences from glog are:
+//
+// - interfaces are used to allow for multiple implementations and instances.
+// In particular, application and runtime logging can be separated.
+// We also expect to stream log messages to external log collectors rather
+// to local storage.
+// - the Warn family of methods are not provided; their main use within
+// google3 is to avoid the flush that's implicit in the Error routines
+// rather than any semantic difference between warnings and errors.
+// - Info logging and Event logging is separated with the former expected
+// to be somewhat spammy and the latter to be used sparingly. An error
+// message that occurs during execution of a test should be treated as
+// a failure of that test regardless of whether the test code itself
+// passes. TODO(cnicolaou,toddw): implement this.
+// - Event logging includes methods for unconditionally (i.e. regardless
+// of any command line options) logging the current goroutine's stack
+// or the stacks of all goroutines.
+// - The use of interfaces and encapsulated state means that a single
+// function (V) can no longer be used for 'if guarded' and 'chained' logging.
+// That is:
+// if vlog.V(1) { ... } and vlog.V(1).Infof( ... )
+// become
+// if logger.V(1) { ... } and logger.VI(1).Infof( ... )
+//
+// vlog also creates a global instance of the Logger (vlog.Log) and
+// provides command line flags (see flags.go). Parsing of these flags is
+// performed by calling one of ConfigureLibraryLoggerFromFlags or
+// ConfigureLoggerFromFlags .
+//
+// The supported flags are:
+//
+// -logtostderr=false
+// Logs are written to standard error instead of to files.
+// -alsologtostderr=false
+// Logs are written to standard error as well as to files.
+// -stderrthreshold=ERROR
+// Log events at or above this severity are logged to standard
+// error as well as to files.
+// -log_dir=""
+// Log files will be written to this directory instead of the
+// default temporary directory.
+//
+// Other flags provide aids to debugging.
+//
+// -log_backtrace_at=""
+// When set to a file and line number holding a logging statement,
+// such as
+// -log_backtrace_at=gopherflakes.go:234
+// a stack trace will be written to the Info log whenever execution
+// hits that statement. (Unlike with -vmodule, the ".go" must be
+// present.)
+// -v=0
+// Enable V-leveled logging at the specified level.
+// -vmodule=""
+// The syntax of the argument is a comma-separated list of pattern=N,
+// where pattern is a literal file name (minus the ".go" suffix) or
+// "glob" pattern and N is a V level. For instance,
+// -vmodule=gopher*=3
+// sets the V level to 3 in all Go files whose names begin "gopher".
+// -max_stack_buf_size=<size in bytes>
+// Set the max size (bytes) of the byte buffer to use for stack
+// traces. The default max is 4M; use powers of 2 since the
+// stack size will be grown exponentially until it exceeds the max.
+// A min of 128K is enforced and any attempts to reduce this will
+// be silently ignored.
+//
+package vlog
diff --git a/vlog/flags.go b/vlog/flags.go
new file mode 100644
index 0000000..28811f8
--- /dev/null
+++ b/vlog/flags.go
@@ -0,0 +1,109 @@
+package vlog
+
+import (
+ "flag"
+ "fmt"
+
+ "github.com/cosmosnicolaou/llog"
+)
+
+var (
+ toStderr bool
+ alsoToStderr bool
+ logDir string
+ verbosity Level
+ stderrThreshold StderrThreshold = StderrThreshold(llog.ErrorLog)
+ vmodule ModuleSpec
+ traceLocation TraceLocation
+ maxStackBufSize int
+)
+
+var flagDefs = []struct {
+ name string
+ variable interface{}
+ defaultValue interface{}
+ description string
+}{
+ {"log_dir", &logDir, "", "if non-empty, write log files to this directory"},
+ {"logtostderr", &toStderr, false, "log to standard error instead of files"},
+ {"alsologtostderr", &alsoToStderr, true, "log to standard error as well as files"},
+ {"max_stack_buf_size", &maxStackBufSize, 4192 * 1024, "max size in bytes of the buffer to use for logging stack traces"},
+ {"v", &verbosity, nil, "log level for V logs"},
+ {"stderrthreshold", &stderrThreshold, nil, "logs at or above this threshold go to stderr"},
+ {"vmodule", &vmodule, nil, "comma-separated list of pattern=N settings for file-filtered logging"},
+ {"log_backtrace_at", &traceLocation, nil, "when logging hits line file:N, emit a stack trace"},
+}
+
+func init() {
+ istest := false
+ if flag.CommandLine.Lookup("test.v") != nil {
+ istest = true
+ }
+ for _, flagDef := range flagDefs {
+ if istest && flagDef.name == "v" {
+ continue
+ }
+ switch v := flagDef.variable.(type) {
+ case *string:
+ flag.StringVar(v, flagDef.name,
+ flagDef.defaultValue.(string), flagDef.description)
+ case *bool:
+ flag.BoolVar(v, flagDef.name,
+ flagDef.defaultValue.(bool), flagDef.description)
+ case *int:
+ flag.IntVar(v, flagDef.name,
+ flagDef.defaultValue.(int), flagDef.description)
+ case flag.Value:
+ if flagDef.defaultValue != nil {
+ panic(fmt.Sprintf("default value not supported for flag %s", flagDef.name))
+ }
+ flag.Var(v, flagDef.name, flagDef.description)
+ default:
+ panic("invalid flag type")
+ }
+ }
+}
+
+// ConfigureLibraryLoggerFromFlags will configure the internal global logger
+// using command line flags. It assumes that flag.Parse() has already been
+// called to initialize the flag variables.
+func ConfigureLibraryLoggerFromFlags() error {
+ return ConfigureLoggerFromFlags(Log)
+}
+
+// ConfigureLoggerFromLogs will configure the supplied logger using
+// command line flags.
+func ConfigureLoggerFromFlags(l Logger) error {
+ return l.Configure(
+ LogToStderr(toStderr),
+ AlsoLogToStderr(alsoToStderr),
+ LogDir(logDir),
+ Level(verbosity),
+ StderrThreshold(stderrThreshold),
+ ModuleSpec(vmodule),
+ TraceLocation(traceLocation),
+ MaxStackBufSize(maxStackBufSize),
+ )
+}
+
+func (l *logger) String() string {
+ return l.log.String()
+}
+
+// ExplicitlySetFlags returns a map of the logging command line flags and their
+// values formatted as strings. Only the flags that were explicitly set are
+// returned. This is intended for use when an application needs to know what
+// value the flags were set to, for example when creating subprocesses.
+func (l *logger) ExplicitlySetFlags() map[string]string {
+ logFlagNames := make(map[string]bool)
+ for _, flagDef := range flagDefs {
+ logFlagNames[flagDef.name] = true
+ }
+ args := make(map[string]string)
+ flag.Visit(func(f *flag.Flag) {
+ if logFlagNames[f.Name] {
+ args[f.Name] = f.Value.String()
+ }
+ })
+ return args
+}
diff --git a/vlog/flags_test.go b/vlog/flags_test.go
new file mode 100644
index 0000000..1904b9c
--- /dev/null
+++ b/vlog/flags_test.go
@@ -0,0 +1,57 @@
+package vlog_test
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "v.io/x/ref/lib/modules"
+
+ "v.io/x/lib/vlog"
+)
+
+//go:generate v23 test generate
+
+func child(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+ tmp := filepath.Join(os.TempDir(), "foo")
+ flag.Set("log_dir", tmp)
+ flag.Set("vmodule", "foo=2")
+ flags := vlog.Log.ExplicitlySetFlags()
+ if v, ok := flags["log_dir"]; !ok || v != tmp {
+ return fmt.Errorf("log_dir was supposed to be %v", tmp)
+ }
+ if v, ok := flags["vmodule"]; !ok || v != "foo=2" {
+ return fmt.Errorf("vmodule was supposed to be foo=2")
+ }
+ if f := flag.Lookup("max_stack_buf_size"); f == nil {
+ return fmt.Errorf("max_stack_buf_size is not a flag")
+ }
+ maxStackBufSizeSet := false
+ flag.Visit(func(f *flag.Flag) {
+ if f.Name == "max_stack_buf_size" {
+ maxStackBufSizeSet = true
+ }
+ })
+ if v, ok := flags["max_stack_buf_size"]; ok && !maxStackBufSizeSet {
+ return fmt.Errorf("max_stack_buf_size unexpectedly set to %v", v)
+ }
+ return nil
+}
+
+func TestFlags(t *testing.T) {
+ sh, err := modules.NewShell(nil, nil)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ defer sh.Cleanup(nil, nil)
+ h, err := sh.Start("child", nil)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err = h.Shutdown(nil, os.Stderr); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
diff --git a/vlog/funcs.go b/vlog/funcs.go
new file mode 100644
index 0000000..394be7d
--- /dev/null
+++ b/vlog/funcs.go
@@ -0,0 +1,99 @@
+package vlog
+
+import (
+ "github.com/cosmosnicolaou/llog"
+)
+
+// Info logs to the INFO log.
+// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
+func Info(args ...interface{}) {
+ Log.log.Print(llog.InfoLog, args...)
+ Log.maybeFlush()
+}
+
+// Infof logs to the INFO log.
+// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+func Infof(format string, args ...interface{}) {
+ Log.log.Printf(llog.InfoLog, format, args...)
+ Log.maybeFlush()
+}
+
+// InfoStack logs the current goroutine's stack if the all parameter
+// is false, or the stacks of all goroutines if it's true.
+func InfoStack(all bool) {
+ infoStack(Log, all)
+}
+
+// V returns true if the configured logging level is greater than or equal to its parameter
+func V(level Level) bool {
+ return Log.log.V(llog.Level(level))
+}
+
+// VI is like V, except that it returns an instance of the Info
+// interface that will either log (if level >= the configured level)
+// or discard its parameters. This allows for logger.VI(2).Info
+// style usage.
+func VI(level Level) InfoLog {
+ if Log.log.V(llog.Level(level)) {
+ return Log
+ }
+ return &discardInfo{}
+}
+
+// Flush flushes all pending log I/O.
+func FlushLog() {
+ Log.FlushLog()
+}
+
+// Error logs to the ERROR and INFO logs.
+// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
+func Error(args ...interface{}) {
+ Log.log.Print(llog.ErrorLog, args...)
+ Log.maybeFlush()
+}
+
+// Errorf logs to the ERROR and INFO logs.
+// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+func Errorf(format string, args ...interface{}) {
+ Log.log.Printf(llog.ErrorLog, format, args...)
+ Log.maybeFlush()
+}
+
+// Fatal logs to the FATAL, ERROR and INFO logs,
+// including a stack trace of all running goroutines, then calls os.Exit(255).
+// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
+func Fatal(args ...interface{}) {
+ Log.log.Print(llog.FatalLog, args...)
+}
+
+// Fatalf logs to the FATAL, ERROR and INFO logs,
+// including a stack trace of all running goroutines, then calls os.Exit(255).
+// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+func Fatalf(format string, args ...interface{}) {
+ Log.log.Printf(llog.FatalLog, format, args...)
+}
+
+// ConfigureLogging configures all future logging. Some options
+// may not be usable if ConfigureLogging is called from an init function,
+// in which case an error will be returned. The Configured error is
+// returned if ConfigureLogger has already been called unless the
+// OverridePriorConfiguration options is included.
+func Configure(opts ...LoggingOpts) error {
+ return Log.Configure(opts...)
+}
+
+// Stats returns stats on how many lines/bytes haven been written to
+// this set of logs.
+func Stats() LevelStats {
+ return Log.Stats()
+}
+
+// Panic is equivalent to Error() followed by a call to panic().
+func Panic(args ...interface{}) {
+ Log.Panic(args...)
+}
+
+// Panicf is equivalent to Errorf() followed by a call to panic().
+func Panicf(format string, args ...interface{}) {
+ Log.Panicf(format, args...)
+}
diff --git a/vlog/log.go b/vlog/log.go
new file mode 100644
index 0000000..b020c03
--- /dev/null
+++ b/vlog/log.go
@@ -0,0 +1,208 @@
+package vlog
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "runtime"
+ "sync"
+
+ "github.com/cosmosnicolaou/llog"
+)
+
+const (
+ initialMaxStackBufSize = 128 * 1024
+)
+
+type logger struct {
+ log *llog.Log
+ mu sync.Mutex // guards updates to the vars below.
+ autoFlush bool
+ maxStackBufSize int
+ logDir string
+ configured bool
+}
+
+func (l *logger) maybeFlush() {
+ if l.autoFlush {
+ l.log.Flush()
+ }
+}
+
+var (
+ Log *logger
+ Configured = errors.New("logger has already been configured")
+)
+
+const stackSkip = 1
+
+func init() {
+ Log = &logger{log: llog.NewLogger("veyron", stackSkip)}
+}
+
+// NewLogger creates a new instance of the logging interface.
+func NewLogger(name string) Logger {
+ // Create an instance of the runtime with just logging enabled.
+ return &logger{log: llog.NewLogger(name, stackSkip)}
+}
+
+// Configure configures all future logging. Some options
+// may not be usable if ConfigureLogging is called from an init function,
+// in which case an error will be returned. The Configured error is returned
+// if ConfigureLogger has already been called unless the
+// OverridePriorConfiguration options is included.
+func (l *logger) Configure(opts ...LoggingOpts) error {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ override := false
+ for _, o := range opts {
+ switch v := o.(type) {
+ case OverridePriorConfiguration:
+ override = bool(v)
+ }
+ }
+ if l.configured && !override {
+ return Configured
+ }
+ for _, o := range opts {
+ switch v := o.(type) {
+ case AlsoLogToStderr:
+ l.log.SetAlsoLogToStderr(bool(v))
+ case Level:
+ l.log.SetV(llog.Level(v))
+ case LogDir:
+ l.logDir = string(v)
+ l.log.SetLogDir(l.logDir)
+ case LogToStderr:
+ l.log.SetLogToStderr(bool(v))
+ case MaxStackBufSize:
+ sz := int(v)
+ if sz > initialMaxStackBufSize {
+ l.maxStackBufSize = sz
+ l.log.SetMaxStackBufSize(sz)
+ }
+ case ModuleSpec:
+ l.log.SetVModule(v.ModuleSpec)
+ case TraceLocation:
+ l.log.SetTraceLocation(v.TraceLocation)
+ case StderrThreshold:
+ l.log.SetStderrThreshold(llog.Severity(v))
+ case AutoFlush:
+ l.autoFlush = bool(v)
+ }
+ }
+ l.configured = true
+ return nil
+}
+
+// LogDir returns the directory where the log files are written.
+func (l *logger) LogDir() string {
+ if len(l.logDir) != 0 {
+ return l.logDir
+ }
+ return os.TempDir()
+}
+
+// Stats returns stats on how many lines/bytes haven been written to
+// this set of logs.
+func (l *logger) Stats() LevelStats {
+ return LevelStats(l.log.Stats())
+}
+
+// Info logs to the INFO log.
+// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
+func (l *logger) Info(args ...interface{}) {
+ l.log.Print(llog.InfoLog, args...)
+ l.maybeFlush()
+}
+
+// Infof logs to the INFO log.
+// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+func (l *logger) Infof(format string, args ...interface{}) {
+ l.log.Printf(llog.InfoLog, format, args...)
+ l.maybeFlush()
+}
+
+func infoStack(l *logger, all bool) {
+ n := initialMaxStackBufSize
+ var trace []byte
+ for n <= l.maxStackBufSize {
+ trace = make([]byte, n)
+ nbytes := runtime.Stack(trace, all)
+ if nbytes < len(trace) {
+ l.log.Printf(llog.InfoLog, "%s", trace[:nbytes])
+ return
+ }
+ n *= 2
+ }
+ l.log.Printf(llog.InfoLog, "%s", trace)
+ l.maybeFlush()
+}
+
+// InfoStack logs the current goroutine's stack if the all parameter
+// is false, or the stacks of all goroutines if it's true.
+func (l *logger) InfoStack(all bool) {
+ infoStack(l, all)
+}
+
+func (l *logger) V(v Level) bool {
+ return l.log.V(llog.Level(v))
+}
+
+type discardInfo struct{}
+
+func (_ *discardInfo) Info(args ...interface{}) {}
+func (_ *discardInfo) Infof(format string, args ...interface{}) {}
+func (_ *discardInfo) InfoStack(all bool) {}
+
+func (l *logger) VI(v Level) InfoLog {
+ if l.log.V(llog.Level(v)) {
+ return l
+ }
+ return &discardInfo{}
+}
+
+// Flush flushes all pending log I/O.
+func (l *logger) FlushLog() {
+ l.log.Flush()
+}
+
+// Error logs to the ERROR and INFO logs.
+// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
+func (l *logger) Error(args ...interface{}) {
+ l.log.Print(llog.ErrorLog, args...)
+ l.maybeFlush()
+}
+
+// Errorf logs to the ERROR and INFO logs.
+// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+func (l *logger) Errorf(format string, args ...interface{}) {
+ l.log.Printf(llog.ErrorLog, format, args...)
+ l.maybeFlush()
+}
+
+// Fatal logs to the FATAL, ERROR and INFO logs,
+// including a stack trace of all running goroutines, then calls os.Exit(255).
+// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
+func (l *logger) Fatal(args ...interface{}) {
+ l.log.Print(llog.FatalLog, args...)
+}
+
+// Fatalf logs to the FATAL, ERROR and INFO logs,
+// including a stack trace of all running goroutines, then calls os.Exit(255).
+// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+func (l *logger) Fatalf(format string, args ...interface{}) {
+ l.log.Printf(llog.FatalLog, format, args...)
+}
+
+// Panic is equivalent to Error() followed by a call to panic().
+func (l *logger) Panic(args ...interface{}) {
+ l.Error(args...)
+ panic(fmt.Sprint(args...))
+}
+
+// Panicf is equivalent to Errorf() followed by a call to panic().
+func (l *logger) Panicf(format string, args ...interface{}) {
+ l.Errorf(format, args...)
+ panic(fmt.Sprintf(format, args...))
+}
diff --git a/vlog/log_test.go b/vlog/log_test.go
new file mode 100644
index 0000000..3b7f7a8
--- /dev/null
+++ b/vlog/log_test.go
@@ -0,0 +1,193 @@
+package vlog_test
+
+import (
+ "bufio"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "testing"
+
+ "v.io/x/lib/vlog"
+)
+
+func ExampleConfigure() {
+ vlog.Configure()
+}
+
+func ExampleInfo() {
+ vlog.Info("hello")
+}
+
+func ExampleError() {
+ vlog.Errorf("%s", "error")
+ if vlog.V(2) {
+ vlog.Info("some spammy message")
+ }
+ vlog.VI(2).Infof("another spammy message")
+}
+
+func readLogFiles(dir string) ([]string, error) {
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+ var contents []string
+ for _, fi := range files {
+ // Skip symlinks to avoid double-counting log lines.
+ if !fi.Mode().IsRegular() {
+ continue
+ }
+ file, err := os.Open(filepath.Join(dir, fi.Name()))
+ if err != nil {
+ return nil, err
+ }
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ if line := scanner.Text(); len(line) > 0 && line[0] == 'I' {
+ contents = append(contents, line)
+ }
+ }
+ }
+ return contents, nil
+}
+
+func TestHeaders(t *testing.T) {
+ dir, err := ioutil.TempDir("", "logtest")
+ defer os.RemoveAll(dir)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ logger := vlog.NewLogger("testHeader")
+ logger.Configure(vlog.LogDir(dir), vlog.Level(2))
+ logger.Infof("abc\n")
+ logger.Infof("wombats\n")
+ logger.VI(1).Infof("wombats again\n")
+ logger.FlushLog()
+ contents, err := readLogFiles(dir)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ fileRE := regexp.MustCompile(`\S+ \S+ \S+ (.*):.*`)
+ for _, line := range contents {
+ name := fileRE.FindStringSubmatch(line)
+ if len(name) < 2 {
+ t.Errorf("failed to find file in %s", line)
+ continue
+ }
+ if name[1] != "log_test.go" {
+ t.Errorf("unexpected file name: %s", name[1])
+ continue
+ }
+ }
+ if want, got := 3, len(contents); want != got {
+ t.Errorf("Expected %d info lines, got %d instead", want, got)
+ }
+}
+
+func myLoggedFunc() {
+ f := vlog.LogCall("entry")
+ f("exit")
+}
+
+func TestLogCall(t *testing.T) {
+ dir, err := ioutil.TempDir("", "logtest")
+ defer os.RemoveAll(dir)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ logger := vlog.NewLogger("testHeader")
+ logger.Configure(vlog.LogDir(dir), vlog.Level(2))
+ saveLog := vlog.Log
+ defer vlog.SetLog(saveLog)
+ vlog.SetLog(logger)
+
+ myLoggedFunc()
+ vlog.FlushLog()
+ contents, err := readLogFiles(dir)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ logCallLineRE := regexp.MustCompile(`\S+ \S+ \S+ ([^:]*):.*(call|return)\[(\S*)`)
+ for _, line := range contents {
+ match := logCallLineRE.FindStringSubmatch(line)
+ if len(match) != 4 {
+ t.Errorf("failed to match %s", line)
+ continue
+ }
+ fileName, callType, funcName := match[1], match[2], match[3]
+ if fileName != "log_test.go" {
+ t.Errorf("unexpected file name: %s", fileName)
+ continue
+ }
+ if callType != "call" && callType != "return" {
+ t.Errorf("unexpected call type: %s", callType)
+ }
+ if funcName != "vlog_test.myLoggedFunc" {
+ t.Errorf("unexpected func name: %s", funcName)
+ }
+ }
+ if want, got := 2, len(contents); want != got {
+ t.Errorf("Expected %d info lines, got %d instead", want, got)
+ }
+}
+
+func TestVModule(t *testing.T) {
+ dir, err := ioutil.TempDir("", "logtest")
+ defer os.RemoveAll(dir)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ logger := vlog.NewLogger("testVmodule")
+ logger.Configure(vlog.LogDir(dir))
+ if logger.V(2) || logger.V(3) {
+ t.Errorf("Logging should not be enabled at levels 2 & 3")
+ }
+ spec := vlog.ModuleSpec{}
+ if err := spec.Set("*log_test=2"); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := logger.Configure(vlog.OverridePriorConfiguration(true), spec); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !logger.V(2) {
+ t.Errorf("logger.V(2) should be true")
+ }
+ if logger.V(3) {
+ t.Errorf("logger.V(3) should be false")
+ }
+ if vlog.V(2) || vlog.V(3) {
+ t.Errorf("Logging should not be enabled at levels 2 & 3")
+ }
+ vlog.Log.Configure(vlog.OverridePriorConfiguration(true), spec)
+ if !vlog.V(2) {
+ t.Errorf("vlog.V(2) should be true")
+ }
+ if vlog.V(3) {
+ t.Errorf("vlog.V(3) should be false")
+ }
+ if vlog.VI(2) != vlog.Log {
+ t.Errorf("vlog.V(2) should be vlog.Log")
+ }
+ if vlog.VI(3) == vlog.Log {
+ t.Errorf("vlog.V(3) should not be vlog.Log")
+ }
+}
+
+func TestConfigure(t *testing.T) {
+ dir, err := ioutil.TempDir("", "logtest")
+ defer os.RemoveAll(dir)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ logger := vlog.NewLogger("testVmodule")
+ if got, want := logger.Configure(vlog.LogDir(dir), vlog.AlsoLogToStderr(false)), error(nil); got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+ if got, want := logger.Configure(vlog.AlsoLogToStderr(true)), vlog.Configured; got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+ if got, want := logger.Configure(vlog.OverridePriorConfiguration(true), vlog.AlsoLogToStderr(false)), error(nil); got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+}
diff --git a/vlog/logcall.go b/vlog/logcall.go
new file mode 100644
index 0000000..cfcffd1
--- /dev/null
+++ b/vlog/logcall.go
@@ -0,0 +1,142 @@
+package vlog
+
+import (
+ "fmt"
+ "path"
+ "reflect"
+ "runtime"
+ "sync/atomic"
+
+ "github.com/cosmosnicolaou/llog"
+)
+
+// logCallLogLevel is the log level beyond which calls are logged.
+const logCallLogLevel = 1
+
+func callerFuncName() string {
+ var funcName string
+ pc, _, _, ok := runtime.Caller(stackSkip + 1)
+ if ok {
+ function := runtime.FuncForPC(pc)
+ if function != nil {
+ funcName = path.Base(function.Name())
+ }
+ }
+ return funcName
+}
+
+// LogCall logs that its caller has been called given the arguments
+// passed to it. It returns a function that is supposed to be called
+// when the caller returns, logging the caller’s return along with the
+// arguments it is provided with.
+// File name and line number of the call site and a randomly generated
+// invocation identifier is logged automatically. The path through which
+// the caller function returns will be logged automatically too.
+//
+// The canonical way to use LogCall is along the lines of the following:
+//
+// func Function(a Type1, b Type2) ReturnType {
+// defer vlog.LogCall(a, b)()
+// // ... function body ...
+// return retVal
+// }
+//
+// To log the return value as the function returns, the following
+// pattern should be used. Note that pointers to the output
+// variables should be passed to the returning function, not the
+// variables themselves:
+//
+// func Function(a Type1, b Type2) (r ReturnType) {
+// defer vlog.LogCall(a, b)(&r)
+// // ... function body ...
+// return computeReturnValue()
+// }
+//
+// Note that when using this pattern, you do not need to actually
+// assign anything to the named return variable explicitly. A regular
+// return statement would automatically do the proper return variable
+// assignments.
+//
+// The log injector tool will automatically insert a LogCall invocation
+// into all implementations of the public API it runs, unless a Valid
+// Log Construct is found. A Valid Log Construct is defined as one of
+// the following at the beginning of the function body (i.e. should not
+// be preceded by any non-whitespace or non-comment tokens):
+// 1. defer vlog.LogCall(optional arguments)(optional pointers to return values)
+// 2. defer vlog.LogCallf(argsFormat, optional arguments)(returnValuesFormat, optional pointers to return values)
+// 3. // nologcall
+//
+// The comment "// nologcall" serves as a hint to log injection and
+// checking tools to exclude the function from their consideration.
+// It is used as follows:
+//
+// func FunctionWithoutLogging(args ...interface{}) {
+// // nologcall
+// // ... function body ...
+// }
+//
+func LogCall(v ...interface{}) func(...interface{}) {
+ if !V(logCallLogLevel) {
+ return func(...interface{}) {}
+ }
+ callerFuncName := callerFuncName()
+ invocationId := newInvocationIdentifier()
+ if len(v) > 0 {
+ Log.log.Printf(llog.InfoLog, "call[%s %s]: args:%v", callerFuncName, invocationId, v)
+ } else {
+ Log.log.Printf(llog.InfoLog, "call[%s %s]", callerFuncName, invocationId)
+ }
+ return func(v ...interface{}) {
+ if len(v) > 0 {
+ Log.log.Printf(llog.InfoLog, "return[%s %s]: %v", callerFuncName, invocationId, derefSlice(v))
+ } else {
+ Log.log.Printf(llog.InfoLog, "return[%s %s]", callerFuncName, invocationId)
+ }
+ }
+}
+
+// LogCallf behaves identically to LogCall, except it lets the caller to
+// customize the log messages via format specifiers, like the following:
+//
+// func Function(a Type1, b Type2) (r, t ReturnType) {
+// defer vlog.LogCallf("a: %v, b: %v", a, b)("(r,t)=(%v,%v)", &r, &t)
+// // ... function body ...
+// return finalR, finalT
+// }
+//
+func LogCallf(format string, v ...interface{}) func(string, ...interface{}) {
+ if !V(logCallLogLevel) {
+ return func(string, ...interface{}) {}
+ }
+ callerFuncName := callerFuncName()
+ invocationId := newInvocationIdentifier()
+ Log.log.Printf(llog.InfoLog, "call[%s %s]: %s", callerFuncName, invocationId, fmt.Sprintf(format, v...))
+ return func(format string, v ...interface{}) {
+ Log.log.Printf(llog.InfoLog, "return[%s %s]: %v", callerFuncName, invocationId, fmt.Sprintf(format, derefSlice(v)...))
+ }
+}
+
+func derefSlice(slice []interface{}) []interface{} {
+ o := make([]interface{}, 0, len(slice))
+ for _, x := range slice {
+ o = append(o, reflect.Indirect(reflect.ValueOf(x)).Interface())
+ }
+ return o
+}
+
+var invocationCounter uint64 = 0
+
+// newInvocationIdentifier generates a unique identifier for a method invocation
+// to make it easier to match up log lines for the entry and exit of a function
+// when looking at a log transcript.
+func newInvocationIdentifier() string {
+ const (
+ charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"
+ charSetLen = uint64(len(charSet))
+ )
+ r := []byte{'@'}
+ for n := atomic.AddUint64(&invocationCounter, 1); n > 0; n /= charSetLen {
+ r = append(r, charSet[n%charSetLen])
+ }
+ return string(r)
+}
diff --git a/vlog/model.go b/vlog/model.go
new file mode 100644
index 0000000..6a4ea96
--- /dev/null
+++ b/vlog/model.go
@@ -0,0 +1,153 @@
+package vlog
+
+import (
+ // TODO(cnicolaou): remove this dependency in the future. For now this
+ // saves us some code.
+ "github.com/cosmosnicolaou/llog"
+)
+
+type InfoLog interface {
+ // Info logs to the INFO log.
+ // Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
+ Info(args ...interface{})
+
+ // Infoln logs to the INFO log.
+ // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+ Infof(format string, args ...interface{})
+
+ // InfoStack logs the current goroutine's stack if the all parameter
+ // is false, or the stacks of all goroutines if it's true.
+ InfoStack(all bool)
+}
+
+type Verbosity interface {
+ // V returns true if the configured logging level is greater than or equal to its parameter
+ V(level Level) bool
+ // VI is like V, except that it returns an instance of the Info
+ // interface that will either log (if level >= the configured level)
+ // or discard its parameters. This allows for logger.VI(2).Info
+ // style usage.
+ VI(level Level) InfoLog
+}
+
+// Level specifies a level of verbosity for V logs.
+// It can be set via the Level optional parameter to Configure.
+// It implements the flag.Value interface to support command line option parsing.
+type Level llog.Level
+
+// Set is part of the flag.Value interface.
+func (l *Level) Set(v string) error {
+ return (*llog.Level)(l).Set(v)
+}
+
+// Get is part of the flag.Value interface.
+func (l *Level) Get(v string) interface{} {
+ return *l
+}
+
+// String is part of the flag.Value interface.
+func (l *Level) String() string {
+ return (*llog.Level)(l).String()
+}
+
+// StderrThreshold identifies the sort of log: info, warning etc.
+// The values match the corresponding constants in C++ - e.g WARNING etc.
+// It can be set via the StderrThreshold optional parameter to Configure.
+// It implements the flag.Value interface to support command line option parsing.
+type StderrThreshold llog.Severity
+
+// Set is part of the flag.Value interface.
+func (s *StderrThreshold) Set(v string) error {
+ return (*llog.Severity)(s).Set(v)
+}
+
+// Get is part of the flag.Value interface.
+func (s *StderrThreshold) Get(v string) interface{} {
+ return *s
+}
+
+// String is part of the flag.Value interface.
+func (s *StderrThreshold) String() string {
+ return (*llog.Severity)(s).String()
+}
+
+// ModuleSpec allows for the setting of specific log levels for specific
+// modules. The syntax is recordio=2,file=1,gfs*=3
+// It can be set via the ModuleSpec optional parameter to Configure.
+// It implements the flag.Value interface to support command line option parsing.
+type ModuleSpec struct {
+ llog.ModuleSpec
+}
+
+// TraceLocation specifies the location, file:N, which when encountered will
+// cause logging to emit a stack trace.
+// It can be set via the TraceLocation optional parameter to Configure.
+// It implements the flag.Value interface to support command line option parsing.
+type TraceLocation struct {
+ llog.TraceLocation
+}
+
+// LevelStats tracks the number of lines of output and number of bytes
+// per severity level.
+type LevelStats llog.Stats
+
+type Logger interface {
+ InfoLog
+ Verbosity
+
+ // Flush flushes all pending log I/O.
+ FlushLog()
+
+ // Error logs to the ERROR and INFO logs.
+ // Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
+ Error(args ...interface{})
+
+ // Errorf logs to the ERROR and INFO logs.
+ // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+ Errorf(format string, args ...interface{})
+
+ // Fatal logs to the FATAL, ERROR and INFO logs,
+ // including a stack trace of all running goroutines, then calls os.Exit(255).
+ // Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
+ Fatal(args ...interface{})
+
+ // Fatalf logs to the FATAL, ERROR and INFO logs,
+ // including a stack trace of all running goroutines, then calls os.Exit(255).
+ // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+ Fatalf(format string, args ...interface{})
+
+ // Panic is equivalent to Error() followed by a call to panic().
+ Panic(args ...interface{})
+
+ // Panicf is equivalent to Errorf() followed by a call to panic().
+ Panicf(format string, args ...interface{})
+
+ // Configure configures all future logging. Some options
+ // may not be usable if Configure is called from an init function,
+ // in which case an error will be returned. The Configured error is
+ // returned if ConfigureLogger has already been called unless the
+ // OverridePriorConfiguration options is included.
+ // Some options only take effect if they are set before the logger
+ // is used. Once anything is logged using the logger, these options
+ // will silently be ignored. For example, LogDir, LogToStderr or
+ // AlsoLogToStderr fall in this category.
+ Configure(opts ...LoggingOpts) error
+
+ // Stats returns stats on how many lines/bytes haven been written to
+ // this set of logs per severity level.
+ Stats() LevelStats
+
+ // LogDir returns the currently configured directory for storing logs.
+ LogDir() string
+}
+
+// Runtime defines the methods that the runtime must implement.
+type Runtime interface {
+ // Logger returns the current logger, if any, in use by the Runtime.
+ // TODO(cnicolaou): remove this.
+ Logger() Logger
+
+ // NewLogger creates a new instance of the logging interface that is
+ // separate from the one provided by Runtime.
+ NewLogger(name string, opts ...LoggingOpts) (Logger, error)
+}
diff --git a/vlog/opts.go b/vlog/opts.go
new file mode 100644
index 0000000..9f5f74b
--- /dev/null
+++ b/vlog/opts.go
@@ -0,0 +1,56 @@
+package vlog
+
+type LoggingOpts interface {
+ LoggingOpt()
+}
+
+type AutoFlush bool
+type AlsoLogToStderr bool
+type LogDir string
+type LogToStderr bool
+type OverridePriorConfiguration bool
+type MaxStackBufSize int
+
+// If true, logs are written to standard error as well as to files.
+func (_ AlsoLogToStderr) LoggingOpt() {}
+
+// Enable V-leveled logging at the specified level.
+func (_ Level) LoggingOpt() {}
+
+// log files will be written to this directory instead of the
+// default temporary directory.
+func (_ LogDir) LoggingOpt() {}
+
+// If true, logs are written to standard error instead of to files.
+func (_ LogToStderr) LoggingOpt() {}
+
+// Set the max size (bytes) of the byte buffer to use for stack
+// traces. The default max is 4M; use powers of 2 since the
+// stack size will be grown exponentially until it exceeds the max.
+// A min of 128K is enforced and any attempts to reduce this will
+// be silently ignored.
+func (_ MaxStackBufSize) LoggingOpt() {}
+
+// The syntax of the argument is a comma-separated list of pattern=N,
+// where pattern is a literal file name (minus the ".go" suffix) or
+// "glob" pattern and N is a V level. For instance,
+// -gopher*=3
+// sets the V level to 3 in all Go files whose names begin "gopher".
+func (_ ModuleSpec) LoggingOpt() {}
+
+// Log events at or above this severity are logged to standard
+// error as well as to files.
+func (_ StderrThreshold) LoggingOpt() {}
+
+// When set to a file and line number holding a logging statement, such as
+// gopherflakes.go:234
+// a stack trace will be written to the Info log whenever execution
+// hits that statement. (Unlike with -vmodule, the ".go" must be
+// present.)
+func (_ TraceLocation) LoggingOpt() {}
+
+// If true, enables automatic flushing of log output on every call
+func (_ AutoFlush) LoggingOpt() {}
+
+// If true, allows this call to ConfigureLogger to override a prior configuration.
+func (_ OverridePriorConfiguration) LoggingOpt() {}
diff --git a/vlog/util_test.go b/vlog/util_test.go
new file mode 100644
index 0000000..3b8aa44
--- /dev/null
+++ b/vlog/util_test.go
@@ -0,0 +1,6 @@
+package vlog
+
+// SetLog allows us to override the Log global for testing purposes.
+func SetLog(l Logger) {
+ Log = l.(*logger)
+}
diff --git a/vlog/v23_test.go b/vlog/v23_test.go
new file mode 100644
index 0000000..5de1cfe
--- /dev/null
+++ b/vlog/v23_test.go
@@ -0,0 +1,30 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+package vlog_test
+
+import "fmt"
+import "testing"
+import "os"
+
+import "v.io/x/ref/lib/modules"
+import "v.io/x/ref/lib/testutil"
+
+func init() {
+ modules.RegisterChild("child", ``, child)
+}
+
+func TestMain(m *testing.M) {
+ testutil.Init()
+ if modules.IsModulesChildProcess() {
+ if err := modules.Dispatch(); err != nil {
+ fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
+ os.Exit(1)
+ }
+ return
+ }
+ os.Exit(m.Run())
+}