You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ntool/nsys/cmdr/cmd.go

450 lines
8.7 KiB
Go

package cmdr
import (
"context"
"fmt"
"git.noahlan.cn/noahlan/ntool/narr"
"git.noahlan.cn/noahlan/ntool/ncli/cmdline"
"io"
"os"
"os/exec"
"path/filepath"
)
// Cmd struct
type Cmd struct {
*exec.Cmd
// Name of the command
Name string
// DryRun if True, not real execute command
DryRun bool
// Vars mapping
Vars map[string]string
// BeforeRun hook
BeforeRun func(c *Cmd)
// AfterRun hook
AfterRun func(c *Cmd, err error)
}
// NewGitCmd instance
func NewGitCmd(subCmd string, args ...string) *Cmd {
return NewCmd("git", subCmd).AddArgs(args)
}
// NewCmdline instance
//
// see exec.Command
func NewCmdline(line string) *Cmd {
bin, args := cmdline.NewParser(line).WithParseEnv().BinAndArgs()
return NewCmd(bin, args...)
}
// NewCmd instance
//
// see exec.Command
func NewCmd(bin string, args ...string) *Cmd {
return WrapGoCmd(exec.Command(bin, args...))
}
// CmdWithCtx create new instance with context.
//
// see exec.CommandContext
func CmdWithCtx(ctx context.Context, bin string, args ...string) *Cmd {
return WrapGoCmd(exec.CommandContext(ctx, bin, args...))
}
// WrapGoCmd instance
func WrapGoCmd(cmd *exec.Cmd) *Cmd {
return &Cmd{
Cmd: cmd,
Vars: make(map[string]string),
}
}
// -------------------------------------------------
// config the command
// -------------------------------------------------
// Config the command
func (c *Cmd) Config(fn func(c *Cmd)) *Cmd {
fn(c)
return c
}
// WithDryRun on exec command
func (c *Cmd) WithDryRun(dryRun bool) *Cmd {
c.DryRun = dryRun
return c
}
// PrintCmdline on exec command
func (c *Cmd) PrintCmdline() *Cmd {
c.BeforeRun = PrintCmdline
return c
}
// OnBefore exec add hook
func (c *Cmd) OnBefore(fn func(c *Cmd)) *Cmd {
c.BeforeRun = fn
return c
}
// OnAfter exec add hook
func (c *Cmd) OnAfter(fn func(c *Cmd, err error)) *Cmd {
c.AfterRun = fn
return c
}
// WithBin name returns the current object
func (c *Cmd) WithBin(name string) *Cmd {
c.Args[0] = name
c.lookPath(name)
return c
}
func (c *Cmd) lookPath(name string) {
if filepath.Base(name) == name {
lp, err := exec.LookPath(name)
if lp != "" {
// Update cmd.Path even if err is non-nil.
// If err is ErrDot (especially on Windows), lp may include a resolved
// extension (like .exe or .bat) that should be preserved.
c.Path = lp
}
if err != nil {
panic(fmt.Sprintf("cmdr: look %q path error: %v", name, err))
}
}
}
// WithGoCmd and returns the current instance.
func (c *Cmd) WithGoCmd(ec *exec.Cmd) *Cmd {
c.Cmd = ec
return c
}
// WithWorkDir returns the current object
func (c *Cmd) WithWorkDir(dir string) *Cmd {
c.Dir = dir
return c
}
// WorkDirOnNE set workdir on input is not empty
func (c *Cmd) WorkDirOnNE(dir string) *Cmd {
if dir == "" {
c.Dir = dir
}
return c
}
// WithEnvMap override set new ENV for run
func (c *Cmd) WithEnvMap(mp map[string]string) *Cmd {
if ln := len(mp); ln > 0 {
c.Env = make([]string, 0, ln)
for key, val := range mp {
c.Env = append(c.Env, key+"="+val)
}
}
return c
}
// AppendEnv to the os ENV for run command
func (c *Cmd) AppendEnv(mp map[string]string) *Cmd {
if len(mp) > 0 {
// init env data
if c.Env == nil {
c.Env = os.Environ()
}
for name, val := range mp {
c.Env = append(c.Env, name+"="+val)
}
}
return c
}
// OutputToOS output to OS stdout and error
func (c *Cmd) OutputToOS() *Cmd {
return c.ToOSStdoutStderr()
}
// ToOSStdoutStderr output to OS stdout and error
func (c *Cmd) ToOSStdoutStderr() *Cmd {
c.Stdout = os.Stdout
c.Stderr = os.Stderr
return c
}
// ToOSStdout output to OS stdout
func (c *Cmd) ToOSStdout() *Cmd {
c.Stdout = os.Stdout
c.Stderr = os.Stdout
return c
}
// WithStdin returns the current argument
func (c *Cmd) WithStdin(in io.Reader) *Cmd {
c.Stdin = in
return c
}
// WithOutput returns the current instance
func (c *Cmd) WithOutput(out, errOut io.Writer) *Cmd {
c.Stdout = out
if errOut != nil {
c.Stderr = errOut
}
return c
}
// WithAnyArgs add args and returns the current object.
func (c *Cmd) WithAnyArgs(args ...any) *Cmd {
c.Args = append(c.Args, narr.SliceToStrings(args)...)
return c
}
// AddArg add args and returns the current object
func (c *Cmd) AddArg(args ...string) *Cmd { return c.WithArg(args...) }
// WithArg add args and returns the current object. alias of the WithArg()
func (c *Cmd) WithArg(args ...string) *Cmd {
c.Args = append(c.Args, args...)
return c
}
// AddArgf add args and returns the current object. alias of the WithArgf()
func (c *Cmd) AddArgf(format string, args ...any) *Cmd {
return c.WithArgf(format, args...)
}
// WithArgf add arg and returns the current object
func (c *Cmd) WithArgf(format string, args ...any) *Cmd {
c.Args = append(c.Args, fmt.Sprintf(format, args...))
return c
}
// ArgIf add arg and returns the current object
func (c *Cmd) ArgIf(arg string, exprOk bool) *Cmd {
if exprOk {
c.Args = append(c.Args, arg)
}
return c
}
// WithArgIf add arg and returns the current object
func (c *Cmd) WithArgIf(arg string, exprOk bool) *Cmd {
return c.ArgIf(arg, exprOk)
}
// AddArgs for the git. alias of WithArgs()
func (c *Cmd) AddArgs(args []string) *Cmd { return c.WithArgs(args) }
// WithArgs for the git
func (c *Cmd) WithArgs(args []string) *Cmd {
if len(args) > 0 {
c.Args = append(c.Args, args...)
}
return c
}
// WithArgsIf add arg and returns the current object
func (c *Cmd) WithArgsIf(args []string, exprOk bool) *Cmd {
if exprOk && len(args) > 0 {
c.Args = append(c.Args, args...)
}
return c
}
// WithVars add vars and returns the current object
func (c *Cmd) WithVars(vs map[string]string) *Cmd {
if len(vs) > 0 {
c.Vars = vs
}
return c
}
// SetVar add var and returns the current object
func (c *Cmd) SetVar(name, val string) *Cmd {
c.Vars[name] = val
return c
}
// -------------------------------------------------
// helper command
// -------------------------------------------------
// IDString of the command
func (c *Cmd) IDString() string {
if c.Name != "" {
return c.Name
}
return c.BinOrPath()
}
// BinName of the command
func (c *Cmd) BinName() string {
if len(c.Args) > 0 {
return c.Args[0]
}
return ""
}
// BinOrPath of the command
func (c *Cmd) BinOrPath() string {
if len(c.Args) > 0 {
return c.Args[0]
}
return c.Path
}
// OnlyArgs of the command, not contains bin name.
func (c *Cmd) OnlyArgs() (ss []string) {
if len(c.Args) > 1 {
return c.Args[1:]
}
return
}
// ResetArgs for command, but will keep bin name.
func (c *Cmd) ResetArgs() {
if len(c.Args) > 0 {
c.Args = c.Args[0:1]
} else {
c.Args = c.Args[:0]
}
}
// Workdir of the command
func (c *Cmd) Workdir() string {
return c.Dir
}
// Cmdline to command line
func (c *Cmd) Cmdline() string {
return cmdline.Cmdline(c.Args)
}
// Copy new instance from current command, with new args.
func (c *Cmd) Copy(args ...string) *Cmd {
nc := *c
// copy bin name.
if len(c.Args) > 0 {
nc.Args = append([]string{c.Args[0]}, args...)
} else {
nc.Args = args
}
return &nc
}
// GoCmd get exec.Cmd
func (c *Cmd) GoCmd() *exec.Cmd { return c.Cmd }
// -------------------------------------------------
// run command
// -------------------------------------------------
// Success run and return whether success
func (c *Cmd) Success() bool {
return c.Run() == nil
}
// HasStdout output setting.
func (c *Cmd) HasStdout() bool {
return c.Stdout != nil
}
// SafeLines run and return output as lines
func (c *Cmd) SafeLines() []string {
ss, _ := c.OutputLines()
return ss
}
// OutputLines run and return output as lines
func (c *Cmd) OutputLines() ([]string, error) {
out, err := c.Output()
if err != nil {
return nil, err
}
return OutputLines(out), err
}
// SafeOutput run and return output
func (c *Cmd) SafeOutput() string {
out, err := c.Output()
if err != nil {
return ""
}
return out
}
// Output run and return output
func (c *Cmd) Output() (string, error) {
if c.BeforeRun != nil {
c.BeforeRun(c)
}
if c.DryRun {
return "DRY-RUN: ok", nil
}
output, err := c.Cmd.Output()
if c.AfterRun != nil {
c.AfterRun(c, err)
}
return string(output), err
}
// CombinedOutput run and return output, will combine stderr and stdout output
func (c *Cmd) CombinedOutput() (string, error) {
if c.BeforeRun != nil {
c.BeforeRun(c)
}
if c.DryRun {
return "DRY-RUN: ok", nil
}
output, err := c.Cmd.CombinedOutput()
if c.AfterRun != nil {
c.AfterRun(c, err)
}
return string(output), err
}
// MustRun a command. will panic on error
func (c *Cmd) MustRun() {
if err := c.Run(); err != nil {
panic(err)
}
}
// FlushRun runs command and flush output to stdout
func (c *Cmd) FlushRun() error {
return c.ToOSStdoutStderr().Run()
}
// Run runs command
func (c *Cmd) Run() error {
if c.BeforeRun != nil {
c.BeforeRun(c)
}
if c.DryRun {
return nil
}
// do running
err := c.Cmd.Run()
if c.AfterRun != nil {
c.AfterRun(c, err)
}
return err
}