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