fix(helpers): honor alias/env/default fallback for integer leaf flags

The leaf fallback chain read only string flags, so LeafInt/LeafInt64
flags could never satisfy Required via alias or env, alias values for
integer flags were silently dropped, and a registered Default shadowed
alias/env values. Resolution order is now explicit flag > alias > env >
Default > ArgDefault, aliases register with the primary flag's Kind, and
unparsable integer env values fail loudly.
This commit is contained in:
玉澜
2026-07-28 21:13:29 +08:00
parent 4b0f71eedc
commit eb2658ca68
2 changed files with 192 additions and 19 deletions
+68 -19
View File
@@ -16,6 +16,7 @@ package helpers
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
@@ -53,7 +54,7 @@ type LeafFlag struct {
Name string // flag 名(kebab-case)
Usage string // 注册 usage 文案
Kind LeafFlagKind // 值类型,默认 LeafString
Default string // 注册默认值(仅 LeafString 有效)
Default string // 注册默认值(仅 LeafString 有效);回退链链尾兜底,不遮蔽别名/env
// Required 为 true 时在 RunE 期校验有效值非空。普通 Required 汇聚为
// cmdutil.ValidateRequiredFlags 兼容的统一报错;配置 EnvVar 时回退读
@@ -64,8 +65,8 @@ type LeafFlag struct {
// 投影的硬下限),cobra 会在 RunE 之前先行报错。
MarkRequired bool
Aliases []string // 隐藏别名,主 flag 为空时按序回退(字符串)
EnvVar string // 有效值为空时回退读取的环境变量
Aliases []string // 隐藏别名,按主 flag 的 Kind 注册;主 flag 未显式提供时按序回退
EnvVar string // 有效值为空时回退读取的环境变量(整型 flag 的 env 值必须可解析)
// ArgDefault:注册默认值为空、但 toolArgs 需要兜底的场景(如 list type
// 注册默认 "ALL" 之外的旧命令);有效值为空时以 ArgDefault 入参。
ArgDefault string
@@ -134,8 +135,16 @@ func NewLeafCommand(spec LeafSpec) *cobra.Command {
default:
cmd.Flags().String(flag.Name, flag.Default, flag.Usage)
}
// 别名按主 flag 的 Kind 注册,否则整型别名的值永远读不到(静默丢弃)。
for _, alias := range flag.Aliases {
cmd.Flags().String(alias, "", flag.Usage+" (alias)")
switch flag.Kind {
case LeafInt64:
cmd.Flags().Int64(alias, 0, flag.Usage+" (alias)")
case LeafInt:
cmd.Flags().Int(alias, 0, flag.Usage+" (alias)")
default:
cmd.Flags().String(alias, "", flag.Usage+" (alias)")
}
_ = cmd.Flags().MarkHidden(alias)
}
if flag.MarkRequired {
@@ -210,15 +219,17 @@ func leafArgs(cmd *cobra.Command, spec LeafSpec) (map[string]any, error) {
if bind == "" {
bind = flag.Name
}
if flag.Kind == LeafInt64 {
if v, _ := cmd.Flags().GetInt64(flag.Name); v > 0 {
toolArgs[bind] = v
if flag.Kind == LeafInt64 || flag.Kind == LeafInt {
v, err := leafIntegerValue(cmd, flag)
if err != nil {
return nil, err
}
continue
}
if flag.Kind == LeafInt {
if v, _ := cmd.Flags().GetInt(flag.Name); v != 0 {
// LeafInt64 保持「值 > 0 才入参」(分页游标语义);LeafInt 保持
// 「非零才入参」(putInt 语义)。
if flag.Kind == LeafInt64 && v > 0 {
toolArgs[bind] = v
} else if flag.Kind == LeafInt && v != 0 {
toolArgs[bind] = int(v)
}
continue
}
@@ -245,8 +256,9 @@ func leafArgs(cmd *cobra.Command, spec LeafSpec) (map[string]any, error) {
return toolArgs, nil
}
// leafEffectiveValue 按「主 flag → 别名 → 环境变量」顺序取有效值;Trim 为
// true 时对结果统一 TrimSpace。
// leafEffectiveValue 按「显式主 flag → 别名 → 环境变量 → 注册默认值」顺序取
// 有效值(字符串形态,整型 flag 统一格式化);Trim 为 true 时对结果统一
// TrimSpace。
func leafEffectiveValue(cmd *cobra.Command, flag LeafFlag) string {
v := leafRawValue(cmd, flag)
if flag.Trim {
@@ -255,18 +267,55 @@ func leafEffectiveValue(cmd *cobra.Command, flag LeafFlag) string {
return v
}
// leafRawValue 取未 trim 的原始有效值(主 flag → 别名 → 环境变量)。
// leafRawValue 取未 trim 的原始有效值。主 flag 仅在用户显式提供(Changed)
// 且非空时命中;注册默认值降级为链尾兜底,不再遮蔽别名与环境变量。
func leafRawValue(cmd *cobra.Command, flag LeafFlag) string {
if v := mustGetFlag(cmd, flag.Name); v != "" {
return v
if cmd.Flags().Changed(flag.Name) {
if v := leafFlagString(cmd, flag.Kind, flag.Name); v != "" {
return v
}
}
for _, alias := range flag.Aliases {
if v, _ := cmd.Flags().GetString(alias); v != "" {
if !cmd.Flags().Changed(alias) {
continue
}
if v := leafFlagString(cmd, flag.Kind, alias); v != "" {
return v
}
}
if flag.EnvVar != "" {
return os.Getenv(flag.EnvVar)
if v := os.Getenv(flag.EnvVar); v != "" {
return v
}
}
return ""
return flag.Default
}
// leafFlagString 按注册类型读取 flag 并统一为字符串形态,使整型 flag 能复用
// 同一条回退链(required 校验、别名、env)。
func leafFlagString(cmd *cobra.Command, kind LeafFlagKind, name string) string {
switch kind {
case LeafInt64:
v, _ := cmd.Flags().GetInt64(name)
return strconv.FormatInt(v, 10)
case LeafInt:
v, _ := cmd.Flags().GetInt(name)
return strconv.Itoa(v)
default:
return mustGetFlag(cmd, name)
}
}
// leafIntegerValue 按回退链取整型 flag 的有效值;环境变量提供的字符串必须
// 可解析,否则报错而非静默丢弃。
func leafIntegerValue(cmd *cobra.Command, flag LeafFlag) (int64, error) {
raw := leafEffectiveValue(cmd, flag)
if raw == "" {
return 0, nil
}
v, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return 0, fmt.Errorf("flag --%s: invalid integer value %q", flag.Name, raw)
}
return v, nil
}
+124
View File
@@ -116,3 +116,127 @@ func TestLeafValidateRequiredEnvDefaultHint(t *testing.T) {
t.Fatalf("leafValidateRequired() = %v, want default hint", err)
}
}
// ── CR 探针回归:A1/A2/A3 ──────────────────────────────────────────────
// TestLeafIntRequiredAcceptsExplicitValue(A1 探针):LeafInt + Required
// 传了值必须通过校验,不得报「missing required flag(s)」。
func TestLeafIntRequiredAcceptsExplicitValue(t *testing.T) {
spec := LeafSpec{
Use: "list", Tool: "list_thing",
Flags: []LeafFlag{{Name: "n", Usage: "数量", Kind: LeafInt, Required: true, Bind: "count"}},
}
cmd := NewLeafCommand(spec)
if err := cmd.Flags().Set("n", "5"); err != nil {
t.Fatal(err)
}
if err := leafValidateRequired(cmd, spec); err != nil {
t.Fatalf("leafValidateRequired() = %v, want nil for --n 5", err)
}
args, err := leafArgs(cmd, spec)
if err != nil {
t.Fatal(err)
}
if got := args["count"]; got != 5 {
t.Fatalf("count = %v (%T), want int 5", got, got)
}
// 未提供值时仍按主 flag 名报缺失。
missing := NewLeafCommand(spec)
if err := leafValidateRequired(missing, spec); err == nil || !strings.Contains(err.Error(), "missing required flag(s): --n") {
t.Fatalf("leafValidateRequired() = %v, want missing --n", err)
}
}
// TestLeafIntAliasAndEnvFallback(A2 探针):整型 flag 的别名与 env 回退
// 必须生效,不得静默丢值。
func TestLeafIntAliasAndEnvFallback(t *testing.T) {
spec := LeafSpec{
Use: "list", Tool: "list_thing",
Flags: []LeafFlag{{Name: "page-size", Usage: "分页大小", Kind: LeafInt, Aliases: []string{"limit"}, EnvVar: "DWS_LEAF_TEST_PAGE_SIZE", Bind: "pageSize"}},
}
// 别名提供值。
cmd := NewLeafCommand(spec)
if err := cmd.Flags().Set("limit", "7"); err != nil {
t.Fatal(err)
}
args, err := leafArgs(cmd, spec)
if err != nil {
t.Fatal(err)
}
if got := args["pageSize"]; got != 7 {
t.Fatalf("pageSize = %v (%T), want int 7 from alias", got, got)
}
// env 提供值。
t.Setenv("DWS_LEAF_TEST_PAGE_SIZE", "9")
cmd = NewLeafCommand(spec)
args, err = leafArgs(cmd, spec)
if err != nil {
t.Fatal(err)
}
if got := args["pageSize"]; got != 9 {
t.Fatalf("pageSize = %v (%T), want int 9 from env", got, got)
}
// env 值不可解析必须报错而非静默丢弃。
t.Setenv("DWS_LEAF_TEST_PAGE_SIZE", "not-a-number")
cmd = NewLeafCommand(spec)
if _, err := leafArgs(cmd, spec); err == nil || !strings.Contains(err.Error(), "invalid integer value") {
t.Fatalf("leafArgs() = %v, want parse error for garbage env", err)
}
}
// TestLeafInt64AliasRegisteredTyped:LeafInt64 别名按 Kind 注册且回退生效。
func TestLeafInt64AliasRegisteredTyped(t *testing.T) {
spec := LeafSpec{
Use: "list", Tool: "list_thing",
Flags: []LeafFlag{{Name: "cursor", Usage: "游标", Kind: LeafInt64, Aliases: []string{"offset"}, Bind: "cursor"}},
}
cmd := NewLeafCommand(spec)
if f := cmd.Flags().Lookup("offset"); f == nil || f.Value.Type() != "int64" {
t.Fatalf("alias offset = %+v, want registered as int64", f)
}
if err := cmd.Flags().Set("offset", "11"); err != nil {
t.Fatal(err)
}
args, err := leafArgs(cmd, spec)
if err != nil {
t.Fatal(err)
}
if got := args["cursor"]; got != int64(11) {
t.Fatalf("cursor = %v (%T), want int64 11 from alias", got, got)
}
}
// TestLeafDefaultDoesNotShadowFallback(A3 探针):注册默认值不得遮蔽
// 别名与环境变量,只能作为链尾兜底。
func TestLeafDefaultDoesNotShadowFallback(t *testing.T) {
spec := LeafSpec{
Use: "list", Tool: "list_thing",
Flags: []LeafFlag{{Name: "type", Usage: "类型", Default: "ALL", Aliases: []string{"kind"}, EnvVar: "DWS_LEAF_TEST_TYPE", Bind: "type"}},
}
// env 覆盖注册默认值。
t.Setenv("DWS_LEAF_TEST_TYPE", "from-env")
cmd := NewLeafCommand(spec)
if got := leafEffectiveValue(cmd, spec.Flags[0]); got != "from-env" {
t.Fatalf("effective = %q, want env to beat registered default", got)
}
// 别名覆盖 env 与默认值。
if err := cmd.Flags().Set("kind", "from-alias"); err != nil {
t.Fatal(err)
}
if got := leafEffectiveValue(cmd, spec.Flags[0]); got != "from-alias" {
t.Fatalf("effective = %q, want alias to beat env", got)
}
// 显式主 flag 最高优先。
if err := cmd.Flags().Set("type", "explicit"); err != nil {
t.Fatal(err)
}
if got := leafEffectiveValue(cmd, spec.Flags[0]); got != "explicit" {
t.Fatalf("effective = %q, want explicit primary to win", got)
}
// 全部缺席时回落注册默认值。
bare := NewLeafCommand(spec)
t.Setenv("DWS_LEAF_TEST_TYPE", "")
if got := leafEffectiveValue(bare, spec.Flags[0]); got != "ALL" {
t.Fatalf("effective = %q, want registered default as tail fallback", got)
}
}