Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(flag): refactor separateArgs function #197

Merged
merged 2 commits into from
Jul 25, 2024
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
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/savioxavier/termlink v1.3.0
github.com/shirou/gopsutil/v3 v3.24.5
github.com/spf13/afero v1.11.0
github.com/stretchr/testify v1.9.0
github.com/syndtr/goleveldb v1.0.0
github.com/urfave/cli/v2 v2.27.2
github.com/valyala/bytebufferpool v1.0.0
Expand All @@ -31,6 +32,7 @@ require (
github.com/zeebo/xxh3 v1.0.2
go.szostok.io/version v1.2.0
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc
golang.org/x/sys v0.20.0
gopkg.in/yaml.v3 v3.0.1
)

Expand Down Expand Up @@ -65,6 +67,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
Expand All @@ -77,6 +80,7 @@ require (
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
Expand All @@ -85,6 +89,5 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)
89 changes: 89 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"runtime/debug"
"slices"
"strings"

"github.com/Equationzhao/g/internal/cli"
"github.com/Equationzhao/g/internal/config"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/Equationzhao/g/internal/global/doc"
"github.com/Equationzhao/g/internal/util"
"github.com/Equationzhao/g/man"
ucli "github.com/urfave/cli/v2"
)

func main() {
Expand Down Expand Up @@ -52,6 +54,7 @@ func catchPanic(err any) {
}

func preprocessArgs() {
rearrangeArgs()
// normal logic
// load config if the args do not contains -no-config
if !slices.ContainsFunc(os.Args, hasNoConfig) {
Expand All @@ -73,6 +76,92 @@ func preprocessArgs() {
}
}

func rearrangeArgs() {
if len(os.Args) <= 2 {
return
}
flags, paths := separateArgs(os.Args[1:])
newArgs := append([]string{os.Args[0]}, append(flags, paths...)...)
os.Args = newArgs
fmt.Println(os.Args)
}

func separateArgs(args []string) (flags []string, paths []string) {
flagsWithArgs := buildFlagsWithArgsMap()
expectValue, hasDoubleDash := false, false
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" {
hasDoubleDash = true
if i+1 < len(args) {
paths = append(paths, args[i+1])
i++
}
continue
}
if strings.HasPrefix(arg, "--") {
i = handleLongFlag(arg, args, i, &flags, &expectValue, flagsWithArgs)
} else if strings.HasPrefix(arg, "-") {
i = handleShortFlag(arg, args, i, &flags, &expectValue, flagsWithArgs)
} else {
if expectValue {
flags = append(flags, arg)
expectValue = false
} else {
paths = append(paths, arg)
}
}
}
if hasDoubleDash {
flags = append(flags, "--")
}
return flags, paths
}

func buildFlagsWithArgsMap() map[string]bool {
flagsWithArgs := make(map[string]bool)
for _, flag := range cli.G.Flags {
switch flag.(type) {
case *ucli.BoolFlag:
for _, s := range flag.Names() {
flagsWithArgs[s] = false
}
default:
for _, s := range flag.Names() {
flagsWithArgs[s] = true
}
}
}
return flagsWithArgs
}

func handleLongFlag(arg string, args []string, i int, flags *[]string, expectValue *bool, flagsWithArgs map[string]bool) int {
parts := strings.SplitN(arg, "=", 2)
flagName := strings.TrimPrefix(parts[0], "--")
*flags = append(*flags, arg)

if len(parts) == 2 || !flagsWithArgs[flagName] {
return i
}

if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
*expectValue = true
}
return i
}

