cmd.ha (6170B)
1 // License: MPL-2.0 2 // (c) 2021-2022 Drew DeVault <sir@cmpwn.com> 3 // (c) 2021 Ember Sawady <ecs@d2evs.net> 4 // (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz> 5 use ascii; 6 use errors; 7 use io; 8 use os; 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 file = 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(name)?) { 34 case void => 35 return nocmd; 36 case let p: platform_cmd => 37 yield p; 38 }; 39 }; 40 return cmdfile(file, name, args...); 41 }; 42 43 // Same as [[cmd]] except that executable file is determined by [[io::file]]. 44 export fn cmdfile(file: io::file, name: str, args: str...) command = { 45 let cmd = command { 46 platform = file, 47 argv = alloc([], len(args) + 1), 48 env = strings::dupall(os::getenvs()), 49 files = [], 50 dir = "", 51 }; 52 append(cmd.argv, name); 53 append(cmd.argv, args...); 54 return cmd; 55 }; 56 57 // Sets the 0th value of argv for this command. It is uncommon to need this. 58 export fn setname(cmd: *command, name: str) void = { 59 free(cmd.argv[0]); 60 cmd.argv[0] = name; 61 }; 62 63 // Frees state associated with a command. You only need to call this if you do 64 // not execute the command with [[exec]] or [[start]]; in those cases the state is 65 // cleaned up for you. 66 export fn finish(cmd: *command) void = { 67 platform_finish(cmd); 68 free(cmd.argv); 69 free(cmd.files); 70 strings::freeall(cmd.env); 71 }; 72 73 // Executes a prepared command in the current address space, overwriting the 74 // running process with the new command. 75 export @noreturn fn exec(cmd: *command) void = { 76 defer finish(cmd); // Note: doesn't happen if exec succeeds 77 platform_exec(cmd): void; 78 abort("os::exec::exec failed"); 79 }; 80 81 // Starts a prepared command in a new process. 82 export fn start(cmd: *command) (process | error) = { 83 defer finish(cmd); 84 match (platform_start(cmd)) { 85 case let err: errors::error => 86 return err; 87 case let proc: process => 88 return proc; 89 }; 90 }; 91 92 // Empties the environment variables for the command. By default, the command 93 // inherits the environment of the parent process. 94 export fn clearenv(cmd: *command) void = { 95 strings::freeall(cmd.env); 96 cmd.env = []; 97 }; 98 99 // Removes a variable in the command environment. This does not affect the 100 // current process environment. The key may not contain '=' or '\0'. 101 export fn unsetenv(cmd: *command, key: str) (void | errors::invalid) = { 102 if (strings::contains(key, '=', '\0')) return errors::invalid; 103 104 // XXX: This can be a binary search 105 let fullkey = strings::concat(key, "="); 106 defer free(fullkey); 107 for (let i = 0z; i < len(cmd.env); i += 1) { 108 if (strings::hasprefix(cmd.env[i], fullkey)) { 109 free(cmd.env[i]); 110 delete(cmd.env[i]); 111 break; 112 }; 113 }; 114 }; 115 116 117 // Adds or sets a variable in the command environment. This does not affect the 118 // current process environment. The key may not contain '=' or '\0'. 119 export fn setenv(cmd: *command, key: str, value: str) (void | errors::invalid) = { 120 unsetenv(cmd, key)?; 121 append(cmd.env, strings::join("=", key, value)); 122 }; 123 124 // Configures a file in the child process's file table, such that the file 125 // described by the 'source' parameter is mapped onto file descriptor slot 126 // 'child' in the child process via dup(2). 127 // 128 // This operation is performed atomically, such that the following code swaps 129 // stdout and stderr: 130 // 131 // exec::addfile(&cmd, os::stderr_file, os::stdout_file); 132 // exec::addfile(&cmd, os::stdout_file, os::stderr_file); 133 // 134 // Pass [[nullfd]] in the 'source' argument to map the child's file descriptor 135 // to /dev/null or the appropriate platform-specific equivalent. 136 // 137 // Pass [[closefd]] in the 'source' argument to close a file descriptor which 138 // was not opened with the CLOEXEC flag. Note that Hare opens all files with 139 // CLOEXEC by default, so this is not usually necessary. 140 // 141 // To write to a process's stdin, capture its stdout, or pipe two programs 142 // together, see the [[pipe]] function. 143 export fn addfile( 144 cmd: *command, 145 child: io::file, 146 source: (io::file | nullfd | closefd), 147 ) void = { 148 append(cmd.files, (source, child)); 149 }; 150 151 // Closes all standard files (stdin, stdout, and stderr) in the child process. 152 // Many programs do not work well under these conditions; you may want 153 // [[nullstd]] instead. 154 export fn closestd(cmd: *command) void = { 155 addfile(cmd, os::stdin_file, closefd); 156 addfile(cmd, os::stdout_file, closefd); 157 addfile(cmd, os::stderr_file, closefd); 158 }; 159 160 // Redirects all standard files (stdin, stdout, and stderr) to /dev/null or the 161 // platform-specific equivalent. 162 export fn nullstd(cmd: *command) void = { 163 addfile(cmd, os::stdin_file, nullfd); 164 addfile(cmd, os::stdout_file, nullfd); 165 addfile(cmd, os::stderr_file, nullfd); 166 }; 167 168 // Configures the child process's working directory. This does not affect the 169 // process environment. The path is borrowed from the input, and must outlive 170 // the command. 171 export fn chdir(cmd: *command, dir: str) void = { 172 cmd.dir = dir; 173 }; 174 175 fn lookup(name: str) (platform_cmd | void | error) = { 176 const path = match (os::getenv("PATH")) { 177 case void => 178 return; 179 case let s: str => 180 yield s; 181 }; 182 let tok = strings::tokenize(path, ":"); 183 for (true) { 184 const item = match (strings::next_token(&tok)) { 185 case void => 186 break; 187 case let s: str => 188 yield s; 189 }; 190 let path = strings::concat(item, "/", name); 191 defer free(path); 192 match (open(path)) { 193 case (errors::noaccess | errors::noentry) => 194 continue; 195 case let err: error => 196 return err; 197 case let p: platform_cmd => 198 return p; 199 }; 200 }; 201 };