diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index be8e9068..dd285600 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -3,7 +3,7 @@ package main import ( "errors" "fmt" - "os" + "io" "strings" "github.com/containers/image/copy" @@ -11,100 +11,33 @@ import ( "github.com/containers/image/manifest" "github.com/containers/image/transports" "github.com/containers/image/transports/alltransports" - "github.com/containers/image/types" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/urfave/cli" ) -// contextsFromGlobalOptions returns source and destionation types.SystemContext depending on c. -func contextsFromGlobalOptions(c *cli.Context) (*types.SystemContext, *types.SystemContext, error) { - sourceCtx, err := contextFromGlobalOptions(c, "src-") - if err != nil { - return nil, nil, err - } - - destinationCtx, err := contextFromGlobalOptions(c, "dest-") - if err != nil { - return nil, nil, err - } - - return sourceCtx, destinationCtx, nil +type copyOptions struct { + global *globalOptions + srcImage *imageOptions + destImage *imageDestOptions + additionalTags cli.StringSlice // For docker-archive: destinations, in addition to the name:tag specified as destination, also add these + removeSignatures bool // Do not copy signatures from the source image + signByFingerprint string // Sign the image using a GPG key with the specified fingerprint + format optionalString // Force conversion of the image to a specified format } -func copyHandler(c *cli.Context) error { - if len(c.Args()) != 2 { - cli.ShowCommandHelp(c, "copy") - return errors.New("Exactly two arguments expected") +func copyCmd(global *globalOptions) cli.Command { + sharedFlags, sharedOpts := sharedImageFlags() + srcFlags, srcOpts := imageFlags(global, sharedOpts, "src-", "screds") + destFlags, destOpts := imageDestFlags(global, sharedOpts, "dest-", "dcreds") + opts := copyOptions{global: global, + srcImage: srcOpts, + destImage: destOpts, } - policyContext, err := getPolicyContext(c) - if err != nil { - return fmt.Errorf("Error loading trust policy: %v", err) - } - defer policyContext.Destroy() - - srcRef, err := alltransports.ParseImageName(c.Args()[0]) - if err != nil { - return fmt.Errorf("Invalid source name %s: %v", c.Args()[0], err) - } - destRef, err := alltransports.ParseImageName(c.Args()[1]) - if err != nil { - return fmt.Errorf("Invalid destination name %s: %v", c.Args()[1], err) - } - signBy := c.String("sign-by") - removeSignatures := c.Bool("remove-signatures") - - sourceCtx, destinationCtx, err := contextsFromGlobalOptions(c) - if err != nil { - return err - } - - var manifestType string - if c.IsSet("format") { - switch c.String("format") { - case "oci": - manifestType = imgspecv1.MediaTypeImageManifest - case "v2s1": - manifestType = manifest.DockerV2Schema1SignedMediaType - case "v2s2": - manifestType = manifest.DockerV2Schema2MediaType - default: - return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", c.String("format")) - } - } - - if c.IsSet("additional-tag") { - for _, image := range c.StringSlice("additional-tag") { - ref, err := reference.ParseNormalizedNamed(image) - if err != nil { - return fmt.Errorf("error parsing additional-tag '%s': %v", image, err) - } - namedTagged, isNamedTagged := ref.(reference.NamedTagged) - if !isNamedTagged { - return fmt.Errorf("additional-tag '%s' must be a tagged reference", image) - } - destinationCtx.DockerArchiveAdditionalTags = append(destinationCtx.DockerArchiveAdditionalTags, namedTagged) - } - } - - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) - defer cancel() - - _, err = copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{ - RemoveSignatures: removeSignatures, - SignBy: signBy, - ReportWriter: os.Stdout, - SourceCtx: sourceCtx, - DestinationCtx: destinationCtx, - ForceManifestMIMEType: manifestType, - }) - return err -} - -var copyCmd = cli.Command{ - Name: "copy", - Usage: "Copy an IMAGE-NAME from one location to another", - Description: fmt.Sprintf(` + return cli.Command{ + Name: "copy", + Usage: "Copy an IMAGE-NAME from one location to another", + Description: fmt.Sprintf(` Container "IMAGE-NAME" uses a "transport":"details" format. @@ -113,86 +46,99 @@ var copyCmd = cli.Command{ See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), - ArgsUsage: "SOURCE-IMAGE DESTINATION-IMAGE", - Action: copyHandler, - // FIXME: Do we need to namespace the GPG aspect? - Flags: []cli.Flag{ - cli.StringSliceFlag{ - Name: "additional-tag", - Usage: "additional tags (supports docker-archive)", - }, - cli.StringFlag{ - Name: "authfile", - Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", - }, - cli.BoolFlag{ - Name: "remove-signatures", - Usage: "Do not copy signatures from SOURCE-IMAGE", - }, - cli.StringFlag{ - Name: "sign-by", - Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", - }, - cli.StringFlag{ - Name: "src-creds, screds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the source registry", - }, - cli.StringFlag{ - Name: "dest-creds, dcreds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the destination registry", - }, - cli.StringFlag{ - Name: "src-cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the source registry or daemon", - }, - cli.BoolTFlag{ - Name: "src-tls-verify", - Usage: "require HTTPS and verify certificates when talking to the container source registry or daemon (defaults to true)", - }, - cli.StringFlag{ - Name: "dest-cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the destination registry or daemon", - }, - cli.BoolTFlag{ - Name: "dest-tls-verify", - Usage: "require HTTPS and verify certificates when talking to the container destination registry or daemon (defaults to true)", - }, - cli.StringFlag{ - Name: "dest-ostree-tmp-dir", - Value: "", - Usage: "`DIRECTORY` to use for OSTree temporary files", - }, - cli.StringFlag{ - Name: "src-shared-blob-dir", - Value: "", - Usage: "`DIRECTORY` to use to fetch retrieved blobs (OCI layout sources only)", - }, - cli.StringFlag{ - Name: "dest-shared-blob-dir", - Value: "", - Usage: "`DIRECTORY` to use to store retrieved blobs (OCI layout destinations only)", - }, - cli.StringFlag{ - Name: "format, f", - Usage: "`MANIFEST TYPE` (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)", - }, - cli.BoolFlag{ - Name: "dest-compress", - Usage: "Compress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source)", - }, - cli.StringFlag{ - Name: "src-daemon-host", - Value: "", - Usage: "use docker daemon host at `HOST` (docker-daemon sources only)", - }, - cli.StringFlag{ - Name: "dest-daemon-host", - Value: "", - Usage: "use docker daemon host at `HOST` (docker-daemon destinations only)", - }, - }, + ArgsUsage: "SOURCE-IMAGE DESTINATION-IMAGE", + Action: commandAction(opts.run), + // FIXME: Do we need to namespace the GPG aspect? + Flags: append(append(append([]cli.Flag{ + cli.StringSliceFlag{ + Name: "additional-tag", + Usage: "additional tags (supports docker-archive)", + Value: &opts.additionalTags, // Surprisingly StringSliceFlag does not support Destination:, but modifies Value: in place. + }, + cli.BoolFlag{ + Name: "remove-signatures", + Usage: "Do not copy signatures from SOURCE-IMAGE", + Destination: &opts.removeSignatures, + }, + cli.StringFlag{ + Name: "sign-by", + Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", + Destination: &opts.signByFingerprint, + }, + cli.GenericFlag{ + Name: "format, f", + Usage: "`MANIFEST TYPE` (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)", + Value: newOptionalStringValue(&opts.format), + }, + }, sharedFlags...), srcFlags...), destFlags...), + } +} + +func (opts *copyOptions) run(args []string, stdout io.Writer) error { + if len(args) != 2 { + return errorShouldDisplayUsage{errors.New("Exactly two arguments expected")} + } + + policyContext, err := opts.global.getPolicyContext() + if err != nil { + return fmt.Errorf("Error loading trust policy: %v", err) + } + defer policyContext.Destroy() + + srcRef, err := alltransports.ParseImageName(args[0]) + if err != nil { + return fmt.Errorf("Invalid source name %s: %v", args[0], err) + } + destRef, err := alltransports.ParseImageName(args[1]) + if err != nil { + return fmt.Errorf("Invalid destination name %s: %v", args[1], err) + } + + sourceCtx, err := opts.srcImage.newSystemContext() + if err != nil { + return err + } + destinationCtx, err := opts.destImage.newSystemContext() + if err != nil { + return err + } + + var manifestType string + if opts.format.present { + switch opts.format.value { + case "oci": + manifestType = imgspecv1.MediaTypeImageManifest + case "v2s1": + manifestType = manifest.DockerV2Schema1SignedMediaType + case "v2s2": + manifestType = manifest.DockerV2Schema2MediaType + default: + return fmt.Errorf("unknown format %q. Choose one of the supported formats: 'oci', 'v2s1', or 'v2s2'", opts.format.value) + } + } + + for _, image := range opts.additionalTags { + ref, err := reference.ParseNormalizedNamed(image) + if err != nil { + return fmt.Errorf("error parsing additional-tag '%s': %v", image, err) + } + namedTagged, isNamedTagged := ref.(reference.NamedTagged) + if !isNamedTagged { + return fmt.Errorf("additional-tag '%s' must be a tagged reference", image) + } + destinationCtx.DockerArchiveAdditionalTags = append(destinationCtx.DockerArchiveAdditionalTags, namedTagged) + } + + ctx, cancel := opts.global.commandTimeoutContext() + defer cancel() + + _, err = copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{ + RemoveSignatures: opts.removeSignatures, + SignBy: opts.signByFingerprint, + ReportWriter: stdout, + SourceCtx: sourceCtx, + DestinationCtx: destinationCtx, + ForceManifestMIMEType: manifestType, + }) + return err } diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index 5cc7e203..07a7cea2 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -3,6 +3,7 @@ package main import ( "errors" "fmt" + "io" "strings" "github.com/containers/image/transports" @@ -10,30 +11,22 @@ import ( "github.com/urfave/cli" ) -func deleteHandler(c *cli.Context) error { - if len(c.Args()) != 1 { - return errors.New("Usage: delete imageReference") - } - - ref, err := alltransports.ParseImageName(c.Args()[0]) - if err != nil { - return fmt.Errorf("Invalid source name %s: %v", c.Args()[0], err) - } - - sys, err := contextFromGlobalOptions(c, "") - if err != nil { - return err - } - - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) - defer cancel() - return ref.DeleteImage(ctx, sys) +type deleteOptions struct { + global *globalOptions + image *imageOptions } -var deleteCmd = cli.Command{ - Name: "delete", - Usage: "Delete image IMAGE-NAME", - Description: fmt.Sprintf(` +func deleteCmd(global *globalOptions) cli.Command { + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageFlags(global, sharedOpts, "", "") + opts := deleteOptions{ + global: global, + image: imageOpts, + } + return cli.Command{ + Name: "delete", + Usage: "Delete image IMAGE-NAME", + Description: fmt.Sprintf(` Delete an "IMAGE_NAME" from a transport Supported transports: @@ -41,26 +34,28 @@ var deleteCmd = cli.Command{ See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), - ArgsUsage: "IMAGE-NAME", - Action: deleteHandler, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "authfile", - Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", - }, - cli.StringFlag{ - Name: "creds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", - }, - cli.StringFlag{ - Name: "cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry", - }, - cli.BoolTFlag{ - Name: "tls-verify", - Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", - }, - }, + ArgsUsage: "IMAGE-NAME", + Action: commandAction(opts.run), + Flags: append(sharedFlags, imageFlags...), + } +} + +func (opts *deleteOptions) run(args []string, stdout io.Writer) error { + if len(args) != 1 { + return errors.New("Usage: delete imageReference") + } + + ref, err := alltransports.ParseImageName(args[0]) + if err != nil { + return fmt.Errorf("Invalid source name %s: %v", args[0], err) + } + + sys, err := opts.image.newSystemContext() + if err != nil { + return err + } + + ctx, cancel := opts.global.commandTimeoutContext() + defer cancel() + return ref.DeleteImage(ctx, sys) } diff --git a/cmd/skopeo/flag.go b/cmd/skopeo/flag.go new file mode 100644 index 00000000..89774ebd --- /dev/null +++ b/cmd/skopeo/flag.go @@ -0,0 +1,75 @@ +package main + +import ( + "strconv" + + "github.com/urfave/cli" +) + +// optionalBool is a boolean with a separate presence flag. +type optionalBool struct { + present bool + value bool +} + +// optionalBool is a cli.Generic == flag.Value implementation equivalent to +// the one underlying flag.Bool, except that it records whether the flag has been set. +// This is distinct from optionalBool to (pretend to) force callers to use +// newOptionalBool +type optionalBoolValue optionalBool + +func newOptionalBoolValue(p *optionalBool) cli.Generic { + p.present = false + return (*optionalBoolValue)(p) +} + +func (ob *optionalBoolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + return err + } + ob.value = v + ob.present = true + return nil +} + +func (ob *optionalBoolValue) String() string { + if !ob.present { + return "" // This is, sadly, not round-trip safe: --flag is interpreted as --flag=true + } + return strconv.FormatBool(ob.value) +} + +func (ob *optionalBoolValue) IsBoolFlag() bool { + return true +} + +// optionalString is a string with a separate presence flag. +type optionalString struct { + present bool + value string +} + +// optionalString is a cli.Generic == flag.Value implementation equivalent to +// the one underlying flag.String, except that it records whether the flag has been set. +// This is distinct from optionalString to (pretend to) force callers to use +// newoptionalString +type optionalStringValue optionalString + +func newOptionalStringValue(p *optionalString) cli.Generic { + p.present = false + return (*optionalStringValue)(p) +} + +func (ob *optionalStringValue) Set(s string) error { + ob.value = s + ob.present = true + return nil +} + +func (ob *optionalStringValue) String() string { + if !ob.present { + return "" // This is, sadly, not round-trip safe: --flag= is interpreted as {present:true, value:""} + } + return ob.value +} diff --git a/cmd/skopeo/flag_test.go b/cmd/skopeo/flag_test.go new file mode 100644 index 00000000..4f75effa --- /dev/null +++ b/cmd/skopeo/flag_test.go @@ -0,0 +1,239 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli" +) + +func TestOptionalBoolSet(t *testing.T) { + for _, c := range []struct { + input string + accepted bool + value bool + }{ + // Valid inputs documented for strconv.ParseBool == flag.BoolVar + {"1", true, true}, + {"t", true, true}, + {"T", true, true}, + {"TRUE", true, true}, + {"true", true, true}, + {"True", true, true}, + {"0", true, false}, + {"f", true, false}, + {"F", true, false}, + {"FALSE", true, false}, + {"false", true, false}, + {"False", true, false}, + // A few invalid inputs + {"", false, false}, + {"yes", false, false}, + {"no", false, false}, + {"2", false, false}, + } { + var ob optionalBool + v := newOptionalBoolValue(&ob) + require.False(t, ob.present) + err := v.Set(c.input) + if c.accepted { + assert.NoError(t, err, c.input) + assert.Equal(t, c.value, ob.value) + } else { + assert.Error(t, err, c.input) + assert.False(t, ob.present) // Just to be extra paranoid. + } + } + + // Nothing actually explicitly says that .Set() is never called when the flag is not present on the command line; + // so, check that it is not being called, at least in the straightforward case (it's not possible to test that it + // is not called in any possible situation). + var globalOB, commandOB optionalBool + actionRun := false + app := cli.NewApp() + app.EnableBashCompletion = true + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "global-OB", + Value: newOptionalBoolValue(&globalOB), + }, + } + app.Commands = []cli.Command{{ + Name: "cmd", + Flags: []cli.Flag{ + cli.GenericFlag{ + Name: "command-OB", + Value: newOptionalBoolValue(&commandOB), + }, + }, + Action: func(*cli.Context) error { + assert.False(t, globalOB.present) + assert.False(t, commandOB.present) + actionRun = true + return nil + }, + }} + err := app.Run([]string{"app", "cmd"}) + require.NoError(t, err) + assert.True(t, actionRun) +} + +func TestOptionalBoolString(t *testing.T) { + for _, c := range []struct { + input optionalBool + expected string + }{ + {optionalBool{present: true, value: true}, "true"}, + {optionalBool{present: true, value: false}, "false"}, + {optionalBool{present: false, value: true}, ""}, + {optionalBool{present: false, value: false}, ""}, + } { + var ob optionalBool + v := newOptionalBoolValue(&ob) + ob = c.input + res := v.String() + assert.Equal(t, c.expected, res) + } +} + +func TestOptionalBoolIsBoolFlag(t *testing.T) { + // IsBoolFlag means that the argument value must either be part of the same argument, with =; + // if there is no =, the value is set to true. + // This differs form other flags, where the argument is required and may be either separated with = or supplied in the next argument. + for _, c := range []struct { + input []string + expectedOB optionalBool + expectedArgs []string + }{ + {[]string{"1", "2"}, optionalBool{present: false}, []string{"1", "2"}}, // Flag not present + {[]string{"--OB=true", "1", "2"}, optionalBool{present: true, value: true}, []string{"1", "2"}}, // --OB=true + {[]string{"--OB=false", "1", "2"}, optionalBool{present: true, value: false}, []string{"1", "2"}}, // --OB=false + {[]string{"--OB", "true", "1", "2"}, optionalBool{present: true, value: true}, []string{"true", "1", "2"}}, // --OB true + {[]string{"--OB", "false", "1", "2"}, optionalBool{present: true, value: true}, []string{"false", "1", "2"}}, // --OB false + } { + var ob optionalBool + actionRun := false + app := cli.NewApp() + app.Commands = []cli.Command{{ + Name: "cmd", + Flags: []cli.Flag{ + cli.GenericFlag{ + Name: "OB", + Value: newOptionalBoolValue(&ob), + }, + }, + Action: func(ctx *cli.Context) error { + assert.Equal(t, c.expectedOB, ob) + assert.Equal(t, c.expectedArgs, ([]string)(ctx.Args())) + actionRun = true + return nil + }, + }} + err := app.Run(append([]string{"app", "cmd"}, c.input...)) + require.NoError(t, err) + assert.True(t, actionRun) + } +} + +func TestOptionalStringSet(t *testing.T) { + // Really just a smoke test, but differentiating between not present and empty. + for _, c := range []string{"", "hello"} { + var os optionalString + v := newOptionalStringValue(&os) + require.False(t, os.present) + err := v.Set(c) + assert.NoError(t, err, c) + assert.Equal(t, c, os.value) + } + + // Nothing actually explicitly says that .Set() is never called when the flag is not present on the command line; + // so, check that it is not being called, at least in the straightforward case (it's not possible to test that it + // is not called in any possible situation). + var globalOS, commandOS optionalString + actionRun := false + app := cli.NewApp() + app.EnableBashCompletion = true + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "global-OS", + Value: newOptionalStringValue(&globalOS), + }, + } + app.Commands = []cli.Command{{ + Name: "cmd", + Flags: []cli.Flag{ + cli.GenericFlag{ + Name: "command-OS", + Value: newOptionalStringValue(&commandOS), + }, + }, + Action: func(*cli.Context) error { + assert.False(t, globalOS.present) + assert.False(t, commandOS.present) + actionRun = true + return nil + }, + }} + err := app.Run([]string{"app", "cmd"}) + require.NoError(t, err) + assert.True(t, actionRun) +} + +func TestOptionalStringString(t *testing.T) { + for _, c := range []struct { + input optionalString + expected string + }{ + {optionalString{present: true, value: "hello"}, "hello"}, + {optionalString{present: true, value: ""}, ""}, + {optionalString{present: false, value: "hello"}, ""}, + {optionalString{present: false, value: ""}, ""}, + } { + var os optionalString + v := newOptionalStringValue(&os) + os = c.input + res := v.String() + assert.Equal(t, c.expected, res) + } +} + +func TestOptionalStringIsBoolFlag(t *testing.T) { + // NOTE: optionalStringValue does not implement IsBoolFlag! + // IsBoolFlag means that the argument value must either be part of the same argument, with =; + // if there is no =, the value is set to true. + // This differs form other flags, where the argument is required and may be either separated with = or supplied in the next argument. + for _, c := range []struct { + input []string + expectedOS optionalString + expectedArgs []string + }{ + {[]string{"1", "2"}, optionalString{present: false}, []string{"1", "2"}}, // Flag not present + {[]string{"--OS=hello", "1", "2"}, optionalString{present: true, value: "hello"}, []string{"1", "2"}}, // --OS=true + {[]string{"--OS=", "1", "2"}, optionalString{present: true, value: ""}, []string{"1", "2"}}, // --OS=false + {[]string{"--OS", "hello", "1", "2"}, optionalString{present: true, value: "hello"}, []string{"1", "2"}}, // --OS true + {[]string{"--OS", "", "1", "2"}, optionalString{present: true, value: ""}, []string{"1", "2"}}, // --OS false + } { + var os optionalString + actionRun := false + app := cli.NewApp() + app.Commands = []cli.Command{{ + Name: "cmd", + Flags: []cli.Flag{ + cli.GenericFlag{ + Name: "OS", + Value: newOptionalStringValue(&os), + }, + }, + Action: func(ctx *cli.Context) error { + assert.Equal(t, c.expectedOS, os) + assert.Equal(t, c.expectedArgs, ([]string)(ctx.Args())) + actionRun = true + return nil + }, + }} + err := app.Run(append([]string{"app", "cmd"}, c.input...)) + require.NoError(t, err) + assert.True(t, actionRun) + } +} diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 2238e436..9c524b5f 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "io" "strings" "time" @@ -29,10 +30,23 @@ type inspectOutput struct { Layers []string } -var inspectCmd = cli.Command{ - Name: "inspect", - Usage: "Inspect image IMAGE-NAME", - Description: fmt.Sprintf(` +type inspectOptions struct { + global *globalOptions + image *imageOptions + raw bool // Output the raw manifest instead of parsing information about the image +} + +func inspectCmd(global *globalOptions) cli.Command { + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageFlags(global, sharedOpts, "", "") + opts := inspectOptions{ + global: global, + image: imageOpts, + } + return cli.Command{ + Name: "inspect", + Usage: "Inspect image IMAGE-NAME", + Description: fmt.Sprintf(` Return low-level information about "IMAGE-NAME" in a registry/transport Supported transports: @@ -40,101 +54,90 @@ var inspectCmd = cli.Command{ See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), - ArgsUsage: "IMAGE-NAME", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "authfile", - Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", - }, - cli.StringFlag{ - Name: "cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry", - }, - cli.BoolTFlag{ - Name: "tls-verify", - Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", - }, - cli.BoolFlag{ - Name: "raw", - Usage: "output raw manifest", - }, - cli.StringFlag{ - Name: "creds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", - }, - }, - Action: func(c *cli.Context) (retErr error) { - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) - defer cancel() - - img, err := parseImage(ctx, c) - if err != nil { - return err - } - - defer func() { - if err := img.Close(); err != nil { - retErr = errors.Wrapf(retErr, fmt.Sprintf("(could not close image: %v) ", err)) - } - }() - - rawManifest, _, err := img.Manifest(ctx) - if err != nil { - return err - } - if c.Bool("raw") { - _, err := c.App.Writer.Write(rawManifest) - if err != nil { - return fmt.Errorf("Error writing manifest to standard output: %v", err) - } - return nil - } - imgInspect, err := img.Inspect(ctx) - if err != nil { - return err - } - outputData := inspectOutput{ - Name: "", // Set below if DockerReference() is known - Tag: imgInspect.Tag, - // Digest is set below. - RepoTags: []string{}, // Possibly overriden for docker.Transport. - Created: imgInspect.Created, - DockerVersion: imgInspect.DockerVersion, - Labels: imgInspect.Labels, - Architecture: imgInspect.Architecture, - Os: imgInspect.Os, - Layers: imgInspect.Layers, - } - outputData.Digest, err = manifest.Digest(rawManifest) - if err != nil { - return fmt.Errorf("Error computing manifest digest: %v", err) - } - if dockerRef := img.Reference().DockerReference(); dockerRef != nil { - outputData.Name = dockerRef.Name() - } - if img.Reference().Transport() == docker.Transport { - sys, err := contextFromGlobalOptions(c, "") - if err != nil { - return err - } - outputData.RepoTags, err = docker.GetRepositoryTags(ctx, sys, img.Reference()) - if err != nil { - // some registries may decide to block the "list all tags" endpoint - // gracefully allow the inspect to continue in this case. Currently - // the IBM Bluemix container registry has this restriction. - if !strings.Contains(err.Error(), "401") { - return fmt.Errorf("Error determining repository tags: %v", err) - } - logrus.Warnf("Registry disallows tag list retrieval; skipping") - } - } - out, err := json.MarshalIndent(outputData, "", " ") - if err != nil { - return err - } - fmt.Fprintln(c.App.Writer, string(out)) - return nil - }, + ArgsUsage: "IMAGE-NAME", + Flags: append(append([]cli.Flag{ + cli.BoolFlag{ + Name: "raw", + Usage: "output raw manifest", + Destination: &opts.raw, + }, + }, sharedFlags...), imageFlags...), + Action: commandAction(opts.run), + } +} + +func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) { + ctx, cancel := opts.global.commandTimeoutContext() + defer cancel() + + if len(args) != 1 { + return errors.New("Exactly one argument expected") + } + img, err := parseImage(ctx, opts.image, args[0]) + if err != nil { + return err + } + + defer func() { + if err := img.Close(); err != nil { + retErr = errors.Wrapf(retErr, fmt.Sprintf("(could not close image: %v) ", err)) + } + }() + + rawManifest, _, err := img.Manifest(ctx) + if err != nil { + return err + } + if opts.raw { + _, err := stdout.Write(rawManifest) + if err != nil { + return fmt.Errorf("Error writing manifest to standard output: %v", err) + } + return nil + } + imgInspect, err := img.Inspect(ctx) + if err != nil { + return err + } + outputData := inspectOutput{ + Name: "", // Set below if DockerReference() is known + Tag: imgInspect.Tag, + // Digest is set below. + RepoTags: []string{}, // Possibly overriden for docker.Transport. + Created: imgInspect.Created, + DockerVersion: imgInspect.DockerVersion, + Labels: imgInspect.Labels, + Architecture: imgInspect.Architecture, + Os: imgInspect.Os, + Layers: imgInspect.Layers, + } + outputData.Digest, err = manifest.Digest(rawManifest) + if err != nil { + return fmt.Errorf("Error computing manifest digest: %v", err) + } + if dockerRef := img.Reference().DockerReference(); dockerRef != nil { + outputData.Name = dockerRef.Name() + } + if img.Reference().Transport() == docker.Transport { + sys, err := opts.image.newSystemContext() + if err != nil { + return err + } + outputData.RepoTags, err = docker.GetRepositoryTags(ctx, sys, img.Reference()) + if err != nil { + // some registries may decide to block the "list all tags" endpoint + // gracefully allow the inspect to continue in this case. Currently + // the IBM Bluemix container registry has this restriction. + if !strings.Contains(err.Error(), "401") { + return fmt.Errorf("Error determining repository tags: %v", err) + } + logrus.Warnf("Registry disallows tag list retrieval; skipping") + } + } + out, err := json.MarshalIndent(outputData, "", " ") + if err != nil { + return err + } + fmt.Fprintf(stdout, "%s\n", string(out)) + return nil } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index ec95c795..5c236dc3 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "io/ioutil" "os" "strings" @@ -15,114 +16,130 @@ import ( "github.com/urfave/cli" ) -var layersCmd = cli.Command{ - Name: "layers", - Usage: "Get layers of IMAGE-NAME", - ArgsUsage: "IMAGE-NAME [LAYER...]", - Hidden: true, - Action: func(c *cli.Context) (retErr error) { - fmt.Fprintln(os.Stderr, `DEPRECATED: skopeo layers is deprecated in favor of skopeo copy`) - if c.NArg() == 0 { - return errors.New("Usage: layers imageReference [layer...]") +type layersOptions struct { + global *globalOptions + image *imageOptions +} + +func layersCmd(global *globalOptions) cli.Command { + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageFlags(global, sharedOpts, "", "") + opts := layersOptions{ + global: global, + image: imageOpts, + } + return cli.Command{ + Name: "layers", + Usage: "Get layers of IMAGE-NAME", + ArgsUsage: "IMAGE-NAME [LAYER...]", + Hidden: true, + Action: commandAction(opts.run), + Flags: append(sharedFlags, imageFlags...), + } +} + +func (opts *layersOptions) run(args []string, stdout io.Writer) (retErr error) { + fmt.Fprintln(os.Stderr, `DEPRECATED: skopeo layers is deprecated in favor of skopeo copy`) + if len(args) == 0 { + return errors.New("Usage: layers imageReference [layer...]") + } + + ctx, cancel := opts.global.commandTimeoutContext() + defer cancel() + + sys, err := opts.image.newSystemContext() + if err != nil { + return err + } + cache := blobinfocache.DefaultCache(sys) + rawSource, err := parseImageSource(ctx, opts.image, args[0]) + if err != nil { + return err + } + src, err := image.FromSource(ctx, sys, rawSource) + if err != nil { + if closeErr := rawSource.Close(); closeErr != nil { + return errors.Wrapf(err, " (close error: %v)", closeErr) } - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) - defer cancel() + return err + } + defer func() { + if err := src.Close(); err != nil { + retErr = errors.Wrapf(retErr, " (close error: %v)", err) + } + }() - sys, err := contextFromGlobalOptions(c, "") + type blobDigest struct { + digest digest.Digest + isConfig bool + } + var blobDigests []blobDigest + for _, dString := range args[1:] { + if !strings.HasPrefix(dString, "sha256:") { + dString = "sha256:" + dString + } + d, err := digest.Parse(dString) if err != nil { return err } - cache := blobinfocache.DefaultCache(sys) - rawSource, err := parseImageSource(ctx, c, c.Args()[0]) + blobDigests = append(blobDigests, blobDigest{digest: d, isConfig: false}) + } + + if len(blobDigests) == 0 { + layers := src.LayerInfos() + seenLayers := map[digest.Digest]struct{}{} + for _, info := range layers { + if _, ok := seenLayers[info.Digest]; !ok { + blobDigests = append(blobDigests, blobDigest{digest: info.Digest, isConfig: false}) + seenLayers[info.Digest] = struct{}{} + } + } + configInfo := src.ConfigInfo() + if configInfo.Digest != "" { + blobDigests = append(blobDigests, blobDigest{digest: configInfo.Digest, isConfig: true}) + } + } + + tmpDir, err := ioutil.TempDir(".", "layers-") + if err != nil { + return err + } + tmpDirRef, err := directory.NewReference(tmpDir) + if err != nil { + return err + } + dest, err := tmpDirRef.NewImageDestination(ctx, nil) + if err != nil { + return err + } + + defer func() { + if err := dest.Close(); err != nil { + retErr = errors.Wrapf(retErr, " (close error: %v)", err) + } + }() + + for _, bd := range blobDigests { + r, blobSize, err := rawSource.GetBlob(ctx, types.BlobInfo{Digest: bd.digest, Size: -1}, cache) if err != nil { return err } - src, err := image.FromSource(ctx, sys, rawSource) - if err != nil { - if closeErr := rawSource.Close(); closeErr != nil { + if _, err := dest.PutBlob(ctx, r, types.BlobInfo{Digest: bd.digest, Size: blobSize}, cache, bd.isConfig); err != nil { + if closeErr := r.Close(); closeErr != nil { return errors.Wrapf(err, " (close error: %v)", closeErr) } - return err } - defer func() { - if err := src.Close(); err != nil { - retErr = errors.Wrapf(retErr, " (close error: %v)", err) - } - }() + } - type blobDigest struct { - digest digest.Digest - isConfig bool - } - var blobDigests []blobDigest - for _, dString := range c.Args().Tail() { - if !strings.HasPrefix(dString, "sha256:") { - dString = "sha256:" + dString - } - d, err := digest.Parse(dString) - if err != nil { - return err - } - blobDigests = append(blobDigests, blobDigest{digest: d, isConfig: false}) - } + manifest, _, err := src.Manifest(ctx) + if err != nil { + return err + } + if err := dest.PutManifest(ctx, manifest); err != nil { + return err + } - if len(blobDigests) == 0 { - layers := src.LayerInfos() - seenLayers := map[digest.Digest]struct{}{} - for _, info := range layers { - if _, ok := seenLayers[info.Digest]; !ok { - blobDigests = append(blobDigests, blobDigest{digest: info.Digest, isConfig: false}) - seenLayers[info.Digest] = struct{}{} - } - } - configInfo := src.ConfigInfo() - if configInfo.Digest != "" { - blobDigests = append(blobDigests, blobDigest{digest: configInfo.Digest, isConfig: true}) - } - } - - tmpDir, err := ioutil.TempDir(".", "layers-") - if err != nil { - return err - } - tmpDirRef, err := directory.NewReference(tmpDir) - if err != nil { - return err - } - dest, err := tmpDirRef.NewImageDestination(ctx, nil) - if err != nil { - return err - } - - defer func() { - if err := dest.Close(); err != nil { - retErr = errors.Wrapf(retErr, " (close error: %v)", err) - } - }() - - for _, bd := range blobDigests { - r, blobSize, err := rawSource.GetBlob(ctx, types.BlobInfo{Digest: bd.digest, Size: -1}, cache) - if err != nil { - return err - } - if _, err := dest.PutBlob(ctx, r, types.BlobInfo{Digest: bd.digest, Size: blobSize}, cache, bd.isConfig); err != nil { - if closeErr := r.Close(); closeErr != nil { - return errors.Wrapf(err, " (close error: %v)", closeErr) - } - return err - } - } - - manifest, _, err := src.Manifest(ctx) - if err != nil { - return err - } - if err := dest.PutManifest(ctx, manifest); err != nil { - return err - } - - return dest.Commit(ctx) - }, + return dest.Commit(ctx) } diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index 78fa2af3..fb56568c 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -1,8 +1,10 @@ package main import ( + "context" "fmt" "os" + "time" "github.com/containers/image/signature" "github.com/containers/skopeo/version" @@ -15,8 +17,21 @@ import ( // and will be populated by the Makefile var gitCommit = "" -// createApp returns a cli.App to be run or tested. -func createApp() *cli.App { +type globalOptions struct { + debug bool // Enable debug output + tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) + policyPath string // Path to a signature verification policy file + insecurePolicy bool // Use an "allow everything" signature verification policy + registriesDirPath string // Path to a "registries.d" registry configuratio directory + overrideArch string // Architecture to use for choosing images, instead of the runtime one + overrideOS string // OS to use for choosing images, instead of the runtime one + commandTimeout time.Duration // Timeout for the command execution +} + +// createApp returns a cli.App, and the underlying globalOptions object, to be run or tested. +func createApp() (*cli.App, *globalOptions) { + opts := globalOptions{} + app := cli.NewApp() app.EnableBashCompletion = true app.Name = "skopeo" @@ -28,89 +43,106 @@ func createApp() *cli.App { app.Usage = "Various operations with container images and container image registries" app.Flags = []cli.Flag{ cli.BoolFlag{ - Name: "debug", - Usage: "enable debug output", + Name: "debug", + Usage: "enable debug output", + Destination: &opts.debug, }, - cli.BoolTFlag{ + cli.GenericFlag{ Name: "tls-verify", Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", Hidden: true, + Value: newOptionalBoolValue(&opts.tlsVerify), }, cli.StringFlag{ - Name: "policy", - Value: "", - Usage: "Path to a trust policy file", + Name: "policy", + Usage: "Path to a trust policy file", + Destination: &opts.policyPath, }, cli.BoolFlag{ - Name: "insecure-policy", - Usage: "run the tool without any policy check", + Name: "insecure-policy", + Usage: "run the tool without any policy check", + Destination: &opts.insecurePolicy, }, cli.StringFlag{ - Name: "registries.d", - Value: "", - Usage: "use registry configuration files in `DIR` (e.g. for container signature storage)", + Name: "registries.d", + Usage: "use registry configuration files in `DIR` (e.g. for container signature storage)", + Destination: &opts.registriesDirPath, }, cli.StringFlag{ - Name: "override-arch", - Value: "", - Usage: "use `ARCH` instead of the architecture of the machine for choosing images", + Name: "override-arch", + Usage: "use `ARCH` instead of the architecture of the machine for choosing images", + Destination: &opts.overrideArch, }, cli.StringFlag{ - Name: "override-os", - Value: "", - Usage: "use `OS` instead of the running OS for choosing images", + Name: "override-os", + Usage: "use `OS` instead of the running OS for choosing images", + Destination: &opts.overrideOS, }, cli.DurationFlag{ - Name: "command-timeout", - Usage: "timeout for the command execution", + Name: "command-timeout", + Usage: "timeout for the command execution", + Destination: &opts.commandTimeout, }, } - app.Before = func(c *cli.Context) error { - if c.GlobalBool("debug") { - logrus.SetLevel(logrus.DebugLevel) - } - if c.GlobalIsSet("tls-verify") { - logrus.Warn("'--tls-verify' is deprecated, please set this on the specific subcommand") - } - return nil - } + app.Before = opts.before app.Commands = []cli.Command{ - copyCmd, - inspectCmd, - layersCmd, - deleteCmd, - manifestDigestCmd, - standaloneSignCmd, - standaloneVerifyCmd, - untrustedSignatureDumpCmd, + copyCmd(&opts), + inspectCmd(&opts), + layersCmd(&opts), + deleteCmd(&opts), + manifestDigestCmd(), + standaloneSignCmd(), + standaloneVerifyCmd(), + untrustedSignatureDumpCmd(), } - return app + return app, &opts +} + +// before is run by the cli package for any command, before running the command-specific handler. +func (opts *globalOptions) before(_ *cli.Context) error { + if opts.debug { + logrus.SetLevel(logrus.DebugLevel) + } + if opts.tlsVerify.present { + logrus.Warn("'--tls-verify' is deprecated, please set this on the specific subcommand") + } + return nil } func main() { if reexec.Init() { return } - app := createApp() + app, _ := createApp() if err := app.Run(os.Args); err != nil { logrus.Fatal(err) } } -// getPolicyContext handles the global "policy" flag. -func getPolicyContext(c *cli.Context) (*signature.PolicyContext, error) { - policyPath := c.GlobalString("policy") - var policy *signature.Policy // This could be cached across calls, if we had an application context. +// getPolicyContext returns a *signature.PolicyContext based on opts. +func (opts *globalOptions) getPolicyContext() (*signature.PolicyContext, error) { + var policy *signature.Policy // This could be cached across calls in opts. var err error - if c.GlobalBool("insecure-policy") { + if opts.insecurePolicy { policy = &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}} - } else if policyPath == "" { + } else if opts.policyPath == "" { policy, err = signature.DefaultPolicy(nil) } else { - policy, err = signature.NewPolicyFromFile(policyPath) + policy, err = signature.NewPolicyFromFile(opts.policyPath) } if err != nil { return nil, err } return signature.NewPolicyContext(policy) } + +// commandTimeoutContext returns a context.Context and a cancellation callback based on opts. +// The caller should usually "defer cancel()" immediately after calling this. +func (opts *globalOptions) commandTimeoutContext() (context.Context, context.CancelFunc) { + ctx := context.Background() + var cancel context.CancelFunc = func() {} + if opts.commandTimeout > 0 { + ctx, cancel = context.WithTimeout(ctx, opts.commandTimeout) + } + return ctx, cancel +} diff --git a/cmd/skopeo/main_test.go b/cmd/skopeo/main_test.go index 42df5069..8b1c421f 100644 --- a/cmd/skopeo/main_test.go +++ b/cmd/skopeo/main_test.go @@ -5,7 +5,7 @@ import "bytes" // runSkopeo creates an app object and runs it with args, with an implied first "skopeo". // Returns output intended for stdout and the returned error, if any. func runSkopeo(args ...string) (string, error) { - app := createApp() + app, _ := createApp() stdout := bytes.Buffer{} app.Writer = &stdout args = append([]string{"skopeo"}, args...) diff --git a/cmd/skopeo/manifest.go b/cmd/skopeo/manifest.go index a04f5e21..5ada000a 100644 --- a/cmd/skopeo/manifest.go +++ b/cmd/skopeo/manifest.go @@ -3,17 +3,31 @@ package main import ( "errors" "fmt" + "io" "io/ioutil" "github.com/containers/image/manifest" "github.com/urfave/cli" ) -func manifestDigest(context *cli.Context) error { - if len(context.Args()) != 1 { +type manifestDigestOptions struct { +} + +func manifestDigestCmd() cli.Command { + opts := manifestDigestOptions{} + return cli.Command{ + Name: "manifest-digest", + Usage: "Compute a manifest digest of a file", + ArgsUsage: "MANIFEST", + Action: commandAction(opts.run), + } +} + +func (opts *manifestDigestOptions) run(args []string, stdout io.Writer) error { + if len(args) != 1 { return errors.New("Usage: skopeo manifest-digest manifest") } - manifestPath := context.Args()[0] + manifestPath := args[0] man, err := ioutil.ReadFile(manifestPath) if err != nil { @@ -23,13 +37,6 @@ func manifestDigest(context *cli.Context) error { if err != nil { return fmt.Errorf("Error computing digest: %v", err) } - fmt.Fprintf(context.App.Writer, "%s\n", digest) + fmt.Fprintf(stdout, "%s\n", digest) return nil } - -var manifestDigestCmd = cli.Command{ - Name: "manifest-digest", - Usage: "Compute a manifest digest of a file", - ArgsUsage: "MANIFEST", - Action: manifestDigest, -} diff --git a/cmd/skopeo/signing.go b/cmd/skopeo/signing.go index 500fedd5..6affd643 100644 --- a/cmd/skopeo/signing.go +++ b/cmd/skopeo/signing.go @@ -4,20 +4,41 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "github.com/containers/image/signature" "github.com/urfave/cli" ) -func standaloneSign(c *cli.Context) error { - outputFile := c.String("output") - if len(c.Args()) != 3 || outputFile == "" { +type standaloneSignOptions struct { + output string // Output file path +} + +func standaloneSignCmd() cli.Command { + opts := standaloneSignOptions{} + return cli.Command{ + Name: "standalone-sign", + Usage: "Create a signature using local files", + ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT", + Action: commandAction(opts.run), + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "output, o", + Usage: "output the signature to `SIGNATURE`", + Destination: &opts.output, + }, + }, + } +} + +func (opts *standaloneSignOptions) run(args []string, stdout io.Writer) error { + if len(args) != 3 || opts.output == "" { return errors.New("Usage: skopeo standalone-sign manifest docker-reference key-fingerprint -o signature") } - manifestPath := c.Args()[0] - dockerReference := c.Args()[1] - fingerprint := c.Args()[2] + manifestPath := args[0] + dockerReference := args[1] + fingerprint := args[2] manifest, err := ioutil.ReadFile(manifestPath) if err != nil { @@ -34,33 +55,33 @@ func standaloneSign(c *cli.Context) error { return fmt.Errorf("Error creating signature: %v", err) } - if err := ioutil.WriteFile(outputFile, signature, 0644); err != nil { - return fmt.Errorf("Error writing signature to %s: %v", outputFile, err) + if err := ioutil.WriteFile(opts.output, signature, 0644); err != nil { + return fmt.Errorf("Error writing signature to %s: %v", opts.output, err) } return nil } -var standaloneSignCmd = cli.Command{ - Name: "standalone-sign", - Usage: "Create a signature using local files", - ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT", - Action: standaloneSign, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "output, o", - Usage: "output the signature to `SIGNATURE`", - }, - }, +type standaloneVerifyOptions struct { } -func standaloneVerify(c *cli.Context) error { - if len(c.Args()) != 4 { +func standaloneVerifyCmd() cli.Command { + opts := standaloneVerifyOptions{} + return cli.Command{ + Name: "standalone-verify", + Usage: "Verify a signature using local files", + ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT SIGNATURE", + Action: commandAction(opts.run), + } +} + +func (opts *standaloneVerifyOptions) run(args []string, stdout io.Writer) error { + if len(args) != 4 { return errors.New("Usage: skopeo standalone-verify manifest docker-reference key-fingerprint signature") } - manifestPath := c.Args()[0] - expectedDockerReference := c.Args()[1] - expectedFingerprint := c.Args()[2] - signaturePath := c.Args()[3] + manifestPath := args[0] + expectedDockerReference := args[1] + expectedFingerprint := args[2] + signaturePath := args[3] unverifiedManifest, err := ioutil.ReadFile(manifestPath) if err != nil { @@ -81,22 +102,35 @@ func standaloneVerify(c *cli.Context) error { return fmt.Errorf("Error verifying signature: %v", err) } - fmt.Fprintf(c.App.Writer, "Signature verified, digest %s\n", sig.DockerManifestDigest) + fmt.Fprintf(stdout, "Signature verified, digest %s\n", sig.DockerManifestDigest) return nil } -var standaloneVerifyCmd = cli.Command{ - Name: "standalone-verify", - Usage: "Verify a signature using local files", - ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT SIGNATURE", - Action: standaloneVerify, +// WARNING: Do not use the contents of this for ANY security decisions, +// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable. +// There is NO REASON to expect the values to be correct, or not intentionally misleading +// (including things like “✅ Verified by $authority”) +// +// The subcommand is undocumented, and it may be renamed or entirely disappear in the future. +type untrustedSignatureDumpOptions struct { } -func untrustedSignatureDump(c *cli.Context) error { - if len(c.Args()) != 1 { +func untrustedSignatureDumpCmd() cli.Command { + opts := untrustedSignatureDumpOptions{} + return cli.Command{ + Name: "untrusted-signature-dump-without-verification", + Usage: "Dump contents of a signature WITHOUT VERIFYING IT", + ArgsUsage: "SIGNATURE", + Hidden: true, + Action: commandAction(opts.run), + } +} + +func (opts *untrustedSignatureDumpOptions) run(args []string, stdout io.Writer) error { + if len(args) != 1 { return errors.New("Usage: skopeo untrusted-signature-dump-without-verification signature") } - untrustedSignaturePath := c.Args()[0] + untrustedSignaturePath := args[0] untrustedSignature, err := ioutil.ReadFile(untrustedSignaturePath) if err != nil { @@ -111,20 +145,6 @@ func untrustedSignatureDump(c *cli.Context) error { if err != nil { return err } - fmt.Fprintln(c.App.Writer, string(untrustedOut)) + fmt.Fprintln(stdout, string(untrustedOut)) return nil } - -// WARNING: Do not use the contents of this for ANY security decisions, -// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable. -// There is NO REASON to expect the values to be correct, or not intentionally misleading -// (including things like “✅ Verified by $authority”) -// -// The subcommand is undocumented, and it may be renamed or entirely disappear in the future. -var untrustedSignatureDumpCmd = cli.Command{ - Name: "untrusted-signature-dump-without-verification", - Usage: "Dump contents of a signature WITHOUT VERIFYING IT", - ArgsUsage: "SIGNATURE", - Hidden: true, - Action: untrustedSignatureDump, -} diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 1fb29eb1..d2636479 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "io" "strings" "github.com/containers/image/transports/alltransports" @@ -10,31 +11,125 @@ import ( "github.com/urfave/cli" ) -func contextFromGlobalOptions(c *cli.Context, flagPrefix string) (*types.SystemContext, error) { +// errorShouldDisplayUsage is a subtype of error used by command handlers to indicate that cli.ShowSubcommandHelp should be called. +type errorShouldDisplayUsage struct { + error +} + +// commandAction intermediates between the cli.ActionFunc interface and the real handler, +// primarily to ensure that cli.Context is not available to the handler, which in turn +// makes sure that the cli.String() etc. flag access functions are not used, +// and everything is done using the *Options structures and the Destination: members of cli.Flag. +// handler may return errorShouldDisplayUsage to cause cli.ShowSubcommandHelp to be called. +func commandAction(handler func(args []string, stdout io.Writer) error) cli.ActionFunc { + return func(c *cli.Context) error { + err := handler(([]string)(c.Args()), c.App.Writer) + if _, ok := err.(errorShouldDisplayUsage); ok { + cli.ShowSubcommandHelp(c) + } + return err + } +} + +// sharedImageOptions collects CLI flags which are image-related, but do not change across images. +// This really should be a part of globalOptions, but that would break existing users of (skopeo copy --authfile=). +type sharedImageOptions struct { + authFilePath string // Path to a */containers/auth.json +} + +// imageFlags prepares a collection of CLI flags writing into sharedImageOptions, and the managed sharedImageOptions structure. +func sharedImageFlags() ([]cli.Flag, *sharedImageOptions) { + opts := sharedImageOptions{} + return []cli.Flag{ + cli.StringFlag{ + Name: "authfile", + Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + Destination: &opts.authFilePath, + }, + }, &opts +} + +// imageOptions collects CLI flags which are the same across subcommands, but may be different for each image +// (e.g. may differ between the source and destination of a copy) +type imageOptions struct { + global *globalOptions // May be shared across several imageOptions instances. + shared *sharedImageOptions // May be shared across several imageOptions instances. + credsOption optionalString // username[:password] for accessing a registry + dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon + tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) + sharedBlobDir string // A directory to use for OCI blobs, shared across repositories + dockerDaemonHost string // docker-daemon: host to connect to +} + +// imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. +func imageFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageOptions) { + opts := imageOptions{ + global: global, + shared: shared, + } + + // This is horribly ugly, but we need to support the old option forms of (skopeo copy) for compatibility. + // Don't add any more cases like this. + credsOptionExtra := "" + if credsOptionAlias != "" { + credsOptionExtra += "," + credsOptionAlias + } + + return []cli.Flag{ + cli.GenericFlag{ + Name: flagPrefix + "creds" + credsOptionExtra, + Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", + Value: newOptionalStringValue(&opts.credsOption), + }, + cli.StringFlag{ + Name: flagPrefix + "cert-dir", + Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry or daemon", + Destination: &opts.dockerCertPath, + }, + cli.GenericFlag{ + Name: flagPrefix + "tls-verify", + Usage: "require HTTPS and verify certificates when talking to the container registry or daemon (defaults to true)", + Value: newOptionalBoolValue(&opts.tlsVerify), + }, + cli.StringFlag{ + Name: flagPrefix + "shared-blob-dir", + Usage: "`DIRECTORY` to use to share blobs across OCI repositories", + Destination: &opts.sharedBlobDir, + }, + cli.StringFlag{ + Name: flagPrefix + "daemon-host", + Usage: "use docker daemon host at `HOST` (docker-daemon: only)", + Destination: &opts.dockerDaemonHost, + }, + }, &opts +} + +// newSystemContext returns a *types.SystemContext corresponding to opts. +// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it. +func (opts *imageOptions) newSystemContext() (*types.SystemContext, error) { ctx := &types.SystemContext{ - RegistriesDirPath: c.GlobalString("registries.d"), - ArchitectureChoice: c.GlobalString("override-arch"), - OSChoice: c.GlobalString("override-os"), - DockerCertPath: c.String(flagPrefix + "cert-dir"), - OSTreeTmpDirPath: c.String(flagPrefix + "ostree-tmp-dir"), - OCISharedBlobDirPath: c.String(flagPrefix + "shared-blob-dir"), - DirForceCompress: c.Bool(flagPrefix + "compress"), - AuthFilePath: c.String("authfile"), - DockerDaemonHost: c.String(flagPrefix + "daemon-host"), - DockerDaemonCertPath: c.String(flagPrefix + "cert-dir"), - DockerDaemonInsecureSkipTLSVerify: !c.BoolT(flagPrefix + "tls-verify"), + RegistriesDirPath: opts.global.registriesDirPath, + ArchitectureChoice: opts.global.overrideArch, + OSChoice: opts.global.overrideOS, + DockerCertPath: opts.dockerCertPath, + OCISharedBlobDirPath: opts.sharedBlobDir, + AuthFilePath: opts.shared.authFilePath, + DockerDaemonHost: opts.dockerDaemonHost, + DockerDaemonCertPath: opts.dockerCertPath, } - // DEPRECATED: we support --tls-verify for backward compatibility, but override - // it if per-subcommand flags are provided (see below). - if c.GlobalIsSet("tls-verify") { - ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.GlobalBoolT("tls-verify")) + if opts.tlsVerify.present { + ctx.DockerDaemonInsecureSkipTLSVerify = !opts.tlsVerify.value } - if c.IsSet(flagPrefix + "tls-verify") { - ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.BoolT(flagPrefix + "tls-verify")) + // DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided. + if opts.global.tlsVerify.present { + ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.global.tlsVerify.value) } - if c.IsSet(flagPrefix + "creds") { + if opts.tlsVerify.present { + ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.tlsVerify.value) + } + if opts.credsOption.present { var err error - ctx.DockerAuthConfig, err = getDockerAuth(c.String(flagPrefix + "creds")) + ctx.DockerAuthConfig, err = getDockerAuth(opts.credsOption.value) if err != nil { return nil, err } @@ -42,13 +137,43 @@ func contextFromGlobalOptions(c *cli.Context, flagPrefix string) (*types.SystemC return ctx, nil } -func commandTimeoutContextFromGlobalOptions(c *cli.Context) (context.Context, context.CancelFunc) { - ctx := context.Background() - var cancel context.CancelFunc = func() {} - if c.GlobalDuration("command-timeout") > 0 { - ctx, cancel = context.WithTimeout(ctx, c.GlobalDuration("command-timeout")) +// imageDestOptions is a superset of imageOptions specialized for iamge destinations. +type imageDestOptions struct { + *imageOptions + osTreeTmpDir string // A directory to use for OSTree temporary files + dirForceCompression bool // Compress layers when saving to the dir: transport +} + +// imageDestFlags prepares a collection of CLI flags writing into imageDestOptions, and the managed imageDestOptions structure. +func imageDestFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageDestOptions) { + genericFlags, genericOptions := imageFlags(global, shared, flagPrefix, credsOptionAlias) + opts := imageDestOptions{imageOptions: genericOptions} + + return append(genericFlags, []cli.Flag{ + cli.StringFlag{ + Name: flagPrefix + "ostree-tmp-dir", + Usage: "`DIRECTORY` to use for OSTree temporary files", + Destination: &opts.osTreeTmpDir, + }, + cli.BoolFlag{ + Name: flagPrefix + "compress", + Usage: "Compress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source)", + Destination: &opts.dirForceCompression, + }, + }...), &opts +} + +// newSystemContext returns a *types.SystemContext corresponding to opts. +// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it. +func (opts *imageDestOptions) newSystemContext() (*types.SystemContext, error) { + ctx, err := opts.imageOptions.newSystemContext() + if err != nil { + return nil, err } - return ctx, cancel + + ctx.OSTreeTmpDirPath = opts.osTreeTmpDir + ctx.DirForceCompress = opts.dirForceCompression + return ctx, err } func parseCreds(creds string) (string, string, error) { @@ -78,13 +203,12 @@ func getDockerAuth(creds string) (*types.DockerAuthConfig, error) { // parseImage converts image URL-like string to an initialized handler for that image. // The caller must call .Close() on the returned ImageCloser. -func parseImage(ctx context.Context, c *cli.Context) (types.ImageCloser, error) { - imgName := c.Args().First() - ref, err := alltransports.ParseImageName(imgName) +func parseImage(ctx context.Context, opts *imageOptions, name string) (types.ImageCloser, error) { + ref, err := alltransports.ParseImageName(name) if err != nil { return nil, err } - sys, err := contextFromGlobalOptions(c, "") + sys, err := opts.newSystemContext() if err != nil { return nil, err } @@ -93,12 +217,12 @@ func parseImage(ctx context.Context, c *cli.Context) (types.ImageCloser, error) // parseImageSource converts image URL-like string to an ImageSource. // The caller must call .Close() on the returned ImageSource. -func parseImageSource(ctx context.Context, c *cli.Context, name string) (types.ImageSource, error) { +func parseImageSource(ctx context.Context, opts *imageOptions, name string) (types.ImageSource, error) { ref, err := alltransports.ParseImageName(name) if err != nil { return nil, err } - sys, err := contextFromGlobalOptions(c, "") + sys, err := opts.newSystemContext() if err != nil { return nil, err } diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index b174daf6..f88653df 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -7,67 +7,60 @@ import ( "github.com/containers/image/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/urfave/cli" ) -// fakeContext creates inputs for contextFromGlobalOptions. +// fakeGlobalOptions creates globalOptions and sets it according to flags. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. -func fakeContext(t *testing.T, cmdName string, globalFlags []string, cmdFlags []string) *cli.Context { - app := createApp() +func fakeGlobalOptions(t *testing.T, flags []string) *globalOptions { + app, opts := createApp() - globalSet := flag.NewFlagSet(app.Name, flag.ContinueOnError) + flagSet := flag.NewFlagSet(app.Name, flag.ContinueOnError) for _, f := range app.Flags { - f.Apply(globalSet) + f.Apply(flagSet) } - err := globalSet.Parse(globalFlags) + err := flagSet.Parse(flags) require.NoError(t, err) - globalCtx := cli.NewContext(app, globalSet, nil) - cmd := app.Command(cmdName) - require.NotNil(t, cmd) - cmdSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) - for _, f := range cmd.Flags { - f.Apply(cmdSet) - } - err = cmdSet.Parse(cmdFlags) - require.NoError(t, err) - return cli.NewContext(app, cmdSet, globalCtx) + return opts } -func TestContextFromGlobalOptions(t *testing.T) { - // FIXME: All of this only tests (skopeo copy --dest) - // FIXME FIXME: Apparently BoolT values are set to false if the flag is not declared for the specific subcommand!! +// fakeImageOptions creates imageOptions and sets it according to globalFlags/cmdFlags. +// NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. +func fakeImageOptions(t *testing.T, flagPrefix string, globalFlags []string, cmdFlags []string) *imageOptions { + globalOpts := fakeGlobalOptions(t, globalFlags) - // Default state - c := fakeContext(t, "copy", []string{}, []string{}) - res, err := contextFromGlobalOptions(c, "dest-") + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageFlags(globalOpts, sharedOpts, flagPrefix, "") + flagSet := flag.NewFlagSet("fakeImageOptions", flag.ContinueOnError) + for _, f := range append(sharedFlags, imageFlags...) { + f.Apply(flagSet) + } + err := flagSet.Parse(cmdFlags) require.NoError(t, err) - assert.Equal(t, &types.SystemContext{}, res) + return imageOpts +} - // Explicitly set everything to default, except for when the default is “not present” - c = fakeContext(t, "copy", []string{}, []string{ - "--dest-compress=false", - }) - res, err = contextFromGlobalOptions(c, "dest-") +func TestImageOptionsNewSystemContext(t *testing.T) { + // Default state + opts := fakeImageOptions(t, "dest-", []string{}, []string{}) + res, err := opts.newSystemContext() require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Set everything to non-default values. - c = fakeContext(t, "copy", []string{ + opts = fakeImageOptions(t, "dest-", []string{ "--registries.d", "/srv/registries.d", "--override-arch", "overridden-arch", "--override-os", "overridden-os", }, []string{ "--authfile", "/srv/authfile", "--dest-cert-dir", "/srv/cert-dir", - "--dest-ostree-tmp-dir", "/srv/ostree-tmp-dir", "--dest-shared-blob-dir", "/srv/shared-blob-dir", - "--dest-compress=true", "--dest-daemon-host", "daemon-host.example.com", "--dest-tls-verify=false", "--dest-creds", "creds-user:creds-password", }) - res, err = contextFromGlobalOptions(c, "dest-") + res, err = opts.newSystemContext() require.NoError(t, err) assert.Equal(t, &types.SystemContext{ RegistriesDirPath: "/srv/registries.d", @@ -78,11 +71,9 @@ func TestContextFromGlobalOptions(t *testing.T) { DockerCertPath: "/srv/cert-dir", DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, DockerAuthConfig: &types.DockerAuthConfig{Username: "creds-user", Password: "creds-password"}, - OSTreeTmpDirPath: "/srv/ostree-tmp-dir", DockerDaemonCertPath: "/srv/cert-dir", DockerDaemonHost: "daemon-host.example.com", DockerDaemonInsecureSkipTLSVerify: true, - DirForceCompress: true, }, res) // Global/per-command tlsVerify behavior @@ -109,15 +100,85 @@ func TestContextFromGlobalOptions(t *testing.T) { if c.cmd != "" { cmdFlags = append(cmdFlags, "--dest-tls-verify="+c.cmd) } - ctx := fakeContext(t, "copy", globalFlags, cmdFlags) - res, err = contextFromGlobalOptions(ctx, "dest-") + opts := fakeImageOptions(t, "dest-", globalFlags, cmdFlags) + res, err = opts.newSystemContext() require.NoError(t, err) assert.Equal(t, c.expectedDocker, res.DockerInsecureSkipTLSVerify, "%#v", c) assert.Equal(t, c.expectedDockerDaemon, res.DockerDaemonInsecureSkipTLSVerify, "%#v", c) } // Invalid option values - c = fakeContext(t, "copy", []string{}, []string{"--dest-creds", ""}) - _, err = contextFromGlobalOptions(c, "dest-") + opts = fakeImageOptions(t, "dest-", []string{}, []string{"--dest-creds", ""}) + _, err = opts.newSystemContext() + assert.Error(t, err) +} + +// fakeImageDestOptions creates imageDestOptions and sets it according to globalFlags/cmdFlags. +// NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. +func fakeImageDestOptions(t *testing.T, flagPrefix string, globalFlags []string, cmdFlags []string) *imageDestOptions { + globalOpts := fakeGlobalOptions(t, globalFlags) + + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageDestFlags(globalOpts, sharedOpts, flagPrefix, "") + flagSet := flag.NewFlagSet("fakeImageDestOptions", flag.ContinueOnError) + for _, f := range append(sharedFlags, imageFlags...) { + f.Apply(flagSet) + } + err := flagSet.Parse(cmdFlags) + require.NoError(t, err) + return imageOpts +} + +func TestImageDestOptionsNewSystemContext(t *testing.T) { + // Default state + opts := fakeImageDestOptions(t, "dest-", []string{}, []string{}) + res, err := opts.newSystemContext() + require.NoError(t, err) + assert.Equal(t, &types.SystemContext{}, res) + + // Explicitly set everything to default, except for when the default is “not present” + opts = fakeImageDestOptions(t, "dest-", []string{}, []string{ + "--dest-compress=false", + }) + res, err = opts.newSystemContext() + require.NoError(t, err) + assert.Equal(t, &types.SystemContext{}, res) + + // Set everything to non-default values. + opts = fakeImageDestOptions(t, "dest-", []string{ + "--registries.d", "/srv/registries.d", + "--override-arch", "overridden-arch", + "--override-os", "overridden-os", + }, []string{ + "--authfile", "/srv/authfile", + "--dest-cert-dir", "/srv/cert-dir", + "--dest-ostree-tmp-dir", "/srv/ostree-tmp-dir", + "--dest-shared-blob-dir", "/srv/shared-blob-dir", + "--dest-compress=true", + "--dest-daemon-host", "daemon-host.example.com", + "--dest-tls-verify=false", + "--dest-creds", "creds-user:creds-password", + }) + res, err = opts.newSystemContext() + require.NoError(t, err) + assert.Equal(t, &types.SystemContext{ + RegistriesDirPath: "/srv/registries.d", + AuthFilePath: "/srv/authfile", + ArchitectureChoice: "overridden-arch", + OSChoice: "overridden-os", + OCISharedBlobDirPath: "/srv/shared-blob-dir", + DockerCertPath: "/srv/cert-dir", + DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, + DockerAuthConfig: &types.DockerAuthConfig{Username: "creds-user", Password: "creds-password"}, + OSTreeTmpDirPath: "/srv/ostree-tmp-dir", + DockerDaemonCertPath: "/srv/cert-dir", + DockerDaemonHost: "daemon-host.example.com", + DockerDaemonInsecureSkipTLSVerify: true, + DirForceCompress: true, + }, res) + + // Invalid option values in imageOptions + opts = fakeImageDestOptions(t, "dest-", []string{}, []string{"--dest-creds", ""}) + _, err = opts.newSystemContext() assert.Error(t, err) } diff --git a/vendor.conf b/vendor.conf index 93052d53..e1faf689 100644 --- a/vendor.conf +++ b/vendor.conf @@ -1,4 +1,4 @@ -github.com/urfave/cli v1.17.0 +github.com/urfave/cli v1.20.0 github.com/kr/pretty v0.1.0 github.com/kr/text v0.1.0 github.com/containers/image master diff --git a/vendor/github.com/urfave/cli/LICENSE b/vendor/github.com/urfave/cli/LICENSE index 5515ccfb..42a597e2 100644 --- a/vendor/github.com/urfave/cli/LICENSE +++ b/vendor/github.com/urfave/cli/LICENSE @@ -1,21 +1,21 @@ -Copyright (C) 2013 Jeremy Saenz -All Rights Reserved. +MIT License -MIT LICENSE +Copyright (c) 2016 Jeremy Saenz & Contributors -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/urfave/cli/README.md b/vendor/github.com/urfave/cli/README.md index c1709cef..2bbbd8ea 100644 --- a/vendor/github.com/urfave/cli/README.md +++ b/vendor/github.com/urfave/cli/README.md @@ -1,43 +1,146 @@ -[![Coverage](http://gocover.io/_badge/github.com/codegangsta/cli?0)](http://gocover.io/github.com/codegangsta/cli) -[![Build Status](https://travis-ci.org/codegangsta/cli.svg?branch=master)](https://travis-ci.org/codegangsta/cli) -[![GoDoc](https://godoc.org/github.com/codegangsta/cli?status.svg)](https://godoc.org/github.com/codegangsta/cli) -[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-codegangsta-cli) -[![Go Report Card](https://goreportcard.com/badge/codegangsta/cli)](https://goreportcard.com/report/codegangsta/cli) +cli +=== -# cli +[![Build Status](https://travis-ci.org/urfave/cli.svg?branch=master)](https://travis-ci.org/urfave/cli) +[![Windows Build Status](https://ci.appveyor.com/api/projects/status/rtgk5xufi932pb2v?svg=true)](https://ci.appveyor.com/project/urfave/cli) +[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli) +[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) +[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) +[![top level coverage](https://gocover.io/_badge/github.com/urfave/cli?0 "top level coverage")](http://gocover.io/github.com/urfave/cli) / +[![altsrc coverage](https://gocover.io/_badge/github.com/urfave/cli/altsrc?0 "altsrc coverage")](http://gocover.io/github.com/urfave/cli/altsrc) -cli is a simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. +**Notice:** This is the library formerly known as +`github.com/codegangsta/cli` -- Github will automatically redirect requests +to this repository, but we recommend updating your references for clarity. + +cli is a simple, fast, and fun package for building command line apps in Go. The +goal is to enable developers to write fast and distributable command line +applications in an expressive way. + + + +- [Overview](#overview) +- [Installation](#installation) + * [Supported platforms](#supported-platforms) + * [Using the `v2` branch](#using-the-v2-branch) + * [Pinning to the `v1` releases](#pinning-to-the-v1-releases) +- [Getting Started](#getting-started) +- [Examples](#examples) + * [Arguments](#arguments) + * [Flags](#flags) + + [Placeholder Values](#placeholder-values) + + [Alternate Names](#alternate-names) + + [Ordering](#ordering) + + [Values from the Environment](#values-from-the-environment) + + [Values from alternate input sources (YAML, TOML, and others)](#values-from-alternate-input-sources-yaml-toml-and-others) + * [Subcommands](#subcommands) + * [Subcommands categories](#subcommands-categories) + * [Exit code](#exit-code) + * [Bash Completion](#bash-completion) + + [Enabling](#enabling) + + [Distribution](#distribution) + + [Customization](#customization) + * [Generated Help Text](#generated-help-text) + + [Customization](#customization-1) + * [Version Flag](#version-flag) + + [Customization](#customization-2) + + [Full API Example](#full-api-example) +- [Contribution Guidelines](#contribution-guidelines) + + ## Overview -Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app. +Command line apps are usually so tiny that there is absolutely no reason why +your code should *not* be self-documenting. Things like generating help text and +parsing command flags/options should not hinder productivity when writing a +command line app. -**This is where cli comes into play.** cli makes command line programming fun, organized, and expressive! +**This is where cli comes into play.** cli makes command line programming fun, +organized, and expressive! ## Installation -Make sure you have a working Go environment (go 1.1+ is *required*). [See the install instructions](http://golang.org/doc/install.html). +Make sure you have a working Go environment. Go version 1.2+ is supported. [See +the install instructions for Go](http://golang.org/doc/install.html). To install cli, simply run: ``` -$ go get github.com/codegangsta/cli +$ go get github.com/urfave/cli ``` -Make sure your `PATH` includes to the `$GOPATH/bin` directory so your commands can be easily used: +Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can +be easily used: ``` export PATH=$PATH:$GOPATH/bin ``` +### Supported platforms + +cli is tested against multiple versions of Go on Linux, and against the latest +released version of Go on OS X and Windows. For full details, see +[`./.travis.yml`](./.travis.yml) and [`./appveyor.yml`](./appveyor.yml). + +### Using the `v2` branch + +**Warning**: The `v2` branch is currently unreleased and considered unstable. + +There is currently a long-lived branch named `v2` that is intended to land as +the new `master` branch once development there has settled down. The current +`master` branch (mirrored as `v1`) is being manually merged into `v2` on +an irregular human-based schedule, but generally if one wants to "upgrade" to +`v2` *now* and accept the volatility (read: "awesomeness") that comes along with +that, please use whatever version pinning of your preference, such as via +`gopkg.in`: + +``` +$ go get gopkg.in/urfave/cli.v2 +``` + +``` go +... +import ( + "gopkg.in/urfave/cli.v2" // imports as package "cli" +) +... +``` + +### Pinning to the `v1` releases + +Similarly to the section above describing use of the `v2` branch, if one wants +to avoid any unexpected compatibility pains once `v2` becomes `master`, then +pinning to `v1` is an acceptable option, e.g.: + +``` +$ go get gopkg.in/urfave/cli.v1 +``` + +``` go +... +import ( + "gopkg.in/urfave/cli.v1" // imports as package "cli" +) +... +``` + +This will pull the latest tagged `v1` release (e.g. `v1.18.1` at the time of writing). + ## Getting Started -One of the philosophies behind cli is that an API should be playful and full of discovery. So a cli app can be as little as one line of code in `main()`. +One of the philosophies behind cli is that an API should be playful and full of +discovery. So a cli app can be as little as one line of code in `main()`. + ``` go package main import ( "os" - "github.com/codegangsta/cli" + + "github.com/urfave/cli" ) func main() { @@ -45,7 +148,8 @@ func main() { } ``` -This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation: +This app will run and show help text, but is not very useful. Let's give an +action to execute and some help documentation: ``` go -... -app.Action = func(c *cli.Context) error { - fmt.Println("Hello", c.Args()[0]) - return nil +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Action = func(c *cli.Context) error { + fmt.Printf("Hello %q", c.Args().Get(0)) + return nil + } + + app.Run(os.Args) } -... ``` ### Flags Setting and querying flags is simple. + ``` go -... -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - }, -} -app.Action = func(c *cli.Context) error { - name := "someone" - if c.NArg() > 0 { - name = c.Args()[0] +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + }, } - if c.String("lang") == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) + + app.Action = func(c *cli.Context) error { + name := "Nefertiti" + if c.NArg() > 0 { + name = c.Args().Get(0) + } + if c.String("lang") == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil } - return nil + + app.Run(os.Args) } -... ``` -You can also set a destination variable for a flag, to which the content will be scanned. +You can also set a destination variable for a flag, to which the content will be +scanned. + ``` go -... -var language string -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - Destination: &language, - }, -} -app.Action = func(c *cli.Context) error { - name := "someone" - if c.NArg() > 0 { - name = c.Args()[0] +package main + +import ( + "os" + "fmt" + + "github.com/urfave/cli" +) + +func main() { + var language string + + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + Destination: &language, + }, } - if language == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) + + app.Action = func(c *cli.Context) error { + name := "someone" + if c.NArg() > 0 { + name = c.Args()[0] + } + if language == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil } - return nil + + app.Run(os.Args) } -... ``` -See full list of flags at http://godoc.org/github.com/codegangsta/cli +See full list of flags at http://godoc.org/github.com/urfave/cli #### Placeholder Values -Sometimes it's useful to specify a flag's value within the usage string itself. Such placeholders are -indicated with back quotes. +Sometimes it's useful to specify a flag's value within the usage string itself. +Such placeholders are indicated with back quotes. For example this: + ```go -cli.StringFlag{ - Name: "config, c", - Usage: "Load configuration from `FILE`", +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "config, c", + Usage: "Load configuration from `FILE`", + }, + } + + app.Run(os.Args) } ``` @@ -231,145 +411,318 @@ Will result in help output like: --config FILE, -c FILE Load configuration from FILE ``` -Note that only the first placeholder is used. Subsequent back-quoted words will be left as-is. +Note that only the first placeholder is used. Subsequent back-quoted words will +be left as-is. #### Alternate Names -You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g. +You can set alternate (or short) names for flags by providing a comma-delimited +list for the `Name`. e.g. + ``` go -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - }, +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + }, + } + + app.Run(os.Args) } ``` -That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. +That flag can then be set with `--lang spanish` or `-l spanish`. Note that +giving two different forms of the same flag in the same command invocation is an +error. + +#### Ordering + +Flags for the application and commands are shown in the order they are defined. +However, it's possible to sort them from outside this library by using `FlagsByName` +or `CommandsByName` with `sort`. + +For example this: + + +``` go +package main + +import ( + "os" + "sort" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "Language for the greeting", + }, + cli.StringFlag{ + Name: "config, c", + Usage: "Load configuration from `FILE`", + }, + } + + app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + app.Run(os.Args) +} +``` + +Will result in help output like: + +``` +--config FILE, -c FILE Load configuration from FILE +--lang value, -l value Language for the greeting (default: "english") +``` #### Values from the Environment You can also have the default value set from the environment via `EnvVar`. e.g. + ``` go -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "APP_LANG", - }, +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "APP_LANG", + }, + } + + app.Run(os.Args) } ``` -The `EnvVar` may also be given as a comma-delimited "cascade", where the first environment variable that resolves is used as the default. +The `EnvVar` may also be given as a comma-delimited "cascade", where the first +environment variable that resolves is used as the default. + ``` go -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", - }, +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", + }, + } + + app.Run(os.Args) } ``` -#### Values from alternate input sources (YAML and others) +#### Values from alternate input sources (YAML, TOML, and others) -There is a separate package altsrc that adds support for getting flag values from other input sources like YAML. +There is a separate package altsrc that adds support for getting flag values +from other file input sources. -In order to get values for a flag from an alternate input source the following code would be added to wrap an existing cli.Flag like below: +Currently supported input source formats: +* YAML +* TOML + +In order to get values for a flag from an alternate input source the following +code would be added to wrap an existing cli.Flag like below: ``` go altsrc.NewIntFlag(cli.IntFlag{Name: "test"}) ``` -Initialization must also occur for these flags. Below is an example initializing getting data from a yaml file below. +Initialization must also occur for these flags. Below is an example initializing +getting data from a yaml file below. ``` go command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) ``` -The code above will use the "load" string as a flag name to get the file name of a yaml file from the cli.Context. -It will then use that file name to initialize the yaml input source for any flags that are defined on that command. -As a note the "load" flag used would also have to be defined on the command flags in order for this code snipped to work. +The code above will use the "load" string as a flag name to get the file name of +a yaml file from the cli.Context. It will then use that file name to initialize +the yaml input source for any flags that are defined on that command. As a note +the "load" flag used would also have to be defined on the command flags in order +for this code snipped to work. -Currently only YAML files are supported but developers can add support for other input sources by implementing the +Currently only the aboved specified formats are supported but developers can +add support for other input sources by implementing the altsrc.InputSourceContext for their given sources. Here is a more complete sample of a command using YAML support: + ``` go - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - // Action to run - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test"}), - cli.StringFlag{Name: "load"}}, +package notmain + +import ( + "fmt" + "os" + + "github.com/urfave/cli" + "github.com/urfave/cli/altsrc" +) + +func main() { + app := cli.NewApp() + + flags := []cli.Flag{ + altsrc.NewIntFlag(cli.IntFlag{Name: "test"}), + cli.StringFlag{Name: "load"}, } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + + app.Action = func(c *cli.Context) error { + fmt.Println("yaml ist rad") + return nil + } + + app.Before = altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")) + app.Flags = flags + + app.Run(os.Args) +} ``` ### Subcommands Subcommands can be defined for a more git-like command line app. + ```go -... -app.Commands = []cli.Command{ - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(c *cli.Context) error { - fmt.Println("added task: ", c.Args().First()) - return nil - }, - }, - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - fmt.Println("completed task: ", c.Args().First()) - return nil - }, - }, - { - Name: "template", - Aliases: []string{"r"}, - Usage: "options for task templates", - Subcommands: []cli.Command{ - { - Name: "add", - Usage: "add a new template", - Action: func(c *cli.Context) error { - fmt.Println("new task template: ", c.Args().First()) - return nil - }, +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Commands = []cli.Command{ + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + fmt.Println("added task: ", c.Args().First()) + return nil }, - { - Name: "remove", - Usage: "remove an existing template", - Action: func(c *cli.Context) error { - fmt.Println("removed task template: ", c.Args().First()) - return nil + }, + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + fmt.Println("completed task: ", c.Args().First()) + return nil + }, + }, + { + Name: "template", + Aliases: []string{"t"}, + Usage: "options for task templates", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(c *cli.Context) error { + fmt.Println("new task template: ", c.Args().First()) + return nil + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(c *cli.Context) error { + fmt.Println("removed task template: ", c.Args().First()) + return nil + }, }, }, }, - }, + } + + app.Run(os.Args) } -... ``` ### Subcommands categories @@ -381,7 +734,17 @@ output. E.g. ```go -... +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Commands = []cli.Command{ { Name: "noop", @@ -395,20 +758,20 @@ E.g. Category: "template", }, } -... + + app.Run(os.Args) +} ``` Will include: ``` -... COMMANDS: noop Template actions: add remove -... ``` ### Exit code @@ -424,7 +787,7 @@ package main import ( "os" - "github.com/codegangsta/cli" + "github.com/urfave/cli" ) func main() { @@ -453,42 +816,58 @@ flag on the `App` object. By default, this setting will only auto-complete to show an app's subcommands, but you can write your own completion methods for the App or its subcommands. -```go -... -var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"} -app := cli.NewApp() -app.EnableBashCompletion = true -app.Commands = []cli.Command{ - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - fmt.Println("completed task: ", c.Args().First()) - return nil - }, - BashComplete: func(c *cli.Context) { - // This will complete if no args are passed - if c.NArg() > 0 { - return - } - for _, t := range tasks { - fmt.Println(t) - } + +``` go +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +func main() { + tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} + + app := cli.NewApp() + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + fmt.Println("completed task: ", c.Args().First()) + return nil + }, + BashComplete: func(c *cli.Context) { + // This will complete if no args are passed + if c.NArg() > 0 { + return + } + for _, t := range tasks { + fmt.Println(t) + } + }, }, } + + app.Run(os.Args) } -... ``` -#### To Enable +#### Enabling Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while setting the `PROG` variable to the name of your program: `PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` -#### To Distribute +#### Distribution Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename it to the name of the program you wish to add autocomplete support for (or @@ -504,7 +883,48 @@ Alternatively, you can just document that users should source the generic `autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set to the name of their program (as above). -### Generated Help Text Customization +#### Customization + +The default bash completion flag (`--generate-bash-completion`) is defined as +`cli.BashCompletionFlag`, and may be redefined if desired, e.g.: + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.BashCompletionFlag = cli.BoolFlag{ + Name: "compgen", + Hidden: true, + } + + app := cli.NewApp() + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "wat", + }, + } + app.Run(os.Args) +} +``` + +### Generated Help Text + +The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked +by the cli internals in order to print generated help text for the app, command, +or subcommand, and break execution. + +#### Customization All of the help text generation may be customized, and at multiple levels. The templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and @@ -523,7 +943,7 @@ import ( "io" "os" - "github.com/codegangsta/cli" + "github.com/urfave/cli" ) func main() { @@ -540,16 +960,13 @@ SUPPORT: support@awesometown.example.com cli.AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: - {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command -[command options]{{end}} {{if -.ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if len .Authors}} -AUTHOR(S): +AUTHOR: {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} COMMANDS: -{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t" -}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} +{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} @@ -570,10 +987,395 @@ VERSION: } ``` +The default flag may be customized to something other than `-h/--help` by +setting `cli.HelpFlag`, e.g.: + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.HelpFlag = cli.BoolFlag{ + Name: "halp, haaaaalp", + Usage: "HALP", + EnvVar: "SHOW_HALP,HALPPLZ", + } + + cli.NewApp().Run(os.Args) +} +``` + +### Version Flag + +The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which +is checked by the cli internals in order to print the `App.Version` via +`cli.VersionPrinter` and break execution. + +#### Customization + +The default flag may be customized to something other than `-v/--version` by +setting `cli.VersionFlag`, e.g.: + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.VersionFlag = cli.BoolFlag{ + Name: "print-version, V", + Usage: "print only the version", + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "19.99.0" + app.Run(os.Args) +} +``` + +Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: + + +``` go +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +var ( + Revision = "fafafaf" +) + +func main() { + cli.VersionPrinter = func(c *cli.Context) { + fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "19.99.0" + app.Run(os.Args) +} +``` + +#### Full API Example + +**Notice**: This is a contrived (functioning) example meant strictly for API +demonstration purposes. Use of one's imagination is encouraged. + + +``` go +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + "github.com/urfave/cli" +) + +func init() { + cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" + cli.CommandHelpTemplate += "\nYMMV\n" + cli.SubcommandHelpTemplate += "\nor something\n" + + cli.HelpFlag = cli.BoolFlag{Name: "halp"} + cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} + cli.VersionFlag = cli.BoolFlag{Name: "print-version, V"} + + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Fprintf(w, "best of luck to you\n") + } + cli.VersionPrinter = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "version=%s\n", c.App.Version) + } + cli.OsExiter = func(c int) { + fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", c) + } + cli.ErrWriter = ioutil.Discard + cli.FlagStringer = func(fl cli.Flag) string { + return fmt.Sprintf("\t\t%s", fl.GetName()) + } +} + +type hexWriter struct{} + +func (w *hexWriter) Write(p []byte) (int, error) { + for _, b := range p { + fmt.Printf("%x", b) + } + fmt.Printf("\n") + + return len(p), nil +} + +type genericType struct{ + s string +} + +func (g *genericType) Set(value string) error { + g.s = value + return nil +} + +func (g *genericType) String() string { + return g.s +} + +func main() { + app := cli.NewApp() + app.Name = "kənˈtrīv" + app.Version = "19.99.0" + app.Compiled = time.Now() + app.Authors = []cli.Author{ + cli.Author{ + Name: "Example Human", + Email: "human@example.com", + }, + } + app.Copyright = "(c) 1999 Serious Enterprise" + app.HelpName = "contrive" + app.Usage = "demonstrate available API" + app.UsageText = "contrive - demonstrating the available API" + app.ArgsUsage = "[args and such]" + app.Commands = []cli.Command{ + cli.Command{ + Name: "doo", + Aliases: []string{"do"}, + Category: "motion", + Usage: "do the doo", + UsageText: "doo - does the dooing", + Description: "no really, there is a lot of dooing to be done", + ArgsUsage: "[arrgh]", + Flags: []cli.Flag{ + cli.BoolFlag{Name: "forever, forevvarr"}, + }, + Subcommands: cli.Commands{ + cli.Command{ + Name: "wop", + Action: wopAction, + }, + }, + SkipFlagParsing: false, + HideHelp: false, + Hidden: false, + HelpName: "doo!", + BashComplete: func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "--better\n") + }, + Before: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "brace for impact\n") + return nil + }, + After: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "did we lose anyone?\n") + return nil + }, + Action: func(c *cli.Context) error { + c.Command.FullName() + c.Command.HasName("wop") + c.Command.Names() + c.Command.VisibleFlags() + fmt.Fprintf(c.App.Writer, "dodododododoodododddooooododododooo\n") + if c.Bool("forever") { + c.Command.Run(c) + } + return nil + }, + OnUsageError: func(c *cli.Context, err error, isSubcommand bool) error { + fmt.Fprintf(c.App.Writer, "for shame\n") + return err + }, + }, + } + app.Flags = []cli.Flag{ + cli.BoolFlag{Name: "fancy"}, + cli.BoolTFlag{Name: "fancier"}, + cli.DurationFlag{Name: "howlong, H", Value: time.Second * 3}, + cli.Float64Flag{Name: "howmuch"}, + cli.GenericFlag{Name: "wat", Value: &genericType{}}, + cli.Int64Flag{Name: "longdistance"}, + cli.Int64SliceFlag{Name: "intervals"}, + cli.IntFlag{Name: "distance"}, + cli.IntSliceFlag{Name: "times"}, + cli.StringFlag{Name: "dance-move, d"}, + cli.StringSliceFlag{Name: "names, N"}, + cli.UintFlag{Name: "age"}, + cli.Uint64Flag{Name: "bigage"}, + } + app.EnableBashCompletion = true + app.HideHelp = false + app.HideVersion = false + app.BashComplete = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") + } + app.Before = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "HEEEERE GOES\n") + return nil + } + app.After = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "Phew!\n") + return nil + } + app.CommandNotFound = func(c *cli.Context, command string) { + fmt.Fprintf(c.App.Writer, "Thar be no %q here.\n", command) + } + app.OnUsageError = func(c *cli.Context, err error, isSubcommand bool) error { + if isSubcommand { + return err + } + + fmt.Fprintf(c.App.Writer, "WRONG: %#v\n", err) + return nil + } + app.Action = func(c *cli.Context) error { + cli.DefaultAppComplete(c) + cli.HandleExitCoder(errors.New("not an exit coder, though")) + cli.ShowAppHelp(c) + cli.ShowCommandCompletions(c, "nope") + cli.ShowCommandHelp(c, "also-nope") + cli.ShowCompletions(c) + cli.ShowSubcommandHelp(c) + cli.ShowVersion(c) + + categories := c.App.Categories() + categories.AddCommand("sounds", cli.Command{ + Name: "bloop", + }) + + for _, category := range c.App.Categories() { + fmt.Fprintf(c.App.Writer, "%s\n", category.Name) + fmt.Fprintf(c.App.Writer, "%#v\n", category.Commands) + fmt.Fprintf(c.App.Writer, "%#v\n", category.VisibleCommands()) + } + + fmt.Printf("%#v\n", c.App.Command("doo")) + if c.Bool("infinite") { + c.App.Run([]string{"app", "doo", "wop"}) + } + + if c.Bool("forevar") { + c.App.RunAsSubcommand(c) + } + c.App.Setup() + fmt.Printf("%#v\n", c.App.VisibleCategories()) + fmt.Printf("%#v\n", c.App.VisibleCommands()) + fmt.Printf("%#v\n", c.App.VisibleFlags()) + + fmt.Printf("%#v\n", c.Args().First()) + if len(c.Args()) > 0 { + fmt.Printf("%#v\n", c.Args()[1]) + } + fmt.Printf("%#v\n", c.Args().Present()) + fmt.Printf("%#v\n", c.Args().Tail()) + + set := flag.NewFlagSet("contrive", 0) + nc := cli.NewContext(c.App, set, c) + + fmt.Printf("%#v\n", nc.Args()) + fmt.Printf("%#v\n", nc.Bool("nope")) + fmt.Printf("%#v\n", nc.BoolT("nerp")) + fmt.Printf("%#v\n", nc.Duration("howlong")) + fmt.Printf("%#v\n", nc.Float64("hay")) + fmt.Printf("%#v\n", nc.Generic("bloop")) + fmt.Printf("%#v\n", nc.Int64("bonk")) + fmt.Printf("%#v\n", nc.Int64Slice("burnks")) + fmt.Printf("%#v\n", nc.Int("bips")) + fmt.Printf("%#v\n", nc.IntSlice("blups")) + fmt.Printf("%#v\n", nc.String("snurt")) + fmt.Printf("%#v\n", nc.StringSlice("snurkles")) + fmt.Printf("%#v\n", nc.Uint("flub")) + fmt.Printf("%#v\n", nc.Uint64("florb")) + fmt.Printf("%#v\n", nc.GlobalBool("global-nope")) + fmt.Printf("%#v\n", nc.GlobalBoolT("global-nerp")) + fmt.Printf("%#v\n", nc.GlobalDuration("global-howlong")) + fmt.Printf("%#v\n", nc.GlobalFloat64("global-hay")) + fmt.Printf("%#v\n", nc.GlobalGeneric("global-bloop")) + fmt.Printf("%#v\n", nc.GlobalInt("global-bips")) + fmt.Printf("%#v\n", nc.GlobalIntSlice("global-blups")) + fmt.Printf("%#v\n", nc.GlobalString("global-snurt")) + fmt.Printf("%#v\n", nc.GlobalStringSlice("global-snurkles")) + + fmt.Printf("%#v\n", nc.FlagNames()) + fmt.Printf("%#v\n", nc.GlobalFlagNames()) + fmt.Printf("%#v\n", nc.GlobalIsSet("wat")) + fmt.Printf("%#v\n", nc.GlobalSet("wat", "nope")) + fmt.Printf("%#v\n", nc.NArg()) + fmt.Printf("%#v\n", nc.NumFlags()) + fmt.Printf("%#v\n", nc.Parent()) + + nc.Set("wat", "also-nope") + + ec := cli.NewExitError("ohwell", 86) + fmt.Fprintf(c.App.Writer, "%d", ec.ExitCode()) + fmt.Printf("made it!\n") + return ec + } + + if os.Getenv("HEXY") != "" { + app.Writer = &hexWriter{} + app.ErrWriter = &hexWriter{} + } + + app.Metadata = map[string]interface{}{ + "layers": "many", + "explicable": false, + "whatever-values": 19.99, + } + + app.Run(os.Args) +} + +func wopAction(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, ":wave: over here, eh\n") + return nil +} +``` + ## Contribution Guidelines -Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. +Feel free to put up a pull request to fix a bug or maybe add a feature. I will +give it a code review and make sure that it does not break backwards +compatibility. If I or any other collaborators agree that it is in line with +the vision of the project, we will work with you to get the code into +a mergeable state and merge it into the master branch. -If you have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together. +If you have contributed something significant to the project, we will most +likely add you as a collaborator. As a collaborator you are given the ability +to merge others pull requests. It is very important that new code does not +break existing code, so be careful about what code you do choose to merge. -If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out. +If you feel like you have contributed to the project but have not yet been +added as a collaborator, we probably forgot to add you, please open an issue. diff --git a/vendor/github.com/urfave/cli/app.go b/vendor/github.com/urfave/cli/app.go index 7c9b9580..51fc45d8 100644 --- a/vendor/github.com/urfave/cli/app.go +++ b/vendor/github.com/urfave/cli/app.go @@ -6,23 +6,19 @@ import ( "io/ioutil" "os" "path/filepath" - "reflect" "sort" "time" ) var ( - changeLogURL = "https://github.com/codegangsta/cli/blob/master/CHANGELOG.md" + changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md" appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL) contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." - errNonFuncAction = NewExitError("ERROR invalid Action type. "+ - fmt.Sprintf("Must be a func of type `cli.ActionFunc`. %s", contactSysadmin)+ - fmt.Sprintf("See %s", appActionDeprecationURL), 2) - errInvalidActionSignature = NewExitError("ERROR invalid Action signature. "+ - fmt.Sprintf("Must be `cli.ActionFunc`. %s", contactSysadmin)+ + errInvalidActionType = NewExitError("ERROR invalid Action type. "+ + fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ fmt.Sprintf("See %s", appActionDeprecationURL), 2) ) @@ -41,6 +37,8 @@ type App struct { ArgsUsage string // Version of the program Version string + // Description of the program + Description string // List of commands to execute Commands []Command // List of flags to parse @@ -61,10 +59,11 @@ type App struct { // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics After AfterFunc + // The action to execute when no subcommands are specified + // Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}` + // *Note*: support for the deprecated `Action` signature will be removed in a future version Action interface{} - // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind - // of deprecation period has passed, maybe? // Execute this function if the proper command cannot be found CommandNotFound CommandNotFoundFunc @@ -86,6 +85,12 @@ type App struct { ErrWriter io.Writer // Other custom info Metadata map[string]interface{} + // Carries a function which returns app specific info. + ExtraInfo func() map[string]string + // CustomAppHelpTemplate the text template for app help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomAppHelpTemplate string didSetup bool } @@ -139,13 +144,6 @@ func (a *App) Setup() { } a.Commands = newCmds - a.categories = CommandCategories{} - for _, command := range a.Commands { - a.categories = a.categories.AddCommand(command.Category, command) - } - sort.Sort(a.categories) - - // append help to commands if a.Command(helpCommand.Name) == nil && !a.HideHelp { a.Commands = append(a.Commands, helpCommand) if (HelpFlag != BoolFlag{}) { @@ -153,14 +151,23 @@ func (a *App) Setup() { } } - //append version/help flags - if a.EnableBashCompletion { - a.appendFlag(BashCompletionFlag) - } - if !a.HideVersion { a.appendFlag(VersionFlag) } + + a.categories = CommandCategories{} + for _, command := range a.Commands { + a.categories = a.categories.AddCommand(command.Category, command) + } + sort.Sort(a.categories) + + if a.Metadata == nil { + a.Metadata = make(map[string]interface{}) + } + + if a.Writer == nil { + a.Writer = os.Stdout + } } // Run is the entry point to the cli app. Parses the arguments slice and routes @@ -168,8 +175,20 @@ func (a *App) Setup() { func (a *App) Run(arguments []string) (err error) { a.Setup() + // handle the completion flag separately from the flagset since + // completion could be attempted after a flag, but before its value was put + // on the command line. this causes the flagset to interpret the completion + // flag name as the value of the flag before it which is undesirable + // note that we can only do this because the shell autocomplete function + // always appends the completion flag at the end of the command + shellComplete, arguments := checkShellCompleteFlag(a, arguments) + // parse flags - set := flagSet(a.Name, a.Flags) + set, err := flagSet(a.Name, a.Flags) + if err != nil { + return err + } + set.SetOutput(ioutil.Discard) err = set.Parse(arguments[1:]) nerr := normalizeFlags(a.Flags, set) @@ -179,6 +198,7 @@ func (a *App) Run(arguments []string) (err error) { ShowAppHelp(context) return nerr } + context.shellComplete = shellComplete if checkCompletions(context) { return nil @@ -190,7 +210,7 @@ func (a *App) Run(arguments []string) (err error) { HandleExitCoder(err) return err } - fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") + fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) ShowAppHelp(context) return err } @@ -220,7 +240,6 @@ func (a *App) Run(arguments []string) (err error) { if a.Before != nil { beforeErr := a.Before(context) if beforeErr != nil { - fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) ShowAppHelp(context) HandleExitCoder(beforeErr) err = beforeErr @@ -237,6 +256,10 @@ func (a *App) Run(arguments []string) (err error) { } } + if a.Action == nil { + a.Action = helpCommand.Action + } + // Run default Action err = HandleAction(a.Action, context) @@ -244,11 +267,12 @@ func (a *App) Run(arguments []string) (err error) { return err } -// DEPRECATED: Another entry point to the cli app, takes care of passing arguments and error handling +// RunAndExitOnError calls .Run() and exits non-zero if an error was returned +// +// Deprecated: instead you should return an error that fulfills cli.ExitCoder +// to cli.App.Run. This will cause the application to exit with the given eror +// code in the cli.ExitCoder func (a *App) RunAndExitOnError() { - fmt.Fprintf(a.errWriter(), - "DEPRECATED cli.App.RunAndExitOnError. %s See %s\n", - contactSysadmin, runAndExitOnErrorDeprecationURL) if err := a.Run(os.Args); err != nil { fmt.Fprintln(a.errWriter(), err) OsExiter(1) @@ -277,13 +301,12 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } a.Commands = newCmds - // append flags - if a.EnableBashCompletion { - a.appendFlag(BashCompletionFlag) + // parse flags + set, err := flagSet(a.Name, a.Flags) + if err != nil { + return err } - // parse flags - set := flagSet(a.Name, a.Flags) set.SetOutput(ioutil.Discard) err = set.Parse(ctx.Args().Tail()) nerr := normalizeFlags(a.Flags, set) @@ -310,7 +333,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { HandleExitCoder(err) return err } - fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") + fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) ShowSubcommandHelp(context) return err } @@ -451,48 +474,24 @@ type Author struct { func (a Author) String() string { e := "" if a.Email != "" { - e = "<" + a.Email + "> " + e = " <" + a.Email + ">" } - return fmt.Sprintf("%v %v", a.Name, e) + return fmt.Sprintf("%v%v", a.Name, e) } -// HandleAction uses ✧✧✧reflection✧✧✧ to figure out if the given Action is an -// ActionFunc, a func with the legacy signature for Action, or some other -// invalid thing. If it's an ActionFunc or a func with the legacy signature for -// Action, the func is run! +// HandleAction attempts to figure out which Action signature was used. If +// it's an ActionFunc or a func with the legacy signature for Action, the func +// is run! func HandleAction(action interface{}, context *Context) (err error) { - defer func() { - if r := recover(); r != nil { - switch r.(type) { - case error: - err = r.(error) - default: - err = NewExitError(fmt.Sprintf("ERROR unknown Action error: %v. See %s", r, appActionDeprecationURL), 2) - } - } - }() - - if reflect.TypeOf(action).Kind() != reflect.Func { - return errNonFuncAction - } - - vals := reflect.ValueOf(action).Call([]reflect.Value{reflect.ValueOf(context)}) - - if len(vals) == 0 { - fmt.Fprintf(ErrWriter, - "DEPRECATED Action signature. Must be `cli.ActionFunc`. %s See %s\n", - contactSysadmin, appActionDeprecationURL) + if a, ok := action.(ActionFunc); ok { + return a(context) + } else if a, ok := action.(func(*Context) error); ok { + return a(context) + } else if a, ok := action.(func(*Context)); ok { // deprecated function signature + a(context) return nil + } else { + return errInvalidActionType } - - if len(vals) > 1 { - return errInvalidActionSignature - } - - if retErr, ok := vals[0].Interface().(error); vals[0].IsValid() && ok { - return retErr - } - - return err } diff --git a/vendor/github.com/urfave/cli/cli.go b/vendor/github.com/urfave/cli/cli.go index f0440c56..90c07eb8 100644 --- a/vendor/github.com/urfave/cli/cli.go +++ b/vendor/github.com/urfave/cli/cli.go @@ -12,8 +12,11 @@ // app.Usage = "say a greeting" // app.Action = func(c *cli.Context) error { // println("Greetings") +// return nil // } // // app.Run(os.Args) // } package cli + +//go:generate python ./generate-flag-types cli -i flag-types.json -o flag_generated.go diff --git a/vendor/github.com/urfave/cli/command.go b/vendor/github.com/urfave/cli/command.go index 8950ccae..23de2944 100644 --- a/vendor/github.com/urfave/cli/command.go +++ b/vendor/github.com/urfave/cli/command.go @@ -46,6 +46,11 @@ type Command struct { Flags []Flag // Treat all flags as normal arguments if true SkipFlagParsing bool + // Skip argument reordering which attempts to move flags before arguments, + // but only works if all flags appear after all arguments. This behavior was + // removed n version 2 since it only works under specific conditions so we + // backport here by exposing it as an option for compatibility. + SkipArgReorder bool // Boolean to hide built-in help command HideHelp bool // Boolean to hide this command from help or completion @@ -54,6 +59,25 @@ type Command struct { // Full name of command for help, defaults to full command name, including parent commands. HelpName string commandNamePath []string + + // CustomHelpTemplate the text template for the command help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomHelpTemplate string +} + +type CommandsByName []Command + +func (c CommandsByName) Len() int { + return len(c) +} + +func (c CommandsByName) Less(i, j int) bool { + return c[i].Name < c[j].Name +} + +func (c CommandsByName) Swap(i, j int) { + c[i], c[j] = c[j], c[i] } // FullName returns the full name of the command. @@ -82,14 +106,15 @@ func (c Command) Run(ctx *Context) (err error) { ) } - if ctx.App.EnableBashCompletion { - c.Flags = append(c.Flags, BashCompletionFlag) + set, err := flagSet(c.Name, c.Flags) + if err != nil { + return err } - - set := flagSet(c.Name, c.Flags) set.SetOutput(ioutil.Discard) - if !c.SkipFlagParsing { + if c.SkipFlagParsing { + err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...)) + } else if !c.SkipArgReorder { firstFlagIndex := -1 terminatorIndex := -1 for index, arg := range ctx.Args() { @@ -122,21 +147,7 @@ func (c Command) Run(ctx *Context) (err error) { err = set.Parse(ctx.Args().Tail()) } } else { - if c.SkipFlagParsing { - err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...)) - } - } - - if err != nil { - if c.OnUsageError != nil { - err := c.OnUsageError(ctx, err, false) - HandleExitCoder(err) - return err - } - fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) - return err + err = set.Parse(ctx.Args().Tail()) } nerr := normalizeFlags(c.Flags, set) @@ -148,11 +159,23 @@ func (c Command) Run(ctx *Context) (err error) { } context := NewContext(ctx.App, set, ctx) - + context.Command = c if checkCommandCompletions(context, c.Name) { return nil } + if err != nil { + if c.OnUsageError != nil { + err := c.OnUsageError(context, err, false) + HandleExitCoder(err) + return err + } + fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) + fmt.Fprintln(context.App.Writer) + ShowCommandHelp(context, c.Name) + return err + } + if checkCommandHelp(context, c.Name) { return nil } @@ -174,15 +197,16 @@ func (c Command) Run(ctx *Context) (err error) { if c.Before != nil { err = c.Before(context) if err != nil { - fmt.Fprintln(ctx.App.Writer, err) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) + ShowCommandHelp(context, c.Name) HandleExitCoder(err) return err } } - context.Command = c + if c.Action == nil { + c.Action = helpSubcommand.Action + } + err = HandleAction(c.Action, context) if err != nil { @@ -223,14 +247,13 @@ func (c Command) startApp(ctx *Context) error { app.HelpName = app.Name } - if c.Description != "" { - app.Usage = c.Description - } else { - app.Usage = c.Usage - } + app.Usage = c.Usage + app.Description = c.Description + app.ArgsUsage = c.ArgsUsage // set CommandNotFound app.CommandNotFound = ctx.App.CommandNotFound + app.CustomAppHelpTemplate = c.CustomHelpTemplate // set the flags and commands app.Commands = c.Subcommands @@ -243,6 +266,7 @@ func (c Command) startApp(ctx *Context) error { app.Author = ctx.App.Author app.Email = ctx.App.Email app.Writer = ctx.App.Writer + app.ErrWriter = ctx.App.ErrWriter app.categories = CommandCategories{} for _, command := range c.Subcommands { @@ -265,6 +289,7 @@ func (c Command) startApp(ctx *Context) error { } else { app.Action = helpSubcommand.Action } + app.OnUsageError = c.OnUsageError for index, cc := range app.Commands { app.Commands[index].commandNamePath = []string{c.Name, cc.Name} diff --git a/vendor/github.com/urfave/cli/context.go b/vendor/github.com/urfave/cli/context.go index c3424636..db94191e 100644 --- a/vendor/github.com/urfave/cli/context.go +++ b/vendor/github.com/urfave/cli/context.go @@ -3,9 +3,9 @@ package cli import ( "errors" "flag" - "strconv" + "reflect" "strings" - "time" + "syscall" ) // Context is a type that is passed through to @@ -13,147 +13,23 @@ import ( // can be used to retrieve context-specific Args and // parsed command-line options. type Context struct { - App *App - Command Command - flagSet *flag.FlagSet - setFlags map[string]bool - globalSetFlags map[string]bool - parentContext *Context + App *App + Command Command + shellComplete bool + flagSet *flag.FlagSet + setFlags map[string]bool + parentContext *Context } // NewContext creates a new context. For use in when invoking an App or Command action. func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { - return &Context{App: app, flagSet: set, parentContext: parentCtx} -} + c := &Context{App: app, flagSet: set, parentContext: parentCtx} -// Int looks up the value of a local int flag, returns 0 if no int flag exists -func (c *Context) Int(name string) int { - return lookupInt(name, c.flagSet) -} - -// Duration looks up the value of a local time.Duration flag, returns 0 if no -// time.Duration flag exists -func (c *Context) Duration(name string) time.Duration { - return lookupDuration(name, c.flagSet) -} - -// Float64 looks up the value of a local float64 flag, returns 0 if no float64 -// flag exists -func (c *Context) Float64(name string) float64 { - return lookupFloat64(name, c.flagSet) -} - -// Bool looks up the value of a local bool flag, returns false if no bool flag exists -func (c *Context) Bool(name string) bool { - return lookupBool(name, c.flagSet) -} - -// BoolT looks up the value of a local boolT flag, returns false if no bool flag exists -func (c *Context) BoolT(name string) bool { - return lookupBoolT(name, c.flagSet) -} - -// String looks up the value of a local string flag, returns "" if no string flag exists -func (c *Context) String(name string) string { - return lookupString(name, c.flagSet) -} - -// StringSlice looks up the value of a local string slice flag, returns nil if no -// string slice flag exists -func (c *Context) StringSlice(name string) []string { - return lookupStringSlice(name, c.flagSet) -} - -// IntSlice looks up the value of a local int slice flag, returns nil if no int -// slice flag exists -func (c *Context) IntSlice(name string) []int { - return lookupIntSlice(name, c.flagSet) -} - -// Generic looks up the value of a local generic flag, returns nil if no generic -// flag exists -func (c *Context) Generic(name string) interface{} { - return lookupGeneric(name, c.flagSet) -} - -// GlobalInt looks up the value of a global int flag, returns 0 if no int flag exists -func (c *Context) GlobalInt(name string) int { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupInt(name, fs) + if parentCtx != nil { + c.shellComplete = parentCtx.shellComplete } - return 0 -} -// GlobalFloat64 looks up the value of a global float64 flag, returns float64(0) -// if no float64 flag exists -func (c *Context) GlobalFloat64(name string) float64 { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupFloat64(name, fs) - } - return float64(0) -} - -// GlobalDuration looks up the value of a global time.Duration flag, returns 0 -// if no time.Duration flag exists -func (c *Context) GlobalDuration(name string) time.Duration { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupDuration(name, fs) - } - return 0 -} - -// GlobalBool looks up the value of a global bool flag, returns false if no bool -// flag exists -func (c *Context) GlobalBool(name string) bool { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupBool(name, fs) - } - return false -} - -// GlobalBoolT looks up the value of a global bool flag, returns true if no bool -// flag exists -func (c *Context) GlobalBoolT(name string) bool { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupBoolT(name, fs) - } - return false -} - -// GlobalString looks up the value of a global string flag, returns "" if no -// string flag exists -func (c *Context) GlobalString(name string) string { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupString(name, fs) - } - return "" -} - -// GlobalStringSlice looks up the value of a global string slice flag, returns -// nil if no string slice flag exists -func (c *Context) GlobalStringSlice(name string) []string { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupStringSlice(name, fs) - } - return nil -} - -// GlobalIntSlice looks up the value of a global int slice flag, returns nil if -// no int slice flag exists -func (c *Context) GlobalIntSlice(name string) []int { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupIntSlice(name, fs) - } - return nil -} - -// GlobalGeneric looks up the value of a global generic flag, returns nil if no -// generic flag exists -func (c *Context) GlobalGeneric(name string) interface{} { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupGeneric(name, fs) - } - return nil + return c } // NumFlags returns the number of flags set @@ -163,11 +39,13 @@ func (c *Context) NumFlags() int { // Set sets a context flag to a value. func (c *Context) Set(name, value string) error { + c.setFlags = nil return c.flagSet.Set(name, value) } // GlobalSet sets a context flag to a value on the global flagset func (c *Context) GlobalSet(name, value string) error { + globalContext(c).setFlags = nil return globalContext(c).flagSet.Set(name, value) } @@ -175,28 +53,78 @@ func (c *Context) GlobalSet(name, value string) error { func (c *Context) IsSet(name string) bool { if c.setFlags == nil { c.setFlags = make(map[string]bool) + c.flagSet.Visit(func(f *flag.Flag) { c.setFlags[f.Name] = true }) + + c.flagSet.VisitAll(func(f *flag.Flag) { + if _, ok := c.setFlags[f.Name]; ok { + return + } + c.setFlags[f.Name] = false + }) + + // XXX hack to support IsSet for flags with EnvVar + // + // There isn't an easy way to do this with the current implementation since + // whether a flag was set via an environment variable is very difficult to + // determine here. Instead, we intend to introduce a backwards incompatible + // change in version 2 to add `IsSet` to the Flag interface to push the + // responsibility closer to where the information required to determine + // whether a flag is set by non-standard means such as environment + // variables is avaliable. + // + // See https://github.com/urfave/cli/issues/294 for additional discussion + flags := c.Command.Flags + if c.Command.Name == "" { // cannot == Command{} since it contains slice types + if c.App != nil { + flags = c.App.Flags + } + } + for _, f := range flags { + eachName(f.GetName(), func(name string) { + if isSet, ok := c.setFlags[name]; isSet || !ok { + return + } + + val := reflect.ValueOf(f) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + envVarValue := val.FieldByName("EnvVar") + if !envVarValue.IsValid() { + return + } + + eachName(envVarValue.String(), func(envVar string) { + envVar = strings.TrimSpace(envVar) + if _, ok := syscall.Getenv(envVar); ok { + c.setFlags[name] = true + return + } + }) + }) + } } - return c.setFlags[name] == true + + return c.setFlags[name] } // GlobalIsSet determines if the global flag was actually set func (c *Context) GlobalIsSet(name string) bool { - if c.globalSetFlags == nil { - c.globalSetFlags = make(map[string]bool) - ctx := c - if ctx.parentContext != nil { - ctx = ctx.parentContext - } - for ; ctx != nil && c.globalSetFlags[name] == false; ctx = ctx.parentContext { - ctx.flagSet.Visit(func(f *flag.Flag) { - c.globalSetFlags[f.Name] = true - }) + ctx := c + if ctx.parentContext != nil { + ctx = ctx.parentContext + } + + for ; ctx != nil; ctx = ctx.parentContext { + if ctx.IsSet(name) { + return true } } - return c.globalSetFlags[name] + return false } // FlagNames returns a slice of flag names used in this context. @@ -228,6 +156,11 @@ func (c *Context) Parent() *Context { return c.parentContext } +// value returns the value of the flag coressponding to `name` +func (c *Context) value(name string) interface{} { + return c.flagSet.Lookup(name).Value.(flag.Getter).Get() +} + // Args contains apps console arguments type Args []string @@ -303,107 +236,6 @@ func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet { return nil } -func lookupInt(name string, set *flag.FlagSet) int { - f := set.Lookup(name) - if f != nil { - val, err := strconv.Atoi(f.Value.String()) - if err != nil { - return 0 - } - return val - } - - return 0 -} - -func lookupDuration(name string, set *flag.FlagSet) time.Duration { - f := set.Lookup(name) - if f != nil { - val, err := time.ParseDuration(f.Value.String()) - if err == nil { - return val - } - } - - return 0 -} - -func lookupFloat64(name string, set *flag.FlagSet) float64 { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseFloat(f.Value.String(), 64) - if err != nil { - return 0 - } - return val - } - - return 0 -} - -func lookupString(name string, set *flag.FlagSet) string { - f := set.Lookup(name) - if f != nil { - return f.Value.String() - } - - return "" -} - -func lookupStringSlice(name string, set *flag.FlagSet) []string { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*StringSlice)).Value() - - } - - return nil -} - -func lookupIntSlice(name string, set *flag.FlagSet) []int { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*IntSlice)).Value() - - } - - return nil -} - -func lookupGeneric(name string, set *flag.FlagSet) interface{} { - f := set.Lookup(name) - if f != nil { - return f.Value - } - return nil -} - -func lookupBool(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return false - } - return val - } - - return false -} - -func lookupBoolT(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return true - } - return val - } - - return false -} - func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { switch ff.Value.(type) { case *StringSlice: diff --git a/vendor/github.com/urfave/cli/errors.go b/vendor/github.com/urfave/cli/errors.go index ea551be1..562b2953 100644 --- a/vendor/github.com/urfave/cli/errors.go +++ b/vendor/github.com/urfave/cli/errors.go @@ -24,7 +24,7 @@ func NewMultiError(err ...error) MultiError { return MultiError{Errors: err} } -// Error implents the error interface. +// Error implements the error interface. func (m MultiError) Error() string { errs := make([]string, len(m.Errors)) for i, err := range m.Errors { @@ -34,6 +34,10 @@ func (m MultiError) Error() string { return strings.Join(errs, "\n") } +type ErrorFormatter interface { + Format(s fmt.State, verb rune) +} + // ExitCoder is the interface checked by `App` and `Command` for a custom exit // code type ExitCoder interface { @@ -44,11 +48,11 @@ type ExitCoder interface { // ExitError fulfills both the builtin `error` interface and `ExitCoder` type ExitError struct { exitCode int - message string + message interface{} } // NewExitError makes a new *ExitError -func NewExitError(message string, exitCode int) *ExitError { +func NewExitError(message interface{}, exitCode int) *ExitError { return &ExitError{ exitCode: exitCode, message: message, @@ -58,7 +62,7 @@ func NewExitError(message string, exitCode int) *ExitError { // Error returns the string message, fulfilling the interface required by // `error` func (ee *ExitError) Error() string { - return ee.message + return fmt.Sprintf("%v", ee.message) } // ExitCode returns the exit code, fulfilling the interface required by @@ -70,7 +74,7 @@ func (ee *ExitError) ExitCode() int { // HandleExitCoder checks if the error fulfills the ExitCoder interface, and if // so prints the error to stderr (if it is non-empty) and calls OsExiter with the // given exit code. If the given error is a MultiError, then this func is -// called on all members of the Errors slice. +// called on all members of the Errors slice and calls OsExiter with the last exit code. func HandleExitCoder(err error) { if err == nil { return @@ -78,15 +82,34 @@ func HandleExitCoder(err error) { if exitErr, ok := err.(ExitCoder); ok { if err.Error() != "" { - fmt.Fprintln(ErrWriter, err) + if _, ok := exitErr.(ErrorFormatter); ok { + fmt.Fprintf(ErrWriter, "%+v\n", err) + } else { + fmt.Fprintln(ErrWriter, err) + } } OsExiter(exitErr.ExitCode()) return } if multiErr, ok := err.(MultiError); ok { - for _, merr := range multiErr.Errors { - HandleExitCoder(merr) - } + code := handleMultiError(multiErr) + OsExiter(code) + return } } + +func handleMultiError(multiErr MultiError) int { + code := 1 + for _, merr := range multiErr.Errors { + if multiErr2, ok := merr.(MultiError); ok { + code = handleMultiError(multiErr2) + } else { + fmt.Fprintln(ErrWriter, merr) + if exitErr, ok := merr.(ExitCoder); ok { + code = exitErr.ExitCode() + } + } + } + return code +} diff --git a/vendor/github.com/urfave/cli/flag.go b/vendor/github.com/urfave/cli/flag.go index 1e8112e7..877ff352 100644 --- a/vendor/github.com/urfave/cli/flag.go +++ b/vendor/github.com/urfave/cli/flag.go @@ -3,24 +3,24 @@ package cli import ( "flag" "fmt" - "os" "reflect" "runtime" "strconv" "strings" + "syscall" "time" ) const defaultPlaceholder = "value" // BashCompletionFlag enables bash-completion for all commands and subcommands -var BashCompletionFlag = BoolFlag{ +var BashCompletionFlag Flag = BoolFlag{ Name: "generate-bash-completion", Hidden: true, } // VersionFlag prints the version for the application -var VersionFlag = BoolFlag{ +var VersionFlag Flag = BoolFlag{ Name: "version, v", Usage: "print the version", } @@ -28,7 +28,7 @@ var VersionFlag = BoolFlag{ // HelpFlag prints the help for all commands and subcommands // Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand // unless HideHelp is set to true) -var HelpFlag = BoolFlag{ +var HelpFlag Flag = BoolFlag{ Name: "help, h", Usage: "show help", } @@ -37,6 +37,21 @@ var HelpFlag = BoolFlag{ // to display a flag. var FlagStringer FlagStringFunc = stringifyFlag +// FlagsByName is a slice of Flag. +type FlagsByName []Flag + +func (f FlagsByName) Len() int { + return len(f) +} + +func (f FlagsByName) Less(i, j int) bool { + return f[i].GetName() < f[j].GetName() +} + +func (f FlagsByName) Swap(i, j int) { + f[i], f[j] = f[j], f[i] +} + // Flag is a common interface related to parsing flags in cli. // For more advanced flag parsing techniques, it is recommended that // this interface be implemented. @@ -47,13 +62,29 @@ type Flag interface { GetName() string } -func flagSet(name string, flags []Flag) *flag.FlagSet { +// errorableFlag is an interface that allows us to return errors during apply +// it allows flags defined in this library to return errors in a fashion backwards compatible +// TODO remove in v2 and modify the existing Flag interface to return errors +type errorableFlag interface { + Flag + + ApplyWithError(*flag.FlagSet) error +} + +func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) for _, f := range flags { - f.Apply(set) + //TODO remove in v2 when errorableFlag is removed + if ef, ok := f.(errorableFlag); ok { + if err := ef.ApplyWithError(set); err != nil { + return nil, err + } + } else { + f.Apply(set) + } } - return set + return set, nil } func eachName(longName string, fn func(string)) { @@ -70,31 +101,24 @@ type Generic interface { String() string } -// GenericFlag is the flag type for types implementing Generic -type GenericFlag struct { - Name string - Value Generic - Usage string - EnvVar string - Hidden bool -} - -// String returns the string representation of the generic flag to display the -// help text to the user (uses the String() method of the generic flag to show -// the value) -func (f GenericFlag) String() string { - return FlagStringer(f) -} - // Apply takes the flagset and calls Set on the generic flag with the value // provided by the user for parsing by the flag +// Ignores parsing errors func (f GenericFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError takes the flagset and calls Set on the generic flag with the value +// provided by the user for parsing by the flag +func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error { val := f.Value if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - val.Set(envVal) + if envVal, ok := syscall.Getenv(envVar); ok { + if err := val.Set(envVal); err != nil { + return fmt.Errorf("could not parse %s as value for flag %s: %s", envVal, f.Name, err) + } break } } @@ -103,14 +127,11 @@ func (f GenericFlag) Apply(set *flag.FlagSet) { eachName(f.Name, func(name string) { set.Var(f.Value, name, f.Usage) }) + + return nil } -// GetName returns the name of a flag. -func (f GenericFlag) GetName() string { - return f.Name -} - -// StringSlice is an opaque type for []string to satisfy flag.Value +// StringSlice is an opaque type for []string to satisfy flag.Value and flag.Getter type StringSlice []string // Set appends the string value to the list of values @@ -129,31 +150,29 @@ func (f *StringSlice) Value() []string { return *f } -// StringSliceFlag is a string flag that can be specified multiple times on the -// command-line -type StringSliceFlag struct { - Name string - Value *StringSlice - Usage string - EnvVar string - Hidden bool -} - -// String returns the usage -func (f StringSliceFlag) String() string { - return FlagStringer(f) +// Get returns the slice of strings set by this flag +func (f *StringSlice) Get() interface{} { + return *f } // Apply populates the flag given the flag set and environment +// Ignores errors func (f StringSliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { newVal := &StringSlice{} for _, s := range strings.Split(envVal, ",") { s = strings.TrimSpace(s) - newVal.Set(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err) + } } f.Value = newVal break @@ -167,14 +186,11 @@ func (f StringSliceFlag) Apply(set *flag.FlagSet) { } set.Var(f.Value, name, f.Usage) }) + + return nil } -// GetName returns the name of a flag. -func (f StringSliceFlag) GetName() string { - return f.Name -} - -// IntSlice is an opaque type for []int to satisfy flag.Value +// IntSlice is an opaque type for []int to satisfy flag.Value and flag.Getter type IntSlice []int // Set parses the value into an integer and appends it to the list of values @@ -189,7 +205,7 @@ func (f *IntSlice) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (f *IntSlice) String() string { - return fmt.Sprintf("%d", *f) + return fmt.Sprintf("%#v", *f) } // Value returns the slice of ints set by this flag @@ -197,33 +213,28 @@ func (f *IntSlice) Value() []int { return *f } -// IntSliceFlag is an int flag that can be specified multiple times on the -// command-line -type IntSliceFlag struct { - Name string - Value *IntSlice - Usage string - EnvVar string - Hidden bool -} - -// String returns the usage -func (f IntSliceFlag) String() string { - return FlagStringer(f) +// Get returns the slice of ints set by this flag +func (f *IntSlice) Get() interface{} { + return *f } // Apply populates the flag given the flag set and environment +// Ignores errors func (f IntSliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { newVal := &IntSlice{} for _, s := range strings.Split(envVal, ",") { s = strings.TrimSpace(s) - err := newVal.Set(s) - if err != nil { - fmt.Fprintf(ErrWriter, err.Error()) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err) } } f.Value = newVal @@ -238,38 +249,96 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) { } set.Var(f.Value, name, f.Usage) }) + + return nil } -// GetName returns the name of the flag. -func (f IntSliceFlag) GetName() string { - return f.Name -} +// Int64Slice is an opaque type for []int to satisfy flag.Value and flag.Getter +type Int64Slice []int64 -// BoolFlag is a switch that defaults to false -type BoolFlag struct { - Name string - Usage string - EnvVar string - Destination *bool - Hidden bool +// Set parses the value into an integer and appends it to the list of values +func (f *Int64Slice) Set(value string) error { + tmp, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + *f = append(*f, tmp) + return nil } // String returns a readable representation of this value (for usage defaults) -func (f BoolFlag) String() string { - return FlagStringer(f) +func (f *Int64Slice) String() string { + return fmt.Sprintf("%#v", *f) +} + +// Value returns the slice of ints set by this flag +func (f *Int64Slice) Value() []int64 { + return *f +} + +// Get returns the slice of ints set by this flag +func (f *Int64Slice) Get() interface{} { + return *f } // Apply populates the flag given the flag set and environment +// Ignores errors +func (f Int64SliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + newVal := &Int64Slice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err) + } + } + f.Value = newVal + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Value == nil { + f.Value = &Int64Slice{} + } + set.Var(f.Value, name, f.Usage) + }) + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors func (f BoolFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error { val := false if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool + if envVal, ok := syscall.Getenv(envVar); ok { + if envVal == "" { + val = false + break } + + envValBool, err := strconv.ParseBool(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) + } + + val = envValBool break } } @@ -282,40 +351,35 @@ func (f BoolFlag) Apply(set *flag.FlagSet) { } set.Bool(name, val, f.Usage) }) -} -// GetName returns the name of the flag. -func (f BoolFlag) GetName() string { - return f.Name -} - -// BoolTFlag this represents a boolean flag that is true by default, but can -// still be set to false by --some-flag=false -type BoolTFlag struct { - Name string - Usage string - EnvVar string - Destination *bool - Hidden bool -} - -// String returns a readable representation of this value (for usage defaults) -func (f BoolTFlag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f BoolTFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error { val := true if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool + if envVal, ok := syscall.Getenv(envVar); ok { + if envVal == "" { + val = false break } + + envValBool, err := strconv.ParseBool(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) + } + + val = envValBool + break } } } @@ -327,34 +391,22 @@ func (f BoolTFlag) Apply(set *flag.FlagSet) { } set.Bool(name, val, f.Usage) }) -} -// GetName returns the name of the flag. -func (f BoolTFlag) GetName() string { - return f.Name -} - -// StringFlag represents a flag that takes as string value -type StringFlag struct { - Name string - Value string - Usage string - EnvVar string - Destination *string - Hidden bool -} - -// String returns the usage -func (f StringFlag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f StringFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f StringFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { f.Value = envVal break } @@ -368,40 +420,28 @@ func (f StringFlag) Apply(set *flag.FlagSet) { } set.String(name, f.Value, f.Usage) }) -} -// GetName returns the name of the flag. -func (f StringFlag) GetName() string { - return f.Name -} - -// IntFlag is a flag that takes an integer -// Errors if the value provided cannot be parsed -type IntFlag struct { - Name string - Value int - Usage string - EnvVar string - Destination *int - Hidden bool -} - -// String returns the usage -func (f IntFlag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f IntFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f IntFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err == nil { - f.Value = int(envValInt) - break + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) } + f.Value = int(envValInt) + break } } } @@ -413,40 +453,131 @@ func (f IntFlag) Apply(set *flag.FlagSet) { } set.Int(name, f.Value, f.Usage) }) -} -// GetName returns the name of the flag. -func (f IntFlag) GetName() string { - return f.Name -} - -// DurationFlag is a flag that takes a duration specified in Go's duration -// format: https://golang.org/pkg/time/#ParseDuration -type DurationFlag struct { - Name string - Value time.Duration - Usage string - EnvVar string - Destination *time.Duration - Hidden bool -} - -// String returns a readable representation of this value (for usage defaults) -func (f DurationFlag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment -func (f DurationFlag) Apply(set *flag.FlagSet) { +// Ignores errors +func (f Int64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValDuration, err := time.ParseDuration(envVal) - if err == nil { - f.Value = envValDuration - break + if envVal, ok := syscall.Getenv(envVar); ok { + envValInt, err := strconv.ParseInt(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) } + + f.Value = envValInt + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.Int64Var(f.Destination, name, f.Value, f.Usage) + return + } + set.Int64(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f UintFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f UintFlag) ApplyWithError(set *flag.FlagSet) error { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + envValInt, err := strconv.ParseUint(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err) + } + + f.Value = uint(envValInt) + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.UintVar(f.Destination, name, f.Value, f.Usage) + return + } + set.Uint(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f Uint64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + envValInt, err := strconv.ParseUint(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err) + } + + f.Value = uint64(envValInt) + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.Uint64Var(f.Destination, name, f.Value, f.Usage) + return + } + set.Uint64(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f DurationFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + envValDuration, err := time.ParseDuration(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err) + } + + f.Value = envValDuration + break } } } @@ -458,39 +589,29 @@ func (f DurationFlag) Apply(set *flag.FlagSet) { } set.Duration(name, f.Value, f.Usage) }) -} -// GetName returns the name of the flag. -func (f DurationFlag) GetName() string { - return f.Name -} - -// Float64Flag is a flag that takes an float value -// Errors if the value provided cannot be parsed -type Float64Flag struct { - Name string - Value float64 - Usage string - EnvVar string - Destination *float64 - Hidden bool -} - -// String returns the usage -func (f Float64Flag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f Float64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValFloat, err := strconv.ParseFloat(envVal, 10) - if err == nil { - f.Value = float64(envValFloat) + if err != nil { + return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err) } + + f.Value = float64(envValFloat) + break } } } @@ -502,17 +623,15 @@ func (f Float64Flag) Apply(set *flag.FlagSet) { } set.Float64(name, f.Value, f.Usage) }) -} -// GetName returns the name of the flag. -func (f Float64Flag) GetName() string { - return f.Name + return nil } func visibleFlags(fl []Flag) []Flag { visible := []Flag{} for _, flag := range fl { - if !reflect.ValueOf(flag).FieldByName("Hidden").Bool() { + field := flagValue(flag).FieldByName("Hidden") + if !field.IsValid() || !field.Bool() { visible = append(visible, flag) } } @@ -578,13 +697,24 @@ func withEnvHint(envVar, str string) string { return str + envText } -func stringifyFlag(f Flag) string { +func flagValue(f Flag) reflect.Value { fv := reflect.ValueOf(f) + for fv.Kind() == reflect.Ptr { + fv = reflect.Indirect(fv) + } + return fv +} + +func stringifyFlag(f Flag) string { + fv := flagValue(f) switch f.(type) { case IntSliceFlag: return withEnvHint(fv.FieldByName("EnvVar").String(), stringifyIntSliceFlag(f.(IntSliceFlag))) + case Int64SliceFlag: + return withEnvHint(fv.FieldByName("EnvVar").String(), + stringifyInt64SliceFlag(f.(Int64SliceFlag))) case StringSliceFlag: return withEnvHint(fv.FieldByName("EnvVar").String(), stringifyStringSliceFlag(f.(StringSliceFlag))) @@ -594,9 +724,8 @@ func stringifyFlag(f Flag) string { needsPlaceholder := false defaultValueString := "" - val := fv.FieldByName("Value") - if val.IsValid() { + if val := fv.FieldByName("Value"); val.IsValid() { needsPlaceholder = true defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface()) @@ -630,6 +759,17 @@ func stringifyIntSliceFlag(f IntSliceFlag) string { return stringifySliceFlag(f.Usage, f.Name, defaultVals) } +func stringifyInt64SliceFlag(f Int64SliceFlag) string { + defaultVals := []string{} + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, fmt.Sprintf("%d", i)) + } + } + + return stringifySliceFlag(f.Usage, f.Name, defaultVals) +} + func stringifyStringSliceFlag(f StringSliceFlag) string { defaultVals := []string{} if f.Value != nil && len(f.Value.Value()) > 0 { diff --git a/vendor/github.com/urfave/cli/flag_generated.go b/vendor/github.com/urfave/cli/flag_generated.go new file mode 100644 index 00000000..491b6195 --- /dev/null +++ b/vendor/github.com/urfave/cli/flag_generated.go @@ -0,0 +1,627 @@ +package cli + +import ( + "flag" + "strconv" + "time" +) + +// WARNING: This file is generated! + +// BoolFlag is a flag with type bool +type BoolFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Destination *bool +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f BoolFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f BoolFlag) GetName() string { + return f.Name +} + +// Bool looks up the value of a local BoolFlag, returns +// false if not found +func (c *Context) Bool(name string) bool { + return lookupBool(name, c.flagSet) +} + +// GlobalBool looks up the value of a global BoolFlag, returns +// false if not found +func (c *Context) GlobalBool(name string) bool { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupBool(name, fs) + } + return false +} + +func lookupBool(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return parsed + } + return false +} + +// BoolTFlag is a flag with type bool that is true by default +type BoolTFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Destination *bool +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f BoolTFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f BoolTFlag) GetName() string { + return f.Name +} + +// BoolT looks up the value of a local BoolTFlag, returns +// false if not found +func (c *Context) BoolT(name string) bool { + return lookupBoolT(name, c.flagSet) +} + +// GlobalBoolT looks up the value of a global BoolTFlag, returns +// false if not found +func (c *Context) GlobalBoolT(name string) bool { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupBoolT(name, fs) + } + return false +} + +func lookupBoolT(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return parsed + } + return false +} + +// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) +type DurationFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value time.Duration + Destination *time.Duration +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f DurationFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f DurationFlag) GetName() string { + return f.Name +} + +// Duration looks up the value of a local DurationFlag, returns +// 0 if not found +func (c *Context) Duration(name string) time.Duration { + return lookupDuration(name, c.flagSet) +} + +// GlobalDuration looks up the value of a global DurationFlag, returns +// 0 if not found +func (c *Context) GlobalDuration(name string) time.Duration { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupDuration(name, fs) + } + return 0 +} + +func lookupDuration(name string, set *flag.FlagSet) time.Duration { + f := set.Lookup(name) + if f != nil { + parsed, err := time.ParseDuration(f.Value.String()) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// Float64Flag is a flag with type float64 +type Float64Flag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value float64 + Destination *float64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Float64Flag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Float64Flag) GetName() string { + return f.Name +} + +// Float64 looks up the value of a local Float64Flag, returns +// 0 if not found +func (c *Context) Float64(name string) float64 { + return lookupFloat64(name, c.flagSet) +} + +// GlobalFloat64 looks up the value of a global Float64Flag, returns +// 0 if not found +func (c *Context) GlobalFloat64(name string) float64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupFloat64(name, fs) + } + return 0 +} + +func lookupFloat64(name string, set *flag.FlagSet) float64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseFloat(f.Value.String(), 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// GenericFlag is a flag with type Generic +type GenericFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value Generic +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f GenericFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f GenericFlag) GetName() string { + return f.Name +} + +// Generic looks up the value of a local GenericFlag, returns +// nil if not found +func (c *Context) Generic(name string) interface{} { + return lookupGeneric(name, c.flagSet) +} + +// GlobalGeneric looks up the value of a global GenericFlag, returns +// nil if not found +func (c *Context) GlobalGeneric(name string) interface{} { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupGeneric(name, fs) + } + return nil +} + +func lookupGeneric(name string, set *flag.FlagSet) interface{} { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value, error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// Int64Flag is a flag with type int64 +type Int64Flag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value int64 + Destination *int64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Int64Flag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Int64Flag) GetName() string { + return f.Name +} + +// Int64 looks up the value of a local Int64Flag, returns +// 0 if not found +func (c *Context) Int64(name string) int64 { + return lookupInt64(name, c.flagSet) +} + +// GlobalInt64 looks up the value of a global Int64Flag, returns +// 0 if not found +func (c *Context) GlobalInt64(name string) int64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt64(name, fs) + } + return 0 +} + +func lookupInt64(name string, set *flag.FlagSet) int64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// IntFlag is a flag with type int +type IntFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value int + Destination *int +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f IntFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f IntFlag) GetName() string { + return f.Name +} + +// Int looks up the value of a local IntFlag, returns +// 0 if not found +func (c *Context) Int(name string) int { + return lookupInt(name, c.flagSet) +} + +// GlobalInt looks up the value of a global IntFlag, returns +// 0 if not found +func (c *Context) GlobalInt(name string) int { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt(name, fs) + } + return 0 +} + +func lookupInt(name string, set *flag.FlagSet) int { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return int(parsed) + } + return 0 +} + +// IntSliceFlag is a flag with type *IntSlice +type IntSliceFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value *IntSlice +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f IntSliceFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f IntSliceFlag) GetName() string { + return f.Name +} + +// IntSlice looks up the value of a local IntSliceFlag, returns +// nil if not found +func (c *Context) IntSlice(name string) []int { + return lookupIntSlice(name, c.flagSet) +} + +// GlobalIntSlice looks up the value of a global IntSliceFlag, returns +// nil if not found +func (c *Context) GlobalIntSlice(name string) []int { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupIntSlice(name, fs) + } + return nil +} + +func lookupIntSlice(name string, set *flag.FlagSet) []int { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*IntSlice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// Int64SliceFlag is a flag with type *Int64Slice +type Int64SliceFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value *Int64Slice +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Int64SliceFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Int64SliceFlag) GetName() string { + return f.Name +} + +// Int64Slice looks up the value of a local Int64SliceFlag, returns +// nil if not found +func (c *Context) Int64Slice(name string) []int64 { + return lookupInt64Slice(name, c.flagSet) +} + +// GlobalInt64Slice looks up the value of a global Int64SliceFlag, returns +// nil if not found +func (c *Context) GlobalInt64Slice(name string) []int64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt64Slice(name, fs) + } + return nil +} + +func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*Int64Slice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// StringFlag is a flag with type string +type StringFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value string + Destination *string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f StringFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f StringFlag) GetName() string { + return f.Name +} + +// String looks up the value of a local StringFlag, returns +// "" if not found +func (c *Context) String(name string) string { + return lookupString(name, c.flagSet) +} + +// GlobalString looks up the value of a global StringFlag, returns +// "" if not found +func (c *Context) GlobalString(name string) string { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupString(name, fs) + } + return "" +} + +func lookupString(name string, set *flag.FlagSet) string { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value.String(), error(nil) + if err != nil { + return "" + } + return parsed + } + return "" +} + +// StringSliceFlag is a flag with type *StringSlice +type StringSliceFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value *StringSlice +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f StringSliceFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f StringSliceFlag) GetName() string { + return f.Name +} + +// StringSlice looks up the value of a local StringSliceFlag, returns +// nil if not found +func (c *Context) StringSlice(name string) []string { + return lookupStringSlice(name, c.flagSet) +} + +// GlobalStringSlice looks up the value of a global StringSliceFlag, returns +// nil if not found +func (c *Context) GlobalStringSlice(name string) []string { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupStringSlice(name, fs) + } + return nil +} + +func lookupStringSlice(name string, set *flag.FlagSet) []string { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*StringSlice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// Uint64Flag is a flag with type uint64 +type Uint64Flag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value uint64 + Destination *uint64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Uint64Flag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Uint64Flag) GetName() string { + return f.Name +} + +// Uint64 looks up the value of a local Uint64Flag, returns +// 0 if not found +func (c *Context) Uint64(name string) uint64 { + return lookupUint64(name, c.flagSet) +} + +// GlobalUint64 looks up the value of a global Uint64Flag, returns +// 0 if not found +func (c *Context) GlobalUint64(name string) uint64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupUint64(name, fs) + } + return 0 +} + +func lookupUint64(name string, set *flag.FlagSet) uint64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// UintFlag is a flag with type uint +type UintFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value uint + Destination *uint +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f UintFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f UintFlag) GetName() string { + return f.Name +} + +// Uint looks up the value of a local UintFlag, returns +// 0 if not found +func (c *Context) Uint(name string) uint { + return lookupUint(name, c.flagSet) +} + +// GlobalUint looks up the value of a global UintFlag, returns +// 0 if not found +func (c *Context) GlobalUint(name string) uint { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupUint(name, fs) + } + return 0 +} + +func lookupUint(name string, set *flag.FlagSet) uint { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return uint(parsed) + } + return 0 +} diff --git a/vendor/github.com/urfave/cli/help.go b/vendor/github.com/urfave/cli/help.go index 801d2b16..57ec98d5 100644 --- a/vendor/github.com/urfave/cli/help.go +++ b/vendor/github.com/urfave/cli/help.go @@ -13,27 +13,31 @@ import ( // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var AppHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} + {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} - {{if .Version}}{{if not .HideVersion}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + VERSION: - {{.Version}} - {{end}}{{end}}{{if len .Authors}} -AUTHOR(S): - {{range .Authors}}{{.}}{{end}} - {{end}}{{if .VisibleCommands}} + {{.Version}}{{end}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if len .Authors}} + +AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: + {{range $index, $author := .Authors}}{{if $index}} + {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} + COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{end}}{{range .VisibleCommands}} - {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{"\t"}}{{.Usage}}{{end}} -{{end}}{{end}}{{if .VisibleFlags}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + GLOBAL OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}}{{if .Copyright}} + {{range $index, $option := .VisibleFlags}}{{if $index}} + {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} + COPYRIGHT: - {{.Copyright}} - {{end}} + {{.Copyright}}{{end}} ` // CommandHelpTemplate is the text template for the command help topic. @@ -43,7 +47,7 @@ var CommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} @@ -60,14 +64,14 @@ OPTIONS: // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var SubcommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} + {{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}} USAGE: - {{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{end}}{{range .VisibleCommands}} - {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{"\t"}}{{.Usage}}{{end}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}} {{end}}{{if .VisibleFlags}} OPTIONS: {{range .VisibleFlags}}{{.}} @@ -108,17 +112,43 @@ var helpSubcommand = Command{ // Prints help for the App or Command type helpPrinter func(w io.Writer, templ string, data interface{}) +// Prints help for the App or Command with custom template function. +type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) + // HelpPrinter is a function that writes the help output. If not set a default // is used. The function signature is: // func(w io.Writer, templ string, data interface{}) var HelpPrinter helpPrinter = printHelp +// HelpPrinterCustom is same as HelpPrinter but +// takes a custom function for template function map. +var HelpPrinterCustom helpPrinterCustom = printHelpCustom + // VersionPrinter prints the version for the App var VersionPrinter = printVersion +// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. +func ShowAppHelpAndExit(c *Context, exitCode int) { + ShowAppHelp(c) + os.Exit(exitCode) +} + // ShowAppHelp is an action that displays the help. -func ShowAppHelp(c *Context) { - HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) +func ShowAppHelp(c *Context) (err error) { + if c.App.CustomAppHelpTemplate == "" { + HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) + return + } + customAppData := func() map[string]interface{} { + if c.App.ExtraInfo == nil { + return nil + } + return map[string]interface{}{ + "ExtraInfo": c.App.ExtraInfo, + } + } + HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, customAppData()) + return nil } // DefaultAppComplete prints the list of subcommands as the default app completion method @@ -133,6 +163,12 @@ func DefaultAppComplete(c *Context) { } } +// ShowCommandHelpAndExit - exits with code after showing help +func ShowCommandHelpAndExit(c *Context, command string, code int) { + ShowCommandHelp(c, command) + os.Exit(code) +} + // ShowCommandHelp prints help for the given command func ShowCommandHelp(ctx *Context, command string) error { // show the subcommand help for a command with subcommands @@ -143,7 +179,11 @@ func ShowCommandHelp(ctx *Context, command string) error { for _, c := range ctx.App.Commands { if c.HasName(command) { - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + if c.CustomHelpTemplate != "" { + HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil) + } else { + HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + } return nil } } @@ -186,12 +226,17 @@ func ShowCommandCompletions(ctx *Context, command string) { } } -func printHelp(out io.Writer, templ string, data interface{}) { +func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) { funcMap := template.FuncMap{ "join": strings.Join, } + if customFunc != nil { + for key, value := range customFunc { + funcMap[key] = value + } + } - w := tabwriter.NewWriter(out, 0, 8, 1, '\t', 0) + w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) err := t.Execute(w, data) if err != nil { @@ -205,10 +250,14 @@ func printHelp(out io.Writer, templ string, data interface{}) { w.Flush() } +func printHelp(out io.Writer, templ string, data interface{}) { + printHelpCustom(out, templ, data, nil) +} + func checkVersion(c *Context) bool { found := false - if VersionFlag.Name != "" { - eachName(VersionFlag.Name, func(name string) { + if VersionFlag.GetName() != "" { + eachName(VersionFlag.GetName(), func(name string) { if c.GlobalBool(name) || c.Bool(name) { found = true } @@ -219,8 +268,8 @@ func checkVersion(c *Context) bool { func checkHelp(c *Context) bool { found := false - if HelpFlag.Name != "" { - eachName(HelpFlag.Name, func(name string) { + if HelpFlag.GetName() != "" { + eachName(HelpFlag.GetName(), func(name string) { if c.GlobalBool(name) || c.Bool(name) { found = true } @@ -239,7 +288,7 @@ func checkCommandHelp(c *Context, name string) bool { } func checkSubcommandHelp(c *Context) bool { - if c.GlobalBool("h") || c.GlobalBool("help") { + if c.Bool("h") || c.Bool("help") { ShowSubcommandHelp(c) return true } @@ -247,20 +296,43 @@ func checkSubcommandHelp(c *Context) bool { return false } -func checkCompletions(c *Context) bool { - if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion { - ShowCompletions(c) - return true +func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { + if !a.EnableBashCompletion { + return false, arguments } - return false + pos := len(arguments) - 1 + lastArg := arguments[pos] + + if lastArg != "--"+BashCompletionFlag.GetName() { + return false, arguments + } + + return true, arguments[:pos] +} + +func checkCompletions(c *Context) bool { + if !c.shellComplete { + return false + } + + if args := c.Args(); args.Present() { + name := args.First() + if cmd := c.App.Command(name); cmd != nil { + // let the command handle the completion + return false + } + } + + ShowCompletions(c) + return true } func checkCommandCompletions(c *Context, name string) bool { - if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { - ShowCommandCompletions(c, name) - return true + if !c.shellComplete { + return false } - return false + ShowCommandCompletions(c, name) + return true }