bonzai/bonzai.go

270 lines
9.0 KiB
Go
Raw Normal View History

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-04-01 10:12:53 +00:00
Package Z (bonzai) provides a rooted node tree of commands and singular
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"
"path/filepath"
2022-02-17 04:31:12 +00:00
"strings"
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"
"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
)
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()
if err != nil {
log.Print(err)
return
}
2022-03-26 21:12:50 +00:00
ExePath, err = filepath.EvalSymlinks(ExePath)
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-26 21:12:50 +00:00
// ExePath holds the full path to the current running process executable
// 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
// (ex: .exe) and is set at init() time (see ExePath).
2022-03-26 21:12:50 +00:00
var ExeName string
// 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)
// 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 {
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-27 07:09:06 +00:00
// If a URL to a checksum file (sumurl) is not empty will optionally
// 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.
// 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
//
// 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) {
// 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] == ' ' {
args = append(args, "")
2022-02-17 04:31:12 +00:00
}
return args
}
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) }