| | |
| | |
| | |
| |
|
| | package script |
| |
|
| | import ( |
| | "cmd/internal/pathcache" |
| | "cmd/internal/robustio" |
| | "errors" |
| | "fmt" |
| | "internal/diff" |
| | "io/fs" |
| | "os" |
| | "os/exec" |
| | "path/filepath" |
| | "regexp" |
| | "runtime" |
| | "strconv" |
| | "strings" |
| | "sync" |
| | "time" |
| | ) |
| |
|
| | |
| | |
| | |
| | |
| | func DefaultCmds() map[string]Cmd { |
| | return map[string]Cmd{ |
| | "cat": Cat(), |
| | "cd": Cd(), |
| | "chmod": Chmod(), |
| | "cmp": Cmp(), |
| | "cmpenv": Cmpenv(), |
| | "cp": Cp(), |
| | "echo": Echo(), |
| | "env": Env(), |
| | "exec": Exec(func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }, 100*time.Millisecond), |
| | "exists": Exists(), |
| | "grep": Grep(), |
| | "help": Help(), |
| | "mkdir": Mkdir(), |
| | "mv": Mv(), |
| | "rm": Rm(), |
| | "replace": Replace(), |
| | "sleep": Sleep(), |
| | "stderr": Stderr(), |
| | "stdout": Stdout(), |
| | "stop": Stop(), |
| | "symlink": Symlink(), |
| | "wait": Wait(), |
| | } |
| | } |
| |
|
| | |
| | |
| | func Command(usage CmdUsage, run func(*State, ...string) (WaitFunc, error)) Cmd { |
| | return &funcCmd{ |
| | usage: usage, |
| | run: run, |
| | } |
| | } |
| |
|
| | |
| | type funcCmd struct { |
| | usage CmdUsage |
| | run func(*State, ...string) (WaitFunc, error) |
| | } |
| |
|
| | func (c *funcCmd) Run(s *State, args ...string) (WaitFunc, error) { |
| | return c.run(s, args...) |
| | } |
| |
|
| | func (c *funcCmd) Usage() *CmdUsage { return &c.usage } |
| |
|
| | |
| | |
| | func firstNonFlag(rawArgs ...string) []int { |
| | for i, arg := range rawArgs { |
| | if !strings.HasPrefix(arg, "-") { |
| | return []int{i} |
| | } |
| | if arg == "--" { |
| | return []int{i + 1} |
| | } |
| | } |
| | return nil |
| | } |
| |
|
| | |
| | |
| | func Cat() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "concatenate files and print to the script's stdout buffer", |
| | Args: "files...", |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) == 0 { |
| | return nil, ErrUsage |
| | } |
| |
|
| | paths := make([]string, 0, len(args)) |
| | for _, arg := range args { |
| | paths = append(paths, s.Path(arg)) |
| | } |
| |
|
| | var buf strings.Builder |
| | errc := make(chan error, 1) |
| | go func() { |
| | for _, p := range paths { |
| | b, err := os.ReadFile(p) |
| | buf.Write(b) |
| | if err != nil { |
| | errc <- err |
| | return |
| | } |
| | } |
| | errc <- nil |
| | }() |
| |
|
| | wait := func(*State) (stdout, stderr string, err error) { |
| | err = <-errc |
| | return buf.String(), "", err |
| | } |
| | return wait, nil |
| | }) |
| | } |
| |
|
| | |
| | func Cd() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "change the working directory", |
| | Args: "dir", |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) != 1 { |
| | return nil, ErrUsage |
| | } |
| | return nil, s.Chdir(args[0]) |
| | }) |
| | } |
| |
|
| | |
| | func Chmod() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "change file mode bits", |
| | Args: "perm paths...", |
| | Detail: []string{ |
| | "Changes the permissions of the named files or directories to be equal to perm.", |
| | "Only numerical permissions are supported.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) < 2 { |
| | return nil, ErrUsage |
| | } |
| |
|
| | perm, err := strconv.ParseUint(args[0], 0, 32) |
| | if err != nil || perm&uint64(fs.ModePerm) != perm { |
| | return nil, fmt.Errorf("invalid mode: %s", args[0]) |
| | } |
| |
|
| | for _, arg := range args[1:] { |
| | err := os.Chmod(s.Path(arg), fs.FileMode(perm)) |
| | if err != nil { |
| | return nil, err |
| | } |
| | } |
| | return nil, nil |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | func Cmp() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Args: "[-q] file1 file2", |
| | Summary: "compare files for differences", |
| | Detail: []string{ |
| | "By convention, file1 is the actual data and file2 is the expected data.", |
| | "The command succeeds if the file contents are identical.", |
| | "File1 can be 'stdout' or 'stderr' to compare the stdout or stderr buffer from the most recent command.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | return nil, doCompare(s, false, args...) |
| | }) |
| | } |
| |
|
| | |
| | |
| | func Cmpenv() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Args: "[-q] file1 file2", |
| | Summary: "compare files for differences, with environment expansion", |
| | Detail: []string{ |
| | "By convention, file1 is the actual data and file2 is the expected data.", |
| | "The command succeeds if the file contents are identical after substituting variables from the script environment.", |
| | "File1 can be 'stdout' or 'stderr' to compare the script's stdout or stderr buffer.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | return nil, doCompare(s, true, args...) |
| | }) |
| | } |
| |
|
| | func doCompare(s *State, env bool, args ...string) error { |
| | quiet := false |
| | if len(args) > 0 && args[0] == "-q" { |
| | quiet = true |
| | args = args[1:] |
| | } |
| | if len(args) != 2 { |
| | return ErrUsage |
| | } |
| |
|
| | name1, name2 := args[0], args[1] |
| | var text1, text2 string |
| | switch name1 { |
| | case "stdout": |
| | text1 = s.Stdout() |
| | case "stderr": |
| | text1 = s.Stderr() |
| | default: |
| | data, err := os.ReadFile(s.Path(name1)) |
| | if err != nil { |
| | return err |
| | } |
| | text1 = string(data) |
| | } |
| |
|
| | data, err := os.ReadFile(s.Path(name2)) |
| | if err != nil { |
| | return err |
| | } |
| | text2 = string(data) |
| |
|
| | if env { |
| | text1 = s.ExpandEnv(text1, false) |
| | text2 = s.ExpandEnv(text2, false) |
| | } |
| |
|
| | if text1 != text2 { |
| | if !quiet { |
| | diffText := diff.Diff(name1, []byte(text1), name2, []byte(text2)) |
| | s.Logf("%s\n", diffText) |
| | } |
| | return fmt.Errorf("%s and %s differ", name1, name2) |
| | } |
| | return nil |
| | } |
| |
|
| | |
| | func Cp() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "copy files to a target file or directory", |
| | Args: "src... dst", |
| | Detail: []string{ |
| | "src can include 'stdout' or 'stderr' to copy from the script's stdout or stderr buffer.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) < 2 { |
| | return nil, ErrUsage |
| | } |
| |
|
| | dst := s.Path(args[len(args)-1]) |
| | info, err := os.Stat(dst) |
| | dstDir := err == nil && info.IsDir() |
| | if len(args) > 2 && !dstDir { |
| | return nil, &fs.PathError{Op: "cp", Path: dst, Err: errors.New("destination is not a directory")} |
| | } |
| |
|
| | for _, arg := range args[:len(args)-1] { |
| | var ( |
| | src string |
| | data []byte |
| | mode fs.FileMode |
| | ) |
| | switch arg { |
| | case "stdout": |
| | src = arg |
| | data = []byte(s.Stdout()) |
| | mode = 0666 |
| | case "stderr": |
| | src = arg |
| | data = []byte(s.Stderr()) |
| | mode = 0666 |
| | default: |
| | src = s.Path(arg) |
| | info, err := os.Stat(src) |
| | if err != nil { |
| | return nil, err |
| | } |
| | mode = info.Mode() & 0777 |
| | data, err = os.ReadFile(src) |
| | if err != nil { |
| | return nil, err |
| | } |
| | } |
| | targ := dst |
| | if dstDir { |
| | targ = filepath.Join(dst, filepath.Base(src)) |
| | } |
| | err := os.WriteFile(targ, data, mode) |
| | if err != nil { |
| | return nil, err |
| | } |
| | } |
| |
|
| | return nil, nil |
| | }) |
| | } |
| |
|
| | |
| | func Echo() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "display a line of text", |
| | Args: "string...", |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | var buf strings.Builder |
| | for i, arg := range args { |
| | if i > 0 { |
| | buf.WriteString(" ") |
| | } |
| | buf.WriteString(arg) |
| | } |
| | buf.WriteString("\n") |
| | out := buf.String() |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | return func(*State) (stdout, stderr string, err error) { |
| | return out, "", nil |
| | }, nil |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func Env() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "set or log the values of environment variables", |
| | Args: "[key[=value]...]", |
| | Detail: []string{ |
| | "With no arguments, print the script environment to the log.", |
| | "Otherwise, add the listed key=value pairs to the environment or print the listed keys.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | out := new(strings.Builder) |
| | if len(args) == 0 { |
| | for _, kv := range s.env { |
| | fmt.Fprintf(out, "%s\n", kv) |
| | } |
| | } else { |
| | for _, env := range args { |
| | i := strings.Index(env, "=") |
| | if i < 0 { |
| | |
| | fmt.Fprintf(out, "%s=%s\n", env, s.envMap[env]) |
| | continue |
| | } |
| | if err := s.Setenv(env[:i], env[i+1:]); err != nil { |
| | return nil, err |
| | } |
| | } |
| | } |
| | var wait WaitFunc |
| | if out.Len() > 0 || len(args) == 0 { |
| | wait = func(*State) (stdout, stderr string, err error) { |
| | return out.String(), "", nil |
| | } |
| | } |
| | return wait, nil |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func Exec(cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "run an executable program with arguments", |
| | Args: "program [args...]", |
| | Detail: []string{ |
| | "Note that 'exec' does not terminate the script (unlike Unix shells).", |
| | }, |
| | Async: true, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) < 1 { |
| | return nil, ErrUsage |
| | } |
| |
|
| | |
| | |
| | |
| | name := filepath.FromSlash(args[0]) |
| | path := name |
| | if !strings.Contains(name, string(filepath.Separator)) { |
| | var err error |
| | path, err = lookPath(s, name) |
| | if err != nil { |
| | return nil, err |
| | } |
| | } |
| |
|
| | return startCommand(s, name, path, args[1:], cancel, waitDelay) |
| | }) |
| | } |
| |
|
| | func startCommand(s *State, name, path string, args []string, cancel func(*exec.Cmd) error, waitDelay time.Duration) (WaitFunc, error) { |
| | var ( |
| | cmd *exec.Cmd |
| | stdoutBuf, stderrBuf strings.Builder |
| | ) |
| | for { |
| | cmd = exec.CommandContext(s.Context(), path, args...) |
| | if cancel == nil { |
| | cmd.Cancel = nil |
| | } else { |
| | cmd.Cancel = func() error { return cancel(cmd) } |
| | } |
| | cmd.WaitDelay = waitDelay |
| | cmd.Args[0] = name |
| | cmd.Dir = s.Getwd() |
| | cmd.Env = s.env |
| | cmd.Stdout = &stdoutBuf |
| | cmd.Stderr = &stderrBuf |
| | err := cmd.Start() |
| | if err == nil { |
| | break |
| | } |
| | if isETXTBSY(err) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | } else { |
| | return nil, err |
| | } |
| | } |
| |
|
| | wait := func(s *State) (stdout, stderr string, err error) { |
| | err = cmd.Wait() |
| | return stdoutBuf.String(), stderrBuf.String(), err |
| | } |
| | return wait, nil |
| | } |
| |
|
| | |
| | |
| | func lookPath(s *State, command string) (string, error) { |
| | var strEqual func(string, string) bool |
| | if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { |
| | |
| | |
| | strEqual = strings.EqualFold |
| | } else { |
| | strEqual = func(a, b string) bool { return a == b } |
| | } |
| |
|
| | var pathExt []string |
| | var searchExt bool |
| | var isExecutable func(os.FileInfo) bool |
| | if runtime.GOOS == "windows" { |
| | |
| | |
| | |
| | |
| | |
| | pathExt = strings.Split(os.Getenv("PathExt"), string(filepath.ListSeparator)) |
| | searchExt = true |
| | cmdExt := filepath.Ext(command) |
| | for _, ext := range pathExt { |
| | if strEqual(cmdExt, ext) { |
| | searchExt = false |
| | break |
| | } |
| | } |
| | isExecutable = func(fi os.FileInfo) bool { |
| | return fi.Mode().IsRegular() |
| | } |
| | } else { |
| | isExecutable = func(fi os.FileInfo) bool { |
| | return fi.Mode().IsRegular() && fi.Mode().Perm()&0111 != 0 |
| | } |
| | } |
| |
|
| | pathEnv, _ := s.LookupEnv(pathEnvName()) |
| | for dir := range strings.SplitSeq(pathEnv, string(filepath.ListSeparator)) { |
| | if dir == "" { |
| | continue |
| | } |
| |
|
| | |
| | |
| | |
| | sep := string(filepath.Separator) |
| | if os.IsPathSeparator(dir[len(dir)-1]) { |
| | sep = "" |
| | } |
| |
|
| | if searchExt { |
| | ents, err := os.ReadDir(dir) |
| | if err != nil { |
| | continue |
| | } |
| | for _, ent := range ents { |
| | for _, ext := range pathExt { |
| | if !ent.IsDir() && strEqual(ent.Name(), command+ext) { |
| | return dir + sep + ent.Name(), nil |
| | } |
| | } |
| | } |
| | } else { |
| | path := dir + sep + command |
| | if fi, err := os.Stat(path); err == nil && isExecutable(fi) { |
| | return path, nil |
| | } |
| | } |
| | } |
| | return "", &exec.Error{Name: command, Err: exec.ErrNotFound} |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func pathEnvName() string { |
| | switch runtime.GOOS { |
| | case "plan9": |
| | return "path" |
| | default: |
| | return "PATH" |
| | } |
| | } |
| |
|
| | |
| | func Exists() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "check that files exist", |
| | Args: "[-readonly] [-exec] file...", |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | var readonly, exec bool |
| | loop: |
| | for len(args) > 0 { |
| | switch args[0] { |
| | case "-readonly": |
| | readonly = true |
| | args = args[1:] |
| | case "-exec": |
| | exec = true |
| | args = args[1:] |
| | default: |
| | break loop |
| | } |
| | } |
| | if len(args) == 0 { |
| | return nil, ErrUsage |
| | } |
| |
|
| | for _, file := range args { |
| | file = s.Path(file) |
| | info, err := os.Stat(file) |
| | if err != nil { |
| | return nil, err |
| | } |
| | if readonly && info.Mode()&0222 != 0 { |
| | return nil, fmt.Errorf("%s exists but is writable", file) |
| | } |
| | if exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 { |
| | return nil, fmt.Errorf("%s exists but is not executable", file) |
| | } |
| | } |
| |
|
| | return nil, nil |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func Grep() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "find lines in a file that match a pattern", |
| | Args: matchUsage + " file", |
| | Detail: []string{ |
| | "The command succeeds if at least one match (or the exact count, if given) is found.", |
| | "The -q flag suppresses printing of matches.", |
| | }, |
| | RegexpArgs: firstNonFlag, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | return nil, match(s, args, "", "grep") |
| | }) |
| | } |
| |
|
| | const matchUsage = "[-count=N] [-q] 'pattern'" |
| |
|
| | |
| | func match(s *State, args []string, text, name string) error { |
| | n := 0 |
| | if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") { |
| | var err error |
| | n, err = strconv.Atoi(args[0][len("-count="):]) |
| | if err != nil { |
| | return fmt.Errorf("bad -count=: %v", err) |
| | } |
| | if n < 1 { |
| | return fmt.Errorf("bad -count=: must be at least 1") |
| | } |
| | args = args[1:] |
| | } |
| | quiet := false |
| | if len(args) >= 1 && args[0] == "-q" { |
| | quiet = true |
| | args = args[1:] |
| | } |
| |
|
| | isGrep := name == "grep" |
| |
|
| | wantArgs := 1 |
| | if isGrep { |
| | wantArgs = 2 |
| | } |
| | if len(args) != wantArgs { |
| | return ErrUsage |
| | } |
| |
|
| | pattern := `(?m)` + args[0] |
| | re, err := regexp.Compile(pattern) |
| | if err != nil { |
| | return err |
| | } |
| |
|
| | if isGrep { |
| | name = args[1] |
| | data, err := os.ReadFile(s.Path(args[1])) |
| | if err != nil { |
| | return err |
| | } |
| | text = string(data) |
| | } |
| |
|
| | if n > 0 { |
| | count := len(re.FindAllString(text, -1)) |
| | if count != n { |
| | return fmt.Errorf("found %d matches for %#q in %s", count, pattern, name) |
| | } |
| | return nil |
| | } |
| |
|
| | if !re.MatchString(text) { |
| | return fmt.Errorf("no match for %#q in %s", pattern, name) |
| | } |
| |
|
| | if !quiet { |
| | |
| | loc := re.FindStringIndex(text) |
| | for loc[0] > 0 && text[loc[0]-1] != '\n' { |
| | loc[0]-- |
| | } |
| | for loc[1] < len(text) && text[loc[1]] != '\n' { |
| | loc[1]++ |
| | } |
| | lines := strings.TrimSuffix(text[loc[0]:loc[1]], "\n") |
| | s.Logf("matched: %s\n", lines) |
| | } |
| | return nil |
| | } |
| |
|
| | |
| | func Help() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "log help text for commands and conditions", |
| | Args: "[-v] name...", |
| | Detail: []string{ |
| | "To display help for a specific condition, enclose it in brackets: 'help [amd64]'.", |
| | "To display complete documentation when listing all commands, pass the -v flag.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if s.engine == nil { |
| | return nil, errors.New("no engine configured") |
| | } |
| |
|
| | verbose := false |
| | if len(args) > 0 { |
| | verbose = true |
| | if args[0] == "-v" { |
| | args = args[1:] |
| | } |
| | } |
| |
|
| | var cmds, conds []string |
| | for _, arg := range args { |
| | if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") { |
| | conds = append(conds, arg[1:len(arg)-1]) |
| | } else { |
| | cmds = append(cmds, arg) |
| | } |
| | } |
| |
|
| | out := new(strings.Builder) |
| |
|
| | if len(conds) > 0 || (len(args) == 0 && len(s.engine.Conds) > 0) { |
| | if conds == nil { |
| | out.WriteString("conditions:\n\n") |
| | } |
| | s.engine.ListConds(out, s, conds...) |
| | } |
| |
|
| | if len(cmds) > 0 || len(args) == 0 { |
| | if len(args) == 0 { |
| | out.WriteString("\ncommands:\n\n") |
| | } |
| | s.engine.ListCmds(out, verbose, cmds...) |
| | } |
| |
|
| | wait := func(*State) (stdout, stderr string, err error) { |
| | return out.String(), "", nil |
| | } |
| | return wait, nil |
| | }) |
| | } |
| |
|
| | |
| | func Mkdir() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "create directories, if they do not already exist", |
| | Args: "path...", |
| | Detail: []string{ |
| | "Unlike Unix mkdir, parent directories are always created if needed.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) < 1 { |
| | return nil, ErrUsage |
| | } |
| | for _, arg := range args { |
| | if err := os.MkdirAll(s.Path(arg), 0777); err != nil { |
| | return nil, err |
| | } |
| | } |
| | return nil, nil |
| | }) |
| | } |
| |
|
| | |
| | func Mv() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "rename a file or directory to a new path", |
| | Args: "old new", |
| | Detail: []string{ |
| | "OS-specific restrictions may apply when old and new are in different directories.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) != 2 { |
| | return nil, ErrUsage |
| | } |
| | return nil, os.Rename(s.Path(args[0]), s.Path(args[1])) |
| | }) |
| | } |
| |
|
| | |
| | |
| | func Program(name string, cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd { |
| | var ( |
| | shortName string |
| | summary string |
| | lookPathOnce sync.Once |
| | path string |
| | pathErr error |
| | ) |
| | if filepath.IsAbs(name) { |
| | lookPathOnce.Do(func() { path = filepath.Clean(name) }) |
| | shortName = strings.TrimSuffix(filepath.Base(path), ".exe") |
| | summary = "run the '" + shortName + "' program provided by the script host" |
| | } else { |
| | shortName = name |
| | summary = "run the '" + shortName + "' program from the script host's PATH" |
| | } |
| |
|
| | return Command( |
| | CmdUsage{ |
| | Summary: summary, |
| | Args: "[args...]", |
| | Async: true, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | lookPathOnce.Do(func() { |
| | path, pathErr = pathcache.LookPath(name) |
| | }) |
| | if pathErr != nil { |
| | return nil, pathErr |
| | } |
| | return startCommand(s, shortName, path, args, cancel, waitDelay) |
| | }) |
| | } |
| |
|
| | |
| | func Replace() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "replace strings in a file", |
| | Args: "[old new]... file", |
| | Detail: []string{ |
| | "The 'old' and 'new' arguments are unquoted as if in quoted Go strings.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args)%2 != 1 { |
| | return nil, ErrUsage |
| | } |
| |
|
| | oldNew := make([]string, 0, len(args)-1) |
| | for _, arg := range args[:len(args)-1] { |
| | s, err := strconv.Unquote(`"` + arg + `"`) |
| | if err != nil { |
| | return nil, err |
| | } |
| | oldNew = append(oldNew, s) |
| | } |
| |
|
| | r := strings.NewReplacer(oldNew...) |
| | file := s.Path(args[len(args)-1]) |
| |
|
| | data, err := os.ReadFile(file) |
| | if err != nil { |
| | return nil, err |
| | } |
| | replaced := r.Replace(string(data)) |
| |
|
| | return nil, os.WriteFile(file, []byte(replaced), 0666) |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func Rm() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "remove a file or directory", |
| | Args: "path...", |
| | Detail: []string{ |
| | "If the path is a directory, its contents are removed recursively.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) < 1 { |
| | return nil, ErrUsage |
| | } |
| | for _, arg := range args { |
| | if err := removeAll(s.Path(arg)); err != nil { |
| | return nil, err |
| | } |
| | } |
| | return nil, nil |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func removeAll(dir string) error { |
| | |
| | |
| | filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error { |
| | |
| | |
| | if err != nil || info.IsDir() { |
| | os.Chmod(path, 0777) |
| | } |
| | return nil |
| | }) |
| | return robustio.RemoveAll(dir) |
| | } |
| |
|
| | |
| | |
| | func Sleep() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "sleep for a specified duration", |
| | Args: "duration", |
| | Detail: []string{ |
| | "The duration must be given as a Go time.Duration string.", |
| | }, |
| | Async: true, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) != 1 { |
| | return nil, ErrUsage |
| | } |
| |
|
| | d, err := time.ParseDuration(args[0]) |
| | if err != nil { |
| | return nil, err |
| | } |
| |
|
| | timer := time.NewTimer(d) |
| | wait := func(s *State) (stdout, stderr string, err error) { |
| | ctx := s.Context() |
| | select { |
| | case <-ctx.Done(): |
| | timer.Stop() |
| | return "", "", ctx.Err() |
| | case <-timer.C: |
| | return "", "", nil |
| | } |
| | } |
| | return wait, nil |
| | }) |
| | } |
| |
|
| | |
| | func Stderr() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "find lines in the stderr buffer that match a pattern", |
| | Args: matchUsage + " file", |
| | Detail: []string{ |
| | "The command succeeds if at least one match (or the exact count, if given) is found.", |
| | "The -q flag suppresses printing of matches.", |
| | }, |
| | RegexpArgs: firstNonFlag, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | return nil, match(s, args, s.Stderr(), "stderr") |
| | }) |
| | } |
| |
|
| | |
| | func Stdout() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "find lines in the stdout buffer that match a pattern", |
| | Args: matchUsage + " file", |
| | Detail: []string{ |
| | "The command succeeds if at least one match (or the exact count, if given) is found.", |
| | "The -q flag suppresses printing of matches.", |
| | }, |
| | RegexpArgs: firstNonFlag, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | return nil, match(s, args, s.Stdout(), "stdout") |
| | }) |
| | } |
| |
|
| | |
| | |
| | func Stop() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "stop execution of the script", |
| | Args: "[msg]", |
| | Detail: []string{ |
| | "The message is written to the script log, but no error is reported from the script engine.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) > 1 { |
| | return nil, ErrUsage |
| | } |
| | |
| | |
| | if len(args) == 1 { |
| | return nil, stopError{msg: args[0]} |
| | } |
| | return nil, stopError{} |
| | }) |
| | } |
| |
|
| | |
| | type stopError struct { |
| | msg string |
| | } |
| |
|
| | func (s stopError) Error() string { |
| | if s.msg == "" { |
| | return "stop" |
| | } |
| | return "stop: " + s.msg |
| | } |
| |
|
| | |
| | func Symlink() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "create a symlink", |
| | Args: "path -> target", |
| | Detail: []string{ |
| | "Creates path as a symlink to target.", |
| | "The '->' token (like in 'ls -l' output on Unix) is required.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) != 3 || args[1] != "->" { |
| | return nil, ErrUsage |
| | } |
| |
|
| | |
| | |
| | return nil, os.Symlink(filepath.FromSlash(args[2]), s.Path(args[0])) |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func Wait() Cmd { |
| | return Command( |
| | CmdUsage{ |
| | Summary: "wait for completion of background commands", |
| | Args: "", |
| | Detail: []string{ |
| | "Waits for all background commands to complete.", |
| | "The output (and any error) from each command is printed to the log in the order in which the commands were started.", |
| | "After the call to 'wait', the script's stdout and stderr buffers contain the concatenation of the background commands' outputs.", |
| | }, |
| | }, |
| | func(s *State, args ...string) (WaitFunc, error) { |
| | if len(args) > 0 { |
| | return nil, ErrUsage |
| | } |
| |
|
| | var stdouts, stderrs []string |
| | var errs []*CommandError |
| | for _, bg := range s.background { |
| | stdout, stderr, err := bg.wait(s) |
| |
|
| | beforeArgs := "" |
| | if len(bg.args) > 0 { |
| | beforeArgs = " " |
| | } |
| | s.Logf("[background] %s%s%s\n", bg.name, beforeArgs, quoteArgs(bg.args)) |
| |
|
| | if stdout != "" { |
| | s.Logf("[stdout]\n%s", stdout) |
| | stdouts = append(stdouts, stdout) |
| | } |
| | if stderr != "" { |
| | s.Logf("[stderr]\n%s", stderr) |
| | stderrs = append(stderrs, stderr) |
| | } |
| | if err != nil { |
| | s.Logf("[%v]\n", err) |
| | } |
| | if cmdErr := checkStatus(bg.command, err); cmdErr != nil { |
| | errs = append(errs, cmdErr.(*CommandError)) |
| | } |
| | } |
| |
|
| | s.stdout = strings.Join(stdouts, "") |
| | s.stderr = strings.Join(stderrs, "") |
| | s.background = nil |
| | if len(errs) > 0 { |
| | return nil, waitError{errs: errs} |
| | } |
| | return nil, nil |
| | }) |
| | } |
| |
|
| | |
| | type waitError struct { |
| | errs []*CommandError |
| | } |
| |
|
| | func (w waitError) Error() string { |
| | b := new(strings.Builder) |
| | for i, err := range w.errs { |
| | if i != 0 { |
| | b.WriteString("\n") |
| | } |
| | b.WriteString(err.Error()) |
| | } |
| | return b.String() |
| | } |
| |
|
| | func (w waitError) Unwrap() error { |
| | if len(w.errs) == 1 { |
| | return w.errs[0] |
| | } |
| | return nil |
| | } |
| |
|