File: cmd.ha

package info (click to toggle)
hare 0.25.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,948 kB
  • sloc: asm: 1,264; makefile: 123; sh: 114; lisp: 101
file content (261 lines) | stat: -rw-r--r-- 7,433 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
// SPDX-License-Identifier: MPL-2.0
// (c) Hare authors <https://harelang.org>

use errors;
use fs;
use io;
use os;
use path;
use strings;

// Prepares a [[command]] based on its name and a list of arguments. The
// argument list should not start with the command name; it will be added for
// you. The argument list is borrowed from the strings you pass into this
// command.
//
// If 'name' does not contain a '/', the $PATH will be consulted to find the
// correct executable. If path resolution fails, [[nocmd]] is returned.
//
//	let cmd = exec::cmd("echo", "hello world")!;
//	let proc = exec::start(&cmd)!;
//	let status = exec::wait(&proc)!;
//	assert(exec::check(&status) is void);
//
// By default, the new command will inherit the current process's environment.
export fn cmd(name: str, args: str...) (command | error | nomem) = {
	let platcmd = if (strings::contains(name, '/')) {
		yield match (open(name)) {
		case let p: platform_cmd =>
			yield p;
		case let err: error =>
			return err;
		case =>
			return nocmd;
		};
	} else {
		yield match (lookup_open(name)?) {
		case void =>
			return nocmd;
		case let p: platform_cmd =>
			yield p;
		};
	};

	let argv = platform_newargv(name, args...)?;

	let env = match (platform_dup_env(os::getenvs())) {
	case let env: platform_env =>
		yield env;
	case nomem =>
		platform_free_argv(argv);
		return nomem;
	};

	return command {
		platform = platcmd,
		files = [],
		dir = "",
		argv = argv,
		env = env,
		...
	};
};

// Frees state associated with a command. You only need to call this if you do
// not execute the command with [[exec]] or [[start]]; in those cases the state
// is cleaned up for you.
export fn finish(cmd: *command) void = {
	platform_finish(cmd);
	platform_free_argv(cmd.argv);
	platform_free_env(cmd.env);
	free(cmd.files);
};

// Executes a prepared command in the current address space, overwriting the
// running process with the new command.
//
// This function will only return if an error occurs.
//
//	const cmd = exec::cmd("/bin/sh", "-c", "echo hello world")?;
//	const err = exec::exec(&cmd);
//	// This only runs if exec failed:
//	fmt::fatal("exec /bin/sh:", exec::strerror(err));
export fn exec(cmd: *command) error = {
	defer finish(cmd); // Note: doesn't happen if exec succeeds
	return platform_exec(cmd);
};

// Starts a prepared command in a new process.
export fn start(cmd: *command) (process | error) = {
	defer finish(cmd);
	match (platform_start(cmd)) {
	case let err: errors::error =>
		return err;
	case let proc: process =>
		return proc;
	};
};

// Empties the environment variables for the command. By default, the command
// inherits the environment of the parent process.
export fn clearenv(cmd: *command) void = {
	platform_free_env(cmd.env);
	cmd.env = platfrom_newenv();
};

// Adds or sets a variable in the command environment. This does not affect the
// current process environment. The key may not contain '=' or '\0'.
export fn setenv(
	cmd: *command,
	key: str,
	value: str
) (void | errors::invalid | nomem) = {
	if (strings::contains(value, '\0')) return errors::invalid;
	unsetenv(cmd, key)?;
	return platform_setenv(cmd, key, value);
};

// Removes a variable in the command environment. This does not affect the
// current process environment. The key may not contain '=' or '\0'.
export fn unsetenv(cmd: *command, key: str) (void | errors::invalid) = {
	if (strings::contains(key, '=', '\0')) return errors::invalid;
	return platform_unsetenv(cmd, key);
};

// Sets the 0th value of argv for this command. It is uncommon to need this.
export fn setname(cmd: *command, name: str) (void | nomem) =
	platform_setname(cmd, name);

