hare

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

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 };