1use std::path::Path;
2use std::sync::{atomic::AtomicBool, Arc};
3
4use nu_cli::{add_cli_context, gather_parent_env_vars};
5use nu_cmd_lang::create_default_context;
6use nu_command::add_shell_command_context;
7use nu_engine::eval_block_with_early_return;
8use nu_parser::parse;
9use nu_plugin_engine::{GetPlugin, PluginDeclaration};
10use nu_protocol::engine::Command;
11use nu_protocol::format_cli_error;
12use nu_protocol::{
13 debugger::WithoutDebug,
14 engine::{Closure, EngineState, Redirection, Stack, StateWorkingSet},
15 OutDest, PipelineData, PluginIdentity, RegisteredPlugin, ShellError, Signals, Span, Value,
16};
17
18use crate::commands::{
19 HighlightCommand, HighlightLangCommand, HighlightThemeCommand, MjCommand, MjCompileCommand,
20 MjRenderCommand, ResponseStartCommand, ReverseProxyCommand, StaticCommand, ToSse,
21};
22use crate::logging::log_error;
23use crate::stdlib::load_http_nu_stdlib;
24use crate::Error;
25
26#[derive(Clone)]
27pub struct Engine {
28 pub state: EngineState,
29 pub closure: Option<Closure>,
30}
31
32impl Engine {
33 pub fn new() -> Result<Self, Error> {
34 let mut engine_state = create_default_context();
35
36 engine_state = add_shell_command_context(engine_state);
37 engine_state = add_cli_context(engine_state);
38 engine_state = nu_cmd_extra::extra::add_extra_command_context(engine_state);
39
40 load_http_nu_stdlib(&mut engine_state)?;
41
42 let init_cwd = std::env::current_dir()?;
43 gather_parent_env_vars(&mut engine_state, init_cwd.as_ref());
44
45 Ok(Self {
46 state: engine_state,
47 closure: None,
48 })
49 }
50
51 pub fn add_commands(&mut self, commands: Vec<Box<dyn Command>>) -> Result<(), Error> {
52 let mut working_set = StateWorkingSet::new(&self.state);
53 for command in commands {
54 working_set.add_decl(command);
55 }
56 self.state.merge_delta(working_set.render())?;
57 Ok(())
58 }
59
60 pub fn load_plugin(&mut self, path: &Path) -> Result<(), Error> {
62 let path = path.canonicalize().map_err(|e| {
64 Error::from(format!("Failed to canonicalize plugin path {path:?}: {e}"))
65 })?;
66
67 let identity = PluginIdentity::new(&path, None).map_err(|_| {
69 Error::from(format!(
70 "Invalid plugin path {path:?}: must be named nu_plugin_*"
71 ))
72 })?;
73
74 let mut working_set = StateWorkingSet::new(&self.state);
75
76 let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
78
79 self.state.merge_delta(working_set.render())?;
81
82 let interface = plugin.clone().get_plugin(None)?;
84
85 plugin.set_metadata(Some(interface.get_metadata()?));
87
88 let mut working_set = StateWorkingSet::new(&self.state);
90 for signature in interface.get_signature()? {
91 let decl = PluginDeclaration::new(plugin.clone(), signature);
92 working_set.add_decl(Box::new(decl));
93 }
94 self.state.merge_delta(working_set.render())?;
95
96 Ok(())
97 }
98
99 pub fn parse_closure(&mut self, script: &str) -> Result<(), Error> {
100 let mut working_set = StateWorkingSet::new(&self.state);
101 let block = parse(&mut working_set, None, script.as_bytes(), false);
102
103 if let Some(err) = working_set.parse_errors.first() {
105 let shell_error = ShellError::GenericError {
106 error: "Parse error".into(),
107 msg: format!("{err:?}"),
108 span: Some(err.span()),
109 help: None,
110 inner: vec![],
111 };
112 return Err(Error::from(format_cli_error(
113 &working_set,
114 &shell_error,
115 None,
116 )));
117 }
118
119 if let Some(err) = working_set.compile_errors.first() {
121 let shell_error = ShellError::GenericError {
122 error: format!("Compile error {err}"),
123 msg: "".into(),
124 span: None,
125 help: None,
126 inner: vec![],
127 };
128 return Err(Error::from(format_cli_error(
129 &working_set,
130 &shell_error,
131 None,
132 )));
133 }
134
135 self.state.merge_delta(working_set.render())?;
136
137 let mut stack = Stack::new();
138 let result = eval_block_with_early_return::<WithoutDebug>(
139 &self.state,
140 &mut stack,
141 &block,
142 PipelineData::empty(),
143 )
144 .map_err(|err| {
145 let working_set = StateWorkingSet::new(&self.state);
146 Error::from(format_cli_error(&working_set, &err, None))
147 })?;
148
149 let closure = result
150 .body
151 .into_value(Span::unknown())
152 .map_err(|err| {
153 let working_set = StateWorkingSet::new(&self.state);
154 Error::from(format_cli_error(&working_set, &err, None))
155 })?
156 .into_closure()
157 .map_err(|err| {
158 let working_set = StateWorkingSet::new(&self.state);
159 Error::from(format_cli_error(&working_set, &err, None))
160 })?;
161
162 let block = self.state.get_block(closure.block_id);
164 if block.signature.required_positional.len() != 1 {
165 return Err(format!(
166 "Closure must accept exactly one request argument, found {}",
167 block.signature.required_positional.len()
168 )
169 .into());
170 }
171
172 self.state.merge_env(&mut stack)?;
173
174 self.closure = Some(closure);
175 Ok(())
176 }
177
178 pub fn set_signals(&mut self, interrupt: Arc<AtomicBool>) {
180 self.state.set_signals(Signals::new(interrupt));
181 }
182
183 pub fn eval(&self, script: &str) -> Result<Value, Error> {
185 let mut working_set = StateWorkingSet::new(&self.state);
186 let block = parse(&mut working_set, None, script.as_bytes(), false);
187
188 if let Some(err) = working_set.parse_errors.first() {
189 let shell_error = ShellError::GenericError {
190 error: "Parse error".into(),
191 msg: format!("{err:?}"),
192 span: Some(err.span()),
193 help: None,
194 inner: vec![],
195 };
196 return Err(Error::from(format_cli_error(
197 &working_set,
198 &shell_error,
199 None,
200 )));
201 }
202
203 if let Some(err) = working_set.compile_errors.first() {
204 let shell_error = ShellError::GenericError {
205 error: format!("Compile error {err}"),
206 msg: "".into(),
207 span: None,
208 help: None,
209 inner: vec![],
210 };
211 return Err(Error::from(format_cli_error(
212 &working_set,
213 &shell_error,
214 None,
215 )));
216 }
217
218 let mut engine_state = self.state.clone();
220 engine_state.merge_delta(working_set.render())?;
221
222 let mut stack = Stack::new();
223 let result = eval_block_with_early_return::<WithoutDebug>(
224 &engine_state,
225 &mut stack,
226 &block,
227 PipelineData::empty(),
228 )
229 .map_err(|err| {
230 let working_set = StateWorkingSet::new(&engine_state);
231 Error::from(format_cli_error(&working_set, &err, None))
232 })?;
233
234 result.body.into_value(Span::unknown()).map_err(|err| {
235 let working_set = StateWorkingSet::new(&engine_state);
236 Error::from(format_cli_error(&working_set, &err, None))
237 })
238 }
239
240 pub fn run_closure(
242 &self,
243 input: Value,
244 pipeline_data: PipelineData,
245 ) -> Result<PipelineData, Error> {
246 let closure = self.closure.as_ref().ok_or("Closure not parsed")?;
247
248 let mut stack = Stack::new().captures_to_stack(closure.captures.clone());
249 let mut stack =
250 stack.push_redirection(Some(Redirection::Pipe(OutDest::PipeSeparate)), None);
251 let block = self.state.get_block(closure.block_id);
252
253 stack.add_var(
254 block.signature.required_positional[0].var_id.unwrap(),
255 input,
256 );
257
258 eval_block_with_early_return::<WithoutDebug>(&self.state, &mut stack, block, pipeline_data)
259 .map(|exec_data| exec_data.body)
260 .map_err(|err| {
261 let working_set = StateWorkingSet::new(&self.state);
262 Error::from(format_cli_error(&working_set, &err, None))
263 })
264 }
265
266 pub fn add_custom_commands(&mut self) -> Result<(), Error> {
268 self.add_commands(vec![
269 Box::new(ResponseStartCommand::new()),
270 Box::new(ReverseProxyCommand::new()),
271 Box::new(StaticCommand::new()),
272 Box::new(ToSse {}),
273 Box::new(MjCommand::new()),
274 Box::new(MjCompileCommand::new()),
275 Box::new(MjRenderCommand::new()),
276 Box::new(HighlightCommand::new()),
277 Box::new(HighlightThemeCommand::new()),
278 Box::new(HighlightLangCommand::new()),
279 ])
280 }
281}
282
283pub fn script_to_engine(base: &Engine, script: &str) -> Option<Engine> {
286 let mut engine = base.clone();
287
288 if let Err(e) = engine.parse_closure(script) {
289 log_error(&nu_utils::strip_ansi_string_likely(e.to_string()));
290 return None;
291 }
292
293 Some(engine)
294}