Print help that was asked for to stdout (closes #179)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
--help, -h and the help subcommand now print to stdout and exit 0, so the help text can be paged and searched. The usage text of a usage error stays on stderr with exit 2. Cobra prints both through the command's output writer, which stays stderr; the help function points it at stdout before printing. README "Settings" and goal 4 say so, and the help tests read stdout. Model: opus-5-5
This commit is contained in:
1 parent
e16c46804d
commit
82277f84bd
5 files changed
+59
-39
No files matched your search
@@ -255,6 +255,7 @@ In order:
|
|||||||
time, against what the database holds.
|
time, against what the database holds.
|
||||||
4. **Keep the streams apart.** Everything on stdout is machine-readable data.
|
4. **Keep the streams apart.** Everything on stdout is machine-readable data.
|
||||||
Everything for a human reader, such as progress and warnings, goes to stderr.
|
Everything for a human reader, such as progress and warnings, goes to stderr.
|
||||||
|
The one exception is help that was asked for, which goes to stdout.
|
||||||
|
|
||||||
### Subcommands
|
### Subcommands
|
||||||
|
|
||||||
@@ -324,12 +325,13 @@ The reference below is also the program's specification.
|
|||||||
"Operands").
|
"Operands").
|
||||||
- `--version`, `-v` prints `sfdupes VERSION` (see "Install") to stdout and
|
- `--version`, `-v` prints `sfdupes VERSION` (see "Install") to stdout and
|
||||||
exits 0. It writes nothing to stderr.
|
exits 0. It writes nothing to stderr.
|
||||||
- `--help`, `-h`, alone or after a subcommand, prints the help text to stderr
|
- `--help`, `-h`, alone or after a subcommand, prints the help text to stdout
|
||||||
and exits 0. It writes nothing to stdout. The help of `sfdupes` and of `scan`
|
and exits 0. It writes nothing to stderr. `sfdupes help` and
|
||||||
names the database. The help of `report`, `trees`, `info` and `dwim` says what
|
`sfdupes help COMMAND` do the same. The help of `sfdupes` and of `scan` names
|
||||||
each prints. `sfdupes scan --help` and `sfdupes dwim --help` also list the
|
the database. The help of `report`, `trees`, `info` and `dwim` says what each
|
||||||
default excludes of the system they run on, and the filesystem types the walk
|
prints. `sfdupes scan --help` and `sfdupes dwim --help` also list the default
|
||||||
never records anything on.
|
excludes of the system they run on, and the filesystem types the walk never
|
||||||
|
records anything on.
|
||||||
|
|
||||||
### Duplicate detection
|
### Duplicate detection
|
||||||
|
|
||||||
|
|||||||
+15
-3
@@ -201,8 +201,9 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newRootCommand builds the command tree. Everything on stdout is
|
// newRootCommand builds the command tree. Everything on stdout is
|
||||||
// machine-readable data, the version line included; all human-facing
|
// machine-readable data, the version line included, or help that was
|
||||||
// output (help, usage, errors) goes to stderr.
|
// asked for. The rest of the human-facing output (usage, errors) goes
|
||||||
|
// to stderr.
|
||||||
func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
|
func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
|
||||||
var showVersion bool
|
var showVersion bool
|
||||||
|
|
||||||
@@ -235,11 +236,22 @@ func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
|
|||||||
return errNoSubcommand
|
return errNoSubcommand
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// Cobra prints the usage text of a usage error to the command's
|
||||||
|
// output writer, so that writer is stderr.
|
||||||
root.SetOut(stderr)
|
root.SetOut(stderr)
|
||||||
root.SetErr(stderr)
|
root.SetErr(stderr)
|
||||||
root.CompletionOptions.DisableDefaultCmd = true
|
root.CompletionOptions.DisableDefaultCmd = true
|
||||||
|
|
||||||
// Cobra's built-in version flag prints through the help writer,
|
// Cobra's default help also prints to the output writer. Help that
|
||||||
|
// was asked for, with --help, -h or the help subcommand, goes to
|
||||||
|
// stdout instead.
|
||||||
|
printHelp := root.HelpFunc()
|
||||||
|
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||||
|
cmd.SetOut(stdout)
|
||||||
|
printHelp(cmd, args)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cobra's built-in version flag prints to the output writer,
|
||||||
// stderr; this one prints to stdout.
|
// stderr; this one prints to stdout.
|
||||||
root.Flags().BoolVarP(&showVersion, "version", "v", false,
|
root.Flags().BoolVarP(&showVersion, "version", "v", false,
|
||||||
"print the version to stdout")
|
"print the version to stdout")
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ const usageMarker = "Usage:"
|
|||||||
// helpFlag asks for the help text.
|
// helpFlag asks for the help text.
|
||||||
const helpFlag = "--help"
|
const helpFlag = "--help"
|
||||||
|
|
||||||
|
// helpCommand is the subcommand that prints the help text.
|
||||||
|
const helpCommand = "help"
|
||||||
|
|
||||||
// workersFlag is the scan and dwim flag that sets the worker count.
|
// workersFlag is the scan and dwim flag that sets the worker count.
|
||||||
const workersFlag = "--workers"
|
const workersFlag = "--workers"
|
||||||
|
|
||||||
@@ -700,9 +703,12 @@ func TestRunScanRejectsWorkersBelowOne(t *testing.T) {
|
|||||||
func TestRunHelp(t *testing.T) {
|
func TestRunHelp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// README §Settings: help goes to stderr, exits 0, and leaves stdout
|
// README §Settings: help goes to stdout, exits 0, and leaves stderr
|
||||||
// empty.
|
// empty.
|
||||||
cases := [][]string{{helpFlag}, {"-h"}, {cmdScan, helpFlag}}
|
cases := [][]string{
|
||||||
|
{helpFlag}, {"-h"}, {helpCommand},
|
||||||
|
{cmdScan, helpFlag}, {helpCommand, cmdScan},
|
||||||
|
}
|
||||||
|
|
||||||
for _, args := range cases {
|
for _, args := range cases {
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
@@ -712,14 +718,13 @@ func TestRunHelp(t *testing.T) {
|
|||||||
t.Errorf("run(%v) = %d, want %d", args, code, exitOK)
|
t.Errorf("run(%v) = %d, want %d", args, code, exitOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), usageMarker) {
|
if !strings.Contains(stdout.String(), usageMarker) {
|
||||||
t.Errorf("run(%v) stderr = %q, want the help text",
|
t.Errorf("run(%v) stdout = %q, want the help text",
|
||||||
args, stderr.String())
|
args, stdout.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := stdout.String(); got != "" {
|
if got := stderr.String(); got != "" {
|
||||||
t.Errorf("run(%v) stdout = %q, want nothing (data only)",
|
t.Errorf("run(%v) stderr = %q, want nothing", args, got)
|
||||||
args, got)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// README §Location: the help of sfdupes and of scan says where
|
// README §Location: the help of sfdupes and of scan says where
|
||||||
@@ -729,9 +734,9 @@ func TestRunHelp(t *testing.T) {
|
|||||||
"scan and dwim store", databaseEnv, rootDatabasePath,
|
"scan and dwim store", databaseEnv, rootDatabasePath,
|
||||||
stateHomeEnv, "~/.local/state", "not an absolute path is an error",
|
stateHomeEnv, "~/.local/state", "not an absolute path is an error",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(stderr.String(), want) {
|
if !strings.Contains(stdout.String(), want) {
|
||||||
t.Errorf("run(%v) stderr = %q, want it to name %q",
|
t.Errorf("run(%v) stdout = %q, want it to name %q",
|
||||||
args, stderr.String(), want)
|
args, stdout.String(), want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -741,8 +746,8 @@ func TestRunHelp(t *testing.T) {
|
|||||||
|
|
||||||
run([]string{cmdScan, "-h"}, &stdout, &stderr)
|
run([]string{cmdScan, "-h"}, &stdout, &stderr)
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), excludeFlag+" PATH") {
|
if !strings.Contains(stdout.String(), excludeFlag+" PATH") {
|
||||||
t.Errorf("scan -h stderr = %q, want %s PATH", stderr.String(),
|
t.Errorf("scan -h stdout = %q, want %s PATH", stdout.String(),
|
||||||
excludeFlag)
|
excludeFlag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -751,7 +756,8 @@ func TestRunScanUsageLine(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// README §Subcommands shows this usage line for scan, and the help
|
// README §Subcommands shows this usage line for scan, and the help
|
||||||
// and the usage error both print it.
|
// and the usage error both print it. TestRunHelp and
|
||||||
|
// TestRunUsageErrors check which stream each goes to.
|
||||||
const want = "\n sfdupes scan [--workers N] [-x] [--no-default-excludes] " +
|
const want = "\n sfdupes scan [--workers N] [-x] [--no-default-excludes] " +
|
||||||
"[--exclude PATH]... PATH...\n"
|
"[--exclude PATH]... PATH...\n"
|
||||||
|
|
||||||
@@ -764,13 +770,13 @@ func TestRunScanUsageLine(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
var output bytes.Buffer
|
||||||
|
|
||||||
run(args, &stdout, &stderr)
|
run(args, &output, &output)
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), want) {
|
if !strings.Contains(output.String(), want) {
|
||||||
t.Errorf("run(%v) stderr = %q, want the usage line %q",
|
t.Errorf("run(%v) output = %q, want the usage line %q",
|
||||||
args, stderr.String(), want)
|
args, output.String(), want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -836,9 +842,9 @@ func TestRunHelpDescribesOutput(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, want := range tc.want {
|
for _, want := range tc.want {
|
||||||
if !strings.Contains(stderr.String(), want) {
|
if !strings.Contains(stdout.String(), want) {
|
||||||
t.Errorf("run(%v) help lacks %q:\n%s",
|
t.Errorf("run(%v) help lacks %q:\n%s",
|
||||||
args, want, stderr.String())
|
args, want, stdout.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,21 +190,21 @@ func TestRunDwimHelpListsScanFlags(t *testing.T) {
|
|||||||
|
|
||||||
run([]string{cmdDwim, "-h"}, &stdout, &stderr)
|
run([]string{cmdDwim, "-h"}, &stdout, &stderr)
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), want) {
|
if !strings.Contains(stdout.String(), want) {
|
||||||
t.Errorf("dwim -h stderr = %q, want the usage line %q",
|
t.Errorf("dwim -h stdout = %q, want the usage line %q",
|
||||||
stderr.String(), want)
|
stdout.String(), want)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), excludeFlag+" PATH") {
|
if !strings.Contains(stdout.String(), excludeFlag+" PATH") {
|
||||||
t.Errorf("dwim -h stderr = %q, want %s PATH", stderr.String(),
|
t.Errorf("dwim -h stdout = %q, want %s PATH", stdout.String(),
|
||||||
excludeFlag)
|
excludeFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// README "Settings": the help lists the default excludes and the
|
// README "Settings": the help lists the default excludes and the
|
||||||
// filesystem types the walk never records anything on.
|
// filesystem types the walk never records anything on.
|
||||||
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
|
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
|
||||||
if !strings.Contains(stderr.String(), help) {
|
if !strings.Contains(stdout.String(), help) {
|
||||||
t.Errorf("dwim -h stderr = %q, want it to hold %q", stderr.String(),
|
t.Errorf("dwim -h stdout = %q, want it to hold %q", stdout.String(),
|
||||||
help)
|
help)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,8 +196,8 @@ func TestDefaultExcludesHelp(t *testing.T) {
|
|||||||
run([]string{cmdScan, "-h"}, &stdout, &stderr)
|
run([]string{cmdScan, "-h"}, &stdout, &stderr)
|
||||||
|
|
||||||
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
|
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
|
||||||
if !strings.Contains(stderr.String(), help) {
|
if !strings.Contains(stdout.String(), help) {
|
||||||
t.Errorf("scan --help = %q, want it to hold %q", stderr.String(), help)
|
t.Errorf("scan --help = %q, want it to hold %q", stdout.String(), help)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user