// Configures a file in the child process's file table, such that the file
// described by the 'source' parameter is mapped onto file descriptor slot
// 'child' in the child process via dup(2).
//
// This operation is performed atomically, such that the following code swaps
// stdout and stderr:
//
// 	exec::addfile(&cmd, os::stderr_file, os::stdout_file);
// 	exec::addfile(&cmd, os::stdout_file, os::stderr_file);
//
// Pass [[nullfd]] in the 'source' argument to map the child's file descriptor
// to /dev/null or the appropriate platform-specific equivalent.
//
// Pass [[closefd]] in the 'source' argument to close a file descriptor which
// was not opened with the CLOEXEC flag. Note that Hare opens all files with
// CLOEXEC by default, so this is not usually necessary.
//
// To write to a process's stdin, capture its stdout, or pipe two programs
// together, see the [[pipe]] function.
export fn addfile(
	cmd: *command,
	child: io::file,
	source: (io::file | nullfd | closefd),
) (void | nomem) = {
	append(cmd.files, (source, child))?;
};

// Closes all standard files (stdin, stdout, and stderr) in the child process.
// Many programs do not work well under these conditions; you may want
// [[nullstd]] instead.
export fn closestd(cmd: *command) (void | nomem) = {
	addfile(cmd, os::stdin_file, closefd)?;
	addfile(cmd, os::stdout_file, closefd)?;
	addfile(cmd, os::stderr_file, closefd)?;
};

// Redirects all standard files (stdin, stdout, and stderr) to /dev/null or the
// platform-specific equivalent.
export fn nullstd(cmd: *command) (void | nomem) = {
	addfile(cmd, os::stdin_file, nullfd)?;
	addfile(cmd, os::stdout_file, nullfd)?;
	addfile(cmd, os::stderr_file, nullfd)?;
};

// Configures the child process's working directory. This does not affect the
// process environment. The path is borrowed from the input, and must outlive
// the command.
export fn chdir(cmd: *command, dir: str) void = {
	cmd.dir = dir;
};

// Similar to [[lookup]] but TOCTOU-proof
fn lookup_open(name: str) (platform_cmd | void | error) = {
	static let buf = path::buffer { ... };
	path::set(&buf)!;

	// Try to open file directly
	if (strings::contains(name, "/")) {
		match (open(name)) {
		case let err: error =>
			return err;
		case let p: platform_cmd =>
			return p;
		};
	};

	const path = match (os::getenv("PATH")) {
	case void =>
		return;
	case let s: str =>
		yield s;
	};

	let tok = strings::tokenize(path, ":");
	for (let item => strings::next_token(&tok)) {
		path::set(&buf, item, name)!;

		match (open(path::string(&buf))) {
		case (errors::noaccess | errors::noentry) =>
			continue;
		case let err: error =>
			return err;
		case let p: platform_cmd =>
			return p;
		};
	};
};

// Looks up an executable by name in the system PATH. The return value is
// statically allocated.
//
// The use of this function is lightly discouraged if [[cmd]] is suitable;
// otherwise you may have a TOCTOU issue.
export fn lookup(name: str) (str | fs::error) = {
	static let buf = path::buffer { ... };
	path::set(&buf)!;

	// Try to open file directly
	if (strings::contains(name, "/")) {
		match (os::access(name, os::amode::X_OK)) {
		case let exec: bool =>
			if (exec) {
				path::set(&buf, name)!;
				return path::string(&buf);
			};
			return errors::noaccess;
		case =>
			return errors::noentry;
		};
	};

	const path = match (os::getenv("PATH")) {
	case let s: str =>
		yield s;
	case void =>
		return errors::noentry;
	};

	let tok = strings::tokenize(path, ":");
	for (let item => strings::next_token(&tok)) {
		path::set(&buf, item, name)!;

		match (os::access(path::string(&buf), os::amode::X_OK)) {
		case let exec: bool =>
			if (exec) {
				return path::string(&buf);
			};
		case => void; // Keep looking
		};
	};

	return errors::noentry;
};