[ARVADOS] created: 1.1.2-12-ga6ee0e9

Git user git at public.curoverse.com
Tue Jan 2 09:45:05 EST 2018


        at  a6ee0e990641973aa4d79e550821eb42eab25ca4 (commit)


commit a6ee0e990641973aa4d79e550821eb42eab25ca4
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Tue Jan 2 00:47:04 2018 -0500

    12876: Pass commands through to ruby/python programs.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/cmd/arvados-client/cmd.go b/cmd/arvados-client/cmd.go
index 73d772a..b616b54 100644
--- a/cmd/arvados-client/cmd.go
+++ b/cmd/arvados-client/cmd.go
@@ -5,7 +5,6 @@
 package main
 
 import (
-	"flag"
 	"fmt"
 	"io"
 	"os"
@@ -14,38 +13,67 @@ import (
 
 	"git.curoverse.com/arvados.git/lib/cli"
 	"git.curoverse.com/arvados.git/lib/cmd"
-	"rsc.io/getopt"
 )
 
-var version = "dev"
+var (
+	version                = "dev"
+	cmdVersion cmd.Handler = versionCmd{}
+	handler                = cmd.Multi(map[string]cmd.Handler{
+		"-e":        cmdVersion,
+		"version":   cmdVersion,
+		"-version":  cmdVersion,
+		"--version": cmdVersion,
 
-var Run = cmd.Multi(map[string]cmd.RunFunc{
-	"get":       cli.Get,
-	"-e":        cmdVersion,
-	"version":   cmdVersion,
-	"-version":  cmdVersion,
-	"--version": cmdVersion,
-})
+		"copy":     cli.Copy,
+		"create":   cli.Create,
+		"edit":     cli.Edit,
+		"get":      cli.Get,
+		"keep":     cli.Keep,
+		"pipeline": cli.Pipeline,
+		"run":      cli.Run,
+		"tag":      cli.Tag,
+		"ws":       cli.Ws,
 
-func cmdVersion(prog string, args []string, _ io.Reader, stdout, _ io.Writer) int {
+		"api_client_authorization": cli.APICall,
+		"api_client":               cli.APICall,
+		"authorized_key":           cli.APICall,
+		"collection":               cli.APICall,
+		"container":                cli.APICall,
+		"container_request":        cli.APICall,
+		"group":                    cli.APICall,
+		"human":                    cli.APICall,
+		"job":                      cli.APICall,
+		"job_task":                 cli.APICall,
+		"keep_disk":                cli.APICall,
+		"keep_service":             cli.APICall,
+		"link":                     cli.APICall,
+		"log":                      cli.APICall,
+		"node":                     cli.APICall,
+		"pipeline_instance":        cli.APICall,
+		"pipeline_template":        cli.APICall,
+		"repository":               cli.APICall,
+		"specimen":                 cli.APICall,
+		"trait":                    cli.APICall,
+		"user_agreement":           cli.APICall,
+		"user":                     cli.APICall,
+		"virtual_machine":          cli.APICall,
+		"workflow":                 cli.APICall,
+	})
+)
+
+type versionCmd struct{}
+
+func (versionCmd) RunCommand(prog string, args []string, _ io.Reader, stdout, _ io.Writer) int {
 	prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
 	fmt.Fprintf(stdout, "%s %s (%s)\n", prog, version, runtime.Version())
 	return 0
 }
 
 func fixLegacyArgs(args []string) []string {
-	flags := getopt.NewFlagSet("", flag.ContinueOnError)
-	flags.Bool("dry-run", false, "dry run")
-	flags.Alias("n", "dry-run")
-	flags.String("format", "json", "output format")
-	flags.Alias("f", "format")
-	flags.Bool("short", false, "short")
-	flags.Alias("s", "short")
-	flags.Bool("verbose", false, "verbose")
-	flags.Alias("v", "verbose")
+	flags, _ := cli.LegacyFlagSet()
 	return cmd.SubcommandToFront(args, flags)
 }
 
 func main() {
-	os.Exit(Run(os.Args[0], fixLegacyArgs(os.Args[1:]), os.Stdin, os.Stdout, os.Stderr))
+	os.Exit(handler.RunCommand(os.Args[0], fixLegacyArgs(os.Args[1:]), os.Stdin, os.Stdout, os.Stderr))
 }
diff --git a/cmd/arvados-client/cmd_test.go b/cmd/arvados-client/cmd_test.go
index b1ab5ba..cbbc7b1 100644
--- a/cmd/arvados-client/cmd_test.go
+++ b/cmd/arvados-client/cmd_test.go
@@ -22,19 +22,19 @@ var _ = check.Suite(&ClientSuite{})
 type ClientSuite struct{}
 
 func (s *ClientSuite) TestBadCommand(c *check.C) {
-	exited := Run("arvados-client", []string{"no such command"}, bytes.NewReader(nil), ioutil.Discard, ioutil.Discard)
+	exited := handler.RunCommand("arvados-client", []string{"no such command"}, bytes.NewReader(nil), ioutil.Discard, ioutil.Discard)
 	c.Check(exited, check.Equals, 2)
 }
 
 func (s *ClientSuite) TestBadSubcommandArgs(c *check.C) {
-	exited := Run("arvados-client", []string{"get"}, bytes.NewReader(nil), ioutil.Discard, ioutil.Discard)
+	exited := handler.RunCommand("arvados-client", []string{"get"}, bytes.NewReader(nil), ioutil.Discard, ioutil.Discard)
 	c.Check(exited, check.Equals, 2)
 }
 
 func (s *ClientSuite) TestVersion(c *check.C) {
 	stdout := bytes.NewBuffer(nil)
 	stderr := bytes.NewBuffer(nil)
-	exited := Run("arvados-client", []string{"version"}, bytes.NewReader(nil), stdout, stderr)
+	exited := handler.RunCommand("arvados-client", []string{"version"}, bytes.NewReader(nil), stdout, stderr)
 	c.Check(exited, check.Equals, 0)
 	c.Check(stdout.String(), check.Matches, `arvados-client dev \(go[0-9\.]+\)\n`)
 	c.Check(stderr.String(), check.Equals, "")
diff --git a/lib/cli/external.go b/lib/cli/external.go
new file mode 100644
index 0000000..ba85aae
--- /dev/null
+++ b/lib/cli/external.go
@@ -0,0 +1,99 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package cli
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os/exec"
+	"strings"
+	"syscall"
+
+	"git.curoverse.com/arvados.git/lib/cmd"
+)
+
+var (
+	Create = rubyArvCmd{"create"}
+	Edit   = rubyArvCmd{"edit"}
+
+	Copy = externalCmd{"arv-copy"}
+	Tag  = externalCmd{"arv-tag"}
+	Ws   = externalCmd{"arv-ws"}
+	Run  = externalCmd{"arv-run"}
+
+	Keep = cmd.Multi(map[string]cmd.Handler{
+		"get":       externalCmd{"arv-get"},
+		"put":       externalCmd{"arv-put"},
+		"ls":        externalCmd{"arv-ls"},
+		"normalize": externalCmd{"arv-normalize"},
+		"docker":    externalCmd{"arv-keepdocker"},
+	})
+	Pipeline = cmd.Multi(map[string]cmd.Handler{
+		"run": externalCmd{"arv-run-pipeline-instance"},
+	})
+	// user, group, container, specimen, etc.
+	APICall = apiCallCmd{}
+)
+
+// When using the ruby "arv" command, flags must come before the
+// subcommand: "arv --format=yaml get foo" works, but "arv get
+// --format=yaml foo" does not work.
+func legacyFlagsToFront(subcommand string, argsin []string) (argsout []string) {
+	flags, _ := LegacyFlagSet()
+	flags.SetOutput(ioutil.Discard)
+	flags.Parse(argsin)
+	narg := flags.NArg()
+	argsout = append(argsout, argsin[:len(argsin)-narg]...)
+	argsout = append(argsout, subcommand)
+	argsout = append(argsout, argsin[len(argsin)-narg:]...)
+	return
+}
+
+type apiCallCmd struct{}
+
+func (cmd apiCallCmd) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	split := strings.Split(prog, " ")
+	if len(split) < 2 {
+		fmt.Fprintf(stderr, "internal error: no api model in %q\n", prog)
+		return 2
+	}
+	model := split[len(split)-1]
+	return externalCmd{"arv"}.RunCommand("arv", legacyFlagsToFront(model, args), stdin, stdout, stderr)
+}
+
+type rubyArvCmd struct {
+	subcommand string
+}
+
+func (rc rubyArvCmd) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	return externalCmd{"arv"}.RunCommand("arv", legacyFlagsToFront(rc.subcommand, args), stdin, stdout, stderr)
+}
+
+type externalCmd struct {
+	prog string
+}
+
+func (ec externalCmd) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	cmd := exec.Command(ec.prog, args...)
+	cmd.Stdin = stdin
+	cmd.Stdout = stdout
+	cmd.Stderr = stderr
+	err := cmd.Run()
+	switch err := err.(type) {
+	case nil:
+		return 0
+	case *exec.ExitError:
+		status := err.Sys().(syscall.WaitStatus)
+		if status.Exited() {
+			return status.ExitStatus()
+		}
+		fmt.Fprintf(stderr, "%s failed: %s\n", ec.prog, err)
+		return 1
+	default:
+		fmt.Fprintf(stderr, "error running %s: %s\n", ec.prog, err)
+		return 1
+	}
+}
diff --git a/lib/cli/flags.go b/lib/cli/flags.go
new file mode 100644
index 0000000..7147e0c
--- /dev/null
+++ b/lib/cli/flags.go
@@ -0,0 +1,33 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package cli
+
+import (
+	"flag"
+
+	"git.curoverse.com/arvados.git/lib/cmd"
+	"rsc.io/getopt"
+)
+
+type LegacyFlagValues struct {
+	Format  string
+	DryRun  bool
+	Short   bool
+	Verbose bool
+}
+
+func LegacyFlagSet() (cmd.FlagSet, *LegacyFlagValues) {
+	values := &LegacyFlagValues{Format: "json"}
+	flags := getopt.NewFlagSet("", flag.ContinueOnError)
+	flags.BoolVar(&values.DryRun, "dry-run", false, "Don't actually do anything")
+	flags.Alias("n", "dry-run")
+	flags.StringVar(&values.Format, "format", values.Format, "Output format: json, yaml, or uuid")
+	flags.Alias("f", "format")
+	flags.BoolVar(&values.Short, "short", false, "Return only UUIDs (equivalent to --format=uuid)")
+	flags.Alias("s", "short")
+	flags.BoolVar(&values.Verbose, "verbose", false, "Print more debug/progress messages on stderr")
+	flags.Alias("v", "verbose")
+	return flags, values
+}
diff --git a/lib/cli/get.go b/lib/cli/get.go
index baa1df7..2c60f43 100644
--- a/lib/cli/get.go
+++ b/lib/cli/get.go
@@ -6,16 +6,19 @@ package cli
 
 import (
 	"encoding/json"
-	"flag"
 	"fmt"
 	"io"
 
+	"git.curoverse.com/arvados.git/lib/cmd"
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"github.com/ghodss/yaml"
-	"rsc.io/getopt"
 )
 
