hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

cmd.ha (7237B)


      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) =>
    176 			yield;
    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 (true) {
    193 		const item = match (strings::next_token(&tok)) {
    194 		case void =>
    195 			break;
    196 		case let s: str =>
    197 			yield s;
    198 		};
    199 		path::set(&buf, item, name)!;
    200 		match (open(path::string(&buf))) {
    201 		case (errors::noaccess | errors::noentry) =>
    202 			continue;
    203 		case let err: error =>
    204 			return err;
    205 		case let p: platform_cmd =>
    206 			return p;
    207 		};
    208 	};
    209 };
    210 
    211 // Looks up an executable by name in the system PATH. The return value is
    212 // statically allocated.
    213 //
    214 // The use of this function is lightly discouraged if [[cmd]] is suitable;
    215 // otherwise you may have a TOCTOU issue.
    216 export fn lookup(name: str) (str | void) = {
    217 	static let buf = path::buffer { ... };
    218 	path::set(&buf)!;
    219 
    220 	// Try to open file directly
    221 	if (strings::contains(name, "/")) {
    222 		match (os::access(name, os::amode::X_OK)) {
    223 		case let exec: bool =>
    224 			if (exec) {
    225 				return name;
    226 			};
    227 		case => void; // Keep looking
    228 		};
    229 	};
    230 
    231 	const path = match (os::getenv("PATH")) {
    232 	case void =>
    233 		return;
    234 	case let s: str =>
    235 		yield s;
    236 	};
    237 
    238 	let tok = strings::tokenize(path, ":");
    239 	for (true) {
    240 		const item = match (strings::next_token(&tok)) {
    241 		case void =>
    242 			break;
    243 		case let s: str =>
    244 			yield s;
    245 		};
    246 		path::set(&buf, item, name)!;
    247 		match (os::access(path::string(&buf), os::amode::X_OK)) {
    248 		case let exec: bool =>
    249 			if (exec) {
    250 				return path::string(&buf);
    251 			};
    252 		case => void; // Keep looking
    253 		};
    254 	};
    255 };