1use std::path::Path;
2use std::sync::{atomic::AtomicBool, Arc};
3
4use tokio_util::sync::CancellationToken;
5
6use nu_cli::{add_cli_context, gather_parent_env_vars};
7use nu_cmd_lang::create_default_context;
8use nu_command::add_shell_command_context;
9use nu_engine::eval_block_with_early_return;
10use nu_parser::parse;
11use nu_plugin_engine::{GetPlugin, PluginDeclaration};
12use nu_protocol::engine::Command;
13use nu_protocol::format_cli_error;
14use nu_protocol::{
15 debugger::WithoutDebug,
16 engine::{Closure, EngineState, Redirection, Stack, StateWorkingSet},
17 OutDest, PipelineData, PluginIdentity, RegisteredPlugin, ShellError, Signals, Span, Type,
18 Value,
19};
20
21use crate::commands::{
22 HighlightCommand, HighlightLangCommand, HighlightThemeCommand, MdCommand, MjCommand,
23 MjCompileCommand, MjRenderCommand, PrintCommand, ReverseProxyCommand, StaticCommand, ToSse,
24};
25use crate::logging::log_error;
26use crate::stdlib::load_http_nu_stdlib;
27use crate::Error;
28
29#[derive(Clone)]
30pub struct Engine {
31 pub state: EngineState,
32 pub closure: Option<Closure>,
33 pub reload_token: CancellationToken,
35}
36
37impl Engine {
38 pub fn new() -> Result<Self, Error> {
39 let mut engine_state = create_default_context();
40
41 engine_state = add_shell_command_context(engine_state);
42 engine_state = add_cli_context(engine_state);
43 engine_state = nu_cmd_extra::extra::add_extra_command_context(engine_state);
44
45 load_http_nu_stdlib(&mut engine_state)?;
46
47 let init_cwd = std::env::current_dir()?;
48 gather_parent_env_vars(&mut engine_state, init_cwd.as_ref());
49
50 Ok(Self {
51 state: engine_state,
52 closure: None,
53 reload_token: CancellationToken::new(),
54 })
55 }
56
57 pub fn add_commands(&mut self, commands: Vec<Box<dyn Command>>) -> Result<(), Error> {
58 let mut working_set = StateWorkingSet::new(&self.state);
59 for command in commands {
60 working_set.add_decl(command);
61 }
62 self.state.merge_delta(working_set.render())?;
63 Ok(())
64 }
65
66 pub fn load_plugin(&mut self, path: &Path) -> Result<(), Error> {
68 let path = path.canonicalize().map_err(|e| {
70 Error::from(format!("Failed to canonicalize plugin path {path:?}: {e}"))
71 })?;
72
73 let identity = PluginIdentity::new(&path, None).map_err(|_| {
75 Error::from(format!(
76 "Invalid plugin path {path:?}: must be named nu_plugin_*"
77 ))
78 })?;
79
80 let mut working_set = StateWorkingSet::new(&self.state);
81
82 let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
84
85 self.state.merge_delta(working_set.render())?;
87
88 let interface = plugin.clone().get_plugin(None)?;
90
91 plugin.set_metadata(Some(interface.get_metadata()?));
93
94 let mut working_set = StateWorkingSet::new(&self.state);
96 for signature in interface.get_signature()? {
97 let decl = PluginDeclaration::new(plugin.clone(), signature);
98 working_set.add_decl(Box::new(decl));
99 }
100 self.state.merge_delta(working_set.render())?;
101
102 Ok(())
103 }
104
105 pub fn parse_closure(&mut self, script: &str) -> Result<(), Error> {
106 let mut working_set = StateWorkingSet::new(&self.state);
107 let block = parse(&mut working_set, None, script.as_bytes(), false);
108
109 if let Some(err) = working_set.parse_errors.first() {
111 let shell_error = ShellError::GenericError {
112 error: "Parse error".into(),
113 msg: format!("{err:?}"),
114 span: Some(err.span()),
115 help: None,
116 inner: vec![],
117 };
118 return Err(Error::from(format_cli_error(
119 None,
120 &working_set,
121 &shell_error,
122 None,
123 )));
124 }
125
126 if let Some(err) = working_set.compile_errors.first() {
128 let shell_error = ShellError::GenericError {
129 error: format!("Compile error {err}"),
130 msg: "".into(),
131 span: None,
132 help: None,
133 inner: vec![],
134 };
135 return Err(Error::from(format_cli_error(
136 None,
137 &working_set,
138 &shell_error,
139 None,
140 )));
141 }
142
143 self.state.merge_delta(working_set.render())?;
144
145 let mut stack = Stack::new();
146 let result = eval_block_with_early_return::<WithoutDebug>(
147 &self.state,
148 &mut stack,
149 &block,
150 PipelineData::empty(),
151 )
152 .map_err(|err| {
153 let working_set = StateWorkingSet::new(&self.state);
154 Error::from(format_cli_error(None, &working_set, &err, None))
155 })?;
156
157 let closure = result
158 .body
159 .into_value(Span::unknown())
160 .map_err(|err| {
161 let working_set = StateWorkingSet::new(&self.state);
162 Error::from(format_cli_error(None, &working_set, &err, None))
163 })?
164 .into_closure()
165 .map_err(|err| {
166 let working_set = StateWorkingSet::new(&self.state);
167 Error::from(format_cli_error(None, &working_set, &err, None))
168 })?;
169
170 let block = self.state.get_block(closure.block_id);
172 if block.signature.required_positional.len() != 1 {
173 return Err(format!(
174 "Closure must accept exactly one request argument, found {}",
175 block.signature.required_positional.len()
176 )
177 .into());
178 }
179
180 self.state.merge_env(&mut stack)?;
181
182 self.closure = Some(closure);
183 Ok(())
184 }
185
186 pub fn set_signals(&mut self, interrupt: Arc<AtomicBool>) {
188 self.state.set_signals(Signals::new(interrupt));
189 }
190
191 pub fn set_lib_dirs(&mut self, paths: &[std::path::PathBuf]) -> Result<(), Error> {
193 if paths.is_empty() {
194 return Ok(());
195 }
196 let span = Span::unknown();
197 let vals: Vec<Value> = paths
198 .iter()
199 .map(|p| Value::string(p.to_string_lossy(), span))
200 .collect();
201
202 let mut working_set = StateWorkingSet::new(&self.state);
203 let var_id = working_set.add_variable(
204 b"$NU_LIB_DIRS".into(),
205 span,
206 Type::List(Box::new(Type::String)),
207 false,
208 );
209 working_set.set_variable_const_val(var_id, Value::list(vals, span));
210 self.state.merge_delta(working_set.render())?;
211 Ok(())
212 }
213
214 pub fn eval(&self, script: &str) -> Result<Value, Error> {
216 let mut working_set = StateWorkingSet::new(&self.state);
217 let block = parse(&mut working_set, None, script.as_bytes(), false);
218
219 if let Some(err) = working_set.parse_errors.first() {
220 let shell_error = ShellError::GenericError {
221 error: "Parse error".into(),
222 msg: format!("{err:?}"),
223 span: Some(err.span()),
224 help: None,
225 inner: vec![],
226 };
227 return Err(Error::from(format_cli_error(
228 None,
229 &working_set,
230 &shell_error,
231 None,
232 )));
233 }
234
235 if let Some(err) = working_set.compile_errors.first() {
236 let shell_error = ShellError::GenericError {
237 error: format!("Compile error {err}"),
238 msg: "".into(),
239 span: None,
240 help: None,
241 inner: vec![],
242 };
243 return Err(Error::from(format_cli_error(
244 None,
245 &working_set,
246 &shell_error,
247 None,
248 )));
249 }
250
251 let mut engine_state = self.state.clone();
253 engine_state.merge_delta(working_set.render())?;
254
255 let mut stack = Stack::new();
256 let result = eval_block_with_early_return::<WithoutDebug>(
257 &engine_state,
258 &mut stack,
259 &block,
260 PipelineData::empty(),
261 )
262 .map_err(|err| {
263 let working_set = StateWorkingSet::new(&engine_state);
264 Error::from(format_cli_error(None, &working_set, &err, None))
265 })?;
266
267 result.body.into_value(Span::unknown()).map_err(|err| {
268 let working_set = StateWorkingSet::new(&engine_state);
269 Error::from(format_cli_error(None, &working_set, &err, None))
270 })
271 }
272
273 pub fn run_closure(
275 &self,
276 input: Value,
277 pipeline_data: PipelineData,
278 ) -> Result<PipelineData, Error> {
279 let closure = self.closure.as_ref().ok_or("Closure not parsed")?;
280
281 let mut stack = Stack::new().captures_to_stack(closure.captures.clone());
282 let mut stack =
283 stack.push_redirection(Some(Redirection::Pipe(OutDest::PipeSeparate)), None);
284 let block = self.state.get_block(closure.block_id);
285
286 stack.add_var(
287 block.signature.required_positional[0].var_id.unwrap(),
288 input,
289 );
290
291 eval_block_with_early_return::<WithoutDebug>(&self.state, &mut stack, block, pipeline_data)
292 .map(|exec_data| exec_data.body)
293 .map_err(|err| {
294 let working_set = StateWorkingSet::new(&self.state);
295 Error::from(format_cli_error(None, &working_set, &err, None))
296 })
297 }
298
299 pub fn add_custom_commands(&mut self) -> Result<(), Error> {
301 self.add_commands(vec![
302 Box::new(ReverseProxyCommand::new()),
303 Box::new(StaticCommand::new()),
304 Box::new(ToSse {}),
305 Box::new(MjCommand::new()),
306 Box::new(MjCompileCommand::new()),
307 Box::new(MjRenderCommand::new()),
308 Box::new(HighlightCommand::new()),
309 Box::new(HighlightThemeCommand::new()),
310 Box::new(HighlightLangCommand::new()),
311 Box::new(MdCommand::new()),
312 Box::new(PrintCommand::new()),
313 ])
314 }
315
316 #[cfg(feature = "cross-stream")]
318 pub fn add_store_commands(&mut self, store: &xs::store::Store) -> Result<(), Error> {
319 self.add_commands(vec![
320 Box::new(xs::nu::commands::cat_stream_command::CatStreamCommand::new(
321 store.clone(),
322 )),
323 Box::new(xs::nu::commands::append_command::AppendCommand::new(
324 store.clone(),
325 serde_json::json!({}),
326 )),
327 Box::new(xs::nu::commands::cas_command::CasCommand::new(
328 store.clone(),
329 )),
330 Box::new(xs::nu::commands::last_stream_command::LastStreamCommand::new(store.clone())),
331 Box::new(xs::nu::commands::get_command::GetCommand::new(
332 store.clone(),
333 )),
334 Box::new(xs::nu::commands::remove_command::RemoveCommand::new(
335 store.clone(),
336 )),
337 Box::new(xs::nu::commands::scru128_command::Scru128Command::new()),
338 ])
339 }
340}
341
342pub fn script_to_engine(base: &Engine, script: &str) -> Option<Engine> {
345 let mut engine = base.clone();
346 engine.reload_token = CancellationToken::new();
348
349 if let Err(e) = engine.parse_closure(script) {
350 log_error(&nu_utils::strip_ansi_string_likely(e.to_string()));
351 return None;
352 }
353
354 Some(engine)
355}