use super::{
execute_command, AsciiCast, ErrorType, ExecutionContext, InstructionTrait, ParseContext,
};
use std::io::Write;
#[derive(Debug, PartialEq, Eq)]
pub struct CommandInstruction {
command: String,
start: bool,
continuation: bool,
}
impl InstructionTrait for CommandInstruction {
fn parse(s: &str, context: &mut ParseContext) -> Result<Self, ErrorType> {
context.front_matter_state.end()?;
let s = s.trim();
let start = match context.start {
'$' => true,
'>' => false,
_ => return Err(ErrorType::UnknownInstruction),
};
let continuation = s.ends_with('\\');
if start && context.expect_continuation {
return Err(ErrorType::ExpectedContinuation);
} else if !start && !context.expect_continuation {
return Err(ErrorType::UnexpectedContinuation);
}
context.expect_continuation = continuation;
let command = if continuation {
s[..s.len() - 1].trim_end()
} else {
s
};
Ok(Self {
command: command.to_string(),
start,
continuation,
})
}
fn execute(
&self,
context: &mut ExecutionContext,
cast: &mut AsciiCast<impl std::io::Write>,
) -> Result<(), ErrorType> {
let temp = context.temporary.get(!self.continuation);
let config = context.persistent.combine(temp);
if config.hidden {
if context.execute {
let expect = config.expect;
let reader = execute_command(context, &self.command)?;
let result = || -> Result<(), ErrorType> {
for chunk in reader {
chunk?;
}
Ok(())
}();
handle_error(result, expect)?;
}
return Ok(());
}
let prompt = if self.start {
&config.prompt
} else {
&config.secondary_prompt
};
let interval = config.interval;
cast.output(context.elapsed, prompt)?;
context.preview(prompt);
context.elapsed += config.start_lag;
if interval > 0 {
for character in self.command.chars() {
context.elapsed += interval;
cast.output(context.elapsed, character.encode_utf8(&mut [0u8; 4]))?;
}
} else {
cast.output(context.elapsed, &self.command)?;
}
context.preview(&self.command);
context.elapsed += interval;
if self.continuation {
cast.output(context.elapsed, &config.line_continuation)?;
context.preview(&config.line_continuation);
context.elapsed += interval;
context.elapsed += config.end_lag;
cast.output(context.elapsed, "\r\n")?;
context.preview("\r\n");
context.command.push_str(&self.command);
context.command.push(' ');
} else {
context.elapsed += config.end_lag;
cast.output(context.elapsed, "\r\n")?;
context.preview("\r\n");
let mut command = std::mem::take(&mut context.command);
command.push_str(&self.command);
if context.execute {
let expect = config.expect;
let mut prev = std::time::Instant::now();
let reader = execute_command(context, &command)?;
let mut lock = std::io::stdout().lock();
let result = || -> Result<(), ErrorType> {
for chunk in reader {
let chunk = chunk?;
let now = std::time::Instant::now();
context.elapsed += now.duration_since(prev).as_micros();
prev = now;
cast.output(context.elapsed, &chunk)?;
if context.preview {
print!("{chunk}");
lock.flush()?;
}
}
Ok(())
}();
handle_error(result, expect)?;
}
}
Ok(())
}
}
fn handle_error(result: Result<(), ErrorType>, expect: Option<bool>) -> Result<(), ErrorType> {
if let Err(e) = &result {
if !matches!(e, ErrorType::Subprocess(_)) {
return result;
}
}
let Some(expect) = expect else {
return Ok(());
};
match result {
Ok(()) => {
if expect {
Ok(())
} else {
Err(ErrorType::Subprocess(
"command expected failure, but succeeded".to_string(),
))
}
}
Err(e) => {
if expect {
Err(e)
} else {
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
fn io_error() -> Result<(), ErrorType> {
Err(ErrorType::Io(io::Error::new(io::ErrorKind::Other, "error")))
}
#[test]
fn command_instruction() {
let instructions = [
(("hello", true), ("hello", true, false)),
(("world", false), ("world", false, false)),
((" hello \\", true), ("hello", true, true)),
(("world\\", false), ("world", false, true)),
];
for ((input, start_input), (command, start_output, continuation)) in &instructions {
assert_eq!(start_input, start_output);
let mut context = ParseContext::new();
context.start = if *start_input { '$' } else { '>' };
context.expect_continuation = !start_input;
let instruction = CommandInstruction::parse(input, &mut context).unwrap();
assert_eq!(instruction.command, *command);
assert_eq!(instruction.start, *start_output);
assert_eq!(instruction.continuation, *continuation);
}
}
#[test]
fn error_handling() {
let should_succeed: [(Result<(), ErrorType>, Option<_>); 4] = [
(Ok(()), None),
(Err(ErrorType::Subprocess("error".to_string())), None),
(Ok(()), Some(true)),
(Err(ErrorType::Subprocess("error".to_string())), Some(false)),
];
for (result, expect) in should_succeed {
let desc = format!("handle_error({result:?}, {expect:?})");
assert!(handle_error(result, expect).is_ok(), "{desc}");
}
let should_fail: [(Result<(), ErrorType>, Option<_>); 5] = [
(Ok(()), Some(false)),
(Err(ErrorType::Subprocess("error".to_string())), Some(true)),
(io_error(), None),
(io_error(), Some(true)),
(io_error(), Some(false)),
];
for (result, expect) in should_fail {
let desc = format!("handle_error({result:?}, {expect:?})");
assert!(handle_error(result, expect).is_err(), "{desc}");
}
}
}