Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+20
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,559 @@
# Config
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/config?style=flat-square)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1e0f0ca096d94ffdab375234ec4167ee)](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings)
[![Build Status](https://travis-ci.org/gookit/config.svg?branch=master)](https://travis-ci.org/gookit/config)
[![Actions Status](https://github.com/gookit/config/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/config/actions)
[![Coverage Status](https://coveralls.io/repos/github/gookit/config/badge.svg?branch=master)](https://coveralls.io/github/gookit/config?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/gookit/config)](https://goreportcard.com/report/github.com/gookit/config)
[![Go Reference](https://pkg.go.dev/badge/github.com/gookit/config/v2.svg)](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
View File
@@ -0,0 +1,545 @@
# Config
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/config?style=flat-square)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1e0f0ca096d94ffdab375234ec4167ee)](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings)
[![Build Status](https://travis-ci.org/gookit/config.svg?branch=master)](https://travis-ci.org/gookit/config)
[![Actions Status](https://github.com/gookit/config/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/config/actions)
[![Coverage Status](https://coveralls.io/repos/github/gookit/config/badge.svg?branch=master)](https://coveralls.io/github/gookit/config?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/gookit/config)](https://goreportcard.com/report/github.com/gookit/config)
[![Go Reference](https://pkg.go.dev/badge/github.com/gookit/config/v2.svg)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 = &copyConf
// 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+85
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,6 @@
//go:build !windows
package comdef
// Newline string for non-windows
const Newline = "\n"
+4
View File
@@ -0,0 +1,4 @@
package comdef
// Newline string for windows
const Newline = "\r\n"
+38
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,3 @@
# Common func for internal use
- don't depend on other external packages
+93
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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