cli: improve argument validation output

Improve the output for these validation errors:

- Removes the short command description from the output. This information
  does not provide much useful help, and distracts from the error message.
- Reduces punctuation, and
- Prefixes the error message with the binary / root-command name
  (usually `docker:`) to be consistent with other similar errors.
- Adds an empty line between the error-message and the "call to action"
  (`Run 'docker volume --help'...` in the example below). This helps
  separating the error message and "usage" from the call-to-action.

Before this patch:

    $ docker volume ls one two three
    "docker volume ls" accepts no arguments.
    See 'docker volume ls --help'.

    Usage:  docker volume ls [OPTIONS]

    List volumes

    $ docker volume create one two three
    "docker volume create" requires at most 1 argument.
    See 'docker volume create --help'.

    Usage:  docker volume create [OPTIONS] [VOLUME]

    Create a volume

With this patch:

    $ docker volume ls one two three
    docker: 'docker volume ls' accepts no arguments

    Usage:  docker volume ls [OPTIONS]

    Run 'docker volume ls --help' for more information

    $ docker voludocker volume create one two three
    docker: 'docker volume create' requires at most 1 argument

    Usage:  docker volume create [OPTIONS] [VOLUME]

    SRun 'docker volume create --help' for more information

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2024-07-05 02:49:31 +02:00
parent a6e96c758e
commit c60b360c33
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
37 changed files with 71 additions and 75 deletions

View File

