431 lines
14 KiB
Go
431 lines
14 KiB
Go
// ======================================================================================= go toolbox (mwx'2026)
|
|
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/AlecAivazis/survey/v2/core"
|
|
"github.com/AlecAivazis/survey/v2/terminal"
|
|
"github.com/eknkc/basex"
|
|
"github.com/fatih/color"
|
|
"github.com/mgutz/ansi"
|
|
"github.com/spf13/viper"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
var tbversion = "0.7.0"
|
|
|
|
var CHRS = "VW9IdGJ6eXh1T25DRHdrc2M5MlhOQVNQcEJFWnJhWVY2ZEowaFJLdmoxNUdxVDRJZkZpTTdRZW0zTFc4Z2w="
|
|
var LR = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
|
|
// Updating oneself lives in selfupdate.go — one file per program, configured at
|
|
// its head, driven by the --update and --check-update options. It fetches from
|
|
// the releases of the gitea and weighs every download against the checksum
|
|
// published beside it.
|
|
|
|
func checkaccess(NETS []string) { // ------------------------------------------------- check ip net based access
|
|
match:=0;
|
|
addrs, err := net.InterfaceAddrs()
|
|
if err != nil { PE("Error getting addresses", err.Error()); os.Exit(1) }
|
|
|
|
for _, validnet := range NETS {
|
|
_, ipNet, err := net.ParseCIDR(validnet)
|
|
if err != nil { PE("invalid network", validnet); continue }
|
|
for _, address := range addrs {
|
|
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
|
if ipnet.IP.To4() != nil {
|
|
if (ipNet.Contains(ipnet.IP)) { match++ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (match==0) { PE("access violation, permission denied"); os.Exit(1) }
|
|
}
|
|
|
|
|
|
func Enc(str string) string { // ----------------------------------------------------------------- encode string
|
|
enc, _ := basex.NewEncoding(Db64(CHRS))
|
|
return enc.Encode([]byte(Rndstr(2) + str))
|
|
}
|
|
|
|
func Dec(str string) string { // ----------------------------------------------------------------- decode string
|
|
enc, err := basex.NewEncoding(Db64(CHRS))
|
|
if err != nil { return "" }
|
|
b, err := enc.Decode(str)
|
|
if err != nil || len(b) < 2 { return "" }
|
|
return string(b[2:])
|
|
}
|
|
|
|
func Db64(txt string) string { // ------------------------------------------------------------- string to base64
|
|
d, _ := base64.StdEncoding.DecodeString(txt)
|
|
return string(d)
|
|
}
|
|
|
|
func Rndstr(n int) string { // ------------------------------------------------------- random string with length
|
|
b := make([]rune, n)
|
|
for i := range b {
|
|
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(LR))))
|
|
b[i] = LR[n.Int64()]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func Input(msg string, def string) string { // -------------------------------- AlecAivazis/survey: input string
|
|
tmp := ""
|
|
err := survey.AskOne(&survey.Input{Message: msg, Default: def}, &tmp)
|
|
if err != nil {
|
|
if err == terminal.InterruptErr {
|
|
P(Crb("Interrupted."))
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
return tmp
|
|
}
|
|
|
|
func Inputpw(msg string) string { // ---------------------------------------- AlecAivazis/survey: input password
|
|
tmp := ""
|
|
err := survey.AskOne(&survey.Password{Message: msg}, &tmp)
|
|
if err != nil {
|
|
if err == terminal.InterruptErr {
|
|
P(Crb("Interrupted."))
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
return tmp
|
|
}
|
|
|
|
func Yesno(msg string, def bool, overwrite bool) bool { // -------------------------- AlecAivazis/survey: yes/no
|
|
|
|
if (overwrite) { return true }
|
|
|
|
var err error
|
|
tmp := ""
|
|
if def {
|
|
err = survey.AskOne(&survey.Select{Message: msg, Options: []string{"Yes", "No"}}, &tmp)
|
|
} else {
|
|
err = survey.AskOne(&survey.Select{Message: msg, Options: []string{"No", "Yes"}}, &tmp)
|
|
}
|
|
|
|
if err != nil {
|
|
if err == terminal.InterruptErr {
|
|
P(Crb("Interrupted."))
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
if tmp == "Yes" {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func Getid(n int) string { // --------------------------------------------------------- get base62 random string
|
|
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
ret := make([]byte, n)
|
|
for i := 0; i < n; i++ {
|
|
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
|
ret[i] = letters[num.Int64()]
|
|
}
|
|
return string(ret)
|
|
}
|
|
|
|
func GETid(n int) string { // --------------------------------------------------------- get base36 random string
|
|
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
ret := make([]byte, n)
|
|
for i := 0; i < n; i++ {
|
|
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
|
ret[i] = letters[num.Int64()]
|
|
}
|
|
return string(ret)
|
|
}
|
|
|
|
func Mytable(rows *sql.Rows) []map[string]interface{} { // ----------------------------- load mysql result table
|
|
defer rows.Close()
|
|
columns, err := rows.Columns()
|
|
if err != nil { return nil }
|
|
count := len(columns)
|
|
tableData := make([]map[string]interface{}, 0)
|
|
values := make([]interface{}, count)
|
|
valuePtrs := make([]interface{}, count)
|
|
for rows.Next() {
|
|
for i := 0; i < count; i++ {
|
|
valuePtrs[i] = &values[i]
|
|
}
|
|
if err := rows.Scan(valuePtrs...); err != nil { break }
|
|
entry := make(map[string]interface{})
|
|
for i, col := range columns {
|
|
var v interface{}
|
|
val := values[i]
|
|
b, ok := val.([]byte)
|
|
if ok {
|
|
v = string(b)
|
|
} else {
|
|
v = val
|
|
}
|
|
entry[col] = v
|
|
}
|
|
tableData = append(tableData, entry)
|
|
}
|
|
return (tableData)
|
|
}
|
|
|
|
func Checkip(network string, ip string) bool { // ---------- check if ip is in range (cidr address or single ip)
|
|
if net.ParseIP(ip) == nil {
|
|
return false
|
|
}
|
|
_, subnet, err := net.ParseCIDR(network)
|
|
if err == nil {
|
|
if subnet.Contains(net.ParseIP(ip)) {
|
|
return true
|
|
}
|
|
} else {
|
|
if network == ip {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func Isflagpassed(name string) bool { // -------------------------------------------------- check if flag is set
|
|
found := false
|
|
flag.Visit(func(f *flag.Flag) {
|
|
if f.Name == name {
|
|
found = true
|
|
}
|
|
})
|
|
return found
|
|
}
|
|
|
|
func Body(r *http.Response) string { // ------------------------------------------------------------ http body
|
|
defer r.Body.Close()
|
|
body, err := io.ReadAll(r.Body)
|
|
if err == nil {
|
|
return string(body)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func Atoi(s string) int { // ------------------------------------------------------------------------------ atoi
|
|
i, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return i
|
|
}
|
|
|
|
func Itoa(i int) string { // ------------------------------------------------------------------------------ itoa
|
|
return strconv.Itoa(i)
|
|
}
|
|
|
|
func GJA(j string, k string) []string { // -------------------------------------------------- convert gjson array
|
|
var ret []string
|
|
for _, c := range gjson.Get(j, k).Array() {
|
|
ret = append(ret, c.String())
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------ print simple
|
|
|
|
func P(a ...any) (n int, err error) { return fmt.Fprintln(os.Stdout, a...) }
|
|
func PN(a ...any) (n int, err error) { return fmt.Fprint(os.Stdout, a...) }
|
|
func PF(format string, a ...any) (n int, err error) { return fmt.Fprintf(os.Stdout, format, a...) }
|
|
func SF(format string, a ...any) string { return fmt.Sprintf(format, a...) }
|
|
func SE(format string, a ...any) error { return fmt.Errorf(format, a...) }
|
|
|
|
func PE(msg ...string) (n int, err error) {
|
|
if (len(msg)==0) { return 0, nil }
|
|
if (len(msg)>=2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Crb("ERROR"), Cwb(msg[0]),msg[1]) }
|
|
return fmt.Fprintf(os.Stdout, "%s: %s\n", Crb("ERROR"), Cwb(msg[0]))
|
|
}
|
|
func PO(msg ...string) (n int, err error) {
|
|
if (len(msg)==0) { return 0, nil }
|
|
if (len(msg)>=2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Cgb("OK"), Cwb(msg[0]),msg[1]) }
|
|
return fmt.Fprintf(os.Stdout, "%s: %s\n", Cgb("OK"), Cwb(msg[0]))
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------ regular expression
|
|
|
|
func ReplaceFirst(re *regexp.Regexp, str, replace string) string {
|
|
loc := re.FindStringIndex(str)
|
|
if loc == nil { return str }
|
|
return str[:loc[0]] + replace + str[loc[1]:]
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------------- string functions
|
|
|
|
func Shortstr(s string, length int) string {
|
|
runes := []rune(s)
|
|
if len(runes) <= length {
|
|
return s
|
|
}
|
|
if length <= 2 {
|
|
return ".."
|
|
}
|
|
return string(runes[:length-2]) + ".."
|
|
}
|
|
|
|
func SR(str string, n int) string { return strings.Repeat(str, n) }
|
|
|
|
func RemoveAllMatches(slice []string, target string) []string { // remove matching string from array
|
|
result := make([]string, 0, len(slice)) // never modify the callers slice
|
|
for _, v := range slice {
|
|
if v != target {
|
|
result = append(result, v)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------------- system functions
|
|
|
|
func prgname() string { // program name
|
|
exepath, err := os.Executable()
|
|
if err != nil {
|
|
PE(SF("Error getting executable path: %s", err))
|
|
return ""
|
|
}
|
|
exename := filepath.Base(exepath)
|
|
return strings.TrimSuffix(exename, ".exe") // windows: gbld.exe -> gbld
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------ viper abbrevations
|
|
|
|
func VX(key string) bool { return viper.IsSet(key) }
|
|
func VS(key string) string { return viper.GetString(key) }
|
|
func VSS(key string) []string { return viper.GetStringSlice(key) }
|
|
func VB(key string) bool { return viper.GetBool(key) }
|
|
func VI(key string) int { return viper.GetInt(key) }
|
|
|
|
// ------------------------------------------------------------------------------------------------- text colors
|
|
//
|
|
// catppuccin mocha - https://terminalcolors.com/themes/catppuccin/mocha/
|
|
// the palette is emitted as 24 bit color. terminals that do not announce truecolor via COLORTERM
|
|
// fall back to the basic ansi colors, so their own theme keeps deciding. NO_COLOR is honoured.
|
|
|
|
type rgb struct{ r, g, b int }
|
|
|
|
var ( // ------------------------------------------------------------------------- catppuccin mocha palette
|
|
CatRosewater = rgb{0xf5, 0xe0, 0xdc}
|
|
CatFlamingo = rgb{0xf2, 0xcd, 0xcd}
|
|
CatPink = rgb{0xf5, 0xc2, 0xe7}
|
|
CatMauve = rgb{0xcb, 0xa6, 0xf7}
|
|
CatRed = rgb{0xf3, 0x8b, 0xa8}
|
|
CatMaroon = rgb{0xeb, 0xa0, 0xac}
|
|
CatPeach = rgb{0xfa, 0xb3, 0x87}
|
|
CatYellow = rgb{0xf9, 0xe2, 0xaf}
|
|
CatGreen = rgb{0xa6, 0xe3, 0xa1}
|
|
CatTeal = rgb{0x94, 0xe2, 0xd5}
|
|
CatSky = rgb{0x89, 0xdc, 0xeb}
|
|
CatSapphire = rgb{0x74, 0xc7, 0xec}
|
|
CatBlue = rgb{0x89, 0xb4, 0xfa}
|
|
CatLavender = rgb{0xb4, 0xbe, 0xfe}
|
|
CatText = rgb{0xcd, 0xd6, 0xf4}
|
|
CatSubtext1 = rgb{0xba, 0xc2, 0xde}
|
|
CatSubtext0 = rgb{0xa6, 0xad, 0xc8}
|
|
CatOverlay1 = rgb{0x7f, 0x84, 0x9c}
|
|
CatSurface1 = rgb{0x45, 0x47, 0x5a}
|
|
)
|
|
|
|
var catansi = map[string]rgb{ // ------------------------------- how mocha fills the ansi slots (bright = normal)
|
|
"black": CatSurface1,
|
|
"red": CatRed,
|
|
"green": CatGreen,
|
|
"yellow": CatYellow,
|
|
"blue": CatBlue,
|
|
"magenta": CatPink,
|
|
"cyan": CatTeal,
|
|
"white": CatSubtext1,
|
|
"default": CatText,
|
|
}
|
|
|
|
func truecolor() bool { // ------------------------------------------ does the terminal understand 24 bit color
|
|
ct := strings.ToLower(os.Getenv("COLORTERM"))
|
|
return ct == "truecolor" || ct == "24bit"
|
|
}
|
|
|
|
func cfunc(c rgb, fallback color.Attribute, attrs ...color.Attribute) func(...interface{}) string {
|
|
col := color.New(attrs...)
|
|
if truecolor() {
|
|
col.AddRGB(c.r, c.g, c.b)
|
|
} else {
|
|
col.Add(fallback)
|
|
}
|
|
return col.SprintFunc()
|
|
}
|
|
|
|
// mocha maps its ansi slots to red/green/yellow/blue/pink/teal, bright is identical to normal -
|
|
// so the bold variants keep the hue and only add weight, exactly as before.
|
|
|
|
var Cr func(...interface{}) string = cfunc(CatRed, color.FgRed)
|
|
var Cg func(...interface{}) string = cfunc(CatGreen, color.FgGreen)
|
|
var Cy func(...interface{}) string = cfunc(CatYellow, color.FgYellow)
|
|
var Cb func(...interface{}) string = cfunc(CatBlue, color.FgBlue)
|
|
var Cm func(...interface{}) string = cfunc(CatPink, color.FgMagenta)
|
|
var Cc func(...interface{}) string = cfunc(CatTeal, color.FgCyan)
|
|
var Cw func(...interface{}) string = cfunc(CatSubtext1, color.FgWhite)
|
|
|
|
var Crb func(...interface{}) string = cfunc(CatRed, color.FgRed, color.Bold)
|
|
var Cgb func(...interface{}) string = cfunc(CatGreen, color.FgGreen, color.Bold)
|
|
var Cyb func(...interface{}) string = cfunc(CatYellow, color.FgYellow, color.Bold)
|
|
var Cbb func(...interface{}) string = cfunc(CatBlue, color.FgBlue, color.Bold)
|
|
var Cmb func(...interface{}) string = cfunc(CatPink, color.FgMagenta, color.Bold)
|
|
var Ccb func(...interface{}) string = cfunc(CatTeal, color.FgCyan, color.Bold)
|
|
var Cwb func(...interface{}) string = cfunc(CatText, color.FgWhite, color.Bold) // text: the themes foreground
|
|
|
|
// ------------------------------------------------------------------------------------- survey prompt colors
|
|
//
|
|
// survey renders its prompts from templates that call {{color "green+hb"}} and friends, which normally
|
|
// ends up in mgutz/ansi - and that only speaks 16/256 colors. replacing the template function is enough
|
|
// to theme every prompt including its icons. survey computes its layout from a separate color free
|
|
// rendering, so the longer truecolor sequences cannot disturb the cursor placement.
|
|
|
|
func init() { core.TemplateFuncsWithColor["color"] = catcolor } // must run before the first prompt is parsed
|
|
|
|
func catcolor(style string) string { // ------------------------- mgutz/ansi style string -> catppuccin mocha
|
|
if style == "reset" {
|
|
return "\033[0m"
|
|
}
|
|
if !truecolor() || strings.Contains(style, ":") { // background colors stay with the original mapping
|
|
return ansi.ColorCode(style)
|
|
}
|
|
|
|
name, attrs, _ := strings.Cut(style, "+")
|
|
c, ok := catansi[name]
|
|
if !ok {
|
|
return ansi.ColorCode(style)
|
|
}
|
|
|
|
seq := ""
|
|
for _, a := range attrs { // 'h' (bright) is a no-op: mocha uses the same value for both
|
|
switch a {
|
|
case 'b':
|
|
seq += "1;"
|
|
case 'd':
|
|
seq += "2;"
|
|
case 'u':
|
|
seq += "4;"
|
|
case 'i':
|
|
seq += "7;"
|
|
}
|
|
}
|
|
return SF("\033[%s38;2;%d;%d;%dm", seq, c.r, c.g, c.b)
|
|
}
|
|
|
|
// ========================================================================================================= END |