-func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+var Get cmd.Handler = getCmd{}
+
+type getCmd struct{}
+
+func (getCmd) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
 	var err error
 	defer func() {
 		if err != nil {
@@ -23,27 +26,19 @@ func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer)
 		}
 	}()
 
-	flags := getopt.NewFlagSet(prog, flag.ContinueOnError)
+	flags, opts := LegacyFlagSet()
 	flags.SetOutput(stderr)
-
-	format := flags.String("format", "json", "output format (json, yaml, or uuid)")
-	flags.Alias("f", "format")
-	short := flags.Bool("short", false, "equivalent to --format=uuid")
-	flags.Alias("s", "short")
-	flags.Bool("dry-run", false, "dry run (ignored, for compatibility)")
-	flags.Alias("n", "dry-run")
-	flags.Bool("verbose", false, "verbose (ignored, for compatibility)")
-	flags.Alias("v", "verbose")
 	err = flags.Parse(args)
 	if err != nil {
 		return 2
 	}
 	if len(flags.Args()) != 1 {
-		flags.Usage()
+		fmt.Fprintf(stderr, "usage of %s:\n", prog)
+		flags.PrintDefaults()
 		return 2
 	}
-	if *short {
-		*format = "uuid"
+	if opts.Short {
+		opts.Format = "uuid"
 	}
 
 	id := flags.Args()[0]
@@ -59,13 +54,13 @@ func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer)
 		err = fmt.Errorf("GET %s: %s", path, err)
 		return 1
 	}
