commit
81765d02c4
@ -0,0 +1,21 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
*/logs/
|
||||
logs/
|
||||
*.log
|
@ -0,0 +1,24 @@
|
||||
module git.noahlan.cn/noahlan/ntool
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/gofrs/uuid/v5 v5.0.0
|
||||
github.com/gookit/color v1.5.3
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
go.opentelemetry.io/otel v1.16.0
|
||||
go.opentelemetry.io/otel/sdk v1.16.0
|
||||
go.opentelemetry.io/otel/trace v1.16.0
|
||||
golang.org/x/crypto v0.10.0
|
||||
golang.org/x/sys v0.9.0
|
||||
golang.org/x/term v0.9.0
|
||||
golang.org/x/text v0.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/otel/metric v1.16.0 // indirect
|
||||
)
|
@ -0,0 +1,40 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE=
|
||||
github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
||||
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
|
||||
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
|
||||
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
|
||||
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
|
||||
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
|
||||
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
@ -0,0 +1,70 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExecCmd a 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
|
||||
}
|
||||
|
||||
// curShell cache
|
||||
var curShell string
|
||||
|
||||
// CurrentShell get current used shell env file.
|
||||
//
|
||||
// eg "/bin/zsh" "/bin/bash".
|
||||
// if onlyName=true, will return "zsh", "bash"
|
||||
func CurrentShell(onlyName bool) (binPath string) {
|
||||
var err error
|
||||
if curShell == "" {
|
||||
binPath = os.Getenv("SHELL")
|
||||
if len(binPath) == 0 {
|
||||
binPath, err = ShellExec("echo $SHELL")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
binPath = strings.TrimSpace(binPath)
|
||||
// cache result
|
||||
curShell = binPath
|
||||
} else {
|
||||
binPath = curShell
|
||||
}
|
||||
|
||||
if onlyName && len(binPath) > 0 {
|
||||
binPath = filepath.Base(binPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
//go:build !windows
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
cmd := exec.Command(shell, "-c", cmdLine)
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
//go:build windows
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// ShellExec exec command by shell
|
||||
// cmdLine e.g. "ls -al"
|
||||
func ShellExec(cmdLine string, shells ...string) (string, error) {
|
||||
// shell := "/bin/sh"
|
||||
shell := "cmd"
|
||||
if len(shells) > 0 {
|
||||
shell = shells[0]
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
cmd := exec.Command(shell, "/c", cmdLine)
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"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
|
||||
}
|
||||
|
||||
// parse env value, allow:
|
||||
//
|
||||
// only key - "${SHELL}"
|
||||
// with default - "${NotExist | defValue}"
|
||||
// multi key - "${GOPATH}/${APP_ENV | prod}/dir"
|
||||
//
|
||||
// Notice:
|
||||
//
|
||||
// must add "?" - To ensure that there is no greedy match
|
||||
// var envRegex = regexp.MustCompile(`\${[\w-| ]+}`)
|
||||
var envRegex = regexp.MustCompile(`\${.+?}`)
|
||||
|
||||
// ParseEnvVar parse ENV var value from input string, support default value.
|
||||
//
|
||||
// Format:
|
||||
//
|
||||
// ${var_name} Only var name
|
||||
// ${var_name | default} With default value
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// comfunc.ParseEnvVar("${ APP_NAME }")
|
||||
// comfunc.ParseEnvVar("${ APP_ENV | dev }")
|
||||
func ParseEnvVar(val string, getFn func(string) string) (newVal string) {
|
||||
if !strings.Contains(val, "${") {
|
||||
return val
|
||||
}
|
||||
|
||||
// default use os.Getenv
|
||||
if getFn == nil {
|
||||
getFn = os.Getenv
|
||||
}
|
||||
|
||||
var name, def string
|
||||
return envRegex.ReplaceAllStringFunc(val, func(eVar string) string {
|
||||
// eVar like "${NotExist|defValue}", first remove "${" and "}", then split it
|
||||
ss := strings.SplitN(eVar[2:len(eVar)-1], "|", 2)
|
||||
|
||||
// with default value. ${NotExist|defValue}
|
||||
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
|
||||
eVal := getFn(name)
|
||||
if eVal == "" {
|
||||
eVal = def
|
||||
}
|
||||
return eVal
|
||||
})
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package common
|
||||
|
||||
import "os"
|
||||
|
||||
// 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:]
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.noahlan.cn/noahlan/ntool/ndef"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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, ndef.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)
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package narr
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/ndef"
|
||||
"git.noahlan.cn/noahlan/ntool/nmath"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NotIn check the given value whether not in the list
|
||||
func NotIn[T ndef.ScalarType](list []T, value T) bool {
|
||||
return !In(list, value)
|
||||
}
|
||||
|
||||
// In check the given value whether in the list
|
||||
func In[T ndef.ScalarType](list []T, value 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 ndef.ScalarType](list, values []T) bool {
|
||||
return IsSubList(values, list)
|
||||
}
|
||||
|
||||
// IsSubList check given values is sub-list of sample list.
|
||||
func IsSubList[T ndef.ScalarType](values, list []T) bool {
|
||||
for _, value := range values {
|
||||
if !In(list, value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsParent check given values is parent-list of samples.
|
||||
func IsParent[T ndef.ScalarType](values, list []T) bool {
|
||||
return IsSubList(list, values)
|
||||
}
|
||||
|
||||
// StringsHas check the []string contains the given element
|
||||
func StringsHas(ss []string, val string) bool {
|
||||
return In(ss, val)
|
||||
}
|
||||
|
||||
// IntsHas check the []int contains the given value
|
||||
func IntsHas(ints []int, val int) bool {
|
||||
return In(ints, val)
|
||||
}
|
||||
|
||||
// Int64sHas check the []int64 contains the given value
|
||||
func Int64sHas(ints []int64, val int64) bool {
|
||||
return In(ints, val)
|
||||
}
|
||||
|
||||
// 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 := nmath.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)
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package narr_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/narr"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIntsHas(t *testing.T) {
|
||||
ints := []int{2, 4, 5}
|
||||
assert.True(t, narr.IntsHas(ints, 2))
|
||||
assert.True(t, narr.IntsHas(ints, 5))
|
||||
assert.False(t, narr.IntsHas(ints, 3))
|
||||
}
|
||||
|
||||
func TestInt64sHas(t *testing.T) {
|
||||
ints := []int64{2, 4, 5}
|
||||
assert.True(t, narr.Int64sHas(ints, 2))
|
||||
assert.True(t, narr.Int64sHas(ints, 5))
|
||||
assert.False(t, narr.Int64sHas(ints, 3))
|
||||
}
|
||||
|
||||
func TestStringsHas(t *testing.T) {
|
||||
ss := []string{"a", "b"}
|
||||
assert.True(t, narr.StringsHas(ss, "a"))
|
||||
assert.True(t, narr.StringsHas(ss, "b"))
|
||||
|
||||
assert.False(t, narr.StringsHas(ss, "c"))
|
||||
}
|
||||
|
||||
func TestInAndNotIn(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
arr := []int{1, 2, 3}
|
||||
is.True(narr.In(arr, 2))
|
||||
is.False(narr.NotIn(arr, 2))
|
||||
|
||||
arr1 := []rune{'a', 'b'}
|
||||
is.True(narr.In(arr1, 'b'))
|
||||
is.False(narr.NotIn(arr1, 'b'))
|
||||
|
||||
arr2 := []string{"a", "b", "c"}
|
||||
is.True(narr.In(arr2, "b"))
|
||||
is.False(narr.NotIn(arr2, "b"))
|
||||
}
|
||||
|
||||
func TestContainsAll(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
arr := []int{1, 2, 3}
|
||||
is.True(narr.ContainsAll(arr, []int{2}))
|
||||
is.False(narr.ContainsAll(arr, []int{2, 45}))
|
||||
is.True(narr.IsParent(arr, []int{2}))
|
||||
|
||||
arr2 := []string{"a", "b", "c"}
|
||||
is.True(narr.ContainsAll(arr2, []string{"b"}))
|
||||
is.False(narr.ContainsAll(arr2, []string{"b", "e"}))
|
||||
is.True(narr.IsParent(arr2, []string{"b"}))
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
tests := map[any]any{
|
||||
1: []int{1, 2, 3},
|
||||
2: []int8{1, 2, 3},
|
||||
3: []int16{1, 2, 3},
|
||||
4: []int32{4, 2, 3},
|
||||
5: []int64{5, 2, 3},
|
||||
6: []uint{6, 2, 3},
|
||||
7: []uint8{7, 2, 3},
|
||||
8: []uint16{8, 2, 3},
|
||||
9: []uint32{9, 2, 3},
|
||||
10: []uint64{10, 3},
|
||||
11: []string{"11", "3"},
|
||||
'a': []int64{97},
|
||||
'b': []rune{'a', 'b'},
|
||||
'c': []byte{'a', 'b', 'c'}, // byte -> uint8
|
||||
"a": []string{"a", "b", "c"},
|
||||
12: [5]uint{12, 1, 2, 3, 4},
|
||||
'A': [3]rune{'A', 'B', 'C'},
|
||||
'd': [4]byte{'a', 'b', 'c', 'd'},
|
||||
"aa": [3]string{"aa", "bb", "cc"},
|
||||
}
|
||||
|
||||
for val, list := range tests {
|
||||
is.True(narr.Contains(list, val))
|
||||
is.False(narr.NotContains(list, val))
|
||||
}
|
||||
|
||||
is.False(narr.Contains(nil, []int{}))
|
||||
is.False(narr.Contains('a', []int{}))
|
||||
//
|
||||
is.False(narr.Contains([]int{2, 3}, []int{2}))
|
||||
is.False(narr.Contains([]int{2, 3}, "a"))
|
||||
is.False(narr.Contains([]string{"a", "b"}, 12))
|
||||
is.False(narr.Contains(nil, 12))
|
||||
is.False(narr.Contains(map[int]int{2: 3}, 12))
|
||||
|
||||
tests1 := map[any]any{
|
||||
2: []int{1, 3},
|
||||
"a": []string{"b", "c"},
|
||||
}
|
||||
|
||||
for val, list := range tests1 {
|
||||
is.True(narr.NotContains(list, val))
|
||||
is.False(narr.Contains(list, val))
|
||||
}
|
||||
}
|
@ -0,0 +1,427 @@
|
||||
package narr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// 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 func(a, b any) int
|
||||
|
||||
// Predicate Function to predicate a struct/value satisfies a condition.
|
||||
type Predicate func(a any) bool
|
||||
|
||||
var (
|
||||
// StringEqualsComparer Comparer for string. It will compare the string by their value.
|
||||
// returns: 0 if equal, -1 if a != b
|
||||
StringEqualsComparer Comparer = func(a, b any) int {
|
||||
typeOfA := reflect.TypeOf(a)
|
||||
if typeOfA.Kind() == reflect.Ptr {
|
||||
typeOfA = typeOfA.Elem()
|
||||
}
|
||||
|
||||
typeOfB := reflect.TypeOf(b)
|
||||
if typeOfB.Kind() == reflect.Ptr {
|
||||
typeOfB = typeOfB.Elem()
|
||||
}
|
||||
|
||||
if typeOfA != typeOfB {
|
||||
return -1
|
||||
}
|
||||
|
||||
strA := ""
|
||||
strB := ""
|
||||
|
||||
if val, ok := a.(string); ok {
|
||||
strA = val
|
||||
} else if val, ok := a.(*string); ok {
|
||||
strA = *val
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
if val, ok := b.(string); ok {
|
||||
strB = val
|
||||
} else if val, ok := b.(*string); ok {
|
||||
strB = *val
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
if strA == strB {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ReferenceEqualsComparer Comparer for strcut ptr. It will compare the struct by their ptr addr.
|
||||
// returns: 0 if equal, -1 if a != b
|
||||
ReferenceEqualsComparer Comparer = func(a, b any) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ElemTypeEqualsComparer Comparer for struct/value. It will compare the struct by their element type (reflect.Type.Elem()).
|
||||
// returns: 0 if same type, -1 if not.
|
||||
ElemTypeEqualsComparer Comparer = func(a, b any) int {
|
||||
at := reflect.TypeOf(a)
|
||||
bt := reflect.TypeOf(b)
|
||||
if at.Kind() == reflect.Ptr {
|
||||
at = at.Elem()
|
||||
}
|
||||
|
||||
if bt.Kind() == reflect.Ptr {
|
||||
bt = bt.Elem()
|
||||
}
|
||||
|
||||
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(data any, item any, fn Comparer) (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")
|
||||
}
|
||||
|
||||
dataType := reflect.TypeOf(data)
|
||||
if dataType.Kind() != reflect.Slice {
|
||||
return -1, errors.New("collections.TwowaySearch: data is not a slice")
|
||||
}
|
||||
|
||||
dataVal := reflect.ValueOf(data)
|
||||
if dataVal.Len() == 0 {
|
||||
return -1, errors.New("collections.TwowaySearch: data is empty")
|
||||
}
|
||||
itemType := dataType.Elem()
|
||||
if itemType.Kind() == reflect.Ptr {
|
||||
itemType = itemType.Elem()
|
||||
}
|
||||
|
||||
if itemType != dataVal.Index(0).Type() {
|
||||
return -1, errors.New("collections.TwowaySearch: item type is not the same as data type")
|
||||
}
|
||||
|
||||
forward := 0
|
||||
backward := dataVal.Len() - 1
|
||||
|
||||
for forward <= backward {
|
||||
forwardVal := dataVal.Index(forward).Interface()
|
||||
if fn(forwardVal, item) == 0 {
|
||||
return forward, nil
|
||||
}
|
||||
|
||||
backwardVal := dataVal.Index(backward).Interface()
|
||||
if fn(backwardVal, item) == 0 {
|
||||
return backward, nil
|
||||
}
|
||||
|
||||
forward++
|
||||
backward--
|
||||
}
|
||||
|
||||
return -1, ErrElementNotFound
|
||||
}
|
||||
|
||||
// MakeEmptySlice Create a new slice with the elements of the source that satisfy the predicate.
|
||||
//
|
||||
// itemType: the type of the elements in the source.
|
||||
// returns: the new slice.
|
||||
func MakeEmptySlice(itemType reflect.Type) any {
|
||||
ret := reflect.MakeSlice(reflect.SliceOf(itemType), 0, 0).Interface()
|
||||
return ret
|
||||
}
|
||||
|
||||
// CloneSlice Clone a slice.
|
||||
//
|
||||
// data: the slice to clone.
|
||||
// returns: the cloned slice.
|
||||
func CloneSlice(data any) any {
|
||||
typeOfData := reflect.TypeOf(data)
|
||||
if typeOfData.Kind() != reflect.Slice {
|
||||
panic("collections.CloneSlice: data must be a slice")
|
||||
}
|
||||
return reflect.AppendSlice(reflect.New(reflect.SliceOf(typeOfData.Elem())).Elem(), reflect.ValueOf(data)).Interface()
|
||||
}
|
||||
|
||||
// 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.
|
||||
func Differences[T any](first, second []T, fn Comparer) []T {
|
||||
typeOfFirst := reflect.TypeOf(first)
|
||||
if typeOfFirst.Kind() != reflect.Slice {
|
||||
panic("collections.Excepts: first must be a slice")
|
||||
}
|
||||
|
||||
typeOfSecond := reflect.TypeOf(second)
|
||||
if typeOfSecond.Kind() != reflect.Slice {
|
||||
panic("collections.Excepts: second must be a slice")
|
||||
}
|
||||
|
||||
firstLen := len(first)
|
||||
if firstLen == 0 {
|
||||
return CloneSlice(second).([]T)
|
||||
}
|
||||
|
||||
secondLen := len(second)
|
||||
if secondLen == 0 {
|
||||
return CloneSlice(first).([]T)
|
||||
}
|
||||
|
||||
max := firstLen
|
||||
if secondLen > firstLen {
|
||||
max = secondLen
|
||||
}
|
||||
|
||||
result := make([]T, 0)
|
||||
for i := 0; i < max; 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.
|
||||
func Excepts(first, second any, fn Comparer) any {
|
||||
typeOfFirst := reflect.TypeOf(first)
|
||||
if typeOfFirst.Kind() != reflect.Slice {
|
||||
panic("collections.Excepts: first must be a slice")
|
||||
}
|
||||
valOfFirst := reflect.ValueOf(first)
|
||||
if valOfFirst.Len() == 0 {
|
||||
return MakeEmptySlice(typeOfFirst.Elem())
|
||||
}
|
||||
|
||||
typeOfSecond := reflect.TypeOf(second)
|
||||
if typeOfSecond.Kind() != reflect.Slice {
|
||||
panic("collections.Excepts: second must be a slice")
|
||||
}
|
||||
|
||||
valOfSecond := reflect.ValueOf(second)
|
||||
if valOfSecond.Len() == 0 {
|
||||
return CloneSlice(first)
|
||||
}
|
||||
|
||||
result := reflect.New(reflect.SliceOf(typeOfFirst.Elem())).Elem()
|
||||
for i := 0; i < valOfFirst.Len(); i++ {
|
||||
s := valOfFirst.Index(i).Interface()
|
||||
if i, _ := TwoWaySearch(second, s, fn); i < 0 {
|
||||
result = reflect.Append(result, reflect.ValueOf(s))
|
||||
}
|
||||
}
|
||||
|
||||
return result.Interface()
|
||||
}
|
||||
|
||||
// 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.
|
||||
func Intersects(first any, second any, fn Comparer) any {
|
||||
typeOfFirst := reflect.TypeOf(first)
|
||||
if typeOfFirst.Kind() != reflect.Slice {
|
||||
panic("collections.Intersects: first must be a slice")
|
||||
}
|
||||
valOfFirst := reflect.ValueOf(first)
|
||||
if valOfFirst.Len() == 0 {
|
||||
return MakeEmptySlice(typeOfFirst.Elem())
|
||||
}
|
||||
|
||||
typeOfSecond := reflect.TypeOf(second)
|
||||
if typeOfSecond.Kind() != reflect.Slice {
|
||||
panic("collections.Intersects: second must be a slice")
|
||||
}
|
||||
|
||||
valOfSecond := reflect.ValueOf(second)
|
||||
if valOfSecond.Len() == 0 {
|
||||
return MakeEmptySlice(typeOfFirst.Elem())
|
||||
}
|
||||
|
||||
result := reflect.New(reflect.SliceOf(typeOfFirst.Elem())).Elem()
|
||||
for i := 0; i < valOfFirst.Len(); i++ {
|
||||
s := valOfFirst.Index(i).Interface()
|
||||
if i, _ := TwoWaySearch(second, s, fn); i >= 0 {
|
||||
result = reflect.Append(result, reflect.ValueOf(s))
|
||||
}
|
||||
}
|
||||
|
||||
return result.Interface()
|
||||
}
|
||||
|
||||
// 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.
|
||||
func Union(first, second any, fn Comparer) any {
|
||||
excepts := Excepts(second, first, fn)
|
||||
|
||||
typeOfFirst := reflect.TypeOf(first)
|
||||
if typeOfFirst.Kind() != reflect.Slice {
|
||||
panic("collections.Intersects: first must be a slice")
|
||||
}
|
||||
valOfFirst := reflect.ValueOf(first)
|
||||
if valOfFirst.Len() == 0 {
|
||||
return CloneSlice(second)
|
||||
}
|
||||
|
||||
result := reflect.AppendSlice(reflect.New(reflect.SliceOf(typeOfFirst.Elem())).Elem(), valOfFirst)
|
||||
result = reflect.AppendSlice(result, reflect.ValueOf(excepts))
|
||||
return result.Interface()
|
||||
}
|
||||
|
||||
// Find Produces the struct/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.
|
||||
func Find(source any, fn Predicate) (any, error) {
|
||||
aType := reflect.TypeOf(source)
|
||||
if aType.Kind() != reflect.Slice {
|
||||
panic("collections.Find: source must be a slice")
|
||||
}
|
||||
|
||||
sourceVal := reflect.ValueOf(source)
|
||||
if sourceVal.Len() == 0 {
|
||||
return nil, ErrElementNotFound
|
||||
}
|
||||
|
||||
for i := 0; i < sourceVal.Len(); i++ {
|
||||
s := sourceVal.Index(i).Interface()
|
||||
if fn(s) {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrElementNotFound
|
||||
}
|
||||
|
||||
// FindOrDefault Produce the struct/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 struct/value of the slice.
|
||||
func FindOrDefault(source any, fn Predicate, defaultValue any) any {
|
||||
item, err := Find(source, fn)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrElementNotFound) {
|
||||
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.
|
||||
func TakeWhile(data any, fn Predicate) any {
|
||||
aType := reflect.TypeOf(data)
|
||||
if aType.Kind() != reflect.Slice {
|
||||
panic("collections.TakeWhile: data must be a slice")
|
||||
}
|
||||
|
||||
sourceVal := reflect.ValueOf(data)
|
||||
if sourceVal.Len() == 0 {
|
||||
return MakeEmptySlice(aType.Elem())
|
||||
}
|
||||
|
||||
result := reflect.New(reflect.SliceOf(aType.Elem())).Elem()
|
||||
for i := 0; i < sourceVal.Len(); i++ {
|
||||
s := sourceVal.Index(i).Interface()
|
||||
if fn(s) {
|
||||
result = reflect.Append(result, reflect.ValueOf(s))
|
||||
}
|
||||
}
|
||||
return result.Interface()
|
||||
}
|
||||
|
||||
// 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.
|
||||
func ExceptWhile(data any, fn Predicate) any {
|
||||
aType := reflect.TypeOf(data)
|
||||
if aType.Kind() != reflect.Slice {
|
||||
panic("collections.ExceptWhile: data must be a slice")
|
||||
}
|
||||
|
||||
sourceVal := reflect.ValueOf(data)
|
||||
if sourceVal.Len() == 0 {
|
||||
return MakeEmptySlice(aType.Elem())
|
||||
}
|
||||
|
||||
result := reflect.New(reflect.SliceOf(aType.Elem())).Elem()
|
||||
for i := 0; i < sourceVal.Len(); i++ {
|
||||
s := sourceVal.Index(i).Interface()
|
||||
if !fn(s) {
|
||||
result = reflect.Append(result, reflect.ValueOf(s))
|
||||
}
|
||||
}
|
||||
return result.Interface()
|
||||
}
|
||||
|
||||
// type MapFn func(obj T) (target V, find bool)
|
||||
|
||||
// Map a list to new list
|
||||
//
|
||||
// eg: mapping [object0{},object1{},...] to flatten list [object0.someKey, object1.someKey, ...]
|
||||
func Map[T any, V any](list []T, mapFn func(obj T) (val V, find bool)) []V {
|
||||
flatArr := make([]V, 0, len(list))
|
||||
|
||||
for _, obj := range list {
|
||||
if target, ok := mapFn(obj); ok {
|
||||
flatArr = append(flatArr, target)
|
||||
}
|
||||
}
|
||||
return flatArr
|
||||
}
|
||||
|
||||
// Column alias of Map func
|
||||
func Column[T any, V any](list []T, mapFn func(obj T) (val V, find bool)) []V {
|
||||
return Map(list, mapFn)
|
||||
}
|
@ -0,0 +1,317 @@
|
||||
package narr_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/narr"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// StringEqualComparer tests
|
||||
func TestStringEqualComparerShouldEquals(t *testing.T) {
|
||||
assert.Eq(t, 0, narr.StringEqualsComparer("a", "a"))
|
||||
}
|
||||
|
||||
func TestStringEqualComparerShouldNotEquals(t *testing.T) {
|
||||
assert.NotEq(t, 0, narr.StringEqualsComparer("a", "b"))
|
||||
}
|
||||
|
||||
func TestStringEqualComparerElementNotString(t *testing.T) {
|
||||
assert.Eq(t, -1, narr.StringEqualsComparer(1, "a"))
|
||||
}
|
||||
|
||||
func TestStringEqualComparerPtr(t *testing.T) {
|
||||
ptrVal := "a"
|
||||
assert.Eq(t, 0, narr.StringEqualsComparer(&ptrVal, "a"))
|
||||
}
|
||||
|
||||
// ReferenceEqualsComparer tests
|
||||
func TestReferenceEqualsComparerShouldEquals(t *testing.T) {
|
||||
assert.Eq(t, 0, narr.ReferenceEqualsComparer(1, 1))
|
||||
}
|
||||
|
||||
func TestReferenceEqualsComparerShouldNotEquals(t *testing.T) {
|
||||
assert.NotEq(t, 0, narr.ReferenceEqualsComparer(1, 2))
|
||||
}
|
||||
|
||||
// ElemTypeEqualCompareFunc
|
||||
func TestElemTypeEqualCompareFuncShouldEquals(t *testing.T) {
|
||||
assert.Eq(t, 0, narr.ElemTypeEqualsComparer(1, 2))
|
||||
}
|
||||
|
||||
func TestElemTypeEqualCompareFuncShouldNotEquals(t *testing.T) {
|
||||
assert.NotEq(t, 0, narr.ElemTypeEqualsComparer(1, "2"))
|
||||
}
|
||||
|
||||
func TestDifferencesShouldPassed(t *testing.T) {
|
||||
data := []string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
}
|
||||
result := narr.Differences[string](data, []string{"a", "b"}, narr.StringEqualsComparer)
|
||||
assert.Eq(t, []string{"c"}, result)
|
||||
result = narr.Differences[string]([]string{"a", "b"}, data, narr.StringEqualsComparer)
|
||||
assert.Eq(t, []string{"c"}, result)
|
||||
result = narr.Differences[string]([]string{"a", "b", "d"}, data, narr.StringEqualsComparer)
|
||||
assert.Eq(t, 2, len(result))
|
||||
}
|
||||
|
||||
func TestExceptsShouldPassed(t *testing.T) {
|
||||
data := []string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
}
|
||||
result := narr.Excepts(data, []string{"a", "b"}, narr.StringEqualsComparer)
|
||||
assert.Eq(t, []string{"c"}, result.([]string))
|
||||
}
|
||||
|
||||
func TestExceptsFirstNotSliceShouldPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}()
|
||||
narr.Excepts([1]string{"a"}, []string{"a", "b"}, narr.StringEqualsComparer)
|
||||
}
|
||||
|
||||
func TestExceptsSecondNotSliceShouldPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}()
|
||||
narr.Excepts([]string{"a", "b"}, [1]string{"a"}, narr.StringEqualsComparer)
|
||||
}
|
||||
|
||||
func TestExceptsFirstEmptyShouldReturnsEmpty(t *testing.T) {
|
||||
data := []string{}
|
||||
result := narr.Excepts(data, []string{"a", "b"}, narr.StringEqualsComparer).([]string)
|
||||
assert.Eq(t, []string{}, result)
|
||||
assert.NotSame(t, &data, &result, "should always returns new slice")
|
||||
}
|
||||
|
||||
func TestExceptsSecondEmptyShouldReturnsFirst(t *testing.T) {
|
||||
data := []string{"a", "b"}
|
||||
result := narr.Excepts(data, []string{}, narr.StringEqualsComparer).([]string)
|
||||
assert.Eq(t, data, result)
|
||||
assert.NotSame(t, &data, &result, "should always returns new slice")
|
||||
}
|
||||
|
||||
// Intersects tests
|
||||
func TestIntersectsShouldPassed(t *testing.T) {
|
||||
data := []string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
}
|
||||
result := narr.Intersects(data, []string{"a", "b"}, narr.StringEqualsComparer)
|
||||
assert.Eq(t, []string{"a", "b"}, result.([]string))
|
||||
}
|
||||
|
||||
func TestIntersectsFirstNotSliceShouldPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}()
|
||||
narr.Intersects([1]string{"a"}, []string{"a", "b"}, narr.StringEqualsComparer)
|
||||
}
|
||||
|
||||
func TestIntersectsSecondNotSliceShouldPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}()
|
||||
narr.Intersects([]string{"a", "b"}, [1]string{"a"}, narr.StringEqualsComparer)
|
||||
}
|
||||
|
||||
func TestIntersectsFirstEmptyShouldReturnsEmpty(t *testing.T) {
|
||||
data := []string{}
|
||||
second := []string{"a", "b"}
|
||||
result := narr.Intersects(data, second, narr.StringEqualsComparer).([]string)
|
||||
assert.Eq(t, []string{}, result)
|
||||
assert.NotSame(t, &second, &result, "should always returns new slice")
|
||||
}
|
||||
|
||||
func TestIntersectsSecondEmptyShouldReturnsEmpty(t *testing.T) {
|
||||
data := []string{"a", "b"}
|
||||
second := []string{}
|
||||
result := narr.Intersects(data, second, narr.StringEqualsComparer).([]string)
|
||||
assert.Eq(t, []string{}, result)
|
||||
assert.NotSame(t, &data, &result, "should always returns new slice")
|
||||
}
|
||||
|
||||
// Union tests
|
||||
|
||||
func TestUnionShouldPassed(t *testing.T) {
|
||||
data := []string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
}
|
||||
result := narr.Union(data, []string{"a", "b", "d"}, narr.StringEqualsComparer).([]string)
|
||||
assert.Eq(t, []string{"a", "b", "c", "d"}, result)
|
||||
}
|
||||
|
||||
func TestUnionFirstNotSliceShouldPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}()
|
||||
narr.Union([1]string{"a"}, []string{"a", "b"}, narr.StringEqualsComparer)
|
||||
}
|
||||
|
||||
func TestUnionSecondNotSliceShouldPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}()
|
||||
|
||||
narr.Union([]string{"a", "b"}, [1]string{"a"}, narr.StringEqualsComparer)
|
||||
}
|
||||
|
||||
func TestUnionFirstEmptyShouldReturnsSecond(t *testing.T) {
|
||||
data := []string{}
|
||||
second := []string{"a", "b"}
|
||||
result := narr.Union(data, second, narr.StringEqualsComparer).([]string)
|
||||
assert.Eq(t, []string{"a", "b"}, result)
|
||||
assert.NotSame(t, &second, &result, "should always returns new slice")
|
||||
}
|
||||
|
||||
func TestUnionSecondEmptyShouldReturnsFirst(t *testing.T) {
|
||||
data := []string{"a", "b"}
|
||||
second := []string{}
|
||||
result := narr.Union(data, second, narr.StringEqualsComparer).([]string)
|
||||
assert.Eq(t, data, result)
|
||||
assert.NotSame(t, &data, &result, "should always returns new slice")
|
||||
}
|
||||
|
||||
// Find tests
|
||||
func TestFindShouldPassed(t *testing.T) {
|
||||
data := []string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
}
|
||||
|
||||
result, err := narr.Find(data, func(a any) bool { return a == "b" })
|
||||
assert.Nil(t, err)
|
||||
assert.Eq(t, "b", result)
|
||||
|
||||
_, err = narr.Find(data, func(a any) bool { return a == "d" })
|
||||
assert.NotNil(t, err)
|
||||
assert.Eq(t, narr.ErrElementNotFound, err)
|
||||
|
||||
}
|
||||
|
||||
func TestFindNotSliceShouldPanic(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
_, _ = narr.Find([1]string{"a"}, func(a any) bool { return a == "b" })
|
||||
})
|
||||
}
|
||||
|
||||
func TestFindEmptyReturnsErrElementNotFound(t *testing.T) {
|
||||
data := []string{}
|
||||
_, err := narr.Find(data, func(a any) bool { return a == "b" })
|
||||
assert.NotNil(t, err)
|
||||
assert.Eq(t, narr.ErrElementNotFound, err)
|
||||
}
|
||||
|
||||
// FindOrDefault tests
|
||||
func TestFindOrDefaultShouldPassed(t *testing.T) {
|
||||
data := []string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
}
|
||||
|
||||
result := narr.FindOrDefault(data, func(a any) bool { return a == "b" }, "d").(string)
|
||||
assert.Eq(t, "b", result)
|
||||
|
||||
result = narr.FindOrDefault(data, func(a any) bool { return a == "d" }, "d").(string)
|
||||
assert.Eq(t, "d", result)
|
||||
}
|
||||
|
||||
// TakeWhile tests
|
||||
func TestTakeWhileShouldPassed(t *testing.T) {
|
||||
data := []string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
}
|
||||
|
||||
result := narr.TakeWhile(data, func(a any) bool { return a == "b" || a == "c" }).([]string)
|
||||
assert.Eq(t, []string{"b", "c"}, result)
|
||||
}
|
||||
|
||||
func TestTakeWhileNotSliceShouldPanic(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
narr.TakeWhile([1]string{"a"}, func(a any) bool { return a == "b" || a == "c" })
|
||||
})
|
||||
}
|
||||
|
||||
func TestTakeWhileEmptyReturnsEmpty(t *testing.T) {
|
||||
var data []string
|
||||
result := narr.TakeWhile(data, func(a any) bool { return a == "b" || a == "c" }).([]string)
|
||||
assert.Eq(t, []string{}, result)
|
||||
assert.NotSame(t, &data, &result, "should always returns new slice")
|
||||
}
|
||||
|
||||
// ExceptWhile tests
|
||||
|
||||
func TestExceptWhileShouldPassed(t *testing.T) {
|
||||
data := []string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
}
|
||||
|
||||
result := narr.ExceptWhile(data, func(a any) bool { return a == "b" || a == "c" }).([]string)
|
||||
assert.Eq(t, []string{"a"}, result)
|
||||
}
|
||||
|
||||
func TestExceptWhileNotSliceShouldPanic(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
narr.ExceptWhile([1]string{"a"}, func(a any) bool { return a == "b" || a == "c" })
|
||||
})
|
||||
}
|
||||
|
||||
func TestExceptWhileEmptyReturnsEmpty(t *testing.T) {
|
||||
var data []string
|
||||
result := narr.ExceptWhile(data, func(a any) bool { return a == "b" || a == "c" }).([]string)
|
||||
|
||||
assert.Eq(t, []string{}, result)
|
||||
assert.NotSame(t, &data, &result, "should always returns new slice")
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
list1 := []map[string]any{
|
||||
{"name": "tom", "age": 23},
|
||||
{"name": "john", "age": 34},
|
||||
}
|
||||
|
||||
flatArr := narr.Column(list1, func(obj map[string]any) (val any, find bool) {
|
||||
return obj["age"], true
|
||||
})
|
||||
|
||||
assert.NotEmpty(t, flatArr)
|
||||
assert.Contains(t, flatArr, 23)
|
||||
assert.Len(t, flatArr, 2)
|
||||
assert.Eq(t, 34, flatArr[1])
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
package narr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.noahlan.cn/noahlan/ntool/ndef"
|
||||
"git.noahlan.cn/noahlan/ntool/nmath"
|
||||
"git.noahlan.cn/noahlan/ntool/nreflect"
|
||||
"git.noahlan.cn/noahlan/ntool/nstr"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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(nstr.SafeString(v))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* helper func for slices
|
||||
*************************************************************/
|
||||
|
||||
// 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 := nmath.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] = nmath.QuietInt64(v)
|
||||
}
|
||||
return i64s
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// StringsToSlice convert []string to []any
|
||||
func StringsToSlice(ss []string) []any {
|
||||
args := make([]any, len(ss))
|
||||
for i, s := range ss {
|
||||
args[i] = s
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// ToStrings convert any(allow: array,slice) to []string
|
||||
func ToStrings(arr any) (ret []string, err error) {
|
||||
rv := reflect.ValueOf(arr)
|
||||
if rv.Kind() == reflect.String {
|
||||
return []string{rv.String()}, nil
|
||||
}
|
||||
|
||||
if rv.Kind() != reflect.Slice && rv.Kind() != reflect.Array {
|
||||
err = ErrInvalidType
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
str, err := nstr.ToString(rv.Index(i).Interface())
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
ret = append(ret, str)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SliceToStrings convert []any to []string
|
||||
func SliceToStrings(arr []any) []string {
|
||||
return QuietStrings(arr)
|
||||
}
|
||||
|
||||
// QuietStrings convert []any to []string
|
||||
func QuietStrings(arr []any) []string {
|
||||
ss := make([]string, len(arr))
|
||||
for i, v := range arr {
|
||||
ss[i] = nstr.SafeString(v)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// ConvType convert type of slice elements to new type slice, by the given newElemTyp type.
|
||||
//
|
||||
// Supports conversion between []string, []intX, []uintX, []floatX.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ints, _ := narr.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 := nreflect.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 []any to string
|
||||
func ToString(arr []any) 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(nstr.SafeString(v))
|
||||
}
|
||||
|
||||
sb.WriteByte(']')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// CombineToMap combine two slice to map[K]V.
|
||||
//
|
||||
// If keys length is greater than values, the extra keys will be ignored.
|
||||
func CombineToMap[K ndef.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
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package narr_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.noahlan.cn/noahlan/ntool/narr"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToInt64s(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
ints, err := narr.ToInt64s([]string{"1", "2"})
|
||||
is.Nil(err)
|
||||
is.Eq("[]int64{1, 2}", fmt.Sprintf("%#v", ints))
|
||||
|
||||
ints = narr.MustToInt64s([]string{"1", "2"})
|
||||
is.Eq("[]int64{1, 2}", fmt.Sprintf("%#v", ints))
|
||||
|
||||
ints = narr.MustToInt64s([]any{"1", "2"})
|
||||
is.Eq("[]int64{1, 2}", fmt.Sprintf("%#v", ints))
|
||||
|
||||
ints = narr.SliceToInt64s([]any{"1", "2"})
|
||||
is.Eq("[]int64{1, 2}", fmt.Sprintf("%#v", ints))
|
||||
|
||||
_, err = narr.ToInt64s([]string{"a", "b"})
|
||||
is.Err(err)
|
||||
}
|
||||
|
||||
func TestToStrings(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
ss, err := narr.ToStrings([]int{1, 2})
|
||||
is.Nil(err)
|
||||
is.Eq(`[]string{"1", "2"}`, fmt.Sprintf("%#v", ss))
|
||||
|
||||
ss = narr.MustToStrings([]int{1, 2})
|
||||
is.Eq(`[]string{"1", "2"}`, fmt.Sprintf("%#v", ss))
|
||||
|
||||
ss = narr.MustToStrings([]any{1, 2})
|
||||
is.Eq(`[]string{"1", "2"}`, fmt.Sprintf("%#v", ss))
|
||||
|
||||
ss = narr.SliceToStrings([]any{1, 2})
|
||||
is.Eq(`[]string{"1", "2"}`, fmt.Sprintf("%#v", ss))
|
||||
|
||||
as := narr.StringsToSlice([]string{"1", "2"})
|
||||
is.Eq(`[]interface {}{"1", "2"}`, fmt.Sprintf("%#v", as))
|
||||
|
||||
ss, err = narr.ToStrings("b")
|
||||
is.Nil(err)
|
||||
is.Eq(`[]string{"b"}`, fmt.Sprintf("%#v", ss))
|
||||
|
||||
_, err = narr.ToStrings([]any{[]int{1}, nil})
|
||||
is.Err(err)
|
||||
}
|
||||
|
||||
func TestStringsToString(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
is.Eq("a,b", narr.JoinStrings(",", []string{"a", "b"}...))
|
||||
is.Eq("a,b", narr.JoinStrings(",", []string{"a", "b"}...))
|
||||
is.Eq("a,b", narr.JoinStrings(",", "a", "b"))
|
||||
}
|
||||
|
||||
func TestAnyToString(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
arr := [2]string{"a", "b"}
|
||||
|
||||
is.Eq("", narr.AnyToString(nil))
|
||||
is.Eq("[]", narr.AnyToString([]string{}))
|
||||
is.Eq("[a, b]", narr.AnyToString(arr))
|
||||
is.Eq("[a, b]", narr.AnyToString([]string{"a", "b"}))
|
||||
is.Eq("", narr.AnyToString("invalid"))
|
||||
}
|
||||
|
||||
func TestSliceToString(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
is.Eq("[]", narr.SliceToString(nil))
|
||||
is.Eq("[a,b]", narr.SliceToString("a", "b"))
|
||||
}
|
||||
|
||||
func TestStringsToInts(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
ints, err := narr.StringsToInts([]string{"1", "2"})
|
||||
is.Nil(err)
|
||||
is.Eq("[]int{1, 2}", fmt.Sprintf("%#v", ints))
|
||||
|
||||
_, err = narr.StringsToInts([]string{"a", "b"})
|
||||
is.Err(err)
|
||||
|
||||
ints = narr.StringsAsInts([]string{"1", "2"})
|
||||
is.Eq("[]int{1, 2}", fmt.Sprintf("%#v", ints))
|
||||
is.Nil(narr.StringsAsInts([]string{"abc"}))
|
||||
}
|
||||
|
||||
func TestConvType(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
// []string => []int
|
||||
arr, err := narr.ConvType([]string{"1", "2"}, 1)
|
||||
is.Nil(err)
|
||||
is.Eq("[]int{1, 2}", fmt.Sprintf("%#v", arr))
|
||||
|
||||
// []int => []string
|
||||
arr1, err := narr.ConvType([]int{1, 2}, "1")
|
||||
is.Nil(err)
|
||||
is.Eq(`[]string{"1", "2"}`, fmt.Sprintf("%#v", arr1))
|
||||
|
||||
// not need conv
|
||||
arr2, err := narr.ConvType([]string{"1", "2"}, "1")
|
||||
is.Nil(err)
|
||||
is.Eq(`[]string{"1", "2"}`, fmt.Sprintf("%#v", arr2))
|
||||
|
||||
// conv error
|
||||
arr3, err := narr.ConvType([]string{"ab", "cd"}, 1)
|
||||
is.Err(err)
|
||||
is.Nil(arr3)
|
||||
}
|
||||
|
||||
func TestJoinSlice(t *testing.T) {
|
||||
assert.Eq(t, "", narr.JoinSlice(","))
|
||||
assert.Eq(t, "", narr.JoinSlice(",", nil))
|
||||
assert.Eq(t, "a,23,b", narr.JoinSlice(",", "a", 23, "b"))
|
||||
}
|
||||
|
||||
func TestCombineToMap(t *testing.T) {
|
||||
keys := []string{"key0", "key1"}
|
||||
|
||||
mp := narr.CombineToMap(keys, []int{1, 2})
|
||||
assert.Len(t, mp, 2)
|
||||
assert.Eq(t, 1, mp["key0"])
|
||||
assert.Eq(t, 2, mp["key1"])
|
||||
|
||||
mp = narr.CombineToMap(keys, []int{1})
|
||||
assert.Len(t, mp, 1)
|
||||
assert.Eq(t, 1, mp["key0"])
|
||||
}
|
||||
|
||||
func TestCombineToSMap(t *testing.T) {
|
||||
keys := []string{"key0", "key1"}
|
||||
|
||||
mp := narr.CombineToSMap(keys, []string{"val0", "val1"})
|
||||
assert.Len(t, mp, 2)
|
||||
assert.Eq(t, "val0", mp["key0"])
|
||||
|
||||
mp = narr.CombineToSMap(keys, []string{"val0"})
|
||||
assert.Len(t, mp, 2)
|
||||
assert.Eq(t, "val0", mp["key0"])
|
||||
assert.Eq(t, "", mp["key1"])
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package narr
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/ndef"
|
||||
"git.noahlan.cn/noahlan/ntool/nstr"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ArrFormatter struct
|
||||
type ArrFormatter struct {
|
||||
ndef.BaseFormatter
|
||||
// Prefix string for each element
|
||||
Prefix string
|
||||
// Indent string for format each element
|
||||
Indent string
|
||||
// ClosePrefix string for last "]"
|
||||
ClosePrefix string
|
||||
}
|
||||
|
||||
// NewFormatter instance
|
||||
func NewFormatter(arr any) *ArrFormatter {
|
||||
f := &ArrFormatter{}
|
||||
f.Src = arr
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// 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 {
|
||||
f.Format()
|
||||
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(nstr.SafeString(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(']')
|
||||
}
|
||||
|
||||
// FormatIndent array data to string.
|
||||
func FormatIndent(arr any, indent string) string {
|
||||
return NewFormatter(arr).WithIndent(indent).Format()
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package narr_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.noahlan.cn/noahlan/ntool/narr"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewFormatter(t *testing.T) {
|
||||
arr := [2]string{"a", "b"}
|
||||
str := narr.FormatIndent(arr, " ")
|
||||
assert.Contains(t, str, "\n ")
|
||||
fmt.Println(str)
|
||||
|
||||
str = narr.FormatIndent(arr, "")
|
||||
assert.NotContains(t, str, "\n ")
|
||||
assert.Eq(t, "[a, b]", str)
|
||||
fmt.Println(str)
|
||||
|
||||
assert.Eq(t, "", narr.FormatIndent("invalid", ""))
|
||||
assert.Eq(t, "[]", narr.FormatIndent([]string{}, ""))
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package narr
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Ints type
|
||||
type Ints []int
|
||||
|
||||
// String to string
|
||||
func (is Ints) String() string {
|
||||
ss := make([]string, len(is))
|
||||
for i, iv := range is {
|
||||
ss[i] = strconv.Itoa(iv)
|
||||
}
|
||||
return strings.Join(ss, ",")
|
||||
}
|
||||
|
||||
// Has given element
|
||||
func (is Ints) Has(i int) bool {
|
||||
for _, iv := range is {
|
||||
if i == iv {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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() string {
|
||||
if len(ss) > 0 {
|
||||
return ss[0]
|
||||
}
|
||||
return ""
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package narr_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/narr"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInts_Has_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
is narr.Ints
|
||||
val int
|
||||
want bool
|
||||
want2 string
|
||||
}{
|
||||
{
|
||||
narr.Ints{12, 23},
|
||||
12,
|
||||
true,
|
||||
"12,23",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Eq(t, tt.want, tt.is.Has(tt.val))
|
||||
assert.False(t, tt.is.Has(999))
|
||||
assert.Eq(t, tt.want2, tt.is.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrings_methods(t *testing.T) {
|
||||
tests := []struct {
|
||||
ss narr.Strings
|
||||
val string
|
||||
want bool
|
||||
want2 string
|
||||
}{
|
||||
{
|
||||
narr.Strings{"a", "b"},
|
||||
"a",
|
||||
true,
|
||||
"a,b",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Eq(t, tt.want, tt.ss.Has(tt.val))
|
||||
assert.False(t, tt.ss.Has("not-exists"))
|
||||
assert.Eq(t, tt.want2, tt.ss.String())
|
||||
}
|
||||
|
||||
ss := narr.Strings{"a", "b"}
|
||||
assert.Eq(t, "a b", ss.Join(" "))
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package narr
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/ndef"
|
||||
"git.noahlan.cn/noahlan/ntool/nrandom"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Reverse string slice [site user info 0] -> [0 info user site]
|
||||
func Reverse(ss []string) {
|
||||
ln := len(ss)
|
||||
for i := 0; i < ln/2; i++ {
|
||||
li := ln - i - 1
|
||||
ss[i], ss[li] = ss[li], ss[i]
|
||||
}
|
||||
}
|
||||
|
||||
// StringsRemove a value form a string slice
|
||||
func StringsRemove(ss []string, s string) []string {
|
||||
ns := make([]string, 0, len(ss))
|
||||
for _, v := range ss {
|
||||
if v != s {
|
||||
ns = append(ns, v)
|
||||
}
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
// StringsFilter given strings, default will filter emtpy string.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // output: [a, b]
|
||||
// ss := narr.StringsFilter([]string{"a", "", "b", ""})
|
||||
func StringsFilter(ss []string, filter ...func(s string) bool) []string {
|
||||
var fn func(s string) bool
|
||||
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 := narr.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
|
||||
}
|
||||
|
||||
// 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 := nrandom.RandInt(0, len(arr))
|
||||
return arr[i]
|
||||
}
|
||||
panic("cannot get value from nil or empty slice")
|
||||
}
|
||||
|
||||
// Unique value in the given slice data.
|
||||
func Unique[T ~string | ndef.XIntOrFloat](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 ~string | ndef.XIntOrFloat](val T, list []T) int {
|
||||
for i, v := range list {
|
||||
if v == val {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package narr_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.noahlan.cn/noahlan/ntool/narr"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
ss := []string{"a", "b", "c"}
|
||||
|
||||
narr.Reverse(ss)
|
||||
assert.Eq(t, []string{"c", "b", "a"}, ss)
|
||||
}
|
||||
|
||||
func TestStringsRemove(t *testing.T) {
|
||||
ss := []string{"a", "b", "c"}
|
||||
ns := narr.StringsRemove(ss, "b")
|
||||
|
||||
assert.Contains(t, ns, "a")
|
||||
assert.NotContains(t, ns, "b")
|
||||
assert.Len(t, ns, 2)
|
||||
}
|
||||
|
||||
func TestStringsFilter(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
ss := narr.StringsFilter([]string{"a", "", "b", ""})
|
||||
is.Eq([]string{"a", "b"}, ss)
|
||||
}
|
||||
|
||||
func TestTrimStrings(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
// TrimStrings
|
||||
ss := narr.TrimStrings([]string{" a", "b ", " c "})
|
||||
is.Eq("[a b c]", fmt.Sprint(ss))
|
||||
ss = narr.TrimStrings([]string{",a", "b.", ",.c,"}, ",.")
|
||||
is.Eq("[a b c]", fmt.Sprint(ss))
|
||||
ss = narr.TrimStrings([]string{",a", "b.", ",.c,"}, ",", ".")
|
||||
is.Eq("[a b c]", fmt.Sprint(ss))
|
||||
}
|
||||
|
||||
func TestGetRandomOne(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
// int slice
|
||||
intSlice := []int{1, 2, 3, 4, 5, 6}
|
||||
intVal := narr.GetRandomOne(intSlice)
|
||||
intVal1 := narr.GetRandomOne(intSlice)
|
||||
for intVal == intVal1 {
|
||||
intVal1 = narr.GetRandomOne(intSlice)
|
||||
}
|
||||
|
||||
assert.IsType(t, 0, intVal)
|
||||
is.True(narr.HasValue(intSlice, intVal))
|
||||
assert.IsType(t, 0, intVal1)
|
||||
is.True(narr.HasValue(intSlice, intVal1))
|
||||
assert.NotEq(t, intVal, intVal1)
|
||||
|
||||
// int array
|
||||
intArray := []int{1, 2, 3, 4, 5, 6}
|
||||
intReturned := narr.GetRandomOne(intArray)
|
||||
intReturned1 := narr.GetRandomOne(intArray)
|
||||
for intReturned == intReturned1 {
|
||||
intReturned1 = narr.GetRandomOne(intArray)
|
||||
}
|
||||
assert.IsType(t, 0, intReturned)
|
||||
is.True(narr.Contains(intArray, intReturned))
|
||||
assert.IsType(t, 0, intReturned1)
|
||||
is.True(narr.Contains(intArray, intReturned1))
|
||||
assert.NotEq(t, intReturned, intReturned1)
|
||||
|
||||
// string slice
|
||||
strSlice := []string{"aa", "bb", "cc", "dd"}
|
||||
strVal := narr.GetRandomOne(strSlice)
|
||||
strVal1 := narr.GetRandomOne(strSlice)
|
||||
for strVal == strVal1 {
|
||||
strVal1 = narr.GetRandomOne(strSlice)
|
||||
}
|
||||
|
||||
assert.IsType(t, "", strVal)
|
||||
is.True(narr.Contains(strSlice, strVal))
|
||||
assert.IsType(t, "", strVal1)
|
||||
is.True(narr.Contains(strSlice, strVal1))
|
||||
assert.NotEq(t, strVal, strVal1)
|
||||
|
||||
// string array
|
||||
strArray := []string{"aa", "bb", "cc", "dd"}
|
||||
strReturned := narr.GetRandomOne(strArray)
|
||||
strReturned1 := narr.GetRandomOne(strArray)
|
||||
for strReturned == strReturned1 {
|
||||
strReturned1 = narr.GetRandomOne(strArray)
|
||||
}
|
||||
|
||||
assert.IsType(t, "", strReturned)
|
||||
is.True(narr.Contains(strArray, strReturned))
|
||||
assert.IsType(t, "", strReturned1)
|
||||
is.True(narr.Contains(strArray, strReturned1))
|
||||
assert.NotEq(t, strReturned, strReturned1)
|
||||
|
||||
// byte slice
|
||||
byteSlice := []byte("abcdefg")
|
||||
byteVal := narr.GetRandomOne(byteSlice)
|
||||
byteVal1 := narr.GetRandomOne(byteSlice)
|
||||
for byteVal == byteVal1 {
|
||||
byteVal1 = narr.GetRandomOne(byteSlice)
|
||||
}
|
||||
|
||||
assert.IsType(t, byte('a'), byteVal)
|
||||
is.True(narr.Contains(byteSlice, byteVal))
|
||||
assert.IsType(t, byte('a'), byteVal1)
|
||||
is.True(narr.Contains(byteSlice, byteVal1))
|
||||
assert.NotEq(t, byteVal, byteVal1)
|
||||
|
||||
is.Panics(func() {
|
||||
narr.RandomOne([]int{})
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnique(t *testing.T) {
|
||||
assert.Eq(t, []int{2, 3, 4}, narr.Unique[int]([]int{2, 3, 2, 4}))
|
||||
assert.Eq(t, []uint{2, 3, 4}, narr.Unique([]uint{2, 3, 2, 4}))
|
||||
assert.Eq(t, []string{"ab", "bc", "cd"}, narr.Unique([]string{"ab", "bc", "ab", "cd"}))
|
||||
|
||||
assert.Eq(t, 1, narr.IndexOf(3, []int{2, 3, 4}))
|
||||
assert.Eq(t, -1, narr.IndexOf(5, []int{2, 3, 4}))
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package nbyte
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Buffer wrap and extends the bytes.Buffer
|
||||
type Buffer struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
// NewBuffer instance
|
||||
func NewBuffer() *Buffer {
|
||||
return &Buffer{}
|
||||
}
|
||||
|
||||
// WriteAny type value to buffer
|
||||
func (b *Buffer) WriteAny(vs ...any) {
|
||||
for _, v := range vs {
|
||||
_, _ = b.Buffer.WriteString(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
|
||||
// QuietWriteByte to buffer
|
||||
func (b *Buffer) QuietWriteByte(c byte) {
|
||||
_ = b.WriteByte(c)
|
||||
}
|
||||
|
||||
// QuietWritef write message to buffer
|
||||
func (b *Buffer) QuietWritef(tpl string, vs ...any) {
|
||||
_, _ = b.WriteString(fmt.Sprintf(tpl, vs...))
|
||||
}
|
||||
|
||||
// Writeln write message to buffer with newline
|
||||
func (b *Buffer) Writeln(ss ...string) {
|
||||
b.QuietWriteln(ss...)
|
||||
}
|
||||
|
||||
// QuietWriteln write message to buffer with newline
|
||||
func (b *Buffer) QuietWriteln(ss ...string) {
|
||||
_, _ = b.WriteString(strings.Join(ss, ""))
|
||||
_ = b.WriteByte('\n')
|
||||
}
|
||||
|
||||
// QuietWriteString to buffer
|
||||
func (b *Buffer) QuietWriteString(ss ...string) {
|
||||
_, _ = b.WriteString(strings.Join(ss, ""))
|
||||
}
|
||||
|
||||
// MustWriteString to buffer
|
||||
func (b *Buffer) MustWriteString(ss ...string) {
|
||||
_, err := b.WriteString(strings.Join(ss, ""))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ResetAndGet buffer string.
|
||||
func (b *Buffer) ResetAndGet() string {
|
||||
s := b.String()
|
||||
b.Reset()
|
||||
return s
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package nbyte_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nbyte"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuffer_WriteAny(t *testing.T) {
|
||||
buf := nbyte.NewBuffer()
|
||||
|
||||
buf.QuietWritef("ab-%s", "c")
|
||||
buf.QuietWriteByte('d')
|
||||
assert.Eq(t, "ab-cd", buf.ResetAndGet())
|
||||
|
||||
buf.QuietWriteString("ab", "-", "cd")
|
||||
buf.MustWriteString("-ef")
|
||||
assert.Eq(t, "ab-cd-ef", buf.ResetAndGet())
|
||||
|
||||
buf.WriteAny(23, "abc")
|
||||
assert.Eq(t, "23abc", buf.ResetAndGet())
|
||||
|
||||
buf.Writeln("abc")
|
||||
assert.Eq(t, "abc\n", buf.ResetAndGet())
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package nbyte
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Md5 Generate a 32-bit md5 bytes
|
||||
func Md5(src any) []byte {
|
||||
h := md5.New()
|
||||
|
||||
if s, ok := src.(string); ok {
|
||||
h.Write([]byte(s))
|
||||
} else {
|
||||
h.Write([]byte(fmt.Sprint(src)))
|
||||
}
|
||||
return h.Sum(nil)
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package nbyte
|
||||
|
||||
// IsLower checks if a character is lower case ('a' to 'z')
|
||||
func IsLower(c byte) bool {
|
||||
return 'a' <= c && c <= 'z'
|
||||
}
|
||||
|
||||
// ToLower converts a character 'A' to 'Z' to its lower case
|
||||
func ToLower(c byte) byte {
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
return c + 32
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// ToLowerAll converts a character 'A' to 'Z' to its lower case
|
||||
func ToLowerAll(bs []byte) []byte {
|
||||
for i := range bs {
|
||||
bs[i] = ToLower(bs[i])
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
// IsUpper checks if a character is upper case ('A' to 'Z')
|
||||
func IsUpper(c byte) bool {
|
||||
return 'A' <= c && c <= 'Z'
|
||||
}
|
||||
|
||||
// ToUpper converts a character 'a' to 'z' to its upper case
|
||||
func ToUpper(r byte) byte {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return r - 32
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ToUpperAll converts a character 'a' to 'z' to its upper case
|
||||
func ToUpperAll(rs []byte) []byte {
|
||||
for i := range rs {
|
||||
rs[i] = ToUpper(rs[i])
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
// IsDigit checks if a character is digit ('0' to '9')
|
||||
func IsDigit(r byte) bool {
|
||||
return r >= '0' && r <= '9'
|
||||
}
|
||||
|
||||
// IsAlphabet byte
|
||||
func IsAlphabet(char byte) bool {
|
||||
// A 65 -> Z 90
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
return true
|
||||
}
|
||||
|
||||
// a 97 -> z 122
|
||||
if char >= 'a' && char <= 'z' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package nbyte
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// BytesEncoder interface
|
||||
type BytesEncoder interface {
|
||||
Encode(src []byte) []byte
|
||||
Decode(src []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// StdEncoder implement the BytesEncoder
|
||||
type StdEncoder struct {
|
||||
encodeFn func(src []byte) []byte
|
||||
decodeFn func(src []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// NewStdEncoder instance
|
||||
func NewStdEncoder(encFn func(src []byte) []byte, decFn func(src []byte) ([]byte, error)) *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
|
||||
})
|
||||
)
|
@ -0,0 +1,27 @@
|
||||
package nbyte_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nbyte"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestB64Encoder(t *testing.T) {
|
||||
src := []byte("abc1234566")
|
||||
dst := nbyte.B64Encoder.Encode(src)
|
||||
assert.NotEmpty(t, dst)
|
||||
|
||||
decSrc, err := nbyte.B64Encoder.Decode(dst)
|
||||
assert.NoError(t, err)
|
||||
assert.Eq(t, src, decSrc)
|
||||
}
|
||||
|
||||
func TestHexEncoder(t *testing.T) {
|
||||
src := []byte("abc1234566")
|
||||
dst := nbyte.HexEncoder.Encode(src)
|
||||
assert.NotEmpty(t, dst)
|
||||
|
||||
decSrc, err := nbyte.HexEncoder.Decode(dst)
|
||||
assert.NoError(t, err)
|
||||
assert.Eq(t, src, decSrc)
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package nbyte
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// FirstLine from command output
|
||||
func FirstLine(bs []byte) []byte {
|
||||
if i := bytes.IndexByte(bs, '\n'); i >= 0 {
|
||||
return bs[0:i]
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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. like the strings.Cut()
|
||||
func Cut(bs []byte, sep byte) (before, after []byte, found bool) {
|
||||
if i := bytes.IndexByte(bs, sep); i >= 0 {
|
||||
return bs[:i], bs[i+1:], true
|
||||
}
|
||||
|
||||
before = bs
|
||||
return
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package nbyte_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.noahlan.cn/noahlan/ntool/nbyte"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"git.noahlan.cn/noahlan/ntool/ntime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFirstLine(t *testing.T) {
|
||||
bs := []byte("hi\ninhere")
|
||||
assert.Eq(t, []byte("hi"), nbyte.FirstLine(bs))
|
||||
assert.Eq(t, []byte("hi"), nbyte.FirstLine([]byte("hi")))
|
||||
}
|
||||
|
||||
func TestStrOrErr(t *testing.T) {
|
||||
bs := []byte("hi, inhere")
|
||||
assert.Eq(t, "hi, inhere", nbyte.SafeString(bs, nil))
|
||||
assert.Eq(t, "", nbyte.SafeString(bs, errors.New("error")))
|
||||
|
||||
str, err := nbyte.StrOrErr(bs, nil)
|
||||
assert.NoErr(t, err)
|
||||
assert.Eq(t, "hi, inhere", str)
|
||||
|
||||
str, err = nbyte.StrOrErr(bs, errors.New("error"))
|
||||
assert.Err(t, err)
|
||||
assert.Eq(t, "", str)
|
||||
}
|
||||
|
||||
func TestMd5(t *testing.T) {
|
||||
assert.NotEmpty(t, nbyte.Md5("abc"))
|
||||
assert.NotEmpty(t, nbyte.Md5([]int{12, 34}))
|
||||
}
|
||||
|
||||
func TestAppendAny(t *testing.T) {
|
||||
assert.Eq(t, []byte("123"), nbyte.AppendAny(nil, 123))
|
||||
assert.Eq(t, []byte("123"), nbyte.AppendAny([]byte{}, 123))
|
||||
assert.Eq(t, []byte("123"), nbyte.AppendAny([]byte("1"), 23))
|
||||
assert.Eq(t, []byte("1<nil>"), nbyte.AppendAny([]byte("1"), nil))
|
||||
assert.Eq(t, "3600000000000", string(nbyte.AppendAny([]byte{}, ntime.OneHour)))
|
||||
}
|
||||
|
||||
func TestCut(t *testing.T) {
|
||||
// test for nbyte.Cut()
|
||||
b, a, ok := nbyte.Cut([]byte("age=123"), '=')
|
||||
assert.True(t, ok)
|
||||
assert.Eq(t, []byte("age"), b)
|
||||
assert.Eq(t, []byte("123"), a)
|
||||
|
||||
b, a, ok = nbyte.Cut([]byte("age=123"), 'x')
|
||||
assert.False(t, ok)
|
||||
assert.Eq(t, []byte("age=123"), b)
|
||||
assert.Empty(t, a)
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package cmdline
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nstr"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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(nstr.SafeString(arg))
|
||||
}
|
||||
}
|
||||
|
||||
// WriteString arg string to the builder, will auto quote special string.
|
||||
// refer strconv.Quote()
|
||||
func (b *LineBuilder) WriteString(a string) (int, error) {
|
||||
var quote byte
|
||||
if pos := strings.IndexByte(a, '"'); pos > -1 {
|
||||
quote = '\''
|
||||
// fix: a = `--pretty=format:"one two three"`
|
||||
if pos > 0 && '"' == a[len(a)-1] {
|
||||
quote = 0
|
||||
}
|
||||
} else if pos := strings.IndexByte(a, '\''); pos > -1 {
|
||||
quote = '"'
|
||||
// fix: a = "--pretty=format:'one two three'"
|
||||
if pos > 0 && '\'' == a[len(a)-1] {
|
||||
quote = 0
|
||||
}
|
||||
} else if a == "" || strings.ContainsRune(a, ' ') {
|
||||
quote = '"'
|
||||
}
|
||||
|
||||
// add sep on not-first write.
|
||||
if b.Len() != 0 {
|
||||
_ = b.WriteByte(' ')
|
||||
}
|
||||
|
||||
// no quote char OR not need quote
|
||||
if quote == 0 {
|
||||
return b.Builder.WriteString(a)
|
||||
}
|
||||
|
||||
_ = b.WriteByte(quote) // add start quote
|
||||
n, err := b.Builder.WriteString(a)
|
||||
_ = b.WriteByte(quote) // add end quote
|
||||
return n, err
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package cmdline_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/ncli/cmdline"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLineBuild(t *testing.T) {
|
||||
s := cmdline.LineBuild("myapp", []string{"-a", "val0", "arg0"})
|
||||
|
||||
assert.Eq(t, "myapp -a val0 arg0", s)
|
||||
|
||||
// case: empty string
|
||||
b := cmdline.NewBuilder("myapp", "-a", "")
|
||||
|
||||
assert.Eq(t, 11, b.Len())
|
||||
assert.Eq(t, `myapp -a ""`, b.String())
|
||||
|
||||
b.Reset()
|
||||
assert.Eq(t, 0, b.Len())
|
||||
|
||||
// case: add first
|
||||
b.AddArg("myapp")
|
||||
assert.Eq(t, `myapp`, b.String())
|
||||
|
||||
b.AddArgs("-a", "val0")
|
||||
assert.Eq(t, "myapp -a val0", b.String())
|
||||
|
||||
// case: contains `"`
|
||||
b.Reset()
|
||||
b.AddArgs("myapp", "-a", `"val0"`)
|
||||
assert.Eq(t, `myapp -a '"val0"'`, b.String())
|
||||
b.Reset()
|
||||
b.AddArgs("myapp", "-a", `the "val0" of option`)
|
||||
assert.Eq(t, `myapp -a 'the "val0" of option'`, b.String())
|
||||
|
||||
// case: contains `'`
|
||||
b.Reset()
|
||||
b.AddArgs("myapp", "-a", `'val0'`)
|
||||
assert.Eq(t, `myapp -a "'val0'"`, b.String())
|
||||
b.Reset()
|
||||
b.AddArgs("myapp", "-a", `the 'val0' of option`)
|
||||
assert.Eq(t, `myapp -a "the 'val0' of option"`, b.String())
|
||||
}
|
||||
|
||||
func TestLineBuild_hasQuote(t *testing.T) {
|
||||
line := "git log --pretty=format:'one two three'"
|
||||
args := cmdline.ParseLine(line)
|
||||
// dump.P(args)
|
||||
assert.Eq(t, line, cmdline.LineBuild("", args))
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package cmdline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
package cmdline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"git.noahlan.cn/noahlan/ntool/internal/common"
|
||||
"git.noahlan.cn/noahlan/ntool/ndef"
|
||||
"git.noahlan.cn/noahlan/ntool/nstr"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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 = common.ParseEnvVar(p.Line, nil)
|
||||
}
|
||||
|
||||
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 == ndef.DoubleQuote || start == ndef.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 == ndef.DoubleQuote || end == ndef.SingleQuote {
|
||||
p.args = append(p.args, node) // only one node: `msg"`
|
||||
} else {
|
||||
// eg: --pretty=format:'one two three'
|
||||
if nstr.ContainsByte(node, ndef.DoubleQuote) {
|
||||
p.quoteIndex = 1 // mark is not on start
|
||||
p.quoteChar = ndef.DoubleQuote
|
||||
} else if nstr.ContainsByte(node, ndef.SingleQuote) {
|
||||
p.quoteIndex = 1
|
||||
p.quoteChar = ndef.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()
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package cmdline_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/ncli/cmdline"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/mock"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLineParser_Parse(t *testing.T) {
|
||||
args := cmdline.NewParser(`./app top sub -a ddd --xx "msg"`).Parse()
|
||||
assert.Len(t, args, 7)
|
||||
assert.Eq(t, "msg", args[6])
|
||||
|
||||
args = cmdline.ParseLine(" ")
|
||||
assert.Len(t, args, 0)
|
||||
|
||||
args = cmdline.ParseLine("./app")
|
||||
assert.Len(t, args, 1)
|
||||
|
||||
p := cmdline.NewParser("./app sub ${A_ENV_VAR}")
|
||||
p.WithParseEnv()
|
||||
assert.True(t, p.ParseEnv)
|
||||
|
||||
mock.MockEnvValue("A_ENV_VAR", "env-value", func(nv string) {
|
||||
bin, args := p.BinAndArgs()
|
||||
assert.Len(t, args, 2)
|
||||
assert.Eq(t, "./app", bin)
|
||||
assert.Eq(t, "env-value", args[1])
|
||||
|
||||
assert.NotEmpty(t, p.NewExecCmd())
|
||||
})
|
||||
|
||||
p = cmdline.NewParser("./app sub ${A_ENV_VAR2}")
|
||||
mock.MockEnvValue("A_ENV_VAR2", "env-value2", func(nv string) {
|
||||
args := p.AlsoEnvParse()
|
||||
assert.Len(t, args, 3)
|
||||
assert.Eq(t, "env-value2", args[2])
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseLine_Parse_withQuote(t *testing.T) {
|
||||
tests := []struct {
|
||||
line string
|
||||
argN int
|
||||
index int
|
||||
value string
|
||||
}{
|
||||
{
|
||||
line: `./app top sub -a ddd --xx "abc
|
||||
def"`,
|
||||
argN: 7, index: 6, value: "abc\ndef",
|
||||
},
|
||||
{
|
||||
line: `./app top sub -a ddd --xx "abc
|
||||
def ghi"`,
|
||||
argN: 7, index: 6, value: "abc\ndef ghi",
|
||||
},
|
||||
{
|
||||
line: `./app top sub --msg "has multi words"`,
|
||||
argN: 5, index: 4, value: "has multi words",
|
||||
},
|
||||
{
|
||||
line: `./app top sub --msg "has inner 'quote'"`,
|
||||
argN: 5, index: 4, value: "has inner 'quote'",
|
||||
},
|
||||
{
|
||||
line: `./app top sub --msg "'has' inner quote"`,
|
||||
argN: 5, index: 4, value: "'has' inner quote",
|
||||
},
|
||||
{
|
||||
line: `./app top sub --msg "has inner 'quote' words"`,
|
||||
argN: 5, index: 4, value: "has inner 'quote' words",
|
||||
},
|
||||
{
|
||||
line: `./app top sub --msg "has 'inner quote' words"`,
|
||||
argN: 5, index: 4, value: "has 'inner quote' words",
|
||||
},
|
||||
{
|
||||
line: `./app top sub --msg "has 'inner quote words'"`,
|
||||
argN: 5, index: 4, value: "has 'inner quote words'",
|
||||
},
|
||||
{
|
||||
line: `./app top sub --msg "'has inner quote' words"`,
|
||||
argN: 5, index: 4, value: "'has inner quote' words",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
args := cmdline.NewParser(tt.line).Parse()
|
||||
assert.Len(t, args, tt.argN)
|
||||
assert.Eq(t, tt.value, args[tt.index])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLine_longLine(t *testing.T) {
|
||||
line := "git log --pretty=format:'one two three'"
|
||||
args := cmdline.ParseLine(line)
|
||||
assert.Len(t, args, 3)
|
||||
assert.Eq(t, "--pretty=format:'one two three'", args[2])
|
||||
|
||||
line = `git log --pretty=format:"one two three""`
|
||||
args = cmdline.ParseLine(line)
|
||||
assert.Len(t, args, 3)
|
||||
assert.Eq(t, `--pretty=format:"one two three""`, args[2])
|
||||
|
||||
line = "git log --color --graph --pretty=format:'%Cred%h%Creset:%C(ul yellow)%d%Creset %s (%Cgreen%cr%Creset, %C(bold blue)%an%Creset)' --abbrev-commit -10"
|
||||
args = cmdline.ParseLine(line)
|
||||
//dump.P(args)
|
||||
assert.Len(t, args, 7)
|
||||
assert.Eq(t, "--graph", args[3])
|
||||
assert.Eq(t, "--abbrev-commit", args[5])
|
||||
}
|
||||
|
||||
func TestParseLine_errLine(t *testing.T) {
|
||||
// exception line string.
|
||||
args := cmdline.NewParser(`./app top sub -a ddd --xx msg"`).Parse()
|
||||
assert.Len(t, args, 7)
|
||||
assert.Eq(t, "msg\"", args[6])
|
||||
|
||||
args = cmdline.ParseLine(`./app top sub -a ddd --xx "msg`)
|
||||
assert.Len(t, args, 7)
|
||||
assert.Eq(t, "msg", args[6])
|
||||
|
||||
args = cmdline.ParseLine(`./app top sub -a ddd --xx "msg text`)
|
||||
assert.Len(t, args, 7)
|
||||
assert.Eq(t, "msg text", args[6])
|
||||
|
||||
args = cmdline.ParseLine(`./app top sub -a ddd --xx "msg "text"`)
|
||||
assert.Len(t, args, 7)
|
||||
assert.Eq(t, "msg \"text", args[6])
|
||||
}
|
||||
|
||||
func TestLineParser_BinAndArgs(t *testing.T) {
|
||||
p := cmdline.NewParser("git status")
|
||||
b, a := p.BinAndArgs()
|
||||
assert.Eq(t, "git", b)
|
||||
assert.Eq(t, "status", strings.Join(a, " "))
|
||||
|
||||
p = cmdline.NewParser("git")
|
||||
b, a = p.BinAndArgs()
|
||||
assert.Eq(t, "git", b)
|
||||
assert.Empty(t, a)
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package ncli
|
||||
|
||||
import "github.com/gookit/color"
|
||||
|
||||
/*************************************************************
|
||||
* quick use color print message
|
||||
*************************************************************/
|
||||
|
||||
// Redp print message with Red color
|
||||
func Redp(a ...any) { color.Red.Print(a...) }
|
||||
|
||||
// Redf print message with Red color
|
||||
func Redf(format string, a ...any) { color.Red.Printf(format, a...) }
|
||||
|
||||
// Redln print message line with Red color
|
||||
func Redln(a ...any) { color.Red.Println(a...) }
|
||||
|
||||
// Bluep print message with Blue color
|
||||
func Bluep(a ...any) { color.Blue.Print(a...) }
|
||||
|
||||
// Bluef print message with Blue color
|
||||
func Bluef(format string, a ...any) { color.Blue.Printf(format, a...) }
|
||||
|
||||
// Blueln print message line with Blue color
|
||||
func Blueln(a ...any) { color.Blue.Println(a...) }
|
||||
|
||||
// Cyanp print message with Cyan color
|
||||
func Cyanp(a ...any) { color.Cyan.Print(a...) }
|
||||
|
||||
// Cyanf print message with Cyan color
|
||||
func Cyanf(format string, a ...any) { color.Cyan.Printf(format, a...) }
|
||||
|
||||
// Cyanln print message line with Cyan color
|
||||
func Cyanln(a ...any) { color.Cyan.Println(a...) }
|
||||
|
||||
// Grayp print message with gray color
|
||||
func Grayp(a ...any) { color.Gray.Print(a...) }
|
||||
|
||||
// Grayf print message with gray color
|
||||
func Grayf(format string, a ...any) { color.Gray.Printf(format, a...) }
|
||||
|
||||
// Grayln print message line with gray color
|
||||
func Grayln(a ...any) { color.Gray.Println(a...) }
|
||||
|
||||
// Greenp print message with green color
|
||||
func Greenp(a ...any) { color.Green.Print(a...) }
|
||||
|
||||
// Greenf print message with green color
|
||||
func Greenf(format string, a ...any) { color.Green.Printf(format, a...) }
|
||||
|
||||
// Greenln print message line with green color
|
||||
func Greenln(a ...any) { color.Green.Println(a...) }
|
||||
|
||||
// Yellowp print message with yellow color
|
||||
func Yellowp(a ...any) { color.Yellow.Print(a...) }
|
||||
|
||||
// Yellowf print message with yellow color
|
||||
func Yellowf(format string, a ...any) { color.Yellow.Printf(format, a...) }
|
||||
|
||||
// Yellowln print message line with yellow color
|
||||
func Yellowln(a ...any) { color.Yellow.Println(a...) }
|
||||
|
||||
// Magentap print message with magenta color
|
||||
func Magentap(a ...any) { color.Magenta.Print(a...) }
|
||||
|
||||
// Magentaf print message with magenta color
|
||||
func Magentaf(format string, a ...any) { color.Magenta.Printf(format, a...) }
|
||||
|
||||
// Magentaln print message line with magenta color
|
||||
func Magentaln(a ...any) { color.Magenta.Println(a...) }
|
||||
|
||||
/*************************************************************
|
||||
* quick use style print message
|
||||
*************************************************************/
|
||||
|
||||
// Infop print message with info color
|
||||
func Infop(a ...any) { color.Info.Print(a...) }
|
||||
|
||||
// Infof print message with info style
|
||||
func Infof(format string, a ...any) { color.Info.Printf(format, a...) }
|
||||
|
||||
// Infoln print message with info style
|
||||
func Infoln(a ...any) { color.Info.Println(a...) }
|
||||
|
||||
// Successp print message with success color
|
||||
func Successp(a ...any) { color.Success.Print(a...) }
|
||||
|
||||
// Successf print message with success style
|
||||
func Successf(format string, a ...any) { color.Success.Printf(format, a...) }
|
||||
|
||||
// Successln print message with success style
|
||||
func Successln(a ...any) { color.Success.Println(a...) }
|
||||
|
||||
// Errorp print message with error color
|
||||
func Errorp(a ...any) { color.Error.Print(a...) }
|
||||
|
||||
// Errorf print message with error style
|
||||
func Errorf(format string, a ...any) { color.Error.Printf(format, a...) }
|
||||
|
||||
// Errorln print message with error style
|
||||
func Errorln(a ...any) { color.Error.Println(a...) }
|
||||
|
||||
// Warnp print message with warn color
|
||||
func Warnp(a ...any) { color.Warn.Print(a...) }
|
||||
|
||||
// Warnf print message with warn style
|
||||
func Warnf(format string, a ...any) { color.Warn.Printf(format, a...) }
|
||||
|
||||
// Warnln print message with warn style
|
||||
func Warnln(a ...any) { color.Warn.Println(a...) }
|
@ -0,0 +1,54 @@
|
||||
package ncli
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/internal/common"
|
||||
"golang.org/x/term"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
// Workdir get
|
||||
func Workdir() string {
|
||||
return common.Workdir()
|
||||
}
|
||||
|
||||
// BinDir get
|
||||
func BinDir() string {
|
||||
return path.Dir(os.Args[0])
|
||||
}
|
||||
|
||||
// BinFile get
|
||||
func BinFile() string {
|
||||
return os.Args[0]
|
||||
}
|
||||
|
||||
// BinName get
|
||||
func BinName() string {
|
||||
return path.Base(os.Args[0])
|
||||
}
|
||||
|
||||
// exec: `stty -a 2>&1`
|
||||
// const (
|
||||
// mac: speed 9600 baud; 97 rows; 362 columns;
|
||||
// macSttyMsgPattern = `(\d+)\s+rows;\s*(\d+)\s+columns;`
|
||||
// linux: speed 38400 baud; rows 97; columns 362; line = 0;
|
||||
// linuxSttyMsgPattern = `rows\s+(\d+);\s*columns\s+(\d+);`
|
||||
// )
|
||||
var terminalWidth, terminalHeight int
|
||||
|
||||
// GetTermSize for current console terminal.
|
||||
func GetTermSize(refresh ...bool) (w int, h int) {
|
||||
if terminalWidth > 0 && len(refresh) > 0 && !refresh[0] {
|
||||
return terminalWidth, terminalHeight
|
||||
}
|
||||
|
||||
var err error
|
||||
w, h, err = term.GetSize(syscallStdinFd())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// cache result
|
||||
terminalWidth, terminalHeight = w, h
|
||||
return
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
//go:build !windows
|
||||
|
||||
package ncli
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func syscallStdinFd() int {
|
||||
return syscall.Stdin
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
//go:build windows
|
||||
|
||||
package ncli
|
||||
|
||||
import "syscall"
|
||||
|
||||
// on Windows, must convert 'syscall.Stdin' to int
|
||||
func syscallStdinFd() int {
|
||||
return int(syscall.Stdin)
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
package ncli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// the global input output stream
|
||||
var (
|
||||
// Input global input stream
|
||||
Input io.Reader = os.Stdin
|
||||
// Output global output stream
|
||||
Output io.Writer = os.Stdout
|
||||
)
|
||||
|
||||
// ReadInput read user input form Stdin
|
||||
func ReadInput(question string) (string, error) {
|
||||
if len(question) > 0 {
|
||||
color.Fprint(Output, question)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(Input)
|
||||
if !scanner.Scan() { // reading
|
||||
return "", scanner.Err()
|
||||
}
|
||||
|
||||
answer := scanner.Text()
|
||||
return strings.TrimSpace(answer), nil
|
||||
}
|
||||
|
||||
// ReadLine read one line from user input.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// in := ncli.ReadLine("")
|
||||
// ans, _ := ncli.ReadLine("your name?")
|
||||
func ReadLine(question string) (string, error) {
|
||||
if len(question) > 0 {
|
||||
color.Fprint(Output, question)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(Input)
|
||||
answer, _, err := reader.ReadLine()
|
||||
return strings.TrimSpace(string(answer)), err
|
||||
}
|
||||
|
||||
// ReadFirst read first char
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ans, _ := ncli.ReadFirst("continue?[y/n] ")
|
||||
func ReadFirst(question string) (string, error) {
|
||||
answer, err := ReadFirstByte(question)
|
||||
return string(answer), err
|
||||
}
|
||||
|
||||
// ReadFirstByte read first byte char
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ans, _ := ncli.ReadFirstByte("continue?[y/n] ")
|
||||
func ReadFirstByte(question string) (byte, error) {
|
||||
if len(question) > 0 {
|
||||
color.Fprint(Output, question)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(Input)
|
||||
return reader.ReadByte()
|
||||
}
|
||||
|
||||
// ReadFirstRune read first rune char
|
||||
func ReadFirstRune(question string) (rune, error) {
|
||||
if len(question) > 0 {
|
||||
color.Fprint(Output, question)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(Input)
|
||||
answer, _, err := reader.ReadRune()
|
||||
return answer, err
|
||||
}
|
||||
|
||||
// ReadAsBool check user inputted answer is right
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ok := ReadAsBool("are you OK? [y/N]", false)
|
||||
func ReadAsBool(tip string, defVal bool) bool {
|
||||
fChar, err := ReadFirstByte(tip)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if fChar != 0 {
|
||||
return ByteIsYes(fChar)
|
||||
}
|
||||
return defVal
|
||||
}
|
||||
|
||||
// ReadPassword from console terminal
|
||||
func ReadPassword(question ...string) string {
|
||||
if len(question) > 0 {
|
||||
print(question[0])
|
||||
} else {
|
||||
print("Enter Password: ")
|
||||
}
|
||||
|
||||
bs, err := term.ReadPassword(syscallStdinFd())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
println() // new line
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
// Confirm with user input
|
||||
func Confirm(tip string, defVal ...bool) bool {
|
||||
mark := " [y/N]: "
|
||||
|
||||
var defV bool
|
||||
if len(defVal) > 0 && defVal[0] {
|
||||
defV = true
|
||||
mark = " [Y/n]: "
|
||||
}
|
||||
|
||||
return ReadAsBool(tip+mark, defV)
|
||||
}
|
||||
|
||||
// InputIsYes answer: yes, y, Yes, Y
|
||||
func InputIsYes(ans string) bool {
|
||||
return len(ans) > 0 && (ans[0] == 'y' || ans[0] == 'Y')
|
||||
}
|
||||
|
||||
// ByteIsYes answer: yes, y, Yes, Y
|
||||
func ByteIsYes(ans byte) bool {
|
||||
return ans == 'y' || ans == 'Y'
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package ncli
|
@ -0,0 +1,54 @@
|
||||
package ncli_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/ncli"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadFirst(t *testing.T) {
|
||||
// testutil.RewriteStdout()
|
||||
// _, err := os.Stdout.WriteString("haha")
|
||||
// ans, err1 := ncli.ReadFirst("hi?")
|
||||
// testutil.RestoreStdout()
|
||||
// assert.NoError(t, err)
|
||||
// assert.NoError(t, err1)
|
||||
// assert.Equal(t, "haha", ans)
|
||||
}
|
||||
|
||||
func TestInputIsYes(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
wnt bool
|
||||
}{
|
||||
{"y", true},
|
||||
{"yes", true},
|
||||
{"yES", true},
|
||||
{"Y", true},
|
||||
{"Yes", true},
|
||||
{"YES", true},
|
||||
{"h", false},
|
||||
{"n", false},
|
||||
{"no", false},
|
||||
{"NO", false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Eq(t, test.wnt, ncli.InputIsYes(test.in))
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteIsYes(t *testing.T) {
|
||||
tests := []struct {
|
||||
in byte
|
||||
wnt bool
|
||||
}{
|
||||
{'y', true},
|
||||
{'Y', true},
|
||||
{'h', false},
|
||||
{'n', false},
|
||||
{'N', false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Eq(t, test.wnt, ncli.ByteIsYes(test.in))
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
package ncli
|
@ -0,0 +1,147 @@
|
||||
package ncli
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/internal/common"
|
||||
"git.noahlan.cn/noahlan/ntool/ncli/cmdline"
|
||||
"git.noahlan.cn/noahlan/ntool/nstr"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LineBuild build command line string by given args.
|
||||
func LineBuild(binFile string, args []string) string {
|
||||
return cmdline.NewBuilder(binFile, args...).String()
|
||||
}
|
||||
|
||||
// BuildLine build command line string by given args.
|
||||
func BuildLine(binFile string, args []string) string {
|
||||
return cmdline.NewBuilder(binFile, args...).String()
|
||||
}
|
||||
|
||||
// String2OSArgs parse input command line text to os.Args
|
||||
func String2OSArgs(line string) []string {
|
||||
return cmdline.NewParser(line).Parse()
|
||||
}
|
||||
|
||||
// StringToOSArgs parse input command line text to os.Args
|
||||
func StringToOSArgs(line string) []string {
|
||||
return cmdline.NewParser(line).Parse()
|
||||
}
|
||||
|
||||
// ParseLine input command line text. alias of the StringToOSArgs()
|
||||
func ParseLine(line string) []string {
|
||||
return cmdline.NewParser(line).Parse()
|
||||
}
|
||||
|
||||
// QuickExec quick exec a simple command line
|
||||
func QuickExec(cmdLine string, workDir ...string) (string, error) {
|
||||
return ExecLine(cmdLine, workDir...)
|
||||
}
|
||||
|
||||
// ExecLine quick exec an command line string
|
||||
func ExecLine(cmdLine string, workDir ...string) (string, error) {
|
||||
p := cmdline.NewParser(cmdLine)
|
||||
|
||||
// create a new Cmd instance
|
||||
cmd := p.NewExecCmd()
|
||||
if len(workDir) > 0 {
|
||||
cmd.Dir = workDir[0]
|
||||
}
|
||||
|
||||
bs, err := cmd.Output()
|
||||
return string(bs), err
|
||||
}
|
||||
|
||||
// ExecCommand alias of the ExecCmd()
|
||||
func ExecCommand(binName string, args []string, workDir ...string) (string, error) {
|
||||
return ExecCmd(binName, args, workDir...)
|
||||
}
|
||||
|
||||
// ExecCmd a command and return output.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ExecCmd("ls", []string{"-al"})
|
||||
func ExecCmd(binName string, args []string, workDir ...string) (string, error) {
|
||||
return common.ExecCmd(binName, args, workDir...)
|
||||
}
|
||||
|
||||
// ShellExec exec command by shell
|
||||
// cmdLine e.g. "ls -al"
|
||||
func ShellExec(cmdLine string, shells ...string) (string, error) {
|
||||
return common.ShellExec(cmdLine, shells...)
|
||||
}
|
||||
|
||||
// CurrentShell get current used shell env file.
|
||||
//
|
||||
// eg "/bin/zsh" "/bin/bash".
|
||||
// if onlyName=true, will return "zsh", "bash"
|
||||
func CurrentShell(onlyName bool) (binPath string) {
|
||||
return common.CurrentShell(onlyName)
|
||||
}
|
||||
|
||||
// HasShellEnv has shell env check.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// HasShellEnv("sh")
|
||||
// HasShellEnv("bash")
|
||||
func HasShellEnv(shell string) bool {
|
||||
return common.HasShellEnv(shell)
|
||||
}
|
||||
|
||||
// BuildOptionHelpName for render flag help
|
||||
func BuildOptionHelpName(names []string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
size := len(names) - 1
|
||||
for i, name := range names {
|
||||
sb.WriteByte('-')
|
||||
if len(name) > 1 {
|
||||
sb.WriteByte('-')
|
||||
}
|
||||
|
||||
sb.WriteString(name)
|
||||
if i < size {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ShellQuote quote a string on contains ', ", SPACE
|
||||
func ShellQuote(s string) string {
|
||||
var quote byte
|
||||
if strings.ContainsRune(s, '"') {
|
||||
quote = '\''
|
||||
} else if s == "" || strings.ContainsRune(s, '\'') || strings.ContainsRune(s, ' ') {
|
||||
quote = '"'
|
||||
}
|
||||
|
||||
if quote > 0 {
|
||||
ln := len(s) + 2
|
||||
bs := make([]byte, ln)
|
||||
|
||||
bs[0] = quote
|
||||
bs[ln-1] = quote
|
||||
if ln > 2 {
|
||||
copy(bs[1:ln-1], s)
|
||||
}
|
||||
|
||||
s = string(bs)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// OutputLines split output to lines
|
||||
func OutputLines(output string) []string {
|
||||
output = strings.TrimSuffix(output, "\n")
|
||||
if output == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(output, "\n")
|
||||
}
|
||||
|
||||
// FirstLine from command output
|
||||
//
|
||||
// Deprecated: please use nstr.FirstLine
|
||||
var FirstLine = nstr.FirstLine
|
@ -0,0 +1,144 @@
|
||||
package ncli_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/ncli"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCurrentShell(t *testing.T) {
|
||||
path := ncli.CurrentShell(true)
|
||||
|
||||
if path != "" {
|
||||
assert.NotEmpty(t, path)
|
||||
assert.True(t, ncli.HasShellEnv(path))
|
||||
|
||||
path = ncli.CurrentShell(false)
|
||||
assert.NotEmpty(t, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecCmd(t *testing.T) {
|
||||
ret, err := ncli.ExecCmd("cmd", []string{"/c", "echo", "OK"})
|
||||
assert.NoErr(t, err)
|
||||
// *nix: "OK\n" win: "OK\r\n"
|
||||
assert.Eq(t, "OK", strings.TrimSpace(ret))
|
||||
|
||||
ret, err = ncli.ExecCommand("cmd", []string{"/c", "echo", "OK1"})
|
||||
assert.NoErr(t, err)
|
||||
assert.Eq(t, "OK1", strings.TrimSpace(ret))
|
||||
|
||||
ret, err = ncli.QuickExec("cmd /c echo OK2")
|
||||
assert.NoErr(t, err)
|
||||
assert.Eq(t, "OK2", strings.TrimSpace(ret))
|
||||
|
||||
ret, err = ncli.ExecLine("cmd /c echo OK3")
|
||||
assert.NoErr(t, err)
|
||||
assert.Eq(t, "OK3", strings.TrimSpace(ret))
|
||||
}
|
||||
|
||||
func TestShellExec(t *testing.T) {
|
||||
ret, err := ncli.ShellExec("echo OK")
|
||||
assert.NoErr(t, err)
|
||||
// *nix: "OK\n" win: "OK\r\n"
|
||||
assert.Eq(t, "OK", strings.TrimSpace(ret))
|
||||
|
||||
ret, err = ncli.ShellExec("echo OK", "powershell")
|
||||
assert.NoErr(t, err)
|
||||
assert.Eq(t, "OK", strings.TrimSpace(ret))
|
||||
}
|
||||
|
||||
func TestLineBuild(t *testing.T) {
|
||||
s := ncli.LineBuild("myapp", []string{"-a", "val0", "arg0"})
|
||||
assert.Eq(t, "myapp -a val0 arg0", s)
|
||||
|
||||
s = ncli.BuildLine("./myapp", []string{
|
||||
"-a", "val0",
|
||||
"-m", "this is message",
|
||||
"arg0",
|
||||
})
|
||||
assert.Eq(t, `./myapp -a val0 -m "this is message" arg0`, s)
|
||||
}
|
||||
|
||||
func TestParseLine(t *testing.T) {
|
||||
args := ncli.ParseLine(`./app top sub -a ddd --xx "msg"`)
|
||||
assert.Len(t, args, 7)
|
||||
assert.Eq(t, "msg", args[6])
|
||||
|
||||
args = ncli.String2OSArgs(`./app top sub --msg "has inner 'quote'"`)
|
||||
//dump.P(args)
|
||||
assert.Len(t, args, 5)
|
||||
assert.Eq(t, "has inner 'quote'", args[4])
|
||||
|
||||
// exception line string.
|
||||
args = ncli.ParseLine(`./app top sub -a ddd --xx msg"`)
|
||||
// dump.P(args)
|
||||
assert.Len(t, args, 7)
|
||||
assert.Eq(t, "msg\"", args[6])
|
||||
|
||||
args = ncli.StringToOSArgs(`./app top sub -a ddd --xx "msg "text"`)
|
||||
// dump.P(args)
|
||||
assert.Len(t, args, 7)
|
||||
assert.Eq(t, "msg \"text", args[6])
|
||||
}
|
||||
|
||||
func TestWorkdir(t *testing.T) {
|
||||
assert.NotEmpty(t, ncli.Workdir())
|
||||
assert.NotEmpty(t, ncli.BinDir())
|
||||
assert.NotEmpty(t, ncli.BinFile())
|
||||
assert.NotEmpty(t, ncli.BinName())
|
||||
}
|
||||
|
||||
func TestColorPrint(t *testing.T) {
|
||||
// code gen by: kite gen parse ncli/_demo/gen-code.tpl
|
||||
ncli.Redp("p:red color message, ")
|
||||
ncli.Redf("f:%s color message, ", "red")
|
||||
ncli.Redln("ln:red color message print in cli.")
|
||||
ncli.Bluep("p:blue color message, ")
|
||||
ncli.Bluef("f:%s color message, ", "blue")
|
||||
ncli.Blueln("ln:blue color message print in cli.")
|
||||
ncli.Cyanp("p:cyan color message, ")
|
||||
ncli.Cyanf("f:%s color message, ", "cyan")
|
||||
ncli.Cyanln("ln:cyan color message print in cli.")
|
||||
ncli.Grayp("p:gray color message, ")
|
||||
ncli.Grayf("f:%s color message, ", "gray")
|
||||
ncli.Grayln("ln:gray color message print in cli.")
|
||||
ncli.Greenp("p:green color message, ")
|
||||
ncli.Greenf("f:%s color message, ", "green")
|
||||
ncli.Greenln("ln:green color message print in cli.")
|
||||
ncli.Yellowp("p:yellow color message, ")
|
||||
ncli.Yellowf("f:%s color message, ", "yellow")
|
||||
ncli.Yellowln("ln:yellow color message print in cli.")
|
||||
ncli.Magentap("p:magenta color message, ")
|
||||
ncli.Magentaf("f:%s color message, ", "magenta")
|
||||
ncli.Magentaln("ln:magenta color message print in cli.")
|
||||
|
||||
ncli.Infop("p:info color message, ")
|
||||
ncli.Infof("f:%s color message, ", "info")
|
||||
ncli.Infoln("ln:info color message print in cli.")
|
||||
ncli.Successp("p:success color message, ")
|
||||
ncli.Successf("f:%s color message, ", "success")
|
||||
ncli.Successln("ln:success color message print in cli.")
|
||||
ncli.Warnp("p:warn color message, ")
|
||||
ncli.Warnf("f:%s color message, ", "warn")
|
||||
ncli.Warnln("ln:warn color message print in cli.")
|
||||
ncli.Errorp("p:error color message, ")
|
||||
ncli.Errorf("f:%s color message, ", "error")
|
||||
ncli.Errorln("ln:error color message print in cli.")
|
||||
}
|
||||
|
||||
func TestBuildOptionHelpName(t *testing.T) {
|
||||
assert.Eq(t, "-a, -b", ncli.BuildOptionHelpName([]string{"a", "b"}))
|
||||
assert.Eq(t, "-h, --help", ncli.BuildOptionHelpName([]string{"h", "help"}))
|
||||
}
|
||||
|
||||
func TestShellQuote(t *testing.T) {
|
||||
assert.Eq(t, `"'"`, ncli.ShellQuote("'"))
|
||||
assert.Eq(t, `""`, ncli.ShellQuote(""))
|
||||
assert.Eq(t, `" "`, ncli.ShellQuote(" "))
|
||||
assert.Eq(t, `"ab s"`, ncli.ShellQuote("ab s"))
|
||||
assert.Eq(t, `"ab's"`, ncli.ShellQuote("ab's"))
|
||||
assert.Eq(t, `'ab"s'`, ncli.ShellQuote(`ab"s`))
|
||||
assert.Eq(t, "abs", ncli.ShellQuote("abs"))
|
||||
}
|
@ -0,0 +1,411 @@
|
||||
package ncrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/des"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ErrUnPadding error
|
||||
var ErrUnPadding = errors.New("un-padding decrypted data fail")
|
||||
|
||||
func GenerateAesKey(key []byte, size int) []byte {
|
||||
genKey := make([]byte, size)
|
||||
copy(genKey, key)
|
||||
for i := size; i < len(key); {
|
||||
for j := 0; j < size && i < len(key); j, i = j+1, i+1 {
|
||||
genKey[j] ^= key[i]
|
||||
}
|
||||
}
|
||||
return genKey
|
||||
}
|
||||
|
||||
func GenerateDesKey(key []byte) []byte {
|
||||
genKey := make([]byte, 8)
|
||||
copy(genKey, key)
|
||||
for i := 8; i < len(key); {
|
||||
for j := 0; j < 8 && i < len(key); j, i = j+1, i+1 {
|
||||
genKey[j] ^= key[i]
|
||||
}
|
||||
}
|
||||
return genKey
|
||||
}
|
||||
|
||||
// PKCS5Padding input data
|
||||
func PKCS5Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padText...)
|
||||
}
|
||||
|
||||
// PKCS5UnPadding input data
|
||||
func PKCS5UnPadding(src []byte) ([]byte, error) {
|
||||
length := len(src)
|
||||
delLen := int(src[length-1])
|
||||
|
||||
if delLen > length {
|
||||
return nil, ErrUnPadding
|
||||
}
|
||||
|
||||
// fix: 检查删除的填充是否是一样的字符,不一样说明 delLen 值是有问题的,无法解码
|
||||
if delLen > 1 && src[length-1] != src[length-2] {
|
||||
return nil, ErrUnPadding
|
||||
}
|
||||
|
||||
return src[:length-delLen], nil
|
||||
}
|
||||
|
||||
// PKCS7Padding input data
|
||||
func PKCS7Padding(src []byte, blockSize int) []byte {
|
||||
return PKCS5Padding(src, blockSize)
|
||||
}
|
||||
|
||||
// PKCS7UnPadding input data
|
||||
func PKCS7UnPadding(src []byte) ([]byte, error) {
|
||||
return PKCS5UnPadding(src)
|
||||
}
|
||||
|
||||
// AesEcbEncrypt encrypt data with key use AES ECB algorithm
|
||||
// len(key) should be 16, 24 or 32.
|
||||
func AesEcbEncrypt(data, key []byte) []byte {
|
||||
size := len(key)
|
||||
if size != 16 && size != 24 && size != 32 {
|
||||
panic("key length shoud be 16 or 24 or 32")
|
||||
}
|
||||
|
||||
length := (len(data) + aes.BlockSize) / aes.BlockSize
|
||||
plain := make([]byte, length*aes.BlockSize)
|
||||
|
||||
copy(plain, data)
|
||||
|
||||
pad := byte(len(plain) - len(data))
|
||||
for i := len(data); i < len(plain); i++ {
|
||||
plain[i] = pad
|
||||
}
|
||||
|
||||
encrypted := make([]byte, len(plain))
|
||||
cipher, _ := aes.NewCipher(GenerateAesKey(key, size))
|
||||
|
||||
for bs, be := 0, cipher.BlockSize(); bs <= len(data); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
|
||||
cipher.Encrypt(encrypted[bs:be], plain[bs:be])
|
||||
}
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// AesEcbDecrypt decrypt data with key use AES ECB algorithm
|
||||
// len(key) should be 16, 24 or 32.
|
||||
func AesEcbDecrypt(encrypted, key []byte) []byte {
|
||||
size := len(key)
|
||||
if size != 16 && size != 24 && size != 32 {
|
||||
panic("key length should be 16 or 24 or 32")
|
||||
}
|
||||
cipher, _ := aes.NewCipher(GenerateAesKey(key, size))
|
||||
decrypted := make([]byte, len(encrypted))
|
||||
|
||||
for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
|
||||
cipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
|
||||
}
|
||||
|
||||
trim := 0
|
||||
if len(decrypted) > 0 {
|
||||
trim = len(decrypted) - int(decrypted[len(decrypted)-1])
|
||||
}
|
||||
|
||||
return decrypted[:trim]
|
||||
}
|
||||
|
||||
// AesCbcEncrypt encrypt data with key use AES CBC algorithm
|
||||
// len(key) should be 16, 24 or 32.
|
||||
func AesCbcEncrypt(data, key []byte) []byte {
|
||||
block, _ := aes.NewCipher(key)
|
||||
data = PKCS7Padding(data, block.BlockSize())
|
||||
|
||||
encrypted := make([]byte, aes.BlockSize+len(data))
|
||||
iv := encrypted[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(encrypted[aes.BlockSize:], data)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// AesCbcDecrypt decrypt data with key use AES CBC algorithm
|
||||
// len(key) should be 16, 24 or 32.
|
||||
func AesCbcDecrypt(encrypted, key []byte) ([]byte, error) {
|
||||
block, _ := aes.NewCipher(key)
|
||||
|
||||
iv := encrypted[:aes.BlockSize]
|
||||
encrypted = encrypted[aes.BlockSize:]
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(encrypted, encrypted)
|
||||
|
||||
return PKCS7UnPadding(encrypted)
|
||||
}
|
||||
|
||||
// AesCtrCrypt encrypt data with key use AES CTR algorithm
|
||||
// len(key) should be 16, 24 or 32.
|
||||
func AesCtrCrypt(data, key []byte) []byte {
|
||||
block, _ := aes.NewCipher(key)
|
||||
|
||||
iv := bytes.Repeat([]byte("1"), block.BlockSize())
|
||||
stream := cipher.NewCTR(block, iv)
|
||||
|
||||
dst := make([]byte, len(data))
|
||||
stream.XORKeyStream(dst, data)
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// AesCfbEncrypt encrypt data with key use AES CFB algorithm
|
||||
// len(key) should be 16, 24 or 32.
|
||||
func AesCfbEncrypt(data, key []byte) []byte {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
encrypted := make([]byte, aes.BlockSize+len(data))
|
||||
iv := encrypted[:aes.BlockSize]
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(encrypted[aes.BlockSize:], data)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// AesCfbDecrypt decrypt data with key use AES CFB algorithm
|
||||
// len(encrypted) should be greater than 16, len(key) should be 16, 24 or 32.
|
||||
func AesCfbDecrypt(encrypted, key []byte) []byte {
|
||||
if len(encrypted) < aes.BlockSize {
|
||||
panic("encrypted data is too short")
|
||||
}
|
||||
|
||||
block, _ := aes.NewCipher(key)
|
||||
iv := encrypted[:aes.BlockSize]
|
||||
encrypted = encrypted[aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
|
||||
stream.XORKeyStream(encrypted, encrypted)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// AesOfbEncrypt encrypt data with key use AES OFB algorithm
|
||||
// len(key) should be 16, 24 or 32.
|
||||
func AesOfbEncrypt(data, key []byte) []byte {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data = PKCS7Padding(data, aes.BlockSize)
|
||||
encrypted := make([]byte, aes.BlockSize+len(data))
|
||||
iv := encrypted[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stream := cipher.NewOFB(block, iv)
|
||||
stream.XORKeyStream(encrypted[aes.BlockSize:], data)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// AesOfbDecrypt decrypt data with key use AES OFB algorithm
|
||||
// len(key) should be 16, 24 or 32.
|
||||
func AesOfbDecrypt(data, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
iv := data[:aes.BlockSize]
|
||||
data = data[aes.BlockSize:]
|
||||
if len(data)%aes.BlockSize != 0 {
|
||||
return nil, errors.New("data must % 16")
|
||||
}
|
||||
|
||||
decrypted := make([]byte, len(data))
|
||||
mode := cipher.NewOFB(block, iv)
|
||||
mode.XORKeyStream(decrypted, data)
|
||||
|
||||
return PKCS7UnPadding(decrypted)
|
||||
}
|
||||
|
||||
// DesEcbEncrypt encrypt data with key use DES ECB algorithm
|
||||
// len(key) should be 8.
|
||||
func DesEcbEncrypt(data, key []byte) []byte {
|
||||
length := (len(data) + des.BlockSize) / des.BlockSize
|
||||
plain := make([]byte, length*des.BlockSize)
|
||||
copy(plain, data)
|
||||
|
||||
pad := byte(len(plain) - len(data))
|
||||
for i := len(data); i < len(plain); i++ {
|
||||
plain[i] = pad
|
||||
}
|
||||
|
||||
encrypted := make([]byte, len(plain))
|
||||
cipher, _ := des.NewCipher(GenerateDesKey(key))
|
||||
|
||||
for bs, be := 0, cipher.BlockSize(); bs <= len(data); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
|
||||
cipher.Encrypt(encrypted[bs:be], plain[bs:be])
|
||||
}
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// DesEcbDecrypt decrypt data with key use DES ECB algorithm
|
||||
// len(key) should be 8.
|
||||
func DesEcbDecrypt(encrypted, key []byte) []byte {
|
||||
cipher, _ := des.NewCipher(GenerateDesKey(key))
|
||||
decrypted := make([]byte, len(encrypted))
|
||||
|
||||
for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
|
||||
cipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
|
||||
}
|
||||
|
||||
trim := 0
|
||||
if len(decrypted) > 0 {
|
||||
trim = len(decrypted) - int(decrypted[len(decrypted)-1])
|
||||
}
|
||||
|
||||
return decrypted[:trim]
|
||||
}
|
||||
|
||||
// DesCbcEncrypt encrypt data with key use DES CBC algorithm
|
||||
// len(key) should be 8.
|
||||
func DesCbcEncrypt(data, key []byte) []byte {
|
||||
block, _ := des.NewCipher(key)
|
||||
data = PKCS7Padding(data, block.BlockSize())
|
||||
|
||||
encrypted := make([]byte, des.BlockSize+len(data))
|
||||
iv := encrypted[:des.BlockSize]
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(encrypted[des.BlockSize:], data)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// DesCbcDecrypt decrypt data with key use DES CBC algorithm
|
||||
// len(key) should be 8.
|
||||
func DesCbcDecrypt(encrypted, key []byte) ([]byte, error) {
|
||||
block, _ := des.NewCipher(key)
|
||||
|
||||
iv := encrypted[:des.BlockSize]
|
||||
encrypted = encrypted[des.BlockSize:]
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(encrypted, encrypted)
|
||||
|
||||
return PKCS7UnPadding(encrypted)
|
||||
}
|
||||
|
||||
// DesCtrCrypt encrypt data with key use DES CTR algorithm
|
||||
// len(key) should be 8.
|
||||
func DesCtrCrypt(data, key []byte) []byte {
|
||||
block, _ := des.NewCipher(key)
|
||||
|
||||
iv := bytes.Repeat([]byte("1"), block.BlockSize())
|
||||
stream := cipher.NewCTR(block, iv)
|
||||
|
||||
dst := make([]byte, len(data))
|
||||
stream.XORKeyStream(dst, data)
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// DesCfbEncrypt encrypt data with key use DES CFB algorithm
|
||||
// len(key) should be 8.
|
||||
func DesCfbEncrypt(data, key []byte) []byte {
|
||||
block, err := des.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
encrypted := make([]byte, des.BlockSize+len(data))
|
||||
iv := encrypted[:des.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(encrypted[des.BlockSize:], data)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// DesCfbDecrypt decrypt data with key use DES CFB algorithm
|
||||
// len(encrypted) should be greater than 16, len(key) should be 8.
|
||||
func DesCfbDecrypt(encrypted, key []byte) []byte {
|
||||
block, _ := des.NewCipher(key)
|
||||
if len(encrypted) < des.BlockSize {
|
||||
panic("encrypted data is too short")
|
||||
}
|
||||
iv := encrypted[:des.BlockSize]
|
||||
encrypted = encrypted[des.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(encrypted, encrypted)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// DesOfbEncrypt encrypt data with key use DES OFB algorithm
|
||||
// len(key) should be 16, 24 or 32.
|
||||
func DesOfbEncrypt(data, key []byte) []byte {
|
||||
block, err := des.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
data = PKCS7Padding(data, des.BlockSize)
|
||||
encrypted := make([]byte, des.BlockSize+len(data))
|
||||
iv := encrypted[:des.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stream := cipher.NewOFB(block, iv)
|
||||
stream.XORKeyStream(encrypted[des.BlockSize:], data)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// DesOfbDecrypt decrypt data with key use DES OFB algorithm
|
||||
// len(key) should be 8.
|
||||
func DesOfbDecrypt(data, key []byte) ([]byte, error) {
|
||||
block, err := des.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
iv := data[:des.BlockSize]
|
||||
data = data[des.BlockSize:]
|
||||
if len(data)%des.BlockSize != 0 {
|
||||
return nil, errors.New("data must % 16")
|
||||
}
|
||||
|
||||
decrypted := make([]byte, len(data))
|
||||
mode := cipher.NewOFB(block, iv)
|
||||
mode.XORKeyStream(decrypted, data)
|
||||
|
||||
return PKCS7UnPadding(decrypted)
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package ncrypt
|
||||
|
||||
import "git.noahlan.cn/noahlan/ntool/nbyte"
|
||||
|
||||
// Base64Encode encode data with base64 encoding.
|
||||
func Base64Encode(s []byte) []byte {
|
||||
return nbyte.B64Encoder.Encode(s)
|
||||
}
|
||||
|
||||
// Base64EncodeStr encode string data with base64 encoding.
|
||||
func Base64EncodeStr(s string) string {
|
||||
bs := Base64Encode([]byte(s))
|
||||
return string(bs)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package ncrypt
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// BcryptEncrypt Bcrypt 加密
|
||||
func BcryptEncrypt(password string, code int) string {
|
||||
if code == 0 {
|
||||
code = bcrypt.DefaultCost
|
||||
}
|
||||
bs, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
// BcryptCheck Bcrypt 检查
|
||||
func BcryptCheck(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package ncrypt
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// HmacMd5 return the hmac hash of string use md5.
|
||||
func HmacMd5(data, key string) string {
|
||||
h := hmac.New(md5.New, []byte(key))
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum([]byte("")))
|
||||
}
|
||||
|
||||
// HmacSha1 return the hmac hash of string use sha1.
|
||||
func HmacSha1(data, key string) string {
|
||||
h := hmac.New(sha1.New, []byte(key))
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum([]byte("")))
|
||||
}
|
||||
|
||||
// HmacSha256 return the hmac hash of string use sha256.
|
||||
func HmacSha256(data, key string) string {
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum([]byte("")))
|
||||
}
|
||||
|
||||
// HmacSha512 return the hmac hash of string use sha512.
|
||||
func HmacSha512(data, key string) string {
|
||||
h := hmac.New(sha512.New, []byte(key))
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum([]byte("")))
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package ncrypt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"git.noahlan.cn/noahlan/ntool/nbyte"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Md5Bytes return the md5 value of bytes.
|
||||
func Md5Bytes(b any) []byte {
|
||||
return nbyte.Md5(b)
|
||||
}
|
||||
|
||||
// Md5String return the md5 value of string.
|
||||
func Md5String(s any) string {
|
||||
return hex.EncodeToString(Md5Bytes(s))
|
||||
}
|
||||
|
||||
// Md5File return the md5 value of file.
|
||||
func Md5File(filename string) (string, error) {
|
||||
if fileInfo, err := os.Stat(filename); err != nil {
|
||||
return "", err
|
||||
} else if fileInfo.IsDir() {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := md5.New()
|
||||
chunkSize := 65536
|
||||
for buf, reader := make([]byte, chunkSize), bufio.NewReader(file); ; {
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
hash.Write(buf[:n])
|
||||
}
|
||||
|
||||
checksum := fmt.Sprintf("%x", hash.Sum(nil))
|
||||
return checksum, nil
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package ncrypt
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
)
|
||||
|
||||
// GenerateRsaKey create rsa private and public pemo file.
|
||||
func GenerateRsaKey(keySize int, priKeyFile, pubKeyFile string) error {
|
||||
// private key
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
derText := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
|
||||
block := pem.Block{
|
||||
Type: "rsa private key",
|
||||
Bytes: derText,
|
||||
}
|
||||
|
||||
file, err := os.Create(priKeyFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = pem.Encode(file, &block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
// public key
|
||||
publicKey := privateKey.PublicKey
|
||||
|
||||
derpText, err := x509.MarshalPKIXPublicKey(&publicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
block = pem.Block{
|
||||
Type: "rsa public key",
|
||||
Bytes: derpText,
|
||||
}
|
||||
|
||||
file, err = os.Create(pubKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = pem.Encode(file, &block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RsaEncrypt encrypt data with ras algorithm.
|
||||
func RsaEncrypt(data []byte, pubKeyFileName string) []byte {
|
||||
file, err := os.Open(pubKeyFileName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
buf := make([]byte, fileInfo.Size())
|
||||
|
||||
_, err = file.Read(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(buf)
|
||||
|
||||
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pubKey := pubInterface.(*rsa.PublicKey)
|
||||
|
||||
cipherText, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cipherText
|
||||
}
|
||||
|
||||
// RsaDecrypt decrypt data with ras algorithm.
|
||||
func RsaDecrypt(data []byte, privateKeyFileName string) []byte {
|
||||
file, err := os.Open(privateKeyFileName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf := make([]byte, fileInfo.Size())
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Read(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(buf)
|
||||
|
||||
priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
plainText, err := rsa.DecryptPKCS1v15(rand.Reader, priKey, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return plainText
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package ncrypt
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// Sha1 return the sha1 value (SHA-1 hash algorithm) of string.
|
||||
func Sha1(data string) string {
|
||||
s := sha1.New()
|
||||
s.Write([]byte(data))
|
||||
return hex.EncodeToString(s.Sum([]byte("")))
|
||||
}
|
||||
|
||||
// Sha256 return the sha256 value (SHA256 hash algorithm) of string.
|
||||
func Sha256(data string) string {
|
||||
s := sha256.New()
|
||||
s.Write([]byte(data))
|
||||
return hex.EncodeToString(s.Sum([]byte("")))
|
||||
}
|
||||
|
||||
// Sha512 return the sha512 value (SHA512 hash algorithm) of string.
|
||||
func Sha512(data string) string {
|
||||
s := sha512.New()
|
||||
s.Write([]byte(data))
|
||||
return hex.EncodeToString(s.Sum([]byte("")))
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package ndef
|
||||
|
||||
// const for compare operation
|
||||
const (
|
||||
OpEq = "="
|
||||
OpNeq = "!="
|
||||
OpLt = "<"
|
||||
OpLte = "<="
|
||||
OpGt = ">"
|
||||
OpGte = ">="
|
||||
)
|
||||
|
||||
// const quote chars
|
||||
const (
|
||||
SingleQuote = '\''
|
||||
DoubleQuote = '"'
|
||||
SlashQuote = '\\'
|
||||
|
||||
SingleQuoteStr = "'"
|
||||
DoubleQuoteStr = `"`
|
||||
SlashQuoteStr = "\\"
|
||||
)
|
||||
|
||||
// NoIdx invalid index or length
|
||||
const NoIdx = -1
|
||||
|
||||
// const VarPathReg = `(\w[\w-]*(?:\.[\w-]+)*)`
|
@ -0,0 +1,6 @@
|
||||
package ndef
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrConvType error
|
||||
var ErrConvType = errors.New("convert value type error")
|
@ -0,0 +1,56 @@
|
||||
package ndef
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
nio "git.noahlan.cn/noahlan/ntool/nstd/io"
|
||||
"io"
|
||||
)
|
||||
|
||||
// DataFormatter interface
|
||||
type DataFormatter interface {
|
||||
Format() string
|
||||
FormatTo(w io.Writer)
|
||||
}
|
||||
|
||||
// BaseFormatter struct
|
||||
type BaseFormatter struct {
|
||||
ow nio.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 build and get
|
||||
func (f *BaseFormatter) BsWriter() nio.ByteStringWriter {
|
||||
if f.ow == nil {
|
||||
if f.Out == nil {
|
||||
f.ow = new(bytes.Buffer)
|
||||
} else if ow, ok := f.Out.(nio.ByteStringWriter); ok {
|
||||
f.ow = ow
|
||||
} else {
|
||||
f.ow = nio.NewWriteWrapper(f.Out)
|
||||
}
|
||||
}
|
||||
|
||||
return f.ow
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package ndef
|
||||
|
||||
type (
|
||||
MarshalFunc func(v any) ([]byte, error)
|
||||
UnmarshalFunc func(data []byte, v any) error
|
||||
|
||||
Marshaler interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
}
|
||||
|
||||
Unmarshaler interface {
|
||||
Unmarshal(data []byte, v any) error
|
||||
}
|
||||
|
||||
Serializer interface {
|
||||
Marshaler
|
||||
Unmarshaler
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
MarshalerWrapper struct {
|
||||
Marshaler
|
||||
}
|
||||
UnmarshalerWrapper struct {
|
||||
Unmarshaler
|
||||
}
|
||||
SerializerWrapper struct {
|
||||
Marshaler
|
||||
Unmarshaler
|
||||
}
|
||||
)
|
||||
|
||||
// NewSerializerWrapper 序列化器包装,用于将序列化/反序列化包装为一个独立结构
|
||||
func NewSerializerWrapper(marshaler Marshaler, unmarshaler Unmarshaler) Serializer {
|
||||
return &SerializerWrapper{Marshaler: marshaler, Unmarshaler: unmarshaler}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package ndef
|
||||
|
||||
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"
|
||||
)
|
@ -0,0 +1,46 @@
|
||||
package ndef
|
||||
|
||||
// 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. all int or uint types
|
||||
type XInt interface {
|
||||
Int | Uint
|
||||
}
|
||||
|
||||
// Float interface type
|
||||
type Float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
// IntOrFloat interface type. all int and float types
|
||||
type IntOrFloat interface {
|
||||
Int | Float
|
||||
}
|
||||
|
||||
// XIntOrFloat interface type. all int, uint and float types
|
||||
type XIntOrFloat interface {
|
||||
Int | Uint | Float
|
||||
}
|
||||
|
||||
// SortedType interface type.
|
||||
// that supports the operators < <= >= >.
|
||||
//
|
||||
// contains: (x)int, float, ~string types
|
||||
type SortedType interface {
|
||||
Int | Uint | Float | ~string
|
||||
}
|
||||
|
||||
// ScalarType interface type.
|
||||
//
|
||||
// contains: (x)int, float, ~string, ~bool types
|
||||
type ScalarType interface {
|
||||
Int | Uint | Float | ~string | ~bool
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
package nenv
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nsys"
|
||||
"golang.org/x/term"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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 nsys.IsMSys()
|
||||
func IsMSys() bool {
|
||||
return nsys.IsMSys()
|
||||
}
|
||||
|
||||
var detectedWSL bool
|
||||
var detectedWSLContents string
|
||||
|
||||
// IsWSL system env
|
||||
// https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364
|
||||
func IsWSL() bool {
|
||||
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")
|
||||
}
|
||||
|
||||
// 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 nsys.IsConsole()
|
||||
func IsConsole(out io.Writer) bool {
|
||||
return nsys.IsConsole(out)
|
||||
}
|
||||
|
||||
// HasShellEnv has shell env check.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// HasShellEnv("sh")
|
||||
// HasShellEnv("bash")
|
||||
func HasShellEnv(shell string) bool {
|
||||
return nsys.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"
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package nenv
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/internal/common"
|
||||
)
|
||||
|
||||
// Environ like os.Environ, but will returns key-value map[string]string data.
|
||||
func Environ() map[string]string {
|
||||
return common.Environ()
|
||||
}
|
||||
|
||||
// ParseEnvVar parse ENV var value from input string, support default value.
|
||||
//
|
||||
// Format:
|
||||
//
|
||||
// ${var_name} Only var name
|
||||
// ${var_name | default} With default value
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// comfunc.ParseEnvVar("${ APP_NAME }")
|
||||
// comfunc.ParseEnvVar("${ APP_ENV | dev }")
|
||||
func ParseEnvVar(val string, getFn func(string) string) (newVal string) {
|
||||
return common.ParseEnvVar(val, getFn)
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// DirExists reports whether the named directory exists.
|
||||
func DirExists(path string) bool {
|
||||
return IsDir(path)
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
|
||||
// IsAbsPath is abs path.
|
||||
func IsAbsPath(aPath string) bool {
|
||||
if len(aPath) > 0 {
|
||||
if aPath[0] == '/' {
|
||||
return true
|
||||
}
|
||||
return filepath.IsAbs(aPath)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package nfs_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//goland:noinspection GoBoolExpressions
|
||||
func TestNfs_common(t *testing.T) {
|
||||
assert.Eq(t, "", nfs.FileExt("testdata/testjpg"))
|
||||
assert.Eq(t, "", nfs.Suffix("testdata/testjpg"))
|
||||
assert.Eq(t, "", nfs.ExtName("testdata/testjpg"))
|
||||
assert.Eq(t, ".txt", nfs.FileExt("testdata/test.txt"))
|
||||
assert.Eq(t, ".txt", nfs.Suffix("testdata/test.txt"))
|
||||
assert.Eq(t, "txt", nfs.ExtName("testdata/test.txt"))
|
||||
|
||||
// IsZipFile
|
||||
assert.False(t, nfs.IsZipFile("testdata/not-exists-file"))
|
||||
assert.False(t, nfs.IsZipFile("testdata/test.txt"))
|
||||
assert.Eq(t, "test.txt", nfs.PathName("testdata/test.txt"))
|
||||
|
||||
assert.Eq(t, "test.txt", nfs.Name("path/to/test.txt"))
|
||||
assert.Eq(t, "", nfs.Name(""))
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
assert.Eq(t, "path\\to", nfs.Dir("path/to/test.txt"))
|
||||
} else {
|
||||
assert.Eq(t, "path/to", nfs.Dir("path/to/test.txt"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExists(t *testing.T) {
|
||||
assert.False(t, nfs.PathExists(""))
|
||||
assert.False(t, nfs.PathExists("/not-exist"))
|
||||
assert.False(t, nfs.PathExists("/not-exist"))
|
||||
assert.True(t, nfs.PathExists("testdata/test.txt"))
|
||||
assert.True(t, nfs.PathExists("testdata/test.txt"))
|
||||
}
|
||||
|
||||
func TestIsFile(t *testing.T) {
|
||||
assert.False(t, nfs.FileExists(""))
|
||||
assert.False(t, nfs.IsFile(""))
|
||||
assert.False(t, nfs.IsFile("/not-exist"))
|
||||
assert.False(t, nfs.FileExists("/not-exist"))
|
||||
assert.True(t, nfs.IsFile("testdata/test.txt"))
|
||||
assert.True(t, nfs.FileExists("testdata/test.txt"))
|
||||
}
|
||||
|
||||
func TestIsDir(t *testing.T) {
|
||||
assert.False(t, nfs.IsDir(""))
|
||||
assert.False(t, nfs.DirExists(""))
|
||||
assert.False(t, nfs.IsDir("/not-exist"))
|
||||
assert.True(t, nfs.IsDir("testdata"))
|
||||
assert.True(t, nfs.DirExists("testdata"))
|
||||
}
|
||||
|
||||
func TestIsAbsPath(t *testing.T) {
|
||||
assert.True(t, nfs.IsAbsPath("/data/some.txt"))
|
||||
assert.False(t, nfs.IsAbsPath(""))
|
||||
assert.False(t, nfs.IsAbsPath("some.txt"))
|
||||
assert.NoErr(t, nfs.DeleteIfFileExist("/not-exist"))
|
||||
}
|
||||
|
||||
func TestGlobMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
p, s string
|
||||
want bool
|
||||
}{
|
||||
{"a*", "abc", true},
|
||||
{"ab.*.ef", "ab.cd.ef", true},
|
||||
{"ab.*.*", "ab.cd.ef", true},
|
||||
{"ab.cd.*", "ab.cd.ef", true},
|
||||
{"ab.*", "ab.cd.ef", true},
|
||||
{"a*/b", "a/c/b", false},
|
||||
{"a*", "a/c/b", false},
|
||||
{"a**", "a/c/b", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Eq(t, tt.want, nfs.PathMatch(tt.p, tt.s), "case %v", tt)
|
||||
}
|
||||
|
||||
assert.False(t, nfs.PathMatch("ab", "abc"))
|
||||
assert.True(t, nfs.PathMatch("ab*", "abc"))
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/narr"
|
||||
"git.noahlan.cn/noahlan/ntool/nstr"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// SearchNameUp find file/dir name in dirPath or parent dirs,
|
||||
// return the name of directory path
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// repoDir := nfs.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.
|
||||
func WalkDir(dir string, fn fs.WalkDirFunc) error {
|
||||
return filepath.WalkDir(dir, fn)
|
||||
}
|
||||
|
||||
// GlobWithFunc handle matched file
|
||||
//
|
||||
// - TIP: will be not find in subdir.
|
||||
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 !narr.StringsHas(names, ent.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// IncludeSuffix on find
|
||||
func IncludeSuffix(ss ...string) FilterFunc {
|
||||
return func(_ string, ent fs.DirEntry) bool {
|
||||
return nstr.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 !nstr.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: will be not find in subdir.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
// names, _ := d.Readdirnames(-1)
|
||||
// sort.Strings(names)
|
||||
|
||||
des, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ent := range des {
|
||||
filePath := dir + "/" + ent.Name()
|
||||
|
||||
// apply filters
|
||||
if len(filters) > 0 && ApplyFilters(filePath, ent, filters) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := handleFn(filePath, ent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package nfs_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSearchNameUp(t *testing.T) {
|
||||
p := nfs.SearchNameUp("testdata", "finder")
|
||||
assert.NotEmpty(t, p)
|
||||
assert.True(t, strings.HasSuffix(p, "nfs"))
|
||||
|
||||
p = nfs.SearchNameUp("testdata", ".dotdir")
|
||||
assert.NotEmpty(t, p)
|
||||
assert.True(t, strings.HasSuffix(p, "testdata"))
|
||||
|
||||
p = nfs.SearchNameUp("testdata", "test.txt")
|
||||
assert.NotEmpty(t, p)
|
||||
assert.True(t, strings.HasSuffix(p, "testdata"))
|
||||
|
||||
p = nfs.SearchNameUp("testdata", "not-exists")
|
||||
assert.Empty(t, p)
|
||||
}
|
||||
|
||||
type dirEnt struct {
|
||||
typ fs.FileMode
|
||||
isDir bool
|
||||
name string
|
||||
}
|
||||
|
||||
func (d *dirEnt) Name() string {
|
||||
return d.name
|
||||
}
|
||||
|
||||
func (d *dirEnt) IsDir() bool {
|
||||
return d.isDir
|
||||
}
|
||||
|
||||
func (d *dirEnt) Type() fs.FileMode {
|
||||
return d.typ
|
||||
}
|
||||
|
||||
func (d *dirEnt) Info() (fs.FileInfo, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func TestApplyFilters(t *testing.T) {
|
||||
e1 := &dirEnt{name: "some-backup"}
|
||||
f1 := nfs.ExcludeSuffix("-backup")
|
||||
|
||||
assert.False(t, f1("", e1))
|
||||
assert.True(t, nfs.ApplyFilters("", e1, []nfs.FilterFunc{f1}))
|
||||
assert.True(t, nfs.ApplyFilters("", e1, []nfs.FilterFunc{nfs.OnlyFindDir}))
|
||||
assert.False(t, nfs.ApplyFilters("", e1, []nfs.FilterFunc{nfs.OnlyFindFile}))
|
||||
assert.False(t, nfs.ApplyFilters("", e1, []nfs.FilterFunc{nfs.ExcludeDotFile}))
|
||||
assert.False(t, nfs.ApplyFilters("", e1, []nfs.FilterFunc{nfs.IncludeSuffix("-backup")}))
|
||||
assert.True(t, nfs.ApplyFilters("", e1, []nfs.FilterFunc{nfs.ExcludeNames("some-backup")}))
|
||||
}
|
||||
|
||||
func TestFindInDir(t *testing.T) {
|
||||
err := nfs.FindInDir("path-not-exist", nil)
|
||||
assert.NoErr(t, err)
|
||||
|
||||
err = nfs.FindInDir("testdata/test.txt", nil)
|
||||
assert.NoErr(t, err)
|
||||
|
||||
files := make([]string, 0, 8)
|
||||
err = nfs.FindInDir("testdata", func(fPath string, de fs.DirEntry) error {
|
||||
files = append(files, fPath)
|
||||
return nil
|
||||
})
|
||||
|
||||
//dump.P(files)
|
||||
assert.NoErr(t, err)
|
||||
assert.True(t, len(files) > 0)
|
||||
|
||||
files = files[:0]
|
||||
err = nfs.FindInDir("testdata", func(fPath string, de fs.DirEntry) error {
|
||||
files = append(files, fPath)
|
||||
return nil
|
||||
}, func(fPath string, de fs.DirEntry) bool {
|
||||
return !strings.HasPrefix(de.Name(), ".")
|
||||
})
|
||||
assert.NoErr(t, err)
|
||||
assert.True(t, len(files) > 0)
|
||||
|
||||
err = nfs.FindInDir("testdata", func(fPath string, de fs.DirEntry) error {
|
||||
return fmt.Errorf("handle error")
|
||||
})
|
||||
assert.Err(t, err)
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
# finder
|
||||
|
||||
`finder` provide a finding tool for find files or dirs, and with some built-in matchers.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nfs/finder"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ff := finder.NewFinder()
|
||||
ff.AddScan("/tmp", "/usr/local", "/usr/local/share")
|
||||
ff.ExcludeDir("abc", "def").ExcludeFile("*.log", "*.tmp")
|
||||
|
||||
//ss := ff.FindPaths()
|
||||
//dump.P(ss)
|
||||
}
|
||||
```
|
||||
|
@ -0,0 +1,492 @@
|
||||
package finder
|
||||
|
||||
import "strings"
|
||||
|
||||
// commonly dot file and dirs
|
||||
var (
|
||||
CommonlyDotDirs = []string{".git", ".idea", ".vscode", ".svn", ".hg"}
|
||||
CommonlyDotFiles = []string{".gitignore", ".dockerignore", ".npmignore", ".DS_Store", ".env"}
|
||||
)
|
||||
|
||||
// FindFlag type for find result.
|
||||
type FindFlag uint8
|
||||
|
||||
// flags for find result.
|
||||
const (
|
||||
FlagFile FindFlag = iota + 1 // only find files(default)
|
||||
FlagDir
|
||||
FlagBoth = FlagFile | FlagDir
|
||||
)
|
||||
|
||||
// ToFlag convert string to FindFlag
|
||||
func ToFlag(s string) FindFlag {
|
||||
switch strings.ToLower(s) {
|
||||
case "dirs", "dir", "d":
|
||||
return FlagDir
|
||||
case "both", "b":
|
||||
return FlagBoth
|
||||
default:
|
||||
return FlagFile
|
||||
}
|
||||
}
|
||||
|
||||
// Config for finder
|
||||
type Config struct {
|
||||
init bool
|
||||
depth int
|
||||
|
||||
// ScanDirs scan dir paths for find.
|
||||
ScanDirs []string `json:"scan_dirs"`
|
||||
// FindFlags for find result. default is FlagFile
|
||||
FindFlags FindFlag `json:"find_flags"`
|
||||
// MaxDepth for find result. default is 0 - not limit
|
||||
MaxDepth int `json:"max_depth"`
|
||||
// UseAbsPath use abs path for find result. default is false
|
||||
UseAbsPath bool `json:"use_abs_path"`
|
||||
// CacheResult cache result for find result. default is false
|
||||
CacheResult bool `json:"cache_result"`
|
||||
// ExcludeDotDir exclude dot dir. default is true
|
||||
ExcludeDotDir bool `json:"exclude_dot_dir"`
|
||||
// ExcludeDotFile exclude dot dir. default is false
|
||||
ExcludeDotFile bool `json:"exclude_dot_file"`
|
||||
|
||||
// Matchers generic include matchers for file/dir elems
|
||||
Matchers []Matcher
|
||||
// ExMatchers generic exclude matchers for file/dir elems
|
||||
ExMatchers []Matcher
|
||||
// DirMatchers include matchers for dir elems
|
||||
DirMatchers []Matcher
|
||||
// DirExMatchers exclude matchers for dir elems
|
||||
DirExMatchers []Matcher
|
||||
// FileMatchers include matchers for file elems
|
||||
FileMatchers []Matcher
|
||||
// FileExMatchers exclude matchers for file elems
|
||||
FileExMatchers []Matcher
|
||||
|
||||
// commonly settings for build matchers
|
||||
|
||||
// IncludeDirs include dir name list. eg: {"model"}
|
||||
IncludeDirs []string `json:"include_dirs"`
|
||||
// IncludeExts include file ext name list. eg: {".go", ".md"}
|
||||
IncludeExts []string `json:"include_exts"`
|
||||
// IncludeFiles include file name list. eg: {"go.mod"}
|
||||
IncludeFiles []string `json:"include_files"`
|
||||
// IncludePaths include file/dir path list. eg: {"path/to"}
|
||||
IncludePaths []string `json:"include_paths"`
|
||||
// IncludeNames include file/dir name list. eg: {"test", "some.go"}
|
||||
IncludeNames []string `json:"include_names"`
|
||||
|
||||
// ExcludeDirs exclude dir name list. eg: {"test"}
|
||||
ExcludeDirs []string `json:"exclude_dirs"`
|
||||
// ExcludeExts exclude file ext name list. eg: {".go", ".md"}
|
||||
ExcludeExts []string `json:"exclude_exts"`
|
||||
// ExcludeFiles exclude file name list. eg: {"go.mod"}
|
||||
ExcludeFiles []string `json:"exclude_files"`
|
||||
// ExcludePaths exclude file/dir path list. eg: {"path/to"}
|
||||
ExcludePaths []string `json:"exclude_paths"`
|
||||
// ExcludeNames exclude file/dir name list. eg: {"test", "some.go"}
|
||||
ExcludeNames []string `json:"exclude_names"`
|
||||
}
|
||||
|
||||
// NewConfig create a new Config
|
||||
func NewConfig(dirs ...string) *Config {
|
||||
return &Config{
|
||||
ScanDirs: dirs,
|
||||
FindFlags: FlagFile,
|
||||
// with default setting.
|
||||
ExcludeDotDir: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEmptyConfig create a new Config
|
||||
func NewEmptyConfig() *Config {
|
||||
return &Config{FindFlags: FlagFile}
|
||||
}
|
||||
|
||||
// NewFinder create a new Finder by config
|
||||
func (c *Config) NewFinder() *Finder {
|
||||
return NewWithConfig(c.Init())
|
||||
}
|
||||
|
||||
// Init build matchers by config and append to Matchers.
|
||||
func (c *Config) Init() *Config {
|
||||
if c.init {
|
||||
return c
|
||||
}
|
||||
|
||||
// generic matchers
|
||||
if len(c.IncludeNames) > 0 {
|
||||
c.Matchers = append(c.Matchers, MatchNames(c.IncludeNames))
|
||||
}
|
||||
|
||||
if len(c.IncludePaths) > 0 {
|
||||
c.Matchers = append(c.Matchers, MatchPaths(c.IncludePaths))
|
||||
}
|
||||
|
||||
if len(c.ExcludePaths) > 0 {
|
||||
c.ExMatchers = append(c.ExMatchers, MatchPaths(c.ExcludePaths))
|
||||
}
|
||||
|
||||
if len(c.ExcludeNames) > 0 {
|
||||
c.ExMatchers = append(c.ExMatchers, MatchNames(c.ExcludeNames))
|
||||
}
|
||||
|
||||
// dir matchers
|
||||
if len(c.IncludeDirs) > 0 {
|
||||
c.DirMatchers = append(c.DirMatchers, MatchNames(c.IncludeDirs))
|
||||
}
|
||||
|
||||
if len(c.ExcludeDirs) > 0 {
|
||||
c.DirExMatchers = append(c.DirExMatchers, MatchNames(c.ExcludeDirs))
|
||||
}
|
||||
|
||||
// file matchers
|
||||
if len(c.IncludeExts) > 0 {
|
||||
c.FileMatchers = append(c.FileMatchers, MatchExts(c.IncludeExts))
|
||||
}
|
||||
|
||||
if len(c.IncludeFiles) > 0 {
|
||||
c.FileMatchers = append(c.FileMatchers, MatchNames(c.IncludeFiles))
|
||||
}
|
||||
|
||||
if len(c.ExcludeExts) > 0 {
|
||||
c.FileExMatchers = append(c.FileExMatchers, MatchExts(c.ExcludeExts))
|
||||
}
|
||||
|
||||
if len(c.ExcludeFiles) > 0 {
|
||||
c.FileExMatchers = append(c.FileExMatchers, MatchNames(c.ExcludeFiles))
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
//
|
||||
// --------- config for finder ---------
|
||||
//
|
||||
|
||||
// WithConfig on the finder
|
||||
func (f *Finder) WithConfig(c *Config) *Finder {
|
||||
f.c = c
|
||||
return f
|
||||
}
|
||||
|
||||
// ConfigFn the finder. alias of WithConfigFn()
|
||||
func (f *Finder) ConfigFn(fns ...func(c *Config)) *Finder { return f.WithConfigFn(fns...) }
|
||||
|
||||
// WithConfigFn the finder
|
||||
func (f *Finder) WithConfigFn(fns ...func(c *Config)) *Finder {
|
||||
if f.c == nil {
|
||||
f.c = &Config{}
|
||||
}
|
||||
|
||||
for _, fn := range fns {
|
||||
fn(f.c)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// AddScanDirs add source dir for find
|
||||
func (f *Finder) AddScanDirs(dirPaths []string) *Finder {
|
||||
f.c.ScanDirs = append(f.c.ScanDirs, dirPaths...)
|
||||
return f
|
||||
}
|
||||
|
||||
// AddScanDir add source dir for find. alias of AddScanDirs()
|
||||
func (f *Finder) AddScanDir(dirPaths ...string) *Finder { return f.AddScanDirs(dirPaths) }
|
||||
|
||||
// AddScan add source dir for find. alias of AddScanDirs()
|
||||
func (f *Finder) AddScan(dirPaths ...string) *Finder { return f.AddScanDirs(dirPaths) }
|
||||
|
||||
// ScanDir add source dir for find. alias of AddScanDirs()
|
||||
func (f *Finder) ScanDir(dirPaths ...string) *Finder { return f.AddScanDirs(dirPaths) }
|
||||
|
||||
// CacheResult cache result for find result.
|
||||
func (f *Finder) CacheResult(enable ...bool) *Finder {
|
||||
if len(enable) > 0 {
|
||||
f.c.CacheResult = enable[0]
|
||||
} else {
|
||||
f.c.CacheResult = true
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// WithFlags set find flags.
|
||||
func (f *Finder) WithFlags(flags FindFlag) *Finder {
|
||||
f.c.FindFlags = flags
|
||||
return f
|
||||
}
|
||||
|
||||
// WithStrFlag set find flags by string.
|
||||
func (f *Finder) WithStrFlag(s string) *Finder {
|
||||
f.c.FindFlags = ToFlag(s)
|
||||
return f
|
||||
}
|
||||
|
||||
// OnlyFindDir only find dir.
|
||||
func (f *Finder) OnlyFindDir() *Finder { return f.WithFlags(FlagDir) }
|
||||
|
||||
// FileAndDir both find file and dir.
|
||||
func (f *Finder) FileAndDir() *Finder { return f.WithFlags(FlagDir | FlagFile) }
|
||||
|
||||
// UseAbsPath use absolute path for find result. alias of WithUseAbsPath()
|
||||
func (f *Finder) UseAbsPath(enable ...bool) *Finder { return f.WithUseAbsPath(enable...) }
|
||||
|
||||
// WithUseAbsPath use absolute path for find result.
|
||||
func (f *Finder) WithUseAbsPath(enable ...bool) *Finder {
|
||||
if len(enable) > 0 {
|
||||
f.c.UseAbsPath = enable[0]
|
||||
} else {
|
||||
f.c.UseAbsPath = true
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// WithMaxDepth set max depth for find.
|
||||
func (f *Finder) WithMaxDepth(i int) *Finder {
|
||||
f.c.MaxDepth = i
|
||||
return f
|
||||
}
|
||||
|
||||
// IncludeDir include dir names.
|
||||
func (f *Finder) IncludeDir(dirs ...string) *Finder {
|
||||
f.c.IncludeDirs = append(f.c.IncludeDirs, dirs...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithDirName include dir names. alias of IncludeDir()
|
||||
func (f *Finder) WithDirName(dirs ...string) *Finder { return f.IncludeDir(dirs...) }
|
||||
|
||||
// IncludeFile include file names.
|
||||
func (f *Finder) IncludeFile(files ...string) *Finder {
|
||||
f.c.IncludeFiles = append(f.c.IncludeFiles, files...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithFileName include file names. alias of IncludeFile()
|
||||
func (f *Finder) WithFileName(files ...string) *Finder { return f.IncludeFile(files...) }
|
||||
|
||||
// IncludeName include file or dir names.
|
||||
func (f *Finder) IncludeName(names ...string) *Finder {
|
||||
f.c.IncludeNames = append(f.c.IncludeNames, names...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithNames include file or dir names. alias of IncludeName()
|
||||
func (f *Finder) WithNames(names []string) *Finder { return f.IncludeName(names...) }
|
||||
|
||||
// IncludeExt include file exts.
|
||||
func (f *Finder) IncludeExt(exts ...string) *Finder {
|
||||
f.c.IncludeExts = append(f.c.IncludeExts, exts...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithExts include file exts. alias of IncludeExt()
|
||||
func (f *Finder) WithExts(exts []string) *Finder { return f.IncludeExt(exts...) }
|
||||
|
||||
// WithFileExt include file exts. alias of IncludeExt()
|
||||
func (f *Finder) WithFileExt(exts ...string) *Finder { return f.IncludeExt(exts...) }
|
||||
|
||||
// IncludePath include file or dir paths.
|
||||
func (f *Finder) IncludePath(paths ...string) *Finder {
|
||||
f.c.IncludePaths = append(f.c.IncludePaths, paths...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithPaths include file or dir paths. alias of IncludePath()
|
||||
func (f *Finder) WithPaths(paths []string) *Finder { return f.IncludePath(paths...) }
|
||||
|
||||
// WithSubPath include file or dir paths. alias of IncludePath()
|
||||
func (f *Finder) WithSubPath(paths ...string) *Finder { return f.IncludePath(paths...) }
|
||||
|
||||
// ExcludeDir exclude dir names.
|
||||
func (f *Finder) ExcludeDir(dirs ...string) *Finder {
|
||||
f.c.ExcludeDirs = append(f.c.ExcludeDirs, dirs...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithoutDir exclude dir names. alias of ExcludeDir()
|
||||
func (f *Finder) WithoutDir(dirs ...string) *Finder { return f.ExcludeDir(dirs...) }
|
||||
|
||||
// WithoutNames exclude file or dir names.
|
||||
func (f *Finder) WithoutNames(names []string) *Finder {
|
||||
f.c.ExcludeNames = append(f.c.ExcludeNames, names...)
|
||||
return f
|
||||
}
|
||||
|
||||
// ExcludeName exclude file names. alias of WithoutNames()
|
||||
func (f *Finder) ExcludeName(names ...string) *Finder { return f.WithoutNames(names) }
|
||||
|
||||
// ExcludeFile exclude file names.
|
||||
func (f *Finder) ExcludeFile(files ...string) *Finder {
|
||||
f.c.ExcludeFiles = append(f.c.ExcludeFiles, files...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithoutFile exclude file names. alias of ExcludeFile()
|
||||
func (f *Finder) WithoutFile(files ...string) *Finder { return f.ExcludeFile(files...) }
|
||||
|
||||
// ExcludeExt exclude file exts.
|
||||
//
|
||||
// eg: ExcludeExt(".go", ".java")
|
||||
func (f *Finder) ExcludeExt(exts ...string) *Finder {
|
||||
f.c.ExcludeExts = append(f.c.ExcludeExts, exts...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithoutExt exclude file exts. alias of ExcludeExt()
|
||||
func (f *Finder) WithoutExt(exts ...string) *Finder { return f.ExcludeExt(exts...) }
|
||||
|
||||
// WithoutExts exclude file exts. alias of ExcludeExt()
|
||||
func (f *Finder) WithoutExts(exts []string) *Finder { return f.ExcludeExt(exts...) }
|
||||
|
||||
// ExcludePath exclude file paths.
|
||||
func (f *Finder) ExcludePath(paths ...string) *Finder {
|
||||
f.c.ExcludePaths = append(f.c.ExcludePaths, paths...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithoutPath exclude file paths. alias of ExcludePath()
|
||||
func (f *Finder) WithoutPath(paths ...string) *Finder { return f.ExcludePath(paths...) }
|
||||
|
||||
// WithoutPaths exclude file paths. alias of ExcludePath()
|
||||
func (f *Finder) WithoutPaths(paths []string) *Finder { return f.ExcludePath(paths...) }
|
||||
|
||||
// ExcludeDotDir exclude dot dir names. eg: ".idea"
|
||||
func (f *Finder) ExcludeDotDir(exclude ...bool) *Finder {
|
||||
if len(exclude) > 0 {
|
||||
f.c.ExcludeDotDir = exclude[0]
|
||||
} else {
|
||||
f.c.ExcludeDotDir = true
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// WithoutDotDir exclude dot dir names. alias of ExcludeDotDir().
|
||||
func (f *Finder) WithoutDotDir(exclude ...bool) *Finder {
|
||||
return f.ExcludeDotDir(exclude...)
|
||||
}
|
||||
|
||||
// NoDotDir exclude dot dir names. alias of ExcludeDotDir().
|
||||
func (f *Finder) NoDotDir(exclude ...bool) *Finder {
|
||||
return f.ExcludeDotDir(exclude...)
|
||||
}
|
||||
|
||||
// ExcludeDotFile exclude dot dir names. eg: ".gitignore"
|
||||
func (f *Finder) ExcludeDotFile(exclude ...bool) *Finder {
|
||||
if len(exclude) > 0 {
|
||||
f.c.ExcludeDotFile = exclude[0]
|
||||
} else {
|
||||
f.c.ExcludeDotFile = true
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// WithoutDotFile exclude dot dir names. alias of ExcludeDotFile().
|
||||
func (f *Finder) WithoutDotFile(exclude ...bool) *Finder {
|
||||
return f.ExcludeDotFile(exclude...)
|
||||
}
|
||||
|
||||
// NoDotFile exclude dot dir names. alias of ExcludeDotFile().
|
||||
func (f *Finder) NoDotFile(exclude ...bool) *Finder {
|
||||
return f.ExcludeDotFile(exclude...)
|
||||
}
|
||||
|
||||
//
|
||||
// --------- add matchers to finder ---------
|
||||
//
|
||||
|
||||
// Includes add include match matchers
|
||||
func (f *Finder) Includes(fls []Matcher) *Finder {
|
||||
f.c.Matchers = append(f.c.Matchers, fls...)
|
||||
return f
|
||||
}
|
||||
|
||||
// Collect add include match matchers. alias of Includes()
|
||||
func (f *Finder) Collect(fls ...Matcher) *Finder { return f.Includes(fls) }
|
||||
|
||||
// Include add include match matchers. alias of Includes()
|
||||
func (f *Finder) Include(fls ...Matcher) *Finder { return f.Includes(fls) }
|
||||
|
||||
// With add include match matchers. alias of Includes()
|
||||
func (f *Finder) With(fls ...Matcher) *Finder { return f.Includes(fls) }
|
||||
|
||||
// Adds include match matchers. alias of Includes()
|
||||
func (f *Finder) Adds(fls []Matcher) *Finder { return f.Includes(fls) }
|
||||
|
||||
// Add include match matchers. alias of Includes()
|
||||
func (f *Finder) Add(fls ...Matcher) *Finder { return f.Includes(fls) }
|
||||
|
||||
// Excludes add exclude match matchers
|
||||
func (f *Finder) Excludes(fls []Matcher) *Finder {
|
||||
f.c.ExMatchers = append(f.c.ExMatchers, fls...)
|
||||
return f
|
||||
}
|
||||
|
||||
// Exclude add exclude match matchers. alias of Excludes()
|
||||
func (f *Finder) Exclude(fls ...Matcher) *Finder { return f.Excludes(fls) }
|
||||
|
||||
// Without add exclude match matchers. alias of Excludes()
|
||||
func (f *Finder) Without(fls ...Matcher) *Finder { return f.Excludes(fls) }
|
||||
|
||||
// Nots add exclude match matchers. alias of Excludes()
|
||||
func (f *Finder) Nots(fls []Matcher) *Finder { return f.Excludes(fls) }
|
||||
|
||||
// Not add exclude match matchers. alias of Excludes()
|
||||
func (f *Finder) Not(fls ...Matcher) *Finder { return f.Excludes(fls) }
|
||||
|
||||
// WithMatchers add include matchers
|
||||
func (f *Finder) WithMatchers(fls []Matcher) *Finder {
|
||||
f.c.Matchers = append(f.c.Matchers, fls...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithFilter add include matchers
|
||||
func (f *Finder) WithFilter(fls ...Matcher) *Finder { return f.WithMatchers(fls) }
|
||||
|
||||
// MatchFiles add include file matchers
|
||||
func (f *Finder) MatchFiles(fls []Matcher) *Finder {
|
||||
f.c.FileMatchers = append(f.c.FileMatchers, fls...)
|
||||
return f
|
||||
}
|
||||
|
||||
// MatchFile add include file matchers
|
||||
func (f *Finder) MatchFile(fls ...Matcher) *Finder { return f.MatchFiles(fls) }
|
||||
|
||||
// AddFiles add include file matchers
|
||||
func (f *Finder) AddFiles(fls []Matcher) *Finder { return f.MatchFiles(fls) }
|
||||
|
||||
// AddFile add include file matchers
|
||||
func (f *Finder) AddFile(fls ...Matcher) *Finder { return f.MatchFiles(fls) }
|
||||
|
||||
// NotFiles add exclude file matchers
|
||||
func (f *Finder) NotFiles(fls []Matcher) *Finder {
|
||||
f.c.FileExMatchers = append(f.c.FileExMatchers, fls...)
|
||||
return f
|
||||
}
|
||||
|
||||
// NotFile add exclude file matchers
|
||||
func (f *Finder) NotFile(fls ...Matcher) *Finder { return f.NotFiles(fls) }
|
||||
|
||||
// MatchDirs add exclude dir matchers
|
||||
func (f *Finder) MatchDirs(fls []Matcher) *Finder {
|
||||
f.c.DirMatchers = append(f.c.DirMatchers, fls...)
|
||||
return f
|
||||
}
|
||||
|
||||
// MatchDir add exclude dir matchers
|
||||
func (f *Finder) MatchDir(fls ...Matcher) *Finder { return f.MatchDirs(fls) }
|
||||
|
||||
// WithDirs add exclude dir matchers
|
||||
func (f *Finder) WithDirs(fls []Matcher) *Finder { return f.MatchDirs(fls) }
|
||||
|
||||
// WithDir add exclude dir matchers
|
||||
func (f *Finder) WithDir(fls ...Matcher) *Finder { return f.MatchDirs(fls) }
|
||||
|
||||
// NotDirs add exclude dir matchers
|
||||
func (f *Finder) NotDirs(fls []Matcher) *Finder {
|
||||
f.c.DirExMatchers = append(f.c.DirExMatchers, fls...)
|
||||
return f
|
||||
}
|
||||
|
||||
// NotDir add exclude dir matchers
|
||||
func (f *Finder) NotDir(fls ...Matcher) *Finder { return f.NotDirs(fls) }
|
@ -0,0 +1,48 @@
|
||||
package finder
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nstr"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
// Elem of find file/dir path result
|
||||
type Elem interface {
|
||||
fs.DirEntry
|
||||
// Path get file/dir 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 elem struct {
|
||||
fs.DirEntry
|
||||
path string
|
||||
stat fs.FileInfo
|
||||
sErr error
|
||||
}
|
||||
|
||||
// NewElem create a new Elem instance
|
||||
func NewElem(fPath string, ent fs.DirEntry) Elem {
|
||||
return &elem{
|
||||
path: fPath,
|
||||
DirEntry: ent,
|
||||
}
|
||||
}
|
||||
|
||||
// Path get full file/dir path. eg: "/path/to/file.go"
|
||||
func (e *elem) Path() string {
|
||||
return e.path
|
||||
}
|
||||
|
||||
// Info get file info, will cache result
|
||||
func (e *elem) 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 *elem) String() string {
|
||||
return nstr.OrCond(e.IsDir(), "dir: ", "file: ") + e.Path()
|
||||
}
|
@ -0,0 +1,353 @@
|
||||
// Package finder provide a finding tool for find files or dirs,
|
||||
// and with some built-in matchers.
|
||||
package finder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileFinder type alias.
|
||||
type FileFinder = Finder
|
||||
|
||||
// Finder struct
|
||||
type Finder struct {
|
||||
// config for finder
|
||||
c *Config
|
||||
// last error
|
||||
err error
|
||||
// num - founded fs elem number
|
||||
num int
|
||||
// ch - founded fs elem chan
|
||||
ch chan Elem
|
||||
// caches - cache found fs elem. if config.CacheResult is true
|
||||
caches []Elem
|
||||
}
|
||||
|
||||
// New instance with source dir paths.
|
||||
func New(dirs []string) *Finder {
|
||||
c := NewConfig(dirs...)
|
||||
return NewWithConfig(c)
|
||||
}
|
||||
|
||||
// NewFinder new instance with source dir paths.
|
||||
func NewFinder(dirPaths ...string) *Finder { return New(dirPaths) }
|
||||
|
||||
// NewWithConfig new instance with config.
|
||||
func NewWithConfig(c *Config) *Finder {
|
||||
return &Finder{c: c}
|
||||
}
|
||||
|
||||
// NewEmpty new empty Finder instance
|
||||
func NewEmpty() *Finder {
|
||||
return &Finder{c: NewEmptyConfig()}
|
||||
}
|
||||
|
||||
// EmptyFinder new empty Finder instance. alias of NewEmpty()
|
||||
func EmptyFinder() *Finder { return NewEmpty() }
|
||||
|
||||
//
|
||||
// --------- do finding ---------
|
||||
//
|
||||
|
||||
// Find files in given dir paths. will return a channel, you can use it to get the result.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := NewFinder("/path/to/dir").Find()
|
||||
// for el := range f {
|
||||
// fmt.Println(el.Path())
|
||||
// }
|
||||
func (f *Finder) Find() <-chan Elem {
|
||||
f.find()
|
||||
return f.ch
|
||||
}
|
||||
|
||||
// Elems find and return founded file Elem. alias of Find()
|
||||
func (f *Finder) Elems() <-chan Elem { return f.Find() }
|
||||
|
||||
// Results find and return founded file Elem. alias of Find()
|
||||
func (f *Finder) Results() <-chan Elem { return f.Find() }
|
||||
|
||||
// FindNames find and return founded file/dir names.
|
||||
func (f *Finder) FindNames() []string {
|
||||
paths := make([]string, 0, 8*len(f.c.ScanDirs))
|
||||
for el := range f.Find() {
|
||||
paths = append(paths, el.Name())
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// FindPaths find and return founded file/dir paths.
|
||||
func (f *Finder) FindPaths() []string {
|
||||
paths := make([]string, 0, 8*len(f.c.ScanDirs))
|
||||
for el := range f.Find() {
|
||||
paths = append(paths, el.Path())
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// Each founded file or dir Elem.
|
||||
func (f *Finder) Each(fn func(el Elem)) { f.EachElem(fn) }
|
||||
|
||||
// EachElem founded file or dir Elem.
|
||||
func (f *Finder) EachElem(fn func(el Elem)) {
|
||||
f.find()
|
||||
for el := range f.ch {
|
||||
fn(el)
|
||||
}
|
||||
}
|
||||
|
||||
// EachPath founded file paths.
|
||||
func (f *Finder) EachPath(fn func(filePath string)) {
|
||||
f.EachElem(func(el Elem) {
|
||||
fn(el.Path())
|
||||
})
|
||||
}
|
||||
|
||||
// EachFile each file os.File
|
||||
func (f *Finder) EachFile(fn func(file *os.File)) {
|
||||
f.EachElem(func(el Elem) {
|
||||
file, err := os.Open(el.Path())
|
||||
if err == nil {
|
||||
fn(file)
|
||||
} else {
|
||||
f.err = err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// EachStat each file os.FileInfo
|
||||
func (f *Finder) EachStat(fn func(fi os.FileInfo, filePath string)) {
|
||||
f.EachElem(func(el Elem) {
|
||||
fi, err := el.Info()
|
||||
if err == nil {
|
||||
fn(fi, el.Path())
|
||||
} else {
|
||||
f.err = err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// EachContents handle each found file contents
|
||||
func (f *Finder) EachContents(fn func(contents, filePath string)) {
|
||||
f.EachElem(func(el Elem) {
|
||||
bs, err := os.ReadFile(el.Path())
|
||||
if err == nil {
|
||||
fn(string(bs), el.Path())
|
||||
} else {
|
||||
f.err = err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// prepare for find.
|
||||
func (f *Finder) prepare() {
|
||||
f.err = nil
|
||||
f.ch = make(chan Elem, 8)
|
||||
|
||||
if f.CacheNum() == 0 {
|
||||
f.num = 0
|
||||
}
|
||||
|
||||
if f.c == nil {
|
||||
f.c = NewConfig()
|
||||
} else {
|
||||
f.c.Init()
|
||||
}
|
||||
}
|
||||
|
||||
// do finding
|
||||
func (f *Finder) find() {
|
||||
f.prepare()
|
||||
|
||||
go func() {
|
||||
defer close(f.ch)
|
||||
|
||||
// read from caches
|
||||
if f.c.CacheResult && len(f.caches) > 0 {
|
||||
for _, el := range f.caches {
|
||||
f.ch <- el
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// do finding
|
||||
var err error
|
||||
for _, dirPath := range f.c.ScanDirs {
|
||||
if f.c.UseAbsPath {
|
||||
dirPath, err = filepath.Abs(dirPath)
|
||||
if err != nil {
|
||||
f.err = err
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
f.c.depth = 0
|
||||
f.findDir(dirPath, f.c)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// code refer filepath.glob()
|
||||
func (f *Finder) findDir(dirPath string, c *Config) {
|
||||
des, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return // ignore I/O error
|
||||
}
|
||||
|
||||
var ok bool
|
||||
c.depth++
|
||||
for _, ent := range des {
|
||||
name := ent.Name()
|
||||
isDir := ent.IsDir()
|
||||
if name[0] == '.' {
|
||||
if isDir {
|
||||
if c.ExcludeDotDir {
|
||||
continue
|
||||
}
|
||||
} else if c.ExcludeDotFile {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(dirPath, name)
|
||||
el := NewElem(fullPath, ent)
|
||||
|
||||
// apply generic filters
|
||||
if !applyExMatchers(el, c.ExMatchers) {
|
||||
continue
|
||||
}
|
||||
|
||||
// --- dir: apply dir filters
|
||||
if isDir {
|
||||
if !applyExMatchers(el, c.DirExMatchers) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(c.Matchers) > 0 {
|
||||
ok = applyMatchers(el, c.Matchers)
|
||||
if !ok && len(c.DirMatchers) > 0 {
|
||||
ok = applyMatchers(el, c.DirMatchers)
|
||||
}
|
||||
} else {
|
||||
ok = applyMatchers(el, c.DirMatchers)
|
||||
}
|
||||
|
||||
if ok && c.FindFlags&FlagDir > 0 {
|
||||
if c.CacheResult {
|
||||
f.caches = append(f.caches, el)
|
||||
}
|
||||
f.num++
|
||||
f.ch <- el
|
||||
|
||||
if c.FindFlags == FlagDir {
|
||||
continue // only find subdir on ok=false
|
||||
}
|
||||
}
|
||||
|
||||
// find in sub dir.
|
||||
if c.MaxDepth == 0 || c.depth < c.MaxDepth {
|
||||
f.findDir(fullPath, c)
|
||||
c.depth-- // restore depth
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// --- type: file
|
||||
if c.FindFlags&FlagFile == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// apply file filters
|
||||
if !applyExMatchers(el, c.FileExMatchers) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(c.Matchers) > 0 {
|
||||
ok = applyMatchers(el, c.Matchers)
|
||||
if !ok && len(c.FileMatchers) > 0 {
|
||||
ok = applyMatchers(el, c.FileMatchers)
|
||||
}
|
||||
} else {
|
||||
ok = applyMatchers(el, c.FileMatchers)
|
||||
}
|
||||
|
||||
// write to consumer
|
||||
if ok && c.FindFlags&FlagFile > 0 {
|
||||
if c.CacheResult {
|
||||
f.caches = append(f.caches, el)
|
||||
}
|
||||
f.num++
|
||||
f.ch <- el
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyMatchers(el Elem, fls []Matcher) bool {
|
||||
for _, f := range fls {
|
||||
if f.Apply(el) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return len(fls) == 0
|
||||
}
|
||||
|
||||
func applyExMatchers(el Elem, fls []Matcher) bool {
|
||||
for _, f := range fls {
|
||||
if f.Apply(el) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Reset filters config setting and results info.
|
||||
func (f *Finder) Reset() {
|
||||
c := NewConfig(f.c.ScanDirs...)
|
||||
c.ExcludeDotDir = f.c.ExcludeDotDir
|
||||
c.FindFlags = f.c.FindFlags
|
||||
c.MaxDepth = f.c.MaxDepth
|
||||
|
||||
f.c = c
|
||||
f.ResetResult()
|
||||
}
|
||||
|
||||
// ResetResult reset result info.
|
||||
func (f *Finder) ResetResult() {
|
||||
f.num = 0
|
||||
f.err = nil
|
||||
f.ch = make(chan Elem, 8)
|
||||
f.caches = []Elem{}
|
||||
}
|
||||
|
||||
// Num get found elem num. only valid after finding.
|
||||
func (f *Finder) Num() int {
|
||||
return f.num
|
||||
}
|
||||
|
||||
// Err get last error
|
||||
func (f *Finder) Err() error {
|
||||
return f.err
|
||||
}
|
||||
|
||||
// Caches get cached results. only valid after finding.
|
||||
func (f *Finder) Caches() []Elem {
|
||||
return f.caches
|
||||
}
|
||||
|
||||
// CacheNum get
|
||||
func (f *Finder) CacheNum() int {
|
||||
return len(f.caches)
|
||||
}
|
||||
|
||||
// Config get
|
||||
func (f *Finder) Config() Config {
|
||||
return *f.c
|
||||
}
|
||||
|
||||
// String all dir paths
|
||||
func (f *Finder) String() string {
|
||||
return strings.Join(f.c.ScanDirs, ";")
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
package finder_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/nfs/finder"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
_, _ = nfs.PutContents("./testdata/test.txt", "hello, in test.txt")
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestFinder_findFile(t *testing.T) {
|
||||
f := finder.EmptyFinder().
|
||||
ScanDir("./testdata").
|
||||
NoDotFile().
|
||||
NoDotDir().
|
||||
WithoutExt(".jpg").
|
||||
CacheResult()
|
||||
|
||||
assert.Nil(t, f.Err())
|
||||
assert.NotEmpty(t, f.String())
|
||||
assert.Eq(t, 0, f.CacheNum())
|
||||
|
||||
// find paths
|
||||
assert.NotEmpty(t, f.FindPaths())
|
||||
assert.Gt(t, f.CacheNum(), 0)
|
||||
assert.NotEmpty(t, f.Caches())
|
||||
|
||||
f.Each(func(elem finder.Elem) {
|
||||
fmt.Println(elem)
|
||||
})
|
||||
|
||||
t.Run("each elem", func(t *testing.T) {
|
||||
f.EachElem(func(elem finder.Elem) {
|
||||
fmt.Println(elem)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("each file", func(t *testing.T) {
|
||||
f.EachFile(func(file *os.File) {
|
||||
fmt.Println(file.Name())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("each path", func(t *testing.T) {
|
||||
f.EachPath(func(filePath string) {
|
||||
fmt.Println(filePath)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("each stat", func(t *testing.T) {
|
||||
f.EachStat(func(fi os.FileInfo, filePath string) {
|
||||
fmt.Println(filePath, "=>", fi.ModTime())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("reset", func(t *testing.T) {
|
||||
f.Reset()
|
||||
assert.Empty(t, f.Caches())
|
||||
assert.NotEmpty(t, f.FindPaths())
|
||||
|
||||
f.EachElem(func(elem finder.Elem) {
|
||||
fmt.Println(elem)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestFinder_OnlyFindDir(t *testing.T) {
|
||||
ff := finder.NewFinder("./../../").
|
||||
OnlyFindDir().
|
||||
UseAbsPath().
|
||||
WithoutDotDir().
|
||||
WithDirName("testdata")
|
||||
|
||||
ff.EachPath(func(filePath string) {
|
||||
fmt.Println(filePath)
|
||||
})
|
||||
assert.Gt(t, ff.Num(), 0)
|
||||
assert.Eq(t, 0, ff.CacheNum())
|
||||
|
||||
t.Run("each elem", func(t *testing.T) {
|
||||
ff.Each(func(elem finder.Elem) {
|
||||
fmt.Println(elem)
|
||||
})
|
||||
})
|
||||
|
||||
ff.ResetResult()
|
||||
assert.Eq(t, 0, ff.Num())
|
||||
assert.Eq(t, 0, ff.CacheNum())
|
||||
|
||||
t.Run("max depth", func(t *testing.T) {
|
||||
ff.WithMaxDepth(2)
|
||||
ff.EachPath(func(filePath string) {
|
||||
fmt.Println(filePath)
|
||||
})
|
||||
assert.Gt(t, ff.Num(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileFinder_NoDotFile(t *testing.T) {
|
||||
f := finder.NewEmpty().
|
||||
CacheResult().
|
||||
ScanDir("./testdata")
|
||||
assert.NotEmpty(t, f.String())
|
||||
|
||||
fileName := ".env"
|
||||
assert.NotEmpty(t, f.FindPaths())
|
||||
assert.Contains(t, f.FindNames(), fileName)
|
||||
|
||||
f = finder.EmptyFinder().
|
||||
ScanDir("./testdata").
|
||||
NoDotFile()
|
||||
assert.NotContains(t, f.FindNames(), fileName)
|
||||
|
||||
t.Run("Not MatchDotFile", func(t *testing.T) {
|
||||
f = finder.EmptyFinder().
|
||||
ScanDir("./testdata").
|
||||
Not(finder.MatchDotFile())
|
||||
|
||||
assert.NotContains(t, f.FindNames(), fileName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileFinder_IncludeName(t *testing.T) {
|
||||
f := finder.NewFinder(".").
|
||||
IncludeName("elem.go").
|
||||
WithNames([]string{"not-exist.file"})
|
||||
|
||||
names := f.FindNames()
|
||||
assert.Len(t, names, 1)
|
||||
assert.Contains(t, names, "elem.go")
|
||||
assert.NotContains(t, names, "not-exist.file")
|
||||
|
||||
f.Reset()
|
||||
t.Run("name in subdir", func(t *testing.T) {
|
||||
f.WithFileName("test.txt")
|
||||
names = f.FindNames()
|
||||
assert.Len(t, names, 1)
|
||||
assert.Contains(t, names, "test.txt")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileFinder_ExcludeName(t *testing.T) {
|
||||
f := finder.NewEmpty().
|
||||
AddScanDir(".").
|
||||
WithMaxDepth(1).
|
||||
ExcludeName("elem.go").
|
||||
WithoutNames([]string{"config.go"})
|
||||
f.Exclude(finder.MatchSuffix("_test.go"), finder.MatchExt(".md"))
|
||||
|
||||
names := f.FindNames()
|
||||
fmt.Println(names)
|
||||
assert.Contains(t, names, "matcher.go")
|
||||
assert.NotContains(t, names, "elem.go")
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package finder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
)
|
||||
|
||||
// Matcher for match file path.
|
||||
type Matcher interface {
|
||||
// Apply check find elem. return False will skip this file.
|
||||
Apply(elem Elem) bool
|
||||
}
|
||||
|
||||
// MatcherFunc for match file info, return False will skip this file
|
||||
type MatcherFunc func(elem Elem) bool
|
||||
|
||||
// Apply check file path. return False will skip this file.
|
||||
func (fn MatcherFunc) Apply(elem Elem) bool {
|
||||
return fn(elem)
|
||||
}
|
||||
|
||||
// ------------------ Multi matcher wrapper ------------------
|
||||
|
||||
// MultiFilter wrapper for multi matchers
|
||||
type MultiFilter struct {
|
||||
Before Matcher
|
||||
Filters []Matcher
|
||||
}
|
||||
|
||||
// Add matchers
|
||||
func (mf *MultiFilter) Add(fls ...Matcher) {
|
||||
mf.Filters = append(mf.Filters, fls...)
|
||||
}
|
||||
|
||||
// Apply check file path. return False will filter this file.
|
||||
func (mf *MultiFilter) Apply(el Elem) bool {
|
||||
if mf.Before != nil && !mf.Before.Apply(el) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, fl := range mf.Filters {
|
||||
if !fl.Apply(el) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// NewDirFilters create a new dir matchers
|
||||
func NewDirFilters(fls ...Matcher) *MultiFilter {
|
||||
return &MultiFilter{
|
||||
Before: MatchDir,
|
||||
Filters: fls,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFileFilters create a new dir matchers
|
||||
func NewFileFilters(fls ...Matcher) *MultiFilter {
|
||||
return &MultiFilter{
|
||||
Before: MatchFile,
|
||||
Filters: fls,
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------ Body Matcher ------------------
|
||||
|
||||
// BodyFilter for filter file contents.
|
||||
type BodyFilter interface {
|
||||
Apply(filePath string, buf *bytes.Buffer) bool
|
||||
}
|
||||
|
||||
// BodyMatcherFunc for filter file contents.
|
||||
type BodyMatcherFunc func(filePath string, buf *bytes.Buffer) bool
|
||||
|
||||
// Apply for filter file contents.
|
||||
func (fn BodyMatcherFunc) Apply(filePath string, buf *bytes.Buffer) bool {
|
||||
return fn(filePath, buf)
|
||||
}
|
||||
|
||||
// BodyFilters multi body matchers as Matcher
|
||||
type BodyFilters struct {
|
||||
Filters []BodyFilter
|
||||
}
|
||||
|
||||
// NewBodyFilters create a new body matchers
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// bf := finder.NewBodyFilters(
|
||||
// finder.BodyMatcherFunc(func(filePath string, buf *bytes.Buffer) bool {
|
||||
// // filter file contents
|
||||
// return true
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// es := finder.NewFinder('path/to/dir').Add(bf).Elems()
|
||||
// for el := range es {
|
||||
// fmt.Println(el.Path())
|
||||
// }
|
||||
func NewBodyFilters(fls ...BodyFilter) *BodyFilters {
|
||||
return &BodyFilters{
|
||||
Filters: fls,
|
||||
}
|
||||
}
|
||||
|
||||
// AddFilter add matchers
|
||||
func (mf *BodyFilters) AddFilter(fls ...BodyFilter) {
|
||||
mf.Filters = append(mf.Filters, fls...)
|
||||
}
|
||||
|
||||
// Apply check file path. return False will filter this file.
|
||||
func (mf *BodyFilters) Apply(el Elem) bool {
|
||||
if el.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
// read file contents
|
||||
buf := bytes.NewBuffer(nil)
|
||||
file, err := nfs.OpenReadFile(el.Path())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err = buf.ReadFrom(file)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return false
|
||||
}
|
||||
file.Close()
|
||||
|
||||
// apply matchers
|
||||
for _, fl := range mf.Filters {
|
||||
if !fl.Apply(el.Path(), buf) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
@ -0,0 +1,289 @@
|
||||
package finder
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/nmath"
|
||||
"git.noahlan.cn/noahlan/ntool/nstr"
|
||||
"git.noahlan.cn/noahlan/ntool/ntime"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ------------------ built in filters ------------------
|
||||
|
||||
// MatchFile only allow file path.
|
||||
var MatchFile = MatcherFunc(func(el Elem) bool {
|
||||
return !el.IsDir()
|
||||
})
|
||||
|
||||
// MatchDir only allow dir path.
|
||||
var MatchDir = MatcherFunc(func(el Elem) bool {
|
||||
return el.IsDir()
|
||||
})
|
||||
|
||||
// StartWithDot match dot file/dir. eg: ".gitignore"
|
||||
func StartWithDot() MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
name := el.Name()
|
||||
return len(name) > 0 && name[0] == '.'
|
||||
}
|
||||
}
|
||||
|
||||
// MatchDotFile match dot filename. eg: ".idea"
|
||||
func MatchDotFile() MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
return !el.IsDir() && el.Name()[0] == '.'
|
||||
}
|
||||
}
|
||||
|
||||
// MatchDotDir match dot dirname. eg: ".idea"
|
||||
func MatchDotDir() MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
return el.IsDir() && el.Name()[0] == '.'
|
||||
}
|
||||
}
|
||||
|
||||
// MatchExt match filepath by given file ext.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := NewFinder('path/to/dir')
|
||||
// f.Add(MatchExt(".go"))
|
||||
// f.Not(MatchExt(".md"))
|
||||
func MatchExt(exts ...string) MatcherFunc { return MatchExts(exts) }
|
||||
|
||||
// MatchExts filter filepath by given file ext.
|
||||
func MatchExts(exts []string) MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
elExt := path.Ext(el.Name())
|
||||
for _, ext := range exts {
|
||||
if ext == elExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MatchName match filepath by given names.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := NewFinder('path/to/dir')
|
||||
// f.Not(MatchName("README.md", "*_test.go"))
|
||||
func MatchName(names ...string) MatcherFunc { return MatchNames(names) }
|
||||
|
||||
// MatchNames match filepath by given names.
|
||||
func MatchNames(names []string) MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
elName := el.Name()
|
||||
for _, name := range names {
|
||||
if name == elName || nfs.PathMatch(name, elName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MatchPrefix match filepath by check given prefixes.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := NewFinder('path/to/dir')
|
||||
// f.Add(finder.MatchPrefix("app_", "README"))
|
||||
func MatchPrefix(prefixes ...string) MatcherFunc { return MatchPrefixes(prefixes) }
|
||||
|
||||
// MatchPrefixes match filepath by check given prefixes.
|
||||
func MatchPrefixes(prefixes []string) MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
for _, pfx := range prefixes {
|
||||
if strings.HasPrefix(el.Name(), pfx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MatchSuffix match filepath by check path has suffixes.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := NewFinder('path/to/dir')
|
||||
// f.Add(finder.MatchSuffix("util.go", "en.md"))
|
||||
// f.Not(finder.MatchSuffix("_test.go", ".log"))
|
||||
func MatchSuffix(suffixes ...string) MatcherFunc { return MatchSuffixes(suffixes) }
|
||||
|
||||
// MatchSuffixes match filepath by check path has suffixes.
|
||||
func MatchSuffixes(suffixes []string) MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
for _, sfx := range suffixes {
|
||||
if strings.HasSuffix(el.Path(), sfx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MatchPath match file/dir by given sub paths.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := NewFinder('path/to/dir')
|
||||
// f.Add(MatchPath("need/path"))
|
||||
func MatchPath(subPaths []string) MatcherFunc { return MatchPaths(subPaths) }
|
||||
|
||||
// MatchPaths match file/dir by given sub paths.
|
||||
func MatchPaths(subPaths []string) MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
for _, subPath := range subPaths {
|
||||
if strings.Contains(el.Path(), subPath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GlobMatch file/dir name by given patterns.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := NewFinder('path/to/dir')
|
||||
// f.AddFilter(GlobMatch("*_test.go"))
|
||||
func GlobMatch(patterns ...string) MatcherFunc { return GlobMatches(patterns) }
|
||||
|
||||
// GlobMatches file/dir name by given patterns.
|
||||
func GlobMatches(patterns []string) MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
for _, pattern := range patterns {
|
||||
if ok, _ := path.Match(pattern, el.Name()); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// RegexMatch match name by given regex pattern
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := NewFinder('path/to/dir')
|
||||
// f.AddFilter(RegexMatch(`[A-Z]\w+`))
|
||||
func RegexMatch(pattern string) MatcherFunc {
|
||||
reg := regexp.MustCompile(pattern)
|
||||
|
||||
return func(el Elem) bool {
|
||||
return reg.MatchString(el.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// NameLike exclude filepath by given name match.
|
||||
func NameLike(patterns ...string) MatcherFunc { return NameLikes(patterns) }
|
||||
|
||||
// NameLikes filter filepath by given name match.
|
||||
func NameLikes(patterns []string) MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
for _, pattern := range patterns {
|
||||
if nstr.LikeMatch(pattern, el.Name()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// ----------------- built in file info filters -----------------
|
||||
//
|
||||
|
||||
// MatchMtime match file by modify time.
|
||||
//
|
||||
// Note: if time is zero, it will be ignored.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := NewFinder('path/to/dir')
|
||||
// // -600 seconds to now(last 10 minutes)
|
||||
// f.AddFile(MatchMtime(timex.NowAddSec(-600), timex.ZeroTime))
|
||||
// // before 600 seconds(before 10 minutes)
|
||||
// f.AddFile(MatchMtime(timex.ZeroTime, timex.NowAddSec(-600)))
|
||||
func MatchMtime(start, end time.Time) MatcherFunc {
|
||||
return MatchModTime(start, end)
|
||||
}
|
||||
|
||||
// MatchModTime filter file by modify time.
|
||||
func MatchModTime(start, end time.Time) MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
if el.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
fi, err := el.Info()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ntime.InRange(fi.ModTime(), start, end)
|
||||
}
|
||||
}
|
||||
|
||||
var timeNumReg = regexp.MustCompile(`(-?\d+)`)
|
||||
|
||||
// HumanModTime filter file by modify time string.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := EmptyFinder()
|
||||
// f.AddFilter(HumanModTime(">10m")) // before 10 minutes
|
||||
// f.AddFilter(HumanModTime("<10m")) // latest 10 minutes, to Now
|
||||
func HumanModTime(expr string) MatcherFunc {
|
||||
opt := &ntime.ParseRangeOpt{AutoSort: true}
|
||||
// convert > to <, < to >
|
||||
expr = nstr.Replaces(expr, map[string]string{">": "<", "<": ">"})
|
||||
expr = timeNumReg.ReplaceAllStringFunc(expr, func(s string) string {
|
||||
if s[0] == '-' {
|
||||
return s
|
||||
}
|
||||
return "-" + s
|
||||
})
|
||||
|
||||
start, end, err := ntime.ParseRange(expr, opt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return MatchModTime(start, end)
|
||||
}
|
||||
|
||||
// FileSize match file by file size. unit: byte
|
||||
func FileSize(min, max uint64) MatcherFunc { return SizeRange(min, max) }
|
||||
|
||||
// SizeRange match file by file size. unit: byte
|
||||
func SizeRange(min, max uint64) MatcherFunc {
|
||||
return func(el Elem) bool {
|
||||
if el.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
fi, err := el.Info()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return nmath.InUintRange(uint64(fi.Size()), min, max)
|
||||
}
|
||||
}
|
||||
|
||||
// HumanSize match file by file size string. eg: ">1k", "<2m", "1g~3g"
|
||||
func HumanSize(expr string) MatcherFunc {
|
||||
min, max, err := nstr.ParseSizeRange(expr, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return SizeRange(min, max)
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package finder_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nfs/finder"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/mock"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newMockElem(fp string, isDir ...bool) finder.Elem {
|
||||
return finder.NewElem(fp, mock.NewDirEnt(fp, isDir...))
|
||||
}
|
||||
|
||||
func TestFilters_simple(t *testing.T) {
|
||||
el := newMockElem("path/some.txt")
|
||||
fn := finder.MatcherFunc(func(el finder.Elem) bool {
|
||||
return false
|
||||
})
|
||||
|
||||
assert.False(t, fn(el))
|
||||
|
||||
// match name
|
||||
fn = finder.MatchName("some.txt")
|
||||
assert.True(t, fn(el))
|
||||
fn = finder.MatchName("not-exist.txt")
|
||||
assert.False(t, fn(el))
|
||||
|
||||
// MatchExt
|
||||
fn = finder.MatchExt(".txt")
|
||||
assert.True(t, fn(el))
|
||||
fn = finder.MatchExt(".js")
|
||||
assert.False(t, fn(el))
|
||||
|
||||
// MatchSuffix
|
||||
fn = finder.MatchSuffix("me.txt")
|
||||
assert.True(t, fn(el))
|
||||
fn = finder.MatchSuffix("not-exist.txt")
|
||||
assert.False(t, fn(el))
|
||||
}
|
||||
|
||||
func TestRegexMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
filePath string
|
||||
pattern string
|
||||
match bool
|
||||
}{
|
||||
{"path/to/util.go", `\.go$`, true},
|
||||
{"path/to/util.go", `\.md$`, false},
|
||||
{"path/to/util.md", `\.md$`, true},
|
||||
{"path/to/util.md", `\.go$`, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
el := newMockElem(tt.filePath)
|
||||
fn := finder.RegexMatch(tt.pattern)
|
||||
assert.Eq(t, tt.match, fn(el))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchDotDir(t *testing.T) {
|
||||
f := finder.EmptyFinder().
|
||||
WithFlags(finder.FlagBoth).
|
||||
ScanDir("./testdata")
|
||||
|
||||
dirName := ".dotdir"
|
||||
assert.Contains(t, f.FindNames(), dirName)
|
||||
|
||||
t.Run("NoDotDir", func(t *testing.T) {
|
||||
f = finder.EmptyFinder().
|
||||
ScanDir("./testdata").
|
||||
NoDotDir()
|
||||
|
||||
assert.NotContains(t, f.FindNames(), dirName)
|
||||
})
|
||||
|
||||
t.Run("Exclude false", func(t *testing.T) {
|
||||
f = finder.NewEmpty().
|
||||
WithStrFlag("dir").
|
||||
ScanDir("./testdata").
|
||||
ExcludeDotDir(false)
|
||||
|
||||
assert.Contains(t, f.FindNames(), dirName)
|
||||
})
|
||||
}
|
@ -0,0 +1 @@
|
||||
hello, in test.txt
|
@ -0,0 +1,7 @@
|
||||
package nfs
|
||||
|
||||
import "os"
|
||||
|
||||
// CloseOnExec makes sure closing the file on process forking.
|
||||
func CloseOnExec(file *os.File) {
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/internal/common"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Dir get dir path from filepath, without last name.
|
||||
func Dir(fpath string) string { return filepath.Dir(fpath) }
|
||||
|
||||
// PathName get file/dir name from full path
|
||||
func PathName(fpath string) string { return path.Base(fpath) }
|
||||
|
||||
// Name get file/dir name from full path.
|
||||
//
|
||||
// eg: path/to/main.go => main.go
|
||||
func Name(fpath string) string {
|
||||
if fpath == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Base(fpath)
|
||||
}
|
||||
|
||||
// FileExt get filename ext. alias of path.Ext()
|
||||
//
|
||||
// eg: path/to/main.go => ".go"
|
||||
func FileExt(fpath string) string { return path.Ext(fpath) }
|
||||
|
||||
// Ext get filename ext. alias of path.Ext()
|
||||
//
|
||||
// eg: path/to/main.go => ".go"
|
||||
func Ext(fpath string) string {
|
||||
return path.Ext(fpath)
|
||||
}
|
||||
|
||||
// ExtName get filename ext. alias of path.Ext()
|
||||
//
|
||||
// eg: path/to/main.go => "go"
|
||||
func ExtName(fpath string) string {
|
||||
if ext := path.Ext(fpath); len(ext) > 0 {
|
||||
return ext[1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Suffix get filename ext. alias of path.Ext()
|
||||
//
|
||||
// eg: path/to/main.go => ".go"
|
||||
func Suffix(fpath string) string { return path.Ext(fpath) }
|
||||
|
||||
// Expand will parse first `~` as user home dir path.
|
||||
func Expand(pathStr string) string {
|
||||
return common.ExpandHome(pathStr)
|
||||
}
|
||||
|
||||
// ExpandPath will parse `~` as user home dir path.
|
||||
func ExpandPath(pathStr string) string {
|
||||
return common.ExpandHome(pathStr)
|
||||
}
|
||||
|
||||
// ResolvePath will parse `~` and env var in path
|
||||
func ResolvePath(pathStr string) string {
|
||||
pathStr = common.ExpandHome(pathStr)
|
||||
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)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
//go:build !windows
|
||||
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/internal/common"
|
||||
"path"
|
||||
)
|
||||
|
||||
// Realpath returns the shortest path name equivalent to path by purely lexical processing.
|
||||
func Realpath(pathStr string) string {
|
||||
pathStr = common.ExpandHome(pathStr)
|
||||
|
||||
if !IsAbsPath(pathStr) {
|
||||
pathStr = JoinSubPaths(common.Workdir(), pathStr)
|
||||
}
|
||||
return path.Clean(pathStr)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package nfs_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpandPath(t *testing.T) {
|
||||
path := "~/.kite"
|
||||
|
||||
assert.NotEq(t, path, nfs.Expand(path))
|
||||
assert.NotEq(t, path, nfs.ExpandPath(path))
|
||||
assert.NotEq(t, path, nfs.ResolvePath(path))
|
||||
|
||||
assert.Eq(t, "", nfs.Expand(""))
|
||||
assert.Eq(t, "/path/to", nfs.Expand("/path/to"))
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
//go:build windows
|
||||
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/internal/common"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Realpath returns the shortest path name equivalent to path by purely lexical processing.
|
||||
func Realpath(pathStr string) string {
|
||||
pathStr = common.ExpandHome(pathStr)
|
||||
|
||||
if !IsAbsPath(pathStr) {
|
||||
pathStr = JoinSubPaths(common.Workdir(), pathStr)
|
||||
}
|
||||
return filepath.Clean(pathStr)
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"git.noahlan.cn/noahlan/ntool/ngo"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Mkdir alias of os.MkdirAll()
|
||||
func Mkdir(dirPath string, perm os.FileMode) error {
|
||||
return os.MkdirAll(dirPath, perm)
|
||||
}
|
||||
|
||||
// MkDirs batch make multi dirs at once
|
||||
func MkDirs(perm os.FileMode, dirPaths ...string) error {
|
||||
for _, dirPath := range dirPaths {
|
||||
if err := os.MkdirAll(dirPath, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MkSubDirs batch make multi sub-dirs at once
|
||||
func MkSubDirs(perm os.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 quick create parent dir
|
||||
func MkParentDir(fpath string) error {
|
||||
dirPath := filepath.Dir(fpath)
|
||||
if !IsDir(dirPath) {
|
||||
return os.MkdirAll(dirPath, 0775)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ************************************************************
|
||||
// open/create files
|
||||
// ************************************************************
|
||||
|
||||
// some commonly flag const 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
|
||||
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 := path.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 := ngo.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 := ngo.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 := ngo.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 := path.Dir(fpath)
|
||||
if !IsDir(dirPath) {
|
||||
err := os.MkdirAll(dirPath, dirPerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
flag := ngo.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
|
||||
// ************************************************************
|
||||
|
||||
// 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) }
|
||||
|
||||
// RmIfExist removes the named file or (empty) directory on exists.
|
||||
func RmIfExist(fPath string) error { return DeleteIfExist(fPath) }
|
||||
|
||||
// DeleteIfExist removes the named file or (empty) directory on exists.
|
||||
func DeleteIfExist(fPath string) error {
|
||||
if PathExists(fPath) {
|
||||
return os.Remove(fPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RmFileIfExist removes the named file on exists.
|
||||
func RmFileIfExist(fPath string) error { return DeleteIfFileExist(fPath) }
|
||||
|
||||
// DeleteIfFileExist removes the named file on exists.
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"text/scanner"
|
||||
)
|
||||
|
||||
// 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 GetContents(in) }
|
||||
|
||||
// GetContents read contents from path or io.Reader, will panic on in type error
|
||||
func GetContents(in any) []byte {
|
||||
r, err := NewIOReader(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return MustReadReader(r)
|
||||
}
|
||||
|
||||
// ReadOrErr read contents from path or io.Reader, will panic on in type error
|
||||
func ReadOrErr(in any) ([]byte, error) {
|
||||
r, err := NewIOReader(in)
|
||||
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
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// 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)
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package nfs_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDiscardReader(t *testing.T) {
|
||||
sr := strings.NewReader("hello")
|
||||
bs, err := nfs.ReadOrErr(sr)
|
||||
assert.NoErr(t, err)
|
||||
assert.Eq(t, []byte("hello"), bs)
|
||||
|
||||
sr = strings.NewReader("hello")
|
||||
assert.Eq(t, []byte("hello"), nfs.GetContents(sr))
|
||||
|
||||
sr = strings.NewReader("hello")
|
||||
nfs.DiscardReader(sr)
|
||||
|
||||
assert.Empty(t, nfs.ReadReader(sr))
|
||||
assert.Empty(t, nfs.ReadAll(sr))
|
||||
|
||||
}
|
||||
|
||||
func TestGetContents(t *testing.T) {
|
||||
fpath := "./testdata/get-contents.txt"
|
||||
assert.NoErr(t, nfs.RmFileIfExist(fpath))
|
||||
|
||||
_, err := nfs.PutContents(fpath, "hello")
|
||||
assert.NoErr(t, err)
|
||||
|
||||
assert.Nil(t, nfs.ReadExistFile("/path-not-exist"))
|
||||
assert.Eq(t, []byte("hello"), nfs.ReadExistFile(fpath))
|
||||
|
||||
assert.Panics(t, func() {
|
||||
nfs.GetContents(45)
|
||||
})
|
||||
assert.Panics(t, func() {
|
||||
nfs.ReadFile("/path-not-exist")
|
||||
})
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package nfs_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nenv"
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMkdir(t *testing.T) {
|
||||
// TODO windows will error
|
||||
if nenv.IsWin() {
|
||||
t.Skip("skip mkdir test on Windows")
|
||||
return
|
||||
}
|
||||
|
||||
err := os.Chmod("./testdata", os.ModePerm)
|
||||
|
||||
if assert.NoErr(t, err) {
|
||||
assert.NoErr(t, nfs.Mkdir("./testdata/sub/sub21", os.ModePerm))
|
||||
assert.NoErr(t, nfs.Mkdir("./testdata/sub/sub22", 0666))
|
||||
// 066X will error
|
||||
assert.NoErr(t, nfs.Mkdir("./testdata/sub/sub23/sub31", 0777))
|
||||
|
||||
assert.NoErr(t, nfs.MkParentDir("./testdata/sub/sub24/sub32"))
|
||||
assert.True(t, nfs.IsDir("./testdata/sub/sub24"))
|
||||
|
||||
assert.NoErr(t, os.RemoveAll("./testdata/sub"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFile(t *testing.T) {
|
||||
// TODO windows will error
|
||||
// if envutil.IsWin() {
|
||||
// return
|
||||
// }
|
||||
|
||||
file, err := nfs.CreateFile("./testdata/test.txt", 0664, 0666)
|
||||
if assert.NoErr(t, err) {
|
||||
assert.Eq(t, "./testdata/test.txt", file.Name())
|
||||
assert.NoErr(t, file.Close())
|
||||
assert.NoErr(t, os.Remove(file.Name()))
|
||||
}
|
||||
|
||||
file, err = nfs.CreateFile("./testdata/sub/test.txt", 0664, 0777)
|
||||
if assert.NoErr(t, err) {
|
||||
assert.Eq(t, "./testdata/sub/test.txt", file.Name())
|
||||
assert.NoErr(t, file.Close())
|
||||
assert.NoErr(t, os.RemoveAll("./testdata/sub"))
|
||||
}
|
||||
|
||||
file, err = nfs.CreateFile("./testdata/sub/sub2/test.txt", 0664, 0777)
|
||||
if assert.NoErr(t, err) {
|
||||
assert.Eq(t, "./testdata/sub/sub2/test.txt", file.Name())
|
||||
assert.NoErr(t, file.Close())
|
||||
assert.NoErr(t, os.RemoveAll("./testdata/sub"))
|
||||
}
|
||||
|
||||
fpath := "./testdata/sub/sub3/test-must-create.txt"
|
||||
assert.NoErr(t, nfs.RmFileIfExist(fpath))
|
||||
file = nfs.MustCreateFile(fpath, 0, 0766)
|
||||
assert.NoErr(t, file.Close())
|
||||
|
||||
err = nfs.RemoveSub("./testdata/sub")
|
||||
assert.NoErr(t, err)
|
||||
}
|
||||
|
||||
func TestQuickOpenFile(t *testing.T) {
|
||||
fpath := "./testdata/quick-open-file.txt"
|
||||
assert.NoErr(t, nfs.RmFileIfExist(fpath))
|
||||
|
||||
file, err := nfs.QuickOpenFile(fpath)
|
||||
assert.NoErr(t, err)
|
||||
assert.Eq(t, fpath, file.Name())
|
||||
|
||||
_, err = file.WriteString("hello")
|
||||
assert.NoErr(t, err)
|
||||
|
||||
// close
|
||||
assert.NoErr(t, file.Close())
|
||||
|
||||
// open for read
|
||||
file, err = nfs.OpenReadFile(fpath)
|
||||
assert.NoErr(t, err)
|
||||
// var bts [5]byte
|
||||
bts := make([]byte, 5)
|
||||
_, err = file.Read(bts)
|
||||
assert.NoErr(t, err)
|
||||
assert.Eq(t, "hello", string(bts))
|
||||
|
||||
// close
|
||||
assert.NoErr(t, file.Close())
|
||||
assert.NoErr(t, nfs.Remove(file.Name()))
|
||||
}
|
||||
|
||||
func TestMustRemove(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
nfs.MustRemove("/path-not-exist")
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuietRemove(t *testing.T) {
|
||||
assert.NotPanics(t, func() {
|
||||
nfs.QuietRemove("/path-not-exist")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnzip(t *testing.T) {
|
||||
assert.Err(t, nfs.Unzip("/path-not-exists", ""))
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/ngo"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ************************************************************
|
||||
// write, copy files
|
||||
// ************************************************************
|
||||
|
||||
// PutContents create file and write contents to file at once.
|
||||
//
|
||||
// data type allow: string, []byte, io.Reader
|
||||
//
|
||||
// Tip: file flag default is FsCWTFlags (override write)
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// nfs.PutContents(filePath, contents, nfs.FsCWAFlags) // append write
|
||||
func PutContents(filePath string, data any, fileFlag ...int) (int, error) {
|
||||
f, err := QuickOpenFile(filePath, ngo.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 file.
|
||||
//
|
||||
// data type allow: string, []byte, io.Reader
|
||||
//
|
||||
// Tip: file flag default is FsCWTFlags (override write)
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// nfs.WriteFile(filePath, contents, nfs.DefaultFilePerm, nfs.FsCWAFlags)
|
||||
func WriteFile(filePath string, data any, perm os.FileMode, fileFlag ...int) error {
|
||||
flag := ngo.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 allow: 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)
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package nfs_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMustCopyFile(t *testing.T) {
|
||||
srcPath := "./testdata/cp-file-src.txt"
|
||||
dstPath := "./testdata/cp-file-dst.txt"
|
||||
|
||||
assert.NoErr(t, nfs.RmIfExist(srcPath))
|
||||
assert.NoErr(t, nfs.RmFileIfExist(dstPath))
|
||||
|
||||
_, err := nfs.PutContents(srcPath, "hello")
|
||||
assert.NoErr(t, err)
|
||||
|
||||
nfs.MustCopyFile(srcPath, dstPath)
|
||||
assert.Eq(t, []byte("hello"), nfs.GetContents(dstPath))
|
||||
assert.Eq(t, "hello", nfs.ReadString(dstPath))
|
||||
|
||||
str, err := nfs.ReadStringOrErr(dstPath)
|
||||
assert.NoErr(t, err)
|
||||
assert.Eq(t, "hello", str)
|
||||
}
|
@ -0,0 +1 @@
|
||||
hello
|
@ -0,0 +1 @@
|
||||
hello
|
@ -0,0 +1 @@
|
||||
hello
|
@ -0,0 +1 @@
|
||||
package testdata
|
@ -0,0 +1,155 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/internal/common"
|
||||
"git.noahlan.cn/noahlan/ntool/ncrypt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// MimeSniffLen sniff Length, use for detect file mime type
|
||||
MimeSniffLen = 512
|
||||
)
|
||||
|
||||
// OSTempFile create a temp file on os.TempDir()
|
||||
// The file is kept as open, the caller should close the file handle, and remove the file by name.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// nfs.OSTempFile("example.*.txt")
|
||||
func OSTempFile(pattern string) (*os.File, error) {
|
||||
return os.CreateTemp(os.TempDir(), pattern)
|
||||
}
|
||||
|
||||
// OSTempFileWithContent create a temp file on os.TempDir() with the given content
|
||||
func OSTempFileWithContent(content string) (*os.File, error) {
|
||||
tmp, err := OSTempFile(ncrypt.Md5String(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = os.WriteFile(tmp.Name(), []byte(content), os.ModeTemporary); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tmp, nil
|
||||
}
|
||||
|
||||
// OSTempFilenameWithContent create a temp file on os.TempDir() with the given content and returns the filename (full path).
|
||||
// The caller should remove the file by name after use.
|
||||
func OSTempFilenameWithContent(content string) (string, error) {
|
||||
tmp, err := OSTempFileWithContent(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filename := tmp.Name()
|
||||
if err = tmp.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// TempFile is like os.CreateTemp, but can custom temp dir.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// nfs.TempFile("", "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:
|
||||
//
|
||||
// nfs.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:
|
||||
//
|
||||
// nfs.TempDir("", "example.*")
|
||||
// nfs.TempDir("testdata", "example.*")
|
||||
func TempDir(dir, pattern string) (string, error) {
|
||||
return os.MkdirTemp(dir, pattern)
|
||||
}
|
||||
|
||||
// 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])
|
||||
}
|
||||
|
||||
// JoinPaths elements, alias of filepath.Join()
|
||||
func JoinPaths(elem ...string) string {
|
||||
return filepath.Join(elem...)
|
||||
}
|
||||
|
||||
// JoinSubPaths elements, like the filepath.Join()
|
||||
func JoinSubPaths(basePath string, elem ...string) string {
|
||||
paths := make([]string, len(elem)+1)
|
||||
paths[0] = basePath
|
||||
copy(paths[1:], elem)
|
||||
return filepath.Join(paths...)
|
||||
}
|
||||
|
||||
// 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 process. will expand home dir
|
||||
//
|
||||
// TIP: will don't check path
|
||||
func ToAbsPath(p string) string {
|
||||
if len(p) == 0 || IsAbsPath(p) {
|
||||
return p
|
||||
}
|
||||
|
||||
// expand home dir
|
||||
if p[0] == '~' {
|
||||
return common.ExpandHome(p)
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return p
|
||||
}
|
||||
return filepath.Join(wd, p)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// CloseOnExec makes sure closing the file on process forking.
|
||||
func CloseOnExec(file *os.File) {
|
||||
if file != nil {
|
||||
syscall.CloseOnExec(int(file.Fd()))
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
//go:build !windows
|
||||
|
||||
package nfs_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSlashPath_nw(t *testing.T) {
|
||||
assert.Eq(t, "path/to/dir", nfs.JoinPaths("path", "to", "dir"))
|
||||
assert.Eq(t, "path/to/dir", nfs.JoinSubPaths("path", "to", "dir"))
|
||||
}
|
||||
|
||||
func TestRealpath_nw(t *testing.T) {
|
||||
inPath := "/path/to/some/../dir"
|
||||
assert.Eq(t, "/path/to/dir", nfs.Realpath(inPath))
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package nfs_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMimeType(t *testing.T) {
|
||||
assert.Eq(t, "", nfs.MimeType(""))
|
||||
assert.Eq(t, "", nfs.MimeType("not-exist"))
|
||||
assert.Eq(t, "text/plain; charset=utf-8", nfs.MimeType("testdata/mimetext.txt"))
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
buf.Write([]byte("\xFF\xD8\xFF"))
|
||||
assert.Eq(t, "image/jpeg", nfs.ReaderMimeType(buf))
|
||||
buf.Reset()
|
||||
|
||||
buf.Write([]byte("text"))
|
||||
assert.Eq(t, "text/plain; charset=utf-8", nfs.ReaderMimeType(buf))
|
||||
buf.Reset()
|
||||
|
||||
buf.Write([]byte(""))
|
||||
assert.Eq(t, "", nfs.ReaderMimeType(buf))
|
||||
buf.Reset()
|
||||
|
||||
assert.False(t, nfs.IsImageFile("testdata/test.txt"))
|
||||
assert.False(t, nfs.IsImageFile("testdata/not-exists"))
|
||||
}
|
||||
|
||||
func TestTempDir(t *testing.T) {
|
||||
dir, err := nfs.TempDir("testdata", "temp.*")
|
||||
assert.NoErr(t, err)
|
||||
assert.True(t, nfs.IsDir(dir))
|
||||
assert.NoErr(t, nfs.Remove(dir))
|
||||
}
|
||||
|
||||
func TestSplitPath(t *testing.T) {
|
||||
dir, file := nfs.SplitPath("/path/to/dir/some.txt")
|
||||
assert.Eq(t, "/path/to/dir/", dir)
|
||||
assert.Eq(t, "some.txt", file)
|
||||
}
|
||||
|
||||
func TestToAbsPath(t *testing.T) {
|
||||
assert.Eq(t, "", nfs.ToAbsPath(""))
|
||||
assert.Eq(t, "/path/to/dir/", nfs.ToAbsPath("/path/to/dir/"))
|
||||
assert.Neq(t, "~/path/to/dir", nfs.ToAbsPath("~/path/to/dir"))
|
||||
assert.Neq(t, ".", nfs.ToAbsPath("."))
|
||||
assert.Neq(t, "..", nfs.ToAbsPath(".."))
|
||||
assert.Neq(t, "./", nfs.ToAbsPath("./"))
|
||||
assert.Neq(t, "../", nfs.ToAbsPath("../"))
|
||||
}
|
||||
|
||||
func TestSlashPath(t *testing.T) {
|
||||
assert.Eq(t, "/path/to/dir", nfs.SlashPath("/path/to/dir"))
|
||||
assert.Eq(t, "/path/to/dir", nfs.UnixPath("/path/to/dir"))
|
||||
assert.Eq(t, "/path/to/dir", nfs.UnixPath("\\path\\to\\dir"))
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
//go:build windows
|
||||
|
||||
package nfs_test
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nfs"
|
||||
"git.noahlan.cn/noahlan/ntool/ntest/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSlashPath_win(t *testing.T) {
|
||||
assert.Eq(t, "path\\to\\dir", nfs.JoinPaths("path", "to", "dir"))
|
||||
assert.Eq(t, "path\\to\\dir", nfs.JoinSubPaths("path", "to", "dir"))
|
||||
}
|
||||
|
||||
func TestRealpath_win(t *testing.T) {
|
||||
inPath := "/path/to/some/../dir"
|
||||
assert.Eq(t, "\\path\\to\\dir", nfs.Realpath(inPath))
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package ngo
|
||||
|
||||
// Must if error is not empty, will panic
|
||||
func Must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// MustV if error is not empty, will panic. otherwise return the value.
|
||||
func MustV[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
|
||||
}
|
||||
|
||||
// FirstOr get first elem or elseVal
|
||||
func FirstOr[T any](sl []T, elseVal T) T {
|
||||
if len(sl) > 0 {
|
||||
return sl[0]
|
||||
}
|
||||
return elseVal
|
||||
}
|
||||
|
||||
// OrValue get
|
||||
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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// CallOrElse call okFunc() on condition is true, else call elseFn()
|
||||
func CallOrElse(cond bool, okFn, elseFn ErrFunc) error {
|
||||
if cond {
|
||||
return okFn()
|
||||
}
|
||||
return elseFn()
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.noahlan.cn/noahlan/ntool/ndef"
|
||||
)
|
||||
|
||||
type JsonSerializer struct {
|
||||
}
|
||||
|
||||
func NewJsonSerializer() ndef.Serializer {
|
||||
return &JsonSerializer{}
|
||||
}
|
||||
|
||||
func (s *JsonSerializer) Marshal(v interface{}) ([]byte, error) {
|
||||
marshal, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshal, nil
|
||||
}
|
||||
|
||||
func (s *JsonSerializer) Unmarshal(data []byte, v interface{}) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package ngo
|
||||
|
||||
import "fmt"
|
||||
|
||||
// DataSize format bytes number friendly. eg: 1024 => 1KB, 1024*1024 => 1MB
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// file, err := os.Open(path)
|
||||
// fl, err := file.Stat()
|
||||
// fmtSize := DataSize(fl.Size())
|
||||
func DataSize(size uint64) string {
|
||||
switch {
|
||||
case size < 1024:
|
||||
return fmt.Sprintf("%dB", size)
|
||||
case size < 1024*1024:
|
||||
return fmt.Sprintf("%.2fK", float64(size)/1024)
|
||||
case size < 1024*1024*1024:
|
||||
return fmt.Sprintf("%.2fM", float64(size)/1024/1024)
|
||||
default:
|
||||
return fmt.Sprintf("%.2fG", float64(size)/1024/1024/1024)
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package nlog
|
||||
|
||||
import (
|
||||
"git.noahlan.cn/noahlan/ntool/nstr"
|
||||
"github.com/gookit/color"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// WithColor is a helper function to add color to a string, only in plain encoding.
|
||||
func WithColor(text string, colour color.Color) string {
|
||||
if atomic.LoadUint32(&encoding) == plainEncodingType {
|
||||
return colour.Render(text)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// WithColorPadding is a helper function to add color to a string with leading and trailing spaces,
|
||||
// only in plain encoding.
|
||||
func WithColorPadding(text string, colour color.Color) string {
|
||||
return WithColor(nstr.PadAround(text, " ", len(text)+2), colour)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue