Skip to content

Commit f3e71b0

Browse files
authored
Allow piping repl output to shell commands (#8878)
Add shell support to repl-util in a manner similar to mdb. Thanks to @rmustacc for the idea. This allows doing things like the following example from tqdb. Find all commit operations in the event log and report their event number: ``` tqdb〉events all ! grep Commit | cut -d " " -f1 30 31 32 33 34 35 43 44 130 131 132 1035 1036 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1059 1060 1061 1062 1063 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 tqdb〉 ``` You can also do things like page output through less: ``` tqdb〉events all ! less ```
1 parent 208a2aa commit f3e71b0

File tree

6 files changed

+53
-3
lines changed

6 files changed

+53
-3
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ensure that we can pipe command output to a shell command
2+
show ! grep DNS

dev-tools/reconfigurator-cli/tests/output/cmds-pipe-to-grep-stderr

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using provided RNG seed: reconfigurator-cli-test
2+
> # Ensure that we can pipe command output to a shell command
3+
> show ! grep DNS
4+
configured external DNS zone name: oxide.example
5+
internal DNS generations:
6+
external DNS generations:
7+

dev-tools/repl-utils/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ camino.workspace = true
1313
clap.workspace = true
1414
reedline.workspace = true
1515
omicron-workspace-hack.workspace = true
16+
subprocess.workspace = true

dev-tools/repl-utils/src/lib.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use reedline::Signal;
1414
use std::fs::File;
1515
use std::io::BufRead;
1616
use std::io::BufReader;
17+
use std::io::Write;
18+
use subprocess::Exec;
1719

1820
/// Runs the same kind of REPL as `run_repl_on_stdin()`, but reads commands from
1921
/// a file
@@ -148,12 +150,21 @@ fn process_entry<C: Parser>(
148150
return LoopResult::Continue;
149151
}
150152

151-
// Parse the line of input as a REPL command.
153+
// Split on the first `!` character if it exists. Use the first
154+
// element of the iterator as the REPL command to parse via clap. Use the
155+
// second element, if it exists, as the shell command to pipe the output of
156+
// the REPL command into.
157+
let mut split = entry.splitn(2, '!');
158+
159+
// Parse the line of input before any `!` as a REPL command.
152160
//
153161
// Using `split_whitespace()` like this is going to be a problem if we ever
154162
// want to support arguments with whitespace in them (using quotes). But
155163
// it's good enough for now.
156-
let parts = entry.split_whitespace();
164+
//
165+
// SAFETY: There is always at least one element in the iterator.
166+
let parts = split.next().expect("element exists").split_whitespace();
167+
157168
let parsed_command = C::command()
158169
.multicall(true)
159170
.try_get_matches_from(parts)
@@ -176,7 +187,35 @@ fn process_entry<C: Parser>(
176187

177188
match run_one(command) {
178189
Err(error) => println!("error: {:#}", error),
179-
Ok(Some(s)) => println!("{}", s),
190+
Ok(Some(repl_cmd_output)) => {
191+
if let Some(shell_cmd) = split.next() {
192+
let mut child_stdin = Exec::shell(shell_cmd)
193+
.stream_stdin()
194+
.expect("stdin opened");
195+
196+
// Using `write_all` doesn't play nicely with the shell group
197+
// leader, likely due to blocking/signal behavior. We therefore
198+
// manually loop over calls to `write`.
199+
let mut written_bytes = 0;
200+
let to_write = repl_cmd_output.len();
201+
while written_bytes < to_write {
202+
match child_stdin
203+
.write(&repl_cmd_output.as_bytes()[written_bytes..])
204+
{
205+
Ok(0) => break,
206+
Ok(n) => written_bytes += n,
207+
Err(_) => {
208+
// Broken pipe is a normal condition reflecting
209+
// that the child process exited early (e.g., as
210+
// `head(1)` does).
211+
break;
212+
}
213+
}
214+
}
215+
} else {
216+
println!("{repl_cmd_output}");
217+
}
218+
}
180219
Ok(None) => (),
181220
}
182221

0 commit comments

Comments
 (0)