Initial QSfera import
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
*.log
|
||||
*.swp
|
||||
.idea
|
||||
*.patch
|
||||
### Go template
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
.DS_Store
|
||||
vendor
|
||||
#go.sum
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 inhere
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+559
@@ -0,0 +1,559 @@
|
||||
# Config
|
||||
|
||||

|
||||
[](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings)
|
||||
[](https://travis-ci.org/gookit/config)
|
||||
[](https://github.com/gookit/config/actions)
|
||||
[](https://coveralls.io/github/gookit/config?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/gookit/config)
|
||||
[](https://pkg.go.dev/github.com/gookit/config/v2)
|
||||
|
||||
`config` - Simple, full-featured Go application configuration management tool library.
|
||||
|
||||
> **[中文说明](README.zh-CN.md)**
|
||||
|
||||
## Features
|
||||
|
||||
- Support multi format: `JSON`(default), `JSON5`, `INI`, `Properties`, `YAML`, `TOML`, `HCL`, `ENV`, `Flags`
|
||||
- `JSON` content support comments. will auto clear comments
|
||||
- Other drivers are used on demand, not used will not be loaded into the application.
|
||||
- Possibility to add custom driver for your specific format
|
||||
- Support multi-file and multi-data loading
|
||||
- Support for loading configuration from system ENV
|
||||
- Support for loading configuration data from remote URLs
|
||||
- Support for setting configuration data from command line(`flags`)
|
||||
- Support listen and fire events on config data changed.
|
||||
- allow events: `set.value`, `set.data`, `load.data`, `clean.data`, `reload.data`
|
||||
- Support data overlay and merge, automatically load by key when loading multiple copies of data
|
||||
- Support for binding all or part of the configuration data to the structure
|
||||
- Support init default value by struct tag `default:"def_value"`
|
||||
- Support init default value from ENV `default:"${APP_ENV | dev}"`
|
||||
- Support get sub value by key-path, like `map.key` `arr.2`
|
||||
- Support parse ENV name and allow with default value. like `envKey: ${SHELL|/bin/bash}` -> `envKey: /bin/zsh`
|
||||
- Generic API: `Get` `Int` `Uint` `Int64` `Float` `String` `Bool` `Ints` `IntMap` `Strings` `StringMap` ...
|
||||
- Complete unit test(code coverage > 95%)
|
||||
|
||||
## Only use INI
|
||||
|
||||
If you just want to use INI for simple config management, recommended use [gookit/ini](https://github.com/gookit/ini)
|
||||
|
||||
### Load dotenv file
|
||||
|
||||
On `gookit/ini`: Provide a sub-package `dotenv` that supports importing data from files (eg `.env`) to ENV
|
||||
|
||||
```shell
|
||||
go get github.com/gookit/ini/v2/dotenv
|
||||
```
|
||||
|
||||
## GoDoc
|
||||
|
||||
- [godoc for github](https://pkg.go.dev/github.com/gookit/config)
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/gookit/config/v2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Here using the yaml format as an example(`testdata/yml_other.yml`):
|
||||
|
||||
```yaml
|
||||
name: app2
|
||||
debug: false
|
||||
baseKey: value2
|
||||
shell: ${SHELL}
|
||||
envKey1: ${NotExist|defValue}
|
||||
|
||||
map1:
|
||||
key: val2
|
||||
key2: val20
|
||||
|
||||
arr1:
|
||||
- val1
|
||||
- val21
|
||||
```
|
||||
|
||||
### Load data
|
||||
|
||||
> examples code please see [_examples/yaml.go](_examples/yaml.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gookit/config/v2"
|
||||
"github.com/gookit/config/v2/yaml"
|
||||
)
|
||||
|
||||
// go run ./examples/yaml.go
|
||||
func main() {
|
||||
// config.ParseEnv: will parse env var in string value. eg: shell: ${SHELL}
|
||||
config.WithOptions(config.ParseEnv)
|
||||
|
||||
// add driver for support yaml content
|
||||
config.AddDriver(yaml.Driver)
|
||||
|
||||
err := config.LoadFiles("testdata/yml_base.yml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// load more files
|
||||
err = config.LoadFiles("testdata/yml_other.yml")
|
||||
// can also load multi at once
|
||||
// err := config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// fmt.Printf("config data: \n %#v\n", config.Data())
|
||||
}
|
||||
```
|
||||
**Usage tips**:
|
||||
|
||||
- More extra options can be added using `WithOptions()`. For example: `ParseEnv`, `ParseDefault`
|
||||
- You can use `AddDriver()` to add the required format driver (`json` is loaded by default, no need to add)
|
||||
- The configuration data can then be loaded using `LoadFiles()` `LoadStrings()` etc.
|
||||
- You can pass in multiple files or call multiple times
|
||||
- Data loaded multiple times will be automatically merged by key
|
||||
|
||||
## Bind Structure
|
||||
|
||||
> Note: The default binding mapping tag of a structure is `mapstructure`, which can be changed by setting the decoder's option `options.DecoderConfig.TagName`
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Age int `mapstructure:"age"`
|
||||
Key string `mapstructure:"key"`
|
||||
UserName string `mapstructure:"user_name"`
|
||||
Tags []int `mapstructure:"tags"`
|
||||
}
|
||||
|
||||
user := User{}
|
||||
err = config.BindStruct("user", &user)
|
||||
|
||||
fmt.Println(user.UserName) // inhere
|
||||
```
|
||||
|
||||
**Change struct tag name**
|
||||
|
||||
```go
|
||||
config.WithOptions(func(opt *Options) {
|
||||
options.DecoderConfig.TagName = "config"
|
||||
})
|
||||
|
||||
// use custom tag name.
|
||||
type User struct {
|
||||
Age int `config:"age"`
|
||||
Key string `config:"key"`
|
||||
UserName string `config:"user_name"`
|
||||
Tags []int `config:"tags"`
|
||||
}
|
||||
|
||||
user := User{}
|
||||
err = config.Decode(&user)
|
||||
```
|
||||
|
||||
**Can bind all config data to a struct**:
|
||||
|
||||
```go
|
||||
config.Decode(&myConf)
|
||||
// can also
|
||||
config.BindStruct("", &myConf)
|
||||
```
|
||||
|
||||
> `config.MapOnExists` like `BindStruct`,but map binding only if key exists
|
||||
|
||||
### Direct read data
|
||||
|
||||
- Get integer
|
||||
|
||||
```go
|
||||
age := config.Int("age")
|
||||
fmt.Print(age) // 100
|
||||
```
|
||||
|
||||
- Get bool
|
||||
|
||||
```go
|
||||
val := config.Bool("debug")
|
||||
fmt.Print(val) // true
|
||||
```
|
||||
|
||||
- Get string
|
||||
|
||||
```go
|
||||
name := config.String("name")
|
||||
fmt.Print(name) // inhere
|
||||
```
|
||||
|
||||
- Get strings(slice)
|
||||
|
||||
```go
|
||||
arr1 := config.Strings("arr1")
|
||||
fmt.Printf("%#v", arr1) // []string{"val1", "val21"}
|
||||
```
|
||||
|
||||
- Get string map
|
||||
|
||||
```go
|
||||
val := config.StringMap("map1")
|
||||
fmt.Printf("%#v",val) // map[string]string{"key":"val2", "key2":"val20"}
|
||||
```
|
||||
|
||||
- Value contains ENV var
|
||||
|
||||
```go
|
||||
value := config.String("shell")
|
||||
fmt.Print(value) // "/bin/zsh"
|
||||
```
|
||||
|
||||
- Get value by key path
|
||||
|
||||
```go
|
||||
// from array
|
||||
value := config.String("arr1.0")
|
||||
fmt.Print(value) // "val1"
|
||||
|
||||
// from map
|
||||
value := config.String("map1.key")
|
||||
fmt.Print(value) // "val2"
|
||||
```
|
||||
|
||||
- Setting new value
|
||||
|
||||
```go
|
||||
// set value
|
||||
config.Set("name", "new name")
|
||||
name = config.String("name")
|
||||
fmt.Print(name) // "new name"
|
||||
```
|
||||
|
||||
## Load from ENV
|
||||
|
||||
Support load ENV vars to config data.
|
||||
|
||||
- Support set value to sub key in map.
|
||||
- eg: `{"DB_USERNAME": "db.username"}` value will set to `username` in `db`
|
||||
|
||||
```go
|
||||
// os env: APP_NAME=config APP_DEBUG=true DB_USERNAME=someone
|
||||
|
||||
// load ENV info
|
||||
config.LoadOSEnvs(map[string]string{"APP_NAME": "app_name", "APP_DEBUG": "app_debug", "DB_USERNAME": "db.username"})
|
||||
|
||||
// read
|
||||
config.Bool("app_debug") // true
|
||||
config.String("app_name") // "config"
|
||||
```
|
||||
|
||||
## Load from flags
|
||||
|
||||
Support simple CLI flags parameter parsing, load to config data.
|
||||
|
||||
- define format: `name:type:desc` OR `name:type` OR `name:desc` (type, desc is optional)
|
||||
- `type` can set `flag` type. allow: `bool`, `int`, `string`(default)
|
||||
- `desc` can set `flag` description
|
||||
- `name` can be in key path format.
|
||||
- eg: `db.username`, input: `--db.username=someone` values will be mapped to `username` of the `db` configuration
|
||||
|
||||
```go
|
||||
// 'debug' flag is bool type
|
||||
config.LoadFlags([]string{"env", "debug:bool"})
|
||||
// can with flag desc message
|
||||
config.LoadFlags([]string{"env:set the run env"})
|
||||
config.LoadFlags([]string{"debug:bool:set debug mode"})
|
||||
// can set value to map key. eg: myapp --map1.sub-key=val
|
||||
config.LoadFlags([]string{"map1.sub-key"})
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```go
|
||||
// flags like: --name inhere --env dev --age 99 --debug --map1.sub-key=val
|
||||
|
||||
// load flag info
|
||||
keys := []string{
|
||||
"name",
|
||||
"env:set the run env",
|
||||
"age:int",
|
||||
"debug:bool:set debug mode",
|
||||
"map1.sub-key",
|
||||
}
|
||||
err := config.LoadFlags(keys)
|
||||
|
||||
// read
|
||||
config.String("name") // "inhere"
|
||||
config.String("env") // "dev"
|
||||
config.Int("age") // 99
|
||||
config.Bool("debug") // true
|
||||
config.Get("map1") // map[string]any{"sub-key":"val"}
|
||||
```
|
||||
|
||||
## New config instance
|
||||
|
||||
You can create custom config instance
|
||||
|
||||
```go
|
||||
// create new instance, will auto register JSON driver
|
||||
myConf := config.New("my-conf")
|
||||
|
||||
// create empty instance
|
||||
myConf := config.NewEmpty("my-conf")
|
||||
|
||||
// create and with some options
|
||||
myConf := config.NewWithOptions("my-conf", config.ParseEnv, config.ReadOnly)
|
||||
```
|
||||
|
||||
## Listen config change
|
||||
|
||||
Now, you can add a hook func for listen config data change. then, you can do something like: write data to file
|
||||
|
||||
**Add hook func on create config**:
|
||||
|
||||
```go
|
||||
hookFn := func(event string, c *Config) {
|
||||
fmt.Println("fire the:", event)
|
||||
}
|
||||
|
||||
c := NewWithOptions("test", config.WithHookFunc(hookFn))
|
||||
// for global config
|
||||
config.WithOptions(config.WithHookFunc(hookFn))
|
||||
```
|
||||
|
||||
After that, when calling `LoadXXX, Set, SetData, ClearData` methods, it will output:
|
||||
|
||||
```text
|
||||
fire the: load.data
|
||||
fire the: set.value
|
||||
fire the: set.data
|
||||
fire the: clean.data
|
||||
```
|
||||
|
||||
### Watch loaded config files
|
||||
|
||||
To listen for changes to loaded config files, and reload the config when it changes, you need to use the https://github.com/fsnotify/fsnotify library.
|
||||
For usage, please refer to the example [./_example/watch_file.go](_examples/watch_file.go)
|
||||
|
||||
Also, you need to listen to the `reload.data` event:
|
||||
|
||||
```go
|
||||
config.WithOptions(config.WithHookFunc(func(event string, c *config.Config) {
|
||||
if event == config.OnReloadData {
|
||||
fmt.Println("config reloaded, you can do something ....")
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
When the configuration changes, you can do related things, for example: rebind the configuration to your struct.
|
||||
|
||||
## Dump config data
|
||||
|
||||
> Can use `config.DumpTo()` export the configuration data to the specified `writer`, such as: buffer,file
|
||||
|
||||
**Dump to JSON file**
|
||||
|
||||
```go
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
_, err := config.DumpTo(buf, config.JSON)
|
||||
ioutil.WriteFile("my-config.json", buf.Bytes(), 0755)
|
||||
```
|
||||
|
||||
**Dump pretty JSON**
|
||||
|
||||
You can set the default var `JSONMarshalIndent` or custom a new JSON driver.
|
||||
|
||||
```go
|
||||
config.JSONMarshalIndent = " "
|
||||
```
|
||||
|
||||
**Dump to YAML file**
|
||||
|
||||
```go
|
||||
_, err := config.DumpTo(buf, config.YAML)
|
||||
ioutil.WriteFile("my-config.yaml", buf.Bytes(), 0755)
|
||||
```
|
||||
|
||||
## Available options
|
||||
|
||||
```go
|
||||
// Options config options
|
||||
type Options struct {
|
||||
// parse env in string value. like: "${EnvName}" "${EnvName|default}"
|
||||
ParseEnv bool
|
||||
// ParseTime parses a duration string to time.Duration
|
||||
// eg: 10s, 2m
|
||||
ParseTime bool
|
||||
// config is readonly. default is False
|
||||
Readonly bool
|
||||
// enable config data cache. default is False
|
||||
EnableCache bool
|
||||
// parse key, allow find value by key path. default is True eg: 'key.sub' will find `map[key]sub`
|
||||
ParseKey bool
|
||||
// the delimiter char for split key path, if `FindByPath=true`. default is '.'
|
||||
Delimiter byte
|
||||
// default write format
|
||||
DumpFormat string
|
||||
// default input format
|
||||
ReadFormat string
|
||||
// DecoderConfig setting for binding data to struct
|
||||
DecoderConfig *mapstructure.DecoderConfig
|
||||
// HookFunc on data changed.
|
||||
HookFunc HookFunc
|
||||
// ParseDefault tag on binding data to struct. tag: default
|
||||
ParseDefault bool
|
||||
}
|
||||
```
|
||||
|
||||
> **TIP**: please visit https://pkg.go.dev/github.com/gookit/config/v2#Options to see the latest options information
|
||||
|
||||
Examples for set options:
|
||||
|
||||
```go
|
||||
config.WithOptions(config.WithTagName("mytag"))
|
||||
config.WithOptions(func(opt *Options) {
|
||||
opt.SetTagNames("config")
|
||||
})
|
||||
```
|
||||
|
||||
### Options: Parse default
|
||||
|
||||
Support parse default value by struct tag `default`, and support parse fields in sub struct.
|
||||
|
||||
> **NOTE**⚠️ If you want to parse a sub-struct, you need to set the `default:""` flag on the parent struct,
|
||||
> otherwise the fields of the sub-struct will not be resolved.
|
||||
|
||||
```go
|
||||
// add option: config.ParseDefault
|
||||
c := config.New("test").WithOptions(config.ParseDefault)
|
||||
|
||||
// only set name
|
||||
c.SetData(map[string]any{
|
||||
"name": "inhere",
|
||||
})
|
||||
|
||||
// age load from default tag
|
||||
type User struct {
|
||||
Age int `default:"30"`
|
||||
Name string
|
||||
Tags []int
|
||||
}
|
||||
|
||||
user := &User{}
|
||||
goutil.MustOk(c.Decode(user))
|
||||
dump.Println(user)
|
||||
```
|
||||
|
||||
**Output**:
|
||||
|
||||
```shell
|
||||
&config_test.User {
|
||||
Age: int(30),
|
||||
Name: string("inhere"), #len=6
|
||||
Tags: []int [ #len=0
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
## API Methods Refer
|
||||
|
||||
### Load Config
|
||||
|
||||
- `LoadOSEnvs(nameToKeyMap map[string]string)` Load data from os ENV
|
||||
- `LoadData(dataSource ...any) (err error)` Load from struts or maps
|
||||
- `LoadFlags(keys []string) (err error)` Load from CLI flags
|
||||
- `LoadExists(sourceFiles ...string) (err error)`
|
||||
- `LoadFiles(sourceFiles ...string) (err error)`
|
||||
- `LoadFromDir(dirPath, format string) (err error)` Load custom format files from the given directory, the file name will be used as the key
|
||||
- `LoadRemote(format, url string) (err error)`
|
||||
- `LoadSources(format string, src []byte, more ...[]byte) (err error)`
|
||||
- `LoadStrings(format string, str string, more ...string) (err error)`
|
||||
- `LoadFilesByFormat(format string, sourceFiles ...string) (err error)`
|
||||
- `LoadExistsByFormat(format string, sourceFiles ...string) error`
|
||||
|
||||
### Getting Values
|
||||
|
||||
- `Bool(key string, defVal ...bool) bool`
|
||||
- `Int(key string, defVal ...int) int`
|
||||
- `Uint(key string, defVal ...uint) uint`
|
||||
- `Int64(key string, defVal ...int64) int64`
|
||||
- `Ints(key string) (arr []int)`
|
||||
- `IntMap(key string) (mp map[string]int)`
|
||||
- `Float(key string, defVal ...float64) float64`
|
||||
- `String(key string, defVal ...string) string`
|
||||
- `Strings(key string) (arr []string)`
|
||||
- `SubDataMap(key string) maputi.Data`
|
||||
- `StringMap(key string) (mp map[string]string)`
|
||||
- `Get(key string, findByPath ...bool) (value any)`
|
||||
|
||||
**Mapping data to struct:**
|
||||
|
||||
- `Decode(dst any) error`
|
||||
- `BindStruct(key string, dst any) error`
|
||||
- `MapOnExists(key string, dst any) error`
|
||||
|
||||
### Setting Values
|
||||
|
||||
- `Set(key string, val any, setByPath ...bool) (err error)`
|
||||
|
||||
### Useful Methods
|
||||
|
||||
- `Getenv(name string, defVal ...string) (val string)`
|
||||
- `AddDriver(driver Driver)`
|
||||
- `Data() map[string]any`
|
||||
- `SetData(data map[string]any)` set data to override the Config.Data
|
||||
- `Exists(key string, findByPath ...bool) bool`
|
||||
- `DumpTo(out io.Writer, format string) (n int64, err error)`
|
||||
|
||||
## Run Tests
|
||||
|
||||
```bash
|
||||
go test -cover
|
||||
// contains all sub-folder
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
## Projects using config
|
||||
|
||||
Check out these projects, which use https://github.com/gookit/config :
|
||||
|
||||
- https://github.com/JanDeDobbeleer/oh-my-posh A prompt theme engine for any shell.
|
||||
- [+ See More](https://pkg.go.dev/github.com/gookit/config?tab=importedby)
|
||||
|
||||
## Gookit packages
|
||||
|
||||
- [gookit/ini](https://github.com/gookit/ini) Go config management, use INI files
|
||||
- [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP
|
||||
- [gookit/gcli](https://github.com/gookit/gcli) build CLI application, tool library, running CLI commands
|
||||
- [gookit/event](https://github.com/gookit/event) Lightweight event manager and dispatcher implements by Go
|
||||
- [gookit/cache](https://github.com/gookit/cache) Generic cache use and cache manager for golang. support File, Memory, Redis, Memcached.
|
||||
- [gookit/config](https://github.com/gookit/config) Go config management. support JSON, YAML, TOML, INI, HCL, ENV and Flags
|
||||
- [gookit/color](https://github.com/gookit/color) A command-line color library with true color support, universal API methods and Windows support
|
||||
- [gookit/filter](https://github.com/gookit/filter) Provide filtering, sanitizing, and conversion of golang data
|
||||
- [gookit/validate](https://github.com/gookit/validate) Use for data validation and filtering. support Map, Struct, Form data
|
||||
- [gookit/goutil](https://github.com/gookit/goutil) Some utils for the Go: string, array/slice, map, format, cli, env, filesystem, test and more
|
||||
- More, please see https://github.com/gookit
|
||||
|
||||
## See also
|
||||
|
||||
- Ini parser [gookit/ini/parser](https://github.com/gookit/ini/tree/master/parser)
|
||||
- Properties parser [gookit/properties](https://github.com/gookit/properties)
|
||||
- Json5 parser
|
||||
- [yosuke-furukawa/json5](https://github.com/yosuke-furukawa/json5)
|
||||
- [titanous/json5](https://github.com/titanous/json5)
|
||||
- Json parser
|
||||
- [goccy/go-json](https://github.com/goccy/go-json)
|
||||
- [json-iterator/go](https://github.com/json-iterator/go)
|
||||
- Yaml parser
|
||||
- [goccy/go-yaml](https://github.com/goccy/go-yaml)
|
||||
- [go-yaml/yaml](https://github.com/go-yaml/yaml)
|
||||
- Toml parser [go toml](https://github.com/BurntSushi/toml)
|
||||
- Data merge [mergo](https://github.com/imdario/mergo)
|
||||
- Map structure [mapstructure](https://github.com/mitchellh/mapstructure)
|
||||
|
||||
## License
|
||||
|
||||
**MIT**
|
||||
+545
@@ -0,0 +1,545 @@
|
||||
# Config
|
||||
|
||||

|
||||
[](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings)
|
||||
[](https://travis-ci.org/gookit/config)
|
||||
[](https://github.com/gookit/config/actions)
|
||||
[](https://coveralls.io/github/gookit/config?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/gookit/config)
|
||||
[](https://pkg.go.dev/github.com/gookit/config/v2)
|
||||
|
||||
`config` - 简洁、功能完善的 Go 应用程序配置管理工具库
|
||||
|
||||
> **[EN README](README.md)**
|
||||
|
||||
## 功能简介
|
||||
|
||||
- 支持多种格式: `JSON`(默认), `JSON5`, `INI`, `Properties`, `YAML`, `TOML`, `HCL`, `ENV`, `Flags`
|
||||
- `JSON` 内容支持注释,可以设置解析时清除注释
|
||||
- 其他驱动都是按需使用,不使用的不会加载编译到应用中
|
||||
- 支持多个文件、多数据加载
|
||||
- 支持从 OS ENV 变量数据加载配置
|
||||
- 支持从远程 URL 加载配置数据
|
||||
- 支持从命令行参数(`flags`)设置配置数据
|
||||
- 数据自动覆盖合并,加载多份数据时将按`key`自动合并
|
||||
- 支持丰富的自定义选项设置
|
||||
- `Readonly` 支持设置配置数据只读
|
||||
- `EnableCache` 支持设置配置数据缓存
|
||||
- `ParseEnv` 支持获取时自动解析string值里的ENV变量(`shell: ${SHELL}` -> `shell: /bin/zsh`)
|
||||
- `ParseDefault` 支持在绑定数据到结构体时解析默认值 (tag: `default:"def_value"`, 配合`ParseEnv`也支持ENV变量)
|
||||
- `ParseTime` 支持绑定数据到struct时自动转换 `10s`,`2m` 为 `time.Duration`
|
||||
- 完整选项设置请查看 `config.Options`
|
||||
- 支持将全部或部分配置数据绑定到结构体 `config.BindStruct("key", &s)`
|
||||
- 支持通过结构体标签 `default` 解析并设置默认值. eg: `default:"def_value"`
|
||||
- 支持从 ENV 初始化设置字段值 `default:"${APP_ENV | dev}"`
|
||||
- 支持通过 `.` 分隔符来按路径获取子级值,也支持自定义分隔符。 e.g `map.key` `arr.2`
|
||||
- 支持在配置数据更改时触发事件
|
||||
- 可用事件: `set.value`, `set.data`, `load.data`, `clean.data`, `reload.data`
|
||||
- 简洁的使用API `Get` `Int` `Uint` `Int64` `String` `Bool` `Ints` `IntMap` `Strings` `StringMap` ...
|
||||
- 完善的单元测试(code coverage > 95%)
|
||||
|
||||
## 只使用INI
|
||||
|
||||
如果你仅仅想用INI来做简单配置管理,推荐使用 [gookit/ini](https://github.com/gookit/ini)
|
||||
|
||||
### 加载 .env 文件
|
||||
|
||||
`gookit/ini`: 提供一个子包 `dotenv`,支持从文件(eg `.env`)中导入数据到ENV
|
||||
|
||||
```shell
|
||||
go get github.com/gookit/ini/v2/dotenv
|
||||
```
|
||||
|
||||
## GoDoc
|
||||
|
||||
- [godoc for github](https://godoc.org/github.com/gookit/config)
|
||||
|
||||
## 安装包
|
||||
|
||||
```bash
|
||||
go get github.com/gookit/config/v2
|
||||
```
|
||||
|
||||
## 快速使用
|
||||
|
||||
这里使用yaml格式内容作为示例(`testdata/yml_other.yml`):
|
||||
|
||||
```yaml
|
||||
name: app2
|
||||
debug: false
|
||||
baseKey: value2
|
||||
shell: ${SHELL}
|
||||
envKey1: ${NotExist|defValue}
|
||||
|
||||
map1:
|
||||
key: val2
|
||||
key2: val20
|
||||
|
||||
arr1:
|
||||
- val1
|
||||
- val21
|
||||
```
|
||||
|
||||
### 载入数据
|
||||
|
||||
> 示例代码请看 [_examples/yaml.go](_examples/yaml.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gookit/config/v2"
|
||||
"github.com/gookit/config/v2/yaml"
|
||||
)
|
||||
|
||||
// go run ./examples/yaml.go
|
||||
func main() {
|
||||
// 设置选项支持ENV变量解析:当获取的值为string类型时,会尝试解析其中的ENV变量
|
||||
config.WithOptions(config.ParseEnv)
|
||||
|
||||
// 添加驱动程序以支持yaml内容解析(除了JSON是默认支持,其他的则是按需使用)
|
||||
config.AddDriver(yaml.Driver)
|
||||
|
||||
// 加载配置,可以同时传入多个文件
|
||||
err := config.LoadFiles("testdata/yml_base.yml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// fmt.Printf("config data: \n %#v\n", config.Data())
|
||||
|
||||
// 加载更多文件
|
||||
err = config.LoadFiles("testdata/yml_other.yml")
|
||||
// 也可以一次性加载多个文件
|
||||
// err := config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用提示**:
|
||||
|
||||
- 可以使用 `WithOptions()` 添加更多额外选项功能. 例如: `ParseEnv`, `ParseDefault`
|
||||
- 可以使用 `AddDriver()` 添加需要使用的格式驱动器(`json` 是默认加载的的,无需添加)
|
||||
- 然后就可以使用 `LoadFiles()` `LoadStrings()` 等方法加载配置数据
|
||||
- 可以传入多个文件,也可以调用多次
|
||||
- 多次加载的数据会自动按key进行合并处理
|
||||
|
||||
### 绑定数据到结构体
|
||||
|
||||
> 注意:结构体默认的绑定映射tag是 `mapstructure`,可以通过设置 `Options.TagName` 来更改它
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Age int `mapstructure:"age"`
|
||||
Key string `mapstructure:"key"`
|
||||
UserName string `mapstructure:"user_name"`
|
||||
Tags []int `mapstructure:"tags"`
|
||||
}
|
||||
|
||||
user := User{}
|
||||
err = config.BindStruct("user", &user)
|
||||
|
||||
fmt.Println(user.UserName) // inhere
|
||||
```
|
||||
|
||||
**更改结构标签名称**
|
||||
|
||||
```go
|
||||
config.WithOptions(func(opt *Options) {
|
||||
options.DecoderConfig.TagName = "config"
|
||||
})
|
||||
|
||||
// use custom tag name.
|
||||
type User struct {
|
||||
Age int `config:"age"`
|
||||
Key string `config:"key"`
|
||||
UserName string `config:"user_name"`
|
||||
Tags []int `config:"tags"`
|
||||
}
|
||||
|
||||
user := User{}
|
||||
err = config.Decode(&user)
|
||||
```
|
||||
|
||||
将所有配置数据绑定到结构:
|
||||
|
||||
```go
|
||||
config.Decode(&myConf)
|
||||
// 也可以
|
||||
config.BindStruct("", &myConf)
|
||||
```
|
||||
|
||||
> `config.MapOnExists` 与 `BindStruct` 一样,但仅当 key 存在时才进行映射绑定
|
||||
|
||||
### 快速获取数据
|
||||
|
||||
```go
|
||||
// 获取整型
|
||||
age := config.Int("age")
|
||||
fmt.Print(age) // 100
|
||||
|
||||
// 获取布尔值
|
||||
val := config.Bool("debug")
|
||||
fmt.Print(val) // true
|
||||
|
||||
// 获取字符串
|
||||
name := config.String("name")
|
||||
fmt.Print(name) // inhere
|
||||
|
||||
// 获取字符串数组
|
||||
arr1 := config.Strings("arr1")
|
||||
fmt.Printf("%#v", arr1) // []string{"val1", "val21"}
|
||||
|
||||
// 获取字符串KV映射
|
||||
val := config.StringMap("map1")
|
||||
fmt.Printf("%#v",val) // map[string]string{"key":"val2", "key2":"val20"}
|
||||
|
||||
// 值包含ENV变量
|
||||
value := config.String("shell")
|
||||
fmt.Print(value) // /bin/zsh
|
||||
|
||||
// 通过key路径获取值
|
||||
// from array
|
||||
value := config.String("arr1.0")
|
||||
fmt.Print(value) // "val1"
|
||||
|
||||
// from map
|
||||
value := config.String("map1.key")
|
||||
fmt.Print(value) // "val2"
|
||||
```
|
||||
|
||||
### 设置新的值
|
||||
|
||||
```go
|
||||
// set value
|
||||
config.Set("name", "new name")
|
||||
// get
|
||||
name = config.String("name")
|
||||
fmt.Print(name) // new name
|
||||
```
|
||||
|
||||
## 加载配置文件
|
||||
|
||||
- `LoadExists(sourceFiles ...string) (err error)` 从存在的配置文件里加载数据,会忽略不存在的文件
|
||||
- `LoadFiles(sourceFiles ...string) (err error)` 从给定的配置文件里加载数据,有文件不存在则会panic
|
||||
|
||||
> **TIP**: 更多加载方式请查看 `config.Load*` 相关方法
|
||||
|
||||
## 从ENV载入数据
|
||||
|
||||
`LoadOSEnvs` 支持从环境变量中读取数据,并解析为配置数据。格式为 `ENV_NAME: config_key`
|
||||
|
||||
- `config_key` 可以是 key path 格式。 eg: `{"DB_USERNAME": "db.username"}` 值将会映射到 `db` 配置的 `username`
|
||||
|
||||
```go
|
||||
// os env: APP_NAME=config APP_DEBUG=true
|
||||
// load ENV info
|
||||
config.LoadOSEnvs(map[string]string{"APP_NAME": "app_name", "APP_DEBUG": "app_debug"})
|
||||
|
||||
// read
|
||||
config.Bool("app_debug") // true
|
||||
config.String("app_name") // "config"
|
||||
```
|
||||
|
||||
## 从命令行参数载入数据
|
||||
|
||||
支持简单的从命令行 `flag` 参数解析,加载数据。
|
||||
|
||||
- 配置参数格式为 `name:type:desc` OR `name:type` OR `name:desc` (type, desc 是可选的)
|
||||
- `type` 可以设置 `flag` 的类型,支持 `bool`, `int`, `string`(默认)
|
||||
- `desc` 可以设置 `flag` 的描述信息
|
||||
- `name` 可以是 key path 格式。 eg: `db.username`, input: `--db.username=someone` 值将会映射到 `db` 配置的 `username`
|
||||
|
||||
```go
|
||||
// 'debug' flag is bool type
|
||||
config.LoadFlags([]string{"env", "debug:bool"})
|
||||
// can with flag desc message
|
||||
config.LoadFlags([]string{"env:set the run env"})
|
||||
config.LoadFlags([]string{"debug:bool:set debug mode"})
|
||||
// can set value to map key. eg: myapp --map1.sub-key=val
|
||||
config.LoadFlags([]string{"map1.sub-key"})
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```go
|
||||
// flags like: --name inhere --env dev --age 99 --debug --map1.sub-key=val
|
||||
|
||||
// load flag info
|
||||
keys := []string{
|
||||
"name",
|
||||
"env:set the run env",
|
||||
"age:int",
|
||||
"debug:bool:set debug mode",
|
||||
"map1.sub-key",
|
||||
}
|
||||
err := config.LoadFlags(keys)
|
||||
|
||||
// read
|
||||
config.String("name") // "inhere"
|
||||
config.String("env") // "dev"
|
||||
config.Int("age") // 99
|
||||
config.Bool("debug") // true
|
||||
config.Get("map1") // map[string]any{"sub-key":"val"}
|
||||
```
|
||||
|
||||
## 创建自定义实例
|
||||
|
||||
您可以创建自定义配置实例:
|
||||
|
||||
```go
|
||||
// create new instance, will auto register JSON driver
|
||||
myConf := config.New("my-conf")
|
||||
|
||||
// create empty instance
|
||||
myConf := config.NewEmpty("my-conf")
|
||||
|
||||
// create and with some options
|
||||
myConf := config.NewWithOptions("my-conf", config.ParseEnv, config.ReadOnly)
|
||||
```
|
||||
|
||||
## 监听配置更改
|
||||
|
||||
现在,您可以添加一个钩子函数来监听配置数据更改。然后,您可以执行一些自定义操作, 例如:将数据写入文件
|
||||
|
||||
**在创建配置时添加钩子函数**:
|
||||
|
||||
```go
|
||||
hookFn := func(event string, c *Config) {
|
||||
fmt.Println("fire the:", event)
|
||||
}
|
||||
|
||||
c := NewWithOptions("test", WithHookFunc(hookFn))
|
||||
// for global config
|
||||
config.WithOptions(WithHookFunc(hookFn))
|
||||
```
|
||||
|
||||
**之后**, 当调用 `LoadXXX, Set, SetData, ClearData` 等方法时, 就会输出:
|
||||
|
||||
```text
|
||||
fire the: load.data
|
||||
fire the: set.value
|
||||
fire the: set.data
|
||||
fire the: clean.data
|
||||
```
|
||||
|
||||
### 监听载入的配置文件变动
|
||||
|
||||
想要监听载入的配置文件变动,并在变动时重新加载配置,你需要使用 https://github.com/fsnotify/fsnotify 库。
|
||||
使用方法可以参考示例 [./_example/watch_file.go](_examples/watch_file.go)
|
||||
|
||||
同时,你需要监听 `reload.data` 事件:
|
||||
|
||||
```go
|
||||
config.WithOptions(config.WithHookFunc(func(event string, c *config.Config) {
|
||||
if event == config.OnReloadData {
|
||||
fmt.Println("config reloaded, you can do something ....")
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
当配置发生变化并重新加载后,你可以做相关的事情,例如:重新绑定配置到你的结构体。
|
||||
|
||||
## 导出配置到文件
|
||||
|
||||
> 可以使用 `config.DumpTo(out io.Writer, format string)` 将整个配置数据导出到指定的writer, 比如 buffer,file。
|
||||
|
||||
**示例:导出为JSON文件**
|
||||
|
||||
```go
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
_, err := config.DumpTo(buf, config.JSON)
|
||||
ioutil.WriteFile("my-config.json", buf.Bytes(), 0755)
|
||||
```
|
||||
|
||||
**示例:导出格式化的JSON**
|
||||
|
||||
可以设置默认变量 `JSONMarshalIndent` 的值 或 自定义新的 JSON 驱动程序。
|
||||
|
||||
```go
|
||||
config.JSONMarshalIndent = " "
|
||||
```
|
||||
|
||||
**示例:导出为YAML文件**
|
||||
|
||||
```go
|
||||
_, err := config.DumpTo(buf, config.YAML)
|
||||
ioutil.WriteFile("my-config.yaml", buf.Bytes(), 0755)
|
||||
```
|
||||
|
||||
## 可用选项
|
||||
|
||||
```go
|
||||
// Options config options
|
||||
type Options struct {
|
||||
// parse env in string value. like: "${EnvName}" "${EnvName|default}"
|
||||
ParseEnv bool
|
||||
// ParseTime parses a duration string to time.Duration
|
||||
// eg: 10s, 2m
|
||||
ParseTime bool
|
||||
// config is readonly. default is False
|
||||
Readonly bool
|
||||
// enable config data cache. default is False
|
||||
EnableCache bool
|
||||
// parse key, allow find value by key path. default is True eg: 'key.sub' will find `map[key]sub`
|
||||
ParseKey bool
|
||||
// the delimiter char for split key, when `FindByPath=true`. default is '.'
|
||||
Delimiter byte
|
||||
// default write format. default is JSON
|
||||
DumpFormat string
|
||||
// default input format. default is JSON
|
||||
ReadFormat string
|
||||
// DecoderConfig setting for binding data to struct
|
||||
DecoderConfig *mapstructure.DecoderConfig
|
||||
// HookFunc on data changed.
|
||||
HookFunc HookFunc
|
||||
// ParseDefault tag on binding data to struct. tag: default
|
||||
ParseDefault bool
|
||||
}
|
||||
```
|
||||
|
||||
> **提示**: 访问 https://pkg.go.dev/github.com/gookit/config/v2#Options 查看最新的选项信息
|
||||
|
||||
Examples for set options:
|
||||
|
||||
```go
|
||||
config.WithOptions(config.WithTagName("mytag"))
|
||||
config.WithOptions(func(opt *Options) {
|
||||
opt.SetTagNames("config")
|
||||
})
|
||||
```
|
||||
|
||||
### 选项: 解析默认值
|
||||
|
||||
NEW: 支持通过结构标签 `default` 解析并设置默认值,支持嵌套解析处理。
|
||||
|
||||
> 注意 ⚠️ 如果想要解析子结构体字段,需要对父结构体设置 `default:""` 标记,否则不会解析子结构体的字段。
|
||||
|
||||
```go
|
||||
// add option: config.ParseDefault
|
||||
c := config.New("test").WithOptions(config.ParseDefault)
|
||||
|
||||
// only set name
|
||||
c.SetData(map[string]any{
|
||||
"name": "inhere",
|
||||
})
|
||||
|
||||
// age load from default tag
|
||||
type User struct {
|
||||
Age int `default:"30"`
|
||||
Name string
|
||||
Tags []int
|
||||
}
|
||||
|
||||
user := &User{}
|
||||
goutil.MustOk(c.Decode(user))
|
||||
dump.Println(user)
|
||||
```
|
||||
|
||||
**Output**:
|
||||
|
||||
```shell
|
||||
&config_test.User {
|
||||
Age: int(30),
|
||||
Name: string("inhere"), #len=6
|
||||
Tags: []int [ #len=0
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
## API方法参考
|
||||
|
||||
### 载入配置
|
||||
|
||||
- `LoadData(dataSource ...any) (err error)` 从struct或map加载数据
|
||||
- `LoadFlags(keys []string) (err error)` 从命令行参数载入数据
|
||||
- `LoadOSEnvs(nameToKeyMap map[string]string)` 从ENV载入配置数据
|
||||
- `LoadExists(sourceFiles ...string) (err error)` 从存在的配置文件里加载数据,会忽略不存在的文件
|
||||
- `LoadFiles(sourceFiles ...string) (err error)` 从给定的配置文件里加载数据,有文件不存在则会panic
|
||||
- `LoadFromDir(dirPath, format string) (err error)` 从给定目录里加载自定格式的文件,文件名会作为 key
|
||||
- `LoadRemote(format, url string) (err error)` 从远程 URL 加载配置数据
|
||||
- `LoadSources(format string, src []byte, more ...[]byte) (err error)` 从给定格式的字节数据加载配置
|
||||
- `LoadStrings(format string, str string, more ...string) (err error)` 从给定格式的字符串配置里加载配置数据
|
||||
- `LoadFilesByFormat(format string, sourceFiles ...string) (err error)` 从给定格式的文件加载配置
|
||||
- `LoadExistsByFormat(format string, sourceFiles ...string) error` 从给定格式的文件加载配置,会忽略不存在的文件
|
||||
|
||||
### 获取值
|
||||
|
||||
- `Bool(key string, defVal ...bool) bool`
|
||||
- `Int(key string, defVal ...int) int`
|
||||
- `Uint(key string, defVal ...uint) uint`
|
||||
- `Int64(key string, defVal ...int64) int64`
|
||||
- `Ints(key string) (arr []int)`
|
||||
- `IntMap(key string) (mp map[string]int)`
|
||||
- `Float(key string, defVal ...float64) float64`
|
||||
- `String(key string, defVal ...string) string`
|
||||
- `Strings(key string) (arr []string)`
|
||||
- `StringMap(key string) (mp map[string]string)`
|
||||
- `SubDataMap(key string) maputi.Data`
|
||||
- `Get(key string, findByPath ...bool) (value any)`
|
||||
|
||||
**将数据映射到结构体:**
|
||||
|
||||
- `BindStruct(key string, dst any) error`
|
||||
- `MapOnExists(key string, dst any) error`
|
||||
|
||||
### 设置值
|
||||
|
||||
- `Set(key string, val any, setByPath ...bool) (err error)`
|
||||
|
||||
### 有用的方法
|
||||
|
||||
- `Getenv(name string, defVal ...string) (val string)`
|
||||
- `AddDriver(driver Driver)`
|
||||
- `Data() map[string]any`
|
||||
- `Exists(key string, findByPath ...bool) bool`
|
||||
- `DumpTo(out io.Writer, format string) (n int64, err error)`
|
||||
- `SetData(data map[string]any)` 设置数据以覆盖 `Config.Data`
|
||||
|
||||
## 单元测试
|
||||
|
||||
```bash
|
||||
go test -cover
|
||||
// contains all sub-folder
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
## 使用Config的项目
|
||||
|
||||
看看这些使用了 https://github.com/gookit/config 的项目:
|
||||
|
||||
- https://github.com/JanDeDobbeleer/oh-my-posh A prompt theme engine for any shell.
|
||||
- [+ See More](https://pkg.go.dev/github.com/gookit/config?tab=importedby)
|
||||
|
||||
## Gookit 工具包
|
||||
|
||||
- [gookit/ini](https://github.com/gookit/ini) INI配置读取管理,支持多文件加载,数据覆盖合并, 解析ENV变量, 解析变量引用
|
||||
- [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP
|
||||
- [gookit/gcli](https://github.com/gookit/gcli) Go的命令行应用,工具库,运行CLI命令,支持命令行色彩,用户交互,进度显示,数据格式化显示
|
||||
- [gookit/event](https://github.com/gookit/event) Go实现的轻量级的事件管理、调度程序库, 支持设置监听器的优先级, 支持对一组事件进行监听
|
||||
- [gookit/cache](https://github.com/gookit/cache) 通用的缓存使用包装库,通过包装各种常用的驱动,来提供统一的使用API
|
||||
- [gookit/config](https://github.com/gookit/config) Go应用配置管理,支持多种格式(JSON, YAML, TOML, INI, HCL, ENV, Flags),多文件加载,远程文件加载,数据合并
|
||||
- [gookit/color](https://github.com/gookit/color) CLI 控制台颜色渲染工具库, 拥有简洁的使用API,支持16色,256色,RGB色彩渲染输出
|
||||
- [gookit/filter](https://github.com/gookit/filter) 提供对Golang数据的过滤,净化,转换
|
||||
- [gookit/validate](https://github.com/gookit/validate) Go通用的数据验证与过滤库,使用简单,内置大部分常用验证、过滤器
|
||||
- [gookit/goutil](https://github.com/gookit/goutil) Go 的一些工具函数,格式化,特殊处理,常用信息获取等
|
||||
- 更多请查看 https://github.com/gookit
|
||||
|
||||
## 相关包
|
||||
|
||||
- Ini 解析 [gookit/ini/parser](https://github.com/gookit/ini/tree/master/parser)
|
||||
- Properties 解析 [gookit/properties](https://github.com/gookit/properties)
|
||||
- Yaml 解析 [go-yaml](https://github.com/go-yaml/yaml)
|
||||
- Toml 解析 [go toml](https://github.com/BurntSushi/toml)
|
||||
- 数据合并 [mergo](https://github.com/imdario/mergo)
|
||||
- 映射数据到结构体 [mapstructure](https://github.com/mitchellh/mapstructure)
|
||||
- JSON5 解析
|
||||
- [yosuke-furukawa/json5](https://github.com/yosuke-furukawa/json5)
|
||||
- [titanous/json5](https://github.com/titanous/json5)
|
||||
|
||||
## License
|
||||
|
||||
**MIT**
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# TODO
|
||||
|
||||
- remote `etcd` `consul`
|
||||
- watch changed config files and reload
|
||||
- [x] set default value on binding struct. use tag `defalut`
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
Package config is a go config management implement. support YAML,TOML,JSON,INI,HCL format.
|
||||
|
||||
Source code and other details for the project are available at GitHub:
|
||||
|
||||
https://github.com/gookit/config
|
||||
|
||||
JSON format content example:
|
||||
|
||||
{
|
||||
"name": "app",
|
||||
"debug": false,
|
||||
"baseKey": "value",
|
||||
"age": 123,
|
||||
"envKey": "${SHELL}",
|
||||
"envKey1": "${NotExist|defValue}",
|
||||
"map1": {
|
||||
"key": "val",
|
||||
"key1": "val1",
|
||||
"key2": "val2"
|
||||
},
|
||||
"arr1": [
|
||||
"val",
|
||||
"val1",
|
||||
"val2"
|
||||
],
|
||||
"lang": {
|
||||
"dir": "res/lang",
|
||||
"defLang": "en",
|
||||
"allowed": {
|
||||
"en": "val",
|
||||
"zh-CN": "val2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Usage please see example(more example please see examples folder in the lib):
|
||||
*/
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// There are supported config format
|
||||
const (
|
||||
Ini = "ini"
|
||||
Hcl = "hcl"
|
||||
Yml = "yml"
|
||||
JSON = "json"
|
||||
Yaml = "yaml"
|
||||
Toml = "toml"
|
||||
Prop = "properties"
|
||||
)
|
||||
|
||||
const (
|
||||
// default delimiter
|
||||
defaultDelimiter byte = '.'
|
||||
// default struct tag name for binding data to struct
|
||||
defaultStructTag = "mapstructure"
|
||||
// struct tag name for set default-value on binding data
|
||||
defaultValueTag = "default"
|
||||
)
|
||||
|
||||
// internal vars
|
||||
// type intArr []int
|
||||
type strArr []string
|
||||
|
||||
// type intMap map[string]int
|
||||
type strMap map[string]string
|
||||
|
||||
// This is a default config manager instance
|
||||
var dc = New("default")
|
||||
|
||||
// Config structure definition
|
||||
type Config struct {
|
||||
// save the latest error, will clear after read.
|
||||
err error
|
||||
// config instance name
|
||||
name string
|
||||
lock sync.RWMutex
|
||||
|
||||
// config options
|
||||
opts *Options
|
||||
// all config data
|
||||
data map[string]any
|
||||
|
||||
// loaded config files records
|
||||
loadedUrls []string
|
||||
loadedFiles []string
|
||||
driverNames []string
|
||||
// driver alias to name map.
|
||||
aliasMap map[string]string
|
||||
reloading bool
|
||||
|
||||
// TODO Deprecated decoder and encoder, use driver instead
|
||||
// drivers map[string]Driver
|
||||
|
||||
// decoders["toml"] = func(blob []byte, v any) (err error){}
|
||||
// decoders["yaml"] = func(blob []byte, v any) (err error){}
|
||||
decoders map[string]Decoder
|
||||
encoders map[string]Encoder
|
||||
|
||||
// cache on got config data
|
||||
intCache map[string]int
|
||||
strCache map[string]string
|
||||
// iArrCache map[string]intArr TODO cache it
|
||||
// iMapCache map[string]intMap
|
||||
sArrCache map[string]strArr
|
||||
sMapCache map[string]strMap
|
||||
}
|
||||
|
||||
// New config instance with custom options, default with JSON driver
|
||||
func New(name string, opts ...OptionFn) *Config {
|
||||
return NewEmpty(name, opts...).WithDriver(JSONDriver)
|
||||
}
|
||||
|
||||
// NewGeneric create generic config instance with custom options.
|
||||
//
|
||||
// - default add options: ParseEnv, ParseDefault, ParseTime
|
||||
func NewGeneric(name string, opts ...OptionFn) *Config {
|
||||
return NewEmpty(name, ParseEnv, ParseDefault, ParseTime).WithOptions(opts...).WithDriver(JSONDriver)
|
||||
}
|
||||
|
||||
// NewEmpty create config instance with custom options
|
||||
func NewEmpty(name string, opts ...OptionFn) *Config {
|
||||
c := &Config{
|
||||
name: name,
|
||||
opts: newDefaultOption(),
|
||||
data: make(map[string]any),
|
||||
// don't add any drivers
|
||||
encoders: map[string]Encoder{},
|
||||
decoders: map[string]Decoder{},
|
||||
aliasMap: make(map[string]string),
|
||||
}
|
||||
|
||||
return c.WithOptions(opts...)
|
||||
}
|
||||
|
||||
// NewWith create config instance, and you can call some init func
|
||||
func NewWith(name string, fn func(c *Config)) *Config {
|
||||
return New(name).With(fn)
|
||||
}
|
||||
|
||||
// NewWithOptions config instance. alias of New()
|
||||
func NewWithOptions(name string, opts ...OptionFn) *Config {
|
||||
return New(name).WithOptions(opts...)
|
||||
}
|
||||
|
||||
// Default get the default instance
|
||||
func Default() *Config { return dc }
|
||||
|
||||
/*************************************************************
|
||||
* config drivers
|
||||
*************************************************************/
|
||||
|
||||
// WithDriver set multi drivers at once.
|
||||
func WithDriver(drivers ...Driver) { dc.WithDriver(drivers...) }
|
||||
|
||||
// WithDriver set multi drivers at once.
|
||||
func (c *Config) WithDriver(drivers ...Driver) *Config {
|
||||
for _, driver := range drivers {
|
||||
c.AddDriver(driver)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// AddDriver set a decoder and encoder driver for a format.
|
||||
func AddDriver(driver Driver) { dc.AddDriver(driver) }
|
||||
|
||||
// AddDriver set a decoder and encoder driver for a format.
|
||||
func (c *Config) AddDriver(driver Driver) {
|
||||
format := driver.Name()
|
||||
if len(driver.Aliases()) > 0 {
|
||||
for _, alias := range driver.Aliases() {
|
||||
c.aliasMap[alias] = format
|
||||
}
|
||||
}
|
||||
|
||||
c.driverNames = append(c.driverNames, format)
|
||||
c.decoders[format] = driver.GetDecoder()
|
||||
c.encoders[format] = driver.GetEncoder()
|
||||
}
|
||||
|
||||
// HasDecoder has decoder
|
||||
func (c *Config) HasDecoder(format string) bool {
|
||||
format = c.resolveFormat(format)
|
||||
_, ok := c.decoders[format]
|
||||
return ok
|
||||
}
|
||||
|
||||
// HasEncoder has encoder
|
||||
func (c *Config) HasEncoder(format string) bool {
|
||||
format = c.resolveFormat(format)
|
||||
_, ok := c.encoders[format]
|
||||
return ok
|
||||
}
|
||||
|
||||
// DelDriver delete driver of the format
|
||||
func (c *Config) DelDriver(format string) {
|
||||
format = c.resolveFormat(format)
|
||||
delete(c.decoders, format)
|
||||
delete(c.encoders, format)
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* helper methods
|
||||
*************************************************************/
|
||||
|
||||
// Name get config name
|
||||
func (c *Config) Name() string { return c.name }
|
||||
|
||||
// AddAlias add alias for a format(driver name)
|
||||
func AddAlias(format, alias string) { dc.AddAlias(format, alias) }
|
||||
|
||||
// AddAlias add alias for a format(driver name)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// config.AddAlias("ini", "conf")
|
||||
func (c *Config) AddAlias(format, alias string) {
|
||||
c.aliasMap[alias] = format
|
||||
}
|
||||
|
||||
// AliasMap get alias map
|
||||
func (c *Config) AliasMap() map[string]string { return c.aliasMap }
|
||||
|
||||
// Error get last error, will clear after read.
|
||||
func (c *Config) Error() error {
|
||||
err := c.err
|
||||
c.err = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// IsEmpty of the config
|
||||
func (c *Config) IsEmpty() bool {
|
||||
return len(c.data) == 0
|
||||
}
|
||||
|
||||
// LoadedUrls get loaded urls list
|
||||
func (c *Config) LoadedUrls() []string { return c.loadedUrls }
|
||||
|
||||
// LoadedFiles get loaded files name
|
||||
func (c *Config) LoadedFiles() []string { return c.loadedFiles }
|
||||
|
||||
// DriverNames get loaded driver names
|
||||
func (c *Config) DriverNames() []string { return c.driverNames }
|
||||
|
||||
// Reset data and caches
|
||||
func Reset() { dc.ClearAll() }
|
||||
|
||||
// ClearAll data and caches
|
||||
func ClearAll() { dc.ClearAll() }
|
||||
|
||||
// ClearAll data and caches
|
||||
func (c *Config) ClearAll() {
|
||||
c.ClearData()
|
||||
c.ClearCaches()
|
||||
|
||||
c.aliasMap = make(map[string]string)
|
||||
// options
|
||||
c.opts.Readonly = false
|
||||
}
|
||||
|
||||
// ClearData clear data
|
||||
func (c *Config) ClearData() {
|
||||
c.fireHook(OnCleanData)
|
||||
|
||||
c.data = make(map[string]any)
|
||||
c.loadedUrls = []string{}
|
||||
c.loadedFiles = []string{}
|
||||
}
|
||||
|
||||
// ClearCaches clear caches
|
||||
func (c *Config) ClearCaches() {
|
||||
if c.opts.EnableCache {
|
||||
c.intCache = nil
|
||||
c.strCache = nil
|
||||
c.sMapCache = nil
|
||||
c.sArrCache = nil
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* helper methods
|
||||
*************************************************************/
|
||||
|
||||
// fire hook
|
||||
func (c *Config) fireHook(name string) {
|
||||
if c.opts.HookFunc != nil {
|
||||
c.opts.HookFunc(name, c)
|
||||
}
|
||||
}
|
||||
|
||||
// record error
|
||||
func (c *Config) addError(err error) {
|
||||
c.err = err
|
||||
}
|
||||
|
||||
// format and record error
|
||||
func (c *Config) addErrorf(format string, a ...any) {
|
||||
c.err = fmt.Errorf(format, a...)
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gookit/goutil/jsonutil"
|
||||
)
|
||||
|
||||
// Driver interface.
|
||||
// TODO refactor: rename GetDecoder() to Decode(), rename GetEncoder() to Encode()
|
||||
type Driver interface {
|
||||
Name() string
|
||||
Aliases() []string // alias format names, use for resolve format name
|
||||
GetDecoder() Decoder
|
||||
GetEncoder() Encoder
|
||||
}
|
||||
|
||||
// DriverV2 interface.
|
||||
type DriverV2 interface {
|
||||
Name() string // driver name, also is format name.
|
||||
Aliases() []string // alias format names, use for resolve format name
|
||||
Decode(blob []byte, v any) (err error)
|
||||
Encode(v any) (out []byte, err error)
|
||||
}
|
||||
|
||||
// Decoder for decode yml,json,toml format content
|
||||
type Decoder func(blob []byte, v any) (err error)
|
||||
|
||||
// Encoder for decode yml,json,toml format content
|
||||
type Encoder func(v any) (out []byte, err error)
|
||||
|
||||
// StdDriver struct
|
||||
type StdDriver struct {
|
||||
name string
|
||||
aliases []string
|
||||
decoder Decoder
|
||||
encoder Encoder
|
||||
}
|
||||
|
||||
// NewDriver new std driver instance.
|
||||
func NewDriver(name string, dec Decoder, enc Encoder) *StdDriver {
|
||||
return &StdDriver{name: name, decoder: dec, encoder: enc}
|
||||
}
|
||||
|
||||
// WithAliases set aliases for driver
|
||||
func (d *StdDriver) WithAliases(aliases ...string) *StdDriver {
|
||||
d.aliases = aliases
|
||||
return d
|
||||
}
|
||||
|
||||
// WithAlias add alias for driver
|
||||
func (d *StdDriver) WithAlias(alias string) *StdDriver {
|
||||
d.aliases = append(d.aliases, alias)
|
||||
return d
|
||||
}
|
||||
|
||||
// Name of driver
|
||||
func (d *StdDriver) Name() string { return d.name }
|
||||
|
||||
// Aliases format name of driver
|
||||
func (d *StdDriver) Aliases() []string {
|
||||
return d.aliases
|
||||
}
|
||||
|
||||
// Decode of driver
|
||||
func (d *StdDriver) Decode(blob []byte, v any) (err error) {
|
||||
return d.decoder(blob, v)
|
||||
}
|
||||
|
||||
// Encode of driver
|
||||
func (d *StdDriver) Encode(v any) ([]byte, error) {
|
||||
return d.encoder(v)
|
||||
}
|
||||
|
||||
// GetDecoder of driver
|
||||
func (d *StdDriver) GetDecoder() Decoder {
|
||||
return d.decoder
|
||||
}
|
||||
|
||||
// GetEncoder of driver
|
||||
func (d *StdDriver) GetEncoder() Encoder {
|
||||
return d.encoder
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* JSON driver
|
||||
*************************************************************/
|
||||
|
||||
var (
|
||||
// JSONAllowComments support write comments on json file.
|
||||
JSONAllowComments = true
|
||||
|
||||
// JSONMarshalIndent if not empty, will use json.MarshalIndent for encode data.
|
||||
//
|
||||
// Deprecated: please use JSONDriver.MarshalIndent
|
||||
JSONMarshalIndent string
|
||||
)
|
||||
|
||||
// JSONDecoder for json decode
|
||||
var JSONDecoder Decoder = func(data []byte, v any) (err error) {
|
||||
JSONDriver.ClearComments = JSONAllowComments
|
||||
return JSONDriver.Decode(data, v)
|
||||
}
|
||||
|
||||
// JSONEncoder for json encode
|
||||
var JSONEncoder Encoder = func(v any) (out []byte, err error) {
|
||||
JSONDriver.MarshalIndent = JSONMarshalIndent
|
||||
return JSONDriver.Encode(v)
|
||||
}
|
||||
|
||||
// JSONDriver instance fot json
|
||||
var JSONDriver = &jsonDriver{
|
||||
driverName: JSON,
|
||||
ClearComments: JSONAllowComments,
|
||||
MarshalIndent: JSONMarshalIndent,
|
||||
}
|
||||
|
||||
// jsonDriver for json format content
|
||||
type jsonDriver struct {
|
||||
driverName string
|
||||
// ClearComments before parse JSON string.
|
||||
ClearComments bool
|
||||
// MarshalIndent if not empty, will use json.MarshalIndent for encode data.
|
||||
MarshalIndent string
|
||||
}
|
||||
|
||||
// Name of the driver
|
||||
func (d *jsonDriver) Name() string {
|
||||
return d.driverName
|
||||
}
|
||||
|
||||
// Aliases of the driver
|
||||
func (d *jsonDriver) Aliases() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decode for the driver
|
||||
func (d *jsonDriver) Decode(data []byte, v any) error {
|
||||
if d.ClearComments {
|
||||
str := jsonutil.StripComments(string(data))
|
||||
return json.Unmarshal([]byte(str), v)
|
||||
}
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// GetDecoder for the driver
|
||||
func (d *jsonDriver) GetDecoder() Decoder {
|
||||
return d.Decode
|
||||
}
|
||||
|
||||
// Encode for the driver
|
||||
func (d *jsonDriver) Encode(v any) (out []byte, err error) {
|
||||
if len(d.MarshalIndent) > 0 {
|
||||
return json.MarshalIndent(v, "", d.MarshalIndent)
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// GetEncoder for the driver
|
||||
func (d *jsonDriver) GetEncoder() Encoder {
|
||||
return d.Encode
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/gookit/goutil/structs"
|
||||
)
|
||||
|
||||
// Decode all config data to the dst ptr
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// myConf := &MyConf{}
|
||||
// config.Decode(myConf)
|
||||
func Decode(dst any) error { return dc.Decode(dst) }
|
||||
|
||||
// Decode all config data to the dst ptr.
|
||||
//
|
||||
// It's equals:
|
||||
//
|
||||
// c.Structure("", dst)
|
||||
func (c *Config) Decode(dst any) error {
|
||||
return c.Structure("", dst)
|
||||
}
|
||||
|
||||
// MapStruct alias method of the 'Structure'
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// dbInfo := &Db{}
|
||||
// config.MapStruct("db", dbInfo)
|
||||
func MapStruct(key string, dst any) error { return dc.MapStruct(key, dst) }
|
||||
|
||||
// MapStruct alias method of the 'Structure'
|
||||
func (c *Config) MapStruct(key string, dst any) error { return c.Structure(key, dst) }
|
||||
|
||||
// BindStruct alias method of the 'Structure'
|
||||
func BindStruct(key string, dst any) error { return dc.BindStruct(key, dst) }
|
||||
|
||||
// BindStruct alias method of the 'Structure'
|
||||
func (c *Config) BindStruct(key string, dst any) error { return c.Structure(key, dst) }
|
||||
|
||||
// MapOnExists mapping data to the dst structure only on key exists.
|
||||
func MapOnExists(key string, dst any) error {
|
||||
return dc.MapOnExists(key, dst)
|
||||
}
|
||||
|
||||
// MapOnExists mapping data to the dst structure only on key exists.
|
||||
//
|
||||
// - Support ParseEnv on mapping
|
||||
// - Support ParseDefault on mapping
|
||||
func (c *Config) MapOnExists(key string, dst any) error {
|
||||
err := c.Structure(key, dst)
|
||||
if err != nil && err == ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Structure get config data and binding to the dst structure.
|
||||
//
|
||||
// - Support ParseEnv on mapping
|
||||
// - Support ParseDefault on mapping
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// dbInfo := Db{}
|
||||
// config.Structure("db", &dbInfo)
|
||||
func (c *Config) Structure(key string, dst any) (err error) {
|
||||
var data any
|
||||
// binding all data on key is empty.
|
||||
if key == "" {
|
||||
// fix: if c.data is nil, don't need to apply map structure
|
||||
if len(c.data) == 0 {
|
||||
// init default value by tag: default
|
||||
if c.opts.ParseDefault {
|
||||
err = structs.InitDefaults(dst, func(opt *structs.InitOptions) {
|
||||
opt.ParseEnv = c.opts.ParseEnv
|
||||
opt.ParseTime = c.opts.ParseTime // add ParseTime support on parse default value
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
data = c.data
|
||||
} else {
|
||||
// binding sub-data of the config
|
||||
var ok bool
|
||||
data, ok = c.GetValue(key)
|
||||
if !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
}
|
||||
|
||||
// map structure from data
|
||||
bindConf := c.opts.makeDecoderConfig()
|
||||
// set result struct ptr
|
||||
bindConf.Result = dst
|
||||
decoder, err := mapstructure.NewDecoder(bindConf)
|
||||
if err == nil {
|
||||
if err = decoder.Decode(data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// init default value by tag: default
|
||||
if c.opts.ParseDefault {
|
||||
err = structs.InitDefaults(dst, func(opt *structs.InitOptions) {
|
||||
opt.ParseEnv = c.opts.ParseEnv
|
||||
opt.ParseTime = c.opts.ParseTime
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ToJSON string, will ignore error
|
||||
func (c *Config) ToJSON() string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
_, err := c.DumpTo(buf, JSON)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// WriteTo a writer
|
||||
func WriteTo(out io.Writer) (int64, error) { return dc.WriteTo(out) }
|
||||
|
||||
// WriteTo Write out config data representing the current state to a writer.
|
||||
func (c *Config) WriteTo(out io.Writer) (n int64, err error) {
|
||||
return c.DumpTo(out, c.opts.DumpFormat)
|
||||
}
|
||||
|
||||
// DumpTo a writer and use format
|
||||
func DumpTo(out io.Writer, format string) (int64, error) { return dc.DumpTo(out, format) }
|
||||
|
||||
// DumpTo use the format(json,yaml,toml) dump config data to a writer
|
||||
func (c *Config) DumpTo(out io.Writer, format string) (n int64, err error) {
|
||||
var ok bool
|
||||
var encoder Encoder
|
||||
|
||||
format = c.resolveFormat(format)
|
||||
if encoder, ok = c.encoders[format]; !ok {
|
||||
err = errors.New("not exists/register encoder for the format: " + format)
|
||||
return
|
||||
}
|
||||
|
||||
// is empty
|
||||
if len(c.data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// encode data to string
|
||||
encoded, err := encoder(c.data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// write content to out
|
||||
num, _ := fmt.Fprintln(out, string(encoded))
|
||||
return int64(num), nil
|
||||
}
|
||||
|
||||
// DumpToFile use the format(json,yaml,toml) dump config data to a writer
|
||||
func (c *Config) DumpToFile(fileName string, format string) (err error) {
|
||||
fsFlags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
|
||||
f, err := os.OpenFile(fileName, fsFlags, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.DumpTo(f, format)
|
||||
if err1 := f.Close(); err1 != nil && err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/gookit/goutil/errorx"
|
||||
"github.com/gookit/goutil/fsutil"
|
||||
)
|
||||
|
||||
// LoadFiles load one or multi files, will fire OnLoadData event
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// config.LoadFiles(file1, file2, ...)
|
||||
func LoadFiles(sourceFiles ...string) error { return dc.LoadFiles(sourceFiles...) }
|
||||
|
||||
// LoadFiles load and parse config files, will fire OnLoadData event
|
||||
func (c *Config) LoadFiles(sourceFiles ...string) (err error) {
|
||||
for _, file := range sourceFiles {
|
||||
if err = c.loadFile(file, false, ""); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadExists load one or multi files, will ignore not exist
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// config.LoadExists(file1, file2, ...)
|
||||
func LoadExists(sourceFiles ...string) error { return dc.LoadExists(sourceFiles...) }
|
||||
|
||||
// LoadExists load and parse config files, but will ignore not exists file.
|
||||
func (c *Config) LoadExists(sourceFiles ...string) (err error) {
|
||||
for _, file := range sourceFiles {
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.loadFile(file, true, ""); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadRemote load config data from remote URL.
|
||||
func LoadRemote(format, url string) error { return dc.LoadRemote(format, url) }
|
||||
|
||||
// LoadRemote load config data from remote URL.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// c.LoadRemote(config.JSON, "http://abc.com/api-config.json")
|
||||
func (c *Config) LoadRemote(format, url string) (err error) {
|
||||
// create http client
|
||||
client := http.Client{Timeout: 300 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//noinspection GoUnhandledErrorResult
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("fetch remote config error, reply status code is %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// read response content
|
||||
bts, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
if err = c.parseSourceCode(format, bts); err != nil {
|
||||
return
|
||||
}
|
||||
c.loadedUrls = append(c.loadedUrls, url)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadOSEnv load data from OS ENV
|
||||
//
|
||||
// Deprecated: please use LoadOSEnvs()
|
||||
func LoadOSEnv(keys []string, keyToLower bool) { dc.LoadOSEnv(keys, keyToLower) }
|
||||
|
||||
// LoadOSEnv load data from os ENV
|
||||
//
|
||||
// Deprecated: please use Config.LoadOSEnvs()
|
||||
func (c *Config) LoadOSEnv(keys []string, keyToLower bool) {
|
||||
for _, key := range keys {
|
||||
// NOTICE: if is Windows os, os.Getenv() Key is not case-sensitive
|
||||
val := os.Getenv(key)
|
||||
if keyToLower {
|
||||
key = strings.ToLower(key)
|
||||
}
|
||||
_ = c.Set(key, val)
|
||||
}
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
|
||||
// LoadOSEnvs load data from OS ENVs. see Config.LoadOSEnvs
|
||||
func LoadOSEnvs(nameToKeyMap map[string]string) { dc.LoadOSEnvs(nameToKeyMap) }
|
||||
|
||||
// LoadOSEnvs load data from os ENVs. format: `{ENV_NAME: config_key}`
|
||||
//
|
||||
// - `config_key` allow use key path. eg: `{"DB_USERNAME": "db.username"}`
|
||||
func (c *Config) LoadOSEnvs(nameToKeyMap map[string]string) {
|
||||
for name, cfgKey := range nameToKeyMap {
|
||||
if val := os.Getenv(name); val != "" {
|
||||
if cfgKey == "" {
|
||||
cfgKey = strings.ToLower(name)
|
||||
}
|
||||
_ = c.Set(cfgKey, val)
|
||||
}
|
||||
}
|
||||
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
|
||||
// support bound types for CLI flags vars
|
||||
var validTypes = map[string]int{
|
||||
"int": 1,
|
||||
"uint": 1,
|
||||
"bool": 1,
|
||||
// string is default
|
||||
"string": 1,
|
||||
}
|
||||
|
||||
// LoadFlags load data from cli flags. see Config.LoadFlags
|
||||
func LoadFlags(defines []string) error { return dc.LoadFlags(defines) }
|
||||
|
||||
// LoadFlags parse command line arguments, based on provide keys.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // 'debug' flag is bool type
|
||||
// c.LoadFlags([]string{"env", "debug:bool"})
|
||||
// // can with flag desc message
|
||||
// c.LoadFlags([]string{"env:set the run env"})
|
||||
// c.LoadFlags([]string{"debug:bool:set debug mode"})
|
||||
// // can set value to map key. eg: myapp --map1.sub-key=val
|
||||
// c.LoadFlags([]string{"--map1.sub-key"})
|
||||
func (c *Config) LoadFlags(defines []string) (err error) {
|
||||
hash := map[string]int8{}
|
||||
|
||||
// bind vars
|
||||
for _, str := range defines {
|
||||
key, typ, desc := parseVarNameAndType(str)
|
||||
if desc == "" {
|
||||
desc = "config flag " + key
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case "int":
|
||||
ptr := new(int)
|
||||
flag.IntVar(ptr, key, c.Int(key), desc)
|
||||
hash[key] = 0
|
||||
case "uint":
|
||||
ptr := new(uint)
|
||||
flag.UintVar(ptr, key, c.Uint(key), desc)
|
||||
hash[key] = 0
|
||||
case "bool":
|
||||
ptr := new(bool)
|
||||
flag.BoolVar(ptr, key, c.Bool(key), desc)
|
||||
hash[key] = 0
|
||||
default: // as string
|
||||
ptr := new(string)
|
||||
flag.StringVar(ptr, key, c.String(key), desc)
|
||||
hash[key] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// parse and collect
|
||||
flag.Parse()
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
name := f.Name
|
||||
// only get name in the keys.
|
||||
if _, ok := hash[name]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// if f.Value implement the flag.Getter, read typed value
|
||||
if gtr, ok := f.Value.(flag.Getter); ok {
|
||||
_ = c.Set(name, gtr.Get())
|
||||
// } else { // TIP: basic type flag always implements Getter interface
|
||||
// _ = c.Set(name, f.Value.String()) // ignore error
|
||||
}
|
||||
})
|
||||
|
||||
c.fireHook(OnLoadData)
|
||||
return
|
||||
}
|
||||
|
||||
// LoadData load one or multi data
|
||||
func LoadData(dataSource ...any) error { return dc.LoadData(dataSource...) }
|
||||
|
||||
// LoadData load data from map OR struct
|
||||
//
|
||||
// The dataSources type allow:
|
||||
// - map[string]any
|
||||
// - map[string]string
|
||||
func (c *Config) LoadData(dataSources ...any) (err error) {
|
||||
if c.opts.Delimiter == 0 {
|
||||
c.opts.Delimiter = defaultDelimiter
|
||||
}
|
||||
|
||||
var loaded bool
|
||||
for _, ds := range dataSources {
|
||||
if smp, ok := ds.(map[string]string); ok {
|
||||
loaded = true
|
||||
c.LoadSMap(smp)
|
||||
continue
|
||||
}
|
||||
|
||||
err = mergo.Merge(&c.data, ds, c.opts.MergeOptions...)
|
||||
if err != nil {
|
||||
return errorx.WithStack(err)
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
|
||||
if loaded {
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadSMap to config
|
||||
func (c *Config) LoadSMap(smp map[string]string) {
|
||||
for k, v := range smp {
|
||||
c.data[k] = v
|
||||
}
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
|
||||
// LoadSources load one or multi byte data
|
||||
func LoadSources(format string, src []byte, more ...[]byte) error {
|
||||
return dc.LoadSources(format, src, more...)
|
||||
}
|
||||
|
||||
// LoadSources load data from byte content.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// config.LoadSources(config.Yaml, []byte(`
|
||||
// name: blog
|
||||
// arr:
|
||||
// key: val
|
||||
//
|
||||
// `))
|
||||
func (c *Config) LoadSources(format string, src []byte, more ...[]byte) (err error) {
|
||||
err = c.parseSourceCode(format, src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, sc := range more {
|
||||
err = c.parseSourceCode(format, sc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadStrings load one or multi string
|
||||
func LoadStrings(format string, str string, more ...string) error {
|
||||
return dc.LoadStrings(format, str, more...)
|
||||
}
|
||||
|
||||
// LoadStrings load data from source string content.
|
||||
func (c *Config) LoadStrings(format string, str string, more ...string) (err error) {
|
||||
err = c.parseSourceCode(format, []byte(str))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range more {
|
||||
err = c.parseSourceCode(format, []byte(s))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadFilesByFormat load one or multi config files by give format, will fire OnLoadData event
|
||||
func LoadFilesByFormat(format string, configFiles ...string) error {
|
||||
return dc.LoadFilesByFormat(format, configFiles...)
|
||||
}
|
||||
|
||||
// LoadFilesByFormat load one or multi files by give format, will fire OnLoadData event
|
||||
func (c *Config) LoadFilesByFormat(format string, configFiles ...string) (err error) {
|
||||
for _, file := range configFiles {
|
||||
if err = c.loadFile(file, false, format); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event
|
||||
func LoadExistsByFormat(format string, configFiles ...string) error {
|
||||
return dc.LoadExistsByFormat(format, configFiles...)
|
||||
}
|
||||
|
||||
// LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event
|
||||
func (c *Config) LoadExistsByFormat(format string, configFiles ...string) (err error) {
|
||||
for _, file := range configFiles {
|
||||
if err = c.loadFile(file, true, format); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadOptions for load config from dir.
|
||||
type LoadOptions struct {
|
||||
// DataKey use for load config from dir.
|
||||
// see https://github.com/gookit/config/issues/173
|
||||
DataKey string
|
||||
}
|
||||
|
||||
// LoadOptFn type func
|
||||
type LoadOptFn func(lo *LoadOptions)
|
||||
|
||||
func newLoadOptions(loFns []LoadOptFn) *LoadOptions {
|
||||
lo := &LoadOptions{}
|
||||
for _, fn := range loFns {
|
||||
fn(lo)
|
||||
}
|
||||
return lo
|
||||
}
|
||||
|
||||
// LoadFromDir Load custom format files from the given directory, the file name will be used as the key.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // file: /somedir/task.json
|
||||
// LoadFromDir("/somedir", "json")
|
||||
//
|
||||
// // after load
|
||||
// Config.data = map[string]any{"task": file data}
|
||||
func LoadFromDir(dirPath, format string, loFns ...LoadOptFn) error {
|
||||
return dc.LoadFromDir(dirPath, format, loFns...)
|
||||
}
|
||||
|
||||
// LoadFromDir Load custom format files from the given directory, the file name will be used as the key.
|
||||
//
|
||||
// NOTE: will not be reloaded on call ReloadFiles(), if data loaded by the method.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // file: /somedir/task.json , will use filename 'task' as key
|
||||
// Config.LoadFromDir("/somedir", "json")
|
||||
//
|
||||
// // after load, the data will be:
|
||||
// Config.data = map[string]any{"task": {file data}}
|
||||
func (c *Config) LoadFromDir(dirPath, format string, loFns ...LoadOptFn) (err error) {
|
||||
extName := "." + format
|
||||
extLen := len(extName)
|
||||
|
||||
lo := newLoadOptions(loFns)
|
||||
dirData := make(map[string]any)
|
||||
dataList := make([]map[string]any, 0, 8)
|
||||
|
||||
err = fsutil.FindInDir(dirPath, func(fPath string, ent fs.DirEntry) error {
|
||||
baseName := ent.Name()
|
||||
if strings.HasSuffix(baseName, extName) {
|
||||
data, err := c.parseSourceToMap(format, fsutil.MustReadFile(fPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// filename without ext.
|
||||
onlyName := baseName[:len(baseName)-extLen]
|
||||
if lo.DataKey != "" {
|
||||
dataList = append(dataList, data)
|
||||
} else {
|
||||
dirData[onlyName] = data
|
||||
}
|
||||
|
||||
// TODO use file name as key, it cannot be reloaded. So, cannot append to loadedFiles
|
||||
// c.loadedFiles = append(c.loadedFiles, fPath)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lo.DataKey != "" {
|
||||
dirData[lo.DataKey] = dataList
|
||||
}
|
||||
|
||||
if len(dirData) == 0 {
|
||||
return nil
|
||||
}
|
||||
return c.loadDataMap(dirData)
|
||||
}
|
||||
|
||||
// ReloadFiles reload config data use loaded files
|
||||
func ReloadFiles() error { return dc.ReloadFiles() }
|
||||
|
||||
// ReloadFiles reload config data use loaded files. use on watching loaded files change
|
||||
func (c *Config) ReloadFiles() (err error) {
|
||||
files := c.loadedFiles
|
||||
if len(files) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
data := c.Data()
|
||||
c.reloading = true
|
||||
c.ClearCaches()
|
||||
|
||||
defer func() {
|
||||
// revert to back up data on error
|
||||
if err != nil {
|
||||
c.data = data
|
||||
}
|
||||
|
||||
c.lock.Unlock()
|
||||
c.reloading = false
|
||||
|
||||
if err == nil {
|
||||
c.fireHook(OnReloadData)
|
||||
}
|
||||
}()
|
||||
|
||||
// with lock
|
||||
c.lock.Lock()
|
||||
|
||||
// reload config files
|
||||
return c.LoadFiles(files...)
|
||||
}
|
||||
|
||||
// load config file, will fire OnLoadData event
|
||||
func (c *Config) loadFile(file string, loadExist bool, format string) (err error) {
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
// skip not exist file
|
||||
if os.IsNotExist(err) && loadExist {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
//noinspection GoUnhandledErrorResult
|
||||
defer fd.Close()
|
||||
|
||||
// read file content
|
||||
bts, err := io.ReadAll(fd)
|
||||
if err == nil {
|
||||
// get format for file ext
|
||||
if format == "" {
|
||||
format = strings.Trim(filepath.Ext(file), ".")
|
||||
}
|
||||
|
||||
// parse file content
|
||||
if err = c.parseSourceCode(format, bts); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.reloading {
|
||||
c.loadedFiles = append(c.loadedFiles, file)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parse config source code to Config.
|
||||
func (c *Config) parseSourceCode(format string, blob []byte) (err error) {
|
||||
data, err := c.parseSourceToMap(format, blob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.loadDataMap(data)
|
||||
}
|
||||
|
||||
func (c *Config) loadDataMap(data map[string]any) (err error) {
|
||||
// first: init config data
|
||||
if len(c.data) == 0 {
|
||||
c.data = data
|
||||
} else {
|
||||
// again ... will merge data
|
||||
err = mergo.Merge(&c.data, data, c.opts.MergeOptions...)
|
||||
}
|
||||
|
||||
if !c.reloading && err == nil {
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// parse config source code to Config.
|
||||
func (c *Config) parseSourceToMap(format string, blob []byte) (map[string]any, error) {
|
||||
format = c.resolveFormat(format)
|
||||
decode := c.decoders[format]
|
||||
if decode == nil {
|
||||
return nil, errors.New("not register decoder for the format: " + format)
|
||||
}
|
||||
|
||||
if c.opts.Delimiter == 0 {
|
||||
c.opts.Delimiter = defaultDelimiter
|
||||
}
|
||||
|
||||
// decode content to data
|
||||
data := make(map[string]any)
|
||||
|
||||
if err := decode(blob, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/gookit/goutil"
|
||||
)
|
||||
|
||||
// there are some event names for config data changed.
|
||||
const (
|
||||
OnSetValue = "set.value"
|
||||
OnSetData = "set.data"
|
||||
OnLoadData = "load.data"
|
||||
OnReloadData = "reload.data"
|
||||
OnCleanData = "clean.data"
|
||||
)
|
||||
|
||||
// HookFunc on config data changed.
|
||||
type HookFunc func(event string, c *Config)
|
||||
|
||||
// Options config options
|
||||
type Options struct {
|
||||
// ParseEnv parse env in string value and default value. default: false
|
||||
//
|
||||
// - like: "${EnvName}" "${EnvName|default}"
|
||||
ParseEnv bool
|
||||
// ParseTime parses a duration string to `time.Duration`. default: false
|
||||
//
|
||||
// eg: 10s, 2m
|
||||
ParseTime bool
|
||||
// ParseDefault tag on binding data to struct. default: false
|
||||
//
|
||||
// - tag: default
|
||||
//
|
||||
// NOTE: If you want to parse a substruct, you need to set the `default:""` flag on the struct,
|
||||
// otherwise the fields that will not resolve to it will not be resolved.
|
||||
ParseDefault bool
|
||||
// Readonly config is readonly. default: false
|
||||
Readonly bool
|
||||
// EnableCache enable config data cache. default: false
|
||||
EnableCache bool
|
||||
// ParseKey support key path, allow finding value by key path. default: true
|
||||
//
|
||||
// - eg: 'key.sub' will find `map[key]sub`
|
||||
ParseKey bool
|
||||
// TagName tag name for binding data to struct
|
||||
//
|
||||
// Deprecated: please set tag name by DecoderConfig, or use SetTagName()
|
||||
TagName string
|
||||
// Delimiter the delimiter char for split key path, on `ParseKey=true`.
|
||||
//
|
||||
// - default is '.'
|
||||
Delimiter byte
|
||||
// DumpFormat default write format. default is 'json'
|
||||
DumpFormat string
|
||||
// ReadFormat default input format. default is 'json'
|
||||
ReadFormat string
|
||||
// DecoderConfig setting for binding data to struct. such as: TagName
|
||||
DecoderConfig *mapstructure.DecoderConfig
|
||||
// MergeOptions settings for merge two data
|
||||
MergeOptions []func(*mergo.Config)
|
||||
// HookFunc on data changed. you can do something...
|
||||
HookFunc HookFunc
|
||||
// WatchChange bool
|
||||
}
|
||||
|
||||
// OptionFn option func
|
||||
type OptionFn func(*Options)
|
||||
|
||||
func newDefaultOption() *Options {
|
||||
return &Options{
|
||||
ParseKey: true,
|
||||
TagName: defaultStructTag,
|
||||
Delimiter: defaultDelimiter,
|
||||
// for export
|
||||
DumpFormat: JSON,
|
||||
ReadFormat: JSON,
|
||||
// struct decoder config
|
||||
DecoderConfig: newDefaultDecoderConfig(""),
|
||||
MergeOptions: []func(*mergo.Config){
|
||||
mergo.WithOverride,
|
||||
mergo.WithTypeCheck,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newDefaultDecoderConfig(tagName string) *mapstructure.DecoderConfig {
|
||||
if tagName == "" {
|
||||
tagName = defaultStructTag
|
||||
}
|
||||
|
||||
return &mapstructure.DecoderConfig{
|
||||
// tag name for binding struct
|
||||
TagName: tagName,
|
||||
// will auto convert string to int/uint
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
}
|
||||
|
||||
// SetTagName for mapping data to struct
|
||||
func (o *Options) SetTagName(tagName string) {
|
||||
o.TagName = tagName
|
||||
o.DecoderConfig.TagName = tagName
|
||||
}
|
||||
|
||||
func (o *Options) shouldAddHookFunc() bool {
|
||||
return o.ParseTime || o.ParseEnv
|
||||
}
|
||||
|
||||
func (o *Options) makeDecoderConfig() *mapstructure.DecoderConfig {
|
||||
var bindConf *mapstructure.DecoderConfig
|
||||
if o.DecoderConfig == nil {
|
||||
bindConf = newDefaultDecoderConfig(o.TagName)
|
||||
} else {
|
||||
// copy new config for each binding.
|
||||
copyConf := *o.DecoderConfig
|
||||
bindConf = ©Conf
|
||||
|
||||
// compatible with previous settings opts.TagName
|
||||
if bindConf.TagName == "" {
|
||||
bindConf.TagName = o.TagName
|
||||
}
|
||||
}
|
||||
|
||||
// add hook on decode value to struct
|
||||
if bindConf.DecodeHook == nil && o.shouldAddHookFunc() {
|
||||
bindConf.DecodeHook = ValDecodeHookFunc(o.ParseEnv, o.ParseTime)
|
||||
}
|
||||
|
||||
return bindConf
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* config setting
|
||||
*************************************************************/
|
||||
|
||||
// WithTagName set tag name for export to struct
|
||||
func WithTagName(tagName string) func(*Options) {
|
||||
return func(opts *Options) {
|
||||
opts.SetTagName(tagName)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseEnv set parse env value
|
||||
func ParseEnv(opts *Options) { opts.ParseEnv = true }
|
||||
|
||||
// ParseTime set parse time string.
|
||||
func ParseTime(opts *Options) { opts.ParseTime = true }
|
||||
|
||||
// ParseDefault tag value on binding data to struct.
|
||||
func ParseDefault(opts *Options) { opts.ParseDefault = true }
|
||||
|
||||
// Readonly set readonly
|
||||
func Readonly(opts *Options) { opts.Readonly = true }
|
||||
|
||||
// Delimiter set delimiter char
|
||||
func Delimiter(sep byte) func(*Options) {
|
||||
return func(opts *Options) {
|
||||
opts.Delimiter = sep
|
||||
}
|
||||
}
|
||||
|
||||
// SaveFileOnSet set hook func, will panic on save error
|
||||
func SaveFileOnSet(fileName string, format string) func(options *Options) {
|
||||
return func(opts *Options) {
|
||||
opts.HookFunc = func(event string, c *Config) {
|
||||
if strings.HasPrefix(event, "set.") {
|
||||
goutil.PanicErr(c.DumpToFile(fileName, format))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHookFunc set hook func
|
||||
func WithHookFunc(fn HookFunc) func(*Options) {
|
||||
return func(opts *Options) {
|
||||
opts.HookFunc = fn
|
||||
}
|
||||
}
|
||||
|
||||
// EnableCache set readonly
|
||||
func EnableCache(opts *Options) { opts.EnableCache = true }
|
||||
|
||||
// WithOptions with options
|
||||
func WithOptions(opts ...OptionFn) { dc.WithOptions(opts...) }
|
||||
|
||||
// WithOptions apply some options
|
||||
func (c *Config) WithOptions(opts ...OptionFn) *Config {
|
||||
if !c.IsEmpty() {
|
||||
panic("config: Cannot set options after data has been loaded")
|
||||
}
|
||||
|
||||
// apply options
|
||||
for _, opt := range opts {
|
||||
opt(c.opts)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// GetOptions get options
|
||||
func GetOptions() *Options { return dc.Options() }
|
||||
|
||||
// Options get
|
||||
func (c *Config) Options() *Options {
|
||||
return c.opts
|
||||
}
|
||||
|
||||
// With apply some options
|
||||
func (c *Config) With(fn func(c *Config)) *Config {
|
||||
fn(c)
|
||||
return c
|
||||
}
|
||||
|
||||
// Readonly disable set data to config.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// config.LoadFiles(a, b, c)
|
||||
// config.Readonly()
|
||||
func (c *Config) Readonly() {
|
||||
c.opts.Readonly = true
|
||||
}
|
||||
+664
@@ -0,0 +1,664 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/goutil/envutil"
|
||||
"github.com/gookit/goutil/maputil"
|
||||
"github.com/gookit/goutil/mathutil"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// Exists key exists check
|
||||
func Exists(key string, findByPath ...bool) bool { return dc.Exists(key, findByPath...) }
|
||||
|
||||
// Exists key exists check
|
||||
func (c *Config) Exists(key string, findByPath ...bool) (ok bool) {
|
||||
sep := c.opts.Delimiter
|
||||
if key = formatKey(key, string(sep)); key == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok = c.data[key]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
// disable find by path.
|
||||
if len(findByPath) > 0 && !findByPath[0] {
|
||||
return
|
||||
}
|
||||
|
||||
// has sub key? eg. "lang.dir"
|
||||
if strings.IndexByte(key, sep) == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
keys := strings.Split(key, string(sep))
|
||||
topK := keys[0]
|
||||
|
||||
// find top item data based on top key
|
||||
var item any
|
||||
if item, ok = c.data[topK]; !ok {
|
||||
return
|
||||
}
|
||||
for _, k := range keys[1:] {
|
||||
switch typeData := item.(type) {
|
||||
case map[string]int: // is map(from Set)
|
||||
if item, ok = typeData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case map[string]string: // is map(from Set)
|
||||
if item, ok = typeData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case map[string]any: // is map(decode from toml/json/yaml.v3)
|
||||
if item, ok = typeData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case map[any]any: // is map(decode from yaml.v2)
|
||||
if item, ok = typeData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case []int: // is array(is from Set)
|
||||
i, err := strconv.Atoi(k)
|
||||
|
||||
// check slice index
|
||||
if err != nil || len(typeData) < i {
|
||||
return false
|
||||
}
|
||||
case []string: // is array(is from Set)
|
||||
i, err := strconv.Atoi(k)
|
||||
if err != nil || len(typeData) < i {
|
||||
return false
|
||||
}
|
||||
case []any: // is array(load from file)
|
||||
i, err := strconv.Atoi(k)
|
||||
if err != nil || len(typeData) < i {
|
||||
return false
|
||||
}
|
||||
default: // error
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* read config data
|
||||
*************************************************************/
|
||||
|
||||
// Data return all config data
|
||||
func Data() map[string]any { return dc.Data() }
|
||||
|
||||
// Data get all config data.
|
||||
//
|
||||
// Note: will don't apply any options, like ParseEnv
|
||||
func (c *Config) Data() map[string]any {
|
||||
return c.data
|
||||
}
|
||||
|
||||
// Sub return a map config data by key
|
||||
func Sub(key string) map[string]any { return dc.Sub(key) }
|
||||
|
||||
// Sub get a map config data by key
|
||||
//
|
||||
// Note: will don't apply any options, like ParseEnv
|
||||
func (c *Config) Sub(key string) map[string]any {
|
||||
if mp, ok := c.GetValue(key); ok {
|
||||
if mmp, ok := mp.(map[string]any); ok {
|
||||
return mmp
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keys return all config data
|
||||
func Keys() []string { return dc.Keys() }
|
||||
|
||||
// Keys get all config data
|
||||
func (c *Config) Keys() []string {
|
||||
keys := make([]string, 0, len(c.data))
|
||||
for key := range c.data {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Get config value by key string, support get sub-value by key path(eg. 'map.key'),
|
||||
func Get(key string, findByPath ...bool) any { return dc.Get(key, findByPath...) }
|
||||
|
||||
// Get config value by key, findByPath default is true.
|
||||
func (c *Config) Get(key string, findByPath ...bool) any {
|
||||
val, _ := c.GetValue(key, findByPath...)
|
||||
return val
|
||||
}
|
||||
|
||||
// GetValue get value by given key string. findByPath default is true.
|
||||
func GetValue(key string, findByPath ...bool) (any, bool) {
|
||||
return dc.GetValue(key, findByPath...)
|
||||
}
|
||||
|
||||
// GetValue get value by given key string. findByPath default is true.
|
||||
//
|
||||
// Return:
|
||||
// - ok is true, find value from config
|
||||
// - ok is false, not found or error
|
||||
func (c *Config) GetValue(key string, findByPath ...bool) (value any, ok bool) {
|
||||
sep := c.opts.Delimiter
|
||||
if key = formatKey(key, string(sep)); key == "" {
|
||||
c.addError(ErrKeyIsEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
// if not is readonly
|
||||
if !c.opts.Readonly {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
}
|
||||
|
||||
// is top key
|
||||
if value, ok = c.data[key]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
// disable find by path.
|
||||
if len(findByPath) > 0 && !findByPath[0] {
|
||||
// c.addError(ErrNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// has sub key? eg. "lang.dir"
|
||||
if strings.IndexByte(key, sep) == -1 {
|
||||
// c.addError(ErrNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
keys := strings.Split(key, string(sep))
|
||||
topK := keys[0]
|
||||
|
||||
// find top item data based on top key
|
||||
var item any
|
||||
if item, ok = c.data[topK]; !ok {
|
||||
// c.addError(ErrNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// find child
|
||||
// NOTICE: don't merge case, will result in an error.
|
||||
// e.g. case []int, []string
|
||||
// OR
|
||||
// case []int:
|
||||
// case []string:
|
||||
for _, k := range keys[1:] {
|
||||
switch typeData := item.(type) {
|
||||
case map[string]int: // is map(from Set)
|
||||
if item, ok = typeData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case map[string]string: // is map(from Set)
|
||||
if item, ok = typeData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case map[string]any: // is map(decode from toml/json)
|
||||
if item, ok = typeData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case map[any]any: // is map(decode from yaml)
|
||||
if item, ok = typeData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case []int: // is array(is from Set)
|
||||
i, err := strconv.Atoi(k)
|
||||
|
||||
// check slice index
|
||||
if err != nil || len(typeData) < i {
|
||||
ok = false
|
||||
c.addError(err)
|
||||
return
|
||||
}
|
||||
|
||||
item = typeData[i]
|
||||
case []string: // is array(is from Set)
|
||||
i, err := strconv.Atoi(k)
|
||||
if err != nil || len(typeData) < i {
|
||||
ok = false
|
||||
c.addError(err)
|
||||
return
|
||||
}
|
||||
|
||||
item = typeData[i]
|
||||
case []any: // is array(load from file)
|
||||
i, err := strconv.Atoi(k)
|
||||
if err != nil || len(typeData) < i {
|
||||
ok = false
|
||||
c.addError(err)
|
||||
return
|
||||
}
|
||||
|
||||
item = typeData[i]
|
||||
default: // error
|
||||
ok = false
|
||||
c.addErrorf("cannot get value of the key '%s'", key)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return item, true
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* read config (basic data type)
|
||||
*************************************************************/
|
||||
|
||||
// String get a string by key
|
||||
func String(key string, defVal ...string) string { return dc.String(key, defVal...) }
|
||||
|
||||
// String get a string by key, if not found return default value
|
||||
func (c *Config) String(key string, defVal ...string) string {
|
||||
value, ok := c.getString(key)
|
||||
|
||||
if !ok && len(defVal) > 0 { // give default value
|
||||
value = defVal[0]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// MustString get a string by key, will panic on empty or not exists
|
||||
func MustString(key string) string { return dc.MustString(key) }
|
||||
|
||||
// MustString get a string by key, will panic on empty or not exists
|
||||
func (c *Config) MustString(key string) string {
|
||||
value, ok := c.getString(key)
|
||||
if !ok {
|
||||
panic("config: string value not found, key: " + key)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *Config) getString(key string) (value string, ok bool) {
|
||||
// find from cache
|
||||
if c.opts.EnableCache && len(c.strCache) > 0 {
|
||||
value, ok = c.strCache[key]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val, ok := c.GetValue(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch typVal := val.(type) {
|
||||
// from json `int` always is float64
|
||||
case string:
|
||||
value = typVal
|
||||
if c.opts.ParseEnv {
|
||||
value = envutil.ParseEnvValue(value)
|
||||
}
|
||||
default:
|
||||
var err error
|
||||
value, err = strutil.AnyToString(val, false)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// add cache
|
||||
if ok && c.opts.EnableCache {
|
||||
if c.strCache == nil {
|
||||
c.strCache = make(map[string]string)
|
||||
}
|
||||
c.strCache[key] = value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Int get an int by key
|
||||
func Int(key string, defVal ...int) int { return dc.Int(key, defVal...) }
|
||||
|
||||
// Int get a int value, if not found return default value
|
||||
func (c *Config) Int(key string, defVal ...int) (value int) {
|
||||
i64, exist := c.tryInt64(key)
|
||||
|
||||
if exist {
|
||||
value = int(i64)
|
||||
} else if len(defVal) > 0 {
|
||||
value = defVal[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Uint get a uint value, if not found return default value
|
||||
func Uint(key string, defVal ...uint) uint { return dc.Uint(key, defVal...) }
|
||||
|
||||
// Uint get a int value, if not found return default value
|
||||
func (c *Config) Uint(key string, defVal ...uint) (value uint) {
|
||||
i64, exist := c.tryInt64(key)
|
||||
|
||||
if exist {
|
||||
value = uint(i64)
|
||||
} else if len(defVal) > 0 {
|
||||
value = defVal[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Int64 get a int value, if not found return default value
|
||||
func Int64(key string, defVal ...int64) int64 { return dc.Int64(key, defVal...) }
|
||||
|
||||
// Int64 get a int value, if not found return default value
|
||||
func (c *Config) Int64(key string, defVal ...int64) (value int64) {
|
||||
value, exist := c.tryInt64(key)
|
||||
|
||||
if !exist && len(defVal) > 0 {
|
||||
value = defVal[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// try to get an int64 value by given key
|
||||
func (c *Config) tryInt64(key string) (value int64, ok bool) {
|
||||
strVal, ok := c.getString(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
value, err := strconv.ParseInt(strVal, 10, 0)
|
||||
if err != nil {
|
||||
c.addError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Duration get a time.Duration type value. if not found return default value
|
||||
func Duration(key string, defVal ...time.Duration) time.Duration { return dc.Duration(key, defVal...) }
|
||||
|
||||
// Duration get a time.Duration type value. if not found return default value
|
||||
func (c *Config) Duration(key string, defVal ...time.Duration) time.Duration {
|
||||
value, exist := c.tryInt64(key)
|
||||
|
||||
if !exist && len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
return time.Duration(value)
|
||||
}
|
||||
|
||||
// Float get a float64 value, if not found return default value
|
||||
func Float(key string, defVal ...float64) float64 { return dc.Float(key, defVal...) }
|
||||
|
||||
// Float get a float64 by key
|
||||
func (c *Config) Float(key string, defVal ...float64) (value float64) {
|
||||
str, ok := c.getString(key)
|
||||
if !ok {
|
||||
if len(defVal) > 0 {
|
||||
value = defVal[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
value, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
c.addError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Bool get a bool value, if not found return default value
|
||||
func Bool(key string, defVal ...bool) bool { return dc.Bool(key, defVal...) }
|
||||
|
||||
// Bool looks up a value for a key in this section and attempts to parse that value as a boolean,
|
||||
// along with a boolean result similar to a map lookup.
|
||||
//
|
||||
// of following(case insensitive):
|
||||
// - true
|
||||
// - yes
|
||||
// - false
|
||||
// - no
|
||||
// - 1
|
||||
// - 0
|
||||
//
|
||||
// The `ok` boolean will be false in the event that the value could not be parsed as a bool
|
||||
func (c *Config) Bool(key string, defVal ...bool) (value bool) {
|
||||
rawVal, ok := c.getString(key)
|
||||
if !ok {
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lowerCase := strings.ToLower(rawVal)
|
||||
switch lowerCase {
|
||||
case "", "0", "false", "no":
|
||||
value = false
|
||||
case "1", "true", "yes":
|
||||
value = true
|
||||
default:
|
||||
c.addErrorf("the value '%s' cannot be convert to bool", lowerCase)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* read config (complex data type)
|
||||
*************************************************************/
|
||||
|
||||
// Ints get config data as an int slice/array
|
||||
func Ints(key string) []int { return dc.Ints(key) }
|
||||
|
||||
// Ints get config data as an int slice/array
|
||||
func (c *Config) Ints(key string) (arr []int) {
|
||||
rawVal, ok := c.GetValue(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch typeData := rawVal.(type) {
|
||||
case []int:
|
||||
arr = typeData
|
||||
case []any:
|
||||
for _, v := range typeData {
|
||||
iv, err := mathutil.ToInt(v)
|
||||
// iv, err := strconv.Atoi(fmt.Sprintf("%v", v))
|
||||
if err != nil {
|
||||
c.addError(err)
|
||||
arr = arr[0:0] // reset
|
||||
return
|
||||
}
|
||||
|
||||
arr = append(arr, iv)
|
||||
}
|
||||
default:
|
||||
c.addErrorf("value cannot be convert to []int, key is '%s'", key)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IntMap get config data as a map[string]int
|
||||
func IntMap(key string) map[string]int { return dc.IntMap(key) }
|
||||
|
||||
// IntMap get config data as a map[string]int
|
||||
func (c *Config) IntMap(key string) (mp map[string]int) {
|
||||
rawVal, ok := c.GetValue(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch typeData := rawVal.(type) {
|
||||
case map[string]int: // from Set
|
||||
mp = typeData
|
||||
case map[string]any: // decode from json,toml
|
||||
mp = make(map[string]int)
|
||||
for k, v := range typeData {
|
||||
// iv, err := strconv.Atoi(fmt.Sprintf("%v", v))
|
||||
iv, err := mathutil.ToInt(v)
|
||||
if err != nil {
|
||||
c.addError(err)
|
||||
mp = map[string]int{} // reset
|
||||
return
|
||||
}
|
||||
mp[k] = iv
|
||||
}
|
||||
case map[any]any: // if decode from yaml
|
||||
mp = make(map[string]int)
|
||||
for k, v := range typeData {
|
||||
// iv, err := strconv.Atoi(fmt.Sprintf( "%v", v))
|
||||
iv, err := mathutil.ToInt(v)
|
||||
if err != nil {
|
||||
c.addError(err)
|
||||
mp = map[string]int{} // reset
|
||||
return
|
||||
}
|
||||
|
||||
// sk := fmt.Sprintf("%v", k)
|
||||
sk, _ := strutil.AnyToString(k, false)
|
||||
mp[sk] = iv
|
||||
}
|
||||
default:
|
||||
c.addErrorf("value cannot be convert to map[string]int, key is '%s'", key)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Strings get strings by key
|
||||
func Strings(key string) []string { return dc.Strings(key) }
|
||||
|
||||
// Strings get config data as a string slice/array
|
||||
func (c *Config) Strings(key string) (arr []string) {
|
||||
var ok bool
|
||||
// find from cache
|
||||
if c.opts.EnableCache && len(c.sArrCache) > 0 {
|
||||
arr, ok = c.sArrCache[key]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rawVal, ok := c.GetValue(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch typeData := rawVal.(type) {
|
||||
case []string:
|
||||
arr = typeData
|
||||
case []any:
|
||||
for _, v := range typeData {
|
||||
// arr = append(arr, fmt.Sprintf("%v", v))
|
||||
arr = append(arr, strutil.MustString(v))
|
||||
}
|
||||
default:
|
||||
c.addErrorf("value cannot be convert to []string, key is '%s'", key)
|
||||
return
|
||||
}
|
||||
|
||||
// add cache
|
||||
if c.opts.EnableCache {
|
||||
if c.sArrCache == nil {
|
||||
c.sArrCache = make(map[string]strArr)
|
||||
}
|
||||
c.sArrCache[key] = arr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StringsBySplit get []string by split a string value.
|
||||
func StringsBySplit(key, sep string) []string { return dc.StringsBySplit(key, sep) }
|
||||
|
||||
// StringsBySplit get []string by split a string value.
|
||||
func (c *Config) StringsBySplit(key, sep string) (ss []string) {
|
||||
if str, ok := c.getString(key); ok {
|
||||
ss = strutil.Split(str, sep)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StringMap get config data as a map[string]string
|
||||
func StringMap(key string) map[string]string { return dc.StringMap(key) }
|
||||
|
||||
// StringMap get config data as a map[string]string
|
||||
func (c *Config) StringMap(key string) (mp map[string]string) {
|
||||
var ok bool
|
||||
|
||||
// find from cache
|
||||
if c.opts.EnableCache && len(c.sMapCache) > 0 {
|
||||
mp, ok = c.sMapCache[key]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rawVal, ok := c.GetValue(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch typeData := rawVal.(type) {
|
||||
case map[string]string: // from Set
|
||||
mp = typeData
|
||||
case map[string]any: // decode from json,toml,yaml.v3
|
||||
mp = make(map[string]string, len(typeData))
|
||||
|
||||
for k, v := range typeData {
|
||||
switch tv := v.(type) {
|
||||
case string:
|
||||
if c.opts.ParseEnv {
|
||||
mp[k] = envutil.ParseEnvValue(tv)
|
||||
} else {
|
||||
mp[k] = tv
|
||||
}
|
||||
default:
|
||||
mp[k] = strutil.QuietString(v)
|
||||
}
|
||||
}
|
||||
case map[any]any: // decode from yaml v2
|
||||
mp = make(map[string]string, len(typeData))
|
||||
|
||||
for k, v := range typeData {
|
||||
sk := strutil.QuietString(k)
|
||||
|
||||
switch typVal := v.(type) {
|
||||
case string:
|
||||
if c.opts.ParseEnv {
|
||||
mp[sk] = envutil.ParseEnvValue(typVal)
|
||||
} else {
|
||||
mp[sk] = typVal
|
||||
}
|
||||
default:
|
||||
mp[sk] = strutil.QuietString(v)
|
||||
}
|
||||
}
|
||||
default:
|
||||
c.addErrorf("value cannot be convert to map[string]string, key is %q", key)
|
||||
return
|
||||
}
|
||||
|
||||
// add cache
|
||||
if c.opts.EnableCache {
|
||||
if c.sMapCache == nil {
|
||||
c.sMapCache = make(map[string]strMap)
|
||||
}
|
||||
c.sMapCache[key] = mp
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SubDataMap get sub config data as maputil.Map
|
||||
func SubDataMap(key string) maputil.Map { return dc.SubDataMap(key) }
|
||||
|
||||
// SubDataMap get sub config data as maputil.Map
|
||||
//
|
||||
// TIP: will not enable parse Env and more
|
||||
func (c *Config) SubDataMap(key string) maputil.Map {
|
||||
if mp, ok := c.GetValue(key); ok {
|
||||
if mmp, ok := mp.(map[string]any); ok {
|
||||
return mmp
|
||||
}
|
||||
}
|
||||
|
||||
// keep is not nil
|
||||
return maputil.Map{}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/gookit/goutil/envutil"
|
||||
"github.com/gookit/goutil/reflects"
|
||||
)
|
||||
|
||||
// ValDecodeHookFunc returns a mapstructure.DecodeHookFunc
|
||||
// that parse ENV var, and more custom parse
|
||||
func ValDecodeHookFunc(parseEnv, parseTime bool) mapstructure.DecodeHookFunc {
|
||||
return func(f reflect.Type, t reflect.Type, data any) (any, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
str := data.(string)
|
||||
if parseEnv {
|
||||
// https://docs.docker.com/compose/environment-variables/env-file/
|
||||
str, err = envutil.ParseOrErr(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(str) < 2 {
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// feat: support parse time or duration string. eg: 10s
|
||||
if parseTime && str[0] > '0' && str[0] <= '9' {
|
||||
return reflects.ToTimeOrDuration(str, t)
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
}
|
||||
|
||||
// resolve format, check is alias
|
||||
func (c *Config) resolveFormat(f string) string {
|
||||
if name, ok := c.aliasMap[f]; ok {
|
||||
return name
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* Deprecated methods
|
||||
*************************************************************/
|
||||
|
||||
// SetDecoder add/set a format decoder
|
||||
//
|
||||
// Deprecated: please use driver instead
|
||||
func SetDecoder(format string, decoder Decoder) {
|
||||
dc.SetDecoder(format, decoder)
|
||||
}
|
||||
|
||||
// SetDecoder set decoder
|
||||
//
|
||||
// Deprecated: please use driver instead
|
||||
func (c *Config) SetDecoder(format string, decoder Decoder) {
|
||||
format = c.resolveFormat(format)
|
||||
c.decoders[format] = decoder
|
||||
}
|
||||
|
||||
// SetDecoders set decoders
|
||||
//
|
||||
// Deprecated: please use driver instead
|
||||
func (c *Config) SetDecoders(decoders map[string]Decoder) {
|
||||
for format, decoder := range decoders {
|
||||
c.SetDecoder(format, decoder)
|
||||
}
|
||||
}
|
||||
|
||||
// SetEncoder set a encoder for the format
|
||||
//
|
||||
// Deprecated: please use driver instead
|
||||
func SetEncoder(format string, encoder Encoder) {
|
||||
dc.SetEncoder(format, encoder)
|
||||
}
|
||||
|
||||
// SetEncoder set a encoder for the format
|
||||
//
|
||||
// Deprecated: please use driver instead
|
||||
func (c *Config) SetEncoder(format string, encoder Encoder) {
|
||||
format = c.resolveFormat(format)
|
||||
c.encoders[format] = encoder
|
||||
}
|
||||
|
||||
// SetEncoders set encoders
|
||||
//
|
||||
// Deprecated: please use driver instead
|
||||
func (c *Config) SetEncoders(encoders map[string]Encoder) {
|
||||
for format, encoder := range encoders {
|
||||
c.SetEncoder(format, encoder)
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* helper methods/functions
|
||||
*************************************************************/
|
||||
|
||||
// LoadENVFiles load
|
||||
// func LoadENVFiles(filePaths ...string) error {
|
||||
// return dotenv.LoadFiles(filePaths...)
|
||||
// }
|
||||
|
||||
// GetEnv get os ENV value by name
|
||||
func GetEnv(name string, defVal ...string) (val string) {
|
||||
return Getenv(name, defVal...)
|
||||
}
|
||||
|
||||
// Getenv get os ENV value by name. like os.Getenv, but support default value
|
||||
//
|
||||
// Notice:
|
||||
// - Key is not case-sensitive when getting
|
||||
func Getenv(name string, defVal ...string) (val string) {
|
||||
if val = os.Getenv(name); val != "" {
|
||||
return
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
val = defVal[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseVarNameAndType(key string) (string, string, string) {
|
||||
var desc string
|
||||
typ := "string"
|
||||
key = strings.Trim(key, "-")
|
||||
|
||||
// can set var type: int, uint, bool
|
||||
if strings.IndexByte(key, ':') > 0 {
|
||||
list := strings.SplitN(key, ":", 3)
|
||||
key, typ = list[0], list[1]
|
||||
if len(list) == 3 {
|
||||
desc = list[2]
|
||||
}
|
||||
|
||||
// if type is not valid and has multi words, as desc message.
|
||||
if _, ok := validTypes[typ]; !ok {
|
||||
if desc == "" && strings.ContainsRune(typ, ' ') {
|
||||
desc = typ
|
||||
}
|
||||
typ = "string"
|
||||
}
|
||||
}
|
||||
return key, typ, desc
|
||||
}
|
||||
|
||||
// format key
|
||||
func formatKey(key, sep string) string {
|
||||
return strings.Trim(strings.TrimSpace(key), sep)
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/maputil"
|
||||
)
|
||||
|
||||
// some common errors definitions
|
||||
var (
|
||||
ErrReadonly = errors.New("the config instance in 'readonly' mode")
|
||||
ErrKeyIsEmpty = errors.New("the config key is cannot be empty")
|
||||
ErrNotFound = errors.New("this key does not exist in the config")
|
||||
)
|
||||
|
||||
// SetData for override the Config.Data
|
||||
func SetData(data map[string]any) {
|
||||
dc.SetData(data)
|
||||
}
|
||||
|
||||
// SetData for override the Config.Data
|
||||
func (c *Config) SetData(data map[string]any) {
|
||||
c.lock.Lock()
|
||||
c.data = data
|
||||
c.lock.Unlock()
|
||||
|
||||
c.fireHook(OnSetData)
|
||||
}
|
||||
|
||||
// Set value by key. setByPath default is true
|
||||
func Set(key string, val any, setByPath ...bool) error {
|
||||
return dc.Set(key, val, setByPath...)
|
||||
}
|
||||
|
||||
// Set a value by key string. setByPath default is true
|
||||
func (c *Config) Set(key string, val any, setByPath ...bool) (err error) {
|
||||
if c.opts.Readonly {
|
||||
return ErrReadonly
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
sep := c.opts.Delimiter
|
||||
if key = formatKey(key, string(sep)); key == "" {
|
||||
return ErrKeyIsEmpty
|
||||
}
|
||||
|
||||
defer c.fireHook(OnSetValue)
|
||||
if strings.IndexByte(key, sep) == -1 {
|
||||
c.data[key] = val
|
||||
return
|
||||
}
|
||||
|
||||
// disable set by path.
|
||||
if len(setByPath) > 0 && !setByPath[0] {
|
||||
c.data[key] = val
|
||||
return
|
||||
}
|
||||
|
||||
// set by path
|
||||
keys := strings.Split(key, string(sep))
|
||||
return maputil.SetByKeys(&c.data, keys, val)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Package yaml is a driver use YAML format content as config source
|
||||
|
||||
Usage please see example:
|
||||
*/
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gookit/config/v2"
|
||||
)
|
||||
|
||||
// Decoder the yaml content decoder
|
||||
var Decoder config.Decoder = yaml.Unmarshal
|
||||
|
||||
// Encoder the yaml content encoder
|
||||
var Encoder config.Encoder = yaml.Marshal
|
||||
|
||||
// Driver for yaml
|
||||
var Driver = config.NewDriver(config.Yaml, Decoder, Encoder).WithAliases(config.Yml)
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
*.log
|
||||
*.swp
|
||||
.idea
|
||||
.vscode/
|
||||
*.patch
|
||||
### Go template
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
*.cov
|
||||
.DS_Store
|
||||
.xenv.toml
|
||||
|
||||
*~
|
||||
.claude/
|
||||
testdata/
|
||||
vendor/
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 inhere
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# link https://github.com/humbug/box/blob/master/Makefile
|
||||
#SHELL = /bin/sh
|
||||
.DEFAULT_GOAL := help
|
||||
# 每行命令之前必须有一个tab键。如果想用其他键,可以用内置变量.RECIPEPREFIX 声明
|
||||
# mac 下这条声明 没起作用 !!
|
||||
#.RECIPEPREFIX = >
|
||||
.PHONY: all usage help clean
|
||||
|
||||
# 需要注意的是,每行命令在一个单独的shell中执行。这些Shell之间没有继承关系。
|
||||
# - 解决办法是将两行命令写在一行,中间用分号分隔。
|
||||
# - 或者在换行符前加反斜杠转义 \
|
||||
|
||||
# 接收命令行传入参数 make COMMAND tag=v2.0.4
|
||||
# TAG=$(tag)
|
||||
|
||||
##There some make command for the project
|
||||
##
|
||||
|
||||
help:
|
||||
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' | sed -e 's/: / /'
|
||||
|
||||
##Available Commands:
|
||||
|
||||
readme: ## Generate or update README file by ./internal/gendoc
|
||||
readme:
|
||||
go run ./internal/gendoc -o README.md
|
||||
go run ./internal/gendoc -o README.zh-CN.md -l zh-CN
|
||||
|
||||
readme-c: ## Generate or update README file and commit change to git
|
||||
readme-c: readme
|
||||
git add README.* internal
|
||||
git commit -m ":memo: doc: update and re-generate README docs"
|
||||
|
||||
csfix: ## Fix code style for all files by go fmt
|
||||
csfix:
|
||||
go fmt ./...
|
||||
|
||||
csdiff: ## Display code style error files by gofmt
|
||||
csdiff:
|
||||
gofmt -l ./
|
||||
+1886
File diff suppressed because it is too large
Load Diff
+1874
File diff suppressed because it is too large
Load Diff
+85
@@ -0,0 +1,85 @@
|
||||
# Array/Slice Utils
|
||||
|
||||
## Install
|
||||
|
||||
```shell
|
||||
go get github.com/gookit/goutil/arrutil
|
||||
```
|
||||
|
||||
## Go docs
|
||||
|
||||
- [Go docs](https://pkg.go.dev/github.com/gookit/goutil/arrutil)
|
||||
|
||||
## Functions API
|
||||
|
||||
> **Note**: doc by run `go doc ./arrutil`
|
||||
|
||||
```go
|
||||
func AnyToString(arr any) string
|
||||
func CloneSlice(data any) interface{}
|
||||
func Contains(arr, val any) bool
|
||||
func ExceptWhile(data any, fn Predicate) interface{}
|
||||
func Excepts(first, second any, fn Comparer) interface{}
|
||||
func Find(source any, fn Predicate) (interface{}, error)
|
||||
func FindOrDefault(source any, fn Predicate, defaultValue any) interface{}
|
||||
func FormatIndent(arr any, indent string) string
|
||||
func GetRandomOne(arr any) interface{}
|
||||
func HasValue(arr, val any) bool
|
||||
func InStrings(elem string, ss []string) bool
|
||||
func Int64sHas(ints []int64, val int64) bool
|
||||
func Intersects(first any, second any, fn Comparer) interface{}
|
||||
func IntsHas(ints []int, val int) bool
|
||||
func JoinSlice(sep string, arr ...any) string
|
||||
func JoinStrings(sep string, ss ...string) string
|
||||
func MakeEmptySlice(itemType reflect.Type) interface{}
|
||||
func Map[T any, V any](list []T, mapFn func(obj T) (val V, find bool)) []V
|
||||
func Column[T any, V any](list []T, mapFn func(obj T) (val V, find bool)) []V
|
||||
func MustToInt64s(arr any) []int64
|
||||
func MustToStrings(arr any) []string
|
||||
func NotContains(arr, val any) bool
|
||||
func RandomOne(arr any) interface{}
|
||||
func Reverse(ss []string)
|
||||
func SliceToInt64s(arr []any) []int64
|
||||
func SliceToString(arr ...any) string
|
||||
func SliceToStrings(arr []any) []string
|
||||
func StringsFilter(ss []string, filter ...func(s string) bool) []string
|
||||
func StringsHas(ss []string, val string) bool
|
||||
func StringsJoin(sep string, ss ...string) string
|
||||
func StringsMap(ss []string, mapFn func(s string) string) []string
|
||||
func StringsRemove(ss []string, s string) []string
|
||||
func StringsToInts(ss []string) (ints []int, err error)
|
||||
func StringsToSlice(ss []string) []interface{}
|
||||
func TakeWhile(data any, fn Predicate) interface{}
|
||||
func ToInt64s(arr any) (ret []int64, err error)
|
||||
func ToString(arr []any) string
|
||||
func ToStrings(arr any) (ret []string, err error)
|
||||
func TrimStrings(ss []string, cutSet ...string) []string
|
||||
func TwowaySearch(data any, item any, fn Comparer) (int, error)
|
||||
func Union(first, second any, fn Comparer) interface{}
|
||||
func Unique(arr any) interface{}
|
||||
type ArrFormatter struct{ ... }
|
||||
func NewFormatter(arr any) *ArrFormatter
|
||||
```
|
||||
|
||||
## Code Check & Testing
|
||||
|
||||
```bash
|
||||
gofmt -w -l ./
|
||||
golint ./...
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
|
||||
```shell
|
||||
go test -v ./cliutil/...
|
||||
```
|
||||
|
||||
**Test limit by regexp**:
|
||||
|
||||
```shell
|
||||
go test -v -run ^TestSetByKeys ./cliutil/...
|
||||
```
|
||||
|
||||
## Refers
|
||||
|
||||
- https://github.com/elliotchance/pie
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
## ArrUtil
|
||||
|
||||
`arrutil` 是一个用于操作数组和切片的工具包,提供了丰富的功能来简化 Go 语言中数组和切片的处理。
|
||||
|
||||
## Install
|
||||
|
||||
```shell
|
||||
go get github.com/gookit/goutil/arrutil
|
||||
```
|
||||
|
||||
## Go docs
|
||||
|
||||
- [Go docs](https://pkg.go.dev/github.com/gookit/goutil/arrutil)
|
||||
|
||||
## 基本功能
|
||||
|
||||
主要包括以下功能:
|
||||
|
||||
1. **数组/切片的基本操作**:
|
||||
- `RandomOne`:从数组或切片中随机获取一个元素。
|
||||
- `Reverse`:反转数组或切片中的元素顺序。
|
||||
|
||||
2. **检查和查找**:
|
||||
- `Contains` 和 `HasValue`:检查数组或切片是否包含特定值。
|
||||
- `InStrings` 和 `StringsHas`:检查字符串切片中是否包含特定字符串。
|
||||
- `IntsHas` 和 `Int64sHas`:检查整数切片中是否包含特定整数值。
|
||||
- `Find` 和 `FindOrDefault`:根据谓词函数查找元素,如果没有找到则返回默认值。
|
||||
|
||||
3. **集合操作**:
|
||||
- `Union`:计算两个切片的并集。
|
||||
- `Intersects`:计算两个切片的交集。
|
||||
- `Excepts` 和 `Differences`:计算两个切片的差集。
|
||||
- `TwowaySearch`:在切片中双向搜索特定元素。
|
||||
|
||||
4. **转换和格式化**:
|
||||
- `ToInt64s` 和 `ToStrings`:将任意类型的切片转换为整数或字符串切片。
|
||||
- `JoinSlice` 和 `JoinStrings`:将切片中的元素连接成一个字符串。
|
||||
- `FormatIndent`:将数组或切片格式化为带有缩进的字符串。
|
||||
|
||||
5. **排序和过滤**:
|
||||
- `Sort`:对切片进行排序。
|
||||
- `Filter`:根据条件过滤切片中的元素。
|
||||
- `Remove`:从切片中移除特定元素。
|
||||
|
||||
6. **其他实用功能**:
|
||||
- `Unique`:去除切片中的重复元素。
|
||||
- `FirstOr`:获取切片的第一个元素,如果切片为空则返回默认值。
|
||||
|
||||
这些功能使得在 Go 语言中处理数组和切片变得更加方便和高效。无论是进行数据处理、集合运算还是字符串操作,`arrutil` 都提供了一系列简洁且易于使用的函数来帮助开发者完成任务。
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Package arrutil provides some util functions for array, slice
|
||||
package arrutil
|
||||
|
||||
import (
|
||||
"github.com/gookit/goutil/mathutil"
|
||||
)
|
||||
|
||||
// GetRandomOne get random element from an array/slice
|
||||
func GetRandomOne[T any](arr []T) T { return RandomOne(arr) }
|
||||
|
||||
// RandomOne get random element from an array/slice
|
||||
func RandomOne[T any](arr []T) T {
|
||||
if ln := len(arr); ln > 0 {
|
||||
i := mathutil.RandomInt(0, len(arr))
|
||||
return arr[i]
|
||||
}
|
||||
panic("cannot get value from nil or empty slice")
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package arrutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/mathutil"
|
||||
)
|
||||
|
||||
// SliceHas check the slice contains the given value
|
||||
func SliceHas[T comdef.ScalarType](slice []T, val T) bool {
|
||||
for _, ele := range slice {
|
||||
if ele == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IntsHas check the []comdef.Integer contains the given value
|
||||
func IntsHas[T comdef.Integer](ints []T, val T) bool {
|
||||
for _, ele := range ints {
|
||||
if ele == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Int64sHas check the []int64 contains the given value
|
||||
func Int64sHas(ints []int64, val int64) bool {
|
||||
for _, ele := range ints {
|
||||
if ele == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StringsHas check the []string contains the given element
|
||||
func StringsHas[T ~string](ss []T, val T) bool {
|
||||
for _, ele := range ss {
|
||||
if ele == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InStrings check elem in the ss. alias of StringsHas()
|
||||
func InStrings[T ~string](elem T, ss []T) bool {
|
||||
return StringsHas(ss, elem)
|
||||
}
|
||||
|
||||
// NotIn check the given value whether not in the list
|
||||
func NotIn[T comdef.ScalarType](value T, list []T) bool {
|
||||
return !In(value, list)
|
||||
}
|
||||
|
||||
// In check the given value whether in the list
|
||||
func In[T comdef.ScalarType](value T, list []T) bool {
|
||||
for _, elem := range list {
|
||||
if elem == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ContainsAll check given values is sub-list of sample list.
|
||||
func ContainsAll[T comdef.ScalarType](list, values []T) bool {
|
||||
return IsSubList(values, list)
|
||||
}
|
||||
|
||||
// IsSubList check given values is sub-list of sample list.
|
||||
func IsSubList[T comdef.ScalarType](values, list []T) bool {
|
||||
for _, value := range values {
|
||||
if !In(value, list) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsParent check given values is parent-list of samples.
|
||||
func IsParent[T comdef.ScalarType](values, list []T) bool {
|
||||
return IsSubList(list, values)
|
||||
}
|
||||
|
||||
// HasValue check array(strings, intXs, uintXs) should be contained the given value(int(X),string).
|
||||
func HasValue(arr, val any) bool { return Contains(arr, val) }
|
||||
|
||||
// Contains check slice/array(strings, intXs, uintXs) should be contained the given value(int(X),string).
|
||||
//
|
||||
// TIP: Difference the In(), Contains() will try to convert value type,
|
||||
// and Contains() support array type.
|
||||
func Contains(arr, val any) bool {
|
||||
if val == nil || arr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// if is string value
|
||||
if strVal, ok := val.(string); ok {
|
||||
if ss, ok := arr.([]string); ok {
|
||||
return StringsHas(ss, strVal)
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(arr)
|
||||
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
if v, ok := rv.Index(i).Interface().(string); ok && strings.EqualFold(v, strVal) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// as int value
|
||||
intVal, err := mathutil.Int64(val)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if int64s, err := ToInt64s(arr); err == nil {
|
||||
return Int64sHas(int64s, intVal)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NotContains check array(strings, ints, uints) should be not contains the given value.
|
||||
func NotContains(arr, val any) bool {
|
||||
return !Contains(arr, val)
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
package arrutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/reflects"
|
||||
)
|
||||
|
||||
// ErrElementNotFound is the error returned when the element is not found.
|
||||
var ErrElementNotFound = errors.New("element not found")
|
||||
|
||||
// Comparer Function to compare two elements.
|
||||
type Comparer[T any] func(a, b T) int
|
||||
|
||||
// type Comparer func(a, b any) int
|
||||
|
||||
// Predicate Function to predicate a struct/value satisfies a condition.
|
||||
type Predicate[T any] func(v T) bool
|
||||
|
||||
// StringEqualsComparer Comparer for string. It will compare the string by their value.
|
||||
//
|
||||
// returns: 0 if equal, -1 if a != b
|
||||
func StringEqualsComparer(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ValueEqualsComparer Comparer for comdef.Compared type. It will compare by their value.
|
||||
//
|
||||
// returns: 0 if equal, -1 if a != b
|
||||
func ValueEqualsComparer[T comdef.Compared](a, b T) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ReflectEqualsComparer Comparer for struct ptr. It will compare by reflect.Value
|
||||
//
|
||||
// returns: 0 if equal, -1 if a != b
|
||||
func ReflectEqualsComparer[T any](a, b T) int {
|
||||
if reflects.IsEqual(a, b) {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ElemTypeEqualsComparer Comparer for struct/value. It will compare the struct by their element type.
|
||||
//
|
||||
// returns: 0 if same type, -1 if not.
|
||||
func ElemTypeEqualsComparer[T any](a, b T) int {
|
||||
at := reflects.TypeOf(a).SafeElem()
|
||||
bt := reflects.TypeOf(b).SafeElem()
|
||||
|
||||
if at == bt {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// TwowaySearch find specialized element in a slice forward and backward in the same time, should be more quickly.
|
||||
//
|
||||
// - data: the slice to search in. MUST BE A SLICE.
|
||||
// - item: the element to search.
|
||||
// - fn: the comparer function.
|
||||
// - return: the index of the element, or -1 if not found.
|
||||
func TwowaySearch[T any](data []T, item T, fn Comparer[T]) (int, error) {
|
||||
if data == nil {
|
||||
return -1, errors.New("collections.TwowaySearch: data is nil")
|
||||
}
|
||||
if fn == nil {
|
||||
return -1, errors.New("collections.TwowaySearch: fn is nil")
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return -1, errors.New("collections.TwowaySearch: data is empty")
|
||||
}
|
||||
|
||||
forward := 0
|
||||
backward := len(data) - 1
|
||||
|
||||
for forward <= backward {
|
||||
if fn(data[forward], item) == 0 {
|
||||
return forward, nil
|
||||
}
|
||||
|
||||
if fn(data[backward], item) == 0 {
|
||||
return backward, nil
|
||||
}
|
||||
|
||||
forward++
|
||||
backward--
|
||||
}
|
||||
|
||||
return -1, ErrElementNotFound
|
||||
}
|
||||
|
||||
// CloneSlice Clone a slice.
|
||||
//
|
||||
// data: the slice to clone.
|
||||
// returns: the cloned slice.
|
||||
func CloneSlice[T any](data []T) []T {
|
||||
nt := make([]T, 0, len(data))
|
||||
nt = append(nt, data...)
|
||||
return nt
|
||||
}
|
||||
|
||||
// Diff Produces the set difference of two slice according to a comparer function. alias of Differences
|
||||
func Diff[T any](first, second []T, fn Comparer[T]) []T {
|
||||
return Differences(first, second, fn)
|
||||
}
|
||||
|
||||
// Differences Produces the set difference of two slice according to a comparer function.
|
||||
//
|
||||
// - first: the first slice. MUST BE A SLICE.
|
||||
// - second: the second slice. MUST BE A SLICE.
|
||||
// - fn: the comparer function.
|
||||
// - returns: the difference of the two slices.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Output: []string{"c"}
|
||||
// Differences([]string{"a", "b", "c"}, []string{"a", "b"}, arrutil.StringEqualsComparer
|
||||
func Differences[T any](first, second []T, fn Comparer[T]) []T {
|
||||
firstLen := len(first)
|
||||
if firstLen == 0 {
|
||||
return CloneSlice(second)
|
||||
}
|
||||
|
||||
secondLen := len(second)
|
||||
if secondLen == 0 {
|
||||
return CloneSlice(first)
|
||||
}
|
||||
|
||||
maxLn := firstLen
|
||||
if secondLen > firstLen {
|
||||
maxLn = secondLen
|
||||
}
|
||||
|
||||
result := make([]T, 0)
|
||||
for i := 0; i < maxLn; i++ {
|
||||
if i < firstLen {
|
||||
s := first[i]
|
||||
if i, _ := TwowaySearch(second, s, fn); i < 0 {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
|
||||
if i < secondLen {
|
||||
t := second[i]
|
||||
if i, _ := TwowaySearch(first, t, fn); i < 0 {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Excepts Produces the set difference of two slice according to a comparer function.
|
||||
//
|
||||
// - first: the first slice. MUST BE A SLICE.
|
||||
// - second: the second slice. MUST BE A SLICE.
|
||||
// - fn: the comparer function.
|
||||
// - returns: the difference of the two slices.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Output: []string{"c"}
|
||||
// Excepts([]string{"a", "b", "c"}, []string{"a", "b"}, arrutil.StringEqualsComparer)
|
||||
func Excepts[T any](first, second []T, fn Comparer[T]) []T {
|
||||
if len(first) == 0 {
|
||||
return make([]T, 0)
|
||||
}
|
||||
if len(second) == 0 {
|
||||
return CloneSlice(first)
|
||||
}
|
||||
|
||||
result := make([]T, 0)
|
||||
for _, s := range first {
|
||||
if i, _ := TwowaySearch(second, s, fn); i < 0 {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Intersects Produces to intersect of two slice according to a comparer function.
|
||||
//
|
||||
// - first: the first slice. MUST BE A SLICE.
|
||||
// - second: the second slice. MUST BE A SLICE.
|
||||
// - fn: the comparer function.
|
||||
// - returns: to intersect of the two slices.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Output: []string{"a", "b"}
|
||||
// Intersects([]string{"a", "b", "c"}, []string{"a", "b"}, arrutil.ValueEqualsComparer)
|
||||
func Intersects[T any](first, second []T, fn Comparer[T]) []T {
|
||||
if len(first) == 0 || len(second) == 0 {
|
||||
return make([]T, 0)
|
||||
}
|
||||
|
||||
result := make([]T, 0)
|
||||
for _, s := range first {
|
||||
if i, _ := TwowaySearch(second, s, fn); i >= 0 {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Union Produces the set union of two slice according to a comparer function
|
||||
//
|
||||
// - first: the first slice. MUST BE A SLICE.
|
||||
// - second: the second slice. MUST BE A SLICE.
|
||||
// - fn: the comparer function.
|
||||
// - returns: the union of the two slices.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Output: []string{"a", "b", "c"}
|
||||
// sl := Union([]string{"a", "b", "c"}, []string{"a", "b"}, arrutil.ValueEqualsComparer)
|
||||
func Union[T any](first, second []T, fn Comparer[T]) []T {
|
||||
if len(first) == 0 {
|
||||
return CloneSlice(second)
|
||||
}
|
||||
|
||||
excepts := Excepts(second, first, fn)
|
||||
nt := make([]T, 0, len(first)+len(second))
|
||||
nt = append(nt, first...)
|
||||
return append(nt, excepts...)
|
||||
}
|
||||
|
||||
// Find Produces the value of a slice according to a predicate function.
|
||||
//
|
||||
// - source: the slice. MUST BE A SLICE.
|
||||
// - fn: the predicate function.
|
||||
// - returns: the struct/value of the slice.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Output: "c"
|
||||
// val := Find([]string{"a", "b", "c"}, func(s string) bool {
|
||||
// return s == "c"
|
||||
// })
|
||||
func Find[T any](source []T, fn Predicate[T]) (v T, err error) {
|
||||
err = ErrElementNotFound
|
||||
if len(source) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range source {
|
||||
if fn(s) {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FindOrDefault Produce the value f a slice to a predicate function,
|
||||
// Produce default value when predicate function not found.
|
||||
//
|
||||
// - source: the slice. MUST BE A SLICE.
|
||||
// - fn: the predicate function.
|
||||
// - defaultValue: the default value.
|
||||
// - returns: the value of the slice.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Output: "d"
|
||||
// val := FindOrDefault([]string{"a", "b", "c"}, func(s string) bool {
|
||||
// return s == "d"
|
||||
// }, "d")
|
||||
func FindOrDefault[T any](source []T, fn Predicate[T], defaultValue T) T {
|
||||
item, err := Find(source, fn)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
// TakeWhile Produce the set of a slice according to a predicate function,
|
||||
// Produce empty slice when predicate function not matched.
|
||||
//
|
||||
// - data: the slice. MUST BE A SLICE.
|
||||
// - fn: the predicate function.
|
||||
// - returns: the set of the slice.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Output: []string{"a", "b"}
|
||||
// sl := TakeWhile([]string{"a", "b", "c"}, func(s string) bool {
|
||||
// return s != "c"
|
||||
// })
|
||||
func TakeWhile[T any](data []T, fn Predicate[T]) []T {
|
||||
result := make([]T, 0)
|
||||
if len(data) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, v := range data {
|
||||
if fn(v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ExceptWhile Produce the set of a slice except with a predicate function,
|
||||
// Produce original slice when predicate function not match.
|
||||
//
|
||||
// - data: the slice. MUST BE A SLICE.
|
||||
// - fn: the predicate function.
|
||||
// - returns: the set of the slice.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Output: []string{"a", "b"}
|
||||
// sl := ExceptWhile([]string{"a", "b", "c"}, func(s string) bool {
|
||||
// return s == "c"
|
||||
// })
|
||||
func ExceptWhile[T any](data []T, fn Predicate[T]) []T {
|
||||
result := make([]T, 0)
|
||||
if len(data) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, v := range data {
|
||||
if !fn(v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
package arrutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/mathutil"
|
||||
"github.com/gookit/goutil/reflects"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// ErrInvalidType error
|
||||
var ErrInvalidType = errors.New("the input param type is invalid")
|
||||
|
||||
/*************************************************************
|
||||
* Join func for slice
|
||||
*************************************************************/
|
||||
|
||||
// JoinStrings alias of strings.Join
|
||||
func JoinStrings(sep string, ss ...string) string {
|
||||
return strings.Join(ss, sep)
|
||||
}
|
||||
|
||||
// StringsJoin alias of strings.Join
|
||||
func StringsJoin(sep string, ss ...string) string {
|
||||
return strings.Join(ss, sep)
|
||||
}
|
||||
|
||||
// JoinTyped join typed []T slice to string.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// JoinTyped(",", 1,2,3) // "1,2,3"
|
||||
// JoinTyped(",", "a","b","c") // "a,b,c"
|
||||
// JoinTyped[any](",", "a",1,"c") // "a,1,c"
|
||||
func JoinTyped[T any](sep string, arr ...T) string {
|
||||
if arr == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for i, v := range arr {
|
||||
if i > 0 {
|
||||
sb.WriteString(sep)
|
||||
}
|
||||
sb.WriteString(strutil.QuietString(v))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// JoinSlice join []any slice to string.
|
||||
func JoinSlice(sep string, arr ...any) string {
|
||||
if arr == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for i, v := range arr {
|
||||
if i > 0 {
|
||||
sb.WriteString(sep)
|
||||
}
|
||||
sb.WriteString(strutil.QuietString(v))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* convert func for ints
|
||||
*************************************************************/
|
||||
|
||||
// IntsToString convert []T to string
|
||||
func IntsToString[T comdef.Integer](ints []T) string {
|
||||
if len(ints) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteByte('[')
|
||||
for i, v := range ints {
|
||||
if i > 0 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
sb.WriteString(strconv.FormatInt(int64(v), 10))
|
||||
}
|
||||
sb.WriteByte(']')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ToInt64s convert any(allow: array,slice) to []int64
|
||||
func ToInt64s(arr any) (ret []int64, err error) {
|
||||
rv := reflect.ValueOf(arr)
|
||||
if rv.Kind() != reflect.Slice && rv.Kind() != reflect.Array {
|
||||
err = ErrInvalidType
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
i64, err := mathutil.Int64(rv.Index(i).Interface())
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
|
||||
ret = append(ret, i64)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MustToInt64s convert any(allow: array,slice) to []int64
|
||||
func MustToInt64s(arr any) []int64 {
|
||||
ret, _ := ToInt64s(arr)
|
||||
return ret
|
||||
}
|
||||
|
||||
// SliceToInt64s convert []any to []int64
|
||||
func SliceToInt64s(arr []any) []int64 {
|
||||
i64s := make([]int64, len(arr))
|
||||
for i, v := range arr {
|
||||
i64s[i] = mathutil.QuietInt64(v)
|
||||
}
|
||||
return i64s
|
||||
}
|
||||
|
||||
// ToMap convert a list to new map.
|
||||
//
|
||||
// Example:
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
// users := []User{{"Tom", 18}, {"Jack", 20}}
|
||||
// mp := arrutil.ToMap(users, func(u User) (string, int) {
|
||||
// return u.Name, u.Age
|
||||
// })
|
||||
// // mp = map[string]int{"Tom":18, "Jack":20}
|
||||
func ToMap[T any, K comdef.ScalarType, V any](list []T, mapFn func(T) (K, V)) map[K]V {
|
||||
mp := make(map[K]V, len(list))
|
||||
for _, item := range list {
|
||||
k, v := mapFn(item)
|
||||
mp[k] = v
|
||||
}
|
||||
return mp
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* convert func for any-slice
|
||||
*************************************************************/
|
||||
|
||||
// AnyToSlice convert any(allow: array,slice) to []any
|
||||
func AnyToSlice(sl any) (ls []any, err error) {
|
||||
rfKeys := reflect.ValueOf(sl)
|
||||
if rfKeys.Kind() != reflect.Slice && rfKeys.Kind() != reflect.Array {
|
||||
return nil, ErrInvalidType
|
||||
}
|
||||
|
||||
for i := 0; i < rfKeys.Len(); i++ {
|
||||
ls = append(ls, rfKeys.Index(i).Interface())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AnyToStrings convert array or slice to []string
|
||||
func AnyToStrings(arr any) []string {
|
||||
ret, _ := ToStrings(arr)
|
||||
return ret
|
||||
}
|
||||
|
||||
// MustToStrings convert array or slice to []string
|
||||
func MustToStrings(arr any) []string {
|
||||
ret, err := ToStrings(arr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// ToStrings convert any(allow: array,slice) to []string
|
||||
func ToStrings(arr any) (ret []string, err error) {
|
||||
// try direct convert
|
||||
switch typVal := arr.(type) {
|
||||
case string:
|
||||
return []string{typVal}, nil
|
||||
case []string:
|
||||
return typVal, nil
|
||||
case []any:
|
||||
return SliceToStrings(typVal), nil
|
||||
}
|
||||
|
||||
// try use reflect to convert
|
||||
rv := reflect.ValueOf(arr)
|
||||
if rv.Kind() != reflect.Slice && rv.Kind() != reflect.Array {
|
||||
err = ErrInvalidType
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
str, err1 := strutil.ToString(rv.Index(i).Interface())
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
ret = append(ret, str)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SliceToStrings safe convert []any to []string
|
||||
func SliceToStrings(arr []any) []string {
|
||||
ss := make([]string, len(arr))
|
||||
for i, v := range arr {
|
||||
ss[i] = strutil.SafeString(v)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// QuietStrings safe convert []any to []string
|
||||
func QuietStrings(arr []any) []string { return SliceToStrings(arr) }
|
||||
|
||||
// ConvType convert type of slice elements to new type slice, by the given newElemTyp type.
|
||||
//
|
||||
// Supports conversion between []string, []intX, []uintX, []floatX.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ints, _ := arrutil.ConvType([]string{"12", "23"}, 1) // []int{12, 23}
|
||||
func ConvType[T any, R any](arr []T, newElemTyp R) ([]R, error) {
|
||||
newArr := make([]R, len(arr))
|
||||
elemTyp := reflect.TypeOf(newElemTyp)
|
||||
|
||||
for i, elem := range arr {
|
||||
var anyElem any = elem
|
||||
// type is same.
|
||||
if _, ok := anyElem.(R); ok {
|
||||
newArr[i] = anyElem.(R)
|
||||
continue
|
||||
}
|
||||
|
||||
// need conv type.
|
||||
rfVal, err := reflects.ValueByType(elem, elemTyp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newArr[i] = rfVal.Interface().(R)
|
||||
}
|
||||
return newArr, nil
|
||||
}
|
||||
|
||||
// AnyToString simple and quickly convert any array, slice to string
|
||||
func AnyToString(arr any) string {
|
||||
return NewFormatter(arr).Format()
|
||||
}
|
||||
|
||||
// SliceToString convert []any to string
|
||||
func SliceToString(arr ...any) string { return ToString(arr) }
|
||||
|
||||
// ToString simple and quickly convert []T to string
|
||||
func ToString[T any](arr []T) string {
|
||||
// like fmt.Println([]any(nil))
|
||||
if arr == nil {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteByte('[')
|
||||
|
||||
for i, v := range arr {
|
||||
if i > 0 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
sb.WriteString(strutil.SafeString(v))
|
||||
}
|
||||
|
||||
sb.WriteByte(']')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// CombineToMap combine []K and []V slice to map[K]V.
|
||||
//
|
||||
// If keys length is greater than values, the extra keys will be ignored.
|
||||
func CombineToMap[K comdef.SortedType, V any](keys []K, values []V) map[K]V {
|
||||
ln := len(values)
|
||||
mp := make(map[K]V, len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
if i >= ln {
|
||||
break
|
||||
}
|
||||
mp[key] = values[i]
|
||||
}
|
||||
return mp
|
||||
}
|
||||
|
||||
// CombineToSMap combine two string-slice to map[string]string
|
||||
func CombineToSMap(keys, values []string) map[string]string {
|
||||
ln := len(values)
|
||||
mp := make(map[string]string, len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
if ln > i {
|
||||
mp[key] = values[i]
|
||||
} else {
|
||||
mp[key] = ""
|
||||
}
|
||||
}
|
||||
return mp
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package arrutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// ArrFormatter struct
|
||||
type ArrFormatter struct {
|
||||
comdef.BaseFormatter
|
||||
// Prefix string for each element
|
||||
Prefix string
|
||||
// Indent string for format each element
|
||||
Indent string
|
||||
// ClosePrefix on before end char: ]
|
||||
ClosePrefix string
|
||||
}
|
||||
|
||||
// NewFormatter instance
|
||||
func NewFormatter(arr any) *ArrFormatter {
|
||||
f := &ArrFormatter{}
|
||||
f.Src = arr
|
||||
return f
|
||||
}
|
||||
|
||||
// FormatIndent array data to string.
|
||||
func FormatIndent(arr any, indent string) string {
|
||||
return NewFormatter(arr).WithIndent(indent).Format()
|
||||
}
|
||||
|
||||
// WithFn for config self
|
||||
func (f *ArrFormatter) WithFn(fn func(f *ArrFormatter)) *ArrFormatter {
|
||||
fn(f)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithIndent string
|
||||
func (f *ArrFormatter) WithIndent(indent string) *ArrFormatter {
|
||||
f.Indent = indent
|
||||
return f
|
||||
}
|
||||
|
||||
// FormatTo to custom buffer
|
||||
func (f *ArrFormatter) FormatTo(w io.Writer) {
|
||||
f.SetOutput(w)
|
||||
f.doFormat()
|
||||
}
|
||||
|
||||
// Format to string
|
||||
func (f *ArrFormatter) String() string {
|
||||
return f.Format()
|
||||
}
|
||||
|
||||
// Format to string
|
||||
func (f *ArrFormatter) Format() string {
|
||||
f.doFormat()
|
||||
return f.BsWriter().String()
|
||||
}
|
||||
|
||||
// Format to string
|
||||
//
|
||||
//goland:noinspection GoUnhandledErrorResult
|
||||
func (f *ArrFormatter) doFormat() {
|
||||
if f.Src == nil {
|
||||
return
|
||||
}
|
||||
|
||||
rv, ok := f.Src.(reflect.Value)
|
||||
if !ok {
|
||||
rv = reflect.ValueOf(f.Src)
|
||||
}
|
||||
|
||||
rv = reflect.Indirect(rv)
|
||||
if rv.Kind() != reflect.Slice && rv.Kind() != reflect.Array {
|
||||
return
|
||||
}
|
||||
|
||||
writer := f.BsWriter()
|
||||
arrLn := rv.Len()
|
||||
if arrLn == 0 {
|
||||
writer.WriteString("[]")
|
||||
return
|
||||
}
|
||||
|
||||
// if f.AfterReset {
|
||||
// defer f.Reset()
|
||||
// }
|
||||
|
||||
// sb.Grow(arrLn * 4)
|
||||
writer.WriteByte('[')
|
||||
|
||||
indentLn := len(f.Indent)
|
||||
if indentLn > 0 {
|
||||
writer.WriteByte('\n')
|
||||
}
|
||||
|
||||
for i := 0; i < arrLn; i++ {
|
||||
if indentLn > 0 {
|
||||
writer.WriteString(f.Indent)
|
||||
}
|
||||
writer.WriteString(strutil.QuietString(rv.Index(i).Interface()))
|
||||
|
||||
if i < arrLn-1 {
|
||||
writer.WriteByte(',')
|
||||
|
||||
// no indent, with space
|
||||
if indentLn == 0 {
|
||||
writer.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
if indentLn > 0 {
|
||||
writer.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
if f.ClosePrefix != "" {
|
||||
writer.WriteString(f.ClosePrefix)
|
||||
}
|
||||
writer.WriteByte(']')
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package arrutil
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
)
|
||||
|
||||
// Ints type
|
||||
type Ints[T comdef.Integer] []T
|
||||
|
||||
// String to string
|
||||
func (is Ints[T]) String() string {
|
||||
return ToString(is)
|
||||
}
|
||||
|
||||
// Has given element
|
||||
func (is Ints[T]) Has(i T) bool {
|
||||
for _, iv := range is {
|
||||
if i == iv {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// First element value.
|
||||
func (is Ints[T]) First(defVal ...T) T {
|
||||
if len(is) > 0 {
|
||||
return is[0]
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
panic("empty integer slice")
|
||||
}
|
||||
|
||||
// Last element value.
|
||||
func (is Ints[T]) Last(defVal ...T) T {
|
||||
if len(is) > 0 {
|
||||
return is[len(is)-1]
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
panic("empty integer slice")
|
||||
}
|
||||
|
||||
// Sort the int slice
|
||||
func (is Ints[T]) Sort() {
|
||||
sort.Sort(is)
|
||||
}
|
||||
|
||||
// Len get length
|
||||
func (is Ints[T]) Len() int {
|
||||
return len(is)
|
||||
}
|
||||
|
||||
// Less compare two elements
|
||||
func (is Ints[T]) Less(i, j int) bool {
|
||||
return is[i] < is[j]
|
||||
}
|
||||
|
||||
// Swap elements by indexes
|
||||
func (is Ints[T]) Swap(i, j int) {
|
||||
is[i], is[j] = is[j], is[i]
|
||||
}
|
||||
|
||||
// Strings type
|
||||
type Strings []string
|
||||
|
||||
// String to string
|
||||
func (ss Strings) String() string {
|
||||
return strings.Join(ss, ",")
|
||||
}
|
||||
|
||||
// Join to string
|
||||
func (ss Strings) Join(sep string) string {
|
||||
return strings.Join(ss, sep)
|
||||
}
|
||||
|
||||
// Has given element
|
||||
func (ss Strings) Has(sub string) bool {
|
||||
return ss.Contains(sub)
|
||||
}
|
||||
|
||||
// Contains given element
|
||||
func (ss Strings) Contains(sub string) bool {
|
||||
for _, s := range ss {
|
||||
if s == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// First element value.
|
||||
func (ss Strings) First(defVal ...string) string {
|
||||
if len(ss) > 0 {
|
||||
return ss[0]
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
panic("empty string list")
|
||||
}
|
||||
|
||||
// Last element value.
|
||||
func (ss Strings) Last(defVal ...string) string {
|
||||
if len(ss) > 0 {
|
||||
return ss[len(ss)-1]
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
panic("empty string list")
|
||||
}
|
||||
|
||||
// Sort the string slice
|
||||
func (ss Strings) Sort() {
|
||||
sort.Strings(ss)
|
||||
}
|
||||
|
||||
// SortedList definition for compared type
|
||||
type SortedList[T comdef.Compared] []T
|
||||
|
||||
// Len get length
|
||||
func (ls SortedList[T]) Len() int {
|
||||
return len(ls)
|
||||
}
|
||||
|
||||
// Less compare two elements
|
||||
func (ls SortedList[T]) Less(i, j int) bool {
|
||||
return ls[i] < ls[j]
|
||||
}
|
||||
|
||||
// Swap elements by indexes
|
||||
func (ls SortedList[T]) Swap(i, j int) {
|
||||
ls[i], ls[j] = ls[j], ls[i]
|
||||
}
|
||||
|
||||
// IsEmpty check
|
||||
func (ls SortedList[T]) IsEmpty() bool {
|
||||
return len(ls) == 0
|
||||
}
|
||||
|
||||
// String to string
|
||||
func (ls SortedList[T]) String() string {
|
||||
return ToString(ls)
|
||||
}
|
||||
|
||||
// Has given element
|
||||
func (ls SortedList[T]) Has(el T) bool {
|
||||
return ls.Contains(el)
|
||||
}
|
||||
|
||||
// Contains given element
|
||||
func (ls SortedList[T]) Contains(el T) bool {
|
||||
for _, v := range ls {
|
||||
if v == el {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// First element value.
|
||||
func (ls SortedList[T]) First(defVal ...T) T {
|
||||
if len(ls) > 0 {
|
||||
return ls[0]
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
panic("empty list")
|
||||
}
|
||||
|
||||
// Last element value.
|
||||
func (ls SortedList[T]) Last(defVal ...T) T {
|
||||
if ln := len(ls); ln > 0 {
|
||||
return ls[ln-1]
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
panic("empty list")
|
||||
}
|
||||
|
||||
// Remove given element
|
||||
func (ls SortedList[T]) Remove(el T) SortedList[T] {
|
||||
return Filter(ls, func(v T) bool {
|
||||
return v != el
|
||||
})
|
||||
}
|
||||
|
||||
// Filter the slice, default will filter zero value.
|
||||
func (ls SortedList[T]) Filter(filter ...comdef.MatchFunc[T]) SortedList[T] {
|
||||
return Filter(ls, filter...)
|
||||
}
|
||||
|
||||
// Map the slice to new slice. TODO syntax ERROR: Method cannot have type parameters
|
||||
// func (ls SortedList[T]) Map[V any](mapFn MapFn[T, V]) SortedList[V] {
|
||||
// return Map(ls, mapFn)
|
||||
// }
|
||||
|
||||
// Sort the slice
|
||||
func (ls SortedList[T]) Sort() {
|
||||
sort.Sort(ls)
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package arrutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
)
|
||||
|
||||
// Reverse any T slice.
|
||||
//
|
||||
// eg: []string{"site", "user", "info", "0"} -> []string{"0", "info", "user", "site"}
|
||||
func Reverse[T any](ls []T) {
|
||||
ln := len(ls)
|
||||
for i := 0; i < ln/2; i++ {
|
||||
li := ln - i - 1
|
||||
ls[i], ls[li] = ls[li], ls[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Remove give element from slice []T.
|
||||
//
|
||||
// eg: []string{"site", "user", "info", "0"} -> []string{"site", "user", "info"}
|
||||
func Remove[T comdef.Compared](ls []T, val T) []T {
|
||||
return Filter(ls, func(el T) bool {
|
||||
return el != val
|
||||
})
|
||||
}
|
||||
|
||||
// Filter given slice, default will filter zero value.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // output: [a, b]
|
||||
// ss := arrutil.Filter([]string{"a", "", "b", ""})
|
||||
func Filter[T any](ls []T, filter ...comdef.MatchFunc[T]) []T {
|
||||
var fn comdef.MatchFunc[T]
|
||||
if len(filter) > 0 && filter[0] != nil {
|
||||
fn = filter[0]
|
||||
} else {
|
||||
fn = func(el T) bool {
|
||||
// if el == nil { // Filter nil value
|
||||
// return false
|
||||
// }
|
||||
return !reflect.ValueOf(el).IsZero()
|
||||
}
|
||||
}
|
||||
|
||||
newLs := make([]T, 0, len(ls))
|
||||
for _, el := range ls {
|
||||
if fn(el) {
|
||||
newLs = append(newLs, el)
|
||||
}
|
||||
}
|
||||
return newLs
|
||||
}
|
||||
|
||||
// Map a list to new list with map filter function.
|
||||
//
|
||||
// eg: mapping [object0{},object1{},...] to flatten list [object0.someKey, object1.someKey, ...]
|
||||
func Map[T, V any](list []T, mapFilter func(input T) (target V, ok bool)) []V {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
flatArr := make([]V, 0, len(list))
|
||||
for _, obj := range list {
|
||||
if target, ok := mapFilter(obj); ok {
|
||||
flatArr = append(flatArr, target)
|
||||
}
|
||||
}
|
||||
return flatArr
|
||||
}
|
||||
|
||||
// Map1 a list to new list with map function.
|
||||
//
|
||||
// eg: mapping [object0{},object1{},...] to flatten list [object0.someKey, object1.someKey, ...]
|
||||
func Map1[T, R any](list []T, mapFn func(t T) R) []R {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make([]R, len(list))
|
||||
for i := range list {
|
||||
ret[i] = mapFn(list[i])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Column collect sub elements from list. alias of Map func
|
||||
//
|
||||
// Example:
|
||||
// list := []map[string]any{
|
||||
// {"id": 1, "name": "one", "age": 23},
|
||||
// {"id": 2, "name": "two", "age": 23},
|
||||
// {"id": 3, "name": "three", "age": 23},
|
||||
// }
|
||||
// names := arrutil.Column(list, func(el map[string]any) string {
|
||||
// return el["name"].(string)
|
||||
// })
|
||||
func Column[T any, V any](list []T, mapFn func(obj T) (val V, find bool)) []V {
|
||||
return Map(list, mapFn)
|
||||
}
|
||||
|
||||
// Unique value in the given slice data.
|
||||
func Unique[T comdef.NumberOrString](list []T) []T {
|
||||
if len(list) < 2 {
|
||||
return list
|
||||
}
|
||||
|
||||
valMap := make(map[T]struct{}, len(list))
|
||||
uniArr := make([]T, 0, len(list))
|
||||
|
||||
for _, t := range list {
|
||||
if _, ok := valMap[t]; !ok {
|
||||
valMap[t] = struct{}{}
|
||||
uniArr = append(uniArr, t)
|
||||
}
|
||||
}
|
||||
return uniArr
|
||||
}
|
||||
|
||||
// IndexOf value in given slice.
|
||||
func IndexOf[T comdef.NumberOrString](val T, list []T) int {
|
||||
for i, v := range list {
|
||||
if v == val {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// FirstOr get first value of slice, if slice is empty, return the default value.
|
||||
func FirstOr[T any](list []T, defVal ...T) T {
|
||||
if len(list) > 0 {
|
||||
return list[0]
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
|
||||
// Chunk split slice to chunks by size.
|
||||
//
|
||||
// eg: [1,2,3,4,5,6,7,8,9,10] -> [[1,2,3,4], [5,6,7,8], [9,10]]
|
||||
func Chunk[T any](list []T, size int) [][]T {
|
||||
if size <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ln := len(list)
|
||||
if ln == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
chunks := make([][]T, 0, ln/size+1)
|
||||
|
||||
for i := 0; i < ln; i += size {
|
||||
end := i + size
|
||||
if end > ln {
|
||||
end = ln
|
||||
}
|
||||
chunks = append(chunks, list[i:end])
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
// ChunkBy split slice to chunks by size, and with custom chunk function.
|
||||
//
|
||||
// Example:
|
||||
// list := []map[string]any{
|
||||
// {"id": 1, "name": "one", "age": 23},
|
||||
// {"id": 2, "name": "two", "age": 23},
|
||||
// {"id": 3, "name": "three", "age": 23},
|
||||
// }
|
||||
// chunks := arrutil.ChunkBy(list, 2, func(el map[string]any) map[string]any {
|
||||
// return map[string]any{
|
||||
// "id": el["id"],
|
||||
// "name": el["name"],
|
||||
// }
|
||||
// })
|
||||
// Output: [
|
||||
// [{"id": 1, "name": "one"}, {"id": 2, "name": "two"}],
|
||||
// [{"id": 3, "name": "three"}]
|
||||
// ]
|
||||
func ChunkBy[T, R any](list []T, size int, mapFn func(el T) R) [][]R {
|
||||
if size <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ln := len(list)
|
||||
if ln == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算需要的块数量
|
||||
numChunks := ln/size + 1
|
||||
if ln%size == 0 {
|
||||
numChunks = ln / size
|
||||
}
|
||||
|
||||
chunks := make([][]R, 0, numChunks)
|
||||
|
||||
for i := 0; i < ln; i += size {
|
||||
end := i + size
|
||||
if end > ln {
|
||||
end = ln
|
||||
}
|
||||
|
||||
// 创建当前块的切片
|
||||
currentChunk := make([]R, 0, size)
|
||||
for j := i; j < end; j++ {
|
||||
currentChunk = append(currentChunk, mapFn(list[j]))
|
||||
}
|
||||
chunks = append(chunks, currentChunk)
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package arrutil
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
)
|
||||
|
||||
// StringsToAnys convert []string to []any
|
||||
func StringsToAnys(ss []string) []any {
|
||||
args := make([]any, len(ss))
|
||||
for i, s := range ss {
|
||||
args[i] = s
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// StringsToSlice convert []string to []any. alias of StringsToAnys()
|
||||
func StringsToSlice(ss []string) []any {
|
||||
return StringsToAnys(ss)
|
||||
}
|
||||
|
||||
// StringsAsInts convert and ignore error
|
||||
func StringsAsInts(ss []string) []int {
|
||||
ints, _ := StringsTryInts(ss)
|
||||
return ints
|
||||
}
|
||||
|
||||
// StringsToInts string slice to int slice
|
||||
func StringsToInts(ss []string) (ints []int, err error) {
|
||||
return StringsTryInts(ss)
|
||||
}
|
||||
|
||||
// StringsTryInts string slice to int slice
|
||||
func StringsTryInts(ss []string) (ints []int, err error) {
|
||||
for _, str := range ss {
|
||||
iVal, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ints = append(ints, iVal)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StringsUnique unique string slice
|
||||
func StringsUnique(ss []string) []string {
|
||||
if len(ss) == 0 {
|
||||
return ss
|
||||
}
|
||||
|
||||
var unique []string
|
||||
for _, s := range ss {
|
||||
if !StringsContains(unique, s) {
|
||||
unique = append(unique, s)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
// StringsContains check string slice contains string
|
||||
func StringsContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StringsRemove value form a string slice
|
||||
func StringsRemove(ss []string, s string) []string {
|
||||
return StringsFilter(ss, func(el string) bool {
|
||||
return s != el
|
||||
})
|
||||
}
|
||||
|
||||
// StringsFilter given strings, default will filter emtpy string.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // output: [a, b]
|
||||
// ss := arrutil.StringsFilter([]string{"a", "", "b", ""})
|
||||
func StringsFilter(ss []string, filter ...comdef.StringMatchFunc) []string {
|
||||
var fn comdef.StringMatchFunc
|
||||
if len(filter) > 0 && filter[0] != nil {
|
||||
fn = filter[0]
|
||||
} else {
|
||||
fn = func(s string) bool {
|
||||
return s != ""
|
||||
}
|
||||
}
|
||||
|
||||
ns := make([]string, 0, len(ss))
|
||||
for _, s := range ss {
|
||||
if fn(s) {
|
||||
ns = append(ns, s)
|
||||
}
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
// StringsMap handle each string item, map to new strings
|
||||
func StringsMap(ss []string, mapFn func(s string) string) []string {
|
||||
ns := make([]string, 0, len(ss))
|
||||
for _, s := range ss {
|
||||
ns = append(ns, mapFn(s))
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
// TrimStrings trim string slice item.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // output: [a, b, c]
|
||||
// ss := arrutil.TrimStrings([]string{",a", "b.", ",.c,"}, ",.")
|
||||
func TrimStrings(ss []string, cutSet ...string) []string {
|
||||
cutSetLn := len(cutSet)
|
||||
hasCutSet := cutSetLn > 0 && cutSet[0] != ""
|
||||
|
||||
var trimSet string
|
||||
if hasCutSet {
|
||||
trimSet = cutSet[0]
|
||||
}
|
||||
if cutSetLn > 1 {
|
||||
trimSet = strings.Join(cutSet, "")
|
||||
}
|
||||
|
||||
ns := make([]string, 0, len(ss))
|
||||
for _, str := range ss {
|
||||
if hasCutSet {
|
||||
ns = append(ns, strings.Trim(str, trimSet))
|
||||
} else {
|
||||
ns = append(ns, strings.TrimSpace(str))
|
||||
}
|
||||
}
|
||||
return ns
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Bytes Util
|
||||
|
||||
Provide some common bytes util functions.
|
||||
|
||||
## Install
|
||||
|
||||
```shell
|
||||
go get github.com/gookit/goutil/byteutil
|
||||
```
|
||||
|
||||
## Go docs
|
||||
|
||||
- [Go docs](https://pkg.go.dev/github.com/gookit/goutil/byteutil)
|
||||
|
||||
## Functions API
|
||||
|
||||
> **Note**: doc by run `go doc ./byteutil`
|
||||
|
||||
```go
|
||||
func AppendAny(dst []byte, v any) []byte
|
||||
func FirstLine(bs []byte) []byte
|
||||
func IsNumChar(c byte) bool
|
||||
func Md5(src any) []byte
|
||||
func Random(length int) ([]byte, error)
|
||||
func SafeString(bs []byte, err error) string
|
||||
func StrOrErr(bs []byte, err error) (string, error)
|
||||
func String(b []byte) string
|
||||
func ToString(b []byte) string
|
||||
type Buffer struct{ ... }
|
||||
func NewBuffer() *Buffer
|
||||
type BytesEncoder interface{ ... }
|
||||
type ChanPool struct{ ... }
|
||||
func NewChanPool(maxSize int, width int, capWidth int) *ChanPool
|
||||
type StdEncoder struct{ ... }
|
||||
func NewStdEncoder(encFn func(src []byte) []byte, decFn func(src []byte) ([]byte, error)) *StdEncoder
|
||||
```
|
||||
|
||||
## Code Check & Testing
|
||||
|
||||
```bash
|
||||
gofmt -w -l ./
|
||||
golint ./...
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
|
||||
```shell
|
||||
go test -v ./byteutil/...
|
||||
```
|
||||
|
||||
**Test limit by regexp**:
|
||||
|
||||
```shell
|
||||
go test -v -run ^TestSetByKeys ./byteutil/...
|
||||
```
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package byteutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Buffer wrap and extends the bytes.Buffer, add some useful methods
|
||||
// and implements the io.Writer, io.Closer and stdio.Flusher interfaces
|
||||
type Buffer struct {
|
||||
bytes.Buffer
|
||||
// custom error for testing
|
||||
CloseErr error
|
||||
FlushErr error
|
||||
SyncErr error
|
||||
}
|
||||
|
||||
// NewBuffer instance
|
||||
func NewBuffer() *Buffer { return &Buffer{} }
|
||||
|
||||
// PrintByte to buffer, ignore error. alias of WriteByte()
|
||||
func (b *Buffer) PrintByte(c byte) {
|
||||
_ = b.WriteByte(c)
|
||||
}
|
||||
|
||||
// WriteStr1 quiet write one string to buffer
|
||||
func (b *Buffer) WriteStr1(s string) { b.writeStringNl(s, false) }
|
||||
|
||||
// WriteStr1Nl quiet write one string and end with newline
|
||||
func (b *Buffer) WriteStr1Nl(s string) { b.writeStringNl(s, true) }
|
||||
|
||||
// writeStringNl quiet write one string and end with newline
|
||||
func (b *Buffer) writeStringNl(s string, nl bool) {
|
||||
_, _ = b.Buffer.WriteString(s)
|
||||
if nl {
|
||||
_ = b.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// WriteStr quiet write strings to buffer
|
||||
func (b *Buffer) WriteStr(ss ...string) {
|
||||
b.writeStringsNl(ss, false)
|
||||
}
|
||||
|
||||
// WriteStrings to buffer, ignore error.
|
||||
func (b *Buffer) WriteStrings(ss []string) {
|
||||
b.writeStringsNl(ss, false)
|
||||
}
|
||||
|
||||
// WriteStringNl write message to buffer and end with newline
|
||||
func (b *Buffer) WriteStringNl(ss ...string) {
|
||||
b.writeStringsNl(ss, true)
|
||||
}
|
||||
|
||||
// writeStringsNl to buffer, ignore error.
|
||||
func (b *Buffer) writeStringsNl(ss []string, nl bool) {
|
||||
for _, s := range ss {
|
||||
_, _ = b.Buffer.WriteString(s)
|
||||
}
|
||||
if nl {
|
||||
_ = b.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAny type value to buffer
|
||||
func (b *Buffer) WriteAny(vs ...any) {
|
||||
b.writeAnysWithNl(vs, false)
|
||||
}
|
||||
|
||||
// Writeln write values to buffer and end with newline
|
||||
func (b *Buffer) Writeln(vs ...any) {
|
||||
b.writeAnysWithNl(vs, true)
|
||||
}
|
||||
|
||||
// WriteAnyNl type value to buffer and end with newline
|
||||
func (b *Buffer) WriteAnyNl(vs ...any) {
|
||||
b.writeAnysWithNl(vs, true)
|
||||
}
|
||||
|
||||
// WriteAnyLn type value to buffer and end with newline
|
||||
func (b *Buffer) writeAnysWithNl(vs []any, nl bool) {
|
||||
for _, v := range vs {
|
||||
_, _ = b.Buffer.WriteString(fmt.Sprint(v))
|
||||
}
|
||||
if nl {
|
||||
_ = b.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// Writef write message to buffer, ignore error. alias of Printf()
|
||||
func (b *Buffer) Writef(tpl string, vs ...any) { _, _ = fmt.Fprintf(b, tpl, vs...) }
|
||||
|
||||
// Printf quick write message to buffer, ignore error.
|
||||
func (b *Buffer) Printf(tpl string, vs ...any) { _, _ = fmt.Fprintf(b, tpl, vs...) }
|
||||
|
||||
// Println quick write message with newline to buffer, will ignore error.
|
||||
func (b *Buffer) Println(vs ...any) { _, _ = fmt.Fprintln(b, vs...) }
|
||||
|
||||
// ResetGet buffer string. alias of ResetAndGet()
|
||||
func (b *Buffer) ResetGet() string {
|
||||
return b.ResetAndGet()
|
||||
}
|
||||
|
||||
// ResetAndGet buffer string.
|
||||
func (b *Buffer) ResetAndGet() string {
|
||||
s := b.String()
|
||||
b.Reset()
|
||||
return s
|
||||
}
|
||||
|
||||
// Close buffer
|
||||
func (b *Buffer) Close() error { return b.CloseErr }
|
||||
|
||||
// Flush buffer
|
||||
func (b *Buffer) Flush() error { return b.FlushErr }
|
||||
|
||||
// Sync anf flush buffer
|
||||
func (b *Buffer) Sync() error { return b.SyncErr }
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
// Package byteutil provides some useful functions for byte slice.
|
||||
package byteutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Md5 Generate a 32-bit md5 bytes
|
||||
func Md5(src any) []byte {
|
||||
bs := Md5Sum(src)
|
||||
dst := make([]byte, hex.EncodedLen(len(bs)))
|
||||
hex.Encode(dst, bs)
|
||||
return dst
|
||||
}
|
||||
|
||||
// Md5Sum Generate a md5 bytes
|
||||
func Md5Sum(src any) []byte {
|
||||
h := md5.New()
|
||||
|
||||
switch val := src.(type) {
|
||||
case []byte:
|
||||
h.Write(val)
|
||||
case string:
|
||||
h.Write([]byte(val))
|
||||
default:
|
||||
h.Write([]byte(fmt.Sprint(src)))
|
||||
}
|
||||
|
||||
return h.Sum(nil) // cap(bs) == 16
|
||||
}
|
||||
|
||||
// ShortMd5 Generate a 16-bit md5 bytes. remove the first 8 and last 8 bytes from 32-bit md5.
|
||||
func ShortMd5(src any) []byte { return Md5(src)[8:24] }
|
||||
|
||||
// Random bytes generate
|
||||
func Random(length int) ([]byte, error) {
|
||||
b := make([]byte, length)
|
||||
// Note that err == nil only if we read len(b) bytes.
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// FirstLine from command output
|
||||
func FirstLine(bs []byte) []byte {
|
||||
if i := bytes.IndexByte(bs, '\n'); i >= 0 {
|
||||
return bs[0:i]
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
// AppendAny append any value to byte slice
|
||||
func AppendAny(dst []byte, v any) []byte {
|
||||
if v == nil {
|
||||
return append(dst, "<nil>"...)
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case []byte:
|
||||
dst = append(dst, val...)
|
||||
case string:
|
||||
dst = append(dst, val...)
|
||||
case int:
|
||||
dst = strconv.AppendInt(dst, int64(val), 10)
|
||||
case int8:
|
||||
dst = strconv.AppendInt(dst, int64(val), 10)
|
||||
case int16:
|
||||
dst = strconv.AppendInt(dst, int64(val), 10)
|
||||
case int32:
|
||||
dst = strconv.AppendInt(dst, int64(val), 10)
|
||||
case int64:
|
||||
dst = strconv.AppendInt(dst, val, 10)
|
||||
case uint:
|
||||
dst = strconv.AppendUint(dst, uint64(val), 10)
|
||||
case uint8:
|
||||
dst = strconv.AppendUint(dst, uint64(val), 10)
|
||||
case uint16:
|
||||
dst = strconv.AppendUint(dst, uint64(val), 10)
|
||||
case uint32:
|
||||
dst = strconv.AppendUint(dst, uint64(val), 10)
|
||||
case uint64:
|
||||
dst = strconv.AppendUint(dst, val, 10)
|
||||
case float32:
|
||||
dst = strconv.AppendFloat(dst, float64(val), 'f', -1, 32)
|
||||
case float64:
|
||||
dst = strconv.AppendFloat(dst, val, 'f', -1, 64)
|
||||
case bool:
|
||||
dst = strconv.AppendBool(dst, val)
|
||||
case time.Time:
|
||||
dst = val.AppendFormat(dst, time.RFC3339)
|
||||
case time.Duration:
|
||||
dst = strconv.AppendInt(dst, int64(val), 10)
|
||||
case error:
|
||||
dst = append(dst, val.Error()...)
|
||||
case fmt.Stringer:
|
||||
dst = append(dst, val.String()...)
|
||||
default:
|
||||
dst = append(dst, fmt.Sprint(v)...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Cut bytes by one byte char. like bytes.Cut(), but sep is byte.
|
||||
func Cut(bs []byte, sep byte) (before, after []byte, found bool) {
|
||||
return bytes.Cut(bs, []byte{sep})
|
||||
}
|
||||
|
||||
// SafeCut bytes by one byte char. always return before and after
|
||||
func SafeCut(bs []byte, sep byte) (before, after []byte) {
|
||||
before, after, _ = bytes.Cut(bs, []byte{sep})
|
||||
return
|
||||
}
|
||||
|
||||
// SafeCuts bytes by sub bytes. like the bytes.Cut(), but always return before and after
|
||||
func SafeCuts(bs []byte, sep []byte) (before, after []byte) {
|
||||
before, after, _ = bytes.Cut(bs, sep)
|
||||
return
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package byteutil
|
||||
|
||||
// IsNumChar returns true if the given character is a numeric, otherwise false.
|
||||
func IsNumChar(c byte) bool { return c >= '0' && c <= '9' }
|
||||
|
||||
// IsAlphaChar returns true if the given character is a alphabet, otherwise false.
|
||||
func IsAlphaChar(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package byteutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
)
|
||||
|
||||
// StrOrErr convert to string, return empty string on error.
|
||||
func StrOrErr(bs []byte, err error) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bs), err
|
||||
}
|
||||
|
||||
// SafeString convert to string, return empty string on error.
|
||||
func SafeString(bs []byte, err error) string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
// String unsafe convert bytes to string
|
||||
func String(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
// ToString convert bytes to string
|
||||
func ToString(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
// ToBytes convert any value to []byte. return error on convert failed.
|
||||
func ToBytes(v any) ([]byte, error) {
|
||||
return ToBytesWithFunc(v, nil)
|
||||
}
|
||||
|
||||
// SafeBytes convert any value to []byte. use fmt.Sprint() on convert failed.
|
||||
func SafeBytes(v any) []byte {
|
||||
bs, _ := ToBytesWithFunc(v, func(v any) ([]byte, error) {
|
||||
return []byte(fmt.Sprint(v)), nil
|
||||
})
|
||||
return bs
|
||||
}
|
||||
|
||||
// ToBytesFunc convert any value to []byte
|
||||
type ToBytesFunc = func(v any) ([]byte, error)
|
||||
|
||||
// ToBytesWithFunc convert any value to []byte with custom fallback func.
|
||||
//
|
||||
// refer the strutil.ToStringWithFunc
|
||||
//
|
||||
// On not convert:
|
||||
// - If usrFn is nil, will return comdef.ErrConvType.
|
||||
// - If usrFn is not nil, will call it to convert.
|
||||
func ToBytesWithFunc(v any, usrFn ToBytesFunc) ([]byte, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case []byte:
|
||||
return val, nil
|
||||
case string:
|
||||
return []byte(val), nil
|
||||
case int:
|
||||
return []byte(strconv.Itoa(val)), nil
|
||||
case int8:
|
||||
return []byte(strconv.Itoa(int(val))), nil
|
||||
case int16:
|
||||
return []byte(strconv.Itoa(int(val))), nil
|
||||
case int32: // same as `rune`
|
||||
return []byte(strconv.Itoa(int(val))), nil
|
||||
case int64:
|
||||
return []byte(strconv.FormatInt(val, 10)), nil
|
||||
case uint:
|
||||
return []byte(strconv.FormatUint(uint64(val), 10)), nil
|
||||
case uint8:
|
||||
return []byte(strconv.FormatUint(uint64(val), 10)), nil
|
||||
case uint16:
|
||||
return []byte(strconv.FormatUint(uint64(val), 10)), nil
|
||||
case uint32:
|
||||
return []byte(strconv.FormatUint(uint64(val), 10)), nil
|
||||
case uint64:
|
||||
return []byte(strconv.FormatUint(val, 10)), nil
|
||||
case float32:
|
||||
return []byte(strconv.FormatFloat(float64(val), 'f', -1, 32)), nil
|
||||
case float64:
|
||||
return []byte(strconv.FormatFloat(val, 'f', -1, 64)), nil
|
||||
case bool:
|
||||
return []byte(strconv.FormatBool(val)), nil
|
||||
case time.Duration:
|
||||
return []byte(strconv.FormatInt(int64(val), 10)), nil
|
||||
case fmt.Stringer:
|
||||
return []byte(val.String()), nil
|
||||
case error:
|
||||
return []byte(val.Error()), nil
|
||||
default:
|
||||
if usrFn == nil {
|
||||
return nil, comdef.ErrConvType
|
||||
}
|
||||
return usrFn(val)
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse 反转字节数组 eg: ABCD -> DCBA
|
||||
func Reverse(arr []byte) {
|
||||
for i := 0; i < len(arr)/2; i++ {
|
||||
arr[i], arr[len(arr)-1-i] = arr[len(arr)-1-i], arr[i]
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package byteutil
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// BytesEncodeFunc type
|
||||
type BytesEncodeFunc func(src []byte) []byte
|
||||
|
||||
// BytesDecodeFunc type
|
||||
type BytesDecodeFunc func(src []byte) ([]byte, error)
|
||||
|
||||
// BytesEncoder interface
|
||||
type BytesEncoder interface {
|
||||
Encode(src []byte) []byte
|
||||
Decode(src []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// StdEncoder implement the BytesEncoder
|
||||
type StdEncoder struct {
|
||||
encodeFn BytesEncodeFunc
|
||||
decodeFn BytesDecodeFunc
|
||||
}
|
||||
|
||||
// NewStdEncoder instance
|
||||
func NewStdEncoder(encFn BytesEncodeFunc, decFn BytesDecodeFunc) *StdEncoder {
|
||||
return &StdEncoder{
|
||||
encodeFn: encFn,
|
||||
decodeFn: decFn,
|
||||
}
|
||||
}
|
||||
|
||||
// Encode input
|
||||
func (e *StdEncoder) Encode(src []byte) []byte {
|
||||
return e.encodeFn(src)
|
||||
}
|
||||
|
||||
// Decode input
|
||||
func (e *StdEncoder) Decode(src []byte) ([]byte, error) {
|
||||
return e.decodeFn(src)
|
||||
}
|
||||
|
||||
var (
|
||||
// HexEncoder instance
|
||||
HexEncoder = NewStdEncoder(func(src []byte) []byte {
|
||||
dst := make([]byte, hex.EncodedLen(len(src)))
|
||||
hex.Encode(dst, src)
|
||||
return dst
|
||||
}, func(src []byte) ([]byte, error) {
|
||||
n, err := hex.Decode(src, src)
|
||||
return src[:n], err
|
||||
})
|
||||
|
||||
// B64Encoder instance
|
||||
B64Encoder = NewStdEncoder(func(src []byte) []byte {
|
||||
b64Dst := make([]byte, base64.StdEncoding.EncodedLen(len(src)))
|
||||
base64.StdEncoding.Encode(b64Dst, src)
|
||||
return b64Dst
|
||||
}, func(src []byte) ([]byte, error) {
|
||||
dBuf := make([]byte, base64.StdEncoding.DecodedLen(len(src)))
|
||||
n, err := base64.StdEncoding.Decode(dBuf, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dBuf[:n], err
|
||||
})
|
||||
)
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package byteutil
|
||||
|
||||
// ChanPool struct
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// bp := strutil.NewByteChanPool(500, 1024, 1024)
|
||||
// buf:=bp.Get()
|
||||
// defer bp.Put(buf)
|
||||
// // use buf do something ...
|
||||
//
|
||||
// refer https://www.flysnow.org/2020/08/21/golang-chan-byte-pool.html
|
||||
// from https://github.com/minio/minio/blob/master/internal/bpool/bpool.go
|
||||
type ChanPool struct {
|
||||
c chan []byte
|
||||
w int // init byte width
|
||||
wcap int // set byte cap
|
||||
}
|
||||
|
||||
// NewChanPool instance
|
||||
func NewChanPool(chSize int, width int, capWidth int) *ChanPool {
|
||||
return &ChanPool{
|
||||
c: make(chan []byte, chSize),
|
||||
w: width,
|
||||
wcap: capWidth,
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets a []byte from the BytePool, or creates a new one if none are
|
||||
// available in the pool.
|
||||
func (bp *ChanPool) Get() (b []byte) {
|
||||
select {
|
||||
case b = <-bp.c: // reuse existing buffer
|
||||
default:
|
||||
// create new buffer
|
||||
if bp.wcap > 0 {
|
||||
b = make([]byte, bp.w, bp.wcap)
|
||||
} else {
|
||||
b = make([]byte, bp.w)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Put returns the given Buffer to the BytePool.
|
||||
func (bp *ChanPool) Put(b []byte) {
|
||||
select {
|
||||
case bp.c <- b:
|
||||
// buffer went back into pool
|
||||
default:
|
||||
// buffer didn't go back into pool, just discard
|
||||
}
|
||||
}
|
||||
|
||||
// Width returns the width of the byte arrays in this pool.
|
||||
func (bp *ChanPool) Width() (n int) {
|
||||
return bp.w
|
||||
}
|
||||
|
||||
// WidthCap returns the cap width of the byte arrays in this pool.
|
||||
func (bp *ChanPool) WidthCap() (n int) {
|
||||
return bp.wcap
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package cmdline
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// LineBuilder build command line string.
|
||||
// codes refer from strings.Builder
|
||||
type LineBuilder struct {
|
||||
strings.Builder
|
||||
}
|
||||
|
||||
// NewBuilder create
|
||||
func NewBuilder(binFile string, args ...string) *LineBuilder {
|
||||
b := &LineBuilder{}
|
||||
|
||||
if binFile != "" {
|
||||
b.AddArg(binFile)
|
||||
}
|
||||
|
||||
b.AddArray(args)
|
||||
return b
|
||||
}
|
||||
|
||||
// ResetGet value, will reset after get.
|
||||
func (b *LineBuilder) ResetGet() string {
|
||||
defer b.Reset()
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// AddArg to builder
|
||||
func (b *LineBuilder) AddArg(arg string) {
|
||||
_, _ = b.WriteString(arg)
|
||||
}
|
||||
|
||||
// AddArgs to builder
|
||||
func (b *LineBuilder) AddArgs(args ...string) {
|
||||
b.AddArray(args)
|
||||
}
|
||||
|
||||
// AddArray to builder
|
||||
func (b *LineBuilder) AddArray(args []string) {
|
||||
for _, arg := range args {
|
||||
_, _ = b.WriteString(arg)
|
||||
}
|
||||
}
|
||||
|
||||
// AddAny args to builder
|
||||
func (b *LineBuilder) AddAny(args ...any) {
|
||||
for _, arg := range args {
|
||||
_, _ = b.WriteString(strutil.SafeString(arg))
|
||||
}
|
||||
}
|
||||
|
||||
// WriteString arg string to the builder, will auto quote special string.
|
||||
// refer strconv.Quote()
|
||||
func (b *LineBuilder) WriteString(a string) (int, error) {
|
||||
// add sep on not-first write.
|
||||
if b.Len() != 0 {
|
||||
_ = b.WriteByte(' ')
|
||||
}
|
||||
|
||||
return b.Builder.WriteString(comfunc.ShellQuote(a))
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Package cmdline provide quick build and parse cmd line string.
|
||||
package cmdline
|
||||
|
||||
import "github.com/gookit/goutil/internal/comfunc"
|
||||
|
||||
// LineBuild build command line string by given args.
|
||||
func LineBuild(binFile string, args []string) string {
|
||||
return NewBuilder(binFile, args...).String()
|
||||
}
|
||||
|
||||
// ParseLine input command line text. alias of the StringToOSArgs()
|
||||
func ParseLine(line string) []string { return NewParser(line).Parse() }
|
||||
|
||||
// Quote string in shell command env
|
||||
func Quote(s string) string { return comfunc.ShellQuote(s) }
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package cmdline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/internal/varexpr"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// LineParser struct
|
||||
// parse input command line to []string, such as cli os.Args
|
||||
type LineParser struct {
|
||||
parsed bool
|
||||
// Line the full input command line text
|
||||
// eg `kite top sub -a "this is a message" --foo val1 --bar "val 2"`
|
||||
Line string
|
||||
// ParseEnv parse ENV var on the line.
|
||||
ParseEnv bool
|
||||
// the exploded nodes by space.
|
||||
nodes []string
|
||||
// the parsed args
|
||||
args []string
|
||||
|
||||
// temp value
|
||||
quoteChar byte
|
||||
quoteIndex int // if > 0, mark is not on start
|
||||
tempNode bytes.Buffer
|
||||
}
|
||||
|
||||
// NewParser create
|
||||
func NewParser(line string) *LineParser {
|
||||
return &LineParser{Line: line}
|
||||
}
|
||||
|
||||
// WithParseEnv with parse ENV var
|
||||
func (p *LineParser) WithParseEnv() *LineParser {
|
||||
p.ParseEnv = true
|
||||
return p
|
||||
}
|
||||
|
||||
// AlsoEnvParse input command line text to os.Args, will parse ENV var
|
||||
func (p *LineParser) AlsoEnvParse() []string {
|
||||
p.ParseEnv = true
|
||||
return p.Parse()
|
||||
}
|
||||
|
||||
// NewExecCmd quick create exec.Cmd by cmdline string
|
||||
func (p *LineParser) NewExecCmd() *exec.Cmd {
|
||||
// parse get bin and args
|
||||
binName, args := p.BinAndArgs()
|
||||
|
||||
// create a new Cmd instance
|
||||
return exec.Command(binName, args...)
|
||||
}
|
||||
|
||||
// BinAndArgs get binName and args
|
||||
func (p *LineParser) BinAndArgs() (bin string, args []string) {
|
||||
p.Parse() // ensure parsed.
|
||||
|
||||
ln := len(p.args)
|
||||
if ln == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
bin = p.args[0]
|
||||
if ln > 1 {
|
||||
args = p.args[1:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Parse input command line text to os.Args
|
||||
func (p *LineParser) Parse() []string {
|
||||
if p.parsed {
|
||||
return p.args
|
||||
}
|
||||
|
||||
p.parsed = true
|
||||
p.Line = strings.TrimSpace(p.Line)
|
||||
if p.Line == "" {
|
||||
return p.args
|
||||
}
|
||||
|
||||
// enable parse Env var
|
||||
if p.ParseEnv {
|
||||
p.Line = varexpr.SafeParse(p.Line)
|
||||
}
|
||||
|
||||
p.nodes = strings.Split(p.Line, " ")
|
||||
if len(p.nodes) == 1 {
|
||||
p.args = p.nodes
|
||||
return p.args
|
||||
}
|
||||
|
||||
for i := 0; i < len(p.nodes); i++ {
|
||||
node := p.nodes[i]
|
||||
if node == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
p.parseNode(node)
|
||||
}
|
||||
|
||||
p.nodes = p.nodes[:0]
|
||||
if p.tempNode.Len() > 0 {
|
||||
p.appendTempNode()
|
||||
}
|
||||
return p.args
|
||||
}
|
||||
|
||||
func (p *LineParser) parseNode(node string) {
|
||||
maxIdx := len(node) - 1
|
||||
start, end := node[0], node[maxIdx]
|
||||
|
||||
// in quotes
|
||||
if p.quoteChar != 0 {
|
||||
p.tempNode.WriteByte(' ')
|
||||
|
||||
// end quotes
|
||||
if end == p.quoteChar {
|
||||
if p.quoteIndex > 0 {
|
||||
p.tempNode.WriteString(node) // eg: node="--pretty=format:'one two'"
|
||||
} else {
|
||||
p.tempNode.WriteString(node[:maxIdx]) // remove last quote
|
||||
}
|
||||
p.appendTempNode()
|
||||
} else { // goon ... write to temp node
|
||||
p.tempNode.WriteString(node)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// quote start
|
||||
if start == comdef.DoubleQuote || start == comdef.SingleQuote {
|
||||
// only one words. eg: `-m "msg"`
|
||||
if end == start {
|
||||
p.args = append(p.args, node[1:maxIdx])
|
||||
return
|
||||
}
|
||||
|
||||
p.quoteChar = start
|
||||
p.tempNode.WriteString(node[1:])
|
||||
} else if end == comdef.DoubleQuote || end == comdef.SingleQuote {
|
||||
p.args = append(p.args, node) // only one node: `msg"`
|
||||
} else {
|
||||
// eg: --pretty=format:'one two three'
|
||||
if strutil.ContainsByte(node, comdef.DoubleQuote) {
|
||||
p.quoteIndex = 1 // mark is not on start
|
||||
p.quoteChar = comdef.DoubleQuote
|
||||
} else if strutil.ContainsByte(node, comdef.SingleQuote) {
|
||||
p.quoteIndex = 1
|
||||
p.quoteChar = comdef.SingleQuote
|
||||
}
|
||||
|
||||
// in quote, append to temp-node
|
||||
if p.quoteChar != 0 {
|
||||
p.tempNode.WriteString(node)
|
||||
} else {
|
||||
p.args = append(p.args, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *LineParser) appendTempNode() {
|
||||
p.args = append(p.args, p.tempNode.String())
|
||||
|
||||
// reset context value
|
||||
p.quoteChar = 0
|
||||
p.quoteIndex = 0
|
||||
p.tempNode.Reset()
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Package comdef provide some common type or constant definitions
|
||||
package comdef
|
||||
|
||||
// ToTypeFunc convert value to defined type
|
||||
type ToTypeFunc[T any] func(any) (T, error)
|
||||
|
||||
// IntCheckFunc check func
|
||||
type IntCheckFunc func(val int) error
|
||||
|
||||
// StrCheckFunc check func
|
||||
type StrCheckFunc func(val string) error
|
||||
|
||||
// ToStringFunc try to convert value to string, return error on fail
|
||||
type ToStringFunc func(v any) (string, error)
|
||||
|
||||
// SafeStringFunc safe convert value to string
|
||||
type SafeStringFunc func(v any) string
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package comdef
|
||||
|
||||
// constants for compare operation
|
||||
const (
|
||||
OpEq = "="
|
||||
OpNeq = "!="
|
||||
OpLt = "<"
|
||||
OpLte = "<="
|
||||
OpGt = ">"
|
||||
OpGte = ">="
|
||||
)
|
||||
|
||||
// constants quote chars
|
||||
const (
|
||||
SingleQuote = '\''
|
||||
DoubleQuote = '"'
|
||||
SlashQuote = '\\'
|
||||
|
||||
SingleQuoteStr = "'"
|
||||
DoubleQuoteStr = `"`
|
||||
SlashQuoteStr = "\\"
|
||||
)
|
||||
|
||||
// NoIdx invalid index or length
|
||||
const NoIdx = -1
|
||||
|
||||
// const VarPathReg = `(\w[\w-]*(?:\.[\w-]+)*)`
|
||||
|
||||
// Align define align, position: L, C, R, Auto
|
||||
type Align uint8
|
||||
type Position = Align // Position alias of Align
|
||||
|
||||
// constants for align, position: L, C, R, Auto
|
||||
const (
|
||||
Left Align = iota
|
||||
Center
|
||||
Right
|
||||
PosAuto
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package comdef
|
||||
|
||||
// Newline string for non-windows
|
||||
const Newline = "\n"
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package comdef
|
||||
|
||||
// Newline string for windows
|
||||
const Newline = "\r\n"
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package comdef
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrConvType error
|
||||
var ErrConvType = errors.New("convert value type error")
|
||||
|
||||
// Errors multi error list
|
||||
type Errors []error
|
||||
|
||||
// Error string
|
||||
func (es Errors) Error() string {
|
||||
var sb strings.Builder
|
||||
for _, err := range es {
|
||||
sb.WriteString(err.Error())
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ErrOrNil error
|
||||
func (es Errors) ErrOrNil() error {
|
||||
if len(es) == 0 {
|
||||
return nil
|
||||
}
|
||||
return es
|
||||
}
|
||||
|
||||
// First error
|
||||
func (es Errors) First() error {
|
||||
if len(es) > 0 {
|
||||
return es[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package comdef
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/gookit/goutil/x/stdio"
|
||||
)
|
||||
|
||||
// DataFormatter interface
|
||||
type DataFormatter interface {
|
||||
Format() string
|
||||
FormatTo(w io.Writer)
|
||||
}
|
||||
|
||||
// BaseFormatter struct
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// type YourFormatter struct {
|
||||
// comdef.BaseFormatter
|
||||
// }
|
||||
// // implement the DataFormatter interface...
|
||||
type BaseFormatter struct {
|
||||
ow ByteStringWriter
|
||||
// Out formatted to the writer
|
||||
Out io.Writer
|
||||
// Src data(array, map, struct) for format
|
||||
Src any
|
||||
// MaxDepth limit depth for array, map data TODO
|
||||
MaxDepth int
|
||||
// Prefix string for each element
|
||||
Prefix string
|
||||
// Indent string for format each element
|
||||
Indent string
|
||||
// ClosePrefix string for last "]", "}"
|
||||
ClosePrefix string
|
||||
}
|
||||
|
||||
// Reset after format
|
||||
func (f *BaseFormatter) Reset() {
|
||||
f.Out = nil
|
||||
f.Src = nil
|
||||
}
|
||||
|
||||
// SetOutput writer
|
||||
func (f *BaseFormatter) SetOutput(out io.Writer) {
|
||||
f.Out = out
|
||||
}
|
||||
|
||||
// BsWriter warp the Out, build a ByteStringWriter
|
||||
func (f *BaseFormatter) BsWriter() ByteStringWriter {
|
||||
if f.ow == nil {
|
||||
if f.Out == nil {
|
||||
f.ow = new(bytes.Buffer)
|
||||
} else if ow, ok := f.Out.(ByteStringWriter); ok {
|
||||
f.ow = ow
|
||||
} else {
|
||||
f.ow = stdio.NewWriteWrapper(f.Out)
|
||||
}
|
||||
}
|
||||
return f.ow
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package comdef
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ByteStringWriter interface
|
||||
type ByteStringWriter interface {
|
||||
io.Writer
|
||||
io.ByteWriter
|
||||
io.StringWriter
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
// StringWriteStringer interface
|
||||
type StringWriteStringer interface {
|
||||
io.StringWriter
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
// Int64able interface
|
||||
type Int64able interface {
|
||||
Int64() (int64, error)
|
||||
}
|
||||
|
||||
// Float64able interface
|
||||
type Float64able interface {
|
||||
Float64() (float64, error)
|
||||
}
|
||||
|
||||
// MapFunc definition
|
||||
type MapFunc func(val any) (any, error)
|
||||
|
||||
//
|
||||
//
|
||||
// Matcher type
|
||||
//
|
||||
//
|
||||
|
||||
// Matcher interface
|
||||
type Matcher[T any] interface {
|
||||
Match(s T) bool
|
||||
}
|
||||
|
||||
// MatchFunc definition. implements Matcher interface
|
||||
type MatchFunc[T any] func(v T) bool
|
||||
|
||||
// Match satisfies the Matcher interface
|
||||
func (fn MatchFunc[T]) Match(v T) bool {
|
||||
return fn(v)
|
||||
}
|
||||
|
||||
// StringMatcher interface
|
||||
type StringMatcher interface {
|
||||
Match(s string) bool
|
||||
}
|
||||
|
||||
// StringMatchFunc definition
|
||||
type StringMatchFunc func(s string) bool
|
||||
|
||||
// Match satisfies the StringMatcher interface
|
||||
func (fn StringMatchFunc) Match(s string) bool {
|
||||
return fn(s)
|
||||
}
|
||||
|
||||
// StringHandler interface
|
||||
type StringHandler interface {
|
||||
Handle(s string) string
|
||||
}
|
||||
|
||||
// StringHandleFunc definition
|
||||
type StringHandleFunc func(s string) string
|
||||
|
||||
// Handle satisfies the StringHandler interface
|
||||
func (fn StringHandleFunc) Handle(s string) string {
|
||||
return fn(s)
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package comdef
|
||||
|
||||
type (
|
||||
// MarshalFunc define
|
||||
MarshalFunc func(v any) ([]byte, error)
|
||||
|
||||
// UnmarshalFunc define
|
||||
UnmarshalFunc func(bts []byte, ptr any) error
|
||||
)
|
||||
|
||||
// Serializer interface definition
|
||||
type Serializer interface {
|
||||
Serialize(v any) ([]byte, error)
|
||||
Deserialize(data []byte, v any) error
|
||||
}
|
||||
|
||||
// GoSerializer interface definition
|
||||
type GoSerializer interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
Unmarshal(v []byte, ptr any) error
|
||||
}
|
||||
|
||||
// Codec interface definition
|
||||
type Codec interface {
|
||||
Decode(blob []byte, v any) (err error)
|
||||
Encode(v any) (out []byte, err error)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package comdef
|
||||
|
||||
const (
|
||||
// CommaStr const define
|
||||
CommaStr = ","
|
||||
// CommaChar define
|
||||
CommaChar = ','
|
||||
|
||||
// EqualStr define
|
||||
EqualStr = "="
|
||||
// EqualChar define
|
||||
EqualChar = '='
|
||||
|
||||
// ColonStr define
|
||||
ColonStr = ":"
|
||||
// ColonChar define
|
||||
ColonChar = ':'
|
||||
|
||||
// SemicolonStr semicolon define
|
||||
SemicolonStr = ";"
|
||||
// SemicolonChar define
|
||||
SemicolonChar = ';'
|
||||
|
||||
// PathStr define const
|
||||
PathStr = "/"
|
||||
// PathChar define
|
||||
PathChar = '/'
|
||||
|
||||
// DefaultSep comma string
|
||||
DefaultSep = ","
|
||||
|
||||
// SpaceChar char
|
||||
SpaceChar = ' '
|
||||
// SpaceStr string
|
||||
SpaceStr = " "
|
||||
|
||||
// NewlineChar char
|
||||
NewlineChar = '\n'
|
||||
// NewlineStr string
|
||||
NewlineStr = "\n"
|
||||
)
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package comdef
|
||||
|
||||
// Int interface type
|
||||
type Int interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
// Uint interface type
|
||||
type Uint interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
// Xint interface type. alias of Integer
|
||||
type Xint interface {
|
||||
Int | Uint
|
||||
}
|
||||
|
||||
// Integer interface type. all int or uint types
|
||||
type Integer interface {
|
||||
Int | Uint
|
||||
}
|
||||
|
||||
// Float interface type
|
||||
type Float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
// IntOrFloat interface type. all int and float types, but NOT uint types
|
||||
type IntOrFloat interface {
|
||||
Int | Float
|
||||
}
|
||||
|
||||
// Number interface type. contains all int, uint and float types
|
||||
type Number interface {
|
||||
Int | Uint | Float
|
||||
}
|
||||
|
||||
// XintOrFloat interface type. all int, uint and float types. alias of Number
|
||||
//
|
||||
// Deprecated: use Number instead.
|
||||
type XintOrFloat interface {
|
||||
Int | Uint | Float
|
||||
}
|
||||
|
||||
// NumberOrString interface type for (x)int, float, ~string types
|
||||
type NumberOrString interface {
|
||||
Int | Uint | Float | ~string
|
||||
}
|
||||
|
||||
// SortedType interface type. same of constraints.Ordered
|
||||
//
|
||||
// it can be ordered, that supports the operators < <= >= >.
|
||||
//
|
||||
// contains: (x)int, float, ~string types
|
||||
type SortedType interface {
|
||||
Int | Uint | Float | ~string
|
||||
}
|
||||
|
||||
// Compared type. alias of constraints.SortedType
|
||||
//
|
||||
// TODO: use type alias, will error on go1.18 Error: types.go:50: interface contains type constraints
|
||||
// type Compared = SortedType
|
||||
type Compared interface {
|
||||
Int | Uint | Float | ~string
|
||||
}
|
||||
|
||||
// SimpleType interface type. alias of ScalarType
|
||||
//
|
||||
// contains: (x)int, float, ~string, ~bool types
|
||||
type SimpleType interface {
|
||||
Int | Uint | Float | ~string | ~bool
|
||||
}
|
||||
|
||||
// ScalarType basic interface type.
|
||||
//
|
||||
// TIP: has bool type, it cannot be ordered
|
||||
//
|
||||
// contains: (x)int, float, ~string, ~bool types
|
||||
type ScalarType interface {
|
||||
Int | Uint | Float | ~string | ~bool
|
||||
}
|
||||
|
||||
// StrMap is alias of map[string]string
|
||||
type StrMap map[string]string
|
||||
|
||||
// AnyMap is alias of map[string]any
|
||||
type AnyMap map[string]any
|
||||
|
||||
// L2StrMap is alias of map[string]map[string]string
|
||||
type L2StrMap map[string]map[string]string
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
package goutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"github.com/gookit/goutil/mathutil"
|
||||
"github.com/gookit/goutil/reflects"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// Bool convert value to bool
|
||||
func Bool(v any) bool {
|
||||
bl, _ := comfunc.ToBool(v)
|
||||
return bl
|
||||
}
|
||||
|
||||
// ToBool try to convert type to bool
|
||||
func ToBool(v any) (bool, error) { return comfunc.ToBool(v) }
|
||||
|
||||
// String func. always converts value to string, will ignore error
|
||||
func String(v any) string {
|
||||
s, _ := strutil.AnyToString(v, false)
|
||||
return s
|
||||
}
|
||||
|
||||
// ToString convert value to string, will return error on fail.
|
||||
func ToString(v any) (string, error) { return strutil.AnyToString(v, true) }
|
||||
|
||||
// Int convert value to int
|
||||
func Int(v any) int {
|
||||
iv, _ := mathutil.ToInt(v)
|
||||
return iv
|
||||
}
|
||||
|
||||
// ToInt try to convert value to int
|
||||
func ToInt(v any) (int, error) { return mathutil.ToInt(v) }
|
||||
|
||||
// Int64 convert value to int64
|
||||
func Int64(v any) int64 {
|
||||
iv, _ := mathutil.ToInt64(v)
|
||||
return iv
|
||||
}
|
||||
|
||||
// ToInt64 try to convert value to int64
|
||||
func ToInt64(v any) (int64, error) { return mathutil.ToInt64(v) }
|
||||
|
||||
// Uint convert value to uint
|
||||
func Uint(v any) uint {
|
||||
iv, _ := mathutil.ToUint(v)
|
||||
return iv
|
||||
}
|
||||
|
||||
// ToUint try to convert value to uint
|
||||
func ToUint(v any) (uint, error) { return mathutil.ToUint(v) }
|
||||
|
||||
// Uint64 convert value to uint64
|
||||
func Uint64(v any) uint64 {
|
||||
iv, _ := mathutil.ToUint64(v)
|
||||
return iv
|
||||
}
|
||||
|
||||
// ToUint64 try to convert value to uint64
|
||||
func ToUint64(v any) (uint64, error) { return mathutil.ToUint64(v) }
|
||||
|
||||
// BoolString convert bool to string
|
||||
func BoolString(bl bool) string { return strconv.FormatBool(bl) }
|
||||
|
||||
// BaseTypeVal convert custom type or intX,uintX,floatX to generic base type.
|
||||
//
|
||||
// intX => int64
|
||||
// unitX => uint64
|
||||
// floatX => float64
|
||||
// string => string
|
||||
//
|
||||
// returns int64,uint64,string,float or error
|
||||
func BaseTypeVal(val any) (value any, err error) {
|
||||
return reflects.BaseTypeVal(reflect.ValueOf(val))
|
||||
}
|
||||
|
||||
// SafeKind convert input any value to given reflect.Kind type.
|
||||
func SafeKind(val any, kind reflect.Kind) (newVal any) {
|
||||
newVal, _ = ToKind(val, kind, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// SafeConv convert input any value to given reflect.Kind type.
|
||||
func SafeConv(val any, kind reflect.Kind) (newVal any) {
|
||||
newVal, _ = ToKind(val, kind, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// ConvTo convert input any value to given reflect.Kind.
|
||||
func ConvTo(val any, kind reflect.Kind) (newVal any, err error) {
|
||||
return ToKind(val, kind, nil)
|
||||
}
|
||||
|
||||
// ConvOrDefault convert input any value to given reflect.Kind.
|
||||
// if fail will return default value.
|
||||
func ConvOrDefault(val any, kind reflect.Kind, defVal any) any {
|
||||
newVal, err := ToKind(val, kind, nil)
|
||||
if err != nil {
|
||||
return defVal
|
||||
}
|
||||
return newVal
|
||||
}
|
||||
|
||||
// ToType
|
||||
// func ToType[T any](val any, kind reflect.Kind, fbFunc func(val any) (T, error)) (newVal T, err error) {
|
||||
// switch typVal.(type) { // assert ERROR
|
||||
// case string:
|
||||
// }
|
||||
// }
|
||||
|
||||
// ToKind convert input any value to given reflect.Kind type.
|
||||
//
|
||||
// TIPs: Only support kind: string, bool, intX, uintX, floatX
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// 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:
|
||||
var dstV int
|
||||
if dstV, err = mathutil.ToInt(val); err == nil {
|
||||
if dstV > math.MaxInt {
|
||||
return nil, fmt.Errorf("value overflow int. val: %v", val)
|
||||
}
|
||||
newVal = dstV
|
||||
}
|
||||
case reflect.Int8:
|
||||
var dstV int
|
||||
if dstV, err = mathutil.ToInt(val); err == nil {
|
||||
if dstV > math.MaxInt8 {
|
||||
return nil, fmt.Errorf("value overflow int8. val: %v", val)
|
||||
}
|
||||
newVal = int8(dstV)
|
||||
}
|
||||
case reflect.Int16:
|
||||
var dstV int
|
||||
if dstV, err = mathutil.ToInt(val); err == nil {
|
||||
if dstV > math.MaxInt16 {
|
||||
return nil, fmt.Errorf("value overflow int16. val: %v", val)
|
||||
}
|
||||
newVal = int16(dstV)
|
||||
}
|
||||
case reflect.Int32:
|
||||
var dstV int
|
||||
if dstV, err = mathutil.ToInt(val); err == nil {
|
||||
if dstV > math.MaxInt32 {
|
||||
return nil, fmt.Errorf("value overflow int32. val: %v", val)
|
||||
}
|
||||
newVal = int32(dstV)
|
||||
}
|
||||
case reflect.Int64:
|
||||
var dstV int64
|
||||
if dstV, err = mathutil.ToInt64(val); err == nil {
|
||||
newVal = dstV
|
||||
}
|
||||
case reflect.Uint:
|
||||
var dstV uint
|
||||
if dstV, err = mathutil.ToUint(val); err == nil {
|
||||
newVal = dstV
|
||||
}
|
||||
case reflect.Uint8:
|
||||
var dstV uint
|
||||
if dstV, err = mathutil.ToUint(val); err == nil {
|
||||
if dstV > math.MaxUint8 {
|
||||
return nil, fmt.Errorf("value overflow uint8. val: %v", val)
|
||||
}
|
||||
newVal = uint8(dstV)
|
||||
}
|
||||
case reflect.Uint16:
|
||||
var dstV uint
|
||||
if dstV, err = mathutil.ToUint(val); err == nil {
|
||||
if dstV > math.MaxUint16 {
|
||||
return nil, fmt.Errorf("value overflow uint16. val: %v", val)
|
||||
}
|
||||
newVal = uint16(dstV)
|
||||
}
|
||||
case reflect.Uint32:
|
||||
var dstV uint
|
||||
if dstV, err = mathutil.ToUint(val); err == nil {
|
||||
if dstV > math.MaxUint32 {
|
||||
return nil, fmt.Errorf("value overflow uint32. val: %v", val)
|
||||
}
|
||||
newVal = uint32(dstV)
|
||||
}
|
||||
case reflect.Uint64:
|
||||
var dstV uint64
|
||||
if dstV, err = mathutil.ToUint64(val); err == nil {
|
||||
newVal = dstV
|
||||
}
|
||||
case reflect.Float32:
|
||||
var dstV float64
|
||||
if dstV, err = mathutil.ToFloat(val); err == nil {
|
||||
if dstV > math.MaxFloat32 {
|
||||
return nil, fmt.Errorf("value overflow float32. val: %v", val)
|
||||
}
|
||||
newVal = float32(dstV)
|
||||
}
|
||||
case reflect.Float64:
|
||||
var dstV float64
|
||||
if dstV, err = mathutil.ToFloat(val); err == nil {
|
||||
newVal = dstV
|
||||
}
|
||||
case reflect.String:
|
||||
var dstV string
|
||||
if dstV, err = strutil.ToString(val); err == nil {
|
||||
newVal = dstV
|
||||
}
|
||||
case reflect.Bool:
|
||||
if bl, err1 := comfunc.ToBool(val); err1 == nil {
|
||||
newVal = bl
|
||||
} else {
|
||||
err = err1
|
||||
}
|
||||
default:
|
||||
if fbFunc != nil {
|
||||
newVal, err = fbFunc(val)
|
||||
} else {
|
||||
err = comdef.ErrConvType
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
# Env Util
|
||||
|
||||
Provide some commonly system or go ENV util functions.
|
||||
|
||||
## Install
|
||||
|
||||
```shell
|
||||
go get github.com/gookit/goutil/envutil
|
||||
```
|
||||
|
||||
## Go docs
|
||||
|
||||
- [Go docs](https://pkg.go.dev/github.com/gookit/goutil/envutil)
|
||||
|
||||
## Functions API
|
||||
|
||||
> **Note**: doc by run `go doc ./envutil`
|
||||
|
||||
```go
|
||||
func Environ() map[string]string
|
||||
func GetBool(name string, def ...bool) bool
|
||||
func GetInt(name string, def ...int) int
|
||||
func Getenv(name string, def ...string) string
|
||||
func HasShellEnv(shell string) bool
|
||||
func IsConsole(out io.Writer) bool
|
||||
func IsGithubActions() bool
|
||||
func IsLinux() bool
|
||||
func IsMSys() bool
|
||||
func IsMac() bool
|
||||
func IsSupport256Color() bool
|
||||
func IsSupportColor() bool
|
||||
func IsSupportTrueColor() bool
|
||||
func IsTerminal(fd uintptr) bool
|
||||
func IsWSL() bool
|
||||
func IsWin() bool
|
||||
func IsWindows() bool
|
||||
func ParseEnvValue(val string) string
|
||||
func ParseValue(val string) (newVal string)
|
||||
func SetEnvs(mp map[string]string)
|
||||
func StdIsTerminal() bool
|
||||
func VarParse(val string) string
|
||||
func VarReplace(s string) string
|
||||
```
|
||||
|
||||
## Code Check & Testing
|
||||
|
||||
```bash
|
||||
gofmt -w -l ./
|
||||
golint ./...
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
|
||||
```shell
|
||||
go test -v ./envutil/...
|
||||
```
|
||||
|
||||
**Test limit by regexp**:
|
||||
|
||||
```shell
|
||||
go test -v -run ^TestSetByKeys ./envutil/...
|
||||
```
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
package envutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
)
|
||||
|
||||
// DefaultEnvFile default file name
|
||||
const DefaultEnvFile = ".env"
|
||||
|
||||
// Dotenv load and parse dotenv files
|
||||
type Dotenv struct {
|
||||
// Files dot env file paths, allow multi files.
|
||||
// - filename support simple glob pattern. eg: ".env.*"
|
||||
//
|
||||
// default: [".env"]
|
||||
Files []string
|
||||
// BaseDir base dir for join files path
|
||||
//
|
||||
// default is workdir
|
||||
BaseDir string
|
||||
// UpperKey change key to upper on set ENV. default: true
|
||||
UpperKey bool
|
||||
// IgnoreNotExist only load exists.
|
||||
//
|
||||
// - default: false - will return error if not exists
|
||||
IgnoreNotExist bool
|
||||
// LoadFirstExist only load first exists env file on Files
|
||||
LoadFirstExist bool
|
||||
|
||||
loadFiles []string
|
||||
loadData map[string]string
|
||||
}
|
||||
|
||||
// NewDotenv create a new dotenv config
|
||||
func NewDotenv() *Dotenv {
|
||||
return &Dotenv{
|
||||
UpperKey: true,
|
||||
Files: []string{DefaultEnvFile},
|
||||
// init fields
|
||||
loadData: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAndInit load dotenv files and parse to os.Environ
|
||||
func (c *Dotenv) LoadAndInit() error {
|
||||
return c.doLoadFiles(c.Files)
|
||||
}
|
||||
|
||||
// LoadFiles append load dotenv files
|
||||
// - filename support simple glob pattern. eg: ".env.*"
|
||||
func (c *Dotenv) LoadFiles(files ...string) error {
|
||||
return c.doLoadFiles(files)
|
||||
}
|
||||
|
||||
// LoadText load dotenv contents and parse to os Env
|
||||
func (c *Dotenv) LoadText(contents string) error {
|
||||
return c.parseAndSetEnv(contents)
|
||||
}
|
||||
|
||||
// do load dotenv files
|
||||
func (c *Dotenv) doLoadFiles(files []string) error {
|
||||
var filePath string
|
||||
for _, file := range files {
|
||||
filePath = strings.TrimSpace(file)
|
||||
if c.BaseDir != "" && !filepath.IsAbs(file) {
|
||||
filePath = filepath.Join(c.BaseDir, file)
|
||||
}
|
||||
|
||||
// load and parse to ENV
|
||||
if err := c.loadFile(filePath); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.LoadFirstExist && len(c.loadFiles) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Dotenv) loadFile(filePath string) error {
|
||||
// filename support simple glob pattern.
|
||||
if strings.ContainsRune(filePath, '*') {
|
||||
matches, err := filepath.Glob(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, matchFile := range matches {
|
||||
if err = c.parseFile(matchFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Load single file
|
||||
return c.parseFile(filePath)
|
||||
}
|
||||
|
||||
// parseFile load single file and parse to ENV
|
||||
func (c *Dotenv) parseFile(filePath string) error {
|
||||
contents, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
// IgnoreNotExist: skip non-existent files
|
||||
if c.IgnoreNotExist && os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.parseAndSetEnv(string(contents))
|
||||
if err == nil {
|
||||
c.loadFiles = append(c.loadFiles, filePath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Dotenv) parseAndSetEnv(contents string) error {
|
||||
// Parse ENV lines
|
||||
envMp, err := comfunc.ParseEnvLines(contents, comfunc.ParseEnvLineOption{
|
||||
SkipOnErrorLine: true,
|
||||
})
|
||||
|
||||
// Set to ENV
|
||||
for key, val := range envMp {
|
||||
key = strings.ToUpper(key)
|
||||
c.loadData[key] = val
|
||||
_ = os.Setenv(key, val)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// UnloadEnv remove loaded dotenv data from os.Environ
|
||||
func (c *Dotenv) UnloadEnv() bool {
|
||||
if len(c.loadData) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for key := range c.loadData {
|
||||
_ = os.Unsetenv(key)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// LoadedData get loaded dotenv data map
|
||||
func (c *Dotenv) LoadedData() map[string]string {
|
||||
return c.loadData
|
||||
}
|
||||
|
||||
// LoadedFiles get loaded dotenv files
|
||||
func (c *Dotenv) LoadedFiles() []string {
|
||||
return c.loadFiles
|
||||
}
|
||||
|
||||
// Reset unload all loaded ENV and reset data
|
||||
func (c *Dotenv) Reset() {
|
||||
c.UnloadEnv()
|
||||
c.loadFiles = nil
|
||||
c.loadData = make(map[string]string)
|
||||
}
|
||||
|
||||
//
|
||||
// region standard dotenv instance
|
||||
//
|
||||
|
||||
var stdEnv = NewDotenv()
|
||||
|
||||
// StdDotenv get standard dotenv instance
|
||||
func StdDotenv() *Dotenv { return stdEnv }
|
||||
|
||||
// DotenvLoad load dotenv file and parse to ENV
|
||||
func DotenvLoad(fns ...func(cfg *Dotenv)) error {
|
||||
for _, fn := range fns {
|
||||
fn(stdEnv)
|
||||
}
|
||||
return stdEnv.LoadAndInit()
|
||||
}
|
||||
|
||||
// LoadEnvFiles load dotenv files and parse to ENV
|
||||
func LoadEnvFiles(baseDir string, files ...string) error {
|
||||
return DotenvLoad(func(cfg *Dotenv) {
|
||||
cfg.BaseDir = baseDir
|
||||
if len(files) > 0 {
|
||||
cfg.Files = files
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// LoadedEnvFiles get loaded dotenv files
|
||||
func LoadedEnvFiles() []string {
|
||||
return stdEnv.LoadedFiles()
|
||||
}
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// Package envutil provide some commonly ENV util functions.
|
||||
package envutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"github.com/gookit/goutil/internal/varexpr"
|
||||
)
|
||||
|
||||
// ValueGetter Env value provider func.
|
||||
//
|
||||
// TIPS: you can custom provide data.
|
||||
var ValueGetter = os.Getenv
|
||||
|
||||
// VarReplace replaces ${var} or $var in the string according to the values.
|
||||
//
|
||||
// is alias of the os.ExpandEnv()
|
||||
func VarReplace(s string) string { return os.ExpandEnv(s) }
|
||||
|
||||
// ParseOrErr parse ENV var value from input string, support default value.
|
||||
//
|
||||
// Diff with the ParseValue, this support return error.
|
||||
//
|
||||
// With error format: ${VAR_NAME | ?error}
|
||||
func ParseOrErr(val string) (string, error) {
|
||||
return varexpr.Parse(val)
|
||||
}
|
||||
|
||||
// ParseValue parse ENV var value from input string, support default value.
|
||||
//
|
||||
// Format:
|
||||
//
|
||||
// ${var_name} Only var name
|
||||
// ${var_name | default} With default value
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// envutil.ParseValue("${ APP_NAME }")
|
||||
// envutil.ParseValue("${ APP_ENV | dev }")
|
||||
func ParseValue(val string) string {
|
||||
return varexpr.SafeParse(val)
|
||||
}
|
||||
|
||||
// VarParse alias of the ParseValue
|
||||
func VarParse(val string) string { return varexpr.SafeParse(val) }
|
||||
|
||||
// ParseEnvValue alias of the ParseValue
|
||||
func ParseEnvValue(val string) string { return varexpr.SafeParse(val) }
|
||||
|
||||
// SplitText2map parse ENV text to map. Can use to parse .env file contents.
|
||||
func SplitText2map(text string) map[string]string {
|
||||
envMp, _ := comfunc.ParseEnvLines(text, comfunc.ParseEnvLineOption{
|
||||
SkipOnErrorLine: true,
|
||||
})
|
||||
return envMp
|
||||
}
|
||||
|
||||
// SplitLineToKv parse ENV line to k-v. eg: 'DEBUG=true' => ['DEBUG', 'true']
|
||||
func SplitLineToKv(line string) (string, string) {
|
||||
if line = strings.TrimSpace(line); line == "" {
|
||||
return "", ""
|
||||
}
|
||||
return comfunc.SplitLineToKv(line, "=")
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package envutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
"github.com/gookit/goutil/x/basefn"
|
||||
)
|
||||
|
||||
// HasEnv check ENV key exists
|
||||
func HasEnv(name string) bool {
|
||||
_, ok := os.LookupEnv(name)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Getenv get ENV value by key name, can with default value
|
||||
func Getenv(name string, def ...string) string {
|
||||
val := os.Getenv(name)
|
||||
if val == "" && len(def) > 0 {
|
||||
val = def[0]
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// MustGet get ENV value by key name, if not exists or empty, will panic
|
||||
func MustGet(name string) string {
|
||||
if val := os.Getenv(name); val != "" {
|
||||
return val
|
||||
}
|
||||
panic("ENV key '" + name + "' not exists")
|
||||
}
|
||||
|
||||
// GetInt get int ENV value by key name, can with default value
|
||||
func GetInt(name string, def ...int) int {
|
||||
if val := os.Getenv(name); val != "" {
|
||||
return strutil.QuietInt(val)
|
||||
}
|
||||
return basefn.FirstOr(def, 0)
|
||||
}
|
||||
|
||||
// GetBool get bool ENV value by key name, can with default value
|
||||
func GetBool(name string, def ...bool) bool {
|
||||
if val := os.Getenv(name); val != "" {
|
||||
return strutil.QuietBool(val)
|
||||
}
|
||||
return basefn.FirstOr(def, false)
|
||||
}
|
||||
|
||||
// GetOne get one not empty ENV value by input names.
|
||||
func GetOne(names []string, defVal ...string) string {
|
||||
for _, name := range names {
|
||||
if val := os.Getenv(name); val != "" {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetMulti ENV values by input names.
|
||||
func GetMulti(names ...string) map[string]string {
|
||||
valMap := make(map[string]string, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
if val := os.Getenv(name); val != "" {
|
||||
valMap[name] = val
|
||||
}
|
||||
}
|
||||
return valMap
|
||||
}
|
||||
|
||||
// OnExist check ENV value by key name, will call fn on value exists(not-empty)
|
||||
func OnExist(name string, fn func(val string)) bool {
|
||||
if val := os.Getenv(name); val != "" {
|
||||
fn(val)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// EnvPaths get and split $PATH to []string
|
||||
func EnvPaths() []string {
|
||||
return filepath.SplitList(os.Getenv("PATH"))
|
||||
}
|
||||
|
||||
// EnvMap like os.Environ, but will returns key-value map[string]string data.
|
||||
func EnvMap() map[string]string { return comfunc.Environ() }
|
||||
|
||||
// Environ like os.Environ, but will returns key-value map[string]string data.
|
||||
func Environ() map[string]string { return comfunc.Environ() }
|
||||
|
||||
// SearchEnvKeys values by given keywords
|
||||
func SearchEnvKeys(keywords string) map[string]string {
|
||||
return SearchEnv(keywords, false)
|
||||
}
|
||||
|
||||
// SearchEnv values by given keywords
|
||||
func SearchEnv(keywords string, matchValue bool) map[string]string {
|
||||
founded := make(map[string]string)
|
||||
|
||||
for name, val := range comfunc.Environ() {
|
||||
if strutil.IContains(name, keywords) {
|
||||
founded[name] = val
|
||||
} else if matchValue && strutil.IContains(val, keywords) {
|
||||
founded[name] = val
|
||||
}
|
||||
}
|
||||
|
||||
return founded
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package envutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"github.com/gookit/goutil/sysutil"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// IsWin system. linux windows darwin
|
||||
func IsWin() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
|
||||
// IsWindows system. alias of IsWin
|
||||
func IsWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
|
||||
// IsMac system
|
||||
func IsMac() bool {
|
||||
return runtime.GOOS == "darwin"
|
||||
}
|
||||
|
||||
// IsLinux system
|
||||
func IsLinux() bool {
|
||||
return runtime.GOOS == "linux"
|
||||
}
|
||||
|
||||
// IsMSys msys(MINGW64) env. alias of the sysutil.IsMSys()
|
||||
func IsMSys() bool {
|
||||
return sysutil.IsMSys()
|
||||
}
|
||||
|
||||
// IsTerminal isatty check
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// envutil.IsTerminal(os.Stdout.Fd())
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
// return isatty.IsTerminal(fd) // "github.com/mattn/go-isatty"
|
||||
return term.IsTerminal(int(fd))
|
||||
}
|
||||
|
||||
// StdIsTerminal os.Stdout is terminal
|
||||
func StdIsTerminal() bool {
|
||||
return IsTerminal(os.Stdout.Fd())
|
||||
}
|
||||
|
||||
// IsConsole check out is console env. alias of the sysutil.IsConsole()
|
||||
func IsConsole(out io.Writer) bool {
|
||||
return sysutil.IsConsole(out)
|
||||
}
|
||||
|
||||
// HasShellEnv has shell env check.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// HasShellEnv("sh")
|
||||
// HasShellEnv("bash")
|
||||
func HasShellEnv(shell string) bool {
|
||||
return comfunc.HasShellEnv(shell)
|
||||
}
|
||||
|
||||
// Support color:
|
||||
//
|
||||
// "TERM=xterm"
|
||||
// "TERM=xterm-vt220"
|
||||
// "TERM=xterm-256color"
|
||||
// "TERM=screen-256color"
|
||||
// "TERM=tmux-256color"
|
||||
// "TERM=rxvt-unicode-256color"
|
||||
//
|
||||
// Don't support color:
|
||||
//
|
||||
// "TERM=cygwin"
|
||||
var specialColorTerms = map[string]bool{
|
||||
"alacritty": true,
|
||||
}
|
||||
|
||||
// IsSupportColor check current console is support color.
|
||||
//
|
||||
// Supported:
|
||||
//
|
||||
// linux, mac, or Windows's ConEmu, Cmder, putty, git-bash.exe
|
||||
//
|
||||
// Not support:
|
||||
//
|
||||
// windows cmd.exe, powerShell.exe
|
||||
func IsSupportColor() bool {
|
||||
envTerm := os.Getenv("TERM")
|
||||
if strings.Contains(envTerm, "xterm") {
|
||||
return true
|
||||
}
|
||||
|
||||
// it's special color term
|
||||
if _, ok := specialColorTerms[envTerm]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// like on ConEmu software, e.g "ConEmuANSI=ON"
|
||||
if os.Getenv("ConEmuANSI") == "ON" {
|
||||
return true
|
||||
}
|
||||
|
||||
// like on ConEmu software, e.g "ANSICON=189x2000 (189x43)"
|
||||
if os.Getenv("ANSICON") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// up: if support 256-color, can also support basic color.
|
||||
return IsSupport256Color()
|
||||
}
|
||||
|
||||
// IsSupport256Color render
|
||||
func IsSupport256Color() bool {
|
||||
// "TERM=xterm-256color"
|
||||
// "TERM=screen-256color"
|
||||
// "TERM=tmux-256color"
|
||||
// "TERM=rxvt-unicode-256color"
|
||||
supported := strings.Contains(os.Getenv("TERM"), "256color")
|
||||
if !supported {
|
||||
// up: if support true-color, can also support 256-color.
|
||||
supported = IsSupportTrueColor()
|
||||
}
|
||||
|
||||
return supported
|
||||
}
|
||||
|
||||
// IsSupportTrueColor render. IsSupportRGBColor
|
||||
func IsSupportTrueColor() bool {
|
||||
// "COLORTERM=truecolor"
|
||||
return strings.Contains(os.Getenv("COLORTERM"), "truecolor")
|
||||
}
|
||||
|
||||
// IsGithubActions env
|
||||
func IsGithubActions() bool {
|
||||
return os.Getenv("GITHUB_ACTIONS") == "true"
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package envutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
)
|
||||
|
||||
// SetEnvMap set multi ENV(string-map) to os
|
||||
func SetEnvMap(mp map[string]string) {
|
||||
for key, value := range mp {
|
||||
_ = os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// SetEnvs set multi k-v ENV pairs to os
|
||||
func SetEnvs(kvPairs ...string) {
|
||||
if len(kvPairs)%2 == 1 {
|
||||
panic("envutil.SetEnvs: odd argument count")
|
||||
}
|
||||
|
||||
for i := 0; i < len(kvPairs); i += 2 {
|
||||
_ = os.Setenv(kvPairs[i], kvPairs[i+1])
|
||||
}
|
||||
}
|
||||
|
||||
// UnsetEnvs from os
|
||||
func UnsetEnvs(keys ...string) {
|
||||
for _, key := range keys {
|
||||
_ = os.Unsetenv(key)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadText parse multiline text to ENV. Can use to load .env file contents.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// envutil.LoadText(fsutil.ReadFile(".env"))
|
||||
func LoadText(text string) {
|
||||
envMp := SplitText2map(text)
|
||||
for key, value := range envMp {
|
||||
_ = os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadString set line to ENV. e.g.: "KEY=VALUE"
|
||||
func LoadString(line string) bool {
|
||||
k, v := comfunc.SplitLineToKv(line, "=")
|
||||
if len(k) > 0 {
|
||||
return os.Setenv(k, v) == nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
# ErrorX
|
||||
|
||||
`errorx` provide an enhanced error implements for go, allow with stacktraces and wrap another error.
|
||||
|
||||
## Install
|
||||
|
||||
```go
|
||||
go get github.com/gookit/goutil/errorx
|
||||
```
|
||||
|
||||
## Go docs
|
||||
|
||||
- [Go docs](https://pkg.go.dev/github.com/gookit/goutil/errorx)
|
||||
|
||||
## Usage
|
||||
|
||||
### Create error with call stack info
|
||||
|
||||
- use the `errorx.New` instead `errors.New`
|
||||
|
||||
```go
|
||||
func doSomething() error {
|
||||
if false {
|
||||
// return errors.New("a error happen")
|
||||
return errorx.New("a error happen")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- use the `errorx.Newf` or `errorx.Errorf` instead `fmt.Errorf`
|
||||
|
||||
```go
|
||||
func doSomething() error {
|
||||
if false {
|
||||
// return fmt.Errorf("a error %s", "happen")
|
||||
return errorx.Newf("a error %s", "happen")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Wrap the previous error
|
||||
|
||||
used like this before:
|
||||
|
||||
```go
|
||||
if err := SomeFunc(); err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
can be replaced with:
|
||||
|
||||
```go
|
||||
if err := SomeFunc(); err != nil {
|
||||
return errors.Stacked(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Output details
|
||||
|
||||
error output details for use `errorx`
|
||||
|
||||
### Use errorx.New
|
||||
|
||||
`errorx` functions for new error:
|
||||
|
||||
```go
|
||||
func New(msg string) error
|
||||
func Newf(tpl string, vars ...interface{}) error
|
||||
func Errorf(tpl string, vars ...interface{}) error
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```go
|
||||
err := errorx.New("the error message")
|
||||
|
||||
fmt.Println(err)
|
||||
// fmt.Printf("%v\n", err)
|
||||
// fmt.Printf("%#v\n", err)
|
||||
```
|
||||
|
||||
> from the test: `errorx_test.TestNew()`
|
||||
|
||||
**Output**:
|
||||
|
||||
```text
|
||||
the error message
|
||||
STACK:
|
||||
github.com/gookit/goutil/errorx_test.returnXErr()
|
||||
/Users/inhere/Workspace/godev/gookit/goutil/errorx/errorx_test.go:21
|
||||
github.com/gookit/goutil/errorx_test.returnXErrL2()
|
||||
/Users/inhere/Workspace/godev/gookit/goutil/errorx/errorx_test.go:25
|
||||
github.com/gookit/goutil/errorx_test.TestNew()
|
||||
/Users/inhere/Workspace/godev/gookit/goutil/errorx/errorx_test.go:29
|
||||
testing.tRunner()
|
||||
/usr/local/Cellar/go/1.18/libexec/src/testing/testing.go:1439
|
||||
runtime.goexit()
|
||||
/usr/local/Cellar/go/1.18/libexec/src/runtime/asm_amd64.s:1571
|
||||
```
|
||||
|
||||
### Use errorx.With
|
||||
|
||||
`errorx` functions for with another error:
|
||||
|
||||
```go
|
||||
func With(err error, msg string) error
|
||||
func Withf(err error, tpl string, vars ...interface{}) error
|
||||
```
|
||||
|
||||
With a go raw error:
|
||||
|
||||
```go
|
||||
err1 := returnErr("first error message")
|
||||
|
||||
err2 := errorx.With(err1, "second error message")
|
||||
fmt.Println(err2)
|
||||
```
|
||||
|
||||
> from the test: `errorx_test.TestWith_goerr()`
|
||||
|
||||
**Output**:
|
||||
|
||||
```text
|
||||
second error message
|
||||
STACK:
|
||||
github.com/gookit/goutil/errorx_test.TestWith_goerr()
|
||||
/Users/inhere/Workspace/godev/gookit/goutil/errorx/errorx_test.go:51
|
||||
testing.tRunner()
|
||||
/usr/local/Cellar/go/1.18/libexec/src/testing/testing.go:1439
|
||||
runtime.goexit()
|
||||
/usr/local/Cellar/go/1.18/libexec/src/runtime/asm_amd64.s:1571
|
||||
|
||||
Previous: first error message
|
||||
```
|
||||
|
||||
With a `errorx` error:
|
||||
|
||||
```go
|
||||
err1 := returnXErr("first error message")
|
||||
err2 := errorx.With(err1, "second error message")
|
||||
fmt.Println(err2)
|
||||
```
|
||||
|
||||
> from the test: `errorx_test.TestWith_errorx()`
|
||||
|
||||
**Output**:
|
||||
|
||||
```text
|
||||
second error message
|
||||
STACK:
|
||||
github.com/gookit/goutil/errorx_test.TestWith_errorx()
|
||||
/Users/inhere/Workspace/godev/gookit/goutil/errorx/errorx_test.go:64
|
||||
testing.tRunner()
|
||||
/usr/local/Cellar/go/1.18/libexec/src/testing/testing.go:1439
|
||||
runtime.goexit()
|
||||
/usr/local/Cellar/go/1.18/libexec/src/runtime/asm_amd64.s:1571
|
||||
|
||||
Previous: first error message
|
||||
STACK:
|
||||
github.com/gookit/goutil/errorx_test.returnXErr()
|
||||
/Users/inhere/Workspace/godev/gookit/goutil/errorx/errorx_test.go:21
|
||||
github.com/gookit/goutil/errorx_test.TestWith_errorx()
|
||||
/Users/inhere/Workspace/godev/gookit/goutil/errorx/errorx_test.go:61
|
||||
testing.tRunner()
|
||||
/usr/local/Cellar/go/1.18/libexec/src/testing/testing.go:1439
|
||||
runtime.goexit()
|
||||
/usr/local/Cellar/go/1.18/libexec/src/runtime/asm_amd64.s:1571
|
||||
|
||||
```
|
||||
|
||||
### Use errorx.Wrap
|
||||
|
||||
```go
|
||||
err := errors.New("first error message")
|
||||
err = errorx.Wrap(err, "second error message")
|
||||
err = errorx.Wrap(err, "third error message")
|
||||
// fmt.Println(err)
|
||||
// fmt.Println(err.Error())
|
||||
```
|
||||
|
||||
Direct print the `err`:
|
||||
|
||||
```text
|
||||
third error message
|
||||
Previous: second error message
|
||||
Previous: first error message
|
||||
```
|
||||
|
||||
Print the `err.Error()`:
|
||||
|
||||
```text
|
||||
third error message; second error message; first error message
|
||||
```
|
||||
|
||||
## Code Check & Testing
|
||||
|
||||
```bash
|
||||
gofmt -w -l ./
|
||||
golint ./...
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
|
||||
```shell
|
||||
go test -v ./errorx/...
|
||||
```
|
||||
|
||||
**Test limit by regexp**:
|
||||
|
||||
```shell
|
||||
go test -v -run ^TestSetByKeys ./errorx/...
|
||||
```
|
||||
|
||||
## Refers
|
||||
|
||||
- golang errors
|
||||
- https://github.com/joomcode/errorx
|
||||
- https://github.com/pkg/errors
|
||||
- https://github.com/juju/errors
|
||||
- https://github.com/go-errors/errors
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gookit/goutil/arrutil"
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
)
|
||||
|
||||
// IsTrue assert result is true, otherwise will return error
|
||||
func IsTrue(result bool, fmtAndArgs ...any) error {
|
||||
if !result {
|
||||
return errors.New(formatErrMsg("result should be True", fmtAndArgs))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsFalse assert result is false, otherwise will return error
|
||||
func IsFalse(result bool, fmtAndArgs ...any) error {
|
||||
if result {
|
||||
return errors.New(formatErrMsg("result should be False", fmtAndArgs))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsIn value should be in the list, otherwise will return error
|
||||
func IsIn[T comdef.ScalarType](value T, list []T, fmtAndArgs ...any) error {
|
||||
if arrutil.NotIn(value, list) {
|
||||
var errMsg string
|
||||
if len(fmtAndArgs) > 0 {
|
||||
errMsg = comfunc.FormatWithArgs(fmtAndArgs)
|
||||
} else {
|
||||
errMsg = fmt.Sprintf("value should be in the %v", list)
|
||||
}
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotIn value should not be in the list, otherwise will return error
|
||||
func NotIn[T comdef.ScalarType](value T, list []T, fmtAndArgs ...any) error {
|
||||
if arrutil.In(value, list) {
|
||||
var errMsg string
|
||||
if len(fmtAndArgs) > 0 {
|
||||
errMsg = comfunc.FormatWithArgs(fmtAndArgs)
|
||||
} else {
|
||||
errMsg = fmt.Sprintf("value should not be in the %v", list)
|
||||
}
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatErrMsg(defMsg string, fmtAndArgs []any) string {
|
||||
if len(fmtAndArgs) > 0 {
|
||||
return comfunc.FormatWithArgs(fmtAndArgs)
|
||||
}
|
||||
return defMsg
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrorCoder interface
|
||||
type ErrorCoder interface {
|
||||
error
|
||||
Code() int
|
||||
}
|
||||
|
||||
// ErrorR useful for web service replay/response.
|
||||
// code == 0 is successful. otherwise, is failed.
|
||||
type ErrorR interface {
|
||||
ErrorCoder
|
||||
fmt.Stringer
|
||||
IsSuc() bool
|
||||
IsFail() bool
|
||||
}
|
||||
|
||||
// error reply struct
|
||||
type errorR struct {
|
||||
code int
|
||||
msg string
|
||||
}
|
||||
|
||||
// NewR code with error response
|
||||
func NewR(code int, msg string) ErrorR {
|
||||
return &errorR{code: code, msg: msg}
|
||||
}
|
||||
|
||||
// Fail code with error response
|
||||
func Fail(code int, msg string) ErrorR {
|
||||
return &errorR{code: code, msg: msg}
|
||||
}
|
||||
|
||||
// Failf code with error response
|
||||
func Failf(code int, tpl string, v ...any) ErrorR {
|
||||
return &errorR{code: code, msg: fmt.Sprintf(tpl, v...)}
|
||||
}
|
||||
|
||||
// Suc success response reply
|
||||
func Suc(msg string) ErrorR {
|
||||
return &errorR{code: 0, msg: msg}
|
||||
}
|
||||
|
||||
// IsSuc code value check
|
||||
func (e *errorR) IsSuc() bool {
|
||||
return e.code == 0
|
||||
}
|
||||
|
||||
// IsFail code value check
|
||||
func (e *errorR) IsFail() bool {
|
||||
return e.code != 0
|
||||
}
|
||||
|
||||
// Code value
|
||||
func (e *errorR) Code() int {
|
||||
return e.code
|
||||
}
|
||||
|
||||
// Error string
|
||||
func (e *errorR) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
// String get
|
||||
func (e *errorR) String() string {
|
||||
return e.msg + "(code: " + strconv.FormatInt(int64(e.code), 10) + ")"
|
||||
}
|
||||
|
||||
// GoString get.
|
||||
func (e *errorR) GoString() string {
|
||||
return e.String()
|
||||
}
|
||||
|
||||
// ErrorM multi error map
|
||||
type ErrorM map[string]error
|
||||
|
||||
// ErrMap alias of ErrorM
|
||||
type ErrMap = ErrorM
|
||||
|
||||
// Error string
|
||||
func (e ErrorM) Error() string {
|
||||
var sb strings.Builder
|
||||
for name, err := range e {
|
||||
sb.WriteString(name)
|
||||
sb.WriteByte(':')
|
||||
sb.WriteString(err.Error())
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ErrorOrNil error
|
||||
func (e ErrorM) ErrorOrNil() error {
|
||||
if len(e) == 0 {
|
||||
return nil
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// IsEmpty error
|
||||
func (e ErrorM) IsEmpty() bool {
|
||||
return len(e) == 0
|
||||
}
|
||||
|
||||
// One error
|
||||
func (e ErrorM) One() error {
|
||||
for _, err := range e {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Errors multi error list
|
||||
type Errors []error
|
||||
|
||||
// ErrList alias for Errors
|
||||
type ErrList = Errors
|
||||
|
||||
// Error string
|
||||
func (es Errors) Error() string {
|
||||
var sb strings.Builder
|
||||
for _, err := range es {
|
||||
sb.WriteString(err.Error())
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ErrorOrNil error
|
||||
func (es Errors) ErrorOrNil() error {
|
||||
if len(es) == 0 {
|
||||
return nil
|
||||
}
|
||||
return es
|
||||
}
|
||||
|
||||
// IsEmpty error
|
||||
func (es Errors) IsEmpty() bool {
|
||||
return len(es) == 0
|
||||
}
|
||||
|
||||
// First error
|
||||
func (es Errors) First() error {
|
||||
if len(es) > 0 {
|
||||
return es[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
// Package errorx provide an enhanced error implements for go,
|
||||
// allow with stacktraces and wrap another error.
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Causer interface for get first cause error
|
||||
type Causer interface {
|
||||
// Cause returns the first cause error by call err.Cause().
|
||||
// Otherwise, will returns current error.
|
||||
Cause() error
|
||||
}
|
||||
|
||||
// Unwrapper interface for get previous error
|
||||
type Unwrapper interface {
|
||||
// Unwrap returns previous error by call err.Unwrap().
|
||||
// Otherwise, will returns nil.
|
||||
Unwrap() error
|
||||
}
|
||||
|
||||
// XErrorFace interface
|
||||
type XErrorFace interface {
|
||||
error
|
||||
Causer
|
||||
Unwrapper
|
||||
}
|
||||
|
||||
// Exception interface
|
||||
// type Exception interface {
|
||||
// XErrorFace
|
||||
// Code() string
|
||||
// Message() string
|
||||
// StackString() string
|
||||
// }
|
||||
|
||||
/*************************************************************
|
||||
* implements XErrorFace interface
|
||||
*************************************************************/
|
||||
|
||||
// ErrorX struct
|
||||
//
|
||||
// TIPS:
|
||||
//
|
||||
// fmt pkg call order: Format > GoString > Error > String
|
||||
type ErrorX struct {
|
||||
// trace stack
|
||||
*stack
|
||||
prev error
|
||||
msg string
|
||||
}
|
||||
|
||||
// Cause implements Causer.
|
||||
func (e *ErrorX) Cause() error {
|
||||
if e.prev == nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if ex, ok := e.prev.(*ErrorX); ok {
|
||||
return ex.Cause()
|
||||
}
|
||||
return e.prev
|
||||
}
|
||||
|
||||
// Unwrap implements Unwrapper.
|
||||
func (e *ErrorX) Unwrap() error {
|
||||
return e.prev
|
||||
}
|
||||
|
||||
// Format error, will output stack information.
|
||||
func (e *ErrorX) Format(s fmt.State, verb rune) {
|
||||
// format current error: only output on have msg
|
||||
if len(e.msg) > 0 {
|
||||
_, _ = io.WriteString(s, e.msg)
|
||||
if e.stack != nil {
|
||||
e.stack.Format(s, verb)
|
||||
}
|
||||
}
|
||||
|
||||
// format prev error
|
||||
if e.prev == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = s.Write([]byte("\nPrevious: "))
|
||||
if ex, ok := e.prev.(*ErrorX); ok {
|
||||
ex.Format(s, verb)
|
||||
} else {
|
||||
_, _ = s.Write([]byte(e.prev.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// GoString to GO string, contains stack information.
|
||||
// printing an error with %#v will produce useful information.
|
||||
func (e *ErrorX) GoString() string {
|
||||
// var sb strings.Builder
|
||||
var buf bytes.Buffer
|
||||
_, _ = e.WriteTo(&buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Error msg string, not contains stack information.
|
||||
func (e *ErrorX) Error() string {
|
||||
var buf bytes.Buffer
|
||||
e.writeMsgTo(&buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// String error to string, contains stack information.
|
||||
func (e *ErrorX) String() string {
|
||||
return e.GoString()
|
||||
}
|
||||
|
||||
// WriteTo write the error to a writer, contains stack information.
|
||||
func (e *ErrorX) WriteTo(w io.Writer) (n int64, err error) {
|
||||
// current error: only output on have msg
|
||||
if len(e.msg) > 0 {
|
||||
_, _ = w.Write([]byte(e.msg))
|
||||
|
||||
// with stack
|
||||
if e.stack != nil {
|
||||
_, _ = e.stack.WriteTo(w)
|
||||
}
|
||||
}
|
||||
|
||||
// with prev error
|
||||
if e.prev != nil {
|
||||
_, _ = io.WriteString(w, "\nPrevious: ")
|
||||
|
||||
if ex, ok := e.prev.(*ErrorX); ok {
|
||||
_, _ = ex.WriteTo(w)
|
||||
} else {
|
||||
_, _ = io.WriteString(w, e.prev.Error())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Message error message of current
|
||||
func (e *ErrorX) Message() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
// StackString returns error stack string of current.
|
||||
func (e *ErrorX) StackString() string {
|
||||
if e.stack != nil {
|
||||
return e.stack.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// writeMsgTo write the error msg to a writer
|
||||
func (e *ErrorX) writeMsgTo(w io.Writer) {
|
||||
// current error
|
||||
if len(e.msg) > 0 {
|
||||
_, _ = w.Write([]byte(e.msg))
|
||||
}
|
||||
|
||||
// with prev error
|
||||
if e.prev != nil {
|
||||
_, _ = w.Write([]byte("; "))
|
||||
if ex, ok := e.prev.(*ErrorX); ok {
|
||||
ex.writeMsgTo(w)
|
||||
} else {
|
||||
_, _ = io.WriteString(w, e.prev.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CallerFunc returns the error caller func. if stack is nil, will return nil
|
||||
func (e *ErrorX) CallerFunc() *Func {
|
||||
if e.stack == nil {
|
||||
return nil
|
||||
}
|
||||
return FuncForPC(e.stack.CallerPC())
|
||||
}
|
||||
|
||||
// Location information for the caller func. more please see CallerFunc
|
||||
//
|
||||
// Returns eg:
|
||||
//
|
||||
// github.com/gookit/goutil/errorx_test.TestWithPrev(), errorx_test.go:34
|
||||
func (e *ErrorX) Location() string {
|
||||
if e.stack == nil {
|
||||
return "unknown"
|
||||
}
|
||||
return e.CallerFunc().Location()
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* new error with call stacks
|
||||
*************************************************************/
|
||||
|
||||
// New error message and with caller stacks
|
||||
func New(msg string) error {
|
||||
return &ErrorX{
|
||||
msg: msg,
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
// Newf error with format message, and with caller stacks.
|
||||
// alias of Errorf()
|
||||
func Newf(tpl string, vars ...any) error {
|
||||
return &ErrorX{
|
||||
msg: fmt.Sprintf(tpl, vars...),
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf error with format message, and with caller stacks
|
||||
func Errorf(tpl string, vars ...any) error {
|
||||
return &ErrorX{
|
||||
msg: fmt.Sprintf(tpl, vars...),
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
// With prev error and error message, and with caller stacks
|
||||
func With(err error, msg string) error {
|
||||
return &ErrorX{
|
||||
msg: msg,
|
||||
prev: err,
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
// Withf error and with format message, and with caller stacks
|
||||
func Withf(err error, tpl string, vars ...any) error {
|
||||
return &ErrorX{
|
||||
msg: fmt.Sprintf(tpl, vars...),
|
||||
prev: err,
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrev error and message, and with caller stacks. alias of With()
|
||||
func WithPrev(err error, msg string) error {
|
||||
return &ErrorX{
|
||||
msg: msg,
|
||||
prev: err,
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrevf error and with format message, and with caller stacks. alias of Withf()
|
||||
func WithPrevf(err error, tpl string, vars ...any) error {
|
||||
return &ErrorX{
|
||||
msg: fmt.Sprintf(tpl, vars...),
|
||||
prev: err,
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* wrap go error with call stacks
|
||||
*************************************************************/
|
||||
|
||||
// WithStack wrap a go error with a stacked trace. If err is nil, will return nil.
|
||||
func WithStack(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &ErrorX{
|
||||
msg: err.Error(),
|
||||
// prev: err,
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
// Traced warp a go error and with caller stacks. alias of WithStack()
|
||||
func Traced(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &ErrorX{
|
||||
msg: err.Error(),
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
// Stacked warp a go error and with caller stacks. alias of WithStack()
|
||||
func Stacked(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &ErrorX{
|
||||
msg: err.Error(),
|
||||
stack: callersStack(stdOpt.SkipDepth, stdOpt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
// WithOptions new error with some option func
|
||||
func WithOptions(msg string, fns ...func(opt *ErrStackOpt)) error {
|
||||
opt := newErrOpt()
|
||||
for _, fn := range fns {
|
||||
fn(opt)
|
||||
}
|
||||
|
||||
return &ErrorX{
|
||||
msg: msg,
|
||||
stack: callersStack(opt.SkipDepth, opt.TraceDepth),
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* helper func for wrap error without stacks
|
||||
*************************************************************/
|
||||
|
||||
// Wrap error and with message, but not with stack
|
||||
func Wrap(err error, msg string) error {
|
||||
if err == nil {
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
return &ErrorX{
|
||||
msg: msg,
|
||||
prev: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapf error with format message, but not with stack
|
||||
func Wrapf(err error, tpl string, vars ...any) error {
|
||||
if err == nil {
|
||||
return fmt.Errorf(tpl, vars...)
|
||||
}
|
||||
|
||||
return &ErrorX{
|
||||
msg: fmt.Sprintf(tpl, vars...),
|
||||
prev: err,
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// stack represents a stack of program counters.
|
||||
type stack []uintptr
|
||||
|
||||
// Format stack trace
|
||||
func (s *stack) Format(fs fmt.State, verb rune) {
|
||||
switch verb {
|
||||
// case 'v', 's':
|
||||
case 'v':
|
||||
_, _ = s.WriteTo(fs)
|
||||
}
|
||||
}
|
||||
|
||||
// StackLen for error
|
||||
func (s *stack) StackLen() int {
|
||||
return len(*s)
|
||||
}
|
||||
|
||||
// WriteTo for error
|
||||
func (s *stack) WriteTo(w io.Writer) (int64, error) {
|
||||
if len(*s) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
nn, _ := w.Write([]byte("\nSTACK:\n"))
|
||||
for _, pc := range *s {
|
||||
// For historical reasons if pc is interpreted as a uintptr
|
||||
// its value represents the program counter + 1.
|
||||
fc := runtime.FuncForPC(pc - 1)
|
||||
if fc == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// file eg: workspace/godev/gookit/goutil/errorx/errorx_test.go
|
||||
file, line := fc.FileLine(pc - 1)
|
||||
// f.Name() eg: github.com/gookit/goutil/errorx_test.TestWithPrev()
|
||||
location := fc.Name() + "()\n " + file + ":" + strconv.Itoa(line) + "\n"
|
||||
|
||||
n, _ := w.Write([]byte(location))
|
||||
nn += n
|
||||
}
|
||||
|
||||
return int64(nn), nil
|
||||
}
|
||||
|
||||
// String format to string
|
||||
func (s *stack) String() string {
|
||||
var buf bytes.Buffer
|
||||
_, _ = s.WriteTo(&buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// StackFrames stack frame list
|
||||
func (s *stack) StackFrames() *runtime.Frames {
|
||||
return runtime.CallersFrames(*s)
|
||||
}
|
||||
|
||||
// CallerPC the caller PC value in the stack. it is first frame.
|
||||
func (s *stack) CallerPC() uintptr {
|
||||
if len(*s) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// For historical reasons if pc is interpreted as a uintptr
|
||||
// its value represents the program counter + 1.
|
||||
return (*s)[0] - 1
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* For error caller func
|
||||
*************************************************************/
|
||||
|
||||
// Func struct
|
||||
type Func struct {
|
||||
*runtime.Func
|
||||
pc uintptr
|
||||
}
|
||||
|
||||
// FuncForPC create.
|
||||
func FuncForPC(pc uintptr) *Func {
|
||||
fc := runtime.FuncForPC(pc)
|
||||
if fc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Func{
|
||||
pc: pc,
|
||||
Func: fc,
|
||||
}
|
||||
}
|
||||
|
||||
// FileLine returns the file name and line number of the source code
|
||||
func (f *Func) FileLine() (file string, line int) {
|
||||
return f.Func.FileLine(f.pc)
|
||||
}
|
||||
|
||||
// Location simple location info for the func
|
||||
//
|
||||
// Returns eg:
|
||||
//
|
||||
// "github.com/gookit/goutil/errorx_test.TestWithPrev(), errorx_test.go:34"
|
||||
func (f *Func) Location() string {
|
||||
file, line := f.FileLine()
|
||||
|
||||
return f.Name() + "(), " + filepath.Base(file) + ":" + strconv.Itoa(line)
|
||||
}
|
||||
|
||||
// String of the func
|
||||
//
|
||||
// Returns eg:
|
||||
//
|
||||
// github.com/gookit/goutil/errorx_test.TestWithPrev()
|
||||
// At /path/to/github.com/gookit/goutil/errorx_test.go:34
|
||||
func (f *Func) String() string {
|
||||
file, line := f.FileLine()
|
||||
|
||||
return f.Name() + "()\n At " + file + ":" + strconv.Itoa(line)
|
||||
}
|
||||
|
||||
// MarshalText handle
|
||||
func (f *Func) MarshalText() ([]byte, error) {
|
||||
return []byte(f.String()), nil
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* helper func for callers stacks
|
||||
*************************************************************/
|
||||
|
||||
// ErrStackOpt struct
|
||||
type ErrStackOpt struct {
|
||||
SkipDepth int
|
||||
TraceDepth int
|
||||
}
|
||||
|
||||
// default option
|
||||
var stdOpt = newErrOpt()
|
||||
|
||||
// ResetStdOpt config
|
||||
func ResetStdOpt() {
|
||||
stdOpt = newErrOpt()
|
||||
}
|
||||
|
||||
func newErrOpt() *ErrStackOpt {
|
||||
return &ErrStackOpt{
|
||||
SkipDepth: 3,
|
||||
TraceDepth: 8,
|
||||
}
|
||||
}
|
||||
|
||||
// Config the stdOpt setting
|
||||
func Config(fns ...func(opt *ErrStackOpt)) {
|
||||
for _, fn := range fns {
|
||||
fn(stdOpt)
|
||||
}
|
||||
}
|
||||
|
||||
// SkipDepth setting
|
||||
func SkipDepth(skipDepth int) func(opt *ErrStackOpt) {
|
||||
return func(opt *ErrStackOpt) {
|
||||
opt.SkipDepth = skipDepth
|
||||
}
|
||||
}
|
||||
|
||||
// TraceDepth setting
|
||||
func TraceDepth(traceDepth int) func(opt *ErrStackOpt) {
|
||||
return func(opt *ErrStackOpt) {
|
||||
opt.TraceDepth = traceDepth
|
||||
}
|
||||
}
|
||||
|
||||
func callersStack(skip, depth int) *stack {
|
||||
pcs := make([]uintptr, depth)
|
||||
num := runtime.Callers(skip, pcs[:])
|
||||
|
||||
var st stack = pcs[0:num]
|
||||
return &st
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// E new a raw go error. alias of errors.New()
|
||||
func E(msg string) error { return errors.New(msg) }
|
||||
|
||||
// Err new a raw go error. alias of errors.New()
|
||||
func Err(msg string) error { return errors.New(msg) }
|
||||
|
||||
// Raw new a raw go error. alias of errors.New()
|
||||
func Raw(msg string) error { return errors.New(msg) }
|
||||
|
||||
// Ef new a raw go error. alias of fmt.Errorf
|
||||
func Ef(tpl string, vars ...any) error { return fmt.Errorf(tpl, vars...) }
|
||||
|
||||
// Errf new a raw go error. alias of fmt.Errorf
|
||||
func Errf(tpl string, vars ...any) error { return fmt.Errorf(tpl, vars...) }
|
||||
|
||||
// Rf new a raw go error. alias of fmt.Errorf
|
||||
func Rf(tpl string, vs ...any) error { return fmt.Errorf(tpl, vs...) }
|
||||
|
||||
// Rawf new a raw go error. alias of fmt.Errorf
|
||||
func Rawf(tpl string, vs ...any) error { return fmt.Errorf(tpl, vs...) }
|
||||
|
||||
/*************************************************************
|
||||
* helper func for error
|
||||
*************************************************************/
|
||||
|
||||
// Cause returns the first cause error by call err.Cause().
|
||||
// Otherwise, will returns current error.
|
||||
func Cause(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err, ok := err.(Causer); ok {
|
||||
return err.Cause()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Unwrap returns previous error by call err.Unwrap().
|
||||
// Otherwise, will returns nil.
|
||||
func Unwrap(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err, ok := err.(Unwrapper); ok {
|
||||
return err.Unwrap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Previous alias of Unwrap()
|
||||
func Previous(err error) error { return Unwrap(err) }
|
||||
|
||||
// IsErrorX check
|
||||
func IsErrorX(err error) (ok bool) {
|
||||
_, ok = err.(*ErrorX)
|
||||
return
|
||||
}
|
||||
|
||||
// ToErrorX convert check. like errors.As()
|
||||
func ToErrorX(err error) (ex *ErrorX, ok bool) {
|
||||
ex, ok = err.(*ErrorX)
|
||||
return
|
||||
}
|
||||
|
||||
// MustEX convert error to *ErrorX, panic if err check failed.
|
||||
func MustEX(err error) *ErrorX {
|
||||
ex, ok := err.(*ErrorX)
|
||||
if !ok {
|
||||
panic("errorx: error is not *ErrorX")
|
||||
}
|
||||
return ex
|
||||
}
|
||||
|
||||
// Has contains target error, or err is eq target.
|
||||
// alias of errors.Is()
|
||||
func Has(err, target error) bool {
|
||||
return errors.Is(err, target)
|
||||
}
|
||||
|
||||
// Is alias of errors.Is()
|
||||
func Is(err, target error) bool {
|
||||
return errors.Is(err, target)
|
||||
}
|
||||
|
||||
// To try convert err to target, returns is result.
|
||||
//
|
||||
// NOTICE: target must be ptr and not nil. alias of errors.As()
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// var ex *errorx.ErrorX
|
||||
// err := doSomething()
|
||||
// if errorx.To(err, &ex) {
|
||||
// fmt.Println(ex.GoString())
|
||||
// }
|
||||
func To(err error, target any) bool {
|
||||
return errors.As(err, target)
|
||||
}
|
||||
|
||||
// As same of the To(), alias of errors.As()
|
||||
//
|
||||
// NOTICE: target must be ptr and not nil
|
||||
func As(err error, target any) bool {
|
||||
return errors.As(err, target)
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
# FileSystem Util
|
||||
|
||||
`fsutil` Provide some commonly file system util functions.
|
||||
|
||||
## Install
|
||||
|
||||
```shell
|
||||
go get github.com/gookit/goutil/fsutil
|
||||
```
|
||||
|
||||
## Go docs
|
||||
|
||||
- [Go docs](https://pkg.go.dev/github.com/gookit/goutil/fsutil)
|
||||
|
||||
## Find files
|
||||
|
||||
More see [./finder](./finder)
|
||||
|
||||
```go
|
||||
// find all files in dir
|
||||
fsutil.FindInDir("./", func(filePath string, de fs.DirEntry) error {
|
||||
fmt.Println(filePath)
|
||||
return nil
|
||||
})
|
||||
|
||||
// find files with filters
|
||||
fsutil.FindInDir("./", func(filePath string, de fs.DirEntry) error {
|
||||
fmt.Println(filePath)
|
||||
return nil
|
||||
}, fsutil.ExcludeDotFile)
|
||||
```
|
||||
|
||||
## Functions API
|
||||
|
||||
> **Note**: doc by run `go doc ./fsutil`
|
||||
|
||||
```go
|
||||
func ApplyFilters(fPath string, ent fs.DirEntry, filters []FilterFunc) bool
|
||||
func CopyFile(srcPath, dstPath string) error
|
||||
func CreateFile(fpath string, filePerm, dirPerm os.FileMode, fileFlag ...int) (*os.File, error)
|
||||
func DeleteIfExist(fPath string) error
|
||||
func DeleteIfFileExist(fPath string) error
|
||||
func Dir(fpath string) string
|
||||
func DiscardReader(src io.Reader)
|
||||
func ExcludeDotFile(_ string, ent fs.DirEntry) bool
|
||||
func Expand(pathStr string) string
|
||||
func ExpandPath(pathStr string) string
|
||||
func Extname(fpath string) string
|
||||
func FileExists(path string) bool
|
||||
func FileExt(fpath string) string
|
||||
func FindInDir(dir string, handleFn HandleFunc, filters ...FilterFunc) (e error)
|
||||
func GetContents(in any) []byte
|
||||
func GlobWithFunc(pattern string, fn func(filePath string) error) (err error)
|
||||
func IsAbsPath(aPath string) bool
|
||||
func IsDir(path string) bool
|
||||
func IsFile(path string) bool
|
||||
func IsImageFile(path string) bool
|
||||
func IsZipFile(filepath string) bool
|
||||
func JoinPaths(elem ...string) string
|
||||
func JoinSubPaths(basePath string, elem ...string) string
|
||||
func LineScanner(in any) *bufio.Scanner
|
||||
func MimeType(path string) (mime string)
|
||||
func MkDirs(perm os.FileMode, dirPaths ...string) error
|
||||
func MkParentDir(fpath string) error
|
||||
func MkSubDirs(perm os.FileMode, parentDir string, subDirs ...string) error
|
||||
func Mkdir(dirPath string, perm os.FileMode) error
|
||||
func MustCopyFile(srcPath, dstPath string)
|
||||
func MustCreateFile(filePath string, filePerm, dirPerm os.FileMode) *os.File
|
||||
func MustReadFile(filePath string) []byte
|
||||
func MustReadReader(r io.Reader) []byte
|
||||
func MustRemove(fPath string)
|
||||
func Name(fpath string) string
|
||||
func NewIOReader(in any) (r io.Reader, err error)
|
||||
func OSTempDir(pattern string) (string, error)
|
||||
func OSTempFile(pattern string) (*os.File, error)
|
||||
func OnlyFindDir(_ string, ent fs.DirEntry) bool
|
||||
func OnlyFindFile(_ string, ent fs.DirEntry) bool
|
||||
func OpenAppendFile(filepath string) (*os.File, error)
|
||||
func OpenFile(filepath string, flag int, perm os.FileMode) (*os.File, error)
|
||||
func OpenReadFile(filepath string) (*os.File, error)
|
||||
func OpenTruncFile(filepath string) (*os.File, error)
|
||||
func PathExists(path string) bool
|
||||
func PathMatch(pattern, s string) bool
|
||||
func PathName(fpath string) string
|
||||
func PutContents(filePath string, data any, fileFlag ...int) (int, error)
|
||||
func QuickOpenFile(filepath string, fileFlag ...int) (*os.File, error)
|
||||
func QuietRemove(fPath string)
|
||||
func ReadAll(in any) []byte
|
||||
func ReadExistFile(filePath string) []byte
|
||||
func ReadFile(filePath string) []byte
|
||||
func ReadOrErr(in any) ([]byte, error)
|
||||
func ReadReader(r io.Reader) []byte
|
||||
func ReadString(in any) string
|
||||
func ReadStringOrErr(in any) (string, error)
|
||||
func ReaderMimeType(r io.Reader) (mime string)
|
||||
func Realpath(pathStr string) string
|
||||
func Remove(fPath string) error
|
||||
func ResolvePath(pathStr string) string
|
||||
func RmFileIfExist(fPath string) error
|
||||
func RmIfExist(fPath string) error
|
||||
func SearchNameUp(dirPath, name string) string
|
||||
func SearchNameUpx(dirPath, name string) (string, bool)
|
||||
func SlashPath(path string) string
|
||||
func SplitPath(pathStr string) (dir, name string)
|
||||
func Suffix(fpath string) string
|
||||
func TempDir(dir, pattern string) (string, error)
|
||||
func TempFile(dir, pattern string) (*os.File, error)
|
||||
func TextScanner(in any) *scanner.Scanner
|
||||
func ToAbsPath(p string) string
|
||||
func UnixPath(path string) string
|
||||
func Unzip(archive, targetDir string) (err error)
|
||||
func WalkDir(dir string, fn fs.WalkDirFunc) error
|
||||
func WriteFile(filePath string, data any, perm os.FileMode, fileFlag ...int) error
|
||||
func WriteOSFile(f *os.File, data any) (n int, err error)
|
||||
type FilterFunc func(fPath string, ent fs.DirEntry) bool
|
||||
func ExcludeSuffix(ss ...string) FilterFunc
|
||||
func IncludeSuffix(ss ...string) FilterFunc
|
||||
type HandleFunc func(fPath string, ent fs.DirEntry) error
|
||||
```
|
||||
|
||||
## Code Check & Testing
|
||||
|
||||
```bash
|
||||
gofmt -w -l ./
|
||||
golint ./...
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
|
||||
```shell
|
||||
go test -v ./fsutil/...
|
||||
```
|
||||
|
||||
**Test limit by regexp**:
|
||||
|
||||
```shell
|
||||
go test -v -run ^TestSetByKeys ./fsutil/...
|
||||
```
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// perm for create dir or file
|
||||
var (
|
||||
DefaultDirPerm os.FileMode = 0775
|
||||
DefaultFilePerm os.FileMode = 0665
|
||||
OnlyReadFilePerm os.FileMode = 0444
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultFileFlags for create and write
|
||||
DefaultFileFlags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
|
||||
// OnlyReadFileFlags open file for read
|
||||
OnlyReadFileFlags = os.O_RDONLY
|
||||
)
|
||||
|
||||
// alias methods
|
||||
var (
|
||||
DirExist = IsDir
|
||||
FileExist = IsFile
|
||||
PathExist = PathExists
|
||||
)
|
||||
|
||||
// PathExists reports whether the named file or directory exists.
|
||||
func PathExists(path string) bool {
|
||||
if path == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsDir reports whether the named directory exists.
|
||||
func IsDir(path string) bool {
|
||||
if path == "" || len(path) > 468 {
|
||||
return false
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(path); err == nil {
|
||||
return fi.IsDir()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FileExists reports whether the named file or directory exists.
|
||||
func FileExists(path string) bool {
|
||||
return IsFile(path)
|
||||
}
|
||||
|
||||
// IsFile reports whether the named file or directory exists.
|
||||
//
|
||||
// - NOTE: not support symlink file
|
||||
func IsFile(path string) bool {
|
||||
if path == "" || len(path) > 468 {
|
||||
return false
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(path); err == nil {
|
||||
return !fi.IsDir()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSymlink reports whether the named file is a symlink.
|
||||
func IsSymlink(path string) bool {
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fi.Mode()&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
// IsAbsPath is abs path.
|
||||
func IsAbsPath(aPath string) bool {
|
||||
if len(aPath) > 0 {
|
||||
if aPath[0] == '/' {
|
||||
return true
|
||||
}
|
||||
return filepath.IsAbs(aPath)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEmptyDir reports whether the named directory is empty.
|
||||
func IsEmptyDir(dirPath string) bool {
|
||||
f, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Readdirnames(1)
|
||||
return err == io.EOF
|
||||
}
|
||||
|
||||
// ImageMimeTypes refer net/http package
|
||||
var ImageMimeTypes = map[string]string{
|
||||
"bmp": "image/bmp",
|
||||
"gif": "image/gif",
|
||||
"ief": "image/ief",
|
||||
"jpg": "image/jpeg",
|
||||
// "jpe": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
"png": "image/png",
|
||||
"svg": "image/svg+xml",
|
||||
"ico": "image/x-icon",
|
||||
"webp": "image/webp",
|
||||
}
|
||||
|
||||
// IsImageFile check file is image file.
|
||||
func IsImageFile(path string) bool {
|
||||
mime := MimeType(path)
|
||||
if mime == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, imgMime := range ImageMimeTypes {
|
||||
if imgMime == mime {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsZipFile check is zip file.
|
||||
// from https://blog.csdn.net/wangshubo1989/article/details/71743374
|
||||
func IsZipFile(filepath string) bool {
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf := make([]byte, 4)
|
||||
if n, err := f.Read(buf); err != nil || n < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Equal(buf, []byte("PK\x03\x04"))
|
||||
}
|
||||
|
||||
// PathMatch check for a string. alias of path.Match()
|
||||
func PathMatch(pattern, s string) bool {
|
||||
ok, err := path.Match(pattern, s)
|
||||
if err != nil {
|
||||
ok = false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// MimeSniffLen sniff Length, use for detect file mime type
|
||||
MimeSniffLen = 512
|
||||
)
|
||||
|
||||
// NameMatchFunc name matches func, alias of comdef.StringMatchFunc
|
||||
type NameMatchFunc = comdef.StringMatchFunc
|
||||
|
||||
// PathMatchFunc path matches func. alias of comdef.StringMatchFunc
|
||||
type PathMatchFunc = comdef.StringMatchFunc
|
||||
|
||||
// Entry extends fs.DirEntry, add some useful methods
|
||||
type Entry interface {
|
||||
fs.DirEntry
|
||||
// Path gets file/dir full path. eg: "/path/to/file.go"
|
||||
Path() string
|
||||
// Info get file info. like fs.DirEntry.Info(), but will cache result.
|
||||
Info() (fs.FileInfo, error)
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
fs.DirEntry
|
||||
path string
|
||||
stat fs.FileInfo
|
||||
sErr error
|
||||
}
|
||||
|
||||
// NewEntry create a new Entry instance
|
||||
func NewEntry(fPath string, ent fs.DirEntry) Entry {
|
||||
return &entry{
|
||||
path: fPath,
|
||||
DirEntry: ent,
|
||||
}
|
||||
}
|
||||
|
||||
// Path gets full file/dir path. eg: "/path/to/file.go"
|
||||
func (e *entry) Path() string {
|
||||
return e.path
|
||||
}
|
||||
|
||||
// Info gets file info, will cache result
|
||||
func (e *entry) Info() (fs.FileInfo, error) {
|
||||
if e.stat == nil {
|
||||
e.stat, e.sErr = e.DirEntry.Info()
|
||||
}
|
||||
return e.stat, e.sErr
|
||||
}
|
||||
|
||||
// String get string representation
|
||||
func (e *entry) String() string {
|
||||
return strutil.OrCond(e.IsDir(), "dir: ", "file: ") + e.Path()
|
||||
}
|
||||
|
||||
// FileInfo extends fs.FileInfo, add some useful methods
|
||||
type FileInfo interface {
|
||||
fs.FileInfo
|
||||
// Path gets file full path. eg: "/path/to/file.go"
|
||||
Path() string
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
fs.FileInfo
|
||||
fullPath string
|
||||
}
|
||||
|
||||
// NewFileInfo create a new FileInfo instance
|
||||
func NewFileInfo(fPath string, info fs.FileInfo) FileInfo {
|
||||
return &fileInfo{
|
||||
fullPath: fPath,
|
||||
FileInfo: info,
|
||||
}
|
||||
}
|
||||
|
||||
// Path gets file full path. eg: "/path/to/file.go"
|
||||
func (fi *fileInfo) Path() string {
|
||||
return fi.fullPath
|
||||
}
|
||||
|
||||
// FileInfos type for FileInfo slice
|
||||
//
|
||||
// implements sort.Interface:
|
||||
//
|
||||
// sorts by oldest time modified in the file info.
|
||||
// eg: [old_220211, old_220212, old_220213]
|
||||
type FileInfos []FileInfo
|
||||
|
||||
// Len get length
|
||||
func (fis FileInfos) Len() int {
|
||||
return len(fis)
|
||||
}
|
||||
|
||||
// Swap swap values
|
||||
func (fis FileInfos) Swap(i, j int) {
|
||||
fis[i], fis[j] = fis[j], fis[i]
|
||||
}
|
||||
|
||||
// Less check by mod time
|
||||
func (fis FileInfos) Less(i, j int) bool {
|
||||
return fis[j].ModTime().After(fis[i].ModTime())
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gookit/goutil/arrutil"
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// FilePathInDirs get full file path in dirs. return empty string if not found.
|
||||
//
|
||||
// Params:
|
||||
// - file: can be relative path, file name, full path.
|
||||
// - dirs: dir paths
|
||||
func FilePathInDirs(fPath string, dirs ...string) string {
|
||||
fPath = comfunc.ExpandHome(fPath)
|
||||
if FileExists(fPath) {
|
||||
return fPath
|
||||
}
|
||||
|
||||
for _, dirPath := range dirs {
|
||||
fPath = JoinSubPaths(dirPath, fPath)
|
||||
if FileExists(fPath) {
|
||||
return fPath
|
||||
}
|
||||
}
|
||||
return "" // not found
|
||||
}
|
||||
|
||||
// FirstExists check multi paths and return first exists path.
|
||||
func FirstExists(paths ...string) string {
|
||||
return MatchFirst(paths, PathExists, "")
|
||||
}
|
||||
|
||||
// FirstExistsDir check multi paths and return first exists dir.
|
||||
func FirstExistsDir(paths ...string) string {
|
||||
return MatchFirst(paths, IsDir, "")
|
||||
}
|
||||
|
||||
// FirstExistsFile check multi paths and return first exists file.
|
||||
func FirstExistsFile(paths ...string) string {
|
||||
return MatchFirst(paths, IsFile, "")
|
||||
}
|
||||
|
||||
// MatchPaths given paths by custom mather func.
|
||||
func MatchPaths(paths []string, matcher PathMatchFunc) []string {
|
||||
var ret []string
|
||||
for _, p := range paths {
|
||||
if matcher(p) {
|
||||
ret = append(ret, p)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// MatchFirst filter paths by filter func and return first match path.
|
||||
func MatchFirst(paths []string, matcher PathMatchFunc, defaultPath string) string {
|
||||
for _, p := range paths {
|
||||
if matcher(p) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return defaultPath
|
||||
}
|
||||
|
||||
// FindParentOption options
|
||||
type FindParentOption struct {
|
||||
MaxLevel int // default: 10
|
||||
// NeedDir true: find dirs; false(default): find files
|
||||
NeedDir bool
|
||||
OnlyOne bool // only find one, default: true
|
||||
// Collector func
|
||||
Collector func(fullPath string)
|
||||
// MatchFunc custom matcher func. return false to stop find.
|
||||
MatchFunc func(currentDir string) bool
|
||||
}
|
||||
|
||||
// FindParentOptFn find parent option func
|
||||
type FindParentOptFn func(opt *FindParentOption)
|
||||
|
||||
// FindAllInParentDirs looks for all match file(default)/dir in the current directory and parent directories
|
||||
func FindAllInParentDirs(dirPath, name string, optFns ...FindParentOptFn) []string {
|
||||
var foundPaths []string
|
||||
optFns = append(optFns, func(opt *FindParentOption) {
|
||||
opt.OnlyOne = false
|
||||
})
|
||||
|
||||
FindNameInParentDirs(dirPath, name, func(fullPath string) {
|
||||
foundPaths = append(foundPaths, fullPath)
|
||||
}, optFns...)
|
||||
return foundPaths
|
||||
}
|
||||
|
||||
// FindOneInParentDirs looks for a file(default)/dir in the current directory and parent directories
|
||||
func FindOneInParentDirs(dirPath, name string, optFns ...FindParentOptFn) string {
|
||||
var foundPath string
|
||||
FindNameInParentDirs(dirPath, name, func(fullPath string) {
|
||||
foundPath = fullPath
|
||||
}, optFns...)
|
||||
return foundPath
|
||||
}
|
||||
|
||||
// FindNameInParentDirs looks for file(default)/dir in the current directory and parent directories
|
||||
func FindNameInParentDirs(dirPath, name string, collectFn func(fullPath string), optFns ...FindParentOptFn) {
|
||||
opts := &FindParentOption{
|
||||
MaxLevel: 10,
|
||||
OnlyOne: true,
|
||||
Collector: collectFn,
|
||||
}
|
||||
for _, fn := range optFns {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
FindInParentDirs(dirPath, func(currentDir string) bool {
|
||||
filePath := filepath.Join(currentDir, name)
|
||||
if fi, err := os.Stat(filePath); err == nil {
|
||||
found := false
|
||||
if fi.IsDir() {
|
||||
found = opts.NeedDir
|
||||
} else {
|
||||
found = !opts.NeedDir
|
||||
}
|
||||
|
||||
if found {
|
||||
opts.Collector(filePath)
|
||||
return !opts.OnlyOne
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, opts.MaxLevel)
|
||||
}
|
||||
|
||||
// FindInParentDirs looks for file/dir in the current directory and parent directories
|
||||
// - MatchFunc custom matcher func. return false to stop find.
|
||||
func FindInParentDirs(dirPath string, matchFunc func(dir string) bool, maxLevel int) {
|
||||
currentLv := 1
|
||||
currentDir := ToAbsPath(dirPath)
|
||||
|
||||
for {
|
||||
// Check if the file exists in the current directory
|
||||
if !matchFunc(currentDir) {
|
||||
return
|
||||
}
|
||||
|
||||
// check find level
|
||||
if maxLevel > 0 && currentLv > maxLevel {
|
||||
break
|
||||
}
|
||||
|
||||
// Get parent directory
|
||||
parentDir := filepath.Dir(currentDir)
|
||||
if parentDir == currentDir {
|
||||
// Reached the root, file not found
|
||||
return
|
||||
}
|
||||
|
||||
// Move to parent directory
|
||||
currentLv++
|
||||
currentDir = parentDir
|
||||
}
|
||||
}
|
||||
|
||||
// SearchNameUp find file/dir name in dirPath or parent dirs,
|
||||
// return the name of directory path
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// repoDir := fsutil.SearchNameUp("/path/to/dir", ".git")
|
||||
func SearchNameUp(dirPath, name string) string {
|
||||
dir, _ := SearchNameUpx(dirPath, name)
|
||||
return dir
|
||||
}
|
||||
|
||||
// SearchNameUpx find file/dir name in dirPath or parent dirs,
|
||||
// return the name of directory path and dir is changed.
|
||||
func SearchNameUpx(dirPath, name string) (string, bool) {
|
||||
var level int
|
||||
dirPath = ToAbsPath(dirPath)
|
||||
|
||||
for {
|
||||
namePath := filepath.Join(dirPath, name)
|
||||
if PathExists(namePath) {
|
||||
return dirPath, level > 0
|
||||
}
|
||||
|
||||
level++
|
||||
prevLn := len(dirPath)
|
||||
dirPath = filepath.Dir(dirPath)
|
||||
if prevLn == len(dirPath) {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WalkDir walks the file tree rooted at root, calling fn for each file or
|
||||
// directory in the tree, including root.
|
||||
//
|
||||
// TIP: will recursively found in sub dirs.
|
||||
func WalkDir(dir string, fn fs.WalkDirFunc) error {
|
||||
return filepath.WalkDir(dir, fn)
|
||||
}
|
||||
|
||||
// Glob finds files by glob path pattern. alias of filepath.Glob()
|
||||
// and support filter matched files by name.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// files := fsutil.Glob("/path/to/dir/*.go")
|
||||
func Glob(pattern string, fls ...NameMatchFunc) []string {
|
||||
files, _ := filepath.Glob(pattern)
|
||||
if len(fls) == 0 || len(files) == 0 {
|
||||
return files
|
||||
}
|
||||
|
||||
var matched []string
|
||||
for _, file := range files {
|
||||
for _, fn := range fls {
|
||||
if fn(path.Base(file)) {
|
||||
matched = append(matched, file)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
// GlobWithFunc find files by glob path pattern, then handle matched file
|
||||
func GlobWithFunc(pattern string, fn func(filePath string) error) (err error) {
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, filePath := range files {
|
||||
err = fn(filePath)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type (
|
||||
// FilterFunc type for FindInDir
|
||||
//
|
||||
// - return False will skip handle the file.
|
||||
FilterFunc func(fPath string, ent fs.DirEntry) bool
|
||||
|
||||
// HandleFunc type for FindInDir
|
||||
HandleFunc func(fPath string, ent fs.DirEntry) error
|
||||
)
|
||||
|
||||
// OnlyFindDir on find
|
||||
func OnlyFindDir(_ string, ent fs.DirEntry) bool { return ent.IsDir() }
|
||||
|
||||
// OnlyFindFile on find
|
||||
func OnlyFindFile(_ string, ent fs.DirEntry) bool { return !ent.IsDir() }
|
||||
|
||||
// ExcludeNames on find
|
||||
func ExcludeNames(names ...string) FilterFunc {
|
||||
return func(_ string, ent fs.DirEntry) bool {
|
||||
return !arrutil.StringsHas(names, ent.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// IncludeSuffix on find
|
||||
func IncludeSuffix(ss ...string) FilterFunc {
|
||||
return func(_ string, ent fs.DirEntry) bool {
|
||||
return strutil.HasOneSuffix(ent.Name(), ss)
|
||||
}
|
||||
}
|
||||
|
||||
// ExcludeDotFile on find
|
||||
func ExcludeDotFile(_ string, ent fs.DirEntry) bool { return ent.Name()[0] != '.' }
|
||||
|
||||
// ExcludeSuffix on find
|
||||
func ExcludeSuffix(ss ...string) FilterFunc {
|
||||
return func(_ string, ent fs.DirEntry) bool {
|
||||
return !strutil.HasOneSuffix(ent.Name(), ss)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyFilters handle
|
||||
func ApplyFilters(fPath string, ent fs.DirEntry, filters []FilterFunc) bool {
|
||||
for _, filter := range filters {
|
||||
if !filter(fPath, ent) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FindInDir code refer the go pkg: path/filepath.glob()
|
||||
//
|
||||
// - TIP: default will be not found in sub-dir.
|
||||
//
|
||||
// filters: return false will skip the file.
|
||||
func FindInDir(dir string, handleFn HandleFunc, filters ...FilterFunc) (e error) {
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil || !fi.IsDir() {
|
||||
return // ignore I/O error
|
||||
}
|
||||
|
||||
des, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// remove the last '/' char
|
||||
dirLn := len(dir)
|
||||
if dirLn > 1 && dir[dirLn-1] == '/' {
|
||||
dir = dir[:dirLn-1]
|
||||
}
|
||||
|
||||
for _, ent := range des {
|
||||
filePath := dir + "/" + ent.Name()
|
||||
|
||||
// apply filters
|
||||
if len(filters) > 0 && ApplyFilters(filePath, ent, filters) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err1 := handleFn(filePath, ent); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileInDirs returns the first file path in the given dirs.
|
||||
func FileInDirs(paths []string, names ...string) string {
|
||||
for _, pathDir := range paths {
|
||||
for _, name := range names {
|
||||
file := pathDir + "/" + name
|
||||
if IsFile(file) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Package fsutil Filesystem util functions, quick create, read and write file. eg: file and dir check, operate
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"github.com/gookit/goutil/x/basefn"
|
||||
)
|
||||
|
||||
// PathSep alias of os.PathSeparator
|
||||
const PathSep = os.PathSeparator
|
||||
|
||||
// JoinPaths elements, alias of filepath.Join()
|
||||
func JoinPaths(elem ...string) string {
|
||||
return filepath.Join(elem...)
|
||||
}
|
||||
|
||||
// JoinPaths3 elements, like the filepath.Join()
|
||||
func JoinPaths3(basePath, secPath string, elems ...string) string {
|
||||
return comfunc.JoinPaths3(basePath, secPath, elems)
|
||||
}
|
||||
|
||||
// JoinSubPaths elements, like the filepath.Join()
|
||||
func JoinSubPaths(basePath string, elems ...string) string {
|
||||
return comfunc.JoinPaths2(basePath, elems)
|
||||
}
|
||||
|
||||
// SlashPath alias of filepath.ToSlash
|
||||
func SlashPath(path string) string {
|
||||
return filepath.ToSlash(path)
|
||||
}
|
||||
|
||||
// UnixPath like of filepath.ToSlash, but always replace
|
||||
func UnixPath(path string) string {
|
||||
if !strings.ContainsRune(path, '\\') {
|
||||
return path
|
||||
}
|
||||
return strings.ReplaceAll(path, "\\", "/")
|
||||
}
|
||||
|
||||
// ToAbsPath convert path to absolute path.
|
||||
// Will expand home dir, if empty will return current work dir
|
||||
//
|
||||
// TIP: will don't check path is really exists
|
||||
func ToAbsPath(p string) string {
|
||||
// return current work dir
|
||||
if len(p) == 0 {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return p
|
||||
}
|
||||
return wd
|
||||
}
|
||||
|
||||
if IsAbsPath(p) {
|
||||
return p
|
||||
}
|
||||
|
||||
// expand home dir
|
||||
if p[0] == '~' {
|
||||
return comfunc.ExpandHome(p)
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return p
|
||||
}
|
||||
return filepath.Join(wd, p)
|
||||
}
|
||||
|
||||
// Must2 ok for (any, error) result. if it has error, will panic
|
||||
func Must2(_ any, err error) { basefn.MustOK(err) }
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
)
|
||||
|
||||
// DirPath get dir path from filepath, without a last name.
|
||||
// eg: "/foo/bar/baz.js" => "/foo/bar"
|
||||
func DirPath(fPath string) string { return filepath.Dir(fPath) }
|
||||
|
||||
// Dir get dir path from filepath, without a last name.
|
||||
// eg: "/foo/bar/baz.js" => "/foo/bar"
|
||||
func Dir(fPath string) string { return filepath.Dir(fPath) }
|
||||
|
||||
// PathName get file/dir name from a full path.
|
||||
// eg: "/foo/bar/baz.js" => "baz.js"
|
||||
func PathName(fPath string) string { return filepath.Base(fPath) }
|
||||
|
||||
// PathNoExt get path from full path, without ext.
|
||||
//
|
||||
// eg: path/to/main.go => "path/to/main"
|
||||
func PathNoExt(fPath string) string {
|
||||
ext := filepath.Ext(fPath)
|
||||
if el := len(ext); el > 0 {
|
||||
return fPath[:len(fPath)-el]
|
||||
}
|
||||
return fPath
|
||||
}
|
||||
|
||||
// Name get file/dir name from full path.
|
||||
//
|
||||
// eg:
|
||||
// "path/to/main.go" => "main.go"
|
||||
// "/foo/bar/baz" => "baz"
|
||||
func Name(fPath string) string {
|
||||
if fPath == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Base(fPath)
|
||||
}
|
||||
|
||||
// NameNoExt get file name from a full path, without an ext.
|
||||
//
|
||||
// eg: path/to/main.go => "main"
|
||||
func NameNoExt(fPath string) string {
|
||||
if fPath == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
fName := filepath.Base(fPath)
|
||||
if pos := strings.LastIndexByte(fName, '.'); pos > 0 {
|
||||
return fName[:pos]
|
||||
}
|
||||
return fName
|
||||
}
|
||||
|
||||
// FileExt get filename ext. alias of filepath.Ext()
|
||||
//
|
||||
// eg: path/to/main.go => ".go"
|
||||
func FileExt(fPath string) string { return filepath.Ext(fPath) }
|
||||
|
||||
// Extname get filename ext. alias of filepath.Ext()
|
||||
//
|
||||
// eg: path/to/main.go => "go"
|
||||
func Extname(fPath string) string {
|
||||
if ext := filepath.Ext(fPath); len(ext) > 0 {
|
||||
return ext[1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Suffix get filename ext. alias of filepath.Ext()
|
||||
//
|
||||
// eg: path/to/main.go => ".go"
|
||||
func Suffix(fPath string) string { return filepath.Ext(fPath) }
|
||||
|
||||
// Expand will parse first `~` to user home dir path.
|
||||
func Expand(pathStr string) string { return comfunc.ExpandHome(pathStr) }
|
||||
|
||||
// ExpandHome will parse first `~` to user home dir path.
|
||||
func ExpandHome(pathStr string) string { return comfunc.ExpandHome(pathStr) }
|
||||
|
||||
// ExpandPath will parse `~` to user home dir path.
|
||||
func ExpandPath(pathStr string) string { return comfunc.ExpandHome(pathStr) }
|
||||
|
||||
// ResolvePath will parse `~` and ENV var in path
|
||||
func ResolvePath(pathStr string) string {
|
||||
pathStr = comfunc.ExpandHome(pathStr)
|
||||
// return comfunc.ParseEnvVar()
|
||||
return os.ExpandEnv(pathStr)
|
||||
}
|
||||
|
||||
// SplitPath splits path immediately following the final Separator, separating it into a directory and file name component
|
||||
func SplitPath(pathStr string) (dir, name string) { return filepath.Split(pathStr) }
|
||||
|
||||
// homeDir cache
|
||||
var _homeDir string
|
||||
|
||||
// UserHomeDir is alias of os.UserHomeDir, but ignore error.(by os.UserHomeDir)
|
||||
func UserHomeDir() string {
|
||||
if _homeDir == "" {
|
||||
_homeDir, _ = os.UserHomeDir()
|
||||
}
|
||||
return _homeDir
|
||||
}
|
||||
|
||||
// HomeDir get user home dir path.
|
||||
func HomeDir() string { return UserHomeDir() }
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
//go:build !windows
|
||||
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
)
|
||||
|
||||
// Realpath returns the shortest path name equivalent to path by purely lexical processing.
|
||||
// Will expand ~ to home dir, and join with workdir if path is relative path.
|
||||
func Realpath(pathStr string) string {
|
||||
pathStr = comfunc.ExpandHome(pathStr)
|
||||
|
||||
if !IsAbsPath(pathStr) {
|
||||
pathStr = JoinSubPaths(comfunc.Workdir(), pathStr)
|
||||
}
|
||||
return path.Clean(pathStr)
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
)
|
||||
|
||||
// Realpath returns the shortest path name equivalent to path by purely lexical processing.
|
||||
func Realpath(pathStr string) string {
|
||||
pathStr = comfunc.ExpandHome(pathStr)
|
||||
|
||||
if !IsAbsPath(pathStr) {
|
||||
pathStr = JoinSubPaths(comfunc.Workdir(), pathStr)
|
||||
}
|
||||
return filepath.Clean(pathStr)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// DetectMime detect a file mime type. alias of MimeType()
|
||||
func DetectMime(path string) string {
|
||||
return MimeType(path)
|
||||
}
|
||||
|
||||
// MimeType get file mime type name. eg "image/png"
|
||||
func MimeType(path string) (mime string) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return ReaderMimeType(file)
|
||||
}
|
||||
|
||||
// ReaderMimeType get the io.Reader mimeType
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// file, err := os.Open(filepath)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// mime := ReaderMimeType(file)
|
||||
func ReaderMimeType(r io.Reader) (mime string) {
|
||||
var buf [MimeSniffLen]byte
|
||||
n, _ := io.ReadFull(r, buf[:])
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return http.DetectContentType(buf[:n])
|
||||
}
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/x/basefn"
|
||||
)
|
||||
|
||||
// Mkdir alias of os.MkdirAll()
|
||||
func Mkdir(dirPath string, perm fs.FileMode) error { return os.MkdirAll(dirPath, perm) }
|
||||
|
||||
// MkdirQuick with default permission 0755.
|
||||
func MkdirQuick(dirPath string) error { return EnsureDir(dirPath) }
|
||||
|
||||
// EnsureDir creates a directory if it doesn't exist
|
||||
func EnsureDir(path string) error {
|
||||
if !DirExist(path) {
|
||||
return os.MkdirAll(path, 0755)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MkDirs batch makes multi dirs at once
|
||||
func MkDirs(perm fs.FileMode, dirPaths ...string) error {
|
||||
for _, dirPath := range dirPaths {
|
||||
if err := os.MkdirAll(dirPath, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MkSubDirs batch makes multi sub-dirs at once
|
||||
func MkSubDirs(perm fs.FileMode, parentDir string, subDirs ...string) error {
|
||||
for _, dirName := range subDirs {
|
||||
dirPath := parentDir + "/" + dirName
|
||||
if err := os.MkdirAll(dirPath, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MkParentDir quickly create parent dir for a given path.
|
||||
func MkParentDir(fpath string) error {
|
||||
dirPath := filepath.Dir(fpath)
|
||||
if !IsDir(dirPath) {
|
||||
return os.MkdirAll(dirPath, 0775)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ************************************************************
|
||||
// options for open file
|
||||
// ************************************************************
|
||||
|
||||
// OpenOption for open file
|
||||
type OpenOption struct {
|
||||
// file open flag. see FsCWTFlags
|
||||
Flag int
|
||||
// file perm. see DefaultFilePerm
|
||||
Perm os.FileMode
|
||||
}
|
||||
|
||||
// OpenOptionFunc for open/write file
|
||||
type OpenOptionFunc func(*OpenOption)
|
||||
|
||||
// NewOpenOption create a new OpenOption instance
|
||||
//
|
||||
// Defaults:
|
||||
// - open flags: FsCWTFlags (override write)
|
||||
// - file Perm: DefaultFilePerm
|
||||
func NewOpenOption(optFns ...OpenOptionFunc) *OpenOption {
|
||||
opt := &OpenOption{
|
||||
Flag: FsCWTFlags,
|
||||
Perm: DefaultFilePerm,
|
||||
}
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(opt)
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
// OpenOptOrNew create a new OpenOption instance if opt is nil
|
||||
func OpenOptOrNew(opt *OpenOption) *OpenOption {
|
||||
if opt == nil {
|
||||
return NewOpenOption()
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithFlag set file open flag
|
||||
func WithFlag(flag int) OpenOptionFunc {
|
||||
return func(opt *OpenOption) {
|
||||
opt.Flag = flag
|
||||
}
|
||||
}
|
||||
|
||||
// WithPerm set file perm
|
||||
func WithPerm(perm os.FileMode) OpenOptionFunc {
|
||||
return func(opt *OpenOption) {
|
||||
opt.Perm = perm
|
||||
}
|
||||
}
|
||||
|
||||
// ************************************************************
|
||||
// open/create files
|
||||
// ************************************************************
|
||||
|
||||
// some commonly flag consts for open file.
|
||||
const (
|
||||
FsCWAFlags = os.O_CREATE | os.O_WRONLY | os.O_APPEND // create, append write-only
|
||||
FsCWTFlags = os.O_CREATE | os.O_WRONLY | os.O_TRUNC // create, override write-only
|
||||
FsCWFlags = os.O_CREATE | os.O_WRONLY // create, write-only
|
||||
FsRWFlags = os.O_RDWR // read-write, dont create.
|
||||
FsRFlags = os.O_RDONLY // read-only
|
||||
)
|
||||
|
||||
// OpenFile like os.OpenFile, but will auto create dir.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// file, err := OpenFile("path/to/file.txt", FsCWFlags, 0666)
|
||||
func OpenFile(filePath string, flag int, perm os.FileMode) (*os.File, error) {
|
||||
fileDir := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(fileDir, DefaultDirPerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filePath, flag, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// MustOpenFile like os.OpenFile, but will auto create dir.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// file := MustOpenFile("path/to/file.txt", FsCWFlags, 0666)
|
||||
func MustOpenFile(filePath string, flag int, perm os.FileMode) *os.File {
|
||||
file, err := OpenFile(filePath, flag, perm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// QuickOpenFile like os.OpenFile, open for append write. if not exists, will create it.
|
||||
//
|
||||
// Alias of OpenAppendFile()
|
||||
func QuickOpenFile(filepath string, fileFlag ...int) (*os.File, error) {
|
||||
flag := basefn.FirstOr(fileFlag, FsCWAFlags)
|
||||
return OpenFile(filepath, flag, DefaultFilePerm)
|
||||
}
|
||||
|
||||
// OpenAppendFile like os.OpenFile, open for append write. if not exists, will create it.
|
||||
func OpenAppendFile(filepath string, filePerm ...os.FileMode) (*os.File, error) {
|
||||
perm := basefn.FirstOr(filePerm, DefaultFilePerm)
|
||||
return OpenFile(filepath, FsCWAFlags, perm)
|
||||
}
|
||||
|
||||
// OpenTruncFile like os.OpenFile, open for override write. if not exists, will create it.
|
||||
func OpenTruncFile(filepath string, filePerm ...os.FileMode) (*os.File, error) {
|
||||
perm := basefn.FirstOr(filePerm, DefaultFilePerm)
|
||||
return OpenFile(filepath, FsCWTFlags, perm)
|
||||
}
|
||||
|
||||
// OpenReadFile like os.OpenFile, open file for read contents
|
||||
func OpenReadFile(filepath string) (*os.File, error) {
|
||||
return os.OpenFile(filepath, FsRFlags, OnlyReadFilePerm)
|
||||
}
|
||||
|
||||
// CreateFile create file if not exists
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// CreateFile("path/to/file.txt", 0664, 0666)
|
||||
func CreateFile(fpath string, filePerm, dirPerm os.FileMode, fileFlag ...int) (*os.File, error) {
|
||||
dirPath := filepath.Dir(fpath)
|
||||
if !IsDir(dirPath) {
|
||||
err := os.MkdirAll(dirPath, dirPerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
flag := basefn.FirstOr(fileFlag, FsCWAFlags)
|
||||
return os.OpenFile(fpath, flag, filePerm)
|
||||
}
|
||||
|
||||
// MustCreateFile create file, will panic on error
|
||||
func MustCreateFile(filePath string, filePerm, dirPerm os.FileMode) *os.File {
|
||||
file, err := CreateFile(filePath, filePerm, dirPerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// ************************************************************
|
||||
// remove files
|
||||
// ************************************************************
|
||||
|
||||
// alias methods
|
||||
var (
|
||||
// MustRm removes the named file or (empty) directory.
|
||||
MustRm = MustRemove
|
||||
// QuietRm removes the named file or (empty) directory.
|
||||
QuietRm = QuietRemove
|
||||
)
|
||||
|
||||
// Remove removes the named file or (empty) directory.
|
||||
func Remove(fPath string) error {
|
||||
return os.Remove(fPath)
|
||||
}
|
||||
|
||||
// MustRemove removes the named file or (empty) directory.
|
||||
// NOTICE: will panic on error
|
||||
func MustRemove(fPath string) {
|
||||
if err := os.Remove(fPath); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// QuietRemove removes the named file or (empty) directory.
|
||||
//
|
||||
// NOTICE: will ignore error
|
||||
func QuietRemove(fPath string) { _ = os.Remove(fPath) }
|
||||
|
||||
// SafeRemoveAll removes path and any children it contains. will ignore error
|
||||
func SafeRemoveAll(path string) {
|
||||
_ = os.RemoveAll(path)
|
||||
}
|
||||
|
||||
// RmIfExist removes the named file or (empty) directory on existing.
|
||||
func RmIfExist(fPath string) error { return DeleteIfExist(fPath) }
|
||||
|
||||
// DeleteIfExist removes the named file or (empty) directory on existing.
|
||||
func DeleteIfExist(fPath string) error {
|
||||
if PathExists(fPath) {
|
||||
return os.Remove(fPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RmFileIfExist removes the named file on existing.
|
||||
func RmFileIfExist(fPath string) error { return DeleteIfFileExist(fPath) }
|
||||
|
||||
// DeleteIfFileExist removes the named file on existing.
|
||||
func DeleteIfFileExist(fPath string) error {
|
||||
if IsFile(fPath) {
|
||||
return os.Remove(fPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSub removes all sub files and dirs of dirPath, but not remove dirPath.
|
||||
func RemoveSub(dirPath string, fns ...FilterFunc) error {
|
||||
return FindInDir(dirPath, func(fPath string, ent fs.DirEntry) error {
|
||||
if ent.IsDir() {
|
||||
if err := RemoveSub(fPath, fns...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// skip rm not empty subdir
|
||||
if !IsEmptyDir(fPath) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return os.Remove(fPath)
|
||||
}, fns...)
|
||||
}
|
||||
|
||||
// ************************************************************
|
||||
// other operates
|
||||
// ************************************************************
|
||||
|
||||
// Unzip a zip archive
|
||||
// from https://blog.csdn.net/wangshubo1989/article/details/71743374
|
||||
func Unzip(archive, targetDir string) (err error) {
|
||||
reader, err := zip.OpenReader(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(targetDir, DefaultDirPerm); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range reader.File {
|
||||
if strings.Contains(file.Name, "..") {
|
||||
return fmt.Errorf("illegal file path in zip: %v", file.Name)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(targetDir, file.Name)
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
err = os.MkdirAll(fullPath, file.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fileReader, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
_ = fileReader.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(targetFile, fileReader)
|
||||
|
||||
// close all
|
||||
_ = fileReader.Close()
|
||||
targetFile.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"text/scanner"
|
||||
|
||||
"github.com/gookit/goutil/x/basefn"
|
||||
)
|
||||
|
||||
// NewIOReader instance by input file path or io.Reader
|
||||
func NewIOReader(in any) (r io.Reader, err error) {
|
||||
switch typIn := in.(type) {
|
||||
case string: // as file path
|
||||
return OpenReadFile(typIn)
|
||||
case io.Reader:
|
||||
return typIn, nil
|
||||
}
|
||||
return nil, errors.New("invalid input type, allow: string, io.Reader")
|
||||
}
|
||||
|
||||
// DiscardReader anything from the reader
|
||||
func DiscardReader(src io.Reader) {
|
||||
_, _ = io.Copy(io.Discard, src)
|
||||
}
|
||||
|
||||
// ReadFile read file contents, will panic on error
|
||||
func ReadFile(filePath string) []byte { return MustReadFile(filePath) }
|
||||
|
||||
// MustReadFile read file contents, will panic on error
|
||||
func MustReadFile(filePath string) []byte {
|
||||
bs, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
// ReadReader read contents from io.Reader, will panic on error
|
||||
func ReadReader(r io.Reader) []byte { return MustReadReader(r) }
|
||||
|
||||
// MustReadReader read contents from io.Reader, will panic on error
|
||||
func MustReadReader(r io.Reader) []byte {
|
||||
bs, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
// ReadString read contents from path or io.Reader, will panic on in type error
|
||||
func ReadString(in any) string { return string(GetContents(in)) }
|
||||
|
||||
// ReadStringOrErr read contents from path or io.Reader, will panic on in type error
|
||||
func ReadStringOrErr(in any) (string, error) {
|
||||
r, err := NewIOReader(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bs, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bs), nil
|
||||
}
|
||||
|
||||
// ReadAll read contents from path or io.Reader, will panic on in type error
|
||||
func ReadAll(in any) []byte { return MustRead(in) }
|
||||
|
||||
// GetContents read contents from path or io.Reader, will panic on in type error
|
||||
func GetContents(in any) []byte { return MustRead(in) }
|
||||
|
||||
// MustRead read contents from path or io.Reader, will panic on in type error
|
||||
func MustRead(in any) []byte { return basefn.Must(ReadOrErr(in)) }
|
||||
|
||||
// ReadOrErr read contents from path or io.Reader, will panic on in type error
|
||||
func ReadOrErr(in any) ([]byte, error) {
|
||||
r, err := NewIOReader(in)
|
||||
defer func() {
|
||||
if r != nil {
|
||||
if file, ok := r.(*os.File); ok {
|
||||
err = file.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.ReadAll(r)
|
||||
}
|
||||
|
||||
// ReadExistFile read file contents if existed, will panic on error
|
||||
func ReadExistFile(filePath string) []byte {
|
||||
if IsFile(filePath) {
|
||||
bs, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TextScanner from filepath or io.Reader, will panic on in type error.
|
||||
// Will scan parse text to tokens: Ident, Int, Float, Char, String, RawString, Comment, etc.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// s := fsutil.TextScanner("/path/to/file")
|
||||
// for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
|
||||
// fmt.Printf("%s: %s\n", s.Position, s.TokenText())
|
||||
// }
|
||||
func TextScanner(in any) *scanner.Scanner {
|
||||
var s scanner.Scanner
|
||||
r, err := NewIOReader(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.Init(r)
|
||||
s.Filename = "text-scanner"
|
||||
return &s
|
||||
}
|
||||
|
||||
// LineScanner create from filepath or io.Reader, will panic on in type error.
|
||||
// Will scan and parse text to lines.
|
||||
//
|
||||
// s := fsutil.LineScanner("/path/to/file")
|
||||
// for s.Scan() {
|
||||
// fmt.Println(s.Text())
|
||||
// }
|
||||
func LineScanner(in any) *bufio.Scanner {
|
||||
r, err := NewIOReader(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bufio.NewScanner(r)
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/gookit/goutil/x/basefn"
|
||||
)
|
||||
|
||||
// ************************************************************
|
||||
// temp file or dir
|
||||
// ************************************************************
|
||||
|
||||
// OSTempFile create a temp file on os.TempDir()
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// fsutil.OSTempFile("example.*.txt")
|
||||
func OSTempFile(pattern string) (*os.File, error) {
|
||||
return os.CreateTemp(os.TempDir(), pattern)
|
||||
}
|
||||
|
||||
// TempFile is like os.CreateTemp, but can custom temp dir.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // create temp file on os.TempDir()
|
||||
// fsutil.TempFile("", "example.*.txt")
|
||||
// // create temp file on "testdata" dir
|
||||
// fsutil.TempFile("testdata", "example.*.txt")
|
||||
func TempFile(dir, pattern string) (*os.File, error) {
|
||||
return os.CreateTemp(dir, pattern)
|
||||
}
|
||||
|
||||
// OSTempDir creates a new temp dir on os.TempDir and return the temp dir path
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// fsutil.OSTempDir("example.*")
|
||||
func OSTempDir(pattern string) (string, error) {
|
||||
return os.MkdirTemp(os.TempDir(), pattern)
|
||||
}
|
||||
|
||||
// TempDir creates a new temp dir and return the temp dir path
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// fsutil.TempDir("", "example.*")
|
||||
// fsutil.TempDir("testdata", "example.*")
|
||||
func TempDir(dir, pattern string) (string, error) {
|
||||
return os.MkdirTemp(dir, pattern)
|
||||
}
|
||||
|
||||
// ************************************************************
|
||||
// write, copy files
|
||||
// ************************************************************
|
||||
|
||||
// MustSave create file and write contents to file, panic on error.
|
||||
//
|
||||
// - data type allow: string, []byte, io.Reader
|
||||
//
|
||||
// default option see NewOpenOption()
|
||||
func MustSave(filePath string, data any, optFns ...OpenOptionFunc) {
|
||||
basefn.MustOK(SaveFile(filePath, data, optFns...))
|
||||
}
|
||||
|
||||
// SaveFile create file and write contents to file. will auto create dir.
|
||||
//
|
||||
// - data type allow: string, []byte, io.Reader
|
||||
//
|
||||
// default option see NewOpenOption()
|
||||
func SaveFile(filePath string, data any, optFns ...OpenOptionFunc) error {
|
||||
opt := NewOpenOption(optFns...)
|
||||
return WriteFile(filePath, data, opt.Perm, opt.Flag)
|
||||
}
|
||||
|
||||
// WriteData Quick write any data to file, alias of PutContents
|
||||
func WriteData(filePath string, data any, fileFlag ...int) (int, error) {
|
||||
return PutContents(filePath, data, fileFlag...)
|
||||
}
|
||||
|
||||
// PutContents create file and write contents to file at once. Will auto create dir
|
||||
//
|
||||
// data type allows: string, []byte, io.Reader
|
||||
//
|
||||
// Tip: file flag default is FsCWTFlags (override write)
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// fsutil.PutContents(filePath, contents, fsutil.FsCWAFlags) // append write
|
||||
// fsutil.Must2(fsutil.PutContents(filePath, contents)) // panic on error
|
||||
func PutContents(filePath string, data any, fileFlag ...int) (int, error) {
|
||||
f, err := QuickOpenFile(filePath, basefn.FirstOr(fileFlag, FsCWTFlags))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return WriteOSFile(f, data)
|
||||
}
|
||||
|
||||
// WriteFile create file and write contents to file, can set perm for a file.
|
||||
//
|
||||
// data type allows: string, []byte, io.Reader
|
||||
//
|
||||
// Tip: file flag default is FsCWTFlags (override write)
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// fsutil.WriteFile(filePath, contents, fsutil.DefaultFilePerm, fsutil.FsCWAFlags)
|
||||
func WriteFile(filePath string, data any, perm os.FileMode, fileFlag ...int) error {
|
||||
flag := basefn.FirstOr(fileFlag, FsCWTFlags)
|
||||
f, err := OpenFile(filePath, flag, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = WriteOSFile(f, data)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteOSFile write data to give os.File, then close file.
|
||||
//
|
||||
// data type allows: string, []byte, io.Reader
|
||||
func WriteOSFile(f *os.File, data any) (n int, err error) {
|
||||
switch typData := data.(type) {
|
||||
case []byte:
|
||||
n, err = f.Write(typData)
|
||||
case string:
|
||||
n, err = f.WriteString(typData)
|
||||
case io.Reader: // eg: buffer
|
||||
var n64 int64
|
||||
n64, err = io.Copy(f, typData)
|
||||
n = int(n64)
|
||||
default:
|
||||
_ = f.Close()
|
||||
panic("WriteFile: data type only allow: []byte, string, io.Reader")
|
||||
}
|
||||
|
||||
if err1 := f.Close(); err1 != nil && err == nil {
|
||||
err = err1
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// CopyFile copy a file to another file path.
|
||||
func CopyFile(srcPath, dstPath string) error {
|
||||
srcFile, err := os.OpenFile(srcPath, FsRFlags, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// create and open file
|
||||
dstFile, err := QuickOpenFile(dstPath, FsCWTFlags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
return err
|
||||
}
|
||||
|
||||
// MustCopyFile copy file to another path.
|
||||
func MustCopyFile(srcPath, dstPath string) {
|
||||
err := CopyFile(srcPath, dstPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateContents read file contents, call handleFn(contents) handle, then write updated contents to file
|
||||
func UpdateContents(filePath string, handleFn func(bs []byte) []byte) error {
|
||||
osFile, err := os.OpenFile(filePath, os.O_RDWR|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer osFile.Close()
|
||||
|
||||
// read file contents
|
||||
if bs, err1 := io.ReadAll(osFile); err1 == nil {
|
||||
bs = handleFn(bs)
|
||||
_, err = osFile.Write(bs)
|
||||
} else {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateSymlink creates a symbolic link
|
||||
func CreateSymlink(target, linkPath string) error {
|
||||
// Check if the link already exists
|
||||
if IsFile(linkPath) {
|
||||
// Remove existing link/file
|
||||
if err := os.Remove(linkPath); err != nil {
|
||||
return fmt.Errorf("failed to remove existing symlink: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return os.Symlink(target, linkPath)
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package goutil
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Go is a basic promise implementation: it wraps calls a function in a goroutine
|
||||
// and returns a channel which will later return the function's return value.
|
||||
func Go(f func() error) error {
|
||||
ch := make(chan error)
|
||||
go func() {
|
||||
ch <- f()
|
||||
}()
|
||||
return <-ch
|
||||
}
|
||||
|
||||
// ErrFunc type
|
||||
type ErrFunc func() error
|
||||
|
||||
// CallOn call func on condition is true
|
||||
func CallOn(cond bool, fn ErrFunc) error {
|
||||
if cond {
|
||||
return fn()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IfElseFn call okFunc() on condition is true, else call elseFn()
|
||||
func IfElseFn(cond bool, okFn, elseFn ErrFunc) error {
|
||||
if cond {
|
||||
return okFn()
|
||||
}
|
||||
return elseFn()
|
||||
}
|
||||
|
||||
// CallOrElse call okFunc() on condition is true, else call elseFn()
|
||||
func CallOrElse(cond bool, okFn, elseFn ErrFunc) error {
|
||||
if cond {
|
||||
return okFn()
|
||||
}
|
||||
return elseFn()
|
||||
}
|
||||
|
||||
// SafeRun sync run a func. If the func panics, the panic value is returned as an error.
|
||||
func SafeRun(fn func()) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if e, ok := r.(error); ok {
|
||||
err = e
|
||||
} else {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
return err
|
||||
}
|
||||
|
||||
// SafeRunWithError sync run a func with error.
|
||||
// If the func panics, the panic value is returned as an error.
|
||||
func SafeRunWithError(fn func() error) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if e, ok := r.(error); ok {
|
||||
err = e
|
||||
} else {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return fn()
|
||||
}
|
||||
|
||||
// SafeGo async run a func.
|
||||
// If the func panics, the panic value will be handle by errHandler.
|
||||
func SafeGo(fn func(), errHandler func(error)) {
|
||||
go func() {
|
||||
if err := SafeRun(fn); err != nil {
|
||||
errHandler(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SafeGoWithError async run a func with error.
|
||||
// If the func panics, the panic value will be handle by errHandler.
|
||||
func SafeGoWithError(fn func() error, errHandler func(error)) {
|
||||
go func() {
|
||||
if err := SafeRunWithError(fn); err != nil {
|
||||
errHandler(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
// Package goutil 💪 Useful utils for Go: byte, int, string, array/slice, map, struct, reflect, error, time, format, CLI, ENV, filesystem,
|
||||
// system, testing, debug and more.
|
||||
package goutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gookit/goutil/internal/checkfn"
|
||||
"github.com/gookit/goutil/reflects"
|
||||
"github.com/gookit/goutil/structs"
|
||||
"github.com/gookit/goutil/x/basefn"
|
||||
"github.com/gookit/goutil/x/goinfo"
|
||||
)
|
||||
|
||||
// Value alias of structs.Value
|
||||
type Value = structs.Value
|
||||
|
||||
// Panicf format panic message use fmt.Sprintf
|
||||
func Panicf(format string, v ...any) {
|
||||
panic(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// PanicIf if cond = true, panics with an error message
|
||||
func PanicIf(cond bool, fmtAndArgs ...any) {
|
||||
basefn.PanicIf(cond, fmtAndArgs...)
|
||||
}
|
||||
|
||||
// PanicErr if error is not empty, will panic.
|
||||
// Alias of basefn.PanicErr()
|
||||
func PanicErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// PanicIfErr if error is not empty, will panic.
|
||||
// Alias of basefn.PanicErr()
|
||||
func PanicIfErr(err error) { PanicErr(err) }
|
||||
|
||||
// MustOK if error is not empty, will panic.
|
||||
// Alias of basefn.MustOK()
|
||||
func MustOK(err error) { PanicErr(err) }
|
||||
|
||||
// MustIgnore for return like (v, error). Ignore return v and will panic on error.
|
||||
//
|
||||
// Useful for io, file operation func: (n int, err error)
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // old
|
||||
// _, err := fn()
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
//
|
||||
// // new
|
||||
// goutil.MustIgnore(fn())
|
||||
func MustIgnore(_ any, err error) { PanicErr(err) }
|
||||
|
||||
// Must return like (v, error). will panic on error, otherwise return v.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // old
|
||||
// v, err := fn()
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
//
|
||||
// // new
|
||||
// v := goutil.Must(fn())
|
||||
func Must[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ErrOnFail return input error on cond is false, otherwise return nil
|
||||
func ErrOnFail(cond bool, err error) error {
|
||||
return OrError(cond, err)
|
||||
}
|
||||
|
||||
// OrError return input error on cond is false, otherwise return nil
|
||||
func OrError(cond bool, err error) error {
|
||||
if !cond {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OrValue get. like: if cond { okVal } else { elVal }
|
||||
func OrValue[T any](cond bool, okVal, elVal T) T {
|
||||
if cond {
|
||||
return okVal
|
||||
}
|
||||
return elVal
|
||||
}
|
||||
|
||||
// OrReturn call okFunc() on condition is true, else call elseFn()
|
||||
func OrReturn[T any](cond bool, okFn, elseFn func() T) T {
|
||||
if cond {
|
||||
return okFn()
|
||||
}
|
||||
return elseFn()
|
||||
}
|
||||
|
||||
//
|
||||
// ------------------------- check functions -------------------------
|
||||
//
|
||||
|
||||
// IsNil value check
|
||||
func IsNil(v any) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
return reflects.IsNil(reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
// IsZero value check, alias of the IsEmpty()
|
||||
var IsZero = IsEmpty
|
||||
|
||||
// IsEmpty value check
|
||||
func IsEmpty(v any) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
return reflects.IsEmpty(reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
// IsZeroReal Alias of the IsEmptyReal()
|
||||
var IsZeroReal = IsEmptyReal
|
||||
|
||||
// IsEmptyReal checks for empty given value and also real empty value if the passed value is a pointer
|
||||
func IsEmptyReal(v any) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
return reflects.IsEmptyReal(reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
// IsFunc value
|
||||
func IsFunc(val any) bool {
|
||||
if val == nil {
|
||||
return false
|
||||
}
|
||||
return reflect.TypeOf(val).Kind() == reflect.Func
|
||||
}
|
||||
|
||||
// IsEqual determines if two objects are considered equal.
|
||||
//
|
||||
// TIP: cannot compare a function type
|
||||
func IsEqual(src, dst any) bool {
|
||||
if src == nil || dst == nil {
|
||||
return src == dst
|
||||
}
|
||||
|
||||
// cannot compare a function type
|
||||
if IsFunc(src) || IsFunc(dst) {
|
||||
return false
|
||||
}
|
||||
return reflects.IsEqual(src, dst)
|
||||
}
|
||||
|
||||
// Contains try loop over the data check if the data includes the element.
|
||||
// alias of the IsContains
|
||||
//
|
||||
// TIP: only support types: string, map, array, slice
|
||||
//
|
||||
// map - check key exists
|
||||
// string - check sub-string exists
|
||||
// array,slice - check sub-element exists
|
||||
func Contains(data, elem any) bool {
|
||||
_, found := checkfn.Contains(data, elem)
|
||||
return found
|
||||
}
|
||||
|
||||
// IsContains try loop over the data check if the data includes the element.
|
||||
//
|
||||
// TIP: only support types: string, map, array, slice
|
||||
//
|
||||
// map - check key exists
|
||||
// string - check sub-string exists
|
||||
// array,slice - check sub-element exists
|
||||
func IsContains(data, elem any) bool {
|
||||
_, found := checkfn.Contains(data, elem)
|
||||
return found
|
||||
}
|
||||
|
||||
//
|
||||
// ------------------------- goinfo functions -------------------------
|
||||
//
|
||||
|
||||
// FuncName get func name
|
||||
func FuncName(f any) string {
|
||||
return goinfo.FuncName(f)
|
||||
}
|
||||
|
||||
// PkgName get the current package name. alias of goinfo.PkgName()
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// funcName := goutil.FuncName(fn)
|
||||
// pgkName := goutil.PkgName(funcName)
|
||||
func PkgName(funcName string) string {
|
||||
return goinfo.PkgName(funcName)
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package goutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gookit/goutil/structs"
|
||||
"github.com/gookit/goutil/syncs"
|
||||
)
|
||||
|
||||
// ErrGroup is a collection of goroutines working on subtasks that
|
||||
// are part of the same overall task.
|
||||
type ErrGroup = syncs.ErrGroup
|
||||
|
||||
// NewCtxErrGroup instance. use for batch run tasks, can with context.
|
||||
//
|
||||
// Deprecated: use syncs.NewCtxErrGroup instead
|
||||
func NewCtxErrGroup(ctx context.Context, limit ...int) (*ErrGroup, context.Context) {
|
||||
return syncs.NewCtxErrGroup(ctx, limit...)
|
||||
}
|
||||
|
||||
// NewErrGroup instance. use for batch run tasks
|
||||
//
|
||||
// Deprecated: use syncs.NewErrGroup instead
|
||||
func NewErrGroup(limit ...int) *ErrGroup {
|
||||
return syncs.NewErrGroup(limit...)
|
||||
}
|
||||
|
||||
// RunFn func
|
||||
type RunFn func(ctx *structs.Data) error
|
||||
|
||||
// QuickRun struct
|
||||
type QuickRun struct {
|
||||
ctx *structs.Data
|
||||
// err error
|
||||
fns []RunFn
|
||||
}
|
||||
|
||||
// NewQuickRun instance
|
||||
func NewQuickRun() *QuickRun {
|
||||
return &QuickRun{
|
||||
ctx: structs.NewData(),
|
||||
}
|
||||
}
|
||||
|
||||
// Add func for run
|
||||
func (p *QuickRun) Add(fns ...RunFn) *QuickRun {
|
||||
p.fns = append(p.fns, fns...)
|
||||
return p
|
||||
}
|
||||
|
||||
// Run all func
|
||||
func (p *QuickRun) Run() error {
|
||||
for i, fn := range p.fns {
|
||||
p.ctx.Set("_index", i)
|
||||
if err := fn(p.ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package checkfn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsNil value check
|
||||
func IsNil(v any) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return rv.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsSimpleKind kind in: string, bool, intX, uintX, floatX
|
||||
func IsSimpleKind(k reflect.Kind) bool {
|
||||
if reflect.String == k {
|
||||
return true
|
||||
}
|
||||
return k > reflect.Invalid && k <= reflect.Float64
|
||||
}
|
||||
|
||||
// IsEqual determines if two objects are considered equal.
|
||||
//
|
||||
// TIP: cannot compare function type
|
||||
func IsEqual(src, dst any) bool {
|
||||
if src == nil || dst == nil {
|
||||
return src == dst
|
||||
}
|
||||
|
||||
bs1, ok := src.([]byte)
|
||||
if !ok {
|
||||
return reflect.DeepEqual(src, dst)
|
||||
}
|
||||
|
||||
bs2, ok := dst.([]byte)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if bs1 == nil || bs2 == nil {
|
||||
return bs1 == nil && bs2 == nil
|
||||
}
|
||||
return bytes.Equal(bs1, bs2)
|
||||
}
|
||||
|
||||
// Contains try loop over the data check if the data includes the element.
|
||||
//
|
||||
// data allow types: string, map, array, slice
|
||||
//
|
||||
// map - check key exists
|
||||
// string - check sub-string exists
|
||||
// array,slice - check sub-element exists
|
||||
//
|
||||
// Returns:
|
||||
// - valid: data is valid
|
||||
// - found: element was found
|
||||
//
|
||||
// return (false, false) if impossible.
|
||||
// return (true, false) if element was not found.
|
||||
// return (true, true) if element was found.
|
||||
func Contains(data, elem any) (valid, found bool) {
|
||||
if data == nil {
|
||||
return false, false
|
||||
}
|
||||
|
||||
dataRv := reflect.ValueOf(data)
|
||||
dataRt := reflect.TypeOf(data)
|
||||
dataKind := dataRt.Kind()
|
||||
|
||||
// string
|
||||
if dataKind == reflect.String {
|
||||
return true, strings.Contains(dataRv.String(), fmt.Sprint(elem))
|
||||
}
|
||||
|
||||
// map
|
||||
if dataKind == reflect.Map {
|
||||
mapKeys := dataRv.MapKeys()
|
||||
for i := 0; i < len(mapKeys); i++ {
|
||||
if IsEqual(mapKeys[i].Interface(), elem) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
// array, slice - other return false
|
||||
if dataKind != reflect.Slice && dataKind != reflect.Array {
|
||||
return false, false
|
||||
}
|
||||
|
||||
for i := 0; i < dataRv.Len(); i++ {
|
||||
if IsEqual(dataRv.Index(i).Interface(), elem) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
// StringsContains check string slice contains sub-string
|
||||
func StringsContains(ss []string, sub string) bool {
|
||||
for _, v := range ss {
|
||||
if v == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
// check is number: int or float
|
||||
numReg = regexp.MustCompile(`^[-+]?\d*\.?\d+$`)
|
||||
// is positive number: int or float
|
||||
pNumReg = regexp.MustCompile(`^\d*\.?\d+$`)
|
||||
)
|
||||
|
||||
// IsNumeric returns true if the given string is a numeric, otherwise false.
|
||||
func IsNumeric(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return numReg.MatchString(s)
|
||||
}
|
||||
|
||||
// IsPositiveNum check input string is positive number
|
||||
func IsPositiveNum(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
if s[0] == '-' {
|
||||
return false
|
||||
}
|
||||
return pNumReg.MatchString(s)
|
||||
}
|
||||
|
||||
// IsHttpURL check input is http/https url
|
||||
func IsHttpURL(s string) bool {
|
||||
return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
|
||||
}
|
||||
|
||||
// IndexByteAfter find index of byte after startIndex. return -1 if not found
|
||||
//
|
||||
// eg:
|
||||
//
|
||||
// IndexByteAfter("abcabc", 'b', 0) = 1
|
||||
// IndexByteAfter("abcabc", 'b', 2) = 4
|
||||
func IndexByteAfter(s string, b byte, startIndex int) int {
|
||||
idx := strings.IndexByte(s[startIndex:], b)
|
||||
if idx < 0 {
|
||||
return -1
|
||||
}
|
||||
return idx + startIndex
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package checkfn
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var detectedWSL bool
|
||||
var detectedWSLContents string
|
||||
|
||||
// IsWSL system env
|
||||
// https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364
|
||||
func IsWSL() bool {
|
||||
// ENV:
|
||||
// WSL_DISTRO_NAME=Debian
|
||||
if len(os.Getenv("WSL_DISTRO_NAME")) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if !detectedWSL {
|
||||
b := make([]byte, 1024)
|
||||
// `cat /proc/version`
|
||||
// on mac:
|
||||
// !not the file!
|
||||
// on linux(debian,ubuntu,alpine):
|
||||
// Linux version 4.19.121-linuxkit (root@18b3f92ade35) (gcc version 9.2.0 (Alpine 9.2.0)) #1 SMP Thu Jan 21 15:36:34 UTC 2021
|
||||
// on win git bash, conEmu:
|
||||
// MINGW64_NT-10.0-19042 version 3.1.7-340.x86_64 (@WIN-N0G619FD3UK) (gcc version 9.3.0 (GCC) ) 2020-10-23 13:08 UTC
|
||||
// on WSL:
|
||||
// Linux version 4.4.0-19041-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #488-Microsoft Mon Sep 01 13:43:00 PST 2020
|
||||
f, err := os.Open("/proc/version")
|
||||
if err == nil {
|
||||
_, _ = f.Read(b) // ignore error
|
||||
f.Close()
|
||||
detectedWSLContents = string(b)
|
||||
}
|
||||
detectedWSL = true
|
||||
}
|
||||
return strings.Contains(detectedWSLContents, "Microsoft")
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# Common func for internal use
|
||||
|
||||
- don't depend on other external packages
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Cmdline build
|
||||
func Cmdline(args []string, binName ...string) string {
|
||||
b := new(strings.Builder)
|
||||
|
||||
if len(binName) > 0 {
|
||||
b.WriteString(binName[0])
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
|
||||
for i, a := range args {
|
||||
if i > 0 {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
|
||||
if strings.ContainsRune(a, '"') {
|
||||
b.WriteString(fmt.Sprintf(`'%s'`, a))
|
||||
} else if a == "" || strings.ContainsRune(a, '\'') || strings.ContainsRune(a, ' ') {
|
||||
b.WriteString(fmt.Sprintf(`"%s"`, a))
|
||||
} else {
|
||||
b.WriteString(a)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ShellQuote quote a string on contains ', ", SPACE. refer strconv.Quote()
|
||||
func ShellQuote(a string) string {
|
||||
if a == "" {
|
||||
return `""`
|
||||
}
|
||||
|
||||
// use quote char
|
||||
var quote byte
|
||||
|
||||
// has double quote
|
||||
if pos := strings.IndexByte(a, '"'); pos > -1 {
|
||||
if !checkNeedQuote(a, pos, '"') {
|
||||
return a
|
||||
}
|
||||
|
||||
quote = '\''
|
||||
} else if pos := strings.IndexByte(a, '\''); pos > -1 {
|
||||
// single quote
|
||||
if !checkNeedQuote(a, pos, '\'') {
|
||||
return a
|
||||
}
|
||||
quote = '"'
|
||||
} else if strings.IndexByte(a, ' ') > -1 {
|
||||
quote = '"'
|
||||
}
|
||||
|
||||
// no quote char OR not need quote
|
||||
if quote == 0 {
|
||||
return a
|
||||
}
|
||||
return fmt.Sprintf("%c%s%c", quote, a, quote)
|
||||
}
|
||||
|
||||
func checkNeedQuote(a string, pos int, char byte) bool {
|
||||
// end with char. eg: "
|
||||
lastIsQ := a[len(a)-1] == char
|
||||
|
||||
// start with char. eg: "
|
||||
if pos == 0 {
|
||||
if lastIsQ {
|
||||
return false
|
||||
}
|
||||
|
||||
if pos1 := strings.IndexByte(a[pos+1:], char); pos1 > -1 {
|
||||
// eg: `"one two" three four`
|
||||
lastS := a[pos1+pos+1:]
|
||||
if !strings.ContainsRune(lastS, ' ') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startS := a[:pos]
|
||||
|
||||
// eg: `--one="two three"`
|
||||
if lastIsQ && strings.IndexByte(startS, ' ') == -1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Environ like os.Environ, but will returns key-value map[string]string data.
|
||||
func Environ() map[string]string {
|
||||
envList := os.Environ()
|
||||
envMap := make(map[string]string, len(envList))
|
||||
|
||||
for _, str := range envList {
|
||||
nodes := strings.SplitN(str, "=", 2)
|
||||
if len(nodes) < 2 {
|
||||
envMap[nodes[0]] = ""
|
||||
} else {
|
||||
envMap[nodes[0]] = nodes[1]
|
||||
}
|
||||
}
|
||||
return envMap
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/internal/checkfn"
|
||||
)
|
||||
|
||||
// Bool try to convert type to bool
|
||||
func Bool(v any) bool {
|
||||
bl, _ := ToBool(v)
|
||||
return bl
|
||||
}
|
||||
|
||||
// ToBool try to convert type to bool
|
||||
func ToBool(v any) (bool, error) {
|
||||
if bl, ok := v.(bool); ok {
|
||||
return bl, nil
|
||||
}
|
||||
|
||||
if str, ok := v.(string); ok {
|
||||
return StrToBool(str)
|
||||
}
|
||||
return false, comdef.ErrConvType
|
||||
}
|
||||
|
||||
// StrToBool parse string to bool. like strconv.ParseBool()
|
||||
func StrToBool(s string) (bool, error) {
|
||||
lower := strings.ToLower(s)
|
||||
switch lower {
|
||||
case "1", "on", "yes", "true":
|
||||
return true, nil
|
||||
case "0", "off", "no", "false":
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("'%s' cannot convert to bool", s)
|
||||
}
|
||||
|
||||
// FormatWithArgs format message with args
|
||||
//
|
||||
// - only one element, format to string
|
||||
// - first is format: fmt.Sprintf(firstElem, fmtAndArgs[1:]...)
|
||||
// - all is args: return fmt.Sprint(fmtAndArgs...)
|
||||
func FormatWithArgs(fmtAndArgs []any) string {
|
||||
ln := len(fmtAndArgs)
|
||||
if ln == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
first := fmtAndArgs[0]
|
||||
if ln == 1 {
|
||||
if str, ok := first.(string); ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprintf("%+v", first)
|
||||
}
|
||||
|
||||
// is template string.
|
||||
if tplStr, ok := first.(string); ok && strings.IndexByte(tplStr, '%') >= 0 {
|
||||
return fmt.Sprintf(tplStr, fmtAndArgs[1:]...)
|
||||
}
|
||||
return fmt.Sprint(fmtAndArgs...)
|
||||
}
|
||||
|
||||
// ConvOption convert options
|
||||
type ConvOption struct {
|
||||
// if ture: value is nil, will return convert error;
|
||||
// if false(default): value is nil, will convert to zero value
|
||||
NilAsFail bool
|
||||
// HandlePtr auto convert ptr type(int,float,string) value. eg: *int to int
|
||||
// - if true: will use real type try convert. default is false
|
||||
// - NOTE: current T type's ptr is default support.
|
||||
HandlePtr bool
|
||||
// set custom fallback convert func for not supported type.
|
||||
UserConvFn comdef.ToStringFunc
|
||||
}
|
||||
|
||||
// ConvOptionFn convert option func
|
||||
type ConvOptionFn func(opt *ConvOption)
|
||||
|
||||
// StrBySprintFn convert any value to string by fmt.Sprint
|
||||
var StrBySprintFn = func(v any) (string, error) {
|
||||
return fmt.Sprint(v), nil
|
||||
}
|
||||
|
||||
// WithUserConvFn set ConvOption.UserConvFn option
|
||||
func WithUserConvFn(fn comdef.ToStringFunc) ConvOptionFn {
|
||||
return func(opt *ConvOption) {
|
||||
opt.UserConvFn = fn
|
||||
}
|
||||
}
|
||||
|
||||
// NewConvOption create a new ConvOption
|
||||
func NewConvOption(optFns ...ConvOptionFn) *ConvOption {
|
||||
opt := &ConvOption{}
|
||||
opt.WithOption(optFns...)
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithOption set convert option
|
||||
func (opt *ConvOption) WithOption(optFns ...ConvOptionFn) {
|
||||
for _, fn := range optFns {
|
||||
if fn != nil {
|
||||
fn(opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToStringWith try to convert value to string. can with some option func, more see ConvOption.
|
||||
func ToStringWith(in any, optFns ...ConvOptionFn) (str string, err error) {
|
||||
switch value := in.(type) {
|
||||
case int:
|
||||
str = strconv.Itoa(value)
|
||||
case int8:
|
||||
str = strconv.Itoa(int(value))
|
||||
case int16:
|
||||
str = strconv.Itoa(int(value))
|
||||
case int32: // same as `rune`
|
||||
str = strconv.Itoa(int(value))
|
||||
case int64:
|
||||
str = strconv.FormatInt(value, 10)
|
||||
case uint:
|
||||
str = strconv.FormatUint(uint64(value), 10)
|
||||
case uint8:
|
||||
str = strconv.FormatUint(uint64(value), 10)
|
||||
case uint16:
|
||||
str = strconv.FormatUint(uint64(value), 10)
|
||||
case uint32:
|
||||
str = strconv.FormatUint(uint64(value), 10)
|
||||
case uint64:
|
||||
str = strconv.FormatUint(value, 10)
|
||||
case float32:
|
||||
str = strconv.FormatFloat(float64(value), 'f', -1, 32)
|
||||
case float64:
|
||||
str = strconv.FormatFloat(value, 'f', -1, 64)
|
||||
case bool:
|
||||
str = strconv.FormatBool(value)
|
||||
case string:
|
||||
str = value
|
||||
case *string:
|
||||
str = *value
|
||||
case []byte:
|
||||
str = string(value)
|
||||
case time.Duration:
|
||||
str = strconv.FormatInt(int64(value), 10)
|
||||
case fmt.Stringer:
|
||||
str = value.String()
|
||||
case error:
|
||||
str = value.Error()
|
||||
default:
|
||||
if len(optFns) == 0 && in == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
opt := NewConvOption(optFns...)
|
||||
if in == nil {
|
||||
if opt.NilAsFail {
|
||||
err = comdef.ErrConvType
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if opt.HandlePtr {
|
||||
if rv := reflect.ValueOf(in); rv.Kind() == reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
if checkfn.IsSimpleKind(rv.Kind()) {
|
||||
return ToStringWith(rv.Interface(), optFns...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opt.UserConvFn != nil {
|
||||
str, err = opt.UserConvFn(in)
|
||||
} else {
|
||||
err = comdef.ErrConvType
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package comfunc
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// JoinPaths2 elements, like the filepath.Join()
|
||||
func JoinPaths2(basePath string, elems []string) string {
|
||||
paths := make([]string, len(elems)+1)
|
||||
paths[0] = basePath
|
||||
copy(paths[1:], elems)
|
||||
return filepath.Join(paths...)
|
||||
}
|
||||
|
||||
// JoinPaths3 elements, like the filepath.Join()
|
||||
func JoinPaths3(basePath, secPath string, elems []string) string {
|
||||
paths := make([]string, len(elems)+2)
|
||||
paths[0] = basePath
|
||||
paths[1] = secPath
|
||||
copy(paths[2:], elems)
|
||||
return filepath.Join(paths...)
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var commentsPrefixes = []string{"#", ";", "//"}
|
||||
|
||||
// ParseEnvLineOption parse env line options
|
||||
type ParseEnvLineOption struct {
|
||||
// NotInlineComments dont parse inline comments.
|
||||
// - default: false. will parse inline comments
|
||||
NotInlineComments bool
|
||||
// SkipOnErrorLine skip error line, continue parse next line
|
||||
// - False: return error, clear parsed map
|
||||
SkipOnErrorLine bool
|
||||
}
|
||||
|
||||
// ParseEnvLines parse simple multiline k-v string to a string-map.
|
||||
// Can use to parse simple INI or DOTENV file contents.
|
||||
//
|
||||
// NOTE:
|
||||
//
|
||||
// - It's like INI/ENV format contents.
|
||||
// - Support comments line starts with: "#", ";", "//"
|
||||
// - Support inline comments split with: " #" eg: "name=tom # a comments"
|
||||
// - DON'T support submap parse.
|
||||
func ParseEnvLines(text string, opt ParseEnvLineOption) (mp map[string]string, err error) {
|
||||
lines := strings.Split(text, "\n")
|
||||
ln := len(lines)
|
||||
if ln == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
strMap := make(map[string]string, ln)
|
||||
|
||||
for _, line := range lines {
|
||||
if line = strings.TrimSpace(line); line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip comments line
|
||||
if line[0] == '#' || line[0] == ';' || strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
}
|
||||
|
||||
key, val := splitLineByChar(line, '=', !opt.NotInlineComments)
|
||||
// invalid line
|
||||
if key == "" {
|
||||
if opt.SkipOnErrorLine {
|
||||
continue
|
||||
}
|
||||
strMap = nil
|
||||
err = fmt.Errorf("invalid line contents: must match `KEY=VAL`(line: %s)", line)
|
||||
return
|
||||
}
|
||||
|
||||
strMap[key] = val
|
||||
}
|
||||
|
||||
return strMap, nil
|
||||
}
|
||||
|
||||
// SplitLineToKv parse string line to k-v, not support comments.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// 'DEBUG=true' => ['DEBUG', 'true']
|
||||
//
|
||||
// NOTE: line must contain '=', allow: 'ENV_KEY='
|
||||
func SplitLineToKv(line, sep string) (string, string) {
|
||||
return SplitKvBySep(line, sep, false)
|
||||
}
|
||||
|
||||
// SplitKvBySep parse string line to k-v, support parse comments.
|
||||
// - rmInlineComments: check and remove inline comments by ' #'
|
||||
func SplitKvBySep(line, sep string, rmInlineComments bool) (key, val string) {
|
||||
sepPos := strings.Index(line, sep)
|
||||
if sepPos < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
return splitKvBySepPos(line, sepPos, len(sep), rmInlineComments)
|
||||
}
|
||||
|
||||
func splitLineByChar(line string, sep byte, rmInlineComments bool) (key, val string) {
|
||||
sepPos := strings.IndexByte(line, sep)
|
||||
if sepPos < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
return splitKvBySepPos(line, sepPos, 1, rmInlineComments)
|
||||
}
|
||||
|
||||
func splitKvBySepPos(line string, sepPos, sepLen int, rmInlineComments bool) (key, val string) {
|
||||
// key cannot be empty
|
||||
key = strings.TrimSpace(line[0:sepPos])
|
||||
if key == "" {
|
||||
return "", ""
|
||||
}
|
||||
val = strings.TrimSpace(line[sepPos+sepLen:])
|
||||
|
||||
// check quotes if present
|
||||
if vln := len(val); vln >= 2 {
|
||||
// remove quotes
|
||||
if (val[0] == '"' && val[vln-1] == '"') || (val[0] == '\'' && val[vln-1] == '\'') {
|
||||
val = val[1 : vln-1]
|
||||
return
|
||||
}
|
||||
|
||||
if !rmInlineComments {
|
||||
return
|
||||
}
|
||||
|
||||
// value is empty, only inline comments
|
||||
if val[0] == '#' {
|
||||
val = ""
|
||||
return
|
||||
}
|
||||
|
||||
// remove inline comments
|
||||
if pos := strings.Index(val, " #"); pos > 0 {
|
||||
val = strings.TrimRight(val[0:pos], " \t")
|
||||
vln = len(val)
|
||||
// remove quotes
|
||||
if (val[0] == '"' && val[vln-1] == '"') || (val[0] == '\'' && val[vln-1] == '\'') {
|
||||
val = val[1 : vln-1]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Workdir get
|
||||
func Workdir() string {
|
||||
dir, _ := os.Getwd()
|
||||
return dir
|
||||
}
|
||||
|
||||
// ExpandHome will parse first `~` as user home dir path.
|
||||
func ExpandHome(pathStr string) string {
|
||||
if len(pathStr) == 0 {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
if pathStr[0] != '~' {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
if len(pathStr) > 1 && pathStr[1] != '/' && pathStr[1] != '\\' {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return pathStr
|
||||
}
|
||||
return homeDir + pathStr[1:]
|
||||
}
|
||||
|
||||
// ExecCmd an command and return output.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ExecCmd("ls", []string{"-al"})
|
||||
func ExecCmd(binName string, args []string, workDir ...string) (string, error) {
|
||||
// create a new Cmd instance
|
||||
cmd := exec.Command(binName, args...)
|
||||
if len(workDir) > 0 {
|
||||
cmd.Dir = workDir[0]
|
||||
}
|
||||
|
||||
bs, err := cmd.Output()
|
||||
return string(bs), err
|
||||
}
|
||||
|
||||
var (
|
||||
cmdList = []string{"cmd", "cmd.exe"}
|
||||
pwshList = []string{"powershell", "powershell.exe", "pwsh", "pwsh.exe"}
|
||||
)
|
||||
|
||||
// ShellExec exec command by shell
|
||||
// cmdLine e.g. "ls -al"
|
||||
func ShellExec(cmdLine string, shells ...string) (string, error) {
|
||||
// shell := "/bin/sh"
|
||||
shell := "sh"
|
||||
if len(shells) > 0 {
|
||||
shell = shells[0]
|
||||
}
|
||||
|
||||
cmd := exec.Command(shell, "-c", cmdLine)
|
||||
bs, err := cmd.Output()
|
||||
return string(bs), err
|
||||
}
|
||||
|
||||
// curShellCache value
|
||||
var curShellCache string
|
||||
|
||||
// CurrentShell get current used shell env file.
|
||||
//
|
||||
// return like: "/bin/zsh" "/bin/bash". if onlyName=true, will return "zsh", "bash"
|
||||
func CurrentShell(onlyName bool, fallbackShell ...string) (binPath string) {
|
||||
var err error
|
||||
|
||||
fbShell := ""
|
||||
if len(fallbackShell) > 0 {
|
||||
fbShell = fallbackShell[0]
|
||||
}
|
||||
|
||||
if curShellCache == "" {
|
||||
// 检查父进程名称
|
||||
parentProcess := os.Getenv("GOPROCESS")
|
||||
if parentProcess != "" {
|
||||
binPath = parentProcess
|
||||
} else {
|
||||
binPath = os.Getenv("SHELL") // 适用于 Unix-like 系统
|
||||
if len(binPath) == 0 {
|
||||
// TODO check on Windows git bash
|
||||
binPath, err = ShellExec("echo $SHELL")
|
||||
if err != nil {
|
||||
binPath = fbShell
|
||||
}
|
||||
}
|
||||
binPath = strings.TrimSpace(binPath)
|
||||
}
|
||||
|
||||
// fix: 去除 .exe 后缀
|
||||
if pos := strings.IndexByte(binPath, '.'); pos > 0 {
|
||||
binPath = binPath[:pos]
|
||||
}
|
||||
|
||||
// cache result
|
||||
curShellCache = binPath
|
||||
} else {
|
||||
binPath = curShellCache
|
||||
}
|
||||
|
||||
if onlyName && len(binPath) > 0 {
|
||||
binPath = filepath.Base(binPath)
|
||||
} else if len(binPath) == 0 {
|
||||
binPath = fbShell
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func checkWinCurrentShell() string {
|
||||
// 在 Windows 上,可以检查 COMSPEC 环境变量
|
||||
comSpec := os.Getenv("COMSPEC")
|
||||
// 没法检查 pwsh, 返回的还是 cmd
|
||||
return comSpec
|
||||
}
|
||||
|
||||
// HasShellEnv has shell env check.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// HasShellEnv("sh")
|
||||
// HasShellEnv("bash")
|
||||
func HasShellEnv(shell string) bool {
|
||||
// can also use: "echo $0"
|
||||
out, err := ShellExec("echo OK", shell)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(out) == "OK"
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// check is duration string. TIP: extend unit d,w. eg: "1d", "2w"
|
||||
//
|
||||
// time.ParseDuration() is max support hour "h".
|
||||
durStrReg = regexp.MustCompile(`^-?([0-9]+(?:\.[0-9]*)?(ns|us|µs|ms|s|m|h|d|w))+$`)
|
||||
|
||||
// check long duration string. 验证整体格式是否符合
|
||||
//
|
||||
// eg: "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks", "1month"
|
||||
//
|
||||
// time.ParseDuration() is not support long unit.
|
||||
durStrRegL = regexp.MustCompile(`^-?([0-9]+(?:\.[0-9]*)?[nuµsmhdw][a-zA-Z]{0,8})+$`)
|
||||
// use for parse duration string. see ToDuration()
|
||||
//
|
||||
// NOTE: 解析时,不能加最后的 `+` 会导致只匹配了最后一组 时间单位
|
||||
durStrRegL2 = regexp.MustCompile(`-?([0-9]+(?:\.[0-9]*)?)([nuµsmhdw][a-z]{0,8})`)
|
||||
)
|
||||
|
||||
// IsDuration check the string is a duration string.
|
||||
func IsDuration(s string) bool {
|
||||
if s == "0" || durStrReg.MatchString(s) {
|
||||
return true
|
||||
}
|
||||
return durStrRegL.MatchString(s)
|
||||
}
|
||||
|
||||
// ToDuration parses a duration string. such as "300ms", "-1.5h" or "2h45m".
|
||||
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
//
|
||||
// Diff of time.ParseDuration:
|
||||
// - support extends unit d, w at the end of string. such as "1d", "2w".
|
||||
// - support extends unit: month, week, day
|
||||
// - support long string unit at the end. such as "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks".
|
||||
//
|
||||
// If the string is not a valid duration string, it will return an error.
|
||||
func ToDuration(s string) (time.Duration, error) {
|
||||
ln := len(s)
|
||||
if ln == 0 {
|
||||
return 0, fmt.Errorf("empty duration string")
|
||||
}
|
||||
|
||||
s = strings.ToLower(s)
|
||||
if s == "0" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// check duration string is valid
|
||||
if !durStrRegL.MatchString(s) {
|
||||
return 0, fmt.Errorf("invalid duration string: %s", s)
|
||||
}
|
||||
|
||||
// if ln < 4 AND end != d|w, directly call time.ParseDuration()
|
||||
if ln < 4 && s[ln-1] != 'd' && s[ln-1] != 'w' {
|
||||
return time.ParseDuration(s)
|
||||
}
|
||||
|
||||
// time.ParseDuration() is not support long unit.
|
||||
ssList := durStrRegL2.FindAllStringSubmatch(s, -1)
|
||||
// fmt.Println(ssList)
|
||||
bts := make([]byte, 0, ln)
|
||||
if s[0] == '-' {
|
||||
bts = append(bts, '-')
|
||||
}
|
||||
|
||||
// only one element. eg: "1day"
|
||||
if len(ssList) == 1 {
|
||||
bts = parseLongUnit(ssList[0], bts)
|
||||
} else {
|
||||
// more than one element. eg: "1day2hour3min"
|
||||
for _, ss := range ssList {
|
||||
if len(ss) == 3 {
|
||||
bts = parseLongUnit(ss, bts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return time.ParseDuration(string(bts))
|
||||
}
|
||||
|
||||
// convert to short unit
|
||||
func parseLongUnit(ss []string, bts []byte) []byte {
|
||||
// eg: "3sec" -> ss=[3sec, -3, sec]
|
||||
num, unit := ss[1], ss[2]
|
||||
switch unit {
|
||||
case "month", "months":
|
||||
// time lib max unit is hour, so need convert by 24 * 30*n
|
||||
bts = appendNumToBytes(bts, num, 24*30)
|
||||
bts = append(bts, 'h')
|
||||
case "w", "week", "weeks":
|
||||
// time lib max unit is hour, so need convert by 24 * 7*n
|
||||
bts = appendNumToBytes(bts, num, 24*7)
|
||||
bts = append(bts, 'h')
|
||||
case "d", "day", "days":
|
||||
// time lib max unit is hour, so need convert by 24*n
|
||||
bts = appendNumToBytes(bts, num, 24)
|
||||
bts = append(bts, 'h')
|
||||
case "hour", "hours":
|
||||
bts = append(bts, num...)
|
||||
bts = append(bts, 'h')
|
||||
case "min", "mins", "minute", "minutes":
|
||||
bts = append(bts, num...)
|
||||
bts = append(bts, 'm')
|
||||
case "sec", "secs", "second", "seconds":
|
||||
bts = append(bts, num...)
|
||||
bts = append(bts, 's')
|
||||
default:
|
||||
first := ss[0]
|
||||
|
||||
// '-' has been added on ToDuration()
|
||||
if first[0] == '-' {
|
||||
bts = append(bts, first[1:]...)
|
||||
} else {
|
||||
bts = append(bts, first...)
|
||||
}
|
||||
}
|
||||
|
||||
return bts
|
||||
}
|
||||
|
||||
func appendNumToBytes(bts []byte, num string, multiple int) []byte {
|
||||
if strings.ContainsRune(num, '.') {
|
||||
f, _ := strconv.ParseFloat(num, 64) // is float number
|
||||
val := f * float64(multiple)
|
||||
|
||||
// 使用 Float 保留两位小数 -> 会始终有两位小数,即使是N.00
|
||||
// bts = strconv.AppendFloat(bts, val, 'f', 2, 64)
|
||||
|
||||
// 四舍五入到两位小数
|
||||
rounded := math.Round(val*100) / 100
|
||||
// 使用 AppendFloat 自动去除末尾的 .0 或 .00
|
||||
bts = strconv.AppendFloat(bts, rounded, 'f', -1, 64)
|
||||
} else {
|
||||
n, _ := strconv.Atoi(num)
|
||||
bts = strconv.AppendInt(bts, int64(n*multiple), 10)
|
||||
}
|
||||
|
||||
return bts
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
// Package varexpr provides some commonly ENV var parse functions.
|
||||
//
|
||||
// parse env value, allow expressions:
|
||||
//
|
||||
// ${VAR_NAME} Only var name
|
||||
// ${VAR_NAME | default} With default value, if value is empty.
|
||||
// ${VAR_NAME | ?error} With error on value is empty.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// only key - "${SHELL}"
|
||||
// with default - "${NotExist | defValue}"
|
||||
// multi key - "${GOPATH}/${APP_ENV | prod}/dir"
|
||||
package varexpr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// SepChar separator char split var name and default value
|
||||
SepChar = "|"
|
||||
VarLeft = "${" // default var left format chars
|
||||
VarRight = "}" // default var right format chars
|
||||
|
||||
mustPrefix = '?' // must prefix char
|
||||
)
|
||||
|
||||
// ParseOptFn option func
|
||||
type ParseOptFn func(o *ParseOpts)
|
||||
|
||||
// ParseOpts parse options for ParseValue
|
||||
type ParseOpts struct {
|
||||
// Getter Env value provider func.
|
||||
Getter func(string) string
|
||||
// ParseFn custom parse expr func. expr like "${SHELL}" "${NotExist|defValue}"
|
||||
ParseFn func(string) (string, error)
|
||||
// Regexp custom expression regex.
|
||||
Regexp *regexp.Regexp
|
||||
// var format chars for expression.
|
||||
// default left="${", right="}"
|
||||
VarLeft, VarRight string
|
||||
}
|
||||
|
||||
func (opt *ParseOpts) useDefaultRegex() {
|
||||
opt.Regexp = envRegex
|
||||
opt.VarLeft = VarLeft
|
||||
opt.VarRight = VarRight
|
||||
}
|
||||
|
||||
// must add "?" - To ensure that there is no greedy match
|
||||
var envRegex = regexp.MustCompile(`\${.+?}`)
|
||||
var std = New()
|
||||
|
||||
// Parse parse ENV var value from input string, support default value.
|
||||
//
|
||||
// Format:
|
||||
//
|
||||
// ${var_name} Only var name
|
||||
// ${var_name | default} With default value
|
||||
// ${var_name | ?error} With error on value is empty.
|
||||
//
|
||||
// see Parser.Parse
|
||||
func Parse(val string) (string, error) {
|
||||
return std.Parse(val)
|
||||
}
|
||||
|
||||
// SafeParse parse ENV var value from input string, support default value.
|
||||
//
|
||||
// see Parser.Parse
|
||||
func SafeParse(val string) string {
|
||||
s, _ := std.Parse(val)
|
||||
return s
|
||||
}
|
||||
|
||||
// ParseWith parse ENV var value from input string, support default value.
|
||||
func ParseWith(val string, optFns ...ParseOptFn) (string, error) {
|
||||
return New(optFns...).Parse(val)
|
||||
}
|
||||
|
||||
// Parser parse ENV var value from input string, support default value.
|
||||
type Parser struct {
|
||||
ParseOpts
|
||||
}
|
||||
|
||||
// New create a new Parser
|
||||
func New(optFns ...ParseOptFn) *Parser {
|
||||
opts := &ParseOpts{Getter: os.Getenv}
|
||||
opts.useDefaultRegex()
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(opts)
|
||||
}
|
||||
return &Parser{ParseOpts: *opts}
|
||||
}
|
||||
|
||||
// Parse parse ENV var value from input string, support default value.
|
||||
//
|
||||
// Format:
|
||||
//
|
||||
// ${var_name} Only var name
|
||||
// ${var_name | default} With default value
|
||||
// ${var_name | ?error} With error on value is empty.
|
||||
// ${VAR_NAME1}/path/${VAR_NAME2} Allow multi var name.
|
||||
func (p *Parser) Parse(val string) (newVal string, err error) {
|
||||
if p.Regexp == nil {
|
||||
p.useDefaultRegex()
|
||||
}
|
||||
|
||||
times := strings.Count(val, p.VarLeft)
|
||||
if times == 0 {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// enhance: see https://github.com/gookit/goutil/issues/135
|
||||
if times == 1 && strings.HasPrefix(val, p.VarLeft) && strings.HasSuffix(val, p.VarRight) {
|
||||
return p.parseOne(val)
|
||||
}
|
||||
|
||||
// parse expression
|
||||
newVal = p.Regexp.ReplaceAllStringFunc(val, func(s string) string {
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
s, err = p.parseOne(s)
|
||||
return s
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// parse one node expression.
|
||||
func (p *Parser) parseOne(eVar string) (val string, err error) {
|
||||
if p.ParseFn != nil {
|
||||
return p.ParseFn(eVar)
|
||||
}
|
||||
|
||||
// like "${NotExist | defValue}". first remove "${" and "}", then split it
|
||||
ss := strings.SplitN(eVar[2:len(eVar)-1], SepChar, 2)
|
||||
var name, def string
|
||||
|
||||
// with default value.
|
||||
if len(ss) == 2 {
|
||||
name, def = strings.TrimSpace(ss[0]), strings.TrimSpace(ss[1])
|
||||
} else {
|
||||
name = strings.TrimSpace(ss[0])
|
||||
}
|
||||
|
||||
// get ENV value by name
|
||||
val = p.Getter(name)
|
||||
if val == "" && def != "" {
|
||||
// check def is "?error"
|
||||
if def[0] == mustPrefix {
|
||||
msg := "value is required for var: " + name
|
||||
if len(def) > 1 {
|
||||
msg = def[1:]
|
||||
}
|
||||
err = errors.New(msg)
|
||||
} else {
|
||||
val = def
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package jsonutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// MustString encode data to json string, will panic on error
|
||||
func MustString(v any) string {
|
||||
bs, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
// Encode data to json bytes. alias of json.Marshal
|
||||
func Encode(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// EncodePretty encode data to pretty JSON bytes.
|
||||
func EncodePretty(v any) ([]byte, error) {
|
||||
return json.MarshalIndent(v, "", " ")
|
||||
}
|
||||
|
||||
// EncodeString encode data to JSON string.
|
||||
func EncodeString(v any) (string, error) {
|
||||
bs, err := json.MarshalIndent(v, "", " ")
|
||||
return string(bs), err
|
||||
}
|
||||
|
||||
// EncodeToWriter encode data to json and write to writer.
|
||||
func EncodeToWriter(v any, w io.Writer) error {
|
||||
return json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
// EncodeUnescapeHTML data to json bytes. will close escape HTML
|
||||
func EncodeUnescapeHTML(v any) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Decode json bytes to data ptr. alias of json.Unmarshal
|
||||
func Decode(bts []byte, ptr any) error {
|
||||
return json.Unmarshal(bts, ptr)
|
||||
}
|
||||
|
||||
// DecodeString json string to data ptr.
|
||||
func DecodeString(str string, ptr any) error {
|
||||
return json.Unmarshal([]byte(str), ptr)
|
||||
}
|
||||
|
||||
// DecodeReader decode JSON from io reader.
|
||||
func DecodeReader(r io.Reader, ptr any) error {
|
||||
return json.NewDecoder(r).Decode(ptr)
|
||||
}
|
||||
|
||||
// DecodeFile decode JSON from file, bind data to ptr.
|
||||
func DecodeFile(file string, ptr any) error {
|
||||
bs, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(bs, ptr)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package jsonutil
|
||||
|
||||
/*
|
||||
TODO json build
|
||||
type JsonBuilder struct {
|
||||
Indent string
|
||||
// mu sync.Mutex
|
||||
// cfg *CConfig
|
||||
buf bytes.Buffer
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// AddField add field to json
|
||||
func (b *JsonBuilder) AddField(key string, value any) *JsonBuilder {
|
||||
b.buf.WriteString(`,"`)
|
||||
b.buf.WriteString(key)
|
||||
b.buf.WriteString(`":`)
|
||||
b.encode(value)
|
||||
return b
|
||||
}
|
||||
*/
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
// Package jsonutil provide some util functions for quick operate JSON data
|
||||
package jsonutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/scanner"
|
||||
)
|
||||
|
||||
// WriteFile write data to JSON file
|
||||
func WriteFile(filePath string, data any) error {
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, jsonBytes, 0664)
|
||||
}
|
||||
|
||||
// WritePretty write pretty data to JSON file
|
||||
func WritePretty(filePath string, data any) error {
|
||||
bs, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, bs, 0664)
|
||||
}
|
||||
|
||||
// ReadFile Read JSON file data
|
||||
func ReadFile(filePath string, v any) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
return json.NewDecoder(file).Decode(v)
|
||||
}
|
||||
|
||||
// Pretty JSON string and return
|
||||
func Pretty(v any) (string, error) {
|
||||
out, err := json.MarshalIndent(v, "", " ")
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
// MustPretty data to JSON string, will panic on error
|
||||
func MustPretty(v any) string {
|
||||
out, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// Mapping src data(map,struct) to dst struct use json tags.
|
||||
//
|
||||
// On src, dst both is struct, equivalent to merging two structures (src should be a subset of dsc)
|
||||
func Mapping(src, dst any) error {
|
||||
bts, err := json.Marshal(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Decode(bts, dst)
|
||||
}
|
||||
|
||||
// IsJSON check if the string is valid JSON. (Note: uses json.Valid)
|
||||
func IsJSON(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return json.Valid([]byte(s))
|
||||
}
|
||||
|
||||
// IsJSONFast simple and fast check input is valid JSON array or object.
|
||||
func IsJSONFast(s string) bool {
|
||||
ln := len(s)
|
||||
if ln < 2 {
|
||||
return false
|
||||
}
|
||||
if ln == 2 {
|
||||
return s == "{}" || s == "[]"
|
||||
}
|
||||
|
||||
// object
|
||||
if s[0] == '{' {
|
||||
return s[ln-1] == '}' && s[1] == '"'
|
||||
}
|
||||
|
||||
// array
|
||||
return s[0] == '[' && s[ln-1] == ']'
|
||||
}
|
||||
|
||||
// IsArray check if the string is valid JSON array.
|
||||
func IsArray(s string) bool {
|
||||
ln := len(s)
|
||||
if ln < 2 {
|
||||
return false
|
||||
}
|
||||
return s[0] == '[' && s[ln-1] == ']'
|
||||
}
|
||||
|
||||
// IsObject check if the string is valid JSON object.
|
||||
func IsObject(s string) bool {
|
||||
ln := len(s)
|
||||
if ln < 2 {
|
||||
return false
|
||||
}
|
||||
if ln == 2 {
|
||||
return s == "{}"
|
||||
}
|
||||
|
||||
// object
|
||||
if s[0] == '{' {
|
||||
return s[ln-1] == '}' && s[1] == '"'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// `(?s:` enable match multi line
|
||||
var jsonMLComments = regexp.MustCompile(`(?s:/\*.*?\*/\s*)`)
|
||||
|
||||
// StripComments strip comments for a JSON string
|
||||
func StripComments(src string) string {
|
||||
// multi line comments
|
||||
if strings.Contains(src, "/*") {
|
||||
src = jsonMLComments.ReplaceAllString(src, "")
|
||||
}
|
||||
|
||||
// single line comments
|
||||
if !strings.Contains(src, "//") {
|
||||
return strings.TrimSpace(src)
|
||||
}
|
||||
|
||||
// strip inline comments
|
||||
var s scanner.Scanner
|
||||
|
||||
s.Init(strings.NewReader(src))
|
||||
s.Filename = "comments"
|
||||
s.Mode ^= scanner.SkipComments // don't skip comments
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
|
||||
txt := s.TokenText()
|
||||
if !strings.HasPrefix(txt, "//") && !strings.HasPrefix(txt, "/*") {
|
||||
buf.WriteString(txt)
|
||||
// } else {
|
||||
// fmt.Printf("%s: %s\n", s.Position, txt)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Map Utils
|
||||
|
||||
`maputil` provide map data util functions. eg: convert, sub-value get, simple merge
|
||||
|
||||
- use `map[string]any` as Data
|
||||
- deep get value by key path
|
||||
- deep set value by key path
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/gookit/goutil/maputil
|
||||
```
|
||||
|
||||
## Go docs
|
||||
|
||||
- [Go docs](https://pkg.go.dev/github.com/gookit/goutil/maputil)
|
||||
|
||||
## Usage
|
||||
|
||||
### Deep get value
|
||||
|
||||
```go
|
||||
mp := map[string]any {
|
||||
"top1": "val1",
|
||||
"arr1": []string{"ab", "cd"}
|
||||
"map1": map[string]any{
|
||||
"sub1": "val2",
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println(maputil.DeepGet(mp, "map1.sub1")) // Output: VAL3
|
||||
|
||||
// get value from slice.
|
||||
fmt.Println(maputil.DeepGet(mp, "arr1.1")) // Output: cd
|
||||
fmt.Println(maputil.DeepGet(mp, "arr1[1]")) // Output: cd
|
||||
```
|
||||
|
||||
### Deep set value
|
||||
|
||||
```go
|
||||
mp := map[string]any {
|
||||
"top1": "val1",
|
||||
"arr1": []string{"ab"}
|
||||
"map1": map[string]any{
|
||||
"sub1": "val2",
|
||||
},
|
||||
}
|
||||
|
||||
err := maputil.SetByPath(&mp, "map1.newKey", "VAL3")
|
||||
|
||||
fmt.Println(maputil.DeepGet(mp, "map1.newKey")) // Output: VAL3
|
||||
```
|
||||
|
||||
## Code Check & Testing
|
||||
|
||||
```bash
|
||||
gofmt -w -l ./
|
||||
golint ./...
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
|
||||
```shell
|
||||
go test -v ./maputil/...
|
||||
```
|
||||
|
||||
**Test limit by regexp**:
|
||||
|
||||
```shell
|
||||
go test -v -run ^TestSetByKeys ./maputil/...
|
||||
```
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package maputil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Aliases implemented a simple string alias map.
|
||||
// - key: alias, value: real name
|
||||
type Aliases map[string]string
|
||||
|
||||
// AddAlias to the Aliases map
|
||||
func (as Aliases) AddAlias(alias, real string) {
|
||||
if rn, ok := as[alias]; ok {
|
||||
panic(fmt.Sprintf("The alias '%s' is already used by '%s'", alias, rn))
|
||||
}
|
||||
as[alias] = real
|
||||
}
|
||||
|
||||
// AddAliases to the Aliases map
|
||||
func (as Aliases) AddAliases(real string, aliases []string) {
|
||||
for _, a := range aliases {
|
||||
as.AddAlias(a, real)
|
||||
}
|
||||
}
|
||||
|
||||
// AddAliasMap to the Aliases map
|
||||
func (as Aliases) AddAliasMap(alias2real map[string]string) {
|
||||
for a, r := range alias2real {
|
||||
as.AddAlias(a, r)
|
||||
}
|
||||
}
|
||||
|
||||
// HasAlias in the Aliases map
|
||||
func (as Aliases) HasAlias(alias string) bool {
|
||||
if _, ok := as[alias]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResolveAlias by given name.
|
||||
func (as Aliases) ResolveAlias(alias string) string {
|
||||
if name, ok := as[alias]; ok {
|
||||
return name
|
||||
}
|
||||
return alias
|
||||
}
|
||||
|
||||
// AliasesNames returns all sorted alias names.
|
||||
func (as Aliases) AliasesNames() []string {
|
||||
ns := make([]string, 0, len(as))
|
||||
for alias := range as {
|
||||
ns = append(ns, alias)
|
||||
}
|
||||
sort.Strings(ns)
|
||||
return ns
|
||||
}
|
||||
|
||||
// GroupAliases groups aliases by real name.
|
||||
//
|
||||
// returns: {real name -> []aliases, ...}
|
||||
func (as Aliases) GroupAliases() map[string][]string {
|
||||
gaMap := make(map[string][]string)
|
||||
for alias, name := range as {
|
||||
gaMap[name] = append(gaMap[name], alias)
|
||||
}
|
||||
return gaMap
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package maputil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/gookit/goutil/reflects"
|
||||
)
|
||||
|
||||
// HasKey check of the given map.
|
||||
func HasKey(mp, key any) (ok bool) {
|
||||
rftVal := reflect.Indirect(reflect.ValueOf(mp))
|
||||
if rftVal.Kind() != reflect.Map {
|
||||
return
|
||||
}
|
||||
|
||||
for _, keyRv := range rftVal.MapKeys() {
|
||||
if reflects.IsEqual(keyRv.Interface(), key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// HasOneKey check of the given map. return the first exist key
|
||||
func HasOneKey(mp any, keys ...any) (ok bool, key any) {
|
||||
rftVal := reflect.Indirect(reflect.ValueOf(mp))
|
||||
if rftVal.Kind() != reflect.Map {
|
||||
return
|
||||
}
|
||||
|
||||
for _, key = range keys {
|
||||
for _, keyRv := range rftVal.MapKeys() {
|
||||
if reflects.IsEqual(keyRv.Interface(), key) {
|
||||
return true, key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// HasAllKeys check of the given map. return the first not exist key
|
||||
func HasAllKeys(mp any, keys ...any) (ok bool, noKey any) {
|
||||
rftVal := reflect.Indirect(reflect.ValueOf(mp))
|
||||
if rftVal.Kind() != reflect.Map {
|
||||
return
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
var exist bool
|
||||
for _, keyRv := range rftVal.MapKeys() {
|
||||
if reflects.IsEqual(keyRv.Interface(), key) {
|
||||
exist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exist {
|
||||
return false, key
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
package maputil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/arrutil"
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/reflects"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// alias functions
|
||||
var (
|
||||
// ToStrMap convert map[string]any to map[string]string
|
||||
ToStrMap = ToStringMap
|
||||
// ToL2StrMap convert map[string]any to map[string]map[string]string
|
||||
ToL2StrMap = ToL2StringMap
|
||||
)
|
||||
|
||||
// KeyToLower convert keys to lower case.
|
||||
func KeyToLower(src map[string]string) map[string]string {
|
||||
if len(src) == 0 {
|
||||
return src
|
||||
}
|
||||
|
||||
newMp := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
k = strings.ToLower(k)
|
||||
newMp[k] = v
|
||||
}
|
||||
return newMp
|
||||
}
|
||||
|
||||
// AnyToStrMap try convert any(map[string]any, map[string]string) to map[string]string
|
||||
func AnyToStrMap(src any) map[string]string {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if m, ok := src.(map[string]string); ok {
|
||||
return m
|
||||
}
|
||||
if m, ok := src.(map[string]any); ok {
|
||||
return ToStringMap(m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToStringMap simple convert map[string]any to map[string]string
|
||||
func ToStringMap(src map[string]any) map[string]string {
|
||||
strMp := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
strMp[k] = strutil.SafeString(v)
|
||||
}
|
||||
return strMp
|
||||
}
|
||||
|
||||
// ToL2StringMap convert map[string]any to map[string]map[string]string
|
||||
func ToL2StringMap(groupsMap map[string]any) map[string]map[string]string {
|
||||
if len(groupsMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
l2sMap := make(map[string]map[string]string, len(groupsMap))
|
||||
|
||||
for k, v := range groupsMap {
|
||||
if mp, ok := v.(map[string]any); ok {
|
||||
l2sMap[k] = ToStringMap(mp)
|
||||
} else if smp, ok := v.(map[string]string); ok {
|
||||
l2sMap[k] = smp
|
||||
}
|
||||
}
|
||||
return l2sMap
|
||||
}
|
||||
|
||||
// CombineToSMap combine two string-slices to SMap(map[string]string)
|
||||
func CombineToSMap(keys, values []string) SMap {
|
||||
return arrutil.CombineToSMap(keys, values)
|
||||
}
|
||||
|
||||
// CombineToMap combine two any slice to map[K]V. alias of arrutil.CombineToMap
|
||||
func CombineToMap[K comdef.SortedType, V any](keys []K, values []V) map[K]V {
|
||||
return arrutil.CombineToMap(keys, values)
|
||||
}
|
||||
|
||||
// SliceToSMap convert string k-v pairs slice to map[string]string
|
||||
// - 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
|
||||
if ln == 0 || ln%2 != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sMap := make(map[string]string, ln/2)
|
||||
for i := 0; i < ln; i += 2 {
|
||||
sMap[kvPairs[i]] = kvPairs[i+1]
|
||||
}
|
||||
return sMap
|
||||
}
|
||||
|
||||
// SliceToMap convert any k-v pairs slice to map[string]any
|
||||
func SliceToMap(kvPairs ...any) map[string]any {
|
||||
ln := len(kvPairs)
|
||||
// check kvPairs length must be even
|
||||
if ln == 0 || ln%2 != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mp := make(map[string]any, ln/2)
|
||||
for i := 0; i < ln; i += 2 {
|
||||
kStr := strutil.SafeString(kvPairs[i])
|
||||
mp[kStr] = kvPairs[i+1]
|
||||
}
|
||||
return mp
|
||||
}
|
||||
|
||||
// SliceToTypeMap convert k-v pairs slice to map[string]T
|
||||
func SliceToTypeMap[T any](valFunc func(any) T, kvPairs ...any) map[string]T {
|
||||
ln := len(kvPairs)
|
||||
// check kvPairs length must be even
|
||||
if ln == 0 || ln%2 != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mp := make(map[string]T, ln/2)
|
||||
for i := 0; i < ln; i += 2 {
|
||||
kStr := strutil.SafeString(kvPairs[i])
|
||||
mp[kStr] = valFunc(kvPairs[i+1])
|
||||
}
|
||||
return mp
|
||||
}
|
||||
|
||||
// ToAnyMap convert map[TYPE1]TYPE2 to map[string]any
|
||||
func ToAnyMap(mp any) map[string]any {
|
||||
amp, _ := TryAnyMap(mp)
|
||||
return amp
|
||||
}
|
||||
|
||||
// TryAnyMap convert map[TYPE1]TYPE2 to map[string]any
|
||||
func TryAnyMap(mp any) (map[string]any, error) {
|
||||
if aMp, ok := mp.(map[string]any); ok {
|
||||
return aMp, nil
|
||||
}
|
||||
if sMp, ok := mp.(map[string]string); ok {
|
||||
anyMp := make(map[string]any, len(sMp))
|
||||
for k, v := range sMp {
|
||||
anyMp[k] = v
|
||||
}
|
||||
return anyMp, nil
|
||||
}
|
||||
|
||||
rv := reflect.Indirect(reflect.ValueOf(mp))
|
||||
if rv.Kind() != reflect.Map {
|
||||
return nil, errors.New("input is not a map value type")
|
||||
}
|
||||
|
||||
anyMp := make(map[string]any, rv.Len())
|
||||
for _, key := range rv.MapKeys() {
|
||||
keyStr := strutil.SafeString(key.Interface())
|
||||
anyMp[keyStr] = rv.MapIndex(key).Interface()
|
||||
}
|
||||
return anyMp, nil
|
||||
}
|
||||
|
||||
// HTTPQueryString convert map[string]any data to http query string.
|
||||
func HTTPQueryString(data map[string]any) string {
|
||||
ss := make([]string, 0, len(data))
|
||||
for k, v := range data {
|
||||
ss = append(ss, k+"="+strutil.QuietString(v))
|
||||
}
|
||||
|
||||
return strings.Join(ss, "&")
|
||||
}
|
||||
|
||||
// StringsMapToAnyMap convert map[string][]string to map[string]any
|
||||
//
|
||||
// Example:
|
||||
// {"k1": []string{"v1", "v2"}, "k2": []string{"v3"}}
|
||||
// =>
|
||||
// {"k": []string{"v1", "v2"}, "k2": "v3"}
|
||||
//
|
||||
// mp := StringsMapToAnyMap(httpReq.Header)
|
||||
func StringsMapToAnyMap(ssMp map[string][]string) map[string]any {
|
||||
if len(ssMp) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
anyMp := make(map[string]any, len(ssMp))
|
||||
for k, v := range ssMp {
|
||||
if len(v) == 1 {
|
||||
anyMp[k] = v[0]
|
||||
continue
|
||||
}
|
||||
anyMp[k] = v
|
||||
}
|
||||
return anyMp
|
||||
}
|
||||
|
||||
// ToString simple and quickly convert map[string]any to string.
|
||||
func ToString(mp map[string]any) string {
|
||||
if mp == nil {
|
||||
return ""
|
||||
}
|
||||
if len(mp) == 0 {
|
||||
return "{}"
|
||||
}
|
||||
|
||||
buf := make([]byte, 0, len(mp)*16)
|
||||
buf = append(buf, '{')
|
||||
|
||||
for k, val := range mp {
|
||||
buf = append(buf, k...)
|
||||
buf = append(buf, ':')
|
||||
|
||||
str := strutil.QuietString(val)
|
||||
buf = append(buf, str...)
|
||||
buf = append(buf, ',', ' ')
|
||||
}
|
||||
|
||||
// remove last ', '
|
||||
buf = append(buf[:len(buf)-2], '}')
|
||||
return strutil.Byte2str(buf)
|
||||
}
|
||||
|
||||
// ToString2 simple and quickly convert a map to string.
|
||||
func ToString2(mp any) string { return NewFormatter(mp).Format() }
|
||||
|
||||
// FormatIndent format map data to string with newline and indent.
|
||||
func FormatIndent(mp any, indent string) string {
|
||||
return NewFormatter(mp).WithIndent(indent).Format()
|
||||
}
|
||||
|
||||
// StrMapToText 将 map[string]string 转换为多行 key=value 格式文本
|
||||
func StrMapToText(m map[string]string) string {
|
||||
if len(m) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for key, value := range m {
|
||||
lines = append(lines, key+"="+value)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* Flat convert tree map to flatten key-value map.
|
||||
*************************************************************/
|
||||
|
||||
// Flatten convert tree map to flat key-value map.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// {"top": {"sub": "value", "sub2": "value2"} }
|
||||
// ->
|
||||
// {"top.sub": "value", "top.sub2": "value2" }
|
||||
func Flatten(mp map[string]any) map[string]any {
|
||||
if mp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
flatMp := make(map[string]any, len(mp)*2)
|
||||
reflects.FlatMap(reflect.ValueOf(mp), func(path string, val reflect.Value) {
|
||||
flatMp[path] = val.Interface()
|
||||
})
|
||||
|
||||
return flatMp
|
||||
}
|
||||
|
||||
// FlatWithFunc flat a tree-map with custom collect handle func
|
||||
func FlatWithFunc(mp map[string]any, fn reflects.FlatFunc) {
|
||||
if mp == nil || fn == nil {
|
||||
return
|
||||
}
|
||||
reflects.FlatMap(reflect.ValueOf(mp), fn)
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
package maputil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/arrutil"
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"github.com/gookit/goutil/mathutil"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// Map alias of Data
|
||||
type Map = Data
|
||||
|
||||
// Data alias of map[string]any
|
||||
type Data map[string]any
|
||||
|
||||
// Has value on the data map
|
||||
func (d Data) Has(key string) bool {
|
||||
_, ok := d.GetByPath(key)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsEmpty if the data map
|
||||
func (d Data) IsEmpty() bool {
|
||||
return len(d) == 0
|
||||
}
|
||||
|
||||
//
|
||||
// endregion
|
||||
// region T: set value(s)
|
||||
//
|
||||
|
||||
// Set value to the data map
|
||||
func (d Data) Set(key string, val any) {
|
||||
d[key] = val
|
||||
}
|
||||
|
||||
// SetByPath sets a value in the map.
|
||||
// Supports dot syntax to set deep values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// d.SetByPath("name.first", "Mat")
|
||||
func (d Data) SetByPath(path string, value any) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
return d.SetByKeys(strings.Split(path, KeySepStr), value)
|
||||
}
|
||||
|
||||
// SetByKeys sets a value in the map by path keys.
|
||||
// Supports dot syntax to set deep values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// d.SetByKeys([]string{"name", "first"}, "Mat")
|
||||
func (d Data) SetByKeys(keys []string, value any) error {
|
||||
kln := len(keys)
|
||||
if kln == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// special handle d is empty.
|
||||
if len(d) == 0 {
|
||||
if kln == 1 {
|
||||
d.Set(keys[0], value)
|
||||
} else {
|
||||
d.Set(keys[0], MakeByKeys(keys[1:], value))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return SetByKeys((*map[string]any)(&d), keys, value)
|
||||
// It's ok, but use `func (d *Data)`
|
||||
// return SetByKeys((*map[string]any)(d), keys, value)
|
||||
}
|
||||
|
||||
//
|
||||
// endregion
|
||||
// region T: read value(s)
|
||||
//
|
||||
|
||||
// Value get from the data map
|
||||
func (d Data) Value(key string) (any, bool) {
|
||||
val, ok := d.GetByPath(key)
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Get value from the data map.
|
||||
// Supports dot syntax to get deep values. eg: top.sub
|
||||
func (d Data) Get(key string) any {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// One get value from the data by multi paths. will return first founded value
|
||||
func (d Data) One(keys ...string) any {
|
||||
if val, ok := d.TryOne(keys...); ok {
|
||||
return val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TryOne get value from the data by multi paths. will return first founded value
|
||||
func (d Data) TryOne(keys ...string) (any, bool) {
|
||||
for _, path := range keys {
|
||||
if val, ok := d.GetByPath(path); ok {
|
||||
return val, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetByPath get value from the data map by path. eg: top.sub
|
||||
// Supports dot syntax to get deep values.
|
||||
func (d Data) GetByPath(path string) (any, bool) {
|
||||
if val, ok := d[path]; ok {
|
||||
return val, true
|
||||
}
|
||||
|
||||
// is a key path.
|
||||
if strings.ContainsRune(path, '.') {
|
||||
val, ok := GetByPath(path, d)
|
||||
if ok {
|
||||
return val, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Default get value from the data map with default value
|
||||
func (d Data) Default(key string, def any) any {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return val
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// Int value get, or default value
|
||||
func (d Data) Int(key string, defVal ...int) int {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return mathutil.SafeInt(val)
|
||||
}
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Int64 value get, or default value
|
||||
func (d Data) Int64(key string, defVal ...int64) int64 {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return mathutil.SafeInt64(val)
|
||||
}
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Uint value get, or default value
|
||||
func (d Data) Uint(key string, defVal ...uint) uint {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return mathutil.QuietUint(val)
|
||||
}
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Uint16 value get, or default value
|
||||
func (d Data) Uint16(key string, defVal ...uint16) uint16 {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return uint16(mathutil.SafeUint(val))
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Uint64 value get, or default value
|
||||
func (d Data) Uint64(key string, defVal ...uint64) uint64 {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return mathutil.QuietUint64(val)
|
||||
}
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Str value gets by key, or default value
|
||||
func (d Data) Str(key string, defVal ...string) string {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return strutil.SafeString(val)
|
||||
}
|
||||
if len(defVal) > 0 {
|
||||
return defVal[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// StrOne value gets by multi keys, will return first value
|
||||
func (d Data) StrOne(keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return strutil.SafeString(val)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Bool value get
|
||||
func (d Data) Bool(key string) bool {
|
||||
val, ok := d.GetByPath(key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return comfunc.Bool(val)
|
||||
}
|
||||
|
||||
// BoolOne value gets from multi keys, return first value
|
||||
func (d Data) BoolOne(keys ...string) bool {
|
||||
for _, key := range keys {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return comfunc.Bool(val)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StringsOne get []string value by multi keys, return first founded value
|
||||
func (d Data) StringsOne(keys ...string) []string {
|
||||
for _, key := range keys {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return arrutil.AnyToStrings(val)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Strings get []string value by key
|
||||
func (d Data) Strings(key string) []string {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return arrutil.AnyToStrings(val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StrSplit get strings by split string value
|
||||
func (d Data) StrSplit(key, sep string) []string {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return strings.Split(strutil.SafeString(val), sep)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StringsByStr value gets by key, will split string value by ","
|
||||
func (d Data) StringsByStr(key string) []string {
|
||||
return d.StrSplit(key, ",")
|
||||
}
|
||||
|
||||
// StrMap get map[string]string value
|
||||
func (d Data) StrMap(key string) map[string]string {
|
||||
return d.StringMap(key)
|
||||
}
|
||||
|
||||
// StringMap get map[string]string value
|
||||
func (d Data) StringMap(key string) map[string]string {
|
||||
val, ok := d.GetByPath(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch tv := val.(type) {
|
||||
case map[string]string:
|
||||
return tv
|
||||
case map[string]any:
|
||||
return ToStringMap(tv)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Sub get sub value(map[string]any) as new Data
|
||||
func (d Data) Sub(key string) Data {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return d.toAnyMap(val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AnyMap get sub value as map[string]any
|
||||
func (d Data) AnyMap(key string) map[string]any {
|
||||
if val, ok := d.GetByPath(key); ok {
|
||||
return d.toAnyMap(val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AnyMap get sub value as map[string]any
|
||||
func (d Data) toAnyMap(val any) map[string]any {
|
||||
switch tv := val.(type) {
|
||||
case map[string]string:
|
||||
return ToAnyMap(tv)
|
||||
case map[string]any:
|
||||
return tv
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Slice get []any value from data map
|
||||
func (d Data) Slice(key string) ([]any, error) {
|
||||
val, ok := d.GetByPath(key)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return arrutil.AnyToSlice(val)
|
||||
}
|
||||
|
||||
// Keys of the data map
|
||||
func (d Data) Keys() []string {
|
||||
keys := make([]string, 0, len(d))
|
||||
for k := range d {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// ToStringMap convert to map[string]string
|
||||
func (d Data) ToStringMap() map[string]string {
|
||||
return ToStringMap(d)
|
||||
}
|
||||
|
||||
// String data to string
|
||||
func (d Data) String() string {
|
||||
return ToString(d)
|
||||
}
|
||||
|
||||
// Load other data to current data map
|
||||
func (d Data) Load(sub map[string]any) {
|
||||
for name, val := range sub {
|
||||
d[name] = val
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSMap to data
|
||||
func (d Data) LoadSMap(smp map[string]string) {
|
||||
for name, val := range smp {
|
||||
d[name] = val
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package maputil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// MapFormatter struct
|
||||
type MapFormatter struct {
|
||||
comdef.BaseFormatter
|
||||
// Prefix string for each element
|
||||
Prefix string
|
||||
// Indent string for each element
|
||||
Indent string
|
||||
// ClosePrefix string for last "}"
|
||||
ClosePrefix string
|
||||
// AfterReset after reset on call Format().
|
||||
// AfterReset bool
|
||||
}
|
||||
|
||||
// NewFormatter instance
|
||||
func NewFormatter(mp any) *MapFormatter {
|
||||
f := &MapFormatter{}
|
||||
f.Src = mp
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// WithFn for config self
|
||||
func (f *MapFormatter) WithFn(fn func(f *MapFormatter)) *MapFormatter {
|
||||
fn(f)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithIndent string
|
||||
func (f *MapFormatter) WithIndent(indent string) *MapFormatter {
|
||||
f.Indent = indent
|
||||
return f
|
||||
}
|
||||
|
||||
// FormatTo to custom buffer
|
||||
func (f *MapFormatter) FormatTo(w io.Writer) {
|
||||
f.SetOutput(w)
|
||||
f.doFormat()
|
||||
}
|
||||
|
||||
// Format to string
|
||||
func (f *MapFormatter) String() string {
|
||||
return f.Format()
|
||||
}
|
||||
|
||||
// Format to string
|
||||
func (f *MapFormatter) Format() string {
|
||||
f.doFormat()
|
||||
return f.BsWriter().String()
|
||||
}
|
||||
|
||||
// Format map data to string.
|
||||
//
|
||||
//goland:noinspection GoUnhandledErrorResult
|
||||
func (f *MapFormatter) doFormat() {
|
||||
if f.Src == nil {
|
||||
return
|
||||
}
|
||||
|
||||
rv, ok := f.Src.(reflect.Value)
|
||||
if !ok {
|
||||
rv = reflect.ValueOf(f.Src)
|
||||
}
|
||||
|
||||
rv = reflect.Indirect(rv)
|
||||
if rv.Kind() != reflect.Map {
|
||||
return
|
||||
}
|
||||
|
||||
buf := f.BsWriter()
|
||||
ln := rv.Len()
|
||||
if ln == 0 {
|
||||
buf.WriteString("{}")
|
||||
return
|
||||
}
|
||||
|
||||
// buf.Grow(ln * 16)
|
||||
buf.WriteByte('{')
|
||||
|
||||
indentLn := len(f.Indent)
|
||||
if indentLn > 0 {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
for i, key := range rv.MapKeys() {
|
||||
strK := strutil.SafeString(key.Interface())
|
||||
if indentLn > 0 {
|
||||
buf.WriteString(f.Indent)
|
||||
}
|
||||
|
||||
buf.WriteString(strK)
|
||||
buf.WriteByte(':')
|
||||
|
||||
strV := strutil.SafeString(rv.MapIndex(key).Interface())
|
||||
buf.WriteString(strV)
|
||||
if i < ln-1 {
|
||||
buf.WriteByte(',')
|
||||
|
||||
// no indent, with space
|
||||
if indentLn == 0 {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
|
||||
// with newline
|
||||
if indentLn > 0 {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
if f.ClosePrefix != "" {
|
||||
buf.WriteString(f.ClosePrefix)
|
||||
}
|
||||
|
||||
buf.WriteByte('}')
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
package maputil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/reflects"
|
||||
)
|
||||
|
||||
// some consts for separators
|
||||
const (
|
||||
Wildcard = "*"
|
||||
PathSep = "."
|
||||
)
|
||||
|
||||
// DeepGet value by key path. eg "top" "top.sub"
|
||||
func DeepGet(mp map[string]any, path string) (val any) {
|
||||
val, _ = GetByPath(path, mp)
|
||||
return
|
||||
}
|
||||
|
||||
// QuietGet value by key path. eg "top" "top.sub"
|
||||
func QuietGet(mp map[string]any, path string) (val any) {
|
||||
val, _ = GetByPath(path, mp)
|
||||
return
|
||||
}
|
||||
|
||||
// GetFromAny get value by key path from any(map,slice) data. eg "top" "top.sub"
|
||||
func GetFromAny(path string, data any) (val any, ok bool) {
|
||||
// empty data
|
||||
if data == nil {
|
||||
return nil, false
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return data, true
|
||||
}
|
||||
|
||||
return getByPathKeys(data, strings.Split(path, "."))
|
||||
}
|
||||
|
||||
// GetByPath get value by key path from a map(map[string]any). eg "top" "top.sub"
|
||||
func GetByPath(path string, mp map[string]any) (val any, ok bool) {
|
||||
if len(path) == 0 {
|
||||
return mp, true
|
||||
}
|
||||
if val, ok := mp[path]; ok {
|
||||
return val, true
|
||||
}
|
||||
|
||||
// no sub key
|
||||
if len(mp) == 0 || strings.IndexByte(path, '.') < 1 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// key is path. eg: "top.sub"
|
||||
return GetByPathKeys(mp, strings.Split(path, "."))
|
||||
}
|
||||
|
||||
// GetByPathKeys get value by path keys from a map(map[string]any). eg "top" "top.sub"
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mp := map[string]any{
|
||||
// "top": map[string]any{
|
||||
// "sub": "value",
|
||||
// },
|
||||
// }
|
||||
// val, ok := GetByPathKeys(mp, []string{"top", "sub"}) // return "value", true
|
||||
func GetByPathKeys(mp map[string]any, keys []string) (val any, ok bool) {
|
||||
kl := len(keys)
|
||||
if kl == 0 {
|
||||
return mp, true
|
||||
}
|
||||
|
||||
// find top item data use top key
|
||||
var item any
|
||||
topK := keys[0]
|
||||
if item, ok = mp[topK]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// find sub item data use sub key
|
||||
return getByPathKeys(item, keys[1:])
|
||||
}
|
||||
|
||||
func getByPathKeys(item any, keys []string) (val any, ok bool) {
|
||||
kl := len(keys)
|
||||
|
||||
for i, k := range keys {
|
||||
switch tData := item.(type) {
|
||||
case map[string]string: // is string map
|
||||
if item, ok = tData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case map[string]any: // is map(decode from toml/json/yaml)
|
||||
if item, ok = tData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case map[any]any: // is map(decode from yaml.v2)
|
||||
if item, ok = tData[k]; !ok {
|
||||
return
|
||||
}
|
||||
case []map[string]any: // is an any-map slice
|
||||
if k == Wildcard {
|
||||
if kl == i+1 { // * is last key
|
||||
return tData, true
|
||||
}
|
||||
|
||||
// * is not last key, find sub item data
|
||||
sl := make([]any, 0, len(tData))
|
||||
for _, v := range tData {
|
||||
if val, ok = getByPathKeys(v, keys[i+1:]); ok {
|
||||
sl = append(sl, val)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sl) > 0 {
|
||||
return sl, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// k is index number
|
||||
idx, err := strconv.Atoi(k)
|
||||
if err != nil || idx >= len(tData) {
|
||||
return nil, false
|
||||
}
|
||||
item = tData[idx]
|
||||
default:
|
||||
if k == Wildcard && kl == i+1 { // * is last key
|
||||
return tData, true
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(tData)
|
||||
// check is slice
|
||||
if rv.Kind() == reflect.Slice {
|
||||
if k == Wildcard {
|
||||
// * is not last key, find sub item data
|
||||
sl := make([]any, 0, rv.Len())
|
||||
for si := 0; si < rv.Len(); si++ {
|
||||
el := reflects.Indirect(rv.Index(si))
|
||||
if el.Kind() != reflect.Map {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// el is map value.
|
||||
if val, ok = getByPathKeys(el.Interface(), keys[i+1:]); ok {
|
||||
sl = append(sl, val)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sl) > 0 {
|
||||
return sl, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// check k is index number
|
||||
ii, err := strconv.Atoi(k)
|
||||
if err != nil || ii >= rv.Len() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
item = rv.Index(ii).Interface()
|
||||
continue
|
||||
}
|
||||
|
||||
// as error
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// next is last key and it is *
|
||||
if kl == i+2 && keys[i+1] == Wildcard {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
|
||||
return item, true
|
||||
}
|
||||
|
||||
// Keys get all keys of the given map.
|
||||
func Keys(mp any) (keys []string) {
|
||||
rftVal := reflect.Indirect(reflect.ValueOf(mp))
|
||||
if rftVal.Kind() != reflect.Map {
|
||||
return
|
||||
}
|
||||
|
||||
keys = make([]string, 0, rftVal.Len())
|
||||
for _, key := range rftVal.MapKeys() {
|
||||
keys = append(keys, key.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TypedKeys get all keys of the given typed map.
|
||||
func TypedKeys[K comdef.SimpleType, V any](mp map[K]V) (keys []K) {
|
||||
for key := range mp {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FirstKey returns the first key of the given map.
|
||||
func FirstKey[T any](mp map[string]T) string {
|
||||
for key := range mp {
|
||||
return key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Values get all values from the given map.
|
||||
func Values(mp any) (values []any) {
|
||||
rv := reflect.Indirect(reflect.ValueOf(mp))
|
||||
if rv.Kind() != reflect.Map {
|
||||
return
|
||||
}
|
||||
|
||||
values = make([]any, 0, rv.Len())
|
||||
for _, key := range rv.MapKeys() {
|
||||
values = append(values, rv.MapIndex(key).Interface())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TypedValues get all values from the given typed map.
|
||||
func TypedValues[K comdef.SimpleType, V any](mp map[K]V) (values []V) {
|
||||
for _, val := range mp {
|
||||
values = append(values, val)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EachAnyMap iterates the given map and calls the given function for each item.
|
||||
func EachAnyMap(mp any, fn func(key string, val any)) {
|
||||
rv := reflect.Indirect(reflect.ValueOf(mp))
|
||||
if rv.Kind() != reflect.Map {
|
||||
panic("not a map value")
|
||||
}
|
||||
|
||||
for _, key := range rv.MapKeys() {
|
||||
fn(key.String(), rv.MapIndex(key).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
// EachTypedMap iterates the given map and calls the given function for each item.
|
||||
func EachTypedMap[K comdef.SimpleType, V any](mp map[K]V, fn func(key K, val V)) {
|
||||
for key, val := range mp {
|
||||
fn(key, val)
|
||||
}
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
// Package maputil provide map data util functions. eg: convert, sub-value get, simple merge
|
||||
package maputil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/arrutil"
|
||||
)
|
||||
|
||||
// Key, value sep char consts
|
||||
const (
|
||||
ValSepStr = ","
|
||||
ValSepChar = ','
|
||||
KeySepStr = "."
|
||||
KeySepChar = '.'
|
||||
)
|
||||
|
||||
// Copy copies all key/value pairs in src adding them to dst.
|
||||
// When a key in src is already present in dst,
|
||||
// the value in dst will be overwritten by the value associated
|
||||
// with the key in src.
|
||||
func Copy[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2) {
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteFunc deletes any key/value pairs from m for which del returns true.
|
||||
func DeleteFunc[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool) {
|
||||
for k, v := range m {
|
||||
if del(k, v) {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SimpleMerge simple merge two data map by string key. will merge the src to dst map
|
||||
func SimpleMerge(src, dst map[string]any) map[string]any {
|
||||
if len(src) == 0 {
|
||||
return dst
|
||||
}
|
||||
if len(dst) == 0 {
|
||||
return src
|
||||
}
|
||||
|
||||
for key, val := range src {
|
||||
if mp, ok := val.(map[string]any); ok {
|
||||
if dmp, ok := dst[key].(map[string]any); ok {
|
||||
dst[key] = SimpleMerge(mp, dmp)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// simple merge
|
||||
dst[key] = val
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Merge1level merge multi any map[string]any data. only merge one level data.
|
||||
func Merge1level(mps ...map[string]any) map[string]any {
|
||||
newMp := make(map[string]any)
|
||||
for _, mp := range mps {
|
||||
for k, v := range mp {
|
||||
newMp[k] = v
|
||||
}
|
||||
}
|
||||
return newMp
|
||||
}
|
||||
|
||||
// func DeepMerge(src, dst map[string]any, deep int) map[string]any { TODO
|
||||
// }
|
||||
|
||||
// MergeSMap simple merge two string map. merge src to dst map
|
||||
func MergeSMap(src, dst map[string]string, ignoreCase bool) map[string]string {
|
||||
return MergeStringMap(src, dst, ignoreCase)
|
||||
}
|
||||
|
||||
// MergeStrMap simple merge two string map. merge src to dst map
|
||||
func MergeStrMap(src, dst map[string]string) map[string]string {
|
||||
return MergeStringMap(src, dst, false)
|
||||
}
|
||||
|
||||
// AppendSMap append string map data to dst map.
|
||||
func AppendSMap(dst, src map[string]string) map[string]string {
|
||||
return MergeStringMap(src, dst, false)
|
||||
}
|
||||
|
||||
// MergeStringMap simple merge two string map. merge src to dst map
|
||||
func MergeStringMap(src, dst map[string]string, ignoreCase bool) map[string]string {
|
||||
if len(src) == 0 {
|
||||
return dst
|
||||
}
|
||||
if len(dst) == 0 {
|
||||
return src
|
||||
}
|
||||
|
||||
for k, v := range src {
|
||||
if ignoreCase {
|
||||
k = strings.ToLower(k)
|
||||
}
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// MergeMultiSMap quick merge multi string-map data.
|
||||
func MergeMultiSMap(mps ...map[string]string) map[string]string {
|
||||
newMp := make(map[string]string)
|
||||
for _, mp := range mps {
|
||||
for k, v := range mp {
|
||||
newMp[k] = v
|
||||
}
|
||||
}
|
||||
return newMp
|
||||
}
|
||||
|
||||
// MergeL2StrMap merge multi level2 string-map data. The back map covers the front.
|
||||
func MergeL2StrMap(mps ...map[string]map[string]string) map[string]map[string]string {
|
||||
newMp := make(map[string]map[string]string)
|
||||
for _, mp := range mps {
|
||||
for k, v := range mp {
|
||||
// merge level 2 value
|
||||
if oldV, ok := newMp[k]; ok {
|
||||
for k1, v1 := range v {
|
||||
oldV[k1] = v1
|
||||
}
|
||||
newMp[k] = oldV
|
||||
} else {
|
||||
newMp[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return newMp
|
||||
}
|
||||
|
||||
// FilterSMap filter empty elem for the string map.
|
||||
func FilterSMap(sm map[string]string) map[string]string {
|
||||
for key, val := range sm {
|
||||
if val == "" {
|
||||
delete(sm, key)
|
||||
}
|
||||
}
|
||||
return sm
|
||||
}
|
||||
|
||||
// MakeByPath build new value by key names
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// "site.info"
|
||||
// ->
|
||||
// map[string]any {
|
||||
// site: {info: val}
|
||||
// }
|
||||
//
|
||||
// // case 2, last key is slice:
|
||||
// "site.tags[1]"
|
||||
// ->
|
||||
// map[string]any {
|
||||
// site: {tags: [val]}
|
||||
// }
|
||||
func MakeByPath(path string, val any) (mp map[string]any) {
|
||||
return MakeByKeys(strings.Split(path, KeySepStr), val)
|
||||
}
|
||||
|
||||
// MakeByKeys build new value by key names
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // case 1:
|
||||
// []string{"site", "info"}
|
||||
// ->
|
||||
// map[string]any {
|
||||
// site: {info: val}
|
||||
// }
|
||||
//
|
||||
// // case 2, last key is slice:
|
||||
// []string{"site", "tags[1]"}
|
||||
// ->
|
||||
// map[string]any {
|
||||
// site: {tags: [val]}
|
||||
// }
|
||||
func MakeByKeys(keys []string, val any) (mp map[string]any) {
|
||||
size := len(keys)
|
||||
|
||||
// if last key contains slice index, make slice wrap the val
|
||||
lastKey := keys[size-1]
|
||||
if newK, idx, ok := parseArrKeyIndex(lastKey); ok {
|
||||
// valTyp := reflect.TypeOf(val)
|
||||
sliTyp := reflect.SliceOf(reflect.TypeOf(val))
|
||||
sliVal := reflect.MakeSlice(sliTyp, idx+1, idx+1)
|
||||
sliVal.Index(idx).Set(reflect.ValueOf(val))
|
||||
|
||||
// update val and last key
|
||||
val = sliVal.Interface()
|
||||
keys[size-1] = newK
|
||||
}
|
||||
|
||||
if size == 1 {
|
||||
return map[string]any{keys[0]: val}
|
||||
}
|
||||
|
||||
// multi nodes
|
||||
arrutil.Reverse(keys)
|
||||
for _, p := range keys {
|
||||
if mp == nil {
|
||||
mp = map[string]any{p: val}
|
||||
} else {
|
||||
mp = map[string]any{p: mp}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
package maputil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// SetByPath set sub-map value by key path.
|
||||
// Supports dot syntax to set deep values.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// SetByPath("name.first", "Mat")
|
||||
func SetByPath(mp *map[string]any, path string, val any) error {
|
||||
return SetByKeys(mp, strings.Split(path, KeySepStr), val)
|
||||
}
|
||||
|
||||
// SetByKeys set sub-map value by path keys.
|
||||
// Supports dot syntax to set deep values.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// SetByKeys([]string{"name", "first"}, "Mat")
|
||||
func SetByKeys(mp *map[string]any, keys []string, val any) (err error) {
|
||||
kln := len(keys)
|
||||
if kln == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mpv := *mp
|
||||
if len(mpv) == 0 {
|
||||
*mp = MakeByKeys(keys, val)
|
||||
return nil
|
||||
}
|
||||
|
||||
topK := keys[0]
|
||||
if kln == 1 {
|
||||
mpv[topK] = val
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, ok := mpv[topK]; !ok {
|
||||
mpv[topK] = MakeByKeys(keys[1:], val)
|
||||
return nil
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(mp).Elem()
|
||||
return setMapByKeys(rv, keys, reflect.ValueOf(val))
|
||||
}
|
||||
|
||||
func setMapByKeys(rv reflect.Value, keys []string, nv reflect.Value) (err error) {
|
||||
if rv.Kind() != reflect.Map {
|
||||
return fmt.Errorf("input parameter#rv must be a Map, but was %s", rv.Kind())
|
||||
}
|
||||
|
||||
// If the map is nil, make a new map
|
||||
if rv.IsNil() {
|
||||
mapType := reflect.MapOf(rv.Type().Key(), rv.Type().Elem())
|
||||
rv.Set(reflect.MakeMap(mapType))
|
||||
}
|
||||
|
||||
var ok bool
|
||||
maxI := len(keys) - 1
|
||||
for i, key := range keys {
|
||||
idx := -1
|
||||
isMap := rv.Kind() == reflect.Map
|
||||
isSlice := rv.Kind() == reflect.Slice
|
||||
isLast := i == len(keys)-1
|
||||
|
||||
// slice index key must be ended on the keys.
|
||||
// eg: "top.arr[2]" -> "arr[2]"
|
||||
if pos := strings.IndexRune(key, '['); pos > 0 {
|
||||
var realKey string
|
||||
if realKey, idx, ok = parseArrKeyIndex(key); ok {
|
||||
// update value
|
||||
key = realKey
|
||||
if !isMap {
|
||||
err = fmt.Errorf(
|
||||
"current value#%s type is %s, cannot get sub-value by key: %s",
|
||||
strings.Join(keys[i:], "."),
|
||||
rv.Kind(),
|
||||
key,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
rftK := reflect.ValueOf(key)
|
||||
tmpV := rv.MapIndex(rftK)
|
||||
if !tmpV.IsValid() {
|
||||
if isLast {
|
||||
sliVal := reflect.MakeSlice(reflect.SliceOf(nv.Type()), idx+1, idx+1)
|
||||
sliVal.Index(idx).Set(nv)
|
||||
rv.SetMapIndex(rftK, sliVal)
|
||||
} else {
|
||||
// deep make map by keys
|
||||
newVal := MakeByKeys(keys[i+1:], nv.Interface())
|
||||
mpVal := reflect.ValueOf(newVal)
|
||||
|
||||
sliVal := reflect.MakeSlice(reflect.SliceOf(mpVal.Type()), idx+1, idx+1)
|
||||
sliVal.Index(idx).Set(mpVal)
|
||||
|
||||
rv.SetMapIndex(rftK, sliVal)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// get real type: any -> map
|
||||
if tmpV.Kind() == reflect.Interface {
|
||||
tmpV = tmpV.Elem()
|
||||
}
|
||||
|
||||
if tmpV.Kind() != reflect.Slice {
|
||||
err = fmt.Errorf(
|
||||
"current value#%s type is %s, cannot set sub by index: %d",
|
||||
strings.Join(keys[i:], "."),
|
||||
tmpV.Kind(),
|
||||
idx,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
wantLen := idx + 1
|
||||
sliLen := tmpV.Len()
|
||||
elemTyp := tmpV.Type().Elem()
|
||||
|
||||
if wantLen > sliLen {
|
||||
newAdd := reflect.MakeSlice(tmpV.Type(), 0, wantLen-sliLen)
|
||||
for i := 0; i < wantLen-sliLen; i++ {
|
||||
newAdd = reflect.Append(newAdd, reflect.New(elemTyp).Elem())
|
||||
}
|
||||
|
||||
tmpV = reflect.AppendSlice(tmpV, newAdd)
|
||||
}
|
||||
|
||||
if !isLast {
|
||||
if elemTyp.Kind() == reflect.Map {
|
||||
err := setMapByKeys(tmpV.Index(idx), keys[i+1:], nv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// tmpV.Index(idx).Set(elemV)
|
||||
rv.SetMapIndex(rftK, tmpV)
|
||||
} else {
|
||||
err = fmt.Errorf(
|
||||
"key %s[%d] elem must be map for set sub-value by remain path: %s",
|
||||
key,
|
||||
idx,
|
||||
strings.Join(keys[i:], "."),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// last - set value
|
||||
tmpV.Index(idx).Set(nv)
|
||||
rv.SetMapIndex(rftK, tmpV)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// set value on last key
|
||||
if isLast {
|
||||
if isMap {
|
||||
rv.SetMapIndex(reflect.ValueOf(key), nv)
|
||||
break
|
||||
}
|
||||
|
||||
if isSlice {
|
||||
// key is slice index
|
||||
if strutil.IsInt(key) {
|
||||
idx, _ = strconv.Atoi(key)
|
||||
}
|
||||
|
||||
if idx > -1 {
|
||||
wantLen := idx + 1
|
||||
sliLen := rv.Len()
|
||||
|
||||
if wantLen > sliLen {
|
||||
elemTyp := rv.Type().Elem()
|
||||
newAdd := reflect.MakeSlice(rv.Type(), 0, wantLen-sliLen)
|
||||
|
||||
for i := 0; i < wantLen-sliLen; i++ {
|
||||
newAdd = reflect.Append(newAdd, reflect.New(elemTyp).Elem())
|
||||
}
|
||||
|
||||
if !rv.CanAddr() {
|
||||
err = fmt.Errorf("cannot set value to a cannot addr slice, key: %s", key)
|
||||
break
|
||||
}
|
||||
|
||||
rv.Set(reflect.AppendSlice(rv, newAdd))
|
||||
}
|
||||
|
||||
rv.Index(idx).Set(nv)
|
||||
} else {
|
||||
err = fmt.Errorf("cannot set slice value by named key %q", key)
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf(
|
||||
"cannot set sub-value for type %q(path %q, key %q)",
|
||||
rv.Kind(),
|
||||
strings.Join(keys[:i], "."),
|
||||
key,
|
||||
)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if isMap {
|
||||
rftK := reflect.ValueOf(key)
|
||||
if tmpV := rv.MapIndex(rftK); tmpV.IsValid() {
|
||||
var isPtr bool
|
||||
// get real type: any -> map
|
||||
tmpV, isPtr = getRealVal(tmpV)
|
||||
if tmpV.Kind() == reflect.Map {
|
||||
rv = tmpV
|
||||
continue
|
||||
}
|
||||
|
||||
// sub is slice and is not ptr
|
||||
if tmpV.Kind() == reflect.Slice {
|
||||
if isPtr {
|
||||
rv = tmpV
|
||||
continue // to (E)
|
||||
}
|
||||
|
||||
// next key is index number.
|
||||
nxtKey := keys[i+1]
|
||||
if strutil.IsInt(nxtKey) {
|
||||
idx, _ = strconv.Atoi(nxtKey)
|
||||
sliLen := tmpV.Len()
|
||||
wantLen := idx + 1
|
||||
|
||||
if wantLen > sliLen {
|
||||
elemTyp := tmpV.Type().Elem()
|
||||
newAdd := reflect.MakeSlice(tmpV.Type(), 0, wantLen-sliLen)
|
||||
for i := 0; i < wantLen-sliLen; i++ {
|
||||
newAdd = reflect.Append(newAdd, reflect.New(elemTyp).Elem())
|
||||
}
|
||||
|
||||
tmpV = reflect.AppendSlice(tmpV, newAdd)
|
||||
}
|
||||
|
||||
// rv = tmpV.Index(idx) // TODO
|
||||
if i+1 == maxI {
|
||||
tmpV.Index(idx).Set(nv)
|
||||
} else {
|
||||
err := setMapByKeys(tmpV.Index(idx), keys[i+1:], nv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
rv.SetMapIndex(rftK, tmpV)
|
||||
} else {
|
||||
err = fmt.Errorf("cannot set slice value by named key %s(parent: %s)", nxtKey, key)
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf(
|
||||
"map item type is %s(path:%q), cannot set sub-value by path %q",
|
||||
tmpV.Kind(),
|
||||
strings.Join(keys[0:i+1], "."),
|
||||
strings.Join(keys[i+1:], "."),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// deep make map by keys
|
||||
newVal := MakeByKeys(keys[i+1:], nv.Interface())
|
||||
rv.SetMapIndex(rftK, reflect.ValueOf(newVal))
|
||||
}
|
||||
|
||||
break
|
||||
} else if isSlice && strutil.IsInt(key) { // (E). slice from ptr slice
|
||||
idx, _ = strconv.Atoi(key)
|
||||
sliLen := rv.Len()
|
||||
wantLen := idx + 1
|
||||
|
||||
if wantLen > sliLen {
|
||||
elemTyp := rv.Type().Elem()
|
||||
newAdd := reflect.MakeSlice(rv.Type(), 0, wantLen-sliLen)
|
||||
for i := 0; i < wantLen-sliLen; i++ {
|
||||
newAdd = reflect.Append(newAdd, reflect.New(elemTyp).Elem())
|
||||
}
|
||||
|
||||
rv = reflect.AppendSlice(rv, newAdd)
|
||||
}
|
||||
|
||||
rv = rv.Index(idx)
|
||||
} else {
|
||||
err = fmt.Errorf(
|
||||
"map item type is %s, cannot set sub-value by path %q",
|
||||
rv.Kind(),
|
||||
strings.Join(keys[i:], "."),
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getRealVal(rv reflect.Value) (reflect.Value, bool) {
|
||||
// get real type: any -> map
|
||||
if rv.Kind() == reflect.Interface {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
isPtr := false
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
isPtr = true
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
return rv, isPtr
|
||||
}
|
||||
|
||||
// "arr[2]" => "arr", 2, true
|
||||
func parseArrKeyIndex(key string) (string, int, bool) {
|
||||
pos := strings.IndexRune(key, '[')
|
||||
if pos < 1 || !strings.HasSuffix(key, "]") {
|
||||
return key, 0, false
|
||||
}
|
||||
|
||||
var idx int
|
||||
var err error
|
||||
|
||||
idxStr := key[pos+1 : len(key)-1]
|
||||
if idxStr != "" {
|
||||
idx, err = strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
return key, 0, false
|
||||
}
|
||||
}
|
||||
|
||||
key = key[:pos]
|
||||
return key, idx, true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user