-	if *format == "yaml" {
+	if opts.Format == "yaml" {
 		var buf []byte
 		buf, err = yaml.Marshal(obj)
 		if err == nil {
 			_, err = stdout.Write(buf)
 		}
-	} else if *format == "uuid" {
+	} else if opts.Format == "uuid" {
 		fmt.Fprintln(stdout, obj["uuid"])
 	} else {
 		enc := json.NewEncoder(stdout)
diff --git a/lib/cli/get_test.go b/lib/cli/get_test.go
index 9b8f1a0..b2128a4 100644
--- a/lib/cli/get_test.go
+++ b/lib/cli/get_test.go
@@ -25,7 +25,7 @@ type GetSuite struct{}
 func (s *GetSuite) TestGetCollectionJSON(c *check.C) {
 	stdout := bytes.NewBuffer(nil)
 	stderr := bytes.NewBuffer(nil)
-	exited := Get("arvados-client get", []string{arvadostest.FooCollection}, bytes.NewReader(nil), stdout, stderr)
+	exited := Get.RunCommand("arvados-client get", []string{arvadostest.FooCollection}, bytes.NewReader(nil), stdout, stderr)
 	c.Check(stdout.String(), check.Matches, `(?ms){.*"uuid": "`+arvadostest.FooCollection+`".*}\n`)
 	c.Check(stdout.String(), check.Matches, `(?ms){.*"portable_data_hash": "`+regexp.QuoteMeta(arvadostest.FooCollectionPDH)+`".*}\n`)
 	c.Check(stderr.String(), check.Equals, "")
diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go
index f33354a..d040065 100644
--- a/lib/cmd/cmd.go
+++ b/lib/cmd/cmd.go
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 
-// package cmd defines a RunFunc type, representing a process that can
-// be invoked from a command line.
+// package cmd helps define reusable functions that can be exposed as
+// [subcommands of] command line programs.
 package cmd
 
 import (
@@ -15,41 +15,47 @@ import (
 	"strings"
 )
 
-// A RunFunc runs a command with the given args, and returns an exit
-// code.
-type RunFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
+type Handler interface {
+	RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
+}
+
+type HandlerFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
+
+func (f HandlerFunc) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	return f(prog, args, stdin, stdout, stderr)
+}
 
-// Multi returns a RunFunc that looks up its first argument in m, and
-// invokes the resulting RunFunc with the remaining args.
+// Multi is a Handler that looks up its first argument in a map, and
+// invokes the resulting Handler with the remaining args.
 //
 // Example:
 //
-//     os.Exit(Multi(map[string]RunFunc{
-//             "foobar": func(prog string, args []string) int {
+//     os.Exit(Multi(map[string]Handler{
+//             "foobar": HandlerFunc(func(prog string, args []string) int {
 //                     fmt.Println(args[0])
 //                     return 2
-//             },
+//             }),
 //     })("/usr/bin/multi", []string{"foobar", "baz"}))
 //
 // ...prints "baz" and exits 2.
-func Multi(m map[string]RunFunc) RunFunc {
-	return func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
-		if len(args) < 1 {
-			fmt.Fprintf(stderr, "usage: %s command [args]\n", prog)
-			multiUsage(stderr, m)
-			return 2
-		}
-		if cmd, ok := m[args[0]]; !ok {
-			fmt.Fprintf(stderr, "unrecognized command %q\n", args[0])
-			multiUsage(stderr, m)
-			return 2
-		} else {
-			return cmd(prog+" "+args[0], args[1:], stdin, stdout, stderr)
-		}
+type Multi map[string]Handler
+
+func (m Multi) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	if len(args) < 1 {
+		fmt.Fprintf(stderr, "usage: %s command [args]\n", prog)
+		m.Usage(stderr)
+		return 2
+	}
+	if cmd, ok := m[args[0]]; !ok {
+		fmt.Fprintf(stderr, "unrecognized command %q\n", args[0])
+		m.Usage(stderr)
+		return 2
+	} else {
+		return cmd.RunCommand(prog+" "+args[0], args[1:], stdin, stdout, stderr)
 	}
 }
 
-func multiUsage(stderr io.Writer, m map[string]RunFunc) {
+func (m Multi) Usage(stderr io.Writer) {
 	var subcommands []string
 	for sc := range m {
 		if strings.HasPrefix(sc, "-") {
@@ -69,9 +75,11 @@ func multiUsage(stderr io.Writer, m map[string]RunFunc) {
 
 type FlagSet interface {
 	Init(string, flag.ErrorHandling)
+	Args() []string
 	NArg() int
 	Parse([]string) error
 	SetOutput(io.Writer)
+	PrintDefaults()
 }
 
 // SubcommandToFront silently parses args using flagset, and returns a
diff --git a/lib/cmd/cmd_test.go b/lib/cmd/cmd_test.go
index 04e98d8..d8a4861 100644
--- a/lib/cmd/cmd_test.go
+++ b/lib/cmd/cmd_test.go
@@ -25,18 +25,18 @@ var _ = check.Suite(&CmdSuite{})
 
 type CmdSuite struct{}
 
-var testCmd = Multi(map[string]RunFunc{
-	"echo": func(prog string, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int {
+var testCmd = Multi(map[string]Handler{
+	"echo": HandlerFunc(func(prog string, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int {
 		fmt.Fprintln(stdout, strings.Join(args, " "))
 		return 0
-	},
+	}),
 })
 
 func (s *CmdSuite) TestHello(c *check.C) {
 	defer cmdtest.LeakCheck(c)()
 	stdout := bytes.NewBuffer(nil)
 	stderr := bytes.NewBuffer(nil)
-	exited := testCmd("prog", []string{"echo", "hello", "world"}, bytes.NewReader(nil), stdout, stderr)
+	exited := testCmd.RunCommand("prog", []string{"echo", "hello", "world"}, bytes.NewReader(nil), stdout, stderr)
 	c.Check(exited, check.Equals, 0)
 	c.Check(stdout.String(), check.Equals, "hello world\n")
 	c.Check(stderr.String(), check.Equals, "")
@@ -46,7 +46,7 @@ func (s *CmdSuite) TestUsage(c *check.C) {
 	defer cmdtest.LeakCheck(c)()
 	stdout := bytes.NewBuffer(nil)
 	stderr := bytes.NewBuffer(nil)
-	exited := testCmd("prog", []string{"nosuchcommand", "hi"}, bytes.NewReader(nil), stdout, stderr)
+	exited := testCmd.RunCommand("prog", []string{"nosuchcommand", "hi"}, bytes.NewReader(nil), stdout, stderr)
 	c.Check(exited, check.Equals, 2)
 	c.Check(stdout.String(), check.Equals, "")
 	c.Check(stderr.String(), check.Matches, `(?ms)^unrecognized command "nosuchcommand"\n.*echo.*\n`)

commit 51bb28e3e62563fa0921f003b8d11ce8740f3a68
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Wed Dec 27 02:19:17 2017 -0500

    12876: Simplify legacy option parsing.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/cmd/arvados-client/cmd.go b/cmd/arvados-client/cmd.go
index b96cce3..73d772a 100644
--- a/cmd/arvados-client/cmd.go
+++ b/cmd/arvados-client/cmd.go
@@ -5,6 +5,7 @@
 package main
 
 import (
+	"flag"
 	"fmt"
 	"io"
 	"os"
@@ -13,17 +14,18 @@ import (
 
 	"git.curoverse.com/arvados.git/lib/cli"
 	"git.curoverse.com/arvados.git/lib/cmd"
+	"rsc.io/getopt"
 )
 
 var version = "dev"
 
-var Run = cmd.WithLateSubcommand(cmd.Multi(map[string]cmd.RunFunc{
+var Run = cmd.Multi(map[string]cmd.RunFunc{
 	"get":       cli.Get,
 	"-e":        cmdVersion,
 	"version":   cmdVersion,
 	"-version":  cmdVersion,
 	"--version": cmdVersion,
-}), []string{"f", "format"}, []string{"n", "dry-run", "v", "verbose", "s", "short"})
+})
 
 func cmdVersion(prog string, args []string, _ io.Reader, stdout, _ io.Writer) int {
 	prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
@@ -31,6 +33,19 @@ func cmdVersion(prog string, args []string, _ io.Reader, stdout, _ io.Writer) in
 	return 0
 }
 
+func fixLegacyArgs(args []string) []string {
+	flags := getopt.NewFlagSet("", flag.ContinueOnError)
+	flags.Bool("dry-run", false, "dry run")
+	flags.Alias("n", "dry-run")
+	flags.String("format", "json", "output format")
+	flags.Alias("f", "format")
+	flags.Bool("short", false, "short")
+	flags.Alias("s", "short")
+	flags.Bool("verbose", false, "verbose")
+	flags.Alias("v", "verbose")
+	return cmd.SubcommandToFront(args, flags)
+}
+
 func main() {
-	os.Exit(Run(os.Args[0], os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
+	os.Exit(Run(os.Args[0], fixLegacyArgs(os.Args[1:]), os.Stdin, os.Stdout, os.Stderr))
 }
diff --git a/lib/cli/get.go b/lib/cli/get.go
index d3177f3..baa1df7 100644
--- a/lib/cli/get.go
+++ b/lib/cli/get.go
@@ -12,6 +12,7 @@ import (
 
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"github.com/ghodss/yaml"
+	"rsc.io/getopt"
 )
 
 func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
@@ -22,17 +23,17 @@ func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer)
 		}
 	}()
 
-	flags := flag.NewFlagSet(prog, flag.ContinueOnError)
+	flags := getopt.NewFlagSet(prog, flag.ContinueOnError)
 	flags.SetOutput(stderr)
 
 	format := flags.String("format", "json", "output format (json, yaml, or uuid)")
-	flags.StringVar(format, "f", "json", "output format (json, yaml, or uuid)")
+	flags.Alias("f", "format")
 	short := flags.Bool("short", false, "equivalent to --format=uuid")
-	flags.BoolVar(short, "s", false, "equivalent to --format=uuid")
+	flags.Alias("s", "short")
 	flags.Bool("dry-run", false, "dry run (ignored, for compatibility)")
-	flags.Bool("n", false, "dry run (ignored, for compatibility)")
+	flags.Alias("n", "dry-run")
 	flags.Bool("verbose", false, "verbose (ignored, for compatibility)")
-	flags.Bool("v", false, "verbose (ignored, for compatibility)")
+	flags.Alias("v", "verbose")
 	err = flags.Parse(args)
 	if err != nil {
 		return 2
diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go
index 46d9965..f33354a 100644
--- a/lib/cmd/cmd.go
+++ b/lib/cmd/cmd.go
@@ -67,37 +67,33 @@ func multiUsage(stderr io.Writer, m map[string]RunFunc) {
 	}
 }
 
-// WithLateSubcommand wraps a RunFunc by skipping over some known
-// flags to find a subcommand, and moving that subcommand to the front
-// of the args before calling the wrapped RunFunc. For example:
+type FlagSet interface {
+	Init(string, flag.ErrorHandling)
+	NArg() int
+	Parse([]string) error
+	SetOutput(io.Writer)
+}
+
+// SubcommandToFront silently parses args using flagset, and returns a
+// copy of args with the first non-flag argument moved to the
+// front. If parsing fails or consumes all of args, args is returned
+// unchanged.
 //
-//	// Translate [           --format foo subcommand bar]
-//	//        to [subcommand --format foo            bar]
-//	WithLateSubcommand(fn, []string{"format"}, nil)
-func WithLateSubcommand(run RunFunc, argFlags, boolFlags []string) RunFunc {
-	return func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
-		flags := flag.NewFlagSet("prog", flag.ContinueOnError)
-		for _, arg := range argFlags {
-			flags.String(arg, "", "")
-		}
-		for _, arg := range boolFlags {
-			flags.Bool(arg, false, "")
-		}
-		// Ignore errors. We can't report a useful error
-		// message anyway.
-		flags.SetOutput(ioutil.Discard)
-		flags.Usage = func() {}
-		flags.Parse(args)
-		if flags.NArg() > 0 {
-			// Move the first arg after the recognized
-			// flags up to the front.
-			flagargs := len(args) - flags.NArg()
-			newargs := make([]string, len(args))
-			newargs[0] = args[flagargs]
-			copy(newargs[1:flagargs+1], args[:flagargs])
-			copy(newargs[flagargs+1:], args[flagargs+1:])
-			args = newargs
-		}
-		return run(prog, args, stdin, stdout, stderr)
+// SubcommandToFront invokes methods on flagset that have side
+// effects, including Parse. In typical usage, flagset will not used
+// for anything else after being passed to SubcommandToFront.
+func SubcommandToFront(args []string, flagset FlagSet) []string {
+	flagset.Init("", flag.ContinueOnError)
+	flagset.SetOutput(ioutil.Discard)
+	if err := flagset.Parse(args); err != nil || flagset.NArg() == 0 {
+		// No subcommand found.
+		return args
 	}
+	// Move subcommand to the front.
+	flagargs := len(args) - flagset.NArg()
+	newargs := make([]string, len(args))
+	newargs[0] = args[flagargs]
+	copy(newargs[1:flagargs+1], args[:flagargs])
+	copy(newargs[flagargs+1:], args[flagargs+1:])
+	return newargs
 }
diff --git a/lib/cmd/cmd_test.go b/lib/cmd/cmd_test.go
index 2d9228a..04e98d8 100644
--- a/lib/cmd/cmd_test.go
+++ b/lib/cmd/cmd_test.go
@@ -6,6 +6,7 @@ package cmd
 
 import (
 	"bytes"
+	"flag"
 	"fmt"
 	"io"
 	"strings"
@@ -51,13 +52,11 @@ func (s *CmdSuite) TestUsage(c *check.C) {
 	c.Check(stderr.String(), check.Matches, `(?ms)^unrecognized command "nosuchcommand"\n.*echo.*\n`)
 }
 
-func (s *CmdSuite) TestWithLateSubcommand(c *check.C) {
+func (s *CmdSuite) TestSubcommandToFront(c *check.C) {
 	defer cmdtest.LeakCheck(c)()
-	stdout := bytes.NewBuffer(nil)
-	stderr := bytes.NewBuffer(nil)
-	run := WithLateSubcommand(testCmd, []string{"format", "f"}, []string{"n"})
-	exited := run("prog", []string{"--format=yaml", "-n", "-format", "beep", "echo", "hi"}, bytes.NewReader(nil), stdout, stderr)
-	c.Check(exited, check.Equals, 0)
-	c.Check(stdout.String(), check.Equals, "--format=yaml -n -format beep hi\n")
-	c.Check(stderr.String(), check.Equals, "")
+	flags := flag.NewFlagSet("", flag.ContinueOnError)
+	flags.String("format", "json", "")
+	flags.Bool("n", false, "")
+	args := SubcommandToFront([]string{"--format=yaml", "-n", "-format", "beep", "echo", "hi"}, flags)
+	c.Check(args, check.DeepEquals, []string{"echo", "--format=yaml", "-n", "-format", "beep", "hi"})
 }

commit e7f41c4af1298b91106f9b1d8fb87a68e46a3f64
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Sun Dec 24 14:44:36 2017 -0500

    12876: Fix up error/usage messages.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/cli/get.go b/lib/cli/get.go
index a7adfb6..d3177f3 100644
--- a/lib/cli/get.go
+++ b/lib/cli/get.go
@@ -23,6 +23,8 @@ func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer)
 	}()
 
 	flags := flag.NewFlagSet(prog, flag.ContinueOnError)
+	flags.SetOutput(stderr)
+
 	format := flags.String("format", "json", "output format (json, yaml, or uuid)")
 	flags.StringVar(format, "f", "json", "output format (json, yaml, or uuid)")
 	short := flags.Bool("short", false, "equivalent to --format=uuid")
diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go
index a3d5ae8..46d9965 100644
--- a/lib/cmd/cmd.go
+++ b/lib/cmd/cmd.go
@@ -10,14 +10,17 @@ import (
 	"flag"
 	"fmt"
 	"io"
+	"io/ioutil"
+	"sort"
+	"strings"
 )
 
 // A RunFunc runs a command with the given args, and returns an exit
 // code.
 type RunFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
 
-// Multi returns a command that looks up its first argument in m, and
-// runs the resulting RunFunc with the remaining args.
+// Multi returns a RunFunc that looks up its first argument in m, and
+// invokes the resulting RunFunc with the remaining args.
 //
 // Example:
 //
@@ -32,11 +35,13 @@ type RunFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io
 func Multi(m map[string]RunFunc) RunFunc {
 	return func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
 		if len(args) < 1 {
-			fmt.Fprintf(stderr, "usage: %s command [args]", prog)
+			fmt.Fprintf(stderr, "usage: %s command [args]\n", prog)
+			multiUsage(stderr, m)
 			return 2
 		}
 		if cmd, ok := m[args[0]]; !ok {
-			fmt.Fprintf(stderr, "unrecognized command %q", args[0])
+			fmt.Fprintf(stderr, "unrecognized command %q\n", args[0])
+			multiUsage(stderr, m)
 			return 2
 		} else {
 			return cmd(prog+" "+args[0], args[1:], stdin, stdout, stderr)
@@ -44,6 +49,24 @@ func Multi(m map[string]RunFunc) RunFunc {
 	}
 }
 
+func multiUsage(stderr io.Writer, m map[string]RunFunc) {
+	var subcommands []string
+	for sc := range m {
+		if strings.HasPrefix(sc, "-") {
+			// Some subcommands have alternate versions
+			// like "--version" for compatibility. Don't
+			// clutter the subcommand summary with those.
+			continue
+		}
+		subcommands = append(subcommands, sc)
+	}
+	sort.Strings(subcommands)
+	fmt.Fprintf(stderr, "\nAvailable commands:\n")
+	for _, sc := range subcommands {
+		fmt.Fprintf(stderr, "    %s\n", sc)
+	}
+}
+
 // WithLateSubcommand wraps a RunFunc by skipping over some known
 // flags to find a subcommand, and moving that subcommand to the front
 // of the args before calling the wrapped RunFunc. For example:
@@ -62,6 +85,8 @@ func WithLateSubcommand(run RunFunc, argFlags, boolFlags []string) RunFunc {
 		}
 		// Ignore errors. We can't report a useful error
 		// message anyway.
+		flags.SetOutput(ioutil.Discard)
+		flags.Usage = func() {}
 		flags.Parse(args)
 		if flags.NArg() > 0 {
 			// Move the first arg after the recognized
diff --git a/lib/cmd/cmd_test.go b/lib/cmd/cmd_test.go
index fc20bef..2d9228a 100644
--- a/lib/cmd/cmd_test.go
+++ b/lib/cmd/cmd_test.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 	"testing"
 
+	"git.curoverse.com/arvados.git/lib/cmdtest"
 	check "gopkg.in/check.v1"
 )
 
@@ -31,6 +32,7 @@ var testCmd = Multi(map[string]RunFunc{
 })
 
 func (s *CmdSuite) TestHello(c *check.C) {
+	defer cmdtest.LeakCheck(c)()
 	stdout := bytes.NewBuffer(nil)
 	stderr := bytes.NewBuffer(nil)
 	exited := testCmd("prog", []string{"echo", "hello", "world"}, bytes.NewReader(nil), stdout, stderr)
@@ -39,7 +41,18 @@ func (s *CmdSuite) TestHello(c *check.C) {
 	c.Check(stderr.String(), check.Equals, "")
 }
 
+func (s *CmdSuite) TestUsage(c *check.C) {
+	defer cmdtest.LeakCheck(c)()
+	stdout := bytes.NewBuffer(nil)
+	stderr := bytes.NewBuffer(nil)
+	exited := testCmd("prog", []string{"nosuchcommand", "hi"}, bytes.NewReader(nil), stdout, stderr)
+	c.Check(exited, check.Equals, 2)
+	c.Check(stdout.String(), check.Equals, "")
+	c.Check(stderr.String(), check.Matches, `(?ms)^unrecognized command "nosuchcommand"\n.*echo.*\n`)
+}
+
 func (s *CmdSuite) TestWithLateSubcommand(c *check.C) {
+	defer cmdtest.LeakCheck(c)()
 	stdout := bytes.NewBuffer(nil)
 	stderr := bytes.NewBuffer(nil)
 	run := WithLateSubcommand(testCmd, []string{"format", "f"}, []string{"n"})
diff --git a/lib/cmdtest/leakcheck.go b/lib/cmdtest/leakcheck.go
new file mode 100644
index 0000000..c132f1b
--- /dev/null
+++ b/lib/cmdtest/leakcheck.go
@@ -0,0 +1,54 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+// Package cmdtest provides tools for testing command line tools.
+package cmdtest
+
+import (
+	"io"
+	"io/ioutil"
+	"os"
+
+	check "gopkg.in/check.v1"
+)
+
+// LeakCheck tests for output being leaked to os.Stdout and os.Stderr
+// that should be sent elsewhere (e.g., the stdout and stderr streams
+// passed to a cmd.RunFunc).
+//
+// It redirects os.Stdout and os.Stderr to a tempfile, and returns a
+// func, which the caller is expected to defer, that restores os.* and
+// checks that the tempfile is empty.
+//
+// Example:
+//
+//	func (s *Suite) TestSomething(c *check.C) {
+//		defer cmdtest.LeakCheck(c)()
+//		// ... do things that shouldn't print to os.Stderr or os.Stdout
+//	}
+func LeakCheck(c *check.C) func() {
+	tmpfiles := map[string]*os.File{"stdout": nil, "stderr": nil}
+	for i := range tmpfiles {
+		var err error
+		tmpfiles[i], err = ioutil.TempFile("", "")
+		c.Assert(err, check.IsNil)
+		err = os.Remove(tmpfiles[i].Name())
+		c.Assert(err, check.IsNil)
+	}
+
+	stdout, stderr := os.Stdout, os.Stderr
+	os.Stdout, os.Stderr = tmpfiles["stdout"], tmpfiles["stderr"]
+	return func() {
+		os.Stdout, os.Stderr = stdout, stderr
+
+		for i, tmpfile := range tmpfiles {
+			c.Log("checking %s", i)
+			_, err := tmpfile.Seek(0, io.SeekStart)
+			c.Assert(err, check.IsNil)
+			leaked, err := ioutil.ReadAll(tmpfile)
+			c.Assert(err, check.IsNil)
+			c.Check(string(leaked), check.Equals, "")
+		}
+	}
+}

commit 6def96daca7a5ec60dc067c04864bc58bd4aaf5c
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Sat Dec 23 01:26:28 2017 -0500

    12876: More compatibility with "arv get".
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/cmd/arvados-client/cmd.go b/cmd/arvados-client/cmd.go
index 0df1b0c..b96cce3 100644
--- a/cmd/arvados-client/cmd.go
+++ b/cmd/arvados-client/cmd.go
@@ -17,12 +17,13 @@ import (
 
 var version = "dev"
 
-var Run = cmd.Multi(map[string]cmd.RunFunc{
+var Run = cmd.WithLateSubcommand(cmd.Multi(map[string]cmd.RunFunc{
 	"get":       cli.Get,
+	"-e":        cmdVersion,
 	"version":   cmdVersion,
 	"-version":  cmdVersion,
 	"--version": cmdVersion,
-})
+}), []string{"f", "format"}, []string{"n", "dry-run", "v", "verbose", "s", "short"})
 
 func cmdVersion(prog string, args []string, _ io.Reader, stdout, _ io.Writer) int {
 	prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
diff --git a/lib/cli/get.go b/lib/cli/get.go
index 725c696..a7adfb6 100644
--- a/lib/cli/get.go
+++ b/lib/cli/get.go
@@ -23,7 +23,14 @@ func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer)
 	}()
 
 	flags := flag.NewFlagSet(prog, flag.ContinueOnError)
-	format := flags.String("format", "json", "output format (json or yaml)")
+	format := flags.String("format", "json", "output format (json, yaml, or uuid)")
+	flags.StringVar(format, "f", "json", "output format (json, yaml, or uuid)")
+	short := flags.Bool("short", false, "equivalent to --format=uuid")
+	flags.BoolVar(short, "s", false, "equivalent to --format=uuid")
+	flags.Bool("dry-run", false, "dry run (ignored, for compatibility)")
+	flags.Bool("n", false, "dry run (ignored, for compatibility)")
+	flags.Bool("verbose", false, "verbose (ignored, for compatibility)")
+	flags.Bool("v", false, "verbose (ignored, for compatibility)")
 	err = flags.Parse(args)
 	if err != nil {
 		return 2
@@ -32,6 +39,9 @@ func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer)
 		flags.Usage()
 		return 2
 	}
+	if *short {
+		*format = "uuid"
+	}
 
 	id := flags.Args()[0]
 	client := arvados.NewClientFromEnv()
@@ -52,6 +62,8 @@ func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer)
 		if err == nil {
 			_, err = stdout.Write(buf)
 		}
+	} else if *format == "uuid" {
+		fmt.Fprintln(stdout, obj["uuid"])
 	} else {
 		enc := json.NewEncoder(stdout)
 		enc.SetIndent("", "  ")
diff --git a/lib/cli/get_test.go b/lib/cli/get_test.go
new file mode 100644
index 0000000..9b8f1a0
--- /dev/null
+++ b/lib/cli/get_test.go
@@ -0,0 +1,33 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package cli
+
+import (
+	"bytes"
+	"regexp"
+	"testing"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvadostest"
+	check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+	check.TestingT(t)
+}
+
+var _ = check.Suite(&GetSuite{})
+
+type GetSuite struct{}
+
+func (s *GetSuite) TestGetCollectionJSON(c *check.C) {
+	stdout := bytes.NewBuffer(nil)
+	stderr := bytes.NewBuffer(nil)
+	exited := Get("arvados-client get", []string{arvadostest.FooCollection}, bytes.NewReader(nil), stdout, stderr)
+	c.Check(stdout.String(), check.Matches, `(?ms){.*"uuid": "`+arvadostest.FooCollection+`".*}\n`)
+	c.Check(stdout.String(), check.Matches, `(?ms){.*"portable_data_hash": "`+regexp.QuoteMeta(arvadostest.FooCollectionPDH)+`".*}\n`)
+	c.Check(stderr.String(), check.Equals, "")
+	c.Check(exited, check.Equals, 0)
+}

commit 243f15e69ed3a5514be89115d3ec231551d31cb8
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Sat Dec 23 01:14:11 2017 -0500

    12876: Util to accept subcommand after initial args.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-tests.sh b/build/run-tests.sh
index 5114cef..46418be 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -72,6 +72,7 @@ apps/workbench_profile
 cmd/arvados-client
 doc
 lib/cli
+lib/cmd
 lib/crunchstat
 services/api
 services/arv-git-httpd
@@ -834,6 +835,7 @@ declare -a gostuff
 gostuff=(
     cmd/arvados-client
     lib/cli
+    lib/cmd
     lib/crunchstat
     sdk/go/arvados
     sdk/go/arvadosclient
diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go
index 03b751a..a3d5ae8 100644
--- a/lib/cmd/cmd.go
+++ b/lib/cmd/cmd.go
@@ -7,6 +7,7 @@
 package cmd
 
 import (
+	"flag"
 	"fmt"
 	"io"
 )
@@ -42,3 +43,36 @@ func Multi(m map[string]RunFunc) RunFunc {
 		}
 	}
 }
+
+// WithLateSubcommand wraps a RunFunc by skipping over some known
+// flags to find a subcommand, and moving that subcommand to the front
+// of the args before calling the wrapped RunFunc. For example:
+//
+//	// Translate [           --format foo subcommand bar]
+//	//        to [subcommand --format foo            bar]
+//	WithLateSubcommand(fn, []string{"format"}, nil)
+func WithLateSubcommand(run RunFunc, argFlags, boolFlags []string) RunFunc {
+	return func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+		flags := flag.NewFlagSet("prog", flag.ContinueOnError)
+		for _, arg := range argFlags {
+			flags.String(arg, "", "")
+		}
+		for _, arg := range boolFlags {
+			flags.Bool(arg, false, "")
+		}
+		// Ignore errors. We can't report a useful error
+		// message anyway.
+		flags.Parse(args)
+		if flags.NArg() > 0 {
+			// Move the first arg after the recognized
+			// flags up to the front.
+			flagargs := len(args) - flags.NArg()
+			newargs := make([]string, len(args))
+			newargs[0] = args[flagargs]
+			copy(newargs[1:flagargs+1], args[:flagargs])
+			copy(newargs[flagargs+1:], args[flagargs+1:])
+			args = newargs
+		}
+		return run(prog, args, stdin, stdout, stderr)
+	}
+}
diff --git a/lib/cmd/cmd_test.go b/lib/cmd/cmd_test.go
new file mode 100644
index 0000000..fc20bef
--- /dev/null
+++ b/lib/cmd/cmd_test.go
@@ -0,0 +1,50 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package cmd
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"strings"
+	"testing"
+
+	check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+	check.TestingT(t)
+}
+
+var _ = check.Suite(&CmdSuite{})
+
+type CmdSuite struct{}
+
+var testCmd = Multi(map[string]RunFunc{
+	"echo": func(prog string, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int {
+		fmt.Fprintln(stdout, strings.Join(args, " "))
+		return 0
+	},
+})
+
+func (s *CmdSuite) TestHello(c *check.C) {
+	stdout := bytes.NewBuffer(nil)
+	stderr := bytes.NewBuffer(nil)
+	exited := testCmd("prog", []string{"echo", "hello", "world"}, bytes.NewReader(nil), stdout, stderr)
+	c.Check(exited, check.Equals, 0)
+	c.Check(stdout.String(), check.Equals, "hello world\n")
+	c.Check(stderr.String(), check.Equals, "")
+}
+
+func (s *CmdSuite) TestWithLateSubcommand(c *check.C) {
+	stdout := bytes.NewBuffer(nil)
+	stderr := bytes.NewBuffer(nil)
+	run := WithLateSubcommand(testCmd, []string{"format", "f"}, []string{"n"})
+	exited := run("prog", []string{"--format=yaml", "-n", "-format", "beep", "echo", "hi"}, bytes.NewReader(nil), stdout, stderr)
+	c.Check(exited, check.Equals, 0)
+	c.Check(stdout.String(), check.Equals, "--format=yaml -n -format beep hi\n")
+	c.Check(stderr.String(), check.Equals, "")
+}

commit 9c4901c09e0395899b268f9a734514ae7be4f9c9
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Fri Dec 22 09:45:58 2017 -0500

    12876: arvados-client get [-format=yaml] uuid
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-build-packages-one-target.sh b/build/run-build-packages-one-target.sh
index ff82e46..2af01e3 100755
--- a/build/run-build-packages-one-target.sh
+++ b/build/run-build-packages-one-target.sh
@@ -161,6 +161,7 @@ popd
 
 if test -z "$packages" ; then
     packages="arvados-api-server
+        arvados-client
         arvados-git-httpd
         arvados-node-manager
         arvados-src
diff --git a/build/run-build-packages.sh b/build/run-build-packages.sh
index 54f8b0a..c56b740 100755
--- a/build/run-build-packages.sh
+++ b/build/run-build-packages.sh
@@ -341,6 +341,8 @@ fi
 cd $WORKSPACE/packages/$TARGET
 export GOPATH=$(mktemp -d)
 go get github.com/kardianos/govendor
+package_go_binary cmd/arvados-client arvados-client \
+    "Arvados command line tool (beta)"
 package_go_binary sdk/go/crunchrunner crunchrunner \
     "Crunchrunner executes a command inside a container and uploads the output"
 package_go_binary services/arv-git-httpd arvados-git-httpd \
diff --git a/build/run-tests.sh b/build/run-tests.sh
index 7d6cb9e..5114cef 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -69,7 +69,10 @@ apps/workbench_functionals (*)
 apps/workbench_integration (*)
 apps/workbench_benchmark
 apps/workbench_profile
+cmd/arvados-client
 doc
+lib/cli
+lib/crunchstat
 services/api
 services/arv-git-httpd
 services/crunchstat
@@ -829,6 +832,9 @@ cd "$GOPATH/src/git.curoverse.com/arvados.git" && \
         fatal "govendor sync failed"
 declare -a gostuff
 gostuff=(
+    cmd/arvados-client
+    lib/cli
+    lib/crunchstat
     sdk/go/arvados
     sdk/go/arvadosclient
     sdk/go/blockdigest
@@ -839,7 +845,6 @@ gostuff=(
     sdk/go/asyncbuf
     sdk/go/crunchrunner
     sdk/go/stats
-    lib/crunchstat
     services/arv-git-httpd
     services/crunchstat
     services/health
diff --git a/cmd/arvados-client/.gitignore b/cmd/arvados-client/.gitignore
new file mode 100644
index 0000000..21dd863
--- /dev/null
+++ b/cmd/arvados-client/.gitignore
@@ -0,0 +1 @@
+arvados-*
diff --git a/cmd/arvados-client/cmd.go b/cmd/arvados-client/cmd.go
new file mode 100644
index 0000000..0df1b0c
--- /dev/null
+++ b/cmd/arvados-client/cmd.go
@@ -0,0 +1,35 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"regexp"
+	"runtime"
+
+	"git.curoverse.com/arvados.git/lib/cli"
+	"git.curoverse.com/arvados.git/lib/cmd"
+)
+
+var version = "dev"
+
+var Run = cmd.Multi(map[string]cmd.RunFunc{
+	"get":       cli.Get,
+	"version":   cmdVersion,
+	"-version":  cmdVersion,
+	"--version": cmdVersion,
+})
+
+func cmdVersion(prog string, args []string, _ io.Reader, stdout, _ io.Writer) int {
+	prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
+	fmt.Fprintf(stdout, "%s %s (%s)\n", prog, version, runtime.Version())
+	return 0
+}
+
+func main() {
+	os.Exit(Run(os.Args[0], os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
+}
diff --git a/cmd/arvados-client/cmd_test.go b/cmd/arvados-client/cmd_test.go
new file mode 100644
index 0000000..b1ab5ba
--- /dev/null
+++ b/cmd/arvados-client/cmd_test.go
@@ -0,0 +1,41 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"bytes"
+	"io/ioutil"
+	"testing"
+
+	check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+	check.TestingT(t)
+}
+
+var _ = check.Suite(&ClientSuite{})
+
+type ClientSuite struct{}
+
+func (s *ClientSuite) TestBadCommand(c *check.C) {
+	exited := Run("arvados-client", []string{"no such command"}, bytes.NewReader(nil), ioutil.Discard, ioutil.Discard)
+	c.Check(exited, check.Equals, 2)
+}
+
+func (s *ClientSuite) TestBadSubcommandArgs(c *check.C) {
+	exited := Run("arvados-client", []string{"get"}, bytes.NewReader(nil), ioutil.Discard, ioutil.Discard)
+	c.Check(exited, check.Equals, 2)
+}
+
+func (s *ClientSuite) TestVersion(c *check.C) {
+	stdout := bytes.NewBuffer(nil)
+	stderr := bytes.NewBuffer(nil)
+	exited := Run("arvados-client", []string{"version"}, bytes.NewReader(nil), stdout, stderr)
+	c.Check(exited, check.Equals, 0)
+	c.Check(stdout.String(), check.Matches, `arvados-client dev \(go[0-9\.]+\)\n`)
+	c.Check(stderr.String(), check.Equals, "")
+}
diff --git a/lib/cli/get.go b/lib/cli/get.go
new file mode 100644
index 0000000..725c696
--- /dev/null
+++ b/lib/cli/get.go
@@ -0,0 +1,65 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package cli
+
+import (
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"github.com/ghodss/yaml"
+)
+
+func Get(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	var err error
+	defer func() {
+		if err != nil {
+			fmt.Fprintf(stderr, "%s\n", err)
+		}
+	}()
+
+	flags := flag.NewFlagSet(prog, flag.ContinueOnError)
+	format := flags.String("format", "json", "output format (json or yaml)")
+	err = flags.Parse(args)
+	if err != nil {
+		return 2
+	}
+	if len(flags.Args()) != 1 {
+		flags.Usage()
+		return 2
+	}
+
+	id := flags.Args()[0]
+	client := arvados.NewClientFromEnv()
+	path, err := client.PathForUUID("show", id)
+	if err != nil {
+		return 1
+	}
+
+	var obj map[string]interface{}
+	err = client.RequestAndDecode(&obj, "GET", path, nil, nil)
+	if err != nil {
+		err = fmt.Errorf("GET %s: %s", path, err)
+		return 1
+	}
+	if *format == "yaml" {
+		var buf []byte
+		buf, err = yaml.Marshal(obj)
+		if err == nil {
+			_, err = stdout.Write(buf)
+		}
+	} else {
+		enc := json.NewEncoder(stdout)
+		enc.SetIndent("", "  ")
+		err = enc.Encode(obj)
+	}
+	if err != nil {
+		err = fmt.Errorf("encoding: %s", err)
+		return 1
+	}
+	return 0
+}
diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go
new file mode 100644
index 0000000..03b751a
--- /dev/null
+++ b/lib/cmd/cmd.go
@@ -0,0 +1,44 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+// package cmd defines a RunFunc type, representing a process that can
+// be invoked from a command line.
+package cmd
+
+import (
+	"fmt"
+	"io"
+)
+
+// A RunFunc runs a command with the given args, and returns an exit
+// code.
+type RunFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
+
+// Multi returns a command that looks up its first argument in m, and
+// runs the resulting RunFunc with the remaining args.
+//
+// Example:
+//
+//     os.Exit(Multi(map[string]RunFunc{
+//             "foobar": func(prog string, args []string) int {
+//                     fmt.Println(args[0])
+//                     return 2
+//             },
+//     })("/usr/bin/multi", []string{"foobar", "baz"}))
+//
+// ...prints "baz" and exits 2.
+func Multi(m map[string]RunFunc) RunFunc {
+	return func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+		if len(args) < 1 {
+			fmt.Fprintf(stderr, "usage: %s command [args]", prog)
+			return 2
+		}
+		if cmd, ok := m[args[0]]; !ok {
+			fmt.Fprintf(stderr, "unrecognized command %q", args[0])
+			return 2
+		} else {
+			return cmd(prog+" "+args[0], args[1:], stdin, stdout, stderr)
+		}
+	}
+}
diff --git a/sdk/go/arvadostest/fixtures.go b/sdk/go/arvadostest/fixtures.go
index 3a611a3..5e53065 100644
--- a/sdk/go/arvadostest/fixtures.go
+++ b/sdk/go/arvadostest/fixtures.go
@@ -16,6 +16,7 @@ const (
 	SpectatorUserUUID       = "zzzzz-tpzed-l1s2piq4t4mps8r"
 	UserAgreementCollection = "zzzzz-4zz18-uukreo9rbgwsujr" // user_agreement_in_anonymously_accessible_project
 	FooCollection           = "zzzzz-4zz18-fy296fx3hot09f7"
+	FooCollectionPDH        = "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"
 	NonexistentCollection   = "zzzzz-4zz18-totallynotexist"
 	HelloWorldCollection    = "zzzzz-4zz18-4en62shvi99lxd4"
 	FooBarDirCollection     = "zzzzz-4zz18-foonbarfilesdir"

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list