use std::path::PathBuf;
use std::process::ExitCode;
#[cfg(unix)]
use std::sync::atomic::{AtomicI32, Ordering};
use std::time::Duration;
use clap::Parser;
use runandlog::session::Session;
use runandlog::tui;
use runandlog_core::{Canceller, ExecOptions};
fn signal_exit_code(signal: i32) -> u8 {
(128 + signal) as u8
}
#[derive(Parser, Debug)]
#[command(name = "runandlog", version, about, long_about = None)]
struct Args {
file: PathBuf,
#[arg(short, long)]
gui: bool,
#[arg(short, long)]
list: bool,
#[arg(short, long, value_name = "N")]
run: Vec<usize>,
#[arg(short = 'a', long)]
run_all: bool,
#[arg(long, value_name = "N", default_value_t = 50)]
max_inline_lines: usize,
#[arg(long, value_name = "PATH")]
shell: Option<PathBuf>,
#[arg(long, value_name = "DIR")]
cwd: Option<PathBuf>,
#[arg(long, value_name = "SECONDS")]
timeout: Option<u64>,
}
fn main() -> ExitCode {
let args = Args::parse();
match dispatch(args) {
Ok(code) => code,
Err(error) => {
eprintln!("runandlog: {error}");
if interrupt_requested() {
ExitCode::from(interrupt_exit_code())
} else {
ExitCode::FAILURE
}
}
}
}
fn dispatch(args: Args) -> std::io::Result<ExitCode> {
let mut session = Session::load(&args.file, exec_options(&args), args.max_inline_lines)?;
if args.gui {
return open_gui(session).map(|()| ExitCode::SUCCESS);
}
if args.list {
print_list(&session);
return Ok(ExitCode::SUCCESS);
}
if !args.run_all && args.run.is_empty() {
tui::run(session)?;
return Ok(ExitCode::SUCCESS);
}
let targets = targets(&args, &session)?;
let canceller = Canceller::new();
catch_interrupts(&canceller);
let mut failed = false;
for index in targets {
if interrupt_requested() {
break;
}
let cell = &session.doc().cells[index];
println!("[{}] {}", index + 1, first_line(&cell.command));
let outcome = session.run_cell_cancellable(index, &canceller)?;
print!("{}", outcome.output);
println!("--- {}", outcome.status_text());
failed |= !outcome.is_success();
if interrupt_requested() {
break;
}
}
if interrupt_requested() {
eprintln!("runandlog: interrupted");
return Ok(ExitCode::from(interrupt_exit_code()));
}
Ok(if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
})
}
#[cfg(unix)]
fn catch_interrupts(canceller: &Canceller) {
extern "C" fn on_signal(signal: libc::c_int) {
if INTERRUPTED_BY.swap(signal, Ordering::SeqCst) != 0 {
unsafe { libc::_exit(signal_exit_code(signal) as libc::c_int) };
}
}
unsafe {
let handler = on_signal as *const () as libc::sighandler_t;
libc::signal(libc::SIGINT, handler);
libc::signal(libc::SIGTERM, handler);
}
let canceller = canceller.clone();
std::thread::spawn(move || {
while INTERRUPTED_BY.load(Ordering::SeqCst) == 0 {
std::thread::sleep(Duration::from_millis(50));
}
canceller.cancel();
});
}
#[cfg(unix)]
static INTERRUPTED_BY: AtomicI32 = AtomicI32::new(0);
#[cfg(unix)]
fn interrupt_requested() -> bool {
INTERRUPTED_BY.load(Ordering::SeqCst) != 0
}
#[cfg(unix)]
fn interrupt_exit_code() -> u8 {
signal_exit_code(INTERRUPTED_BY.load(Ordering::SeqCst))
}
#[cfg(not(unix))]
fn interrupt_requested() -> bool {
false
}
#[cfg(not(unix))]
fn interrupt_exit_code() -> u8 {
signal_exit_code(2)
}
#[cfg(not(unix))]
fn catch_interrupts(_canceller: &Canceller) {}
#[cfg(feature = "gui")]
fn open_gui(session: Session) -> std::io::Result<()> {
runandlog::gui::run(session)
}
#[cfg(not(feature = "gui"))]
fn open_gui(_session: Session) -> std::io::Result<()> {
Err(std::io::Error::other(
"this build has no GUI; rebuild with the \"gui\" feature to use --gui",
))
}
fn exec_options(args: &Args) -> ExecOptions {
let cwd = args.cwd.clone().unwrap_or_else(|| {
let file = args
.file
.canonicalize()
.unwrap_or_else(|_| args.file.clone());
file.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
});
let mut options = ExecOptions::new(cwd);
if let Some(shell) = &args.shell {
options.shell = shell.clone();
}
options.timeout = args.timeout.map(Duration::from_secs);
options
}
fn targets(args: &Args, session: &Session) -> std::io::Result<Vec<usize>> {
if args.run_all {
return Ok((0..session.len()).collect());
}
let mut targets = Vec::new();
for number in &args.run {
if *number == 0 || *number > session.len() {
return Err(std::io::Error::other(format!(
"no such cell: {number} (the file has {} cells)",
session.len()
)));
}
targets.push(number - 1);
}
Ok(targets)
}
fn print_list(session: &Session) {
if session.is_empty() {
println!("no runnable cells in {}", session.path().display());
return;
}
for cell in &session.doc().cells {
let out = match &cell.out_file {
Some(path) => format!(" -> {path}"),
None => String::new(),
};
println!(
"[{}] {}{out}",
cell.display_number(),
first_line(&cell.command)
);
}
}
fn first_line(command: &str) -> String {
let mut lines = command.lines().filter(|line| !line.trim().is_empty());
let first = lines.next().unwrap_or("").trim().to_string();
if lines.next().is_some() {
return format!("{first} ...");
}
first
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_exit_code_names_the_signal_that_stopped_the_run() {
assert_eq!(signal_exit_code(2), 130);
assert_eq!(signal_exit_code(15), 143);
}
#[test]
fn shows_ellipsis_for_multi_line_commands() {
assert_eq!(first_line("ls /opt\nls /tmp\n"), "ls /opt ...");
assert_eq!(first_line("date\n"), "date");
assert_eq!(first_line("\n\n"), "");
}
}