1# exec - running other programs.2#3# Run takes the program and its arguments as a list, no shell in between, and4# waits for it to finish:5#6# exec = import("os/exec")7#8# r = exec.Run(["git", "rev-parse", "HEAD"])9# if r.Status != 0 { ... }10# println(r.Stdout)11#12# Output is the short way to the text of a program that is expected to work,13# and returns an error when it doesn't:14#15# head = exec.Output(["git", "rev-parse", "HEAD"])16#17# The arguments go to the program as they are, so a file named "; rm -rf ."18# is a file name and nothing else. Sh runs a command line through /bin/sh when19# pipes and redirections are really wanted, and then the string is a program:20# don't build it out of anything that came from outside.21#22# ponytail: the program is waited for, its output arrives at the end. Nothing23# runs in the background and nothing is read while it runs, which is what a24# long lived child would need.2526syscall = import("syscall")27os = import("os")28strings = import("strings")2930# MaxOutput is how much of the output of a program is kept by default, one31# megabyte. Options.Max raises it for a program that writes more.32MaxOutput = 1 << 203334mergeStderr = 135inherit = 23637# Options returns the settings of a run, to change and pass to RunWith:38#39# o = exec.Options()40# o.Input = "text on the standard input"41# o.Stderr = true # the error output is kept along with the output42# o.Quiet = false # true lets the program write to the terminal43# o.Max = 1 << 24 # keep up to sixteen megabytes44# o.Dir = "/tmp" # run it there45Options = fn() {46 o = new()47 o.Input = null48 o.Stderr = false49 o.Quiet = false50 o.Max = MaxOutput51 o.Dir = null52 return o53}5455# blobOf packs the arguments the way Spawn wants them: every one of them56# followed by a zero byte.57blobOf = fn(argv) {58 out = []59 for i = 0; i < len(argv); ++i {60 # Bytes are taken as they are: turning them into a string first would61 # stop at the first zero byte, which is what a program may not be62 # given as an argument anyway.63 b = if type(argv[i]) == "bytes" { argv[i] } else { bytes(string(argv[i])) }64 for j = 0; j < len(b); ++j {65 if b[j] == 0 {66 return error("exec: argument {i} has a zero byte in it")67 }68 out = append(out, b[j])69 }70 out = append(out, 0)71 }72 return bytes(out)73}7475# RunWith runs argv with the given options and returns the result: Status is76# the exit status, Stdout what the program wrote, and Truncated whether more77# came out than Max allowed.78RunWith = fn(argv, opts) {79 if type(argv) == "string" {80 argv = [argv]81 }82 if len(argv) == 0 {83 return error("exec: nothing to run")84 }85 if opts == null {86 opts = Options()87 }8889 if failed(blob = blobOf(argv)) {90 return blob91 }9293 input = null94 if opts.Input != null {95 if failed(input = bytes(string(opts.Input))) {96 return input97 }98 }99100 flags = 0101 if opts.Stderr {102 flags = flags | mergeStderr103 }104 if opts.Quiet {105 flags = flags | inherit106 }107108 out = null109 if !opts.Quiet {110 out = bytes(if opts.Max == null { MaxOutput } else { opts.Max })111 }112113 # A directory of its own means going there and back, since the child is114 # forked from where this process stands.115 back = null116 if opts.Dir != null {117 if failed(back = os.Getwd()) {118 return back119 }120 if failed(err = os.Chdir(opts.Dir)) {121 return err122 }123 }124125 n = syscall.Spawn(blob, len(argv), input, out, flags)126 status = syscall.SpawnStatus()127128 if back != null {129 os.Chdir(back)130 }131 if failed(n) {132 return error("exec: cannot run {argv[0]}: {n}")133 }134135 r = new()136 r.Argv = argv137 r.Status = status138 r.Truncated = out != null && n > len(out)139 r.Stdout = if out == null { "" } else { string(slice(out, 0, if r.Truncated { len(out) } else { n })) }140 return r141}142143# Run runs argv, keeps what it writes, and returns the result. Only a program144# that could not be started at all is an error: one that ran and failed is a145# Status other than zero.146Run = fn(argv) { RunWith(argv, null) }147148# Output returns what argv wrote on its standard output, and an error when it149# ended with a status other than zero.150Output = fn(argv) {151 if failed(r = Run(argv)) {152 return r153 }154 if r.Status != 0 {155 return error("exec: {r.Argv[0]} ended with status {r.Status}")156 }157 return r.Stdout158}159160# Status runs argv without keeping its output, which goes to the terminal, and161# returns the exit status.162Status = fn(argv) {163 o = Options()164 o.Quiet = true165166 if failed(r = RunWith(argv, o)) {167 return r168 }169 return r.Status170}171172# Sh runs a command line through /bin/sh, pipes, redirections and all. The173# command is a program, so keep anything that came from outside out of it, or174# pass it in Options.Input.175Sh = fn(cmd) { Run(["/bin/sh", "-c", string(cmd)]) }176177# Look returns the path of the program name, searched for in PATH the way a178# shell would, or an error when there is no such program.179Look = fn(name) {180 name = string(name)181 if strings.Contains(name, "/") {182 if os.Exists(name) {183 return name184 }185 return error("exec: {name} is not there")186 }187188 path = os.Getenv("PATH")189 if path == null || path == "" {190 path = "/usr/local/bin:/usr/bin:/bin"191 }192193 dirs = strings.Split(path, ":")194 for i = 0; i < len(dirs); ++i {195 if dirs[i] == "" {196 continue197 }198199 full = dirs[i] + "/" + name200 if os.Exists(full) {201 return full202 }203 }204 return error("exec: {name} is not in PATH")205}