cli: simplify option model per pike review

- appendList: one home for the List newline-join rule (was duplicated
  across accumulate and set)
- fold the redundant Int64 getter into Int; drop the truthy helper for
  the shared boolWord vocabulary
- add Opt.Metavar so help derives the value placeholder from data
  instead of branching on the option name (seed-time)

No behavior change; --help output is byte-identical.
This commit is contained in:
2026-06-21 14:34:14 +09:00
parent 508708b039
commit 2d99c39604
4 changed files with 50 additions and 39 deletions

View File

@@ -89,24 +89,31 @@ func flagLabel(o *Opt) string {
b.WriteString(" ") b.WriteString(" ")
} }
b.WriteString("--" + o.Long) b.WriteString("--" + o.Long)
switch o.Kind { if m := metavar(o); m != "" {
case Bool: b.WriteString("=" + m)
// no argument shown
case Size:
b.WriteString("=SIZE")
case Int:
b.WriteString("=N")
case Float:
// seed-time is a count of minutes; only seed-ratio is an actual ratio.
if o.Long == "seed-time" {
b.WriteString("=MIN")
} else {
b.WriteString("=RATIO")
}
case Enum:
b.WriteString("=" + strings.Join(o.Enum, "|"))
default:
b.WriteString("=VAL")
} }
return b.String() return b.String()
} }
// metavar is the placeholder shown after a value-taking flag, e.g. the "N" in
// "--split=N". An explicit Opt.Metavar wins; otherwise it derives from the Kind.
// Bool takes no value, so its metavar is empty and no "=ARG" is printed.
func metavar(o *Opt) string {
if o.Metavar != "" {
return o.Metavar
}
switch o.Kind {
case Bool:
return ""
case Size:
return "SIZE"
case Int:
return "N"
case Float:
return "RATIO"
case Enum:
return strings.Join(o.Enum, "|")
default:
return "VAL"
}
}

View File

@@ -50,6 +50,10 @@ type Opt struct {
Kind Kind Kind Kind
Default string Default string
Help string Help string
// Metavar overrides the "=PLACEHOLDER" shown after the flag in --help. Empty
// means derive it from Kind (N, SIZE, RATIO, ...). Set it only when the
// Kind-derived default would mislead, e.g. seed-time is minutes, not a ratio.
Metavar string
Tag Tag Tag Tag
Enum []string // valid values when Kind == Enum Enum []string // valid values when Kind == Enum
// Min is the inclusive lower Int/Size bound. The zero value enforces a // Min is the inclusive lower Int/Size bound. The zero value enforces a
@@ -103,7 +107,7 @@ var options = []Opt{
// --- bittorrent --- // --- bittorrent ---
{Long: "torrent-file", Short: 'T', Kind: Str, Help: "path to a .torrent file", Tag: BitTorrent}, {Long: "torrent-file", Short: 'T', Kind: Str, Help: "path to a .torrent file", Tag: BitTorrent},
{Long: "seed-time", Kind: Float, Help: "minutes to seed after completion (0 = no seeding)", Tag: BitTorrent}, {Long: "seed-time", Kind: Float, Metavar: "MIN", Help: "minutes to seed after completion (0 = no seeding)", Tag: BitTorrent},
{Long: "seed-ratio", Kind: Float, Default: "1.0", Help: "stop seeding at this share ratio (0 = unlimited)", Tag: BitTorrent}, {Long: "seed-ratio", Kind: Float, Default: "1.0", Help: "stop seeding at this share ratio (0 = unlimited)", Tag: BitTorrent},
{Long: "bt-stop-timeout", Kind: Int, Default: "0", Min: 0, Help: "stop a torrent with no download progress for N seconds (0 = off)", Tag: BitTorrent}, {Long: "bt-stop-timeout", Kind: Int, Default: "0", Min: 0, Help: "stop a torrent with no download progress for N seconds (0 = off)", Tag: BitTorrent},
{Long: "bt-metadata-timeout", Kind: Int, Default: "60", Min: 0, Help: "stop a magnet that can't fetch metadata in N seconds (0 = off)", Tag: BitTorrent}, {Long: "bt-metadata-timeout", Kind: Int, Default: "60", Min: 0, Help: "stop a magnet that can't fetch metadata in N seconds (0 = off)", Tag: BitTorrent},

View File

@@ -48,13 +48,11 @@ func (o *Options) Bool(name string) bool {
return val return val
} }
// Int returns the value as an int, or 0 if empty/invalid. // Int returns the value as an int, or 0 if empty/invalid. The value has already
func (o *Options) Int(name string) int { return int(o.Int64(name)) } // passed validate(), so a parse failure here is unreachable in normal flow.
func (o *Options) Int(name string) int {
// Int64 returns the value as an int64, or 0 if empty/invalid.
func (o *Options) Int64(name string) int64 {
n, _ := strconv.ParseInt(strings.TrimSpace(o.vals[name]), 10, 64) n, _ := strconv.ParseInt(strings.TrimSpace(o.vals[name]), 10, 64)
return n return int(n)
} }
// Float returns the value as a float64, or 0 if empty/invalid. // Float returns the value as a float64, or 0 if empty/invalid.

View File

@@ -41,7 +41,7 @@ func Parse(args []string) (*Result, error) {
opts := newOptions() opts := newOptions()
applyDefaults(opts) applyDefaults(opts)
if !truthy(cmdVals["no-conf"]) { if noConf, _ := boolWord(cmdVals["no-conf"]); !noConf {
explicit := cmdVals["conf-path"] explicit := cmdVals["conf-path"]
path, fromUser := confPath(explicit) path, fromUser := confPath(explicit)
if err := applyConfig(opts, path, fromUser); err != nil { if err := applyConfig(opts, path, fromUser); err != nil {
@@ -138,14 +138,23 @@ func parseArgs(args []string) (vals map[string]string, uris []string, action Act
// accumulate stores a value, joining repeatable List options with newlines. // accumulate stores a value, joining repeatable List options with newlines.
func accumulate(vals map[string]string, o *Opt, val string) { func accumulate(vals map[string]string, o *Opt, val string) {
if o.Kind == List { if o.Kind == List {
if prev, ok := vals[o.Long]; ok { vals[o.Long] = appendList(vals[o.Long], val)
vals[o.Long] = prev + "\n" + val
return return
} }
}
vals[o.Long] = val vals[o.Long] = val
} }
// appendList is the single home of the List newline-join rule, shared by the
// raw-parse layer (accumulate) and the resolved layer (set). An empty prev (the
// option not yet seen) yields val unchanged, so the first value carries no
// leading newline.
func appendList(prev, val string) string {
if prev == "" {
return val
}
return prev + "\n" + val
}
func applyDefaults(o *Options) { func applyDefaults(o *Options) {
for i := range options { for i := range options {
opt := &options[i] opt := &options[i]
@@ -215,10 +224,8 @@ func set(o *Options, name, val string) error {
if err := validate(opt, val); err != nil { if err := validate(opt, val); err != nil {
return err return err
} }
if opt.Kind == List { if opt.Kind == List && o.set[name] {
if prev, ok := o.vals[name]; ok && o.set[name] { val = appendList(o.vals[name], val)
val = prev + "\n" + val
}
} }
o.vals[name] = val o.vals[name] = val
o.set[name] = true o.set[name] = true
@@ -304,8 +311,3 @@ func confPath(explicit string) (path string, fromUser bool) {
} }
return filepath.Join(home, ".config", "got", "got.conf"), false return filepath.Join(home, ".config", "got", "got.conf"), false
} }
func truthy(s string) bool {
val, _ := boolWord(s)
return val
}