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
214 changes: 214 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion byteutil/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}

2 changes: 1 addition & 1 deletion byteutil/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ func TestIsAlphaChar(t *testing.T) {
assert.Eq(t, tt.expected, result)
})
}
}
}
2 changes: 1 addition & 1 deletion conv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion envutil/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ func IsMSys() bool {
return sysutil.IsMSys()
}


// IsTerminal isatty check
//
// Usage:
Expand Down
3 changes: 2 additions & 1 deletion envutil/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions envutil/set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -115,4 +115,4 @@ func TestLoadString(t *testing.T) {
assert.Eq(t, tt.want, Getenv(tt.key))
}
}
}
}
2 changes: 1 addition & 1 deletion func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,4 @@ func waitForGoroutine() {
}
runtime.Gosched()
}
}
}
1 change: 0 additions & 1 deletion goutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,3 @@ func FuncName(f any) string {
func PkgName(funcName string) string {
return goinfo.PkgName(funcName)
}

2 changes: 1 addition & 1 deletion internal/comfunc/str_func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ func TestSplitLineToKv(t *testing.T) {
assert.Eq(t, tt.k, k)
assert.Eq(t, tt.v, v)
}
}
}
15 changes: 7 additions & 8 deletions internal/comfunc/time_func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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},
Expand All @@ -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("-")
Expand All @@ -95,4 +95,3 @@ func TestToDuration(t *testing.T) {
})

}

4 changes: 2 additions & 2 deletions internal/gendoc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}

Expand Down
2 changes: 1 addition & 1 deletion jsonutil/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ func DecodeFile(file string, ptr any) error {
}

return json.Unmarshal(bs, ptr)
}
}
2 changes: 1 addition & 1 deletion maputil/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading