Skip to content

Commit 39dbd5e

Browse files
committed
WIP Cmd API overhaul
1 parent 3459e2c commit 39dbd5e

7 files changed

Lines changed: 188 additions & 136 deletions

File tree

ci/all_tests.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ for roc_file in $TESTS_DIR*.roc; do
101101
roc_file_only="$(basename "$roc_file")"
102102
no_ext_name=${roc_file_only%.*}
103103

104+
if [ "$no_ext_name" == "cmd-test" ]; then
105+
continue
106+
fi
107+
104108
expect ci/expect_scripts/$no_ext_name.exp
105109
done
106110

crates/roc_command/src/lib.rs

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! This crate provides common functionality for Roc to interface with `std::process::Command`
2+
23
use roc_std::{RocList, RocResult, RocStr};
34

45
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -61,29 +62,50 @@ impl From<&Command> for std::process::Command {
6162

6263
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
6364
#[repr(C)]
64-
pub struct OutputFromHost {
65-
pub status: roc_std::RocResult<i32, roc_io_error::IOErr>,
66-
pub stderr: roc_std::RocList<u8>,
67-
pub stdout: roc_std::RocList<u8>,
65+
pub struct OutputFromHostSuccess {
66+
pub stdout_bytes: roc_std::RocList<u8>,
67+
pub stderr_bytes: roc_std::RocList<u8>,
68+
}
69+
70+
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
71+
#[repr(C)]
72+
pub struct OutputFromHostFailure {
73+
pub status: i32,
74+
pub stdout_bytes: roc_std::RocList<u8>,
75+
pub stderr_bytes: roc_std::RocList<u8>,
6876
}
6977

70-
impl roc_std::RocRefcounted for OutputFromHost {
78+
impl roc_std::RocRefcounted for OutputFromHostSuccess {
79+
fn inc(&mut self) {
80+
self.stderr_bytes.inc();
81+
self.stdout_bytes.inc();
82+
}
83+
fn dec(&mut self) {
84+
self.stderr_bytes.dec();
85+
self.stdout_bytes.dec();
86+
}
87+
fn is_refcounted() -> bool {
88+
true
89+
}
90+
}
91+
92+
impl roc_std::RocRefcounted for OutputFromHostFailure {
7193
fn inc(&mut self) {
7294
self.status.inc();
73-
self.stderr.inc();
74-
self.stdout.inc();
95+
self.stderr_bytes.inc();
96+
self.stdout_bytes.inc();
7597
}
7698
fn dec(&mut self) {
7799
self.status.dec();
78-
self.stderr.dec();
79-
self.stdout.dec();
100+
self.stderr_bytes.dec();
101+
self.stdout_bytes.dec();
80102
}
81103
fn is_refcounted() -> bool {
82104
true
83105
}
84106
}
85107

86-
pub fn command_status(roc_cmd: &Command) -> RocResult<i32, roc_io_error::IOErr> {
108+
pub fn command_exec_exit_code(roc_cmd: &Command) -> RocResult<i32, roc_io_error::IOErr> {
87109
match std::process::Command::from(roc_cmd).status() {
88110
Ok(status) => from_exit_status(status),
89111
Err(err) => RocResult::err(err.into()),
@@ -94,29 +116,33 @@ pub fn command_status(roc_cmd: &Command) -> RocResult<i32, roc_io_error::IOErr>
94116
fn from_exit_status(status: std::process::ExitStatus) -> RocResult<i32, roc_io_error::IOErr> {
95117
match status.code() {
96118
Some(code) => RocResult::ok(code),
97-
None => killed_by_signal(),
119+
None => RocResult::err(killed_by_signal_err()),
98120
}
99121
}
100122

101-
// If no exit code is returned, the process was terminated by a signal.
102-
fn killed_by_signal() -> RocResult<i32, roc_io_error::IOErr> {
103-
RocResult::err(roc_io_error::IOErr {
123+
fn killed_by_signal_err() -> roc_io_error::IOErr {
124+
roc_io_error::IOErr {
104125
tag: roc_io_error::IOErrTag::Other,
105-
msg: "Killed by signal".into(),
106-
})
126+
msg: "Process was killed by operating system signal.".into(),
127+
}
107128
}
108129

109-
pub fn command_output(roc_cmd: &Command) -> OutputFromHost {
130+
// TODO Can we make this return a tag union (with three variants) ?
131+
pub fn command_exec_output(roc_cmd: &Command) -> RocResult<OutputFromHostSuccess, RocResult<OutputFromHostFailure, roc_io_error::IOErr>> {
110132
match std::process::Command::from(roc_cmd).output() {
111-
Ok(output) => OutputFromHost {
112-
status: from_exit_status(output.status),
113-
stdout: RocList::from(&output.stdout[..]),
114-
stderr: RocList::from(&output.stderr[..]),
115-
},
116-
Err(err) => OutputFromHost {
117-
status: RocResult::err(err.into()),
118-
stdout: RocList::empty(),
119-
stderr: RocList::empty(),
120-
},
133+
Ok(output) =>
134+
match output.status.code() {
135+
Some(status) if status == 0 => RocResult::ok(OutputFromHostSuccess {
136+
stdout_bytes: RocList::from(&output.stdout[..]),
137+
stderr_bytes: RocList::from(&output.stderr[..]),
138+
}),
139+
Some(status) => RocResult::err(RocResult::ok(OutputFromHostFailure {
140+
status: status,
141+
stdout_bytes: RocList::from(&output.stdout[..]),
142+
stderr_bytes: RocList::from(&output.stdout[..]),
143+
})),
144+
None => RocResult::err(RocResult::err(killed_by_signal_err()))
145+
}
146+
Err(err) => RocResult::err(RocResult::err(err.into()))
121147
}
122148
}

crates/roc_host/src/lib.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,8 @@ pub fn init() {
339339
roc_fx_tcp_read_exactly as _,
340340
roc_fx_tcp_read_until as _,
341341
roc_fx_tcp_write as _,
342-
roc_fx_command_status as _,
343-
roc_fx_command_output as _,
342+
roc_fx_command_exec_exit_code as _,
343+
roc_fx_command_exec_output as _,
344344
roc_fx_dir_create as _,
345345
roc_fx_dir_create_all as _,
346346
roc_fx_dir_delete_empty as _,
@@ -742,17 +742,17 @@ pub extern "C" fn roc_fx_tcp_write(stream: RocBox<()>, msg: &RocList<u8>) -> Roc
742742
}
743743

744744
#[no_mangle]
745-
pub extern "C" fn roc_fx_command_status(
745+
pub extern "C" fn roc_fx_command_exec_exit_code(
746746
roc_cmd: &roc_command::Command,
747747
) -> RocResult<i32, roc_io_error::IOErr> {
748-
roc_command::command_status(roc_cmd)
748+
roc_command::command_exec_exit_code(roc_cmd)
749749
}
750750

751751
#[no_mangle]
752-
pub extern "C" fn roc_fx_command_output(
752+
pub extern "C" fn roc_fx_command_exec_output(
753753
roc_cmd: &roc_command::Command,
754-
) -> roc_command::OutputFromHost {
755-
roc_command::command_output(roc_cmd)
754+
) -> RocResult<roc_command::OutputFromHostSuccess, RocResult<roc_command::OutputFromHostFailure, roc_io_error::IOErr>> {
755+
roc_command::command_exec_output(roc_cmd)
756756
}
757757

758758
#[no_mangle]

platform/Cmd.roc

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,104 @@
11
module [
22
Cmd,
3-
Output,
43
new,
54
arg,
65
args,
76
env,
87
envs,
98
clear_envs,
10-
status!,
11-
output!,
9+
exec_output!,
1210
exec!,
11+
exec_cmd!,
1312
]
1413

1514
import InternalCmd
16-
import InternalIOErr
15+
import InternalIOErr exposing [IOErr]
1716
import Host
1817

1918
## Represents a command to be executed in a child process.
2019
Cmd := InternalCmd.Command
2120

22-
## Represents the output of a command.
23-
##
24-
## Output is a record:
21+
## Simplest way to execute a command while inheriting stdin, stdout and stderr from parent.
22+
## If you want to capture the output, use [exec_output!].
23+
## ```
24+
## # Call echo to print "hello world"
25+
## Cmd.exec!("echo", ["hello world"]) ? |err| CmdEchoFailed(err)
26+
## ```
27+
exec! : Str, List Str => Result {} [ExecFailed Str (List Str) I32, FailedToGetExitCode IOErr]
28+
exec! = |cmd_name, arguments|
29+
exit_code =
30+
new(cmd_name)
31+
|> args(arguments)
32+
|> exec_exit_code!?
33+
34+
if exit_code == 0i32 then
35+
Ok({})
36+
else
37+
Err(ExecFailed(cmd_name, arguments, exit_code))
38+
39+
## Execute a Cmd while inheriting stdin, stdout and stderr from parent.
40+
## You should prefer using `exec!`, only use this if you want to use [env], [envs] or [clear_envs].
41+
## If you want to capture the output, use [exec_output!].
2542
## ```
26-
## {
27-
## status : Result I32 InternalIOErr.IOErr,
28-
## stdout : List U8,
29-
## stderr : List U8,
30-
## }
43+
## # Execute `cargo build` with env var.
44+
## Cmd.new("cargo")
45+
## |> Cmd.arg("build")
46+
## |> Cmd.env("RUST_BACKTRACE", "1")
47+
## |> Cmd.exec_cmd!()?
3148
## ```
49+
exec_cmd! : Cmd => Result {} [ExecCmdFailed Cmd I32, FailedToGetExitCode IOErr]
50+
exec_cmd! = |cmd|
51+
exit_code =
52+
exec_exit_code!(cmd)?
53+
54+
if exit_code == 0i32 then
55+
Ok({})
56+
else
57+
Err(ExecCmdFailed(cmd, exit_code))
58+
59+
## Execute command and capture stdout, stderr and the exit code in [Output].
3260
##
33-
Output : InternalCmd.Output
61+
## > Stdin is not inherited from the parent and any attempt by the child process
62+
## > to read from the stdin stream will result in the stream immediately closing.
63+
##
64+
## TODO: explain when to use exec_output_bytes! vs exec_output!
65+
exec_output! : Cmd =>
66+
Result
67+
{stdout_utf8 : Str, stderr_utf8_lossy : Str}
68+
[
69+
StdoutContainsInvalidUtf8([BadUtf8 { index : U64, problem : Str.Utf8Problem }]),
70+
NonZeroExitCode({exit_code: I32, stdout_utf8_lossy: Str, stderr_utf8_lossy: Str}),
71+
FailedToGetExitCode(IOErr)
72+
]
73+
exec_output! = |@Cmd(cmd)|
74+
exec_res = Host.command_exec_output!(cmd)
75+
76+
when exec_res is
77+
Ok({stdout_bytes, stderr_bytes}) ->
78+
stdout_utf8 = Str.from_utf8(stdout_bytes) ? |err| StdoutContainsInvalidUtf8(err)
79+
stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes)
80+
81+
Ok({stdout_utf8, stderr_utf8_lossy})
82+
Err(inside_res) ->
83+
when inside_res is
84+
Ok({exit_code, stdout_bytes, stderr_bytes}) ->
85+
stdout_utf8_lossy = Str.from_utf8_lossy(stdout_bytes)
86+
stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes)
87+
88+
Err(NonZeroExitCode({exit_code, stdout_utf8_lossy, stderr_utf8_lossy}))
89+
Err(err) ->
90+
Err(InternalIOErr.handle_err(err))
91+
|> Result.map_err(FailedToGetExitCode)
92+
93+
# TODO exec_output_bytes
94+
95+
## Execute command and inherit stdin, stdout and stderr from parent. Returns the exit code.
96+
## Helper function.
97+
exec_exit_code! : Cmd => Result I32 [FailedToGetExitCode IOErr]
98+
exec_exit_code! = |@Cmd(cmd)|
99+
Host.command_exec_exit_code!(cmd)
100+
|> Result.map_err(InternalIOErr.handle_err)
101+
|> Result.map_err(FailedToGetExitCode)
34102

35103
# This hits a compiler bug: Alias `6.IdentId(11)` not registered in delayed aliases! ...
36104
# ## Converts output into a utf8 string. Invalid utf8 sequences in stderr are ignored.
@@ -119,39 +187,3 @@ envs = |@Cmd(cmd), key_values|
119187
clear_envs : Cmd -> Cmd
120188
clear_envs = |@Cmd(cmd)|
121189
@Cmd({ cmd & clear_envs: Bool.true })
122-
123-
## Execute command and capture stdout, stderr and the exit code in [Output].
124-
##
125-
## > Stdin is not inherited from the parent and any attempt by the child process
126-
## > to read from the stdin stream will result in the stream immediately closing.
127-
##
128-
output! : Cmd => Output
129-
output! = |@Cmd(cmd)|
130-
Host.command_output!(cmd)
131-
|> InternalCmd.from_host_output
132-
133-
## Execute command and inherit stdin, stdout and stderr from parent. Returns the exit code.
134-
##
135-
status! : Cmd => Result I32 [CmdStatusErr InternalIOErr.IOErr]
136-
status! = |@Cmd(cmd)|
137-
Host.command_status!(cmd)
138-
|> Result.map_err(InternalIOErr.handle_err)
139-
|> Result.map_err(CmdStatusErr)
140-
141-
## Simplest way to execute a command while inheriting stdin, stdout and stderr from parent.
142-
##
143-
## ```
144-
## # Call echo to print "hello world"
145-
## Cmd.exec!("echo", ["hello world"]) ? |err| CmdEchoFailed(err)
146-
## ```
147-
exec! : Str, List Str => Result {} [CmdStatusErr InternalIOErr.IOErr]
148-
exec! = |program, arguments|
149-
exit_code =
150-
new(program)
151-
|> args(arguments)
152-
|> status!?
153-
154-
if exit_code == 0i32 then
155-
Ok({})
156-
else
157-
Err(CmdStatusErr(Other("Non-zero exit code ${Num.to_str(exit_code)}")))

platform/Host.roc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
hosted [
22
FileReader,
33
TcpStream,
4-
command_output!,
5-
command_status!,
4+
command_exec_output!,
5+
command_exec_exit_code!,
66
current_arch_os!,
77
cwd!,
88
dir_create!,
@@ -67,8 +67,8 @@ import InternalPath
6767
import InternalIOErr
6868
import InternalSqlite
6969
# COMMAND
70-
command_status! : InternalCmd.Command => Result I32 InternalIOErr.IOErrFromHost
71-
command_output! : InternalCmd.Command => InternalCmd.OutputFromHost
70+
command_exec_exit_code! : InternalCmd.Command => Result I32 InternalIOErr.IOErrFromHost
71+
command_exec_output! : InternalCmd.Command => Result InternalCmd.OutputFromHostSuccess (Result InternalCmd.OutputFromHostFailure InternalIOErr.IOErrFromHost)
7272

7373
# FILE
7474
file_write_bytes! : List U8, List U8 => Result {} InternalIOErr.IOErrFromHost

0 commit comments

Comments
 (0)