Initial QSfera import
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
package checkfn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsNil value check
|
||||
func IsNil(v any) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return rv.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsSimpleKind kind in: string, bool, intX, uintX, floatX
|
||||
func IsSimpleKind(k reflect.Kind) bool {
|
||||
if reflect.String == k {
|
||||
return true
|
||||
}
|
||||
return k > reflect.Invalid && k <= reflect.Float64
|
||||
}
|
||||
|
||||
// IsEqual determines if two objects are considered equal.
|
||||
//
|
||||
// TIP: cannot compare function type
|
||||
func IsEqual(src, dst any) bool {
|
||||
if src == nil || dst == nil {
|
||||
return src == dst
|
||||
}
|
||||
|
||||
bs1, ok := src.([]byte)
|
||||
if !ok {
|
||||
return reflect.DeepEqual(src, dst)
|
||||
}
|
||||
|
||||
bs2, ok := dst.([]byte)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if bs1 == nil || bs2 == nil {
|
||||
return bs1 == nil && bs2 == nil
|
||||
}
|
||||
return bytes.Equal(bs1, bs2)
|
||||
}
|
||||
|
||||
// Contains try loop over the data check if the data includes the element.
|
||||
//
|
||||
// data allow types: string, map, array, slice
|
||||
//
|
||||
// map - check key exists
|
||||
// string - check sub-string exists
|
||||
// array,slice - check sub-element exists
|
||||
//
|
||||
// Returns:
|
||||
// - valid: data is valid
|
||||
// - found: element was found
|
||||
//
|
||||
// return (false, false) if impossible.
|
||||
// return (true, false) if element was not found.
|
||||
// return (true, true) if element was found.
|
||||
func Contains(data, elem any) (valid, found bool) {
|
||||
if data == nil {
|
||||
return false, false
|
||||
}
|
||||
|
||||
dataRv := reflect.ValueOf(data)
|
||||
dataRt := reflect.TypeOf(data)
|
||||
dataKind := dataRt.Kind()
|
||||
|
||||
// string
|
||||
if dataKind == reflect.String {
|
||||
return true, strings.Contains(dataRv.String(), fmt.Sprint(elem))
|
||||
}
|
||||
|
||||
// map
|
||||
if dataKind == reflect.Map {
|
||||
mapKeys := dataRv.MapKeys()
|
||||
for i := 0; i < len(mapKeys); i++ {
|
||||
if IsEqual(mapKeys[i].Interface(), elem) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
// array, slice - other return false
|
||||
if dataKind != reflect.Slice && dataKind != reflect.Array {
|
||||
return false, false
|
||||
}
|
||||
|
||||
for i := 0; i < dataRv.Len(); i++ {
|
||||
if IsEqual(dataRv.Index(i).Interface(), elem) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
// StringsContains check string slice contains sub-string
|
||||
func StringsContains(ss []string, sub string) bool {
|
||||
for _, v := range ss {
|
||||
if v == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
// check is number: int or float
|
||||
numReg = regexp.MustCompile(`^[-+]?\d*\.?\d+$`)
|
||||
// is positive number: int or float
|
||||
pNumReg = regexp.MustCompile(`^\d*\.?\d+$`)
|
||||
)
|
||||
|
||||
// IsNumeric returns true if the given string is a numeric, otherwise false.
|
||||
func IsNumeric(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return numReg.MatchString(s)
|
||||
}
|
||||
|
||||
// IsPositiveNum check input string is positive number
|
||||
func IsPositiveNum(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
if s[0] == '-' {
|
||||
return false
|
||||
}
|
||||
return pNumReg.MatchString(s)
|
||||
}
|
||||
|
||||
// IsHttpURL check input is http/https url
|
||||
func IsHttpURL(s string) bool {
|
||||
return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
|
||||
}
|
||||
|
||||
// IndexByteAfter find index of byte after startIndex. return -1 if not found
|
||||
//
|
||||
// eg:
|
||||
//
|
||||
// IndexByteAfter("abcabc", 'b', 0) = 1
|
||||
// IndexByteAfter("abcabc", 'b', 2) = 4
|
||||
func IndexByteAfter(s string, b byte, startIndex int) int {
|
||||
idx := strings.IndexByte(s[startIndex:], b)
|
||||
if idx < 0 {
|
||||
return -1
|
||||
}
|
||||
return idx + startIndex
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package checkfn
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var detectedWSL bool
|
||||
var detectedWSLContents string
|
||||
|
||||
// IsWSL system env
|
||||
// https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364
|
||||
func IsWSL() bool {
|
||||
// ENV:
|
||||
// WSL_DISTRO_NAME=Debian
|
||||
if len(os.Getenv("WSL_DISTRO_NAME")) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if !detectedWSL {
|
||||
b := make([]byte, 1024)
|
||||
// `cat /proc/version`
|
||||
// on mac:
|
||||
// !not the file!
|
||||
// on linux(debian,ubuntu,alpine):
|
||||
// Linux version 4.19.121-linuxkit (root@18b3f92ade35) (gcc version 9.2.0 (Alpine 9.2.0)) #1 SMP Thu Jan 21 15:36:34 UTC 2021
|
||||
// on win git bash, conEmu:
|
||||
// MINGW64_NT-10.0-19042 version 3.1.7-340.x86_64 (@WIN-N0G619FD3UK) (gcc version 9.3.0 (GCC) ) 2020-10-23 13:08 UTC
|
||||
// on WSL:
|
||||
// Linux version 4.4.0-19041-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #488-Microsoft Mon Sep 01 13:43:00 PST 2020
|
||||
f, err := os.Open("/proc/version")
|
||||
if err == nil {
|
||||
_, _ = f.Read(b) // ignore error
|
||||
f.Close()
|
||||
detectedWSLContents = string(b)
|
||||
}
|
||||
detectedWSL = true
|
||||
}
|
||||
return strings.Contains(detectedWSLContents, "Microsoft")
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# Common func for internal use
|
||||
|
||||
- don't depend on other external packages
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Cmdline build
|
||||
func Cmdline(args []string, binName ...string) string {
|
||||
b := new(strings.Builder)
|
||||
|
||||
if len(binName) > 0 {
|
||||
b.WriteString(binName[0])
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
|
||||
for i, a := range args {
|
||||
if i > 0 {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
|
||||
if strings.ContainsRune(a, '"') {
|
||||
b.WriteString(fmt.Sprintf(`'%s'`, a))
|
||||
} else if a == "" || strings.ContainsRune(a, '\'') || strings.ContainsRune(a, ' ') {
|
||||
b.WriteString(fmt.Sprintf(`"%s"`, a))
|
||||
} else {
|
||||
b.WriteString(a)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ShellQuote quote a string on contains ', ", SPACE. refer strconv.Quote()
|
||||
func ShellQuote(a string) string {
|
||||
if a == "" {
|
||||
return `""`
|
||||
}
|
||||
|
||||
// use quote char
|
||||
var quote byte
|
||||
|
||||
// has double quote
|
||||
if pos := strings.IndexByte(a, '"'); pos > -1 {
|
||||
if !checkNeedQuote(a, pos, '"') {
|
||||
return a
|
||||
}
|
||||
|
||||
quote = '\''
|
||||
} else if pos := strings.IndexByte(a, '\''); pos > -1 {
|
||||
// single quote
|
||||
if !checkNeedQuote(a, pos, '\'') {
|
||||
return a
|
||||
}
|
||||
quote = '"'
|
||||
} else if strings.IndexByte(a, ' ') > -1 {
|
||||
quote = '"'
|
||||
}
|
||||
|
||||
// no quote char OR not need quote
|
||||
if quote == 0 {
|
||||
return a
|
||||
}
|
||||
return fmt.Sprintf("%c%s%c", quote, a, quote)
|
||||
}
|
||||
|
||||
func checkNeedQuote(a string, pos int, char byte) bool {
|
||||
// end with char. eg: "
|
||||
lastIsQ := a[len(a)-1] == char
|
||||
|
||||
// start with char. eg: "
|
||||
if pos == 0 {
|
||||
if lastIsQ {
|
||||
return false
|
||||
}
|
||||
|
||||
if pos1 := strings.IndexByte(a[pos+1:], char); pos1 > -1 {
|
||||
// eg: `"one two" three four`
|
||||
lastS := a[pos1+pos+1:]
|
||||
if !strings.ContainsRune(lastS, ' ') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startS := a[:pos]
|
||||
|
||||
// eg: `--one="two three"`
|
||||
if lastIsQ && strings.IndexByte(startS, ' ') == -1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Environ like os.Environ, but will returns key-value map[string]string data.
|
||||
func Environ() map[string]string {
|
||||
envList := os.Environ()
|
||||
envMap := make(map[string]string, len(envList))
|
||||
|
||||
for _, str := range envList {
|
||||
nodes := strings.SplitN(str, "=", 2)
|
||||
if len(nodes) < 2 {
|
||||
envMap[nodes[0]] = ""
|
||||
} else {
|
||||
envMap[nodes[0]] = nodes[1]
|
||||
}
|
||||
}
|
||||
return envMap
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/goutil/comdef"
|
||||
"github.com/gookit/goutil/internal/checkfn"
|
||||
)
|
||||
|
||||
// Bool try to convert type to bool
|
||||
func Bool(v any) bool {
|
||||
bl, _ := ToBool(v)
|
||||
return bl
|
||||
}
|
||||
|
||||
// ToBool try to convert type to bool
|
||||
func ToBool(v any) (bool, error) {
|
||||
if bl, ok := v.(bool); ok {
|
||||
return bl, nil
|
||||
}
|
||||
|
||||
if str, ok := v.(string); ok {
|
||||
return StrToBool(str)
|
||||
}
|
||||
return false, comdef.ErrConvType
|
||||
}
|
||||
|
||||
// StrToBool parse string to bool. like strconv.ParseBool()
|
||||
func StrToBool(s string) (bool, error) {
|
||||
lower := strings.ToLower(s)
|
||||
switch lower {
|
||||
case "1", "on", "yes", "true":
|
||||
return true, nil
|
||||
case "0", "off", "no", "false":
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("'%s' cannot convert to bool", s)
|
||||
}
|
||||
|
||||
// FormatWithArgs format message with args
|
||||
//
|
||||
// - only one element, format to string
|
||||
// - first is format: fmt.Sprintf(firstElem, fmtAndArgs[1:]...)
|
||||
// - all is args: return fmt.Sprint(fmtAndArgs...)
|
||||
func FormatWithArgs(fmtAndArgs []any) string {
|
||||
ln := len(fmtAndArgs)
|
||||
if ln == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
first := fmtAndArgs[0]
|
||||
if ln == 1 {
|
||||
if str, ok := first.(string); ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprintf("%+v", first)
|
||||
}
|
||||
|
||||
// is template string.
|
||||
if tplStr, ok := first.(string); ok && strings.IndexByte(tplStr, '%') >= 0 {
|
||||
return fmt.Sprintf(tplStr, fmtAndArgs[1:]...)
|
||||
}
|
||||
return fmt.Sprint(fmtAndArgs...)
|
||||
}
|
||||
|
||||
// ConvOption convert options
|
||||
type ConvOption struct {
|
||||
// if ture: value is nil, will return convert error;
|
||||
// if false(default): value is nil, will convert to zero value
|
||||
NilAsFail bool
|
||||
// HandlePtr auto convert ptr type(int,float,string) value. eg: *int to int
|
||||
// - if true: will use real type try convert. default is false
|
||||
// - NOTE: current T type's ptr is default support.
|
||||
HandlePtr bool
|
||||
// set custom fallback convert func for not supported type.
|
||||
UserConvFn comdef.ToStringFunc
|
||||
}
|
||||
|
||||
// ConvOptionFn convert option func
|
||||
type ConvOptionFn func(opt *ConvOption)
|
||||
|
||||
// StrBySprintFn convert any value to string by fmt.Sprint
|
||||
var StrBySprintFn = func(v any) (string, error) {
|
||||
return fmt.Sprint(v), nil
|
||||
}
|
||||
|
||||
// WithUserConvFn set ConvOption.UserConvFn option
|
||||
func WithUserConvFn(fn comdef.ToStringFunc) ConvOptionFn {
|
||||
return func(opt *ConvOption) {
|
||||
opt.UserConvFn = fn
|
||||
}
|
||||
}
|
||||
|
||||
// NewConvOption create a new ConvOption
|
||||
func NewConvOption(optFns ...ConvOptionFn) *ConvOption {
|
||||
opt := &ConvOption{}
|
||||
opt.WithOption(optFns...)
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithOption set convert option
|
||||
func (opt *ConvOption) WithOption(optFns ...ConvOptionFn) {
|
||||
for _, fn := range optFns {
|
||||
if fn != nil {
|
||||
fn(opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToStringWith try to convert value to string. can with some option func, more see ConvOption.
|
||||
func ToStringWith(in any, optFns ...ConvOptionFn) (str string, err error) {
|
||||
switch value := in.(type) {
|
||||
case int:
|
||||
str = strconv.Itoa(value)
|
||||
case int8:
|
||||
str = strconv.Itoa(int(value))
|
||||
case int16:
|
||||
str = strconv.Itoa(int(value))
|
||||
case int32: // same as `rune`
|
||||
str = strconv.Itoa(int(value))
|
||||
case int64:
|
||||
str = strconv.FormatInt(value, 10)
|
||||
case uint:
|
||||
str = strconv.FormatUint(uint64(value), 10)
|
||||
case uint8:
|
||||
str = strconv.FormatUint(uint64(value), 10)
|
||||
case uint16:
|
||||
str = strconv.FormatUint(uint64(value), 10)
|
||||
case uint32:
|
||||
str = strconv.FormatUint(uint64(value), 10)
|
||||
case uint64:
|
||||
str = strconv.FormatUint(value, 10)
|
||||
case float32:
|
||||
str = strconv.FormatFloat(float64(value), 'f', -1, 32)
|
||||
case float64:
|
||||
str = strconv.FormatFloat(value, 'f', -1, 64)
|
||||
case bool:
|
||||
str = strconv.FormatBool(value)
|
||||
case string:
|
||||
str = value
|
||||
case *string:
|
||||
str = *value
|
||||
case []byte:
|
||||
str = string(value)
|
||||
case time.Duration:
|
||||
str = strconv.FormatInt(int64(value), 10)
|
||||
case fmt.Stringer:
|
||||
str = value.String()
|
||||
case error:
|
||||
str = value.Error()
|
||||
default:
|
||||
if len(optFns) == 0 && in == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
opt := NewConvOption(optFns...)
|
||||
if in == nil {
|
||||
if opt.NilAsFail {
|
||||
err = comdef.ErrConvType
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if opt.HandlePtr {
|
||||
if rv := reflect.ValueOf(in); rv.Kind() == reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
if checkfn.IsSimpleKind(rv.Kind()) {
|
||||
return ToStringWith(rv.Interface(), optFns...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opt.UserConvFn != nil {
|
||||
str, err = opt.UserConvFn(in)
|
||||
} else {
|
||||
err = comdef.ErrConvType
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package comfunc
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// JoinPaths2 elements, like the filepath.Join()
|
||||
func JoinPaths2(basePath string, elems []string) string {
|
||||
paths := make([]string, len(elems)+1)
|
||||
paths[0] = basePath
|
||||
copy(paths[1:], elems)
|
||||
return filepath.Join(paths...)
|
||||
}
|
||||
|
||||
// JoinPaths3 elements, like the filepath.Join()
|
||||
func JoinPaths3(basePath, secPath string, elems []string) string {
|
||||
paths := make([]string, len(elems)+2)
|
||||
paths[0] = basePath
|
||||
paths[1] = secPath
|
||||
copy(paths[2:], elems)
|
||||
return filepath.Join(paths...)
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var commentsPrefixes = []string{"#", ";", "//"}
|
||||
|
||||
// ParseEnvLineOption parse env line options
|
||||
type ParseEnvLineOption struct {
|
||||
// NotInlineComments dont parse inline comments.
|
||||
// - default: false. will parse inline comments
|
||||
NotInlineComments bool
|
||||
// SkipOnErrorLine skip error line, continue parse next line
|
||||
// - False: return error, clear parsed map
|
||||
SkipOnErrorLine bool
|
||||
}
|
||||
|
||||
// ParseEnvLines parse simple multiline k-v string to a string-map.
|
||||
// Can use to parse simple INI or DOTENV file contents.
|
||||
//
|
||||
// NOTE:
|
||||
//
|
||||
// - It's like INI/ENV format contents.
|
||||
// - Support comments line starts with: "#", ";", "//"
|
||||
// - Support inline comments split with: " #" eg: "name=tom # a comments"
|
||||
// - DON'T support submap parse.
|
||||
func ParseEnvLines(text string, opt ParseEnvLineOption) (mp map[string]string, err error) {
|
||||
lines := strings.Split(text, "\n")
|
||||
ln := len(lines)
|
||||
if ln == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
strMap := make(map[string]string, ln)
|
||||
|
||||
for _, line := range lines {
|
||||
if line = strings.TrimSpace(line); line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip comments line
|
||||
if line[0] == '#' || line[0] == ';' || strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
}
|
||||
|
||||
key, val := splitLineByChar(line, '=', !opt.NotInlineComments)
|
||||
// invalid line
|
||||
if key == "" {
|
||||
if opt.SkipOnErrorLine {
|
||||
continue
|
||||
}
|
||||
strMap = nil
|
||||
err = fmt.Errorf("invalid line contents: must match `KEY=VAL`(line: %s)", line)
|
||||
return
|
||||
}
|
||||
|
||||
strMap[key] = val
|
||||
}
|
||||
|
||||
return strMap, nil
|
||||
}
|
||||
|
||||
// SplitLineToKv parse string line to k-v, not support comments.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// 'DEBUG=true' => ['DEBUG', 'true']
|
||||
//
|
||||
// NOTE: line must contain '=', allow: 'ENV_KEY='
|
||||
func SplitLineToKv(line, sep string) (string, string) {
|
||||
return SplitKvBySep(line, sep, false)
|
||||
}
|
||||
|
||||
// SplitKvBySep parse string line to k-v, support parse comments.
|
||||
// - rmInlineComments: check and remove inline comments by ' #'
|
||||
func SplitKvBySep(line, sep string, rmInlineComments bool) (key, val string) {
|
||||
sepPos := strings.Index(line, sep)
|
||||
if sepPos < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
return splitKvBySepPos(line, sepPos, len(sep), rmInlineComments)
|
||||
}
|
||||
|
||||
func splitLineByChar(line string, sep byte, rmInlineComments bool) (key, val string) {
|
||||
sepPos := strings.IndexByte(line, sep)
|
||||
if sepPos < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
return splitKvBySepPos(line, sepPos, 1, rmInlineComments)
|
||||
}
|
||||
|
||||
func splitKvBySepPos(line string, sepPos, sepLen int, rmInlineComments bool) (key, val string) {
|
||||
// key cannot be empty
|
||||
key = strings.TrimSpace(line[0:sepPos])
|
||||
if key == "" {
|
||||
return "", ""
|
||||
}
|
||||
val = strings.TrimSpace(line[sepPos+sepLen:])
|
||||
|
||||
// check quotes if present
|
||||
if vln := len(val); vln >= 2 {
|
||||
// remove quotes
|
||||
if (val[0] == '"' && val[vln-1] == '"') || (val[0] == '\'' && val[vln-1] == '\'') {
|
||||
val = val[1 : vln-1]
|
||||
return
|
||||
}
|
||||
|
||||
if !rmInlineComments {
|
||||
return
|
||||
}
|
||||
|
||||
// value is empty, only inline comments
|
||||
if val[0] == '#' {
|
||||
val = ""
|
||||
return
|
||||
}
|
||||
|
||||
// remove inline comments
|
||||
if pos := strings.Index(val, " #"); pos > 0 {
|
||||
val = strings.TrimRight(val[0:pos], " \t")
|
||||
vln = len(val)
|
||||
// remove quotes
|
||||
if (val[0] == '"' && val[vln-1] == '"') || (val[0] == '\'' && val[vln-1] == '\'') {
|
||||
val = val[1 : vln-1]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Workdir get
|
||||
func Workdir() string {
|
||||
dir, _ := os.Getwd()
|
||||
return dir
|
||||
}
|
||||
|
||||
// ExpandHome will parse first `~` as user home dir path.
|
||||
func ExpandHome(pathStr string) string {
|
||||
if len(pathStr) == 0 {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
if pathStr[0] != '~' {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
if len(pathStr) > 1 && pathStr[1] != '/' && pathStr[1] != '\\' {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return pathStr
|
||||
}
|
||||
return homeDir + pathStr[1:]
|
||||
}
|
||||
|
||||
// ExecCmd an command and return output.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ExecCmd("ls", []string{"-al"})
|
||||
func ExecCmd(binName string, args []string, workDir ...string) (string, error) {
|
||||
// create a new Cmd instance
|
||||
cmd := exec.Command(binName, args...)
|
||||
if len(workDir) > 0 {
|
||||
cmd.Dir = workDir[0]
|
||||
}
|
||||
|
||||
bs, err := cmd.Output()
|
||||
return string(bs), err
|
||||
}
|
||||
|
||||
var (
|
||||
cmdList = []string{"cmd", "cmd.exe"}
|
||||
pwshList = []string{"powershell", "powershell.exe", "pwsh", "pwsh.exe"}
|
||||
)
|
||||
|
||||
// ShellExec exec command by shell
|
||||
// cmdLine e.g. "ls -al"
|
||||
func ShellExec(cmdLine string, shells ...string) (string, error) {
|
||||
// shell := "/bin/sh"
|
||||
shell := "sh"
|
||||
if len(shells) > 0 {
|
||||
shell = shells[0]
|
||||
}
|
||||
|
||||
cmd := exec.Command(shell, "-c", cmdLine)
|
||||
bs, err := cmd.Output()
|
||||
return string(bs), err
|
||||
}
|
||||
|
||||
// curShellCache value
|
||||
var curShellCache string
|
||||
|
||||
// CurrentShell get current used shell env file.
|
||||
//
|
||||
// return like: "/bin/zsh" "/bin/bash". if onlyName=true, will return "zsh", "bash"
|
||||
func CurrentShell(onlyName bool, fallbackShell ...string) (binPath string) {
|
||||
var err error
|
||||
|
||||
fbShell := ""
|
||||
if len(fallbackShell) > 0 {
|
||||
fbShell = fallbackShell[0]
|
||||
}
|
||||
|
||||
if curShellCache == "" {
|
||||
// 检查父进程名称
|
||||
parentProcess := os.Getenv("GOPROCESS")
|
||||
if parentProcess != "" {
|
||||
binPath = parentProcess
|
||||
} else {
|
||||
binPath = os.Getenv("SHELL") // 适用于 Unix-like 系统
|
||||
if len(binPath) == 0 {
|
||||
// TODO check on Windows git bash
|
||||
binPath, err = ShellExec("echo $SHELL")
|
||||
if err != nil {
|
||||
binPath = fbShell
|
||||
}
|
||||
}
|
||||
binPath = strings.TrimSpace(binPath)
|
||||
}
|
||||
|
||||
// fix: 去除 .exe 后缀
|
||||
if pos := strings.IndexByte(binPath, '.'); pos > 0 {
|
||||
binPath = binPath[:pos]
|
||||
}
|
||||
|
||||
// cache result
|
||||
curShellCache = binPath
|
||||
} else {
|
||||
binPath = curShellCache
|
||||
}
|
||||
|
||||
if onlyName && len(binPath) > 0 {
|
||||
binPath = filepath.Base(binPath)
|
||||
} else if len(binPath) == 0 {
|
||||
binPath = fbShell
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func checkWinCurrentShell() string {
|
||||
// 在 Windows 上,可以检查 COMSPEC 环境变量
|
||||
comSpec := os.Getenv("COMSPEC")
|
||||
// 没法检查 pwsh, 返回的还是 cmd
|
||||
return comSpec
|
||||
}
|
||||
|
||||
// HasShellEnv has shell env check.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// HasShellEnv("sh")
|
||||
// HasShellEnv("bash")
|
||||
func HasShellEnv(shell string) bool {
|
||||
// can also use: "echo $0"
|
||||
out, err := ShellExec("echo OK", shell)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(out) == "OK"
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package comfunc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// check is duration string. TIP: extend unit d,w. eg: "1d", "2w"
|
||||
//
|
||||
// time.ParseDuration() is max support hour "h".
|
||||
durStrReg = regexp.MustCompile(`^-?([0-9]+(?:\.[0-9]*)?(ns|us|µs|ms|s|m|h|d|w))+$`)
|
||||
|
||||
// check long duration string. 验证整体格式是否符合
|
||||
//
|
||||
// eg: "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks", "1month"
|
||||
//
|
||||
// time.ParseDuration() is not support long unit.
|
||||
durStrRegL = regexp.MustCompile(`^-?([0-9]+(?:\.[0-9]*)?[nuµsmhdw][a-zA-Z]{0,8})+$`)
|
||||
// use for parse duration string. see ToDuration()
|
||||
//
|
||||
// NOTE: 解析时,不能加最后的 `+` 会导致只匹配了最后一组 时间单位
|
||||
durStrRegL2 = regexp.MustCompile(`-?([0-9]+(?:\.[0-9]*)?)([nuµsmhdw][a-z]{0,8})`)
|
||||
)
|
||||
|
||||
// IsDuration check the string is a duration string.
|
||||
func IsDuration(s string) bool {
|
||||
if s == "0" || durStrReg.MatchString(s) {
|
||||
return true
|
||||
}
|
||||
return durStrRegL.MatchString(s)
|
||||
}
|
||||
|
||||
// ToDuration parses a duration string. such as "300ms", "-1.5h" or "2h45m".
|
||||
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
//
|
||||
// Diff of time.ParseDuration:
|
||||
// - support extends unit d, w at the end of string. such as "1d", "2w".
|
||||
// - support extends unit: month, week, day
|
||||
// - support long string unit at the end. such as "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks".
|
||||
//
|
||||
// If the string is not a valid duration string, it will return an error.
|
||||
func ToDuration(s string) (time.Duration, error) {
|
||||
ln := len(s)
|
||||
if ln == 0 {
|
||||
return 0, fmt.Errorf("empty duration string")
|
||||
}
|
||||
|
||||
s = strings.ToLower(s)
|
||||
if s == "0" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// check duration string is valid
|
||||
if !durStrRegL.MatchString(s) {
|
||||
return 0, fmt.Errorf("invalid duration string: %s", s)
|
||||
}
|
||||
|
||||
// if ln < 4 AND end != d|w, directly call time.ParseDuration()
|
||||
if ln < 4 && s[ln-1] != 'd' && s[ln-1] != 'w' {
|
||||
return time.ParseDuration(s)
|
||||
}
|
||||
|
||||
// time.ParseDuration() is not support long unit.
|
||||
ssList := durStrRegL2.FindAllStringSubmatch(s, -1)
|
||||
// fmt.Println(ssList)
|
||||
bts := make([]byte, 0, ln)
|
||||
if s[0] == '-' {
|
||||
bts = append(bts, '-')
|
||||
}
|
||||
|
||||
// only one element. eg: "1day"
|
||||
if len(ssList) == 1 {
|
||||
bts = parseLongUnit(ssList[0], bts)
|
||||
} else {
|
||||
// more than one element. eg: "1day2hour3min"
|
||||
for _, ss := range ssList {
|
||||
if len(ss) == 3 {
|
||||
bts = parseLongUnit(ss, bts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return time.ParseDuration(string(bts))
|
||||
}
|
||||
|
||||
// convert to short unit
|
||||
func parseLongUnit(ss []string, bts []byte) []byte {
|
||||
// eg: "3sec" -> ss=[3sec, -3, sec]
|
||||
num, unit := ss[1], ss[2]
|
||||
switch unit {
|
||||
case "month", "months":
|
||||
// time lib max unit is hour, so need convert by 24 * 30*n
|
||||
bts = appendNumToBytes(bts, num, 24*30)
|
||||
bts = append(bts, 'h')
|
||||
case "w", "week", "weeks":
|
||||
// time lib max unit is hour, so need convert by 24 * 7*n
|
||||
bts = appendNumToBytes(bts, num, 24*7)
|
||||
bts = append(bts, 'h')
|
||||
case "d", "day", "days":
|
||||
// time lib max unit is hour, so need convert by 24*n
|
||||
bts = appendNumToBytes(bts, num, 24)
|
||||
bts = append(bts, 'h')
|
||||
case "hour", "hours":
|
||||
bts = append(bts, num...)
|
||||
bts = append(bts, 'h')
|
||||
case "min", "mins", "minute", "minutes":
|
||||
bts = append(bts, num...)
|
||||
bts = append(bts, 'm')
|
||||
case "sec", "secs", "second", "seconds":
|
||||
bts = append(bts, num...)
|
||||
bts = append(bts, 's')
|
||||
default:
|
||||
first := ss[0]
|
||||
|
||||
// '-' has been added on ToDuration()
|
||||
if first[0] == '-' {
|
||||
bts = append(bts, first[1:]...)
|
||||
} else {
|
||||
bts = append(bts, first...)
|
||||
}
|
||||
}
|
||||
|
||||
return bts
|
||||
}
|
||||
|
||||
func appendNumToBytes(bts []byte, num string, multiple int) []byte {
|
||||
if strings.ContainsRune(num, '.') {
|
||||
f, _ := strconv.ParseFloat(num, 64) // is float number
|
||||
val := f * float64(multiple)
|
||||
|
||||
// 使用 Float 保留两位小数 -> 会始终有两位小数,即使是N.00
|
||||
// bts = strconv.AppendFloat(bts, val, 'f', 2, 64)
|
||||
|
||||
// 四舍五入到两位小数
|
||||
rounded := math.Round(val*100) / 100
|
||||
// 使用 AppendFloat 自动去除末尾的 .0 或 .00
|
||||
bts = strconv.AppendFloat(bts, rounded, 'f', -1, 64)
|
||||
} else {
|
||||
n, _ := strconv.Atoi(num)
|
||||
bts = strconv.AppendInt(bts, int64(n*multiple), 10)
|
||||
}
|
||||
|
||||
return bts
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
// Package varexpr provides some commonly ENV var parse functions.
|
||||
//
|
||||
// parse env value, allow expressions:
|
||||
//
|
||||
// ${VAR_NAME} Only var name
|
||||
// ${VAR_NAME | default} With default value, if value is empty.
|
||||
// ${VAR_NAME | ?error} With error on value is empty.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// only key - "${SHELL}"
|
||||
// with default - "${NotExist | defValue}"
|
||||
// multi key - "${GOPATH}/${APP_ENV | prod}/dir"
|
||||
package varexpr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// SepChar separator char split var name and default value
|
||||
SepChar = "|"
|
||||
VarLeft = "${" // default var left format chars
|
||||
VarRight = "}" // default var right format chars
|
||||
|
||||
mustPrefix = '?' // must prefix char
|
||||
)
|
||||
|
||||
// ParseOptFn option func
|
||||
type ParseOptFn func(o *ParseOpts)
|
||||
|
||||
// ParseOpts parse options for ParseValue
|
||||
type ParseOpts struct {
|
||||
// Getter Env value provider func.
|
||||
Getter func(string) string
|
||||
// ParseFn custom parse expr func. expr like "${SHELL}" "${NotExist|defValue}"
|
||||
ParseFn func(string) (string, error)
|
||||
// Regexp custom expression regex.
|
||||
Regexp *regexp.Regexp
|
||||
// var format chars for expression.
|
||||
// default left="${", right="}"
|
||||
VarLeft, VarRight string
|
||||
}
|
||||
|
||||
func (opt *ParseOpts) useDefaultRegex() {
|
||||
opt.Regexp = envRegex
|
||||
opt.VarLeft = VarLeft
|
||||
opt.VarRight = VarRight
|
||||
}
|
||||
|
||||
// must add "?" - To ensure that there is no greedy match
|
||||
var envRegex = regexp.MustCompile(`\${.+?}`)
|
||||
var std = New()
|
||||
|
||||
// Parse parse ENV var value from input string, support default value.
|
||||
//
|
||||
// Format:
|
||||
//
|
||||
// ${var_name} Only var name
|
||||
// ${var_name | default} With default value
|
||||
// ${var_name | ?error} With error on value is empty.
|
||||
//
|
||||
// see Parser.Parse
|
||||
func Parse(val string) (string, error) {
|
||||
return std.Parse(val)
|
||||
}
|
||||
|
||||
// SafeParse parse ENV var value from input string, support default value.
|
||||
//
|
||||
// see Parser.Parse
|
||||
func SafeParse(val string) string {
|
||||
s, _ := std.Parse(val)
|
||||
return s
|
||||
}
|
||||
|
||||
// ParseWith parse ENV var value from input string, support default value.
|
||||
func ParseWith(val string, optFns ...ParseOptFn) (string, error) {
|
||||
return New(optFns...).Parse(val)
|
||||
}
|
||||
|
||||
// Parser parse ENV var value from input string, support default value.
|
||||
type Parser struct {
|
||||
ParseOpts
|
||||
}
|
||||
|
||||
// New create a new Parser
|
||||
func New(optFns ...ParseOptFn) *Parser {
|
||||
opts := &ParseOpts{Getter: os.Getenv}
|
||||
opts.useDefaultRegex()
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(opts)
|
||||
}
|
||||
return &Parser{ParseOpts: *opts}
|
||||
}
|
||||
|
||||
// Parse parse ENV var value from input string, support default value.
|
||||
//
|
||||
// Format:
|
||||
//
|
||||
// ${var_name} Only var name
|
||||
// ${var_name | default} With default value
|
||||
// ${var_name | ?error} With error on value is empty.
|
||||
// ${VAR_NAME1}/path/${VAR_NAME2} Allow multi var name.
|
||||
func (p *Parser) Parse(val string) (newVal string, err error) {
|
||||
if p.Regexp == nil {
|
||||
p.useDefaultRegex()
|
||||
}
|
||||
|
||||
times := strings.Count(val, p.VarLeft)
|
||||
if times == 0 {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// enhance: see https://github.com/gookit/goutil/issues/135
|
||||
if times == 1 && strings.HasPrefix(val, p.VarLeft) && strings.HasSuffix(val, p.VarRight) {
|
||||
return p.parseOne(val)
|
||||
}
|
||||
|
||||
// parse expression
|
||||
newVal = p.Regexp.ReplaceAllStringFunc(val, func(s string) string {
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
s, err = p.parseOne(s)
|
||||
return s
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// parse one node expression.
|
||||
func (p *Parser) parseOne(eVar string) (val string, err error) {
|
||||
if p.ParseFn != nil {
|
||||
return p.ParseFn(eVar)
|
||||
}
|
||||
|
||||
// like "${NotExist | defValue}". first remove "${" and "}", then split it
|
||||
ss := strings.SplitN(eVar[2:len(eVar)-1], SepChar, 2)
|
||||
var name, def string
|
||||
|
||||
// with default value.
|
||||
if len(ss) == 2 {
|
||||
name, def = strings.TrimSpace(ss[0]), strings.TrimSpace(ss[1])
|
||||
} else {
|
||||
name = strings.TrimSpace(ss[0])
|
||||
}
|
||||
|
||||
// get ENV value by name
|
||||
val = p.Getter(name)
|
||||
if val == "" && def != "" {
|
||||
// check def is "?error"
|
||||
if def[0] == mustPrefix {
|
||||
msg := "value is required for var: " + name
|
||||
if len(def) > 1 {
|
||||
msg = def[1:]
|
||||
}
|
||||
err = errors.New(msg)
|
||||
} else {
|
||||
val = def
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user