1use 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#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
26pub struct Args {
27 copyright: bool,
29 commands: Vec<OsString>,
31 fixture: Option<PathBuf>,
33 programfile: Option<PathBuf>,
34 argv: Vec<OsString>,
38}
39
40impl Args {
41 #[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 #[must_use]
55 pub fn with_copyright(mut self, copyright: bool) -> Self {
56 self.copyright = copyright;
57 self
58 }
59
60 #[must_use]
62 pub fn with_commands(mut self, commands: Vec<OsString>) -> Self {
63 self.commands = commands;
64 self
65 }
66
67 #[must_use]
69 pub fn with_fixture(mut self, fixture: Option<PathBuf>) -> Self {
70 self.fixture = fixture;
71 self
72 }
73
74 #[must_use]
76 pub fn with_programfile(mut self, programfile: Option<PathBuf>) -> Self {
77 self.programfile = programfile;
78 self
79 }
80
81 #[must_use]
83 pub fn with_argv(mut self, argv: Vec<OsString>) -> Self {
84 self.argv = argv;
85 self
86 }
87}
88
89#[derive(Debug)]
91pub enum ExecutionResult {
92 Success,
94 Error(Error),
96}
97
98pub 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 let result = entrypoint(&mut interp, args, input, error);
118 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 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 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 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#[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}