Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Common func for internal use
- don't depend on other external packages
+93
View File
@@ -0,0 +1,93 @@
package comfunc
import (
"fmt"
"strings"
)
// Cmdline build
func Cmdline(args []string, binName ...string) string {
b := new(strings.Builder)
if len(binName) > 0 {
b.WriteString(binName[0])
b.WriteByte(' ')
}
for i, a := range args {
if i > 0 {
b.WriteByte(' ')
}
if strings.ContainsRune(a, '"') {
b.WriteString(fmt.Sprintf(`'%s'`, a))
} else if a == "" || strings.ContainsRune(a, '\'') || strings.ContainsRune(a, ' ') {
b.WriteString(fmt.Sprintf(`"%s"`, a))
} else {
b.WriteString(a)
}
}
return b.String()
}
// ShellQuote quote a string on contains ', ", SPACE. refer strconv.Quote()
func ShellQuote(a string) string {
if a == "" {
return `""`
}
// use quote char
var quote byte
// has double quote
if pos := strings.IndexByte(a, '"'); pos > -1 {
if !checkNeedQuote(a, pos, '"') {
return a
}
quote = '\''
} else if pos := strings.IndexByte(a, '\''); pos > -1 {
// single quote
if !checkNeedQuote(a, pos, '\'') {
return a
}
quote = '"'
} else if strings.IndexByte(a, ' ') > -1 {
quote = '"'
}
// no quote char OR not need quote
if quote == 0 {
return a
}
return fmt.Sprintf("%c%s%c", quote, a, quote)
}
func checkNeedQuote(a string, pos int, char byte) bool {
// end with char. eg: "
lastIsQ := a[len(a)-1] == char
// start with char. eg: "
if pos == 0 {
if lastIsQ {
return false
}
if pos1 := strings.IndexByte(a[pos+1:], char); pos1 > -1 {
// eg: `"one two" three four`
lastS := a[pos1+pos+1:]
if !strings.ContainsRune(lastS, ' ') {
return false
}
}
} else {
startS := a[:pos]
// eg: `--one="two three"`
if lastIsQ && strings.IndexByte(startS, ' ') == -1 {
return false
}
}
return true
}
+22
View File
@@ -0,0 +1,22 @@
package comfunc
import (
"os"
"strings"
)
// Environ like os.Environ, but will returns key-value map[string]string data.
func Environ() map[string]string {
envList := os.Environ()
envMap := make(map[string]string, len(envList))
for _, str := range envList {
nodes := strings.SplitN(str, "=", 2)
if len(nodes) < 2 {
envMap[nodes[0]] = ""
} else {
envMap[nodes[0]] = nodes[1]
}
}
return envMap
}
+185
View File
@@ -0,0 +1,185 @@
package comfunc
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
"github.com/gookit/goutil/comdef"
"github.com/gookit/goutil/internal/checkfn"
)
// Bool try to convert type to bool
func Bool(v any) bool {
bl, _ := ToBool(v)
return bl
}
// ToBool try to convert type to bool
func ToBool(v any) (bool, error) {
if bl, ok := v.(bool); ok {
return bl, nil
}
if str, ok := v.(string); ok {
return StrToBool(str)
}
return false, comdef.ErrConvType
}
// StrToBool parse string to bool. like strconv.ParseBool()
func StrToBool(s string) (bool, error) {
lower := strings.ToLower(s)
switch lower {
case "1", "on", "yes", "true":
return true, nil
case "0", "off", "no", "false":
return false, nil
}
return false, fmt.Errorf("'%s' cannot convert to bool", s)
}
// FormatWithArgs format message with args
//
// - only one element, format to string
// - first is format: fmt.Sprintf(firstElem, fmtAndArgs[1:]...)
// - all is args: return fmt.Sprint(fmtAndArgs...)
func FormatWithArgs(fmtAndArgs []any) string {
ln := len(fmtAndArgs)
if ln == 0 {
return ""
}
first := fmtAndArgs[0]
if ln == 1 {
if str, ok := first.(string); ok {
return str
}
return fmt.Sprintf("%+v", first)
}
// is template string.
if tplStr, ok := first.(string); ok && strings.IndexByte(tplStr, '%') >= 0 {
return fmt.Sprintf(tplStr, fmtAndArgs[1:]...)
}
return fmt.Sprint(fmtAndArgs...)
}
// ConvOption convert options
type ConvOption struct {
// if ture: value is nil, will return convert error;
// if false(default): value is nil, will convert to zero value
NilAsFail bool
// HandlePtr auto convert ptr type(int,float,string) value. eg: *int to int
// - if true: will use real type try convert. default is false
// - NOTE: current T type's ptr is default support.
HandlePtr bool
// set custom fallback convert func for not supported type.
UserConvFn comdef.ToStringFunc
}
// ConvOptionFn convert option func
type ConvOptionFn func(opt *ConvOption)
// StrBySprintFn convert any value to string by fmt.Sprint
var StrBySprintFn = func(v any) (string, error) {
return fmt.Sprint(v), nil
}
// WithUserConvFn set ConvOption.UserConvFn option
func WithUserConvFn(fn comdef.ToStringFunc) ConvOptionFn {
return func(opt *ConvOption) {
opt.UserConvFn = fn
}
}
// NewConvOption create a new ConvOption
func NewConvOption(optFns ...ConvOptionFn) *ConvOption {
opt := &ConvOption{}
opt.WithOption(optFns...)
return opt
}
// WithOption set convert option
func (opt *ConvOption) WithOption(optFns ...ConvOptionFn) {
for _, fn := range optFns {
if fn != nil {
fn(opt)
}
}
}
// ToStringWith try to convert value to string. can with some option func, more see ConvOption.
func ToStringWith(in any, optFns ...ConvOptionFn) (str string, err error) {
switch value := in.(type) {
case int:
str = strconv.Itoa(value)
case int8:
str = strconv.Itoa(int(value))
case int16:
str = strconv.Itoa(int(value))
case int32: // same as `rune`
str = strconv.Itoa(int(value))
case int64:
str = strconv.FormatInt(value, 10)
case uint:
str = strconv.FormatUint(uint64(value), 10)
case uint8:
str = strconv.FormatUint(uint64(value), 10)
case uint16:
str = strconv.FormatUint(uint64(value), 10)
case uint32:
str = strconv.FormatUint(uint64(value), 10)
case uint64:
str = strconv.FormatUint(value, 10)
case float32:
str = strconv.FormatFloat(float64(value), 'f', -1, 32)
case float64:
str = strconv.FormatFloat(value, 'f', -1, 64)
case bool:
str = strconv.FormatBool(value)
case string:
str = value
case *string:
str = *value
case []byte:
str = string(value)
case time.Duration:
str = strconv.FormatInt(int64(value), 10)
case fmt.Stringer:
str = value.String()
case error:
str = value.Error()
default:
if len(optFns) == 0 && in == nil {
return "", nil
}
opt := NewConvOption(optFns...)
if in == nil {
if opt.NilAsFail {
err = comdef.ErrConvType
}
return
}
if opt.HandlePtr {
if rv := reflect.ValueOf(in); rv.Kind() == reflect.Pointer {
rv = rv.Elem()
if checkfn.IsSimpleKind(rv.Kind()) {
return ToStringWith(rv.Interface(), optFns...)
}
}
}
if opt.UserConvFn != nil {
str, err = opt.UserConvFn(in)
} else {
err = comdef.ErrConvType
}
}
return
}
+20
View File
@@ -0,0 +1,20 @@
package comfunc
import "path/filepath"
// JoinPaths2 elements, like the filepath.Join()
func JoinPaths2(basePath string, elems []string) string {
paths := make([]string, len(elems)+1)
paths[0] = basePath
copy(paths[1:], elems)
return filepath.Join(paths...)
}
// JoinPaths3 elements, like the filepath.Join()
func JoinPaths3(basePath, secPath string, elems []string) string {
paths := make([]string, len(elems)+2)
paths[0] = basePath
paths[1] = secPath
copy(paths[2:], elems)
return filepath.Join(paths...)
}
+135
View File
@@ -0,0 +1,135 @@
package comfunc
import (
"fmt"
"strings"
)
var commentsPrefixes = []string{"#", ";", "//"}
// ParseEnvLineOption parse env line options
type ParseEnvLineOption struct {
// NotInlineComments dont parse inline comments.
// - default: false. will parse inline comments
NotInlineComments bool
// SkipOnErrorLine skip error line, continue parse next line
// - False: return error, clear parsed map
SkipOnErrorLine bool
}
// ParseEnvLines parse simple multiline k-v string to a string-map.
// Can use to parse simple INI or DOTENV file contents.
//
// NOTE:
//
// - It's like INI/ENV format contents.
// - Support comments line starts with: "#", ";", "//"
// - Support inline comments split with: " #" eg: "name=tom # a comments"
// - DON'T support submap parse.
func ParseEnvLines(text string, opt ParseEnvLineOption) (mp map[string]string, err error) {
lines := strings.Split(text, "\n")
ln := len(lines)
if ln == 0 {
return
}
strMap := make(map[string]string, ln)
for _, line := range lines {
if line = strings.TrimSpace(line); line == "" {
continue
}
// skip comments line
if line[0] == '#' || line[0] == ';' || strings.HasPrefix(line, "//") {
continue
}
key, val := splitLineByChar(line, '=', !opt.NotInlineComments)
// invalid line
if key == "" {
if opt.SkipOnErrorLine {
continue
}
strMap = nil
err = fmt.Errorf("invalid line contents: must match `KEY=VAL`(line: %s)", line)
return
}
strMap[key] = val
}
return strMap, nil
}
// SplitLineToKv parse string line to k-v, not support comments.
//
// Example:
//
// 'DEBUG=true' => ['DEBUG', 'true']
//
// NOTE: line must contain '=', allow: 'ENV_KEY='
func SplitLineToKv(line, sep string) (string, string) {
return SplitKvBySep(line, sep, false)
}
// SplitKvBySep parse string line to k-v, support parse comments.
// - rmInlineComments: check and remove inline comments by ' #'
func SplitKvBySep(line, sep string, rmInlineComments bool) (key, val string) {
sepPos := strings.Index(line, sep)
if sepPos < 0 {
return
}
return splitKvBySepPos(line, sepPos, len(sep), rmInlineComments)
}
func splitLineByChar(line string, sep byte, rmInlineComments bool) (key, val string) {
sepPos := strings.IndexByte(line, sep)
if sepPos < 0 {
return
}
return splitKvBySepPos(line, sepPos, 1, rmInlineComments)
}
func splitKvBySepPos(line string, sepPos, sepLen int, rmInlineComments bool) (key, val string) {
// key cannot be empty
key = strings.TrimSpace(line[0:sepPos])
if key == "" {
return "", ""
}
val = strings.TrimSpace(line[sepPos+sepLen:])
// check quotes if present
if vln := len(val); vln >= 2 {
// remove quotes
if (val[0] == '"' && val[vln-1] == '"') || (val[0] == '\'' && val[vln-1] == '\'') {
val = val[1 : vln-1]
return
}
if !rmInlineComments {
return
}
// value is empty, only inline comments
if val[0] == '#' {
val = ""
return
}
// remove inline comments
if pos := strings.Index(val, " #"); pos > 0 {
val = strings.TrimRight(val[0:pos], " \t")
vln = len(val)
// remove quotes
if (val[0] == '"' && val[vln-1] == '"') || (val[0] == '\'' && val[vln-1] == '\'') {
val = val[1 : vln-1]
return
}
}
}
return
}
+142
View File
@@ -0,0 +1,142 @@
package comfunc
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
// Workdir get
func Workdir() string {
dir, _ := os.Getwd()
return dir
}
// ExpandHome will parse first `~` as user home dir path.
func ExpandHome(pathStr string) string {
if len(pathStr) == 0 {
return pathStr
}
if pathStr[0] != '~' {
return pathStr
}
if len(pathStr) > 1 && pathStr[1] != '/' && pathStr[1] != '\\' {
return pathStr
}
homeDir, err := os.UserHomeDir()
if err != nil {
return pathStr
}
return homeDir + pathStr[1:]
}
// ExecCmd an command and return output.
//
// Usage:
//
// ExecCmd("ls", []string{"-al"})
func ExecCmd(binName string, args []string, workDir ...string) (string, error) {
// create a new Cmd instance
cmd := exec.Command(binName, args...)
if len(workDir) > 0 {
cmd.Dir = workDir[0]
}
bs, err := cmd.Output()
return string(bs), err
}
var (
cmdList = []string{"cmd", "cmd.exe"}
pwshList = []string{"powershell", "powershell.exe", "pwsh", "pwsh.exe"}
)
// ShellExec exec command by shell
// cmdLine e.g. "ls -al"
func ShellExec(cmdLine string, shells ...string) (string, error) {
// shell := "/bin/sh"
shell := "sh"
if len(shells) > 0 {
shell = shells[0]
}
cmd := exec.Command(shell, "-c", cmdLine)
bs, err := cmd.Output()
return string(bs), err
}
// curShellCache value
var curShellCache string
// CurrentShell get current used shell env file.
//
// return like: "/bin/zsh" "/bin/bash". if onlyName=true, will return "zsh", "bash"
func CurrentShell(onlyName bool, fallbackShell ...string) (binPath string) {
var err error
fbShell := ""
if len(fallbackShell) > 0 {
fbShell = fallbackShell[0]
}
if curShellCache == "" {
// 检查父进程名称
parentProcess := os.Getenv("GOPROCESS")
if parentProcess != "" {
binPath = parentProcess
} else {
binPath = os.Getenv("SHELL") // 适用于 Unix-like 系统
if len(binPath) == 0 {
// TODO check on Windows git bash
binPath, err = ShellExec("echo $SHELL")
if err != nil {
binPath = fbShell
}
}
binPath = strings.TrimSpace(binPath)
}
// fix: 去除 .exe 后缀
if pos := strings.IndexByte(binPath, '.'); pos > 0 {
binPath = binPath[:pos]
}
// cache result
curShellCache = binPath
} else {
binPath = curShellCache
}
if onlyName && len(binPath) > 0 {
binPath = filepath.Base(binPath)
} else if len(binPath) == 0 {
binPath = fbShell
}
return
}
func checkWinCurrentShell() string {
// 在 Windows 上,可以检查 COMSPEC 环境变量
comSpec := os.Getenv("COMSPEC")
// 没法检查 pwsh, 返回的还是 cmd
return comSpec
}
// HasShellEnv has shell env check.
//
// Usage:
//
// HasShellEnv("sh")
// HasShellEnv("bash")
func HasShellEnv(shell string) bool {
// can also use: "echo $0"
out, err := ShellExec("echo OK", shell)
if err != nil {
return false
}
return strings.TrimSpace(out) == "OK"
}
+149
View File
@@ -0,0 +1,149 @@
package comfunc
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
"time"
)
var (
// check is duration string. TIP: extend unit d,w. eg: "1d", "2w"
//
// time.ParseDuration() is max support hour "h".
durStrReg = regexp.MustCompile(`^-?([0-9]+(?:\.[0-9]*)?(ns|us|µs|ms|s|m|h|d|w))+$`)
// check long duration string. 验证整体格式是否符合
//
// eg: "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks", "1month"
//
// time.ParseDuration() is not support long unit.
durStrRegL = regexp.MustCompile(`^-?([0-9]+(?:\.[0-9]*)?[nuµsmhdw][a-zA-Z]{0,8})+$`)
// use for parse duration string. see ToDuration()
//
// NOTE: 解析时,不能加最后的 `+` 会导致只匹配了最后一组 时间单位
durStrRegL2 = regexp.MustCompile(`-?([0-9]+(?:\.[0-9]*)?)([nuµsmhdw][a-z]{0,8})`)
)
// IsDuration check the string is a duration string.
func IsDuration(s string) bool {
if s == "0" || durStrReg.MatchString(s) {
return true
}
return durStrRegL.MatchString(s)
}
// ToDuration parses a duration string. such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
//
// Diff of time.ParseDuration:
// - support extends unit d, w at the end of string. such as "1d", "2w".
// - support extends unit: month, week, day
// - support long string unit at the end. such as "1hour", "2hours", "3minutes", "4mins", "5days", "1weeks".
//
// If the string is not a valid duration string, it will return an error.
func ToDuration(s string) (time.Duration, error) {
ln := len(s)
if ln == 0 {
return 0, fmt.Errorf("empty duration string")
}
s = strings.ToLower(s)
if s == "0" {
return 0, nil
}
// check duration string is valid
if !durStrRegL.MatchString(s) {
return 0, fmt.Errorf("invalid duration string: %s", s)
}
// if ln < 4 AND end != d|w, directly call time.ParseDuration()
if ln < 4 && s[ln-1] != 'd' && s[ln-1] != 'w' {
return time.ParseDuration(s)
}
// time.ParseDuration() is not support long unit.
ssList := durStrRegL2.FindAllStringSubmatch(s, -1)
// fmt.Println(ssList)
bts := make([]byte, 0, ln)
if s[0] == '-' {
bts = append(bts, '-')
}
// only one element. eg: "1day"
if len(ssList) == 1 {
bts = parseLongUnit(ssList[0], bts)
} else {
// more than one element. eg: "1day2hour3min"
for _, ss := range ssList {
if len(ss) == 3 {
bts = parseLongUnit(ss, bts)
}
}
}
return time.ParseDuration(string(bts))
}
// convert to short unit
func parseLongUnit(ss []string, bts []byte) []byte {
// eg: "3sec" -> ss=[3sec, -3, sec]
num, unit := ss[1], ss[2]
switch unit {
case "month", "months":
// time lib max unit is hour, so need convert by 24 * 30*n
bts = appendNumToBytes(bts, num, 24*30)
bts = append(bts, 'h')
case "w", "week", "weeks":
// time lib max unit is hour, so need convert by 24 * 7*n
bts = appendNumToBytes(bts, num, 24*7)
bts = append(bts, 'h')
case "d", "day", "days":
// time lib max unit is hour, so need convert by 24*n
bts = appendNumToBytes(bts, num, 24)
bts = append(bts, 'h')
case "hour", "hours":
bts = append(bts, num...)
bts = append(bts, 'h')
case "min", "mins", "minute", "minutes":
bts = append(bts, num...)
bts = append(bts, 'm')
case "sec", "secs", "second", "seconds":
bts = append(bts, num...)
bts = append(bts, 's')
default:
first := ss[0]
// '-' has been added on ToDuration()
if first[0] == '-' {
bts = append(bts, first[1:]...)
} else {
bts = append(bts, first...)
}
}
return bts
}
func appendNumToBytes(bts []byte, num string, multiple int) []byte {
if strings.ContainsRune(num, '.') {
f, _ := strconv.ParseFloat(num, 64) // is float number
val := f * float64(multiple)
// 使用 Float 保留两位小数 -> 会始终有两位小数,即使是N.00
// bts = strconv.AppendFloat(bts, val, 'f', 2, 64)
// 四舍五入到两位小数
rounded := math.Round(val*100) / 100
// 使用 AppendFloat 自动去除末尾的 .0 或 .00
bts = strconv.AppendFloat(bts, rounded, 'f', -1, 64)
} else {
n, _ := strconv.Atoi(num)
bts = strconv.AppendInt(bts, int64(n*multiple), 10)
}
return bts
}