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) }