hare

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

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