func handleShortFlag(arg string, args []string, i int, flags *[]string, expectValue *bool, flagsWithArgs map[string]bool) int {
parts := strings.SplitN(arg, "=", 2)
flagName := strings.TrimPrefix(parts[0], "-")
*flags = append(*flags, arg)
if len(parts) == 2 || !flagsWithArgs[flagName] {
return i
}
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
*expectValue = true
}
return i
}
func hasNoConfig(s string) bool {
if s == "-no-config" || s == "--no-config" {
return true
Expand Down
214 changes: 212 additions & 2 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package main
import (
"io/fs"
"os"
"reflect"
"testing"

"github.com/Equationzhao/g/internal/cli"
"github.com/Equationzhao/g/internal/config"
"github.com/agiledragon/gomonkey/v2"
"github.com/zeebo/assert"
"github.com/stretchr/testify/assert"
ucli "github.com/urfave/cli/v2"
)

func Test_catchPanic(t *testing.T) {
Expand Down Expand Up @@ -57,7 +60,7 @@ func Test_preprocessArgs(t *testing.T) {
os.Args = []string{"g", "--args1"}
preprocessArgs() // this will add args from config
assert.Equal(t, 4, len(os.Args))
assert.DeepEqual(t, os.Args, []string{"g", "--args2", "--args3", "--args1"})
assert.Equal(t, os.Args, []string{"g", "--args2", "--args3", "--args1"})

os.Args = []string{"g", "--args1", "-no-config"}
preprocessArgs()
Expand All @@ -68,3 +71,210 @@ func Test_preprocessArgs(t *testing.T) {
preprocessArgs()
assert.Equal(t, 2, len(os.Args))
}

func TestSeparateArgs(t *testing.T) {
originalFlags := cli.G.Flags
defer func() { cli.G.Flags = originalFlags }()
cli.G.Flags = []ucli.Flag{
&ucli.BoolFlag{Name: "all", Aliases: []string{"a"}},
&ucli.StringFlag{Name: "sort", Aliases: []string{"s"}},
&ucli.IntFlag{Name: "term-width"},
}

tests := []struct {
name string
args []string
expectedFlags []string
expectedPaths []string
}{
{
name: "Basic case",
args: []string{"--all", "dir1", "dir2"},
expectedFlags: []string{"--all"},
expectedPaths: []string{"dir1", "dir2"},
},
{
name: "Flag with value",
args: []string{"--sort", "name", "dir1"},
expectedFlags: []string{"--sort", "name"},
expectedPaths: []string{"dir1"},
},
{
name: "Flag with equals",
args: []string{"--sort=name", "dir1"},
expectedFlags: []string{"--sort=name"},
expectedPaths: []string{"dir1"},
},
{
name: "Mixed flags and paths",
args: []string{"--all", "dir1", "--sort", "name", "dir2"},
expectedFlags: []string{"--all", "--sort", "name"},
expectedPaths: []string{"dir1", "dir2"},
},
{
name: "With double dash",
args: []string{"--all", "dir1", "--", "--sort", "name"},
expectedFlags: []string{"--all", "--"},
expectedPaths: []string{"dir1", "--sort", "name"},
},
{
name: "Short flags",
args: []string{"-a", "-s", "name", "dir1"},
expectedFlags: []string{"-a", "-s", "name"},
expectedPaths: []string{"dir1"},
},
{
name: "Complex case",
args: []string{"--all", "dir1", "--term-width", "100", "-s", "name", "--", "--fake-flag", "dir2"},
expectedFlags: []string{"--all", "--term-width", "100", "-s", "name", "--"},
expectedPaths: []string{"dir1", "--fake-flag", "dir2"},
},
{
name: "Complex case with double dash",
args: []string{"--all", "dir1", "--term-width", "100", "-s", "name", "--", "-a", "-a", "-l", "dir2", "--", "--fake-flag"},
expectedFlags: []string{"--all", "--term-width", "100", "-s", "name", "-a", "-l", "--"},
expectedPaths: []string{"dir1", "-a", "dir2", "--fake-flag"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
flags, paths := separateArgs(tt.args)
if !reflect.DeepEqual(flags, tt.expectedFlags) {
t.Errorf("flags = %v, want %v", flags, tt.expectedFlags)
}
if !reflect.DeepEqual(paths, tt.expectedPaths) {
t.Errorf("paths = %v, want %v", paths, tt.expectedPaths)
}
})
}
}

func TestBuildFlagsWithArgsMap(t *testing.T) {
originalFlags := cli.G.Flags
defer func() { cli.G.Flags = originalFlags }()
cli.G.Flags = []ucli.Flag{
&ucli.BoolFlag{Name: "all", Aliases: []string{"a"}},
&ucli.StringFlag{Name: "sort", Aliases: []string{"s"}},
&ucli.IntFlag{Name: "term-width"},
}

expected := map[string]bool{
"all": false,
"a": false,
"sort": true,
"s": true,
"term-width": true,
}

result := buildFlagsWithArgsMap()
assert.Equal(t, result, expected, "buildFlagsWithArgsMap() = %v, want %v", result, expected)
}

func TestHandleLongFlag(t *testing.T) {
tests := []struct {
name string
arg string
args []string
i int
flagsWithArgs map[string]bool
expectedFlags []string
expectedExpectValue bool
expectedI int
}{
{
name: "Flag with equals",
arg: "--sort=name",
args: []string{"--sort=name", "dir1"},
i: 0,
flagsWithArgs: map[string]bool{"sort": true},
expectedFlags: []string{"--sort=name"},
expectedExpectValue: false,
expectedI: 0,
},
{
name: "Flag without value",
arg: "--all",
args: []string{"--all", "dir1"},
i: 0,
flagsWithArgs: map[string]bool{"all": false},
expectedFlags: []string{"--all"},
expectedExpectValue: false,
expectedI: 0,
},
{
name: "Flag expecting value",
arg: "--sort",
args: []string{"--sort", "name", "dir1"},
i: 0,
flagsWithArgs: map[string]bool{"sort": true},
expectedFlags: []string{"--sort"},
expectedExpectValue: true,
expectedI: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
flags := []string{}
expectValue := false
resultI := handleLongFlag(tt.arg, tt.args, tt.i, &flags, &expectValue, tt.flagsWithArgs)

assert.Equal(t, flags, tt.expectedFlags, "flags = %v, want %v", flags, tt.expectedFlags)
assert.Equal(t, resultI, tt.expectedI, "resultI = %v, expectedI %v", expectValue, tt.expectedExpectValue)
assert.Equal(t, expectValue, tt.expectedExpectValue, "expectValue = %v, want %v", expectValue, tt.expectedExpectValue)
})
}
}

func TestHandleShortFlag(t *testing.T) {
tests := []struct {
name string
arg string
args []string
i int
flagsWithArgs map[string]bool
expectedFlags []string
expectedExpectValue bool
expectedI int
}{
{
name: "Short flag without value",
arg: "-a",
args: []string{"-a", "dir1"},
i: 0,
flagsWithArgs: map[string]bool{"a": false},
expectedFlags: []string{"-a"},
expectedExpectValue: false,
expectedI: 0,
},
{
name: "Short flag expecting value",
arg: "-s",
args: []string{"-s", "name", "dir1"},
i: 0,
flagsWithArgs: map[string]bool{"s": true},
expectedFlags: []string{"-s"},
expectedExpectValue: true,
expectedI: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
flags := []string{}
expectValue := false
resultI := handleShortFlag(tt.arg, tt.args, tt.i, &flags, &expectValue, tt.flagsWithArgs)
assert.Equal(t, flags, tt.expectedFlags, "flags = %v, want %v", flags, tt.expectedFlags)
assert.Equal(t, resultI, tt.expectedI, "resultI = %v, expectedI %v", expectValue, tt.expectedExpectValue)
assert.Equal(t, expectValue, tt.expectedExpectValue, "expectValue = %v, want %v", expectValue, tt.expectedExpectValue)
})
}
}

func Test_main(t *testing.T) {
patch := gomonkey.ApplyFunc(os.Exit, func(int) {})
defer patch.Reset()
os.Args = []string{"g", "."}
main()
}
Loading