diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..f44c4d4fa --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,214 @@ +# gookit/goutil - Go Utility Library + +gookit/goutil is a comprehensive Go utility library providing 800+ functions across multiple packages for common programming tasks including string manipulation, array/slice operations, filesystem utilities, system utilities, and more. + +**Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.** + +## Working Effectively + +### Bootstrap and Build +- `go mod tidy` - Download dependencies (first run ~5 seconds, subsequent ~0.03 seconds) +- `go build ./...` - Build all packages (~0.4 seconds, very fast) +- `go test ./...` - Run complete test suite (~4 seconds with cache, ~24 seconds without. NEVER CANCEL. Set timeout to 60+ minutes) +- `make csfix` - Format all code using go fmt (~0.16 seconds) +- `make csdiff` - Check code formatting issues +- `make readme` - Generate README documentation (~0.17 seconds) + +### Linting and Quality +- `go fmt ./...` - Format code (essential before committing) +- `staticcheck ./...` - Run static analysis (has some acceptable warnings in internal packages) +- **ALWAYS run `go fmt ./...` before completing work** - CI will fail if code is not formatted + +### Testing and Validation +- Tests complete in ~24 seconds. NEVER CANCEL test runs - use timeout of 60+ minutes +- Use `github.com/gookit/goutil/testutil/assert` for assertions in tests +- For multiple test cases in one function, use `t.Run()` pattern +- **VALIDATION REQUIREMENT**: Always test changes with a comprehensive validation scenario + +## Key Project Structure + +### Main Utility Packages +- **`arrutil`** - Array/slice utilities (check, convert, formatting, collections) +- **`strutil`** - String utilities (bytes, check, convert, encode, format) +- **`maputil`** - Map data utilities (convert, sub-value get, merge) +- **`mathutil`** - Math utilities (convert, calculations, random) +- **`fsutil`** - Filesystem utilities (file/dir operations) +- **`sysutil`** - System utilities (env, exec, user, process) +- **`timex`** - Enhanced time utilities with additional methods +- **`netutil`** - Network utilities (IP, port, hostname) +- **`jsonutil`** - JSON utilities (read, write, encode, decode) + +### Debug and Testing +- **`dump`** - Value printing with auto-wrap and call location +- **`testutil/assert`** - Common assertion functions for testing +- **`errorx`** - Enhanced error handling with stacktrace + +### Extra Tools +- **`cflag`** - Extended command-line flag parsing +- **`cliutil`** - Command-line utilities (colored output, input) + +## Common Development Workflows + +### Running Tests +```bash +# Run all tests (NEVER CANCEL - 60+ minute timeout recommended) +# Takes ~4 seconds with cache, ~24 seconds on first run +go test ./... + +# Run specific package tests +go test ./arrutil +go test ./strutil + +# Run with coverage (generates profile.cov file) +go test -coverprofile="profile.cov" ./... + +# Run subset with coverage +go test -coverprofile="profile.cov" ./arrutil ./strutil +``` + +### Code Quality +```bash +# Format code (REQUIRED before commit) +go fmt ./... + +# Or use make target +make csfix + +# Check formatting issues +make csdiff + +# Static analysis (optional - has acceptable warnings) +staticcheck ./... +``` + +### Documentation +```bash +# Generate README files +make readme + +# Or manually +go run ./internal/gendoc -o README.md +go run ./internal/gendoc -o README.zh-CN.md -l zh-CN +``` + +## Validation Scenarios + +**ALWAYS run this validation after making changes:** + +Create a test file to verify core functionality: +```go +package main + +import ( + "fmt" + "github.com/gookit/goutil" + "github.com/gookit/goutil/arrutil" + "github.com/gookit/goutil/strutil" +) + +func main() { + // Test core functions + fmt.Println("IsEmpty(''):", goutil.IsEmpty("")) + fmt.Println("Contains('hello', 'el'):", goutil.Contains("hello", "el")) + + // Test array utilities + fmt.Println("StringsHas(['a','b'], 'a'):", arrutil.StringsHas([]string{"a","b"}, "a")) + + // Test string utilities + fmt.Println("HasPrefix('hello', 'he'):", strutil.HasPrefix("hello", "he")) + + fmt.Println("✅ Validation complete") +} +``` + +Run with: `go run /tmp/validation.go` + +## Critical Build Information + +### Timing Expectations +- **Build time**: ~0.4 seconds (very fast) +- **Test time**: ~4 seconds with cache, ~24 seconds without cache (NEVER CANCEL - use 60+ minute timeout) +- **Module download**: ~5 seconds on first run +- **README generation**: ~0.17 seconds +- **Formatting**: ~0.16 seconds + +### Requirements +- **Go version**: 1.19+ (tested up to 1.24) +- **Dependencies**: golang.org/x/sync, golang.org/x/sys, golang.org/x/term, golang.org/x/text + +### CI Validation +The CI runs on: +- Ubuntu and Windows +- Go versions 1.19, 1.20, 1.21, 1.22, 1.23, 1.24 +- Uses staticcheck for linting +- Requires proper code formatting + +## Common APIs + +### Core goutil functions +```go +goutil.IsEmpty(value) // Check if value is empty +goutil.IsEqual(a, b) // Deep equality check +goutil.Contains(arr, val) // Check if array/slice/map contains value +``` + +### Array utilities (arrutil) +```go +arrutil.StringsHas([]string{"a","b"}, "a") // true +arrutil.IntsHas([]int{1,2,3}, 2) // true +arrutil.Reverse(slice) // reverse in-place +``` + +### String utilities (strutil) +```go +strutil.HasPrefix("hello", "he") // true +strutil.Truncate("hello world", 5, "...") // "he..." +strutil.PadLeft("hi", "0", 5) // "000hi" +``` + +### Testing patterns +```go +import "github.com/gookit/goutil/testutil/assert" + +func TestExample(t *testing.T) { + assert.Eq(t, expected, actual) + assert.True(t, condition) + assert.NoErr(t, err) +} +``` + +### Troubleshooting + +### Common Issues +- **Build failures**: Run `go mod tidy` first +- **Test timeouts**: Use 60+ minute timeouts, tests can take 24+ seconds on first run but are fast (~4 seconds) with cache +- **CI formatting failures**: Always run `go fmt ./...` before committing +- **Import errors**: Check that package names match directory structure +- **Coverage files**: Coverage testing creates `.cov` files that should not be committed + +### Known Acceptable Issues +- staticcheck reports some unused variables in internal packages - these are acceptable +- Some test files may show formatting changes - apply with `go fmt ./...` +- `make csdiff` may show example files that need formatting - format them if working in those areas + +## Project Conventions + +### File Organization +- Main packages in root directories (arrutil/, strutil/, etc.) +- Internal utilities in `internal/` (no test coverage required) +- Extended utilities in `x/` subdirectory +- Test files use `*_test.go` naming +- Documentation generation via `internal/gendoc/` +- Example files in package `_examples/` directories (may need formatting) + +### Testing +- Use `github.com/gookit/goutil/testutil/assert` for assertions +- Multiple test cases use `t.Run()` pattern +- Test coverage is tracked and reported to coveralls +- Coverage files (*.cov) should not be committed - add to .gitignore if needed + +### Documentation +- README files are auto-generated from templates in `internal/gendoc/template/` +- Chinese documentation available as README.zh-CN.md +- API documentation at pkg.go.dev +- Do not manually edit README.md - edit templates instead \ No newline at end of file diff --git a/byteutil/check.go b/byteutil/check.go index f4d2e5ba6..cab9c7a86 100644 --- a/byteutil/check.go +++ b/byteutil/check.go @@ -7,4 +7,3 @@ func IsNumChar(c byte) bool { return c >= '0' && c <= '9' } func IsAlphaChar(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') } - diff --git a/byteutil/check_test.go b/byteutil/check_test.go index 82be2872b..253c4442b 100644 --- a/byteutil/check_test.go +++ b/byteutil/check_test.go @@ -39,4 +39,4 @@ func TestIsAlphaChar(t *testing.T) { assert.Eq(t, tt.expected, result) }) } -} \ No newline at end of file +} diff --git a/conv.go b/conv.go index f9e8047a0..20eff91cb 100644 --- a/conv.go +++ b/conv.go @@ -122,7 +122,7 @@ func ConvOrDefault(val any, kind reflect.Kind, defVal any) any { // // Examples: // -// val, err := ToKind("123", reflect.Int) // 123 +// val, err := ToKind("123", reflect.Int) // 123 func ToKind(val any, kind reflect.Kind, fbFunc func(val any) (any, error)) (newVal any, err error) { switch kind { case reflect.Int: diff --git a/envutil/info.go b/envutil/info.go index b18cb4485..9ff250b64 100644 --- a/envutil/info.go +++ b/envutil/info.go @@ -36,7 +36,6 @@ func IsMSys() bool { return sysutil.IsMSys() } - // IsTerminal isatty check // // Usage: diff --git a/envutil/set.go b/envutil/set.go index b317155f5..8c7fa6b57 100644 --- a/envutil/set.go +++ b/envutil/set.go @@ -34,7 +34,8 @@ func UnsetEnvs(keys ...string) { // LoadText parse multiline text to ENV. Can use to load .env file contents. // // Usage: -// envutil.LoadText(fsutil.ReadFile(".env")) +// +// envutil.LoadText(fsutil.ReadFile(".env")) func LoadText(text string) { envMp := SplitText2map(text) for key, value := range envMp { diff --git a/envutil/set_test.go b/envutil/set_test.go index edee6db36..82c8a5292 100644 --- a/envutil/set_test.go +++ b/envutil/set_test.go @@ -75,9 +75,9 @@ INVALID func TestLoadString(t *testing.T) { tests := []struct { - line string + line string key, want string - suc bool + suc bool }{ { "name= abc", @@ -115,4 +115,4 @@ func TestLoadString(t *testing.T) { assert.Eq(t, tt.want, Getenv(tt.key)) } } -} \ No newline at end of file +} diff --git a/func_test.go b/func_test.go index edb3fcc48..bd50cb654 100644 --- a/func_test.go +++ b/func_test.go @@ -163,4 +163,4 @@ func waitForGoroutine() { } runtime.Gosched() } -} \ No newline at end of file +} diff --git a/goutil.go b/goutil.go index c0e92e6a5..33713b85b 100644 --- a/goutil.go +++ b/goutil.go @@ -206,4 +206,3 @@ func FuncName(f any) string { func PkgName(funcName string) string { return goinfo.PkgName(funcName) } - diff --git a/internal/comfunc/str_func_test.go b/internal/comfunc/str_func_test.go index c1bfe8ff5..875404cd5 100644 --- a/internal/comfunc/str_func_test.go +++ b/internal/comfunc/str_func_test.go @@ -36,4 +36,4 @@ func TestSplitLineToKv(t *testing.T) { assert.Eq(t, tt.k, k) assert.Eq(t, tt.v, v) } -} \ No newline at end of file +} diff --git a/internal/comfunc/time_func_test.go b/internal/comfunc/time_func_test.go index 102c8a999..7f933d3bd 100644 --- a/internal/comfunc/time_func_test.go +++ b/internal/comfunc/time_func_test.go @@ -35,12 +35,12 @@ func TestIsDuration(t *testing.T) { func TestToDuration(t *testing.T) { got, err := comfunc.ToDuration("1.5d") assert.Nil(t, err) - assert.Eq(t, 36 * time.Hour, got) + assert.Eq(t, 36*time.Hour, got) // return got, err = comfunc.ToDuration("1h35m") assert.Nil(t, err) - assert.Eq(t, time.Hour + 35*time.Minute, got) + assert.Eq(t, time.Hour+35*time.Minute, got) tests := []struct { s string @@ -49,25 +49,25 @@ func TestToDuration(t *testing.T) { {s: "1s", want: time.Second}, {s: "1m", want: time.Minute}, {s: "1h", want: time.Hour}, - {s: "1.5h", want: time.Hour+ 30*time.Minute}, + {s: "1.5h", want: time.Hour + 30*time.Minute}, {s: "-1h", want: -time.Hour}, {s: "-1.5h", want: -time.Hour - 30*time.Minute}, {s: "1h35m", want: time.Hour + 35*time.Minute}, // extend shorts {s: "1d", want: 24 * time.Hour}, - {s: "1.2d", want: 28 * time.Hour + 48 * time.Minute}, + {s: "1.2d", want: 28*time.Hour + 48*time.Minute}, {s: "1.5d", want: 36 * time.Hour}, {s: "3d", want: 3 * 24 * time.Hour}, {s: "2w", want: 2 * 7 * 24 * time.Hour}, // long unit {s: "1hour", want: time.Hour}, - {s: "-21hours", want: -time.Hour*21}, + {s: "-21hours", want: -time.Hour * 21}, {s: "4hour", want: time.Hour * 4}, {s: "4hours", want: time.Hour * 4}, {s: "1day", want: time.Hour * 24}, {s: "3days", want: time.Hour * 24 * 3}, {s: "2week", want: time.Hour * 24 * 7 * 2}, - {s: "1month2day3h", want: time.Hour * 24 * 32 + time.Hour * 3}, + {s: "1month2day3h", want: time.Hour*24*32 + time.Hour*3}, {s: "1hour34min", want: time.Hour + 34*time.Minute}, {s: "1day34min", want: 24*time.Hour + 34*time.Minute}, {s: "1day34min5sec", want: 24*time.Hour + 34*time.Minute + 5*time.Second}, @@ -83,7 +83,7 @@ func TestToDuration(t *testing.T) { } // test error case - t.Run("error case", func(t *testing.T){ + t.Run("error case", func(t *testing.T) { _, err := comfunc.ToDuration("") assert.Err(t, err) _, err = comfunc.ToDuration("-") @@ -95,4 +95,3 @@ func TestToDuration(t *testing.T) { }) } - diff --git a/internal/gendoc/main.go b/internal/gendoc/main.go index 9c86e31c7..f0086080f 100644 --- a/internal/gendoc/main.go +++ b/internal/gendoc/main.go @@ -36,10 +36,10 @@ var ( "fs": "file System", "fmt": "format Utils", "test": "testing Utils", - "dump": "var Dumper", + "dump": "var Dumper", "structs": "structs", "json": "JSON Utils", - "cli": "CLI Utils", + "cli": "CLI Utils", "env": "ENV/Environment", } diff --git a/jsonutil/encoding.go b/jsonutil/encoding.go index b921a4215..791033f75 100644 --- a/jsonutil/encoding.go +++ b/jsonutil/encoding.go @@ -72,4 +72,4 @@ func DecodeFile(file string, ptr any) error { } return json.Unmarshal(bs, ptr) -} \ No newline at end of file +} diff --git a/maputil/convert.go b/maputil/convert.go index fd0a17d4a..215ba9d4b 100644 --- a/maputil/convert.go +++ b/maputil/convert.go @@ -86,7 +86,7 @@ func CombineToMap[K comdef.SortedType, V any](keys []K, values []V) map[K]V { } // SliceToSMap convert string k-v pairs slice to map[string]string -// - eg: []string{k1,v1,k2,v2} -> map[string]string{k1:v1, k2:v2} +// - eg: []string{k1,v1,k2,v2} -> map[string]string{k1:v1, k2:v2} func SliceToSMap(kvPairs ...string) map[string]string { ln := len(kvPairs) // check kvPairs length must be even diff --git a/maputil/convert_test.go b/maputil/convert_test.go index cd7b9cb7f..085efe0f4 100644 --- a/maputil/convert_test.go +++ b/maputil/convert_test.go @@ -155,10 +155,10 @@ func TestHTTPQueryString(t *testing.T) { // Test with different data types src2 := map[string]any{ - "name": "John Doe", - "age": 30, + "name": "John Doe", + "age": 30, "active": true, - "score": 95.5, + "score": 95.5, } str2 := maputil.HTTPQueryString(src2) assert.Contains(t, str2, "name=John Doe") @@ -304,7 +304,7 @@ func TestFlatten(t *testing.T) { // Test with array values data3 := map[string]any{ - "tags": []string{"web", "api"}, + "tags": []string{"web", "api"}, "numbers": []int{1, 2, 3}, } mp3 := maputil.Flatten(data3) @@ -348,7 +348,7 @@ func TestStringsMapToAnyMap(t *testing.T) { // Test with mixed single and multiple values mixedValues := map[string][]string{ - "single": {"value"}, + "single": {"value"}, "multiple": {"value1", "value2", "value3"}, } result3 := maputil.StringsMapToAnyMap(mixedValues) diff --git a/maputil/data_test.go b/maputil/data_test.go index 26f522709..c4831cf97 100644 --- a/maputil/data_test.go +++ b/maputil/data_test.go @@ -19,7 +19,7 @@ func TestData_usage(t *testing.T) { "anyMp": map[string]any{"b": 23}, "k6": "23,45", "k7": []string{"ab", "cd"}, - "k8": []string{"a1", "b2"}, + "k8": []string{"a1", "b2"}, } assert.False(t, mp.IsEmpty()) diff --git a/structs/convert.go b/structs/convert.go index f8029865d..8c55e9431 100644 --- a/structs/convert.go +++ b/structs/convert.go @@ -66,11 +66,11 @@ func ToString(st any, optFns ...MapOptFunc) string { const defaultFieldTag = "json" // CustomUserFunc for map convert -// - fName: raw field name in struct +// - fName: raw field name in struct // // Returns: -// - ok: return true to collect field, otherwise excluded. -// - newVal: `newVal != nil` return new value to collect, otherwise collect original value. +// - ok: return true to collect field, otherwise excluded. +// - newVal: `newVal != nil` return new value to collect, otherwise collect original value. type CustomUserFunc func(fName string, fv reflect.Value) (ok bool, newVal any) // MapOptions for convert struct to map diff --git a/structs/structs_test.go b/structs/structs_test.go index ac43af224..76d61920c 100644 --- a/structs/structs_test.go +++ b/structs/structs_test.go @@ -43,4 +43,4 @@ func TestIssues_173(t *testing.T) { assert.NoError(t, structs.InitDefaults(&c)) dump.P(c) -} \ No newline at end of file +} diff --git a/strutil/textutil/strvar_expr.go b/strutil/textutil/strvar_expr.go index bcfff8d24..ea91b5b4f 100644 --- a/strutil/textutil/strvar_expr.go +++ b/strutil/textutil/strvar_expr.go @@ -15,8 +15,8 @@ import ( // StrVarRenderer implements like shell vars renderer // 简单的实现类似 php, kotlin, shell 插值变量渲染,表达式解析处理。 // -// - var format: $var_name, ${some_var}, ${top.sub_var} -// - func call: ${func($var_name, 'const string')} +// - var format: $var_name, ${some_var}, ${top.sub_var} +// - func call: ${func($var_name, 'const string')} type StrVarRenderer struct { // global variables vars map[string]any diff --git a/strutil/textutil/strvar_test.go b/strutil/textutil/strvar_test.go index 2a8f86f18..dfbc344a0 100644 --- a/strutil/textutil/strvar_test.go +++ b/strutil/textutil/strvar_test.go @@ -25,8 +25,8 @@ func TestStrVarRenderer(t *testing.T) { sv.SetVar("g2", "global value2") vars := map[string]any{ - "name": "inhere", - "int_var": 345, + "name": "inhere", + "int_var": 345, "some_var": "value1", "mapdata": map[string]string{ "key1": "value1", diff --git a/sysutil/on_nonwin_test.go b/sysutil/on_nonwin_test.go index 29ca8bda1..aae2e3ad7 100644 --- a/sysutil/on_nonwin_test.go +++ b/sysutil/on_nonwin_test.go @@ -13,4 +13,4 @@ import ( func TestUser_func(t *testing.T) { assert.False(t, sysutil.IsAdmin()) assert.NoErr(t, sysutil.ChangeUserUidGid(0, 1000)) -} \ No newline at end of file +} diff --git a/sysutil/os_version_windows.go b/sysutil/os_version_windows.go index a9bb0c983..279d6027c 100644 --- a/sysutil/os_version_windows.go +++ b/sysutil/os_version_windows.go @@ -43,7 +43,7 @@ func VersionInfoBySys() *OSVersionInfo { // OsVersionByParse Get Windows system version information by parse string // -// cmdOut eg: "Microsoft Windows [Version 10.0.22631.4391]" +// cmdOut eg: "Microsoft Windows [Version 10.0.22631.4391]" func OsVersionByParse(cmdOut string) (*OSVersionInfo, error) { return parseOsVersionString(cmdOut) } @@ -153,4 +153,3 @@ func parseOsVersionString(out string) (*OSVersionInfo, error) { } return &ovi, nil } - diff --git a/sysutil/sysutil_darwin.go b/sysutil/sysutil_darwin.go index f0962d8a2..f8bb1503c 100644 --- a/sysutil/sysutil_darwin.go +++ b/sysutil/sysutil_darwin.go @@ -1,4 +1,5 @@ //go:build darwin + package sysutil import "os/exec" diff --git a/x/basefn/basefn_test.go b/x/basefn/basefn_test.go index 5547e314e..1ec89261e 100644 --- a/x/basefn/basefn_test.go +++ b/x/basefn/basefn_test.go @@ -4,8 +4,8 @@ import ( "errors" "testing" - "github.com/gookit/goutil/x/basefn" "github.com/gookit/goutil/testutil/assert" + "github.com/gookit/goutil/x/basefn" ) func TestPanicIf(t *testing.T) { diff --git a/x/ccolor/color_tag_test.go b/x/ccolor/color_tag_test.go index 24a60a4ed..f930ee781 100644 --- a/x/ccolor/color_tag_test.go +++ b/x/ccolor/color_tag_test.go @@ -16,7 +16,7 @@ func TestApplyTag(t *testing.T) { ccolor.ForceEnableColor() defer ccolor.RevertColorSupport() - assert.Equal(t,"\x1b[0;32mMSG\x1b[0m", ccolor.ApplyTag("info", "MSG")) + assert.Equal(t, "\x1b[0;32mMSG\x1b[0m", ccolor.ApplyTag("info", "MSG")) } func TestColorTag(t *testing.T) { diff --git a/x/ccolor/style.go b/x/ccolor/style.go index 5a8089120..2fd9f5791 100644 --- a/x/ccolor/style.go +++ b/x/ccolor/style.go @@ -8,8 +8,9 @@ import ( // String color style string. TODO // eg: -// s := String("red,bold") -// s.Println("some text message") +// +// s := String("red,bold") +// s.Println("some text message") type String string // Style for color render. diff --git a/x/ccolor/util.go b/x/ccolor/util.go index 46ed2b4bb..bfc30df2b 100644 --- a/x/ccolor/util.go +++ b/x/ccolor/util.go @@ -128,4 +128,3 @@ func formatLikePrintln(args []any) (message string) { } return } - diff --git a/x/goinfo/stack.go b/x/goinfo/stack.go index 3912b7aa9..e6183569d 100644 --- a/x/goinfo/stack.go +++ b/x/goinfo/stack.go @@ -124,12 +124,12 @@ type CallerFilterFunc func(file string, fc *runtime.Func) bool // // Usage: // -// cs := sysutil.CallersInfos(3, 2) -// for _, ci := range cs { -// fc := runtime.FuncForPC(pc) -// // maybe need check fc = nil -// fnName = fc.Name() -// } +// cs := sysutil.CallersInfos(3, 2) +// for _, ci := range cs { +// fc := runtime.FuncForPC(pc) +// // maybe need check fc = nil +// fnName = fc.Name() +// } func CallersInfos(skip, num int, filters ...CallerFilterFunc) []*CallerInfo { filterLn := len(filters) callers := make([]*CallerInfo, 0, num) diff --git a/x/termenv/detect_nonwin.go b/x/termenv/detect_nonwin.go index 39f8e3678..94ca89cda 100644 --- a/x/termenv/detect_nonwin.go +++ b/x/termenv/detect_nonwin.go @@ -43,4 +43,4 @@ func detectSpecialTermColor(termVal string) (ColorLevel, bool) { func syscallStdinFd() int { return syscall.Stdin -} \ No newline at end of file +}