2022-02-20 00:22:03 +00:00
|
|
|
// Copyright 2022 Robert S. Muhlestein.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
2022-02-17 22:47:28 +00:00
|
|
|
|
2022-03-23 08:31:00 +00:00
|
|
|
/*
|
2022-04-01 10:12:53 +00:00
|
|
|
Package Z (bonzai) provides a rooted node tree of commands and singular
|
2022-03-23 08:31:00 +00:00
|
|
|
parameters making tab completion a breeze and complicated applications
|
|
|
|
much easier to intuit without reading all the docs. Documentation is
|
|
|
|
embedded with each command removing the need for separate man pages and
|
|
|
|
such and can be viewed as text or a locally served web page.
|
|
|
|
|
|
|
|
Rooted Node Tree
|
|
|
|
|
|
|
|
Commands and parameters are linked to create a rooted node tree of the
|
|
|
|
following types of nodes:
|
|
|
|
|
|
|
|
* Leaves with a method and optional parameters
|
|
|
|
* Branches with leaves, other branches, and a optional method
|
|
|
|
* Parameters, single words that are passed to a leaf command
|
|
|
|
|
|
|
|
*/
|
2022-04-01 10:12:53 +00:00
|
|
|
package Z
|
2022-02-17 04:31:12 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
2022-03-23 08:31:00 +00:00
|
|
|
"path/filepath"
|
2022-02-17 04:31:12 +00:00
|
|
|
"strings"
|
2022-03-23 08:31:00 +00:00
|
|
|
|
2022-03-27 11:12:13 +00:00
|
|
|
config "github.com/rwxrob/config/pkg"
|
2022-04-01 10:12:53 +00:00
|
|
|
"github.com/rwxrob/fn"
|
2022-03-23 08:31:00 +00:00
|
|
|
"github.com/rwxrob/fs/file"
|
2022-03-28 06:20:54 +00:00
|
|
|
"github.com/rwxrob/term"
|
2022-02-17 04:31:12 +00:00
|
|
|
)
|
|
|
|
|
2022-03-23 08:31:00 +00:00
|
|
|
func init() {
|
|
|
|
var err error
|
|
|
|
// get the full path to current running process executable
|
2022-03-26 21:12:50 +00:00
|
|
|
ExePath, err = os.Executable()
|
2022-03-23 08:31:00 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Print(err)
|
|
|
|
return
|
|
|
|
}
|
2022-03-26 21:12:50 +00:00
|
|
|
ExePath, err = filepath.EvalSymlinks(ExePath)
|
2022-03-23 08:31:00 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Print(err)
|
|
|
|
}
|
2022-03-26 21:12:50 +00:00
|
|
|
ExeName = strings.TrimSuffix(
|
|
|
|
filepath.Base(ExePath), filepath.Ext(ExePath))
|
2022-03-23 08:31:00 +00:00
|
|
|
}
|
|
|
|
|
2022-03-26 21:12:50 +00:00
|
|
|
// ExePath holds the full path to the current running process executable
|
2022-03-23 08:31:00 +00:00
|
|
|
// which is determined at init() time by calling os.Executable and
|
|
|
|
// passing it to path/filepath.EvalSymlinks to ensure it is the actual
|
|
|
|
// binary executable file. Errors are reported to stderr, but there
|
|
|
|
// should never be an error logged unless something is wrong with the Go
|
|
|
|
// runtime environment.
|
2022-03-26 21:12:50 +00:00
|
|
|
var ExePath string
|
|
|
|
|
|
|
|
// ExeName holds just the base name of the executable without any suffix
|
2022-03-27 08:53:11 +00:00
|
|
|
// (ex: .exe) and is set at init() time (see ExePath).
|
2022-03-26 21:12:50 +00:00
|
|
|
var ExeName string
|
2022-03-23 08:31:00 +00:00
|
|
|
|
2022-03-30 19:25:12 +00:00
|
|
|
// Commands contains the commands to lookup when Run-ing an executable
|
|
|
|
// in "multicall" mode. Each value must begin with a *Cmd and the rest
|
|
|
|
// will be assumed to be string arguments to prepend. See Run.
|
|
|
|
var Commands map[string][]any
|
|
|
|
|
|
|
|
// Run infers the name of the command to run from the ExeName looked up
|
|
|
|
// in the Commands delegates accordingly, prepending any arguments
|
|
|
|
// provided in the CmdRun. Run produces an "unmapped multicall command"
|
|
|
|
// error if no match is found. This is an alternative to the simpler,
|
|
|
|
// direct Cmd.Run method from main where only one possible Cmd will ever
|
|
|
|
// be the root and allows for BusyBox (https://www.busybox.net)
|
|
|
|
// multicall binaries to be used for such things as very light-weight
|
|
|
|
// Linux distributions when used "FROM SCRATCH" in containers.
|
|
|
|
func Run() {
|
|
|
|
if v, has := Commands[ExeName]; has {
|
|
|
|
if len(v) < 1 {
|
|
|
|
ExitError(fmt.Errorf("multicall command missing"))
|
|
|
|
}
|
|
|
|
cmd, iscmd := v[0].(*Cmd)
|
|
|
|
if !iscmd {
|
|
|
|
ExitError(fmt.Errorf("first value must be *Cmd"))
|
|
|
|
}
|
|
|
|
args := []string{cmd.Name}
|
|
|
|
if len(v) > 1 {
|
|
|
|
rest := os.Args[1:]
|
|
|
|
for _, a := range v[1:] {
|
|
|
|
s, isstring := a.(string)
|
|
|
|
if !isstring {
|
|
|
|
ExitError(fmt.Errorf("only string arguments allowed"))
|
|
|
|
}
|
|
|
|
args = append(args, s)
|
|
|
|
}
|
|
|
|
args = append(args, rest...)
|
|
|
|
}
|
|
|
|
os.Args = args
|
|
|
|
cmd.Run()
|
|
|
|
Exit()
|
|
|
|
}
|
|
|
|
ExitError(fmt.Errorf("unmapped multicall command: %v", ExeName))
|
|
|
|
}
|
|
|
|
|
2022-03-27 11:12:13 +00:00
|
|
|
// DefaultConfigurer is assigned to the Cmd.Root.Config during Cmd.Run.
|
|
|
|
// It is conventional for only Cmd.Root to have a Configurer defined.
|
|
|
|
var DefaultConfigurer = new(config.Configurer)
|
|
|
|
|
2022-03-23 08:31:00 +00:00
|
|
|
// ReplaceSelf replaces the current running executable at its current
|
|
|
|
// location with the successfully retrieved file at the specified URL or
|
|
|
|
// file path and duplicates the original files permissions. Only http
|
|
|
|
// and https URLs are currently supported. If not empty, a checksum file
|
|
|
|
// will be fetched from sumurl and used to validate the download before
|
|
|
|
// making the replacement. For security reasons, no backup copy of the
|
|
|
|
// replaced executable is kept. Also see AutoUpdate.
|
2022-03-27 07:09:06 +00:00
|
|
|
func ReplaceSelf(url, checksum string) error {
|
2022-03-23 08:31:00 +00:00
|
|
|
exe, err := os.Executable()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
exe, err = filepath.EvalSymlinks(exe)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// TODO validate sum
|
|
|
|
return file.Replace(exe, url)
|
|
|
|
}
|
|
|
|
|
2022-03-27 07:09:06 +00:00
|
|
|
// AutoUpdate automatically updates the current process executable
|
|
|
|
// version by starting a goroutine that checks the current semantic
|
|
|
|
// version (version) against a remote one (versurl) and calling
|
|
|
|
// ReplaceSelf with the URL of the binary (binurl) and checksum (sumurl)
|
|
|
|
// if and update is needed. Note that the binary will often be named
|
|
|
|
// quite differently than the name of the currently running executable
|
|
|
|
// (ex: foo-mac -> foo, foo-linux.
|
2022-03-23 08:31:00 +00:00
|
|
|
//
|
2022-03-27 07:09:06 +00:00
|
|
|
// If a URL to a checksum file (sumurl) is not empty will optionally
|
2022-03-23 08:31:00 +00:00
|
|
|
// validate the new version downloaded against the checksum before
|
2022-03-27 07:09:06 +00:00
|
|
|
// replacing the currently running process executable with the new one.
|
2022-03-23 08:31:00 +00:00
|
|
|
// The format of the checksum file is the same as that output by any of
|
|
|
|
// the major checksum commands (sha512sum, for example) with one or more
|
|
|
|
// lines beginning with the checksum, whitespace, and then the name of
|
|
|
|
// the file. A single checksum file can be used for multiple versions
|
|
|
|
// but the most recent should always be at the top. When the update
|
|
|
|
// completes a message notifying of the update is logged to stderr.
|
|
|
|
//
|
2022-03-27 07:09:06 +00:00
|
|
|
// The function will fail silently under any of the following
|
|
|
|
// conditions:
|
|
|
|
//
|
|
|
|
// * current user does not have write access to executable
|
|
|
|
// * unable to establish a network connection
|
|
|
|
// * checksum provided does not match
|
|
|
|
//
|
2022-03-23 08:31:00 +00:00
|
|
|
// Since AutoUpdate happens in the background no return value is
|
2022-03-27 07:09:06 +00:00
|
|
|
// provided. To enable logging of the update process (presumably for
|
|
|
|
// debugging) add the AutoUpdate flag to the Trace flags
|
|
|
|
// (trace.Flags|=trace.AutoUpdate). Also see Cmd.AutoUpdate.
|
|
|
|
func AutoUpdate(version, versurl, binurl, sumurl string) {
|
2022-03-23 08:31:00 +00:00
|
|
|
// TODO
|
|
|
|
}
|
|
|
|
|
2022-02-17 04:31:12 +00:00
|
|
|
// Method defines the main code to execute for a command (Cmd). By
|
2022-02-24 13:06:10 +00:00
|
|
|
// convention the parameter list should be named "args" if there are
|
|
|
|
// args expected and underscore (_) if not. Methods must never write
|
|
|
|
// error output to anything but standard error and should almost always
|
|
|
|
// use the log package to do so.
|
2022-02-24 12:33:54 +00:00
|
|
|
type Method func(caller *Cmd, args ...string) error
|
2022-02-17 04:31:12 +00:00
|
|
|
|
|
|
|
// ----------------------- errors, exit, debug -----------------------
|
|
|
|
|
2022-02-17 23:03:19 +00:00
|
|
|
// DoNotExit effectively disables Exit and ExitError allowing the
|
|
|
|
// program to continue running, usually for test evaluation.
|
2022-02-17 04:31:12 +00:00
|
|
|
var DoNotExit bool
|
|
|
|
|
|
|
|
// ExitOff sets DoNotExit to false.
|
|
|
|
func ExitOff() { DoNotExit = true }
|
|
|
|
|
|
|
|
// ExitOn sets DoNotExit to true.
|
|
|
|
func ExitOn() { DoNotExit = false }
|
|
|
|
|
2022-02-17 23:05:23 +00:00
|
|
|
// Exit calls os.Exit(0) unless DoNotExit has been set to true. Cmds
|
|
|
|
// should never call Exit themselves returning a nil error from their
|
|
|
|
// Methods instead.
|
2022-02-17 04:31:12 +00:00
|
|
|
func Exit() {
|
|
|
|
if !DoNotExit {
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ExitError prints err and exits with 1 return value unless DoNotExit
|
2022-02-17 23:05:23 +00:00
|
|
|
// has been set to true. Commands should usually never call ExitError
|
|
|
|
// themselves returning an error from their Method instead.
|
2022-02-17 04:31:12 +00:00
|
|
|
func ExitError(err ...interface{}) {
|
|
|
|
switch e := err[0].(type) {
|
|
|
|
case string:
|
|
|
|
if len(e) > 1 {
|
2022-02-18 03:06:48 +00:00
|
|
|
log.Printf(e+"\n", err[1:]...)
|
|
|
|
} else {
|
|
|
|
log.Println(e)
|
2022-02-17 04:31:12 +00:00
|
|
|
}
|
|
|
|
case error:
|
|
|
|
out := fmt.Sprintf("%v", e)
|
|
|
|
if len(out) > 0 {
|
|
|
|
log.Println(out)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !DoNotExit {
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-17 23:05:23 +00:00
|
|
|
// ArgsFrom returns a list of field strings split on space with an extra
|
2022-02-17 04:31:12 +00:00
|
|
|
// trailing special space item appended if the line has any trailing
|
|
|
|
// spaces at all signifying a definite word boundary and not a potential
|
|
|
|
// prefix.
|
|
|
|
func ArgsFrom(line string) []string {
|
|
|
|
args := []string{}
|
|
|
|
if line == "" {
|
|
|
|
return args
|
|
|
|
}
|
|
|
|
args = strings.Fields(line)
|
|
|
|
if line[len(line)-1] == ' ' {
|
2022-02-25 00:24:48 +00:00
|
|
|
args = append(args, "")
|
2022-02-17 04:31:12 +00:00
|
|
|
}
|
|
|
|
return args
|
|
|
|
}
|
2022-03-23 08:31:00 +00:00
|
|
|
|
2022-03-28 06:20:54 +00:00
|
|
|
// ArgsOrIn takes an slice or nil as argument and if the slice has any
|
|
|
|
// length greater than 0 returns all the argument joined together with
|
|
|
|
// a single space between them. Otherwise, will read standard input
|
|
|
|
// until end of file reached (Cntl-D).
|
|
|
|
func ArgsOrIn(args []string) string {
|
|
|
|
if args == nil || len(args) == 0 {
|
|
|
|
return term.Read()
|
|
|
|
}
|
|
|
|
return strings.Join(args, " ")
|
|
|
|
}
|
|
|
|
|
2022-03-28 16:32:05 +00:00
|
|
|
// Aliases allows Bonzai tree developers to create aliases (similar to
|
|
|
|
// shell aliases) that are directly translated into arguments to the
|
|
|
|
// Bonzai tree executable by overriding the os.Args in a controlled way.
|
|
|
|
// The value of an alias is always a slice of strings that will replace
|
|
|
|
// the os.Args[2:]. A slice is used (instead of a string parsed with
|
|
|
|
// strings.Fields) to ensure that hard-coded arguments containing
|
|
|
|
// whitespace are properly handled.
|
|
|
|
var Aliases = make(map[string][]string)
|
2022-04-01 10:12:53 +00:00
|
|
|
|
|
|
|
// EscThese is set to the default UNIX shell characters which require
|
|
|
|
// escaping to be used safely on the terminal. It can be changed to suit
|
|
|
|
// the needs of different host shell environments.
|
|
|
|
var EscThese = " \r\t\n|&;()<>![]"
|
|
|
|
|
|
|
|
// Esc returns a shell-escaped version of the string s. The returned value
|
|
|
|
// is a string that can safely be used as one token in a shell command line.
|
|
|
|
func Esc(s string) string {
|
|
|
|
var buf []rune
|
|
|
|
for _, r := range s {
|
|
|
|
for _, esc := range EscThese {
|
|
|
|
if r == esc {
|
|
|
|
buf = append(buf, '\\')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
buf = append(buf, r)
|
|
|
|
}
|
|
|
|
return string(buf)
|
|
|
|
}
|
|
|
|
|
|
|
|
// EscAll calls Esc on all passed strings.
|
|
|
|
func EscAll(args []string) []string { return fn.Map(args, Esc) }
|