Skip to content

Commit 7a2ba91

Browse files
committed
finished Cmd API
1 parent 1159296 commit 7a2ba91

5 files changed

Lines changed: 94 additions & 79 deletions

File tree

crates/roc_command/src/lib.rs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,26 +63,26 @@ impl From<&Command> for std::process::Command {
6363
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
6464
#[repr(C)]
6565
pub struct OutputFromHostSuccess {
66-
pub stdout_bytes: roc_std::RocList<u8>,
6766
pub stderr_bytes: roc_std::RocList<u8>,
67+
pub stdout_bytes: roc_std::RocList<u8>,
6868
}
6969

7070
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
7171
#[repr(C)]
7272
pub struct OutputFromHostFailure {
73-
pub status: i32,
74-
pub stdout_bytes: roc_std::RocList<u8>,
7573
pub stderr_bytes: roc_std::RocList<u8>,
74+
pub stdout_bytes: roc_std::RocList<u8>,
75+
pub exit_code: i32,
7676
}
7777

7878
impl roc_std::RocRefcounted for OutputFromHostSuccess {
7979
fn inc(&mut self) {
80-
self.stderr_bytes.inc();
8180
self.stdout_bytes.inc();
81+
self.stderr_bytes.inc();
8282
}
8383
fn dec(&mut self) {
84-
self.stderr_bytes.dec();
8584
self.stdout_bytes.dec();
85+
self.stderr_bytes.dec();
8686
}
8787
fn is_refcounted() -> bool {
8888
true
@@ -91,14 +91,14 @@ impl roc_std::RocRefcounted for OutputFromHostSuccess {
9191

9292
impl roc_std::RocRefcounted for OutputFromHostFailure {
9393
fn inc(&mut self) {
94-
self.status.inc();
95-
self.stderr_bytes.inc();
94+
self.exit_code.inc();
9695
self.stdout_bytes.inc();
96+
self.stderr_bytes.inc();
9797
}
9898
fn dec(&mut self) {
99-
self.status.dec();
100-
self.stderr_bytes.dec();
99+
self.exit_code.dec();
101100
self.stdout_bytes.dec();
101+
self.stderr_bytes.dec();
102102
}
103103
fn is_refcounted() -> bool {
104104
true
@@ -132,15 +132,27 @@ pub fn command_exec_output(roc_cmd: &Command) -> RocResult<OutputFromHostSuccess
132132
match std::process::Command::from(roc_cmd).output() {
133133
Ok(output) =>
134134
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-
})),
135+
Some(status) => {
136+
137+
let stdout_bytes = RocList::from(&output.stdout[..]);
138+
let stderr_bytes = RocList::from(&output.stderr[..]);
139+
140+
if status == 0 {
141+
// Success case
142+
RocResult::ok(OutputFromHostSuccess {
143+
stderr_bytes,
144+
stdout_bytes,
145+
})
146+
} else {
147+
println!("{:?}", stderr_bytes);
148+
// Failure case
149+
RocResult::err(RocResult::ok(OutputFromHostFailure {
150+
stderr_bytes,
151+
stdout_bytes,
152+
exit_code: status,
153+
}))
154+
}
155+
},
144156
None => RocResult::err(RocResult::err(killed_by_signal_err()))
145157
}
146158
Err(err) => RocResult::err(RocResult::err(err.into()))

examples/command.roc

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import pf.Arg exposing [Arg]
66

77
# Different ways to run commands like you do in a terminal.
88

9+
# TODO add failure branch coverage to command test
10+
911
# To run this example: check the README.md in this folder
1012

1113
main! : List Arg => Result {} _
@@ -20,35 +22,30 @@ main! = |_args|
2022
|> Cmd.args(["Hi"])
2123
|> Cmd.exec_output!()?
2224

23-
Stdout.line!(
24-
"""
25-
stdout:${cmd_output.stdout_utf8}
26-
stderr:${cmd_output.stderr_utf8_lossy}
27-
"""
28-
)?
25+
Stdout.line!("${Inspect.to_str(cmd_output)}")?
2926

3027
# To run a command with environment variables.
31-
cmd_output_env =
32-
Cmd.new("env")
33-
|> Cmd.clear_envs # You probably don't need to clear all other environment variables, this is just an example.
34-
|> Cmd.env("FOO", "BAR")
35-
|> Cmd.envs([("BAZ", "DUCK"), ("XYZ", "ABC")]) # Set multiple environment variables at once with `envs`
36-
|> Cmd.args(["-v"])
37-
|> Cmd.exec_output!()?
38-
39-
Stdout.line!(
40-
"""
41-
stdout:${cmd_output_env.stdout_utf8}
42-
stderr:${cmd_output_env.stderr_utf8_lossy}
43-
"""
44-
)?
28+
Cmd.new("env")
29+
|> Cmd.clear_envs # You probably don't need to clear all other environment variables, this is just an example.
30+
|> Cmd.env("FOO", "BAR")
31+
|> Cmd.envs([("BAZ", "DUCK"), ("XYZ", "ABC")]) # Set multiple environment variables at once with `envs`
32+
|> Cmd.args(["-v"])
33+
|> Cmd.exec_cmd!()?
4534

4635
# To execute and just get the exit code (prints to your terminal).
4736
exit_code =
48-
Cmd.new("echo")
49-
|> Cmd.args(["Yo"])
37+
Cmd.new("cat")
38+
|> Cmd.args(["temp.txt"])
5039
|> Cmd.exec_exit_code!()?
5140

5241
Stdout.line!("Exit code: ${Num.to_str(exit_code)}")?
5342

43+
# To execute and capture the output in the original form as bytes (stdout, stderr, and exit code) without inheriting your terminal.
44+
cmd_output_bytes =
45+
Cmd.new("echo")
46+
|> Cmd.args(["Hi"])
47+
|> Cmd.exec_output_bytes!()?
48+
49+
Stdout.line!("${Inspect.to_str(cmd_output_bytes)}")?
50+
5451
Ok({})

platform/Cmd.roc

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module [
77
envs,
88
clear_envs,
99
exec_output!,
10+
exec_output_bytes!,
1011
exec!,
1112
exec_cmd!,
1213
exec_exit_code!,
@@ -25,12 +26,12 @@ Cmd := InternalCmd.Command
2526
## # Call echo to print "hello world"
2627
## Cmd.exec!("echo", ["hello world"])?
2728
## ```
28-
exec! : Str, List Str => Result {} [ExecFailed Str I32, FailedToGetExitCode Str IOErr]
29+
exec! : Str, List Str => Result {} [ExecFailed Str I32, FailedToGetExitCode({ command : Str, err : IOErr})]
2930
exec! = |cmd_name, arguments|
3031
exit_code =
3132
new(cmd_name)
3233
|> args(arguments)
33-
|> exec_exit_code!?
34+
|> exec_exit_code!()?
3435

3536
if exit_code == 0i32 then
3637
Ok({})
@@ -48,15 +49,15 @@ exec! = |cmd_name, arguments|
4849
## |> Cmd.env("RUST_BACKTRACE", "1")
4950
## |> Cmd.exec_cmd!()?
5051
## ```
51-
exec_cmd! : Cmd => Result {} [ExecCmdFailed Str I32, FailedToGetExitCode Str IOErr]
52+
exec_cmd! : Cmd => Result {} [ExecCmdFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode({ command : Str, err : IOErr})]
5253
exec_cmd! = |@Cmd(cmd)|
5354
exit_code =
5455
exec_exit_code!(@Cmd(cmd))?
5556

5657
if exit_code == 0i32 then
5758
Ok({})
5859
else
59-
Err(ExecCmdFailed(to_str(cmd), exit_code))
60+
Err(ExecCmdFailed({ command: to_str(cmd), exit_code }))
6061

6162
## Execute command and capture stdout, stderr and the exit code in [Output].
6263
##
@@ -68,50 +69,52 @@ exec_output! : Cmd =>
6869
Result
6970
{stdout_utf8 : Str, stderr_utf8_lossy : Str}
7071
[
71-
StdoutContainsInvalidUtf8({ cmd : Str, err: [BadUtf8 { index : U64, problem : Str.Utf8Problem }]}),
72-
NonZeroExitCode({cmd : Str, exit_code: I32, stdout_utf8_lossy: Str, stderr_utf8_lossy: Str}),
73-
FailedToGetExitCode({ cmd : Str, err : IOErr})
72+
StdoutContainsInvalidUtf8({ cmd_str : Str, err: [BadUtf8 { index : U64, problem : Str.Utf8Problem }]}),
73+
NonZeroExitCode({command : Str, exit_code: I32, stdout_utf8_lossy: Str, stderr_utf8_lossy: Str}),
74+
FailedToGetExitCode({ command : Str, err : IOErr})
7475
]
7576
exec_output! = |@Cmd(cmd)|
7677
exec_res = Host.command_exec_output!(cmd)
7778

7879
when exec_res is
79-
Ok({stdout_bytes, stderr_bytes}) ->
80-
stdout_utf8 = Str.from_utf8(stdout_bytes) ? |err| StdoutContainsInvalidUtf8({ cmd: to_str(cmd), err})
80+
Ok({stderr_bytes, stdout_bytes}) ->
81+
stdout_utf8 = Str.from_utf8(stdout_bytes) ? |err| StdoutContainsInvalidUtf8({cmd_str: to_str(cmd), err})#{ cmd: to_str(cmd), err})
8182
stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes)
8283

8384
Ok({stdout_utf8, stderr_utf8_lossy})
8485
Err(inside_res) ->
8586
when inside_res is
86-
Ok({exit_code, stdout_bytes, stderr_bytes}) ->
87-
stdout_utf8_lossy = Str.from_utf8_lossy(stdout_bytes)
88-
stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes)
87+
Ok({exit_code, stderr_bytes, stdout_bytes}) ->
88+
stdout_utf8_lossy = Str.from_utf8_lossy(stdout_bytes)
89+
stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes)
8990

90-
Err(NonZeroExitCode({cmd : to_str(cmd), exit_code, stdout_utf8_lossy, stderr_utf8_lossy}))
91+
Err(NonZeroExitCode({command : to_str(cmd), exit_code, stdout_utf8_lossy, stderr_utf8_lossy}))
9192
Err(err) ->
92-
Err(InternalIOErr.handle_err(err))
93-
|> Result.map_err(|er| FailedToGetExitCode({ cmd : to_str(cmd), err : er }))
93+
Err(FailedToGetExitCode({ command : to_str(cmd), err : InternalIOErr.handle_err(err) }))
9494

95-
# TODO exec_output_bytes
95+
## TODO add type + explain command + when to use exec_output! + example
96+
exec_output_bytes! = |@Cmd(cmd)|
97+
exec_res = Host.command_exec_output!(cmd)
98+
99+
when exec_res is
100+
Ok({stderr_bytes, stdout_bytes}) ->
101+
Ok({stdout_bytes, stderr_bytes})
102+
103+
Err(inside_res) ->
104+
when inside_res is
105+
Ok({exit_code, stderr_bytes, stdout_bytes}) ->
106+
Err(NonZeroExitCodeB({exit_code, stdout_bytes, stderr_bytes}))
107+
108+
Err(err) ->
109+
Err(FailedToGetExitCodeB(InternalIOErr.handle_err(err)))
96110

97111
## Execute command and inherit stdin, stdout and stderr from parent. Returns the exit code.
98112
## Helper function.
99-
exec_exit_code! : Cmd => Result I32 [FailedToGetExitCode Str IOErr]
113+
exec_exit_code! : Cmd => Result I32 [FailedToGetExitCode({ command : Str, err : IOErr})]
100114
exec_exit_code! = |@Cmd(cmd)|
101115
Host.command_exec_exit_code!(cmd)
102116
|> Result.map_err(InternalIOErr.handle_err)
103-
|> Result.map_err(|err| FailedToGetExitCode(to_str(cmd), err))
104-
105-
# This hits a compiler bug: Alias `6.IdentId(11)` not registered in delayed aliases! ...
106-
# ## Converts output into a utf8 string. Invalid utf8 sequences in stderr are ignored.
107-
# to_str : Output -> Result Str [BadUtf8 { index : U64, problem : Str.Utf8Problem }]
108-
# to_str = |output|
109-
# InternalCmd.output_to_str(output)
110-
111-
# ## Converts output into a utf8 string, ignoring any invalid utf8 sequences.
112-
# to_str_lossy : Output -> Str
113-
# to_str_lossy = |output|
114-
# InternalCmd.output_to_str_lossy(output)
117+
|> Result.map_err(|err| FailedToGetExitCode({ command: to_str(cmd), err }))
115118

116119
## Create a new command to execute the given program in a child process.
117120
new : Str -> Cmd

platform/InternalCmd.roc

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ module [
88
Command : {
99
program : Str,
1010
args : List Str, # [arg0, arg1, arg2, arg3, ...]
11-
envs : List Str, # [key0, value0, key1, value1, key2, value2, ...]
11+
envs : List Str, # TODO change this to list of tuples? [key0, value0, key1, value1, key2, value2, ...]
1212
clear_envs : Bool,
1313
}
1414

15+
# Do not change the order of the fields! It will lead to a segfault.
1516
OutputFromHostSuccess : {
16-
stdout_bytes : List U8,
1717
stderr_bytes : List U8,
18+
stdout_bytes : List U8,
1819
}
1920

21+
# Do not change the order of the fields! It will lead to a segfault.
2022
OutputFromHostFailure : {
21-
exit_code : I32,
22-
stdout_bytes : List U8,
2323
stderr_bytes : List U8,
24+
stdout_bytes : List U8,
25+
exit_code : I32,
2426
}
2527

2628
to_str : Command -> Str
@@ -29,12 +31,11 @@ to_str = |cmd|
2931
cmd.envs
3032
#|> List.map(|(key, value)| "${key}=${value}")
3133
|> Str.join_with(" ")
34+
|> Str.trim()
35+
|> (|trimmed_str| if Str.is_empty(trimmed_str) then "" else "envs: ${trimmed_str}")
3236
33-
clear_envs_str = if cmd.clear_envs then "true" else "false"
37+
clear_envs_str = if cmd.clear_envs then ", clear_envs: true" else ""
3438
3539
"""
36-
cmd: ${cmd.program}
37-
args: ${Str.join_with(cmd.args, " ")}
38-
envs: ${envs_str}
39-
clear_envs: ${clear_envs_str}
40+
{ cmd: ${cmd.program}, args: ${Str.join_with(cmd.args, " ")}${envs_str}${clear_envs_str} }
4041
"""

tests/cmd-test.roc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import pf.Arg exposing [Arg]
66

77
# Tests some error branches in Cmd functions.
88

9+
# TODO test all error branches in Cmd functions
10+
911
main! : List Arg => Result {} _
1012
main! = |_args|
1113

@@ -19,7 +21,7 @@ main! = |_args|
1921

2022
_ = when exec_res is
2123
Ok(_) ->
22-
Err(BlaBlaCommandShouldHaveFailed)?
24+
Err(FakeBlaBlaCommandShouldHaveFailed)?
2325
Err(err) ->
2426
Stdout.line!("Expected failure: ${Inspect.to_str(err)}")?
2527
Ok({})

0 commit comments

Comments
 (0)