artichoke/
ruby.rs

1//! Artichoke CLI entry point.
2//!
3//! Artichoke's version of the `ruby` CLI. This module is exported as the
4//! `artichoke` binary.
5
6use std::error;
7use std::ffi::{OsStr, OsString};
8use std::fs;
9use std::io;
10use std::path::{Path, PathBuf};
11
12use scolapasta_path::os_str_to_bytes;
13use scolapasta_string_escape::format_debug_escape_into;
14use termcolor::WriteColor;
15
16use crate::backend::fmt::WriteError;
17use crate::backend::state::parser::Context;
18use crate::backtrace;
19use crate::filename::INLINE_EVAL_SWITCH;
20use crate::prelude::*;
21
22pub mod cli;
23
24/// Command line arguments for Artichoke `ruby` frontend.
25#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
26pub struct Args {
27    /// print the copyright
28    copyright: bool,
29    /// one line of script. Several -e's allowed. Omit \[programfile\]
30    commands: Vec<OsString>,
31    /// file whose contents will be read into the `$fixture` global
32    fixture: Option<PathBuf>,
33    programfile: Option<PathBuf>,
34    /// Trailing positional arguments.
35    ///
36    /// Requires `programfile` to be present.
37    argv: Vec<OsString>,
38}
39
40impl Args {
41    /// Construct a new, empty `Args`.
42    #[must_use]
43    pub const fn empty() -> Self {
44        Self {
45            copyright: false,
46            commands: Vec::new(),
47            fixture: None,
48            programfile: None,
49            argv: Vec::new(),
50        }
51    }
52
53    /// Add a parsed copyright flag to this `Args`.
54    #[must_use]
55    pub fn with_copyright(mut self, copyright: bool) -> Self {
56        self.copyright = copyright;
57        self
58    }
59
60    /// Add a parsed set of `-e` inline eval commands to this `Args`.
61    #[must_use]
62    pub fn with_commands(mut self, commands: Vec<OsString>) -> Self {
63        self.commands = commands;
64        self
65    }
66
67    /// Add a parsed fixture path to this `Args`.
68    #[must_use]
69    pub fn with_fixture(mut self, fixture: Option<PathBuf>) -> Self {
70        self.fixture = fixture;
71        self
72    }
73
74    /// Add a parsed program file path to this `Args`.
75    #[must_use]
76    pub fn with_programfile(mut self, programfile: Option<PathBuf>) -> Self {
77        self.programfile = programfile;
78        self
79    }
80
81    /// Add a parsed ARGV to this `Args`.
82    #[must_use]
83    pub fn with_argv(mut self, argv: Vec<OsString>) -> Self {
84        self.argv = argv;
85        self
86    }
87}
88
89/// Result-like enum for calls to eval code on a Ruby interpreter.
90#[derive(Debug)]
91pub enum ExecutionResult {
92    /// Call to the Ruby interpreter succeeded without error.
93    Success,
94    /// Call to the Ruby interpreter raised an exception.
95    Error(Error),
96}
97
98/// Main entry point for Artichoke's version of the `ruby` CLI.
99///
100/// This entry point handles allocating, initializing, and closing an Artichoke
101/// interpreter.
102///
103/// # Errors
104///
105/// If an exception is raised on the interpreter, then an error is returned.
106pub fn run<R, W>(args: Args, input: R, error: W) -> Result<ExecutionResult, Box<dyn error::Error>>
107where
108    R: io::Read,
109    W: io::Write + WriteColor,
110{
111    let mut interp = crate::interpreter()?;
112    // All operations using the interpreter must occur behind a function
113    // boundary so we can catch all errors and ensure we call `interp.close()`.
114    //
115    // Allowing the `?` operator to be used in the containing `run` function
116    // would result in a memory leak of the interpreter and its heap.
117    let result = entrypoint(&mut interp, args, input, error);
118    // Cleanup and deallocate.
119    interp.close();
120    result
121}
122
123fn entrypoint<R, W>(
124    interp: &mut Artichoke,
125    args: Args,
126    mut input: R,
127    error: W,
128) -> Result<ExecutionResult, Box<dyn error::Error>>
129where
130    R: io::Read,
131    W: io::Write + WriteColor,
132{
133    if args.copyright {
134        interp.eval(b"puts RUBY_COPYRIGHT")?;
135        return Ok(ExecutionResult::Success);
136    }
137
138    // Inject ARGV global.
139    let mut ruby_program_argv = Vec::new();
140    for argument in &args.argv {
141        let argument = os_str_to_bytes(argument)?;
142        let mut argument = interp.try_convert_mut(argument)?;
143        argument.freeze(interp)?;
144        ruby_program_argv.push(argument);
145    }
146    let ruby_program_argv = interp.try_convert_mut(ruby_program_argv)?;
147    interp.define_global_constant("ARGV", ruby_program_argv)?;
148
149    if !args.commands.is_empty() {
150        execute_inline_eval(interp, error, args.commands, args.fixture.as_deref())
151    } else if let Some(programfile) = args.programfile.filter(|file| file != Path::new("-")) {
152        execute_program_file(interp, error, programfile.as_path(), args.fixture.as_deref())
153    } else {
154        let mut program = vec![];
155        input
156            .read_to_end(&mut program)
157            .map_err(|_| IOError::from("Could not read program from STDIN"))?;
158        if let Err(exc) = interp.eval(program.as_slice()) {
159            backtrace::format_cli_trace_into(error, interp, &exc)?;
160            return Ok(ExecutionResult::Error(exc));
161        }
162        Ok(ExecutionResult::Success)
163    }
164}
165
166fn execute_inline_eval<W>(
167    interp: &mut Artichoke,
168    error: W,
169    commands: Vec<OsString>,
170    fixture: Option<&Path>,
171) -> Result<ExecutionResult, Box<dyn error::Error>>
172where
173    W: io::Write + WriteColor,
174{
175    interp.pop_context()?;
176    // SAFETY: `INLINE_EVAL_SWITCH` has no NUL bytes (asserted by tests).
177    let context = unsafe { Context::new_unchecked(INLINE_EVAL_SWITCH) };
178    interp.push_context(context)?;
179    if let Some(fixture) = fixture {
180        setup_fixture_hack(interp, fixture)?;
181    }
182    let mut commands = commands.into_iter();
183    let mut buf = if let Some(command) = commands.next() {
184        command
185    } else {
186        return Ok(ExecutionResult::Success);
187    };
188    for command in commands {
189        buf.push("\n");
190        buf.push(command);
191    }
192    if let Err(exc) = interp.eval_os_str(&buf) {
193        backtrace::format_cli_trace_into(error, interp, &exc)?;
194        // short circuit, but don't return an error since we already printed it
195        return Ok(ExecutionResult::Error(exc));
196    }
197    Ok(ExecutionResult::Success)
198}
199
200fn execute_program_file<W>(
201    interp: &mut Artichoke,
202    error: W,
203    programfile: &Path,
204    fixture: Option<&Path>,
205) -> Result<ExecutionResult, Box<dyn error::Error>>
206where
207    W: io::Write + WriteColor,
208{
209    if let Some(fixture) = fixture {
210        setup_fixture_hack(interp, fixture)?;
211    }
212    if let Err(exc) = interp.eval_file(programfile) {
213        backtrace::format_cli_trace_into(error, interp, &exc)?;
214        return Ok(ExecutionResult::Error(exc));
215    }
216    Ok(ExecutionResult::Success)
217}
218
219fn load_error<P: AsRef<OsStr>>(file: P, message: &str) -> Result<String, Error> {
220    let mut buf = String::from(message);
221    buf.push_str(" -- ");
222    let path = os_str_to_bytes(file.as_ref())?;
223    format_debug_escape_into(&mut buf, path).map_err(WriteError::from)?;
224    Ok(buf)
225}
226
227// This function exists to provide a workaround for Artichoke not being able to
228// read from the local file system.
229//
230// By passing the `--with-fixture PATH` argument, this function loads the file
231// at `PATH` into memory and stores it in the interpreter bound to the
232// `$fixture` global.
233#[inline]
234fn setup_fixture_hack<P: AsRef<Path>>(interp: &mut Artichoke, fixture: P) -> Result<(), Error> {
235    let data = if let Ok(data) = fs::read(fixture.as_ref()) {
236        data
237    } else {
238        return Err(LoadError::from(load_error(fixture.as_ref(), "No such file or directory")?).into());
239    };
240    let value = interp.try_convert_mut(data)?;
241    interp.set_global_variable(&b"$fixture"[..], &value)?;
242    Ok(())
243}
244
245#[cfg(test)]
246mod tests {
247    use std::ffi::OsString;
248    use std::path::PathBuf;
249
250    use termcolor::Ansi;
251
252    use super::{Args, ExecutionResult, run};
253
254    #[test]
255    fn run_with_copyright() {
256        let args = Args::empty().with_copyright(true);
257        let input = Vec::<u8>::new();
258        let mut err = Ansi::new(Vec::new());
259        assert!(matches!(run(args, &input[..], &mut err), Ok(ExecutionResult::Success)));
260    }
261
262    #[test]
263    fn run_with_programfile_from_stdin() {
264        let args = Args::empty().with_programfile(Some(PathBuf::from("-")));
265        let input = b"2 + 7";
266        let mut err = Ansi::new(Vec::new());
267        assert!(matches!(run(args, &input[..], &mut err), Ok(ExecutionResult::Success)));
268    }
269
270    #[test]
271    fn run_with_programfile_from_stdin_raise_exception() {
272        let args = Args::empty().with_programfile(Some(PathBuf::from("-")));
273        let input = b"raise ArgumentError";
274        let mut err = Ansi::new(Vec::new());
275        assert!(matches!(
276            run(args, &input[..], &mut err),
277            Ok(ExecutionResult::Error(..))
278        ));
279    }
280
281    #[test]
282    fn run_with_stdin() {
283        let args = Args::empty();
284        let input = b"2 + 7";
285        let mut err = Ansi::new(Vec::new());
286        assert!(matches!(run(args, &input[..], &mut err), Ok(ExecutionResult::Success)));
287    }
288
289    #[test]
290    fn run_with_stdin_raise_exception() {
291        let args = Args::empty();
292        let input = b"raise ArgumentError";
293        let mut err = Ansi::new(Vec::new());
294        assert!(matches!(
295            run(args, &input[..], &mut err),
296            Ok(ExecutionResult::Error(..))
297        ));
298    }
299
300    #[test]
301    fn run_with_inline_eval() {
302        let args = Args::empty().with_commands(vec![OsString::from("2 + 7")]);
303        let input = Vec::<u8>::new();
304        let mut err = Ansi::new(Vec::new());
305        assert!(matches!(
306            run(args, input.as_slice(), &mut err),
307            Ok(ExecutionResult::Success)
308        ));
309    }
310
311    #[test]
312    fn run_with_inline_eval_raise_exception() {
313        let args = Args::empty().with_commands(vec![OsString::from("raise ArgumentError")]);
314        let input = Vec::<u8>::new();
315        let mut err = Ansi::new(Vec::new());
316        assert!(matches!(
317            run(args, &input[..], &mut err),
318            Ok(ExecutionResult::Error(..))
319        ));
320    }
321}