Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion gflag/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,11 @@ func (p *Parser) Parse(args []string) (err error) {

// POSIX 短选项规范化预处理(EnhanceShort>0 时生效,默认 0 不改变行为)
if p.cfg.EnhanceShort > 0 {
args = expandShortArgs(args, p.fSet.shorts, p.fSet.isBoolShort, p.cfg.EnhanceShort)
stopAt := p.reorderStop
if p.cfg.DisableReorderArgs {
stopAt = func(string) bool { return true }
}
args = expandShortArgs(args, p.fSet, p.cfg.EnhanceShort, stopAt)
}

// 自动重排 args 为标准 "options... arguments" 形态(默认开启)。
Expand Down
75 changes: 48 additions & 27 deletions gflag/shorts.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,50 +24,71 @@ func (f *FlagSet) isBoolShort(short string) bool {
//
// 不满足条件的 token 原样保留(不做猜测,避免误伤混合写法如 -aO)。
//
// - shorts: short=>fullName 映射,判断字符是否为已注册短选项
// - isBool: 判断给定单字符短选项是否为 bool 类型
// Tokens consumed as option values and tokens after -- or stopAt stay verbatim.
//
// - fs: 注册的选项及 short=>fullName 映射
// - level: EnhanceShort 等级(0 直接返回原 args)
func expandShortArgs(args []string, shorts map[string]string, isBool func(short string) bool, level uint8) []string {
func expandShortArgs(args []string, fs *FlagSet, level uint8, stopAt func(string) bool) []string {
if level == 0 || len(args) == 0 {
return args
}

out := make([]string, 0, len(args))
for _, arg := range args {
// 仅处理单 '-' 开头、长度>2、不含 '='
if len(arg) <= 2 || arg[0] != '-' || arg[1] == '-' || strings.IndexByte(arg, '=') >= 0 {
out = append(out, arg)
continue
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" {
out = append(out, args[i:]...)
break
}

body := arg[1:] // 去掉前导 '-'。eg: "aux"
// 仅处理单 '-' 开头、长度>2、不含 '='
if len(arg) > 2 && arg[0] == '-' && arg[1] != '-' && strings.IndexByte(arg, '=') < 0 {
body := arg[1:] // 去掉前导 '-'。eg: "aux"

// level≥1: 全为 bool 短选项 => 逐个拆成 -x
allBool := true
for i := 0; i < len(body); i++ {
if !isBool(string(body[i])) {
allBool = false
break
// level≥1: 全为 bool 短选项 => 逐个拆成 -x
allBool := true
for j := 0; j < len(body); j++ {
if !fs.isBoolShort(string(body[j])) {
allBool = false
break
}
}
}
if allBool {
for i := 0; i < len(body); i++ {
out = append(out, "-"+string(body[i]))
if allBool {
for j := 0; j < len(body); j++ {
out = append(out, "-"+string(body[j]))
}
continue
}
continue
}

// level≥2: 首字符是取值型已注册短选项 => -X + 余下作为值
if level >= 2 {
first := string(body[0])
if _, ok := shorts[first]; ok && !isBool(first) {
out = append(out, "-"+first, body[1:])
continue
// level≥2: 首字符是取值型已注册短选项 => -X + 余下作为值
if level >= 2 {
first := string(body[0])
if _, ok := fs.shorts[first]; ok && !fs.isBoolShort(first) {
out = append(out, "-"+first, body[1:])
continue
}
}
}

// Recognize registered short groups before classifying numeric-looking
// tokens as positional arguments (for example, the bool aliases -1 -2).
if stopAt != nil && !looksLikeOption(arg) && stopAt(arg) {
out = append(out, args[i:]...)
break
}

// 不满足 => 原样保留(不猜测)
out = append(out, arg)

// A known value-taking option consumes its next token, even if that
// token resembles a short group or the -- terminator.
if len(arg) > 1 && arg[0] == '-' && strings.IndexByte(arg, '=') < 0 {
name := strings.TrimPrefix(arg[1:], "-")
if known, isBool := fs.optMeta(name); known && !isBool && i+1 < len(args) {
i++
out = append(out, args[i])
}
}
}
return out
}
167 changes: 167 additions & 0 deletions gflag/shorts_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package gflag_test

import (
"fmt"
"slices"
"testing"

"github.com/gookit/gcli/v3/gflag"
"github.com/gookit/goutil/x/assert"
)

func newContextParser(level uint8) (*gflag.Parser, *bool, *bool, *string) {
p := gflag.New("context")
p.ParserCfg().EnhanceShort = level
var alpha, beta bool
var name string
p.BoolOpt(&alpha, "alpha", "a", false, "alpha")
p.BoolOpt(&beta, "beta", "b", false, "beta")
p.StrOpt(&name, "name", "n", "", "name")
return p, &alpha, &beta, &name
}

func TestParser_EnhanceShortPreservesArgumentContext(t *testing.T) {
tests := []struct {
name string
args []string
wantName string
wantArgs []string
wantBool bool
}{
{"terminator bool data", []string{"--", "-ab"}, "", []string{"-ab"}, false},
{"terminator attached data", []string{"--", "-nfoo"}, "", []string{"-nfoo"}, false},
{"long option bool data", []string{"--name", "-ab"}, "-ab", nil, false},
{"short option bool data", []string{"-n", "-ab"}, "-ab", nil, false},
{"long option attached data", []string{"--name", "-nfoo"}, "-nfoo", nil, false},
{"short option attached data", []string{"-n", "-nfoo"}, "-nfoo", nil, false},
{"terminator as value", []string{"--name", "--", "-ab"}, "--", nil, true},
{"equals value", []string{"--name=-ab"}, "-ab", nil, false},
{"group after value", []string{"--name", "Ada", "-ab"}, "Ada", nil, true},
{"group after equals", []string{"--name=-ab", "-ab"}, "-ab", nil, true},
}
for _, level := range []uint8{1, 2} {
for _, reorder := range []bool{false, true} {
for _, tt := range tests {
t.Run(fmt.Sprintf("level%d/reorder%t/%s", level, reorder, tt.name), func(t *testing.T) {
p, alpha, beta, name := newContextParser(level)
p.WithConfigFn(gflag.WithReorderArgs(reorder))
assert.NoErr(t, p.Parse(tt.args))
assert.Eq(t, tt.wantName, *name)
assert.Eq(t, tt.wantBool, *alpha)
assert.Eq(t, tt.wantBool, *beta)
if !slices.Equal(tt.wantArgs, p.RawArgs()) {
t.Errorf("positional arguments: want %q, got %q", tt.wantArgs, p.RawArgs())
}
})
}
}
}
}

func TestParser_EnhanceShortStopsAtCommandContext(t *testing.T) {
for _, level := range []uint8{1, 2} {
for _, subcommand := range []bool{false, true} {
t.Run(fmt.Sprintf("level%d/subcommand%t", level, subcommand), func(t *testing.T) {
p, alpha, beta, name := newContextParser(level)
if subcommand {
p.SetReorderStop(func(token string) bool { return token == "serve" })
} else {
p.WithConfigFn(gflag.WithReorderArgs(false))
}
args := []string{"serve", "--name", "-ab", "-nfoo"}
assert.NoErr(t, p.Parse(args))
assert.Eq(t, "", *name)
assert.False(t, *alpha)
assert.False(t, *beta)
assert.Eq(t, args, p.RawArgs())
})
}
}
}

func TestParser_EnhanceShortKeepsUnknownGroupErrors(t *testing.T) {
for _, level := range []uint8{1, 2} {
for _, arg := range []string{"-az", "-an"} {
t.Run(fmt.Sprintf("level%d/%s", level, arg), func(t *testing.T) {
p, alpha, beta, name := newContextParser(level)
assert.Err(t, p.Parse([]string{arg}))
assert.False(t, *alpha)
assert.False(t, *beta)
assert.Eq(t, "", *name)
})
}
}
}

func TestParser_EnhanceShortKeepsNumericAliases(t *testing.T) {
for _, level := range []uint8{1, 2} {
for _, reorder := range []bool{false, true} {
for _, value := range []bool{false, true} {
t.Run(fmt.Sprintf("level%d/reorder%t/value%t", level, reorder, value), func(t *testing.T) {
p, alpha, beta, name := newContextParser(level)
p.WithConfigFn(gflag.WithReorderArgs(reorder))
var one, two bool
p.BoolOpt(&one, "one", "1", false, "one")
p.BoolOpt(&two, "two", "2", false, "two")
args := []string{"-12", "-ab"}
if value {
args = []string{"--name", "-12", "-ab"}
}
assert.NoErr(t, p.Parse(args))
assert.Eq(t, !value, one)
assert.Eq(t, !value, two)
assert.True(t, *alpha)
assert.True(t, *beta)
if value {
assert.Eq(t, "-12", *name)
} else {
assert.Eq(t, "", *name)
}
})
}
}
}

for _, reorder := range []bool{false, true} {
t.Run(fmt.Sprintf("attached/reorder%t", reorder), func(t *testing.T) {
p := gflag.New("context")
p.ParserCfg().EnhanceShort = 2
p.WithConfigFn(gflag.WithReorderArgs(reorder))
var name string
p.StrOpt(&name, "name", "1", "", "name")
assert.NoErr(t, p.Parse([]string{"-1Ada"}))
assert.Eq(t, "Ada", name)
})
}
}

func TestParser_EnhanceShortPreservesNumericTails(t *testing.T) {
for _, level := range []uint8{1, 2} {
for _, boundary := range []string{"terminator", "positional", "subcommand"} {
t.Run(fmt.Sprintf("level%d/%s", level, boundary), func(t *testing.T) {
p, alpha, beta, name := newContextParser(level)
var one, two bool
p.BoolOpt(&one, "one", "1", false, "one")
p.BoolOpt(&two, "two", "2", false, "two")
args := []string{"--", "-12", "-1.5", "-.5", "-ab"}
wantArgs := args[1:]
if boundary != "terminator" {
args[0] = "serve"
wantArgs = args
if boundary == "positional" {
p.WithConfigFn(gflag.WithReorderArgs(false))
} else {
p.SetReorderStop(func(token string) bool { return token == "serve" })
}
}
assert.NoErr(t, p.Parse(args))
assert.False(t, one)
assert.False(t, two)
assert.False(t, *alpha)
assert.False(t, *beta)
assert.Eq(t, "", *name)
assert.Eq(t, wantArgs, p.RawArgs())
})
}
}
}
14 changes: 9 additions & 5 deletions gflag/shorts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import (
)

func TestExpandShortArgs(t *testing.T) {
shorts := map[string]string{"a": "aaa", "u": "uuu", "x": "xxx", "O": "out", "b": "bbb"}
isBool := func(s string) bool {
return map[string]bool{"a": true, "u": true, "x": true}[s]
}
p := New("test")
var a, u, x bool
var out, b string
p.BoolOpt(&a, "aaa", "a", false, "bool a")
p.BoolOpt(&u, "uuu", "u", false, "bool u")
p.BoolOpt(&x, "xxx", "x", false, "bool x")
p.StrOpt(&out, "out", "O", "", "string out")
p.StrOpt(&b, "bbb", "b", "", "string b")

tests := []struct {
name string
Expand Down Expand Up @@ -38,7 +42,7 @@ func TestExpandShortArgs(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := expandShortArgs(tt.in, shorts, isBool, tt.level)
got := expandShortArgs(tt.in, p.fSet, tt.level, nil)
assert.Eq(t, tt.want, got)
})
}
Expand Down
Loading