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 nu_std::load_standard_library(&mut engine_state)?;
47
48 let init_cwd = std::env::current_dir()?;
49 gather_parent_env_vars(&mut engine_state, init_cwd.as_ref());
50
51 Ok(Self {
52 state: engine_state,
53 closure: None,
54 reload_token: CancellationToken::new(),
55 })
56 }
57
58 pub fn set_http_nu_env(&mut self, dev: bool) -> Result<(), Error> {
60 let mut stack = Stack::new();
61 let span = Span::unknown();
62 let record = Value::record(
63 nu_protocol::record! {
64 "dev" => Value::bool(dev, span),
65 },
66 span,
67 );
68 stack.add_env_var("HTTP_NU".to_string(), record);
69 self.state.merge_env(&mut stack)?;
70 Ok(())
71 }
72
73 pub fn add_commands(&mut self, commands: Vec<Box<dyn Command>>) -> Result<(), Error> {
74 let mut working_set = StateWorkingSet::new(&self.state);
75 for command in commands {
76 working_set.add_decl(command);
77 }
78 self.state.merge_delta(working_set.render())?;
79 Ok(())
80 }
81
82 pub fn load_plugin(&mut self, path: &Path) -> Result<(), Error> {
84 let path = path.canonicalize().map_err(|e| {
86 Error::from(format!("Failed to canonicalize plugin path {path:?}: {e}"))
87 })?;
88
89 let identity = PluginIdentity::new(&path, None).map_err(|_| {
91 Error::from(format!(
92 "Invalid plugin path {path:?}: must be named nu_plugin_*"
93 ))
94 })?;
95
96 let mut working_set = StateWorkingSet::new(&self.state);
97
98 let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
100
101 self.state.merge_delta(working_set.render())?;
103
104 let interface = plugin.clone().get_plugin(None)?;
106
107 plugin.set_metadata(Some(interface.get_metadata()?));
109
110 let mut working_set = StateWorkingSet::new(&self.state);
112 for signature in interface.get_signature()? {
113 let decl = PluginDeclaration::new(plugin.clone(), signature);
114 working_set.add_decl(Box::new(decl));
115 }
116 self.state.merge_delta(working_set.render())?;
117
118 Ok(())
119 }
120
121 pub fn parse_closure(&mut self, script: &str, file: Option<&Path>) -> Result<(), Error> {
122 self.state.file = file.map(|p| p.to_path_buf());
123 let fname = file.map(|p| p.to_string_lossy().into_owned());
124 let mut working_set = StateWorkingSet::new(&self.state);
125 let block = parse(&mut working_set, fname.as_deref(), script.as_bytes(), false);
126
127 if let Some(err) = working_set.parse_errors.first() {
129 let shell_error = ShellError::GenericError {
130 error: "Parse error".into(),
131 msg: format!("{err:?}"),
132 span: Some(err.span()),
133 help: None,
134 inner: vec![],
135 };
136 return Err(Error::from(format_cli_error(
137 None,
138 &working_set,
139 &shell_error,
140 None,
141 )));
142 }
143
144 if let Some(err) = working_set.compile_errors.first() {
146 let shell_error = ShellError::GenericError {
147 error: format!("Compile error {err}"),
148 msg: "".into(),
149 span: None,
150 help: None,
151 inner: vec![],
152 };
153 return Err(Error::from(format_cli_error(
154 None,
155 &working_set,
156 &shell_error,
157 None,
158 )));
159 }
160
161 self.state.merge_delta(working_set.render())?;
162
163 let mut stack = Stack::new();
164 let result = eval_block_with_early_return::<WithoutDebug>(
165 &self.state,
166 &mut stack,
167 &block,
168 PipelineData::empty(),
169 )
170 .map_err(|err| {
171 let working_set = StateWorkingSet::new(&self.state);
172 Error::from(format_cli_error(None, &working_set, &err, None))
173 })?;
174
175 let closure = result
176 .body
177 .into_value(Span::unknown())
178 .map_err(|err| {
179 let working_set = StateWorkingSet::new(&self.state);
180 Error::from(format_cli_error(None, &working_set, &err, None))
181 })?
182 .into_closure()
183 .map_err(|err| {
184 let working_set = StateWorkingSet::new(&self.state);
185 Error::from(format_cli_error(None, &working_set, &err, None))
186 })?;
187
188 let block = self.state.get_block(closure.block_id);
190 if block.signature.required_positional.len() != 1 {
191 return Err(format!(
192 "Closure must accept exactly one request argument, found {}",
193 block.signature.required_positional.len()
194 )
195 .into());
196 }
197
198 self.state.merge_env(&mut stack)?;
199
200 self.closure = Some(closure);
201 Ok(())
202 }
203
204 pub fn set_signals(&mut self, interrupt: Arc<AtomicBool>) {
206 self.state.set_signals(Signals::new(interrupt));
207 }
208
209 pub fn set_lib_dirs(&mut self, paths: &[std::path::PathBuf]) -> Result<(), Error> {
211 if paths.is_empty() {
212 return Ok(());
213 }
214 let span = Span::unknown();
215 let vals: Vec<Value> = paths
216 .iter()
217 .map(|p| Value::string(p.to_string_lossy(), span))
218 .collect();
219
220 let mut working_set = StateWorkingSet::new(&self.state);
221 let var_id = working_set.add_variable(
222 b"$NU_LIB_DIRS".into(),
223 span,
224 Type::List(Box::new(Type::String)),
225 false,
226 );
227 working_set.set_variable_const_val(var_id, Value::list(vals, span));
228 self.state.merge_delta(working_set.render())?;
229 Ok(())
230 }
231
232 pub fn eval(&mut self, script: &str, file: Option<&Path>) -> Result<Value, Error> {
234 self.state.file = file.map(|p| p.to_path_buf());
235 let fname = file.map(|p| p.to_string_lossy().into_owned());
236 let mut working_set = StateWorkingSet::new(&self.state);
237 let block = parse(&mut working_set, fname.as_deref(), script.as_bytes(), false);
238
239 if let Some(err) = working_set.parse_errors.first() {
240 let shell_error = ShellError::GenericError {
241 error: "Parse error".into(),
242 msg: format!("{err:?}"),
243 span: Some(err.span()),
244 help: None,
245 inner: vec![],
246 };
247 return Err(Error::from(format_cli_error(
248 None,
249 &working_set,
250 &shell_error,
251 None,
252 )));
253 }
254
255 if let Some(err) = working_set.compile_errors.first() {
256 let shell_error = ShellError::GenericError {
257 error: format!("Compile error {err}"),
258 msg: "".into(),
259 span: None,
260 help: None,
261 inner: vec![],
262 };
263 return Err(Error::from(format_cli_error(
264 None,
265 &working_set,
266 &shell_error,
267 None,
268 )));
269 }
270
271 let mut engine_state = self.state.clone();
273 engine_state.merge_delta(working_set.render())?;
274
275 let mut stack = Stack::new();
276 let result = eval_block_with_early_return::<WithoutDebug>(
277 &engine_state,
278 &mut stack,
279 &block,
280 PipelineData::empty(),
281 )
282 .map_err(|err| {
283 let working_set = StateWorkingSet::new(&engine_state);
284 Error::from(format_cli_error(None, &working_set, &err, None))
285 })?;
286
287 result.body.into_value(Span::unknown()).map_err(|err| {
288 let working_set = StateWorkingSet::new(&engine_state);
289 Error::from(format_cli_error(None, &working_set, &err, None))
290 })
291 }
292
293 pub fn run_closure(
295 &self,
296 input: Value,
297 pipeline_data: PipelineData,
298 ) -> Result<PipelineData, Error> {
299 let closure = self.closure.as_ref().ok_or("Closure not parsed")?;
300
301 let mut stack = Stack::new().captures_to_stack(closure.captures.clone());
302 let mut stack =
303 stack.push_redirection(Some(Redirection::Pipe(OutDest::PipeSeparate)), None);
304 let block = self.state.get_block(closure.block_id);
305
306 stack.add_var(
307 block.signature.required_positional[0].var_id.unwrap(),
308 input,
309 );
310
311 eval_block_with_early_return::<WithoutDebug>(&self.state, &mut stack, block, pipeline_data)
312 .map(|exec_data| exec_data.body)
313 .map_err(|err| {
314 let working_set = StateWorkingSet::new(&self.state);
315 Error::from(format_cli_error(None, &working_set, &err, None))
316 })
317 }
318
319 pub fn add_custom_commands(&mut self) -> Result<(), Error> {
321 self.add_commands(vec![
322 Box::new(ReverseProxyCommand::new()),
323 Box::new(StaticCommand::new()),
324 Box::new(ToSse {}),
325 Box::new(MjCommand::new()),
326 Box::new(MjCompileCommand::new()),
327 Box::new(MjRenderCommand::new()),
328 Box::new(HighlightCommand::new()),
329 Box::new(HighlightThemeCommand::new()),
330 Box::new(HighlightLangCommand::new()),
331 Box::new(MdCommand::new()),
332 Box::new(PrintCommand::new()),
333 ])
334 }
335
336 #[cfg(feature = "cross-stream")]
338 pub fn add_store_commands(&mut self, store: &xs::store::Store) -> Result<(), Error> {
339 self.add_commands(vec![
340 Box::new(xs::nu::commands::cat_stream_command::CatStreamCommand::new(
341 store.clone(),
342 )),
343 Box::new(xs::nu::commands::append_command::AppendCommand::new(
344 store.clone(),
345 serde_json::json!({}),
346 )),
347 Box::new(xs::nu::commands::cas_command::CasCommand::new(
348 store.clone(),
349 )),
350 Box::new(xs::nu::commands::last_stream_command::LastStreamCommand::new(store.clone())),
351 Box::new(xs::nu::commands::get_command::GetCommand::new(
352 store.clone(),
353 )),
354 Box::new(xs::nu::commands::remove_command::RemoveCommand::new(
355 store.clone(),
356 )),
357 Box::new(xs::nu::commands::scru128_command::Scru128Command::new()),
358 ])
359 }
360
361 #[cfg(feature = "cross-stream")]
363 pub fn add_store_mj_commands(&mut self, store: &xs::store::Store) -> Result<(), Error> {
364 self.add_commands(vec![
365 Box::new(MjCommand::with_store(store.clone())),
366 Box::new(MjCompileCommand::with_store(store.clone())),
367 ])
368 }
369}
370
371pub fn script_to_engine(base: &Engine, script: &str, file: Option<&Path>) -> Option<Engine> {
374 let mut engine = base.clone();
375 engine.reload_token = CancellationToken::new();
377
378 if let Err(e) = engine.parse_closure(script, file) {
379 log_error(&nu_utils::strip_ansi_string_likely(e.to_string()));
380 return None;
381 }
382
383 Some(engine)
384}