1 Commits
Author SHA1 Message Date
sneak 1d69f08434 Give -v to verbose, move --version to -V (closes #64)
check / check (push) Successful in 1m18s
urfave/cli's built-in version flag claimed -v, colliding with the -v
verbose alias on every subcommand, so "mfer -v --version" failed with a
parser error. Verbose is the more common meaning of -v in tools that
offer both, so verbose keeps -v at the root and on every subcommand, and
the version flag takes -V (--version still works).

User-visible change: -v alone now prints help with verbose logging, not
the version; use -V or --version.

The --version flag and the version subcommand share one printer, so
their output is identical. Tests cover -v, --verbose, --version, -V,
-v --version and --verbose --version. The README roadmap item for
--version is checked off.

Model: opus-4-8 (implementation); opus-5-5 (rebase)
2026-10-03 23:56:26 +00:00
3 changed files with 9 additions and 73 deletions
-41
View File
@@ -177,47 +177,6 @@ func TestVersionFlagAndCommandMatch(t *testing.T) {
assert.Equal(t, testStdout(t, flagOpts), testStdout(t, cmdOpts))
}
// TestVerbosityFlagsBeforeSubcommand asserts that -q and -v given before the
// subcommand name take effect in that subcommand (issue #64).
func TestVerbosityFlagsBeforeSubcommand(t *testing.T) {
t.Parallel()
t.Run("quiet before fetch hides the banner", func(t *testing.T) {
t.Parallel()
opts := testOpts([]string{testApp, "-q", cmdFetch}, afero.NewMemMapFs())
assert.Equal(t, 1, runCLI(opts))
assert.Contains(t, testStderr(t, opts), errURLRequired.Error())
assert.NotContains(t, testStdout(t, opts), banner)
})
t.Run("verbose twice before fetch enables debug logging", func(t *testing.T) {
t.Parallel()
opts := testOpts([]string{testApp, "-v", "-v", cmdFetch}, afero.NewMemMapFs())
assert.Equal(t, 1, runCLI(opts))
assert.Contains(t, testStderr(t, opts), "fetchManifestOperation()")
})
t.Run("quiet before export hides the manifest summary", func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
manifest := buildTestManifest(t, map[string][]byte{"a.txt": []byte("a")})
require.NoError(t, afero.WriteFile(fs, testManifest, manifest, 0o644))
loud := testOpts([]string{testApp, cmdExport, testManifest}, fs)
require.Equal(t, 0, runCLI(loud))
assert.Contains(t, testStderr(t, loud), "loaded manifest")
quiet := testOpts([]string{testApp, "-q", cmdExport, testManifest}, fs)
require.Equal(t, 0, runCLI(quiet))
assert.NotContains(t, testStderr(t, quiet), "loaded manifest")
})
}
func TestHelpCommand(t *testing.T) {
t.Parallel()
+2 -8
View File
@@ -5,7 +5,6 @@ import (
"bytes"
"context"
"flag"
"io"
"net/http"
"net/http/httptest"
"os"
@@ -17,7 +16,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
urfcli "github.com/urfave/cli/v2"
"sneak.berlin/go/mfer/internal/log"
"sneak.berlin/go/mfer/mfer"
)
@@ -38,16 +36,12 @@ const (
msgFpB = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
)
// runLocked runs fn while holding runMu and with the process-global logger
// pointed at io.Discard, so fn's log lines neither race the other CLI runs
// nor land in the last run's buffers, which its test may still be reading.
// runLocked runs fn while holding runMu, so operations that write to the
// process-global logger do not race the other CLI runs.
func runLocked(fn func() error) error {
runMu.Lock()
defer runMu.Unlock()
log.SetOutput(io.Discard, io.Discard)
log.Init()
return fn()
}
+7 -24
View File
@@ -85,34 +85,20 @@ func (mfa *CLIApp) printBanner() {
}
}
// setVerbosity sets the log level from -v and -q, given before the subcommand
// name (the root's copies), after it (the subcommand's own copies), or both.
// urfave/cli reads a flag from the nearest command that defines it, so each
// command in the lineage is asked. The highest -v count wins rather than the
// sum, because a subcommand without its own copies reads the root's.
func (mfa *CLIApp) setVerbosity(c *cli.Context) {
_, present := os.LookupEnv("MFER_DEBUG")
verbosity := 0
quiet := false
for _, ctx := range c.Lineage() {
verbosity = max(verbosity, ctx.Count("verbose"))
quiet = quiet || ctx.Bool("quiet")
}
switch {
case present:
log.EnableDebugLogging()
case quiet:
case c.Bool("quiet"):
log.SetLevel(log.ErrorLevel)
default:
log.SetLevelFromVerbosity(verbosity)
log.SetLevelFromVerbosity(c.Count("verbose"))
}
}
// commonFlags returns the -v and -q flags taken by the root and by the
// generate, check, freshen and fetch subcommands.
// commonFlags returns the flags shared by most commands (-v, -q)
func commonFlags() []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
@@ -280,8 +266,6 @@ func (mfa *CLIApp) exportCommand() *cli.Command {
Usage: "Export manifest contents as JSON",
ArgsUsage: "[manifest file or URL]",
Action: func(c *cli.Context) error {
mfa.setVerbosity(c)
return mfa.exportManifestOperation(c)
},
}
@@ -349,11 +333,10 @@ func (mfa *CLIApp) run(args []string) {
log.Init()
// -v means verbose, not version. urfave/cli's built-in version flag
// claims -v by default, which made "mfer -v --version" fail to parse and
// gave -v a different meaning at the root than on the generate, check,
// freshen and fetch subcommands, where it means verbose. Verbose is the
// more common meaning of -v in tools that offer both, so -v means verbose
// at the root too and the version flag takes the capital -V.
// claims -v by default, colliding with the -v verbose alias used here and
// on every subcommand; that collision makes "mfer -v --version" fail to
// parse. Verbose is the more common meaning of -v in tools that offer
// both, so verbose keeps -v and the version flag takes the capital -V.
// VersionFlag and VersionPrinter are urfave/cli package globals; run() is
// serialized in tests, so assigning them here is safe.
cli.VersionFlag = &cli.BoolFlag{