cmd.ha (7112B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use errors; 5 use io; 6 use os; 7 use path; 8 use strings; 9 10 // Prepares a [[command]] based on its name and a list of arguments. The argument 11 // list should not start with the command name; it will be added for you. The 12 // argument list is borrowed from the strings you pass into this command. 13 // 14 // If 'name' does not contain a '/', the $PATH will be consulted to find the 15 // correct executable. If path resolution fails, [[nocmd]] is returned. 16 // 17 // let cmd = exec::cmd("echo", "hello world")!; 18 // let proc = exec::start(&cmd)!; 19 // let status = exec::wait(&proc)!; 20 // assert(exec::check(&status) is void); 21 // 22 // By default, the new command will inherit the current process's environment. 23 export fn cmd(name: str, args: str...) (command | error) = { 24 let platcmd = if (strings::contains(name, '/')) { 25 yield match (open(name)) { 26 case let p: platform_cmd => 27 yield p; 28 case let err: error => 29 return err; 30 case => 31 return nocmd; 32 }; 33 } else { 34 yield match (lookup_open(name)?) { 35 case void => 36 return nocmd; 37 case let p: platform_cmd => 38 yield p; 39 }; 40 }; 41 let cmd = command { 42 platform = platcmd, 43 argv = alloc([], len(args) + 1)!, 44 env = strings::dupall(os::getenvs()), 45 files = [], 46 dir = "", 47 }; 48 append(cmd.argv, name)!; 49 append(cmd.argv, args...)!; 50 return cmd; 51 }; 52 53 // Sets the 0th value of argv for this command. It is uncommon to need this. 54 export fn setname(cmd: *command, name: str) void = { 55 cmd.argv[0] = name; 56 }; 57 58 // Frees state associated with a command. You only need to call this if you do 59 // not execute the command with [[exec]] or [[start]]; in those cases the state is 60 // cleaned up for you. 61 export fn finish(cmd: *command) void = { 62 platform_finish(cmd); 63 free(cmd.argv); 64 free(cmd.files); 65 strings::freeall(cmd.env); 66 }; 67 68 // Executes a prepared command in the current address space, overwriting the 69 // running process with the new command. 70 export fn exec(cmd: *command) never = { 71 defer finish(cmd); // Note: doesn't happen if exec succeeds 72 platform_exec(cmd): void; 73 abort("os::exec::exec failed"); 74 }; 75 76 // Starts a prepared command in a new process. 77 export fn start(cmd: *command) (process | error) = { 78 defer finish(cmd); 79 match (platform_start(cmd)) { 80 case let err: errors::error => 81 return err; 82 case let proc: process => 83 return proc; 84 }; 85 }; 86 87 // Empties the environment variables for the command. By default, the command 88 // inherits the environment of the parent process. 89 export fn clearenv(cmd: *command) void = { 90 strings::freeall(cmd.env); 91 cmd.env = []; 92 }; 93 94 // Removes a variable in the command environment. This does not affect the 95 // current process environment. The key may not contain '=' or '\0'. 96 export fn unsetenv(cmd: *command, key: str) (void | errors::invalid) = { 97 if (strings::contains(key, '=', '\0')) return errors::invalid; 98 99 // XXX: This can be a binary search 100 for (let i = 0z; i < len(cmd.env); i += 1) { 101 if (strings::cut(cmd.env[i], "=").0 == key) { 102 free(cmd.env[i]); 103 delete(cmd.env[i]); 104 break; 105 }; 106 }; 107 }; 108 109 // Adds or sets a variable in the command environment. This does not affect the 110 // current process environment. The key may not contain '=' or '\0'. 111 export fn setenv(cmd: *command, key: str, value: str) (void | errors::invalid) = { 112 if (strings::contains(value, '\0')) return errors::invalid; 113 unsetenv(cmd, key)?; 114 append(cmd.env, strings::join("=", key, value))!; 115 }; 116 117 // Configures a file in the child process's file table, such that the file 118 // described by the 'source' parameter is mapped onto file descriptor slot 119 // 'child' in the child process via dup(2). 120 // 121 // This operation is performed atomically, such that the following code swaps 122 // stdout and stderr: 123 // 124 // exec::addfile(&cmd, os::stderr_file, os::stdout_file); 125 // exec::addfile(&cmd, os::stdout_file, os::stderr_file); 126 // 127 // Pass [[nullfd]] in the 'source' argument to map the child's file descriptor 128 // to /dev/null or the appropriate platform-specific equivalent. 129 // 130 // Pass [[closefd]] in the 'source' argument to close a file descriptor which 131 // was not opened with the CLOEXEC flag. Note that Hare opens all files with 132 // CLOEXEC by default, so this is not usually necessary. 133 // 134 // To write to a process's stdin, capture its stdout, or pipe two programs 135 // together, see the [[pipe]] function. 136 export fn addfile( 137 cmd: *command, 138 child: io::file, 139 source: (io::file | nullfd | closefd), 140 ) void = { 141 append(cmd.files, (source, child))!; 142 }; 143 144 // Closes all standard files (stdin, stdout, and stderr) in the child process. 145 // Many programs do not work well under these conditions; you may want 146 // [[nullstd]] instead. 147 export fn closestd(cmd: *command) void = { 148 addfile(cmd, os::stdin_file, closefd); 149 addfile(cmd, os::stdout_file, closefd); 150 addfile(cmd, os::stderr_file, closefd); 151 }; 152 153 // Redirects all standard files (stdin, stdout, and stderr) to /dev/null or the 154 // platform-specific equivalent. 155 export fn nullstd(cmd: *command) void = { 156 addfile(cmd, os::stdin_file, nullfd); 157 addfile(cmd, os::stdout_file, nullfd); 158 addfile(cmd, os::stderr_file, nullfd); 159 }; 160 161 // Configures the child process's working directory. This does not affect the 162 // process environment. The path is borrowed from the input, and must outlive 163 // the command. 164 export fn chdir(cmd: *command, dir: str) void = { 165 cmd.dir = dir; 166 }; 167 168 // Similar to [[lookup]] but TOCTOU-proof 169 fn lookup_open(name: str) (platform_cmd | void | error) = { 170 static let pbuf = path::path { ... }; 171 path::set(&pbuf)!; 172 173 // Try to open file directly 174 if (strings::contains(name, "/")) { 175 match (open(name)) { 176 case (errors::noaccess | errors::noentry) => void; 177 case let err: error => 178 return err; 179 case let p: platform_cmd => 180 return p; 181 }; 182 }; 183 184 const path = match (os::getenv("PATH")) { 185 case void => 186 return; 187 case let s: str => 188 yield s; 189 }; 190 191 let tok = strings::tokenize(path, ":"); 192 for (let item => strings::next_token(&tok)) { 193 path::set(&pbuf, item, name)!; 194 195 match (open(path::string(&pbuf))) { 196 case (errors::noaccess | errors::noentry) => 197 continue; 198 case let err: error => 199 return err; 200 case let p: platform_cmd => 201 return p; 202 }; 203 }; 204 }; 205 206 // Looks up an executable by name in the system PATH. The return value is 207 // statically allocated. 208 // 209 // The use of this function is lightly discouraged if [[cmd]] is suitable; 210 // otherwise you may have a TOCTOU issue. 211 export fn lookup(name: str) (str | void) = { 212 static let pbuf = path::path { ... }; 213 path::set(&pbuf)!; 214 215 // Try to open file directly 216 if (strings::contains(name, "/")) { 217 match (os::access(name, os::amode::X_OK)) { 218 case let exec: bool => 219 if (exec) { 220 return name; 221 }; 222 case => void; // Keep looking 223 }; 224 }; 225 226 const path = match (os::getenv("PATH")) { 227 case void => 228 return; 229 case let s: str => 230 yield s; 231 }; 232 233 let tok = strings::tokenize(path, ":"); 234 for (let item => strings::next_token(&tok)) { 235 path::set(&pbuf, item, name)!; 236 237 match (os::access(path::string(&pbuf), os::amode::X_OK)) { 238 case let exec: bool => 239 if (exec) { 240 return path::string(&pbuf); 241 }; 242 case => void; // Keep looking 243 }; 244 }; 245 };