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