first commit

main v1.0.0
NoahLan 2 years ago
commit 81765d02c4

21
.gitignore vendored

@ -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 @@
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…
Cancel
Save