bonzai/z/bonzai.go

315 lines
11 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"
"github.com/rwxrob/bonzai"
"github.com/rwxrob/compcmd"
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 Cmds to lookup when executing with Z.Run in
// "multicall" mode. Each value of the slice keyed to the name must
// begin with a *Cmd and the rest will be assumed to be string arguments
// to prepend. This allows the table of Commands to not only associate
// with a specific Cmd, but to also provide different arguments to that
// command. The name Commands is similar to Cmd.Commands. See Run.
var Commands map[string][]any
// Comp may be optionally assigned any implementation of
// bonzai.Completer and will be used as the default if a Command does not
// provide its own. Comp is assigned rwxrob/compcmd.Completer by default.
// This can be overriden by Bonzai tree developers through simple
// assignment to their own preference. However, for consistency, this
// default is strongly recommended, at least for all branch commands (as
// opposed to leafs). See z.Cmd.Run for documentation on completion
// mode.
var Comp = compcmd.New()
// Conf may be optionally assigned any implementation of
// a bonzai.Configurer. Once assigned it should not be reassigned at any
// later time during runtime. Certain Bonzai branches and commands may
// require Z.Conf to be defined and those that do generally require the
// same implementation throughout all of runtime. Commands that require
2022-04-09 20:27:24 +00:00
// Z.Conf should set ReqConf to true. Other than the exceptional case
// of configuration commands that fulfill bonzai.Configurer (and usually
// assign themselves to Z.Conf at init() time), commands must never
// require a specific implementation of bonzai.Configurer. This
// encourages command creators and Bonzai tree composers to centralize
// on a single form of configuration without creating brittle
// dependencies and tight coupling. Configuration persistence can be
// implemented in any number of ways without a problem and Bonzai trees
// simply need to be recompiled with a different bonzai.Configurer
// implementation to switch everything that depends on configuration.
var Conf bonzai.Configurer
2022-04-12 13:35:43 +00:00
// Vars may be optionally assigned any implementation of a bonzai.Vars
// but this is normally assigned at init() time by a bonzai.Vars driver
// module (see rwxrob/vars). Once assigned it should not be reassigned
// at any later time during runtime. Certain Bonzai branches and
// commands may require Z.Vars to be defined and those that do generally
// require the same implementation throughout all of runtime. Commands
// that require Z.Vars should set ReqVars to true. Other than the
// exceptional case of configuration commands that fulfill bonzai.Vars
// (and usually assign themselves to Z.Vars at init() time), commands
// must never require a specific implementation of bonzai.Vars. This
2022-04-09 20:27:24 +00:00
// encourages command creators and Bonzai tree composers to centralize
2022-04-12 13:35:43 +00:00
// on a single form of caching without creating brittle dependencies and
// tight coupling. Caching persistence can be implemented in any number
// of ways without a problem and Bonzai trees simply need to be
// recompiled with a different bonzai.Vars implementation to switch
// everything that depends on cached variables.
var Vars bonzai.Vars
2022-04-09 20:27:24 +00:00
// UsageFunc is the default first-class function called if a Cmd that
// does not already define its own when usage information is needed (see
// bonzai.UsageFunc and Cmd.UsageError for more). By default,
// InferredUsage is assigned.
//
// It is used to return a usage summary. Generally, it should only
// return a single line (even if that line is very long). Developers
// are encouraged to refer users to their chosen help command rather
2022-04-11 09:41:40 +00:00
// than producing usually long usage lines.
var UsageFunc = InferredUsage
2022-04-02 11:40:00 +00:00
// InferredUsage returns a single line of text summarizing only the
// Commands (less any Hidden commands), Params, and Aliases. If a Cmd
// is currently in an invalid state (Params without Call, no Call and no
// Commands) a string beginning with ERROR and wrapped in braces ({}) is
// returned instead. The string depends on the current language (see
// lang.go). Note that aliases does not include package Z.Aliases.
func InferredUsage(cmd bonzai.Command) string {
x, iscmd := cmd.(*Cmd)
if !iscmd {
return "{ERROR: not a bonzai.Command}"
}
2022-04-02 11:40:00 +00:00
if x.Call == nil && x.Commands == nil {
return "{ERROR: neither Call nor Commands defined}"
}
if x.Call == nil && x.Params != nil {
return "{ERROR: Params without Call: " + strings.Join(x.Params, ", ") + "}"
}
2022-04-02 20:38:16 +00:00
params := UsageGroup(x.Params, x.MinParm, x.MaxParm)
2022-04-02 11:40:00 +00:00
var names string
if x.Commands != nil {
var snames []string
for _, x := range x.Commands {
snames = append(snames, x.UsageNames())
}
if len(snames) > 0 {
2022-04-02 20:38:16 +00:00
names = UsageGroup(snames, 1, 1)
2022-04-02 11:40:00 +00:00
}
}
if params != "" && names != "" {
return "(" + params + "|" + names + ")"
}
if params != "" {
return params
}
return names
}
// Run infers the name of the command to run from the ExeName looked up
// in the Commands and delegates accordingly, prepending any arguments
// provided. This allows for BusyBox-like (https://www.busybox.net)
// multicall binaries to be used for such things as very light-weight
// Linux distributions when used "FROM SCRATCH" in containers. Although
// it shares the same name Z.Run should not confused with Cmd.Run. In
// general, Z.Run is for "multicall" and Cmd.Run is for "monoliths".
// Run may exit with the following errors:
//
// * MultiCallCmdNotFound
// * MultiCallCmdNotCmd
// * MultiCallCmdNotCmd
// * MultiCallCmdArgNotString
//
func Run() {
if v, has := Commands[ExeName]; has {
if len(v) < 1 {
ExitError(MultiCallCmdNotFound{ExeName})
return
}
cmd, iscmd := v[0].(*Cmd)
if !iscmd {
ExitError(MultiCallCmdNotCmd{ExeName, v[0]})
return
}
args := []string{cmd.Name}
if len(v) > 1 {
rest := os.Args[1:]
for _, a := range v[1:] {
s, isstring := a.(string)
if !isstring {
ExitError(MultiCallCmdArgNotString{ExeName, a})
return
}
args = append(args, s)
}
args = append(args, rest...)
}
os.Args = args
cmd.Run()
Exit()
return
}
ExitError(MultiCallCmdNotFound{ExeName})
}
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
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-06 16:36:27 +00:00
2022-04-08 07:59:50 +00:00
// AllowPanic disables TrapPanic stopping it from cleaning panic errors.
var AllowPanic = false
2022-04-06 16:36:27 +00:00
// TrapPanic recovers from any panic and more gracefully displays the
2022-04-06 16:39:55 +00:00
// panic by logging it before exiting with a return value of 1.
2022-04-06 16:36:27 +00:00
var TrapPanic = func() {
2022-04-08 07:59:50 +00:00
if !AllowPanic {
if r := recover(); r != nil {
log.Println(r)
os.Exit(1)
}
2022-04-06 16:36:27 +00:00
}
}