[command] Add a package for common command-line op code

To start with, define a "Cancelable" command which exits
early when the input context emits a Done event.

TEST: go test -count=10000 ./command/...
OUTPUT: ok Fuchsia.googlesource.com/tools/command 13.310s
(Verifying that a test that `sleep`s for 1 millisecond is not flaky)

Change-Id: I6323bc09bc6e90a1fdb818498e8372b16dfe8caa
diff --git a/command/cancelable.go b/command/cancelable.go
new file mode 100644
index 0000000..47e10fe
--- /dev/null
+++ b/command/cancelable.go
@@ -0,0 +1,75 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package command
+
+import (
+	"context"
+	"flag"
+	"log"
+
+	"github.com/google/subcommands"
+)
+
+// Disposer is an object that performs tear down. This is used by Cancelable to gracefully
+// terminate a delegate Command before exiting.
+type Disposer interface {
+	Dispose()
+}
+
+// Cancelable wraps a subcommands.Command so that it is canceled if its input execution
+// context emits a Done event before execution is finished. If the given Command
+// implements the Disposer interface, Dispose is called before the program exits.
+func Cancelable(sub subcommands.Command) subcommands.Command {
+	return &cancelable{sub}
+}
+
+// cancelable wraps a subcommands.Command so that it is canceled if the input execution
+// context emits a Done event before execution is finished. cancelable "masquerades" as
+// the underlying Command. Example Registration:
+//
+//   subcommands.Register(command.Cancelable(&OtherSubcommand{}))
+type cancelable struct {
+	sub subcommands.Command
+}
+
+// Name forwards to the underlying Command.
+func (cmd *cancelable) Name() string {
+	return cmd.sub.Name()
+}
+
+// Usage forwards to the underlying Command.
+func (cmd *cancelable) Usage() string {
+	return cmd.sub.Usage()
+}
+
+// Synopsis forwards to the underlying Command.
+func (cmd *cancelable) Synopsis() string {
+	return cmd.sub.Synopsis()
+}
+
+// SetFlags forwards to the underlying Command.
+func (cmd *cancelable) SetFlags(f *flag.FlagSet) {
+	cmd.sub.SetFlags(f)
+}
+
+// Execute runs the underlying Command in a goroutine. If the input context is canceled
+// before execution finishes, execution is canceled and the context's error is logged.
+func (cmd *cancelable) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
+	status := make(chan subcommands.ExitStatus)
+	go func() {
+		status <- cmd.sub.Execute(ctx, f, args...)
+	}()
+	select {
+	case <-ctx.Done():
+		if d, ok := cmd.sub.(Disposer); ok {
+			d.Dispose()
+		}
+		log.Println(ctx.Err())
+		return subcommands.ExitFailure
+	case s := <-status:
+		close(status)
+		return s
+	}
+}
diff --git a/command/cancelable_test.go b/command/cancelable_test.go
new file mode 100644
index 0000000..2008d18
--- /dev/null
+++ b/command/cancelable_test.go
@@ -0,0 +1,85 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package command_test
+
+import (
+	"context"
+	"flag"
+	"testing"
+	"time"
+
+	"fuchsia.googlesource.com/tools/command"
+	"github.com/google/subcommands"
+)
+
+func TestCancelableExecute(t *testing.T) {
+	tests := []struct {
+		// The name of this test case
+		name string
+
+		// Whether to cancel the execution context early.
+		cancelContextEarly bool
+
+		// Whether the underlying subcommand is expected to finish
+		expectToFinish bool
+	}{
+		{"when context is canceled early", true, false},
+		{"when context is never canceled", false, true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tcmd := &TestCommand{}
+			cmd := command.Cancelable(tcmd)
+			ctx, cancel := context.WithCancel(context.Background())
+			if tt.cancelContextEarly {
+				cancel()
+				cmd.Execute(ctx, flag.NewFlagSet("test", flag.ContinueOnError))
+			} else {
+				cmd.Execute(ctx, flag.NewFlagSet("test", flag.ContinueOnError))
+				cancel()
+			}
+
+			if tcmd.DidFinish && !tt.expectToFinish {
+				t.Errorf("wanted command to exit early but it finished")
+			} else if !tcmd.DidFinish && tt.expectToFinish {
+				t.Errorf("wanted command to finish but it exited early")
+			}
+		})
+	}
+}
+
+// TestCancelableDelegation verifies that Cancelable() returns a subcommand.Command that
+// delegates to the input subcommand.Command.
+func TestCancelableDelegation(t *testing.T) {
+	expectEq := func(t *testing.T, name, expected, actual string) {
+		if expected != actual {
+			t.Errorf("wanted %s to be %q but got %q", name, expected, actual)
+		}
+	}
+	cmd := command.Cancelable(&TestCommand{
+		name:     "test_name",
+		usage:    "test_usage",
+		synopsis: "test_synopsis",
+	})
+	expectEq(t, "Name", "test_name", cmd.Name())
+	expectEq(t, "Usage", "test_usage", cmd.Usage())
+	expectEq(t, "Synopsis", "test_synopsis", cmd.Synopsis())
+}
+
+type TestCommand struct {
+	name, usage, synopsis string
+	DidFinish             bool
+}
+
+func (cmd *TestCommand) Name() string             { return cmd.name }
+func (cmd *TestCommand) Usage() string            { return cmd.usage }
+func (cmd *TestCommand) Synopsis() string         { return cmd.synopsis }
+func (cmd *TestCommand) SetFlags(f *flag.FlagSet) {}
+func (cmd *TestCommand) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
+	time.Sleep(time.Millisecond)
+	cmd.DidFinish = true
+	return subcommands.ExitSuccess
+}
diff --git a/command/doc.go b/command/doc.go
new file mode 100644
index 0000000..bc6b9bf
--- /dev/null
+++ b/command/doc.go
@@ -0,0 +1,6 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Package command defines common code for writing Fuchsia command line tools.
+package command
diff --git a/command/signals.go b/command/signals.go
new file mode 100644
index 0000000..b889d3b
--- /dev/null
+++ b/command/signals.go
@@ -0,0 +1,25 @@
+package command
+
+import (
+	"context"
+	"os"
+	"os/signal"
+)
+
+// CancelOnSignals returns a Context that emits a Done event when any of the input signals
+// are recieved, assuming those signals can be handled by the current process.
+func CancelOnSignals(ctx context.Context, sigs ...os.Signal) context.Context {
+	ctx, cancel := context.WithCancel(ctx)
+	signals := make(chan os.Signal)
+	signal.Notify(signals, sigs...)
+	go func() {
+		select {
+		case s := <-signals:
+			if s != nil {
+				cancel()
+				close(signals)
+			}
+		}
+	}()
+	return ctx
+}