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/runner.go

317 lines
5.7 KiB
Go

1 year ago
package cmdr
import (
"fmt"
"git.noahlan.cn/noahlan/ntool/narr"
"git.noahlan.cn/noahlan/ntool/ncli/cmdline"
"git.noahlan.cn/noahlan/ntool/nmap"
"git.noahlan.cn/noahlan/ntool/nmath"
"git.noahlan.cn/noahlan/ntool/nstr/textutil"
"github.com/gookit/color"
"strings"
)
// Task struct
type Task struct {
err error
index int
// ID for task
ID string
Cmd *Cmd
// BeforeRun hook
BeforeRun func(t *Task)
PrevCond func(prev *Task) bool
}
// NewTask instance
func NewTask(cmd *Cmd) *Task {
return &Task{
Cmd: cmd,
}
}
// get task id by cmd.Name
func (t *Task) ensureID(idx int) {
t.index = idx
if t.ID != "" {
return
}
id := t.Cmd.IDString()
if t.Cmd.Name == "" {
id += nmath.String(idx)
}
t.ID = id
}
var rpl = textutil.NewVarReplacer("$").DisableFlatten()
// RunWith command
func (t *Task) RunWith(ctx nmap.Data) error {
cmdVars := ctx.StringMap("cmdVars")
if len(cmdVars) > 0 {
// rpl := strutil.NewReplacer(cmdVars)
for i, val := range t.Cmd.Args {
if strings.ContainsRune(val, '$') {
t.Cmd.Args[i] = rpl.RenderSimple(val, cmdVars)
}
}
}
return t.Run()
}
// Run command
func (t *Task) Run() error {
if t.BeforeRun != nil {
t.BeforeRun(t)
}
t.err = t.Cmd.Run()
return t.err
}
// Err get
func (t *Task) Err() error {
return t.err
}
// Index get
func (t *Task) Index() int {
return t.index
}
// Cmdline get
func (t *Task) Cmdline() string {
return t.Cmd.Cmdline()
}
// IsSuccess of task
func (t *Task) IsSuccess() bool {
return t.err == nil
}
// RunnerHookFn func
type RunnerHookFn func(r *Runner, t *Task) bool
// Runner use for batch run multi task commands
type Runner struct {
prev *Task
// task name to index
idMap map[string]int
tasks []*Task
// Errs on run tasks, key is Task.ID
Errs nmap.ErrMap
// TODO Concurrent run
// Workdir common workdir
Workdir string
// EnvMap will append to task.Cmd on run
EnvMap map[string]string
// Params for add custom params
Params nmap.Map
// DryRun dry run all commands
DryRun bool
// OutToStd stdout and stderr
OutToStd bool
// IgnoreErr continue on error
IgnoreErr bool
// BeforeRun hooks on each task. return false to skip current task.
BeforeRun func(r *Runner, t *Task) bool
// AfterRun hook on each task. return false to stop running.
AfterRun func(r *Runner, t *Task) bool
}
// NewRunner instance with config func
func NewRunner(fns ...func(rr *Runner)) *Runner {
rr := &Runner{
idMap: make(map[string]int, 0),
tasks: make([]*Task, 0),
Errs: make(nmap.ErrMap),
Params: make(nmap.Map),
}
rr.OutToStd = true
for _, fn := range fns {
fn(rr)
}
return rr
}
// WithOutToStd set
func (r *Runner) WithOutToStd() *Runner {
r.OutToStd = true
return r
}
// Add multitask at once
func (r *Runner) Add(tasks ...*Task) *Runner {
for _, task := range tasks {
r.AddTask(task)
}
return r
}
// AddTask add one task
func (r *Runner) AddTask(task *Task) *Runner {
if task.Cmd == nil {
panic("task command cannot be empty")
}
idx := len(r.tasks)
task.ensureID(idx)
// TODO check id repeat
r.idMap[task.ID] = idx
r.tasks = append(r.tasks, task)
return r
}
// AddCmd commands
func (r *Runner) AddCmd(cmds ...*Cmd) *Runner {
for _, cmd := range cmds {
r.AddTask(&Task{Cmd: cmd})
}
return r
}
// GitCmd quick a git command task
func (r *Runner) GitCmd(subCmd string, args ...string) *Runner {
return r.AddTask(&Task{
Cmd: NewGitCmd(subCmd, args...),
})
}
// CmdWithArgs a command task
func (r *Runner) CmdWithArgs(cmdName string, args ...string) *Runner {
return r.AddTask(&Task{
Cmd: NewCmd(cmdName, args...),
})
}
// CmdWithAnys a command task
func (r *Runner) CmdWithAnys(cmdName string, args ...any) *Runner {
return r.AddTask(&Task{
Cmd: NewCmd(cmdName, narr.SliceToStrings(args)...),
})
}
// AddCmdline as a command task
func (r *Runner) AddCmdline(line string) *Runner {
bin, args := cmdline.NewParser(line).BinAndArgs()
return r.AddTask(&Task{
Cmd: NewCmd(bin, args...),
})
}
// Run all tasks
func (r *Runner) Run() error {
// do run tasks
for i, task := range r.tasks {
if r.BeforeRun != nil && !r.BeforeRun(r, task) {
continue
}
if r.prev != nil && task.PrevCond != nil && !task.PrevCond(r.prev) {
continue
}
if r.DryRun {
color.Infof("DRY-RUN: task#%d execute completed\n\n", i+1)
continue
}
if !r.RunTask(task) {
break
}
fmt.Println() // with newline.
}
if len(r.Errs) == 0 {
return nil
}
return r.Errs
}
// StepRun one command
func (r *Runner) StepRun() error {
return nil // TODO
}
// RunTask command
func (r *Runner) RunTask(task *Task) (goon bool) {
if len(r.EnvMap) > 0 {
task.Cmd.AppendEnv(r.EnvMap)
}
if r.OutToStd && !task.Cmd.HasStdout() {
task.Cmd.ToOSStdoutStderr()
}
// common workdir
if r.Workdir != "" && task.Cmd.Dir == "" {
task.Cmd.WithWorkDir(r.Workdir)
}
// do running
if err := task.RunWith(r.Params); err != nil {
r.Errs[task.ID] = err
color.Errorf("Task#%d run error: %s\n", task.Index()+1, err)
// not ignore error, stop.
if !r.IgnoreErr {
return false
}
}
if r.AfterRun != nil && !r.AfterRun(r, task) {
return false
}
// store prev
r.prev = task
return true
}
// Len of tasks
func (r *Runner) Len() int {
return len(r.tasks)
}
// Reset instance
func (r *Runner) Reset() *Runner {
r.prev = nil
r.tasks = make([]*Task, 0)
r.idMap = make(map[string]int, 0)
return r
}
// TaskIDs get
func (r *Runner) TaskIDs() []string {
ss := make([]string, 0, len(r.idMap))
for id := range r.idMap {
ss = append(ss, id)
}
return ss
}
// Prev task instance after running
func (r *Runner) Prev() *Task {
return r.prev
}
// Task get by id name
func (r *Runner) Task(id string) (*Task, error) {
if idx, ok := r.idMap[id]; ok {
return r.tasks[idx], nil
}
return nil, fmt.Errorf("task %q is not exists", id)
}