Skip to main content

rustpython/
shell.rs

1mod helper;
2
3use rustpython_compiler::{
4    CompileError, ParseError, parser::InterpolatedStringErrorType, parser::LexicalErrorType,
5    parser::ParseErrorType,
6};
7use rustpython_vm::{
8    AsObject, PyResult, VirtualMachine,
9    builtins::PyBaseExceptionRef,
10    compiler::{self},
11    readline::{Readline, ReadlineResult},
12    scope::Scope,
13};
14
15enum ShellExecResult {
16    Ok,
17    PyErr(PyBaseExceptionRef),
18    ContinueBlock,
19    ContinueLine,
20}
21
22fn shell_exec(
23    vm: &VirtualMachine,
24    source: &str,
25    scope: Scope,
26    empty_line_given: bool,
27    continuing_block: bool,
28) -> ShellExecResult {
29    // compiling expects only UNIX style line endings, and will replace windows line endings
30    // internally. Since we might need to analyze the source to determine if an error could be
31    // resolved by future input, we need the location from the error to match the source code that
32    // was actually compiled.
33    #[cfg(windows)]
34    let source = &source.replace("\r\n", "\n");
35    match vm.compile(source, compiler::Mode::Single, "<stdin>".to_owned()) {
36        Ok(code) => {
37            if empty_line_given || !continuing_block {
38                // We want to execute the full code
39                match vm.run_code_obj(code, scope) {
40                    Ok(_val) => ShellExecResult::Ok,
41                    Err(err) => ShellExecResult::PyErr(err),
42                }
43            } else {
44                // We can just return an ok result
45                ShellExecResult::Ok
46            }
47        }
48        Err(CompileError::Parse(ParseError {
49            error: ParseErrorType::Lexical(LexicalErrorType::Eof),
50            ..
51        })) => ShellExecResult::ContinueLine,
52        Err(CompileError::Parse(ParseError {
53            error:
54                ParseErrorType::Lexical(LexicalErrorType::FStringError(
55                    InterpolatedStringErrorType::UnterminatedTripleQuotedString,
56                )),
57            ..
58        })) => ShellExecResult::ContinueLine,
59        Err(err) => {
60            // Check if the error is from an unclosed triple quoted string (which should always
61            // continue)
62            if let CompileError::Parse(ParseError {
63                error: ParseErrorType::Lexical(LexicalErrorType::UnclosedStringError),
64                raw_location,
65                ..
66            }) = err
67            {
68                let loc = raw_location.start().to_usize();
69                let mut iter = source.chars();
70                if let Some(quote) = iter.nth(loc)
71                    && iter.next() == Some(quote)
72                    && iter.next() == Some(quote)
73                {
74                    return ShellExecResult::ContinueLine;
75                }
76            };
77
78            // bad_error == true if we are handling an error that should be thrown even if we are continuing
79            // if its an indentation error, set to true if we are continuing and the error is on column 0,
80            // since indentations errors on columns other than 0 should be ignored.
81            // if its an unrecognized token for dedent, set to false
82
83            let bad_error = match err {
84                CompileError::Parse(ref p) => {
85                    match &p.error {
86                        ParseErrorType::Lexical(LexicalErrorType::IndentationError) => {
87                            continuing_block
88                        } // && p.location.is_some()
89                        ParseErrorType::OtherError(msg) => {
90                            !msg.starts_with("Expected an indented block")
91                        }
92                        _ => true, // !matches!(p, ParseErrorType::UnrecognizedToken(Tok::Dedent, _))
93                    }
94                }
95                _ => true, // It is a bad error for everything else
96            };
97
98            // If we are handling an error on an empty line or an error worthy of throwing
99            if empty_line_given || bad_error {
100                ShellExecResult::PyErr(vm.new_syntax_error(&err, Some(source)))
101            } else {
102                ShellExecResult::ContinueBlock
103            }
104        }
105    }
106}
107
108/// Enter a repl loop
109pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
110    let mut repl = Readline::new(helper::ShellHelper::new(vm, scope.globals.clone()));
111    let mut full_input = String::new();
112
113    // Retrieve a `history_path_str` dependent on the OS
114    let repl_history_path = match dirs::config_dir() {
115        Some(mut path) => {
116            path.push("rustpython");
117            path.push("repl_history.txt");
118            path
119        }
120        None => ".repl_history.txt".into(),
121    };
122
123    if repl.load_history(&repl_history_path).is_err() {
124        println!("No previous history.");
125    }
126
127    // We might either be waiting to know if a block is complete, or waiting to know if a multiline
128    // statement is complete. In the former case, we need to ensure that we read one extra new line
129    // to know that the block is complete. In the latter, we can execute as soon as the statement is
130    // valid.
131    let mut continuing_block = false;
132    let mut continuing_line = false;
133
134    loop {
135        let prompt_name = if continuing_block || continuing_line {
136            "ps2"
137        } else {
138            "ps1"
139        };
140        let prompt = vm
141            .sys_module
142            .get_attr(prompt_name, vm)
143            .and_then(|prompt| prompt.str(vm));
144        let prompt = match prompt {
145            Ok(ref s) => s.expect_str(),
146            Err(_) => "",
147        };
148
149        continuing_line = false;
150        let result = match repl.readline(prompt) {
151            ReadlineResult::Line(line) => {
152                #[cfg(debug_assertions)]
153                debug!("You entered {line:?}");
154
155                repl.add_history_entry(line.trim_end()).unwrap();
156
157                let empty_line_given = line.is_empty();
158
159                if full_input.is_empty() {
160                    full_input = line;
161                } else {
162                    full_input.push_str(&line);
163                }
164                full_input.push('\n');
165
166                match shell_exec(
167                    vm,
168                    &full_input,
169                    scope.clone(),
170                    empty_line_given,
171                    continuing_block,
172                ) {
173                    ShellExecResult::Ok => {
174                        if continuing_block {
175                            if empty_line_given {
176                                // We should exit continue mode since the block successfully executed
177                                continuing_block = false;
178                                full_input.clear();
179                            }
180                        } else {
181                            // We aren't in continue mode so proceed normally
182                            full_input.clear();
183                        }
184                        Ok(())
185                    }
186                    // Continue, but don't change the mode
187                    ShellExecResult::ContinueLine => {
188                        continuing_line = true;
189                        Ok(())
190                    }
191                    ShellExecResult::ContinueBlock => {
192                        continuing_block = true;
193                        Ok(())
194                    }
195                    ShellExecResult::PyErr(err) => {
196                        continuing_block = false;
197                        full_input.clear();
198                        Err(err)
199                    }
200                }
201            }
202            ReadlineResult::Interrupt => {
203                continuing_block = false;
204                full_input.clear();
205                let keyboard_interrupt =
206                    vm.new_exception_empty(vm.ctx.exceptions.keyboard_interrupt.to_owned());
207                Err(keyboard_interrupt)
208            }
209            ReadlineResult::Eof => {
210                break;
211            }
212            #[cfg(unix)]
213            ReadlineResult::OsError(num) => {
214                let os_error = vm.new_exception_msg(
215                    vm.ctx.exceptions.os_error.to_owned(),
216                    format!("{num:?}").into(),
217                );
218                vm.print_exception(os_error);
219                break;
220            }
221            ReadlineResult::Other(err) => {
222                eprintln!("Readline error: {err:?}");
223                break;
224            }
225            ReadlineResult::Io(err) => {
226                eprintln!("IO error: {err:?}");
227                break;
228            }
229        };
230
231        if let Err(exc) = result {
232            if exc.fast_isinstance(vm.ctx.exceptions.system_exit) {
233                repl.save_history(&repl_history_path).unwrap();
234                return Err(exc);
235            }
236            vm.print_exception(exc);
237        }
238    }
239    repl.save_history(&repl_history_path).unwrap();
240
241    Ok(())
242}
Morty Proxy This is a proxified and sanitized view of the page, visit original site.