rust_bf/commands/
read.rs

1use clap::Args;
2use std::{fs, thread};
3use std::io::{self, Write};
4use std::sync::{mpsc, Arc};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::time::Duration;
7use crate::{BrainfuckReader, BrainfuckReaderError};
8use crate::cli_util::print_reader_error;
9use crate::reader::StepControl;
10
11#[derive(Args, Debug)]
12#[command(disable_help_flag = true)]
13pub struct ReadArgs {
14    /// Print a step-by-step table of operations instead of executing
15    #[arg(short = 'd', long = "debug")]
16    pub debug: bool,
17
18    /// Read Brainfuck code from PATH instead of positional "<code>"
19    #[arg(short = 'f', long = "file")]
20    pub file: Option<String>,
21
22    /// Concatenated Brainfuck code parts
23    #[arg(value_name = "code", trailing_var_arg = true)]
24    pub code: Vec<String>,
25
26    /// Wall-clock timeout in milliseconds (fallback BF_TIMEOUT_MS; default 2_000)
27    #[arg(long = "timeout", value_name = "MS")]
28    pub timeout_ms: Option<u64>,
29
30    /// Maximum interpreter steps before abort (fallback BF_MAX_STEPS; default unlimited)
31    #[arg(long = "max-steps", value_name = "N")]
32    pub max_steps: Option<u64>,
33
34    /// Show this help
35    #[arg(short = 'h', long = "help", action = clap::ArgAction::SetTrue)]
36    pub help: bool,
37}
38
39pub fn run(program: &str, args: ReadArgs) -> i32 {
40    if args.help {
41        usage_and_exit(program, 0);
42    }
43
44    let ReadArgs {
45        debug,
46        file,
47        code,
48        timeout_ms,
49        max_steps,
50        ..
51    } = args;
52
53    if file.is_none() && code.is_empty() {
54        usage_and_exit(program, 2);
55    }
56
57    if file.is_some() && !code.is_empty() {
58        eprintln!("{program}: cannot use positional code together with --file");
59        usage_and_exit(program, 2);
60    }
61
62    let code_str = if let Some(path) = file {
63        match fs::read_to_string(&path) {
64            Ok(s) => s,
65            Err(e) => {
66                eprintln!("{program}: failed to read code file as UTF-8: {e}");
67                let _ = io::stderr().flush();
68                return 1;
69            }
70        }
71    } else {
72        code.join("")
73    };
74
75    // Resolve limits: flags -> env -> defaults
76    let timeout_ms = timeout_ms
77        .or_else(|| std::env::var("BF_TIMEOUT_MS").ok().and_then(|s| s.parse::<u64>().ok()))
78        .unwrap_or(2_000);
79    let max_steps = max_steps
80        .or_else(|| std::env::var("BF_MAX_STEPS").ok().and_then(|s| s.parse::<u64>().ok()));
81
82    // Execute on a worker thread with cooperative cancellation
83    let cancel = Arc::new(AtomicBool::new(false));
84    let (tx, rx) = mpsc::channel::<Result<(), BrainfuckReaderError>>();
85    let program_owned = code_str.clone();
86    let cancel_clone = cancel.clone();
87
88    thread::spawn(move || {
89        let max_steps_opt: Option<usize> = max_steps.map(|m| m as usize);
90        let mut bf = BrainfuckReader::new(program_owned);
91        let ctrl = StepControl::new(max_steps_opt, cancel_clone);
92        let res = if debug {
93            bf.run_debug_with_control(ctrl)
94        } else {
95            bf.run_with_control(ctrl)
96        };
97        let _ = tx.send(res);
98    });
99
100    let timeout = Duration::from_millis(timeout_ms);
101    let exit_code = match rx.recv_timeout(timeout) {
102        Ok(Ok(())) => 0,
103        Ok(Err(BrainfuckReaderError::StepLimitExceeded { limit })) => {
104            eprintln!("Execution aborted: step limit exceeded ({limit})");
105            let _ = io::stderr().flush();
106            1
107        }
108        Ok(Err(BrainfuckReaderError::Canceled)) => {
109            eprintln!("Execution aborted: wall-clock timeout exceeded ({timeout_ms} ms)");
110            let _ = io::stderr().flush();
111            1
112        }
113        Ok(Err(other)) => {
114            print_reader_error(Some(program), &code_str, &other);
115            let _ = io::stderr().flush();
116            1
117        }
118        Err(mpsc::RecvTimeoutError::Timeout) => {
119            cancel.store(true, Ordering::Relaxed);
120            eprintln!("Execution aborted: wall-clock timeout exceeded ({timeout_ms} ms)");
121            let _ = io::stderr().flush();
122            1
123        }
124        Err(mpsc::RecvTimeoutError::Disconnected) => 1,
125    };
126
127    println!();
128    let _ = io::stdout().flush();
129    exit_code
130}
131
132fn usage_and_exit(program: &str, code: i32) -> ! {
133    eprintln!(
134        r#"Usage:
135  {0} read [--debug|-d] "<code>"
136  {0} read [--debug|-d] --file <PATH>
137
138Options:
139  --file,  -f <PATH>  Read Brainfuck code from PATH instead of positional "<code>"
140  --debug, -d   Print a step-by-step table of operations instead of executing
141  --help,  -h   Show this help
142
143Notes:
144- Input (`,`) reads a single byte from stdin; on EOF the current cell is set to 0.
145- Any characters outside of Brainfuck's ><+-.,[] will result in an error.
146
147Examples:
148- Load Brainfuck code from a file:
149    {0} read --file ./program.bf
150- Read bytes from a file as stdin (`,` will consume file input):
151    {0} read ",[.,]" < input.txt
152"#,
153        program
154    );
155    let _ = io::stderr().flush();
156    std::process::exit(code);
157}
158