@ -20,11 +20,11 @@ func TestCheckpointCreateErrors(t *testing.T) {
}{ }{
{ {
args: []string{"too-few-arguments"}, args: []string{"too-few-arguments"},
expectedError: "requires exactly 2 arguments", expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments", expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},

View File

@ -20,11 +20,11 @@ func TestCheckpointListErrors(t *testing.T) {
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},

View File

@ -19,11 +19,11 @@ func TestCheckpointRemoveErrors(t *testing.T) {
}{ }{
{ {
args: []string{"too-few-arguments"}, args: []string{"too-few-arguments"},
expectedError: "requires exactly 2 arguments", expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments", expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},

View File

@ -28,11 +28,11 @@ func TestConfigCreateErrors(t *testing.T) {
}{ }{
{ {
args: []string{"too_few"}, args: []string{"too_few"},
expectedError: "requires exactly 2 arguments", expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments", expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"name", filepath.Join("testdata", configDataFile)}, args: []string{"name", filepath.Join("testdata", configDataFile)},

View File

@ -19,7 +19,7 @@ func TestConfigRemoveErrors(t *testing.T) {
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires at least 1 argument.", expectedError: "requires at least 1 argument",
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},

View File

@ -23,7 +23,7 @@ func TestNewHistoryCommandErrors(t *testing.T) {
{ {
name: "wrong-args", name: "wrong-args",
args: []string{}, args: []string{},
expectedError: "requires exactly 1 argument.", expectedError: "requires 1 argument",
}, },
{ {
name: "client-error", name: "client-error",

View File

@ -22,7 +22,7 @@ func TestNewImportCommandErrors(t *testing.T) {
{ {
name: "wrong-args", name: "wrong-args",
args: []string{}, args: []string{},
expectedError: "requires at least 1 argument.", expectedError: "requires at least 1 argument",
}, },
{ {
name: "import-failed", name: "import-failed",

View File

@ -21,7 +21,7 @@ func TestNewInspectCommandErrors(t *testing.T) {
{ {
name: "wrong-args", name: "wrong-args",
args: []string{}, args: []string{},
expectedError: "requires at least 1 argument.", expectedError: "requires at least 1 argument",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {

View File

@ -24,7 +24,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
{ {
name: "wrong-args", name: "wrong-args",
args: []string{"arg1", "arg2"}, args: []string{"arg1", "arg2"},
expectedError: "requires at most 1 argument.", expectedError: "requires at most 1 argument",
}, },
{ {
name: "failed-list", name: "failed-list",

View File

@ -24,7 +24,7 @@ func TestNewLoadCommandErrors(t *testing.T) {
{ {
name: "wrong-args", name: "wrong-args",
args: []string{"arg"}, args: []string{"arg"},
expectedError: "accepts no arguments.", expectedError: "accepts no arguments",
}, },
{ {
name: "input-to-terminal", name: "input-to-terminal",

View File

@ -27,7 +27,7 @@ func TestNewPruneCommandErrors(t *testing.T) {
{ {
name: "wrong-args", name: "wrong-args",
args: []string{"something"}, args: []string{"something"},
expectedError: "accepts no arguments.", expectedError: "accepts no arguments",
}, },
{ {
name: "prune-error", name: "prune-error",

View File

@ -23,7 +23,7 @@ func TestNewPullCommandErrors(t *testing.T) {
}{ }{
{ {
name: "wrong-args", name: "wrong-args",
expectedError: "requires exactly 1 argument.", expectedError: "requires 1 argument",
args: []string{}, args: []string{},
}, },
{ {

View File

@ -21,7 +21,7 @@ func TestNewPushCommandErrors(t *testing.T) {
{ {
name: "wrong-args", name: "wrong-args",
args: []string{}, args: []string{},
expectedError: "requires exactly 1 argument.", expectedError: "requires 1 argument",
}, },
{ {
name: "invalid-name", name: "invalid-name",

View File

@ -39,7 +39,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
}{ }{
{ {
name: "wrong args", name: "wrong args",
expectedError: "requires at least 1 argument.", expectedError: "requires at least 1 argument",
}, },
{ {
name: "ImageRemove fail with force option", name: "ImageRemove fail with force option",

View File

@ -23,7 +23,7 @@ func TestNewSaveCommandErrors(t *testing.T) {
{ {
name: "wrong args", name: "wrong args",
args: []string{}, args: []string{},
expectedError: "requires at least 1 argument.", expectedError: "requires at least 1 argument",
}, },
{ {
name: "output to terminal", name: "output to terminal",

View File

@ -15,7 +15,7 @@ func TestCliNewTagCommandErrors(t *testing.T) {
{"image1"}, {"image1"},
{"image1", "image2", "image3"}, {"image1", "image2", "image3"},
} }
expectedError := "\"tag\" requires exactly 2 arguments." expectedError := "'tag' requires 2 arguments"
for _, args := range testCases { for _, args := range testCases {
cmd := NewTagCommand(test.NewFakeCli(&fakeClient{})) cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}))
cmd.SetArgs(args) cmd.SetArgs(args)

View File

@ -18,7 +18,7 @@ func TestManifestAnnotateError(t *testing.T) {
}{ }{
{ {
args: []string{"too-few-arguments"}, args: []string{"too-few-arguments"},
expectedError: "requires exactly 2 arguments", expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"}, args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"},

View File

@ -31,7 +31,7 @@ func TestManifestPushErrors(t *testing.T) {
}{ }{
{ {
args: []string{"one-arg", "extra-arg"}, args: []string{"one-arg", "extra-arg"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"th!si'sa/fa!ke/li$t/-name"}, args: []string{"th!si'sa/fa!ke/li$t/-name"},

View File

@ -19,7 +19,7 @@ func TestNetworkConnectErrors(t *testing.T) {
expectedError string expectedError string
}{ }{
{ {
expectedError: "requires exactly 2 arguments", expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"toto", "titi"}, args: []string{"toto", "titi"},

View File

@ -21,7 +21,7 @@ func TestNetworkCreateErrors(t *testing.T) {
expectedError string expectedError string
}{ }{
{ {
expectedError: "exactly 1 argument", expectedError: "1 argument",
}, },
{ {
args: []string{"toto"}, args: []string{"toto"},

View File

@ -17,7 +17,7 @@ func TestNetworkDisconnectErrors(t *testing.T) {
expectedError string expectedError string
}{ }{
{ {
expectedError: "requires exactly 2 arguments", expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"toto", "titi"}, args: []string{"toto", "titi"},

View File

@ -20,11 +20,11 @@ func TestNodeUpdateErrors(t *testing.T) {
expectedError string expectedError string
}{ }{
{ {
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"node1", "node2"}, args: []string{"node1", "node2"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"nodeID"}, args: []string{"nodeID"},

View File

@ -19,11 +19,11 @@ func TestPluginDisableErrors(t *testing.T) {
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"plugin-foo"}, args: []string{"plugin-foo"},

View File

@ -20,11 +20,11 @@ func TestPluginEnableErrors(t *testing.T) {
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"too-many", "arguments"}, args: []string{"too-many", "arguments"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"plugin-foo"}, args: []string{"plugin-foo"},

View File

@ -20,7 +20,7 @@ func TestSecretRemoveErrors(t *testing.T) {
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires at least 1 argument.", expectedError: "requires at least 1 argument",
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},

View File

@ -65,12 +65,12 @@ func TestRollbackWithErrors(t *testing.T) {
}{ }{
{ {
name: "not-enough-args", name: "not-enough-args",
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "too-many-args", name: "too-many-args",
args: []string{"service-id-1", "service-id-2"}, args: []string{"service-id-1", "service-id-2"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "service-does-not-exists", name: "service-does-not-exists",

View File

@ -24,11 +24,11 @@ func TestStackPsErrors(t *testing.T) {
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},

View File

@ -23,12 +23,12 @@ func TestSwarmJoinErrors(t *testing.T) {
}{ }{
{ {
name: "not-enough-args", name: "not-enough-args",
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "too-many-args", name: "too-many-args",
args: []string{"remote1", "remote2"}, args: []string{"remote1", "remote2"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "join-failed", name: "join-failed",

View File

@ -27,12 +27,12 @@ func TestSwarmJoinTokenErrors(t *testing.T) {
}{ }{
{ {
name: "not-enough-args", name: "not-enough-args",
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "too-many-args", name: "too-many-args",
args: []string{"worker", "manager"}, args: []string{"worker", "manager"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "invalid-args", name: "invalid-args",

View File

@ -26,12 +26,12 @@ func TestTrustKeyGenerateErrors(t *testing.T) {
}{ }{
{ {
name: "not-enough-args", name: "not-enough-args",
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "too-many-args", name: "too-many-args",
args: []string{"key-1", "key-2"}, args: []string{"key-1", "key-2"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
} }

View File

@ -34,13 +34,14 @@ func TestTrustKeyLoadErrors(t *testing.T) {
}{ }{
{ {
name: "not-enough-args", name: "not-enough-args",
expectedError: "exactly 1 argument", expectedError: "1 argument",
args: []string{},
expectedOutput: "", expectedOutput: "",
}, },
{ {
name: "too-many-args", name: "too-many-args",
args: []string{"iamnotakey", "alsonotakey"}, args: []string{"iamnotakey", "alsonotakey"},
expectedError: "exactly 1 argument", expectedError: "1 argument",
expectedOutput: "", expectedOutput: "",
}, },
{ {

View File

@ -24,12 +24,12 @@ func TestTrustRevokeCommandErrors(t *testing.T) {
}{ }{
{ {
name: "not-enough-args", name: "not-enough-args",
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "too-many-args", name: "too-many-args",
args: []string{"remote1", "remote2"}, args: []string{"remote1", "remote2"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "sha-reference", name: "sha-reference",

View File

@ -32,12 +32,12 @@ func TestTrustSignCommandErrors(t *testing.T) {
}{ }{
{ {
name: "not-enough-args", name: "not-enough-args",
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "too-many-args", name: "too-many-args",
args: []string{"image", "tag"}, args: []string{"image", "tag"},
expectedError: "requires exactly 1 argument", expectedError: "requires 1 argument",
}, },
{ {
name: "sha-reference", name: "sha-reference",

View File

@ -22,11 +22,10 @@ func NoArgs(cmd *cobra.Command, args []string) error {
} }
return errors.Errorf( return errors.Errorf(
"%q accepts no arguments.\nSee '%s --help'.\n\nUsage: %s\n\n%s", "%[1]s: '%[2]s' accepts no arguments\n\nUsage: %[3]s\n\nRun '%[2]s --help' for more information",
cmd.CommandPath(), binName(cmd),
cmd.CommandPath(), cmd.CommandPath(),
cmd.UseLine(), cmd.UseLine(),
cmd.Short,
) )
} }
@ -37,13 +36,12 @@ func RequiresMinArgs(min int) cobra.PositionalArgs {
return nil return nil
} }
return errors.Errorf( return errors.Errorf(
"%q requires at least %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", "%[1]s: '%[2]s' requires at least %[3]d %[4]s\n\nUsage: %[5]s\n\nSee '%[2]s --help' for more information",
binName(cmd),
cmd.CommandPath(), cmd.CommandPath(),
min, min,
pluralize("argument", min), pluralize("argument", min),
cmd.CommandPath(),
cmd.UseLine(), cmd.UseLine(),
cmd.Short,
) )
} }
} }
@ -55,13 +53,12 @@ func RequiresMaxArgs(max int) cobra.PositionalArgs {
return nil return nil
} }
return errors.Errorf( return errors.Errorf(
"%q requires at most %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", "%[1]s: '%[2]s' requires at most %[3]d %[4]s\n\nUsage: %[5]s\n\nSRun '%[2]s --help' for more information",
binName(cmd),
cmd.CommandPath(), cmd.CommandPath(),
max, max,
pluralize("argument", max), pluralize("argument", max),
cmd.CommandPath(),
cmd.UseLine(), cmd.UseLine(),
cmd.Short,
) )
} }
} }
@ -73,14 +70,13 @@ func RequiresRangeArgs(min int, max int) cobra.PositionalArgs {
return nil return nil
} }
return errors.Errorf( return errors.Errorf(
"%q requires at least %d and at most %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", "%[1]s: '%[2]s' requires at least %[3]d and at most %[4]d %[5]s\n\nUsage: %[6]s\n\nRun '%[2]s --help' for more information",
binName(cmd),
cmd.CommandPath(), cmd.CommandPath(),
min, min,
max, max,
pluralize("argument", max), pluralize("argument", max),
cmd.CommandPath(),
cmd.UseLine(), cmd.UseLine(),
cmd.Short,
) )
} }
} }
@ -92,13 +88,12 @@ func ExactArgs(number int) cobra.PositionalArgs {
return nil return nil
} }
return errors.Errorf( return errors.Errorf(
"%q requires exactly %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", "%[1]s: '%[2]s' requires %[3]d %[4]s\n\nUsage: %[5]s\n\nRun '%[2]s --help' for more information",
binName(cmd),
cmd.CommandPath(), cmd.CommandPath(),
number, number,
pluralize("argument", number), pluralize("argument", number),
cmd.CommandPath(),
cmd.UseLine(), cmd.UseLine(),
cmd.Short,
) )
} }
} }

View File

@ -18,7 +18,7 @@ func TestRequiresNoArgs(t *testing.T) {
{ {
args: []string{"foo"}, args: []string{"foo"},
validateFunc: NoArgs, validateFunc: NoArgs,
expectedError: "accepts no arguments.", expectedError: "accepts no arguments",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -36,12 +36,12 @@ func TestRequiresMinArgs(t *testing.T) {
}, },
{ {
validateFunc: RequiresMinArgs(1), validateFunc: RequiresMinArgs(1),
expectedError: "at least 1 argument.", expectedError: "at least 1 argument",
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},
validateFunc: RequiresMinArgs(2), validateFunc: RequiresMinArgs(2),
expectedError: "at least 2 arguments.", expectedError: "at least 2 arguments",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -60,12 +60,12 @@ func TestRequiresMaxArgs(t *testing.T) {
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
validateFunc: RequiresMaxArgs(1), validateFunc: RequiresMaxArgs(1),
expectedError: "at most 1 argument.", expectedError: "at most 1 argument",
}, },
{ {
args: []string{"foo", "bar", "baz"}, args: []string{"foo", "bar", "baz"},
validateFunc: RequiresMaxArgs(2), validateFunc: RequiresMaxArgs(2),
expectedError: "at most 2 arguments.", expectedError: "at most 2 arguments",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -88,12 +88,12 @@ func TestRequiresRangeArgs(t *testing.T) {
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
validateFunc: RequiresRangeArgs(0, 1), validateFunc: RequiresRangeArgs(0, 1),
expectedError: "at most 1 argument.", expectedError: "at most 1 argument",
}, },
{ {
args: []string{"foo", "bar", "baz"}, args: []string{"foo", "bar", "baz"},
validateFunc: RequiresRangeArgs(0, 2), validateFunc: RequiresRangeArgs(0, 2),
expectedError: "at most 2 arguments.", expectedError: "at most 2 arguments",
}, },
{ {
validateFunc: RequiresRangeArgs(1, 2), validateFunc: RequiresRangeArgs(1, 2),
@ -115,11 +115,11 @@ func TestExactArgs(t *testing.T) {
}, },
{ {
validateFunc: ExactArgs(1), validateFunc: ExactArgs(1),
expectedError: "exactly 1 argument.", expectedError: "1 argument",
}, },
{ {
validateFunc: ExactArgs(2), validateFunc: ExactArgs(2),
expectedError: "exactly 2 arguments.", expectedError: "2 arguments",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {

View File

@ -66,7 +66,7 @@ func TestExitStatusForInvalidSubcommandWithHelpFlag(t *testing.T) {
func TestExitStatusForInvalidSubcommand(t *testing.T) { func TestExitStatusForInvalidSubcommand(t *testing.T) {
err := runCliCommand(t, nil, nil, "invalid") err := runCliCommand(t, nil, nil, "invalid")
assert.Check(t, is.ErrorContains(err, "docker: 'docker invalid' is not a docker command.")) assert.Check(t, is.ErrorContains(err, "docker: unknown command: docker invalid"))
} }
func TestVersion(t *testing.T) { func TestVersion(t *testing.T) {

View File

@ -180,14 +180,14 @@ func TestCliPluginsVersion(t *testing.T) {
args: []string{"version", "foo"}, args: []string{"version", "foo"},
expCode: 1, expCode: 1,
expOut: icmd.None, expOut: icmd.None,
expErr: `"docker version" accepts no arguments.`, expErr: `docker: 'docker version' accepts no arguments`,
}, },
{ {
name: "global-with-plugin-arg", name: "global-with-plugin-arg",
args: []string{"version", "helloworld"}, args: []string{"version", "helloworld"},
expCode: 1, expCode: 1,
expOut: icmd.None, expOut: icmd.None,
expErr: `"docker version" accepts no arguments.`, expErr: `docker: 'docker version' accepts no arguments`,
}, },
{ {
name: "global-version-flag-with-unknown-arg", name: "global-version-flag-with-unknown-arg",