1use std::collections::HashMap;
26use std::path::PathBuf;
27use std::sync::Arc;
28
29use anyhow::{Context, Result};
30use tokio::sync::RwLock;
31
32use async_trait::async_trait;
33
34use crate::ast::{Arg, Command, Expr, FileTestOp, Stmt, StringPart, TestExpr, ToolDef, Value, BinaryOp};
35use crate::backend::{BackendError, KernelBackend};
36use kaish_glob::glob_match;
37use crate::dispatch::{CommandDispatcher, PipelinePosition};
38use crate::interpreter::{apply_output_format, eval_expr, expand_tilde, json_to_value, value_to_bool, value_to_string, ControlFlow, ExecResult, Scope};
39use crate::parser::parse;
40use crate::scheduler::{drain_to_stream, is_bool_type, schema_param_lookup, stderr_stream, BoundedStream, JobManager, PipelineRunner, StderrReceiver, DEFAULT_STREAM_MAX_SIZE};
41use crate::tools::{extract_output_format, register_builtins, resolve_in_path, ExecContext, ToolArgs, ToolRegistry};
42use crate::validator::{Severity, Validator};
43use crate::vfs::{BuiltinFs, JobFs, LocalFs, MemoryFs, VfsRouter};
44
45#[derive(Debug, Clone)]
52pub enum VfsMountMode {
53 Passthrough,
62
63 Sandboxed {
78 root: Option<PathBuf>,
81 },
82
83 NoLocal,
93}
94
95impl Default for VfsMountMode {
96 fn default() -> Self {
97 VfsMountMode::Sandboxed { root: None }
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct KernelConfig {
104 pub name: String,
106
107 pub vfs_mode: VfsMountMode,
109
110 pub cwd: PathBuf,
112
113 pub skip_validation: bool,
119
120 pub interactive: bool,
125
126 pub ignore_config: crate::ignore_config::IgnoreConfig,
128
129 pub output_limit: crate::output_limit::OutputLimitConfig,
131
132 pub allow_external_commands: bool,
142
143 pub latch_enabled: bool,
148
149 pub trash_enabled: bool,
155
156 pub nonce_store: Option<crate::nonce::NonceStore>,
162}
163
164fn default_sandbox_root() -> PathBuf {
166 std::env::var("HOME")
167 .map(PathBuf::from)
168 .unwrap_or_else(|_| PathBuf::from("/"))
169}
170
171impl Default for KernelConfig {
172 fn default() -> Self {
173 let home = default_sandbox_root();
174 Self {
175 name: "default".to_string(),
176 vfs_mode: VfsMountMode::Sandboxed { root: None },
177 cwd: home,
178 skip_validation: false,
179 interactive: false,
180 ignore_config: crate::ignore_config::IgnoreConfig::none(),
181 output_limit: crate::output_limit::OutputLimitConfig::none(),
182 allow_external_commands: true,
183 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
184 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
185 nonce_store: None,
186 }
187 }
188}
189
190impl KernelConfig {
191 pub fn transient() -> Self {
193 let home = default_sandbox_root();
194 Self {
195 name: "transient".to_string(),
196 vfs_mode: VfsMountMode::Sandboxed { root: None },
197 cwd: home,
198 skip_validation: false,
199 interactive: false,
200 ignore_config: crate::ignore_config::IgnoreConfig::none(),
201 output_limit: crate::output_limit::OutputLimitConfig::none(),
202 allow_external_commands: true,
203 latch_enabled: false,
204 trash_enabled: false,
205 nonce_store: None,
206 }
207 }
208
209 pub fn named(name: &str) -> Self {
211 let home = default_sandbox_root();
212 Self {
213 name: name.to_string(),
214 vfs_mode: VfsMountMode::Sandboxed { root: None },
215 cwd: home,
216 skip_validation: false,
217 interactive: false,
218 ignore_config: crate::ignore_config::IgnoreConfig::none(),
219 output_limit: crate::output_limit::OutputLimitConfig::none(),
220 allow_external_commands: true,
221 latch_enabled: false,
222 trash_enabled: false,
223 nonce_store: None,
224 }
225 }
226
227 pub fn repl() -> Self {
232 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
233 Self {
234 name: "repl".to_string(),
235 vfs_mode: VfsMountMode::Passthrough,
236 cwd,
237 skip_validation: false,
238 interactive: false,
239 ignore_config: crate::ignore_config::IgnoreConfig::none(),
240 output_limit: crate::output_limit::OutputLimitConfig::none(),
241 allow_external_commands: true,
242 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
243 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
244 nonce_store: None,
245 }
246 }
247
248 pub fn mcp() -> Self {
255 let home = default_sandbox_root();
256 Self {
257 name: "mcp".to_string(),
258 vfs_mode: VfsMountMode::Sandboxed { root: None },
259 cwd: home,
260 skip_validation: false,
261 interactive: false,
262 ignore_config: crate::ignore_config::IgnoreConfig::mcp(),
263 output_limit: crate::output_limit::OutputLimitConfig::mcp(),
264 allow_external_commands: true,
265 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
266 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
267 nonce_store: None,
268 }
269 }
270
271 pub fn mcp_with_root(root: PathBuf) -> Self {
275 Self {
276 name: "mcp".to_string(),
277 vfs_mode: VfsMountMode::Sandboxed { root: Some(root.clone()) },
278 cwd: root,
279 skip_validation: false,
280 interactive: false,
281 ignore_config: crate::ignore_config::IgnoreConfig::mcp(),
282 output_limit: crate::output_limit::OutputLimitConfig::mcp(),
283 allow_external_commands: true,
284 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
285 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
286 nonce_store: None,
287 }
288 }
289
290 pub fn isolated() -> Self {
295 Self {
296 name: "isolated".to_string(),
297 vfs_mode: VfsMountMode::NoLocal,
298 cwd: PathBuf::from("/"),
299 skip_validation: false,
300 interactive: false,
301 ignore_config: crate::ignore_config::IgnoreConfig::none(),
302 output_limit: crate::output_limit::OutputLimitConfig::none(),
303 allow_external_commands: false,
304 latch_enabled: false,
305 trash_enabled: false,
306 nonce_store: None,
307 }
308 }
309
310 pub fn with_vfs_mode(mut self, mode: VfsMountMode) -> Self {
312 self.vfs_mode = mode;
313 self
314 }
315
316 pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
318 self.cwd = cwd;
319 self
320 }
321
322 pub fn with_skip_validation(mut self, skip: bool) -> Self {
324 self.skip_validation = skip;
325 self
326 }
327
328 pub fn with_interactive(mut self, interactive: bool) -> Self {
330 self.interactive = interactive;
331 self
332 }
333
334 pub fn with_ignore_config(mut self, config: crate::ignore_config::IgnoreConfig) -> Self {
336 self.ignore_config = config;
337 self
338 }
339
340 pub fn with_output_limit(mut self, config: crate::output_limit::OutputLimitConfig) -> Self {
342 self.output_limit = config;
343 self
344 }
345
346 pub fn with_allow_external_commands(mut self, allow: bool) -> Self {
352 self.allow_external_commands = allow;
353 self
354 }
355
356 pub fn with_latch(mut self, enabled: bool) -> Self {
358 self.latch_enabled = enabled;
359 self
360 }
361
362 pub fn with_trash(mut self, enabled: bool) -> Self {
364 self.trash_enabled = enabled;
365 self
366 }
367
368 pub fn with_nonce_store(mut self, store: crate::nonce::NonceStore) -> Self {
373 self.nonce_store = Some(store);
374 self
375 }
376}
377
378pub struct Kernel {
383 name: String,
385 scope: RwLock<Scope>,
387 tools: Arc<ToolRegistry>,
389 user_tools: RwLock<HashMap<String, ToolDef>>,
391 vfs: Arc<VfsRouter>,
393 jobs: Arc<JobManager>,
395 runner: PipelineRunner,
397 exec_ctx: RwLock<ExecContext>,
399 skip_validation: bool,
401 interactive: bool,
403 allow_external_commands: bool,
405 stderr_receiver: tokio::sync::Mutex<StderrReceiver>,
410 cancel_token: std::sync::Mutex<tokio_util::sync::CancellationToken>,
416 #[cfg(unix)]
418 terminal_state: Option<Arc<crate::terminal::TerminalState>>,
419}
420
421impl Kernel {
422 pub fn new(config: KernelConfig) -> Result<Self> {
424 let mut vfs = Self::setup_vfs(&config);
425 let jobs = Arc::new(JobManager::new());
426
427 vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
429
430 Self::assemble(config, vfs, jobs, |_| {}, |vfs_ref, tools| {
431 ExecContext::with_vfs_and_tools(vfs_ref.clone(), tools.clone())
432 })
433 }
434
435 fn setup_vfs(config: &KernelConfig) -> VfsRouter {
437 let mut vfs = VfsRouter::new();
438
439 match &config.vfs_mode {
440 VfsMountMode::Passthrough => {
441 vfs.mount("/", LocalFs::new(PathBuf::from("/")));
443 vfs.mount("/v", MemoryFs::new());
445 }
446 VfsMountMode::Sandboxed { root } => {
447 vfs.mount("/", MemoryFs::new());
449 vfs.mount("/v", MemoryFs::new());
450
451 vfs.mount("/tmp", LocalFs::new(PathBuf::from("/tmp")));
453
454 let runtime = crate::paths::xdg_runtime_dir();
456 if runtime.exists() {
457 let runtime_str = runtime.to_string_lossy().to_string();
458 vfs.mount(&runtime_str, LocalFs::new(runtime));
459 }
460
461 let local_root = root.clone().unwrap_or_else(|| {
463 std::env::var("HOME")
464 .map(PathBuf::from)
465 .unwrap_or_else(|_| PathBuf::from("/"))
466 });
467
468 let mount_point = local_root.to_string_lossy().to_string();
472 vfs.mount(&mount_point, LocalFs::new(local_root));
473 }
474 VfsMountMode::NoLocal => {
475 vfs.mount("/", MemoryFs::new());
477 vfs.mount("/tmp", MemoryFs::new());
478 vfs.mount("/v", MemoryFs::new());
479 }
480 }
481
482 vfs
483 }
484
485 pub fn transient() -> Result<Self> {
487 Self::new(KernelConfig::transient())
488 }
489
490 pub fn with_backend(
524 backend: Arc<dyn KernelBackend>,
525 config: KernelConfig,
526 configure_vfs: impl FnOnce(&mut VfsRouter),
527 configure_tools: impl FnOnce(&mut ToolRegistry),
528 ) -> Result<Self> {
529 use crate::backend::VirtualOverlayBackend;
530
531 let mut vfs = VfsRouter::new();
532 let jobs = Arc::new(JobManager::new());
533
534 vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
535 vfs.mount("/v/blobs", MemoryFs::new());
536
537 configure_vfs(&mut vfs);
539
540 Self::assemble(config, vfs, jobs, configure_tools, |vfs_arc: &Arc<VfsRouter>, _: &Arc<ToolRegistry>| {
541 let overlay: Arc<dyn KernelBackend> =
542 Arc::new(VirtualOverlayBackend::new(backend, vfs_arc.clone()));
543 ExecContext::with_backend(overlay)
544 })
545 }
546
547 fn assemble(
553 config: KernelConfig,
554 mut vfs: VfsRouter,
555 jobs: Arc<JobManager>,
556 configure_tools: impl FnOnce(&mut ToolRegistry),
557 make_ctx: impl FnOnce(&Arc<VfsRouter>, &Arc<ToolRegistry>) -> ExecContext,
558 ) -> Result<Self> {
559 let KernelConfig { name, cwd, skip_validation, interactive, ignore_config, output_limit, allow_external_commands, latch_enabled, trash_enabled, nonce_store, .. } = config;
560
561 let mut tools = ToolRegistry::new();
562 register_builtins(&mut tools);
563 configure_tools(&mut tools);
564 let tools = Arc::new(tools);
565
566 vfs.mount("/v/bin", BuiltinFs::new(tools.clone()));
568
569 let vfs = Arc::new(vfs);
570
571 let runner = PipelineRunner::new(tools.clone());
572
573 let (stderr_writer, stderr_receiver) = stderr_stream();
574
575 let mut exec_ctx = make_ctx(&vfs, &tools);
576 exec_ctx.set_cwd(cwd);
577 exec_ctx.set_job_manager(jobs.clone());
578 exec_ctx.set_tool_schemas(tools.schemas());
579 exec_ctx.set_tools(tools.clone());
580 exec_ctx.stderr = Some(stderr_writer);
581 exec_ctx.ignore_config = ignore_config;
582 exec_ctx.output_limit = output_limit;
583 exec_ctx.allow_external_commands = allow_external_commands;
584 if let Some(store) = nonce_store {
585 exec_ctx.nonce_store = store;
586 }
587
588 Ok(Self {
589 name,
590 scope: RwLock::new({
591 let mut scope = Scope::new();
592 if let Ok(home) = std::env::var("HOME") {
593 scope.set("HOME", Value::String(home));
594 }
595 scope.set_latch_enabled(latch_enabled);
596 scope.set_trash_enabled(trash_enabled);
597 scope
598 }),
599 tools,
600 user_tools: RwLock::new(HashMap::new()),
601 vfs,
602 jobs,
603 runner,
604 exec_ctx: RwLock::new(exec_ctx),
605 skip_validation,
606 interactive,
607 allow_external_commands,
608 stderr_receiver: tokio::sync::Mutex::new(stderr_receiver),
609 cancel_token: std::sync::Mutex::new(tokio_util::sync::CancellationToken::new()),
610 #[cfg(unix)]
611 terminal_state: None,
612 })
613 }
614
615 pub fn name(&self) -> &str {
617 &self.name
618 }
619
620 #[cfg(unix)]
625 pub fn init_terminal(&mut self) {
626 if !self.interactive {
627 return;
628 }
629 match crate::terminal::TerminalState::init() {
630 Ok(state) => {
631 let state = Arc::new(state);
632 self.terminal_state = Some(state.clone());
633 self.exec_ctx.get_mut().terminal_state = Some(state);
635 tracing::debug!("terminal job control initialized");
636 }
637 Err(e) => {
638 tracing::warn!("failed to initialize terminal job control: {}", e);
639 }
640 }
641 }
642
643 pub fn cancel(&self) {
649 #[allow(clippy::expect_used)]
650 let token = self.cancel_token.lock().expect("cancel_token poisoned");
651 token.cancel();
652 }
653
654 pub fn is_cancelled(&self) -> bool {
656 #[allow(clippy::expect_used)]
657 let token = self.cancel_token.lock().expect("cancel_token poisoned");
658 token.is_cancelled()
659 }
660
661 fn reset_cancel(&self) -> tokio_util::sync::CancellationToken {
663 #[allow(clippy::expect_used)]
664 let mut token = self.cancel_token.lock().expect("cancel_token poisoned");
665 if token.is_cancelled() {
666 *token = tokio_util::sync::CancellationToken::new();
667 }
668 token.clone()
669 }
670
671 pub async fn execute(&self, input: &str) -> Result<ExecResult> {
675 self.execute_streaming(input, &mut |_| {}).await
676 }
677
678 #[tracing::instrument(level = "info", skip(self, on_output), fields(input_len = input.len()))]
687 pub async fn execute_streaming(
688 &self,
689 input: &str,
690 on_output: &mut dyn FnMut(&ExecResult),
691 ) -> Result<ExecResult> {
692 let program = parse(input).map_err(|errors| {
693 let msg = errors
694 .iter()
695 .map(|e| e.to_string())
696 .collect::<Vec<_>>()
697 .join("; ");
698 anyhow::anyhow!("parse error: {}", msg)
699 })?;
700
701 {
703 let scope = self.scope.read().await;
704 if scope.show_ast() {
705 let output = format!("{:#?}\n", program);
706 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(output)));
707 }
708 }
709
710 if !self.skip_validation {
712 let user_tools = self.user_tools.read().await;
713 let validator = Validator::new(&self.tools, &user_tools);
714 let issues = validator.validate(&program);
715
716 let errors: Vec<_> = issues
718 .iter()
719 .filter(|i| i.severity == Severity::Error)
720 .collect();
721
722 if !errors.is_empty() {
723 let error_msg = errors
724 .iter()
725 .map(|e| e.format(input))
726 .collect::<Vec<_>>()
727 .join("\n");
728 return Err(anyhow::anyhow!("validation failed:\n{}", error_msg));
729 }
730
731 for warning in issues.iter().filter(|i| i.severity == Severity::Warning) {
733 tracing::trace!("validation: {}", warning.format(input));
734 }
735 }
736
737 let mut result = ExecResult::success("");
738
739 let cancel = self.reset_cancel();
741
742 for stmt in program.statements {
743 if matches!(stmt, Stmt::Empty) {
744 continue;
745 }
746
747 if cancel.is_cancelled() {
749 result.code = 130;
750 return Ok(result);
751 }
752
753 let flow = self.execute_stmt_flow(&stmt).await?;
754
755 let drained_stderr = {
759 let mut receiver = self.stderr_receiver.lock().await;
760 receiver.drain_lossy()
761 };
762
763 match flow {
764 ControlFlow::Normal(mut r) => {
765 if !drained_stderr.is_empty() {
766 if !r.err.is_empty() && !r.err.ends_with('\n') {
767 r.err.push('\n');
768 }
769 let combined = format!("{}{}", drained_stderr, r.err);
771 r.err = combined;
772 }
773 on_output(&r);
774 let last_output = r.output.clone();
778 accumulate_result(&mut result, &r);
779 result.output = last_output;
780 }
781 ControlFlow::Exit { code } => {
782 if !drained_stderr.is_empty() {
783 result.err.push_str(&drained_stderr);
784 }
785 result.code = code;
786 return Ok(result);
787 }
788 ControlFlow::Return { mut value } => {
789 if !drained_stderr.is_empty() {
790 value.err = format!("{}{}", drained_stderr, value.err);
791 }
792 on_output(&value);
793 result = value;
794 }
795 ControlFlow::Break { result: mut r, .. } | ControlFlow::Continue { result: mut r, .. } => {
796 if !drained_stderr.is_empty() {
797 r.err = format!("{}{}", drained_stderr, r.err);
798 }
799 on_output(&r);
800 result = r;
801 }
802 }
803 }
804
805 Ok(result)
806 }
807
808 fn execute_stmt_flow<'a>(
810 &'a self,
811 stmt: &'a Stmt,
812 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ControlFlow>> + Send + 'a>> {
813 use tracing::Instrument;
814 let span = tracing::debug_span!("execute_stmt_flow", stmt_type = %stmt.kind_name());
815 Box::pin(async move {
816 match stmt {
817 Stmt::Assignment(assign) => {
818 let value = self.eval_expr_async(&assign.value).await
820 .context("failed to evaluate assignment")?;
821 let mut scope = self.scope.write().await;
822 if assign.local {
823 scope.set(&assign.name, value.clone());
825 } else {
826 scope.set_global(&assign.name, value.clone());
828 }
829 drop(scope);
830
831 Ok(ControlFlow::ok(ExecResult::success("")))
833 }
834 Stmt::Command(cmd) => {
835 let pipeline = crate::ast::Pipeline {
838 commands: vec![cmd.clone()],
839 background: false,
840 };
841 let result = self.execute_pipeline(&pipeline).await?;
842 self.update_last_result(&result).await;
843
844 if !result.ok() {
846 let scope = self.scope.read().await;
847 if scope.error_exit_enabled() {
848 return Ok(ControlFlow::exit_code(result.code));
849 }
850 }
851
852 Ok(ControlFlow::ok(result))
853 }
854 Stmt::Pipeline(pipeline) => {
855 let result = self.execute_pipeline(pipeline).await?;
856 self.update_last_result(&result).await;
857
858 if !result.ok() {
860 let scope = self.scope.read().await;
861 if scope.error_exit_enabled() {
862 return Ok(ControlFlow::exit_code(result.code));
863 }
864 }
865
866 Ok(ControlFlow::ok(result))
867 }
868 Stmt::If(if_stmt) => {
869 let cond_value = self.eval_expr_async(&if_stmt.condition).await?;
871
872 let branch = if is_truthy(&cond_value) {
873 &if_stmt.then_branch
874 } else {
875 if_stmt.else_branch.as_deref().unwrap_or(&[])
876 };
877
878 let mut result = ExecResult::success("");
879 for stmt in branch {
880 let flow = self.execute_stmt_flow(stmt).await?;
881 match flow {
882 ControlFlow::Normal(r) => {
883 accumulate_result(&mut result, &r);
884 self.drain_stderr_into(&mut result).await;
885 }
886 other => {
887 self.drain_stderr_into(&mut result).await;
888 return Ok(other);
889 }
890 }
891 }
892 Ok(ControlFlow::ok(result))
893 }
894 Stmt::For(for_loop) => {
895 let mut items: Vec<Value> = Vec::new();
898 for item_expr in &for_loop.items {
899 if let Expr::GlobPattern(pattern) = item_expr {
901 let glob_enabled = {
902 let scope = self.scope.read().await;
903 scope.glob_enabled()
904 };
905 if glob_enabled {
906 let (paths, cwd) = {
907 let ctx = self.exec_ctx.read().await;
908 let paths = ctx.expand_glob(pattern).await
909 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
910 let cwd = ctx.resolve_path(".");
911 (paths, cwd)
912 };
913 if paths.is_empty() {
914 return Err(anyhow::anyhow!("no matches: {}", pattern));
915 }
916 for path in paths {
917 let display = if !pattern.starts_with('/') {
918 path.strip_prefix(&cwd)
919 .unwrap_or(&path)
920 .to_string_lossy().into_owned()
921 } else {
922 path.to_string_lossy().into_owned()
923 };
924 items.push(Value::String(display));
925 }
926 continue;
927 }
928 }
929 let item = self.eval_expr_async(item_expr).await?;
930 match &item {
932 Value::Json(serde_json::Value::Array(arr)) => {
934 for elem in arr {
935 items.push(json_to_value(elem.clone()));
936 }
937 }
938 Value::String(_) => {
941 items.push(item);
942 }
943 _ => items.push(item),
945 }
946 }
947
948 let mut result = ExecResult::success("");
949 {
950 let mut scope = self.scope.write().await;
951 scope.push_frame();
952 }
953
954 'outer: for item in items {
955 if self.is_cancelled() {
957 let mut scope = self.scope.write().await;
958 scope.pop_frame();
959 result.code = 130;
960 return Ok(ControlFlow::ok(result));
961 }
962 {
963 let mut scope = self.scope.write().await;
964 scope.set(&for_loop.variable, item);
965 }
966 for stmt in &for_loop.body {
967 let mut flow = self.execute_stmt_flow(stmt).await?;
968 self.drain_stderr_into(&mut result).await;
969 match &mut flow {
970 ControlFlow::Normal(r) => {
971 accumulate_result(&mut result, r);
972 if !r.ok() {
973 let scope = self.scope.read().await;
974 if scope.error_exit_enabled() {
975 drop(scope);
976 let mut scope = self.scope.write().await;
977 scope.pop_frame();
978 return Ok(ControlFlow::exit_code(r.code));
979 }
980 }
981 }
982 ControlFlow::Break { .. } => {
983 if flow.decrement_level() {
984 break 'outer;
985 }
986 let mut scope = self.scope.write().await;
987 scope.pop_frame();
988 return Ok(flow);
989 }
990 ControlFlow::Continue { .. } => {
991 if flow.decrement_level() {
992 continue 'outer;
993 }
994 let mut scope = self.scope.write().await;
995 scope.pop_frame();
996 return Ok(flow);
997 }
998 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
999 let mut scope = self.scope.write().await;
1000 scope.pop_frame();
1001 return Ok(flow);
1002 }
1003 }
1004 }
1005 }
1006
1007 {
1008 let mut scope = self.scope.write().await;
1009 scope.pop_frame();
1010 }
1011 Ok(ControlFlow::ok(result))
1012 }
1013 Stmt::While(while_loop) => {
1014 let mut result = ExecResult::success("");
1015
1016 'outer: loop {
1017 if self.is_cancelled() {
1020 result.code = 130;
1021 return Ok(ControlFlow::ok(result));
1022 }
1023
1024 let cond_value = self.eval_expr_async(&while_loop.condition).await?;
1025
1026 if !is_truthy(&cond_value) {
1027 break;
1028 }
1029
1030 for stmt in &while_loop.body {
1032 let mut flow = self.execute_stmt_flow(stmt).await?;
1033 self.drain_stderr_into(&mut result).await;
1034 match &mut flow {
1035 ControlFlow::Normal(r) => {
1036 accumulate_result(&mut result, r);
1037 if !r.ok() {
1038 let scope = self.scope.read().await;
1039 if scope.error_exit_enabled() {
1040 return Ok(ControlFlow::exit_code(r.code));
1041 }
1042 }
1043 }
1044 ControlFlow::Break { .. } => {
1045 if flow.decrement_level() {
1046 break 'outer;
1047 }
1048 return Ok(flow);
1049 }
1050 ControlFlow::Continue { .. } => {
1051 if flow.decrement_level() {
1052 continue 'outer;
1053 }
1054 return Ok(flow);
1055 }
1056 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
1057 return Ok(flow);
1058 }
1059 }
1060 }
1061 }
1062
1063 Ok(ControlFlow::ok(result))
1064 }
1065 Stmt::Case(case_stmt) => {
1066 let match_value = {
1068 let value = self.eval_expr_async(&case_stmt.expr).await?;
1069 value_to_string(&value)
1070 };
1071
1072 for branch in &case_stmt.branches {
1074 let matched = branch.patterns.iter().any(|pattern| {
1075 glob_match(pattern, &match_value)
1076 });
1077
1078 if matched {
1079 let mut result = ExecResult::success("");
1081 for stmt in &branch.body {
1082 let flow = self.execute_stmt_flow(stmt).await?;
1083 match flow {
1084 ControlFlow::Normal(r) => {
1085 accumulate_result(&mut result, &r);
1086 self.drain_stderr_into(&mut result).await;
1087 }
1088 other => {
1089 self.drain_stderr_into(&mut result).await;
1090 return Ok(other);
1091 }
1092 }
1093 }
1094 return Ok(ControlFlow::ok(result));
1095 }
1096 }
1097
1098 Ok(ControlFlow::ok(ExecResult::success("")))
1100 }
1101 Stmt::Break(levels) => {
1102 Ok(ControlFlow::break_n(levels.unwrap_or(1)))
1103 }
1104 Stmt::Continue(levels) => {
1105 Ok(ControlFlow::continue_n(levels.unwrap_or(1)))
1106 }
1107 Stmt::Return(expr) => {
1108 let result = if let Some(e) = expr {
1111 let val = self.eval_expr_async(e).await?;
1112 let code = match val {
1114 Value::Int(n) => n,
1115 Value::Bool(b) => if b { 0 } else { 1 },
1116 _ => 0,
1117 };
1118 ExecResult {
1119 code,
1120 out: String::new(),
1121 err: String::new(),
1122 data: None,
1123 output: None,
1124 did_spill: false,
1125 original_code: None,
1126 }
1127 } else {
1128 ExecResult::success("")
1129 };
1130 Ok(ControlFlow::return_value(result))
1131 }
1132 Stmt::Exit(expr) => {
1133 let code = if let Some(e) = expr {
1134 let val = self.eval_expr_async(e).await?;
1135 match val {
1136 Value::Int(n) => n,
1137 _ => 0,
1138 }
1139 } else {
1140 0
1141 };
1142 Ok(ControlFlow::exit_code(code))
1143 }
1144 Stmt::ToolDef(tool_def) => {
1145 let mut user_tools = self.user_tools.write().await;
1146 user_tools.insert(tool_def.name.clone(), tool_def.clone());
1147 Ok(ControlFlow::ok(ExecResult::success("")))
1148 }
1149 Stmt::AndChain { left, right } => {
1150 {
1153 let mut scope = self.scope.write().await;
1154 scope.suppress_errexit();
1155 }
1156 let left_flow = self.execute_stmt_flow(left).await?;
1157 {
1158 let mut scope = self.scope.write().await;
1159 scope.unsuppress_errexit();
1160 }
1161 match left_flow {
1162 ControlFlow::Normal(mut left_result) => {
1163 self.drain_stderr_into(&mut left_result).await;
1164 self.update_last_result(&left_result).await;
1165 if left_result.ok() {
1166 let right_flow = self.execute_stmt_flow(right).await?;
1167 match right_flow {
1168 ControlFlow::Normal(mut right_result) => {
1169 self.drain_stderr_into(&mut right_result).await;
1170 self.update_last_result(&right_result).await;
1171 let mut combined = left_result;
1172 accumulate_result(&mut combined, &right_result);
1173 Ok(ControlFlow::ok(combined))
1174 }
1175 other => Ok(other),
1176 }
1177 } else {
1178 Ok(ControlFlow::ok(left_result))
1179 }
1180 }
1181 _ => Ok(left_flow),
1182 }
1183 }
1184 Stmt::OrChain { left, right } => {
1185 {
1188 let mut scope = self.scope.write().await;
1189 scope.suppress_errexit();
1190 }
1191 let left_flow = self.execute_stmt_flow(left).await?;
1192 {
1193 let mut scope = self.scope.write().await;
1194 scope.unsuppress_errexit();
1195 }
1196 match left_flow {
1197 ControlFlow::Normal(mut left_result) => {
1198 self.drain_stderr_into(&mut left_result).await;
1199 self.update_last_result(&left_result).await;
1200 if !left_result.ok() {
1201 let right_flow = self.execute_stmt_flow(right).await?;
1202 match right_flow {
1203 ControlFlow::Normal(mut right_result) => {
1204 self.drain_stderr_into(&mut right_result).await;
1205 self.update_last_result(&right_result).await;
1206 let mut combined = left_result;
1207 accumulate_result(&mut combined, &right_result);
1208 Ok(ControlFlow::ok(combined))
1209 }
1210 other => Ok(other),
1211 }
1212 } else {
1213 Ok(ControlFlow::ok(left_result))
1214 }
1215 }
1216 _ => Ok(left_flow), }
1218 }
1219 Stmt::Test(test_expr) => {
1220 let is_true = self.eval_test_async(test_expr).await?;
1221 if is_true {
1222 Ok(ControlFlow::ok(ExecResult::success("")))
1223 } else {
1224 Ok(ControlFlow::ok(ExecResult::failure(1, "")))
1225 }
1226 }
1227 Stmt::Empty => Ok(ControlFlow::ok(ExecResult::success(""))),
1228 }
1229 }.instrument(span))
1230 }
1231
1232 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(background = pipeline.background, command_count = pipeline.commands.len()))]
1234 async fn execute_pipeline(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
1235 if pipeline.commands.is_empty() {
1236 return Ok(ExecResult::success(""));
1237 }
1238
1239 if pipeline.background {
1241 return self.execute_background(pipeline).await;
1242 }
1243
1244 let mut ctx = {
1252 let ec = self.exec_ctx.read().await;
1253 let scope = self.scope.read().await;
1254 ExecContext {
1255 backend: ec.backend.clone(),
1256 scope: scope.clone(),
1257 cwd: ec.cwd.clone(),
1258 prev_cwd: ec.prev_cwd.clone(),
1259 stdin: None,
1260 stdin_data: None,
1261 pipe_stdin: None,
1262 pipe_stdout: None,
1263 stderr: ec.stderr.clone(),
1264 tool_schemas: ec.tool_schemas.clone(),
1265 tools: ec.tools.clone(),
1266 job_manager: ec.job_manager.clone(),
1267 pipeline_position: PipelinePosition::Only,
1268 interactive: self.interactive,
1269 aliases: ec.aliases.clone(),
1270 ignore_config: ec.ignore_config.clone(),
1271 output_limit: ec.output_limit.clone(),
1272 allow_external_commands: self.allow_external_commands,
1273 nonce_store: ec.nonce_store.clone(),
1274 #[cfg(unix)]
1275 terminal_state: ec.terminal_state.clone(),
1276 }
1277 }; let mut result = self.runner.run(&pipeline.commands, &mut ctx, self).await;
1280
1281 if ctx.output_limit.is_enabled() {
1283 let _ = crate::output_limit::spill_if_needed(&mut result, &ctx.output_limit).await;
1284 }
1285
1286 if result.did_spill {
1289 result.original_code = Some(result.code);
1290 result.code = 3;
1291 }
1292
1293 {
1295 let mut ec = self.exec_ctx.write().await;
1296 ec.cwd = ctx.cwd.clone();
1297 ec.prev_cwd = ctx.prev_cwd.clone();
1298 ec.aliases = ctx.aliases.clone();
1299 ec.ignore_config = ctx.ignore_config.clone();
1300 ec.output_limit = ctx.output_limit.clone();
1301 }
1302 {
1303 let mut scope = self.scope.write().await;
1304 *scope = ctx.scope.clone();
1305 }
1306
1307 Ok(result)
1308 }
1309
1310 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(command_count = pipeline.commands.len()))]
1318 async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
1319 use tokio::sync::oneshot;
1320
1321 let command_str = self.format_pipeline(pipeline);
1323
1324 let stdout = Arc::new(BoundedStream::default_size());
1326 let stderr = Arc::new(BoundedStream::default_size());
1327
1328 let (tx, rx) = oneshot::channel();
1330
1331 let job_id = self.jobs.register_with_streams(
1333 command_str.clone(),
1334 rx,
1335 stdout.clone(),
1336 stderr.clone(),
1337 ).await;
1338
1339 let runner = self.runner.clone();
1341 let commands = pipeline.commands.clone();
1342 let backend = {
1343 let ctx = self.exec_ctx.read().await;
1344 ctx.backend.clone()
1345 };
1346 let scope = {
1347 let scope = self.scope.read().await;
1348 scope.clone()
1349 };
1350 let cwd = {
1351 let ctx = self.exec_ctx.read().await;
1352 ctx.cwd.clone()
1353 };
1354 let tools = self.tools.clone();
1355 let tool_schemas = self.tools.schemas();
1356 let allow_ext = self.allow_external_commands;
1357
1358 tokio::spawn(async move {
1360 let mut bg_ctx = ExecContext::with_backend(backend);
1363 bg_ctx.scope = scope;
1364 bg_ctx.cwd = cwd;
1365 bg_ctx.set_tools(tools.clone());
1366 bg_ctx.set_tool_schemas(tool_schemas);
1367 bg_ctx.allow_external_commands = allow_ext;
1368
1369 let dispatcher = crate::dispatch::BackendDispatcher::new(tools);
1372
1373 let result = runner.run(&commands, &mut bg_ctx, &dispatcher).await;
1375
1376 let text = result.text_out();
1378 if !text.is_empty() {
1379 stdout.write(text.as_bytes()).await;
1380 }
1381 if !result.err.is_empty() {
1382 stderr.write(result.err.as_bytes()).await;
1383 }
1384
1385 stdout.close().await;
1387 stderr.close().await;
1388
1389 let _ = tx.send(result);
1391 });
1392
1393 Ok(ExecResult::success(format!("[{}]", job_id)))
1394 }
1395
1396 fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
1398 pipeline.commands
1399 .iter()
1400 .map(|cmd| {
1401 let mut parts = vec![cmd.name.clone()];
1402 for arg in &cmd.args {
1403 match arg {
1404 Arg::Positional(expr) => {
1405 parts.push(self.format_expr(expr));
1406 }
1407 Arg::Named { key, value } => {
1408 parts.push(format!("{}={}", key, self.format_expr(value)));
1409 }
1410 Arg::ShortFlag(name) => {
1411 parts.push(format!("-{}", name));
1412 }
1413 Arg::LongFlag(name) => {
1414 parts.push(format!("--{}", name));
1415 }
1416 Arg::DoubleDash => {
1417 parts.push("--".to_string());
1418 }
1419 }
1420 }
1421 parts.join(" ")
1422 })
1423 .collect::<Vec<_>>()
1424 .join(" | ")
1425 }
1426
1427 fn format_expr(&self, expr: &Expr) -> String {
1429 match expr {
1430 Expr::Literal(Value::String(s)) => {
1431 if s.contains(' ') || s.contains('"') {
1432 format!("'{}'", s.replace('\'', "\\'"))
1433 } else {
1434 s.clone()
1435 }
1436 }
1437 Expr::Literal(Value::Int(i)) => i.to_string(),
1438 Expr::Literal(Value::Float(f)) => f.to_string(),
1439 Expr::Literal(Value::Bool(b)) => b.to_string(),
1440 Expr::Literal(Value::Null) => "null".to_string(),
1441 Expr::VarRef(path) => {
1442 let name = path.segments.iter()
1443 .map(|seg| match seg {
1444 crate::ast::VarSegment::Field(f) => f.clone(),
1445 })
1446 .collect::<Vec<_>>()
1447 .join(".");
1448 format!("${{{}}}", name)
1449 }
1450 Expr::Interpolated(_) => "\"...\"".to_string(),
1451 _ => "...".to_string(),
1452 }
1453 }
1454
1455 async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
1457 self.execute_command_depth(name, args, 0).await
1458 }
1459
1460 #[tracing::instrument(level = "info", skip(self, args, alias_depth), fields(command = %name), err)]
1461 async fn execute_command_depth(&self, name: &str, args: &[Arg], alias_depth: u8) -> Result<ExecResult> {
1462 match name {
1464 "true" => return Ok(ExecResult::success("")),
1465 "false" => return Ok(ExecResult::failure(1, "")),
1466 "source" | "." => return self.execute_source(args).await,
1467 _ => {}
1468 }
1469
1470 if alias_depth < 10 {
1472 let alias_value = {
1473 let ctx = self.exec_ctx.read().await;
1474 ctx.aliases.get(name).cloned()
1475 };
1476 if let Some(alias_val) = alias_value {
1477 let parts: Vec<&str> = alias_val.split_whitespace().collect();
1479 if let Some((alias_cmd, alias_args)) = parts.split_first() {
1480 let mut new_args: Vec<Arg> = alias_args
1481 .iter()
1482 .map(|a| Arg::Positional(Expr::Literal(Value::String(a.to_string()))))
1483 .collect();
1484 new_args.extend_from_slice(args);
1485 return Box::pin(self.execute_command_depth(alias_cmd, &new_args, alias_depth + 1)).await;
1486 }
1487 }
1488 }
1489
1490 if let Some(builtin_name) = name.strip_prefix("/v/bin/") {
1492 return match self.tools.get(builtin_name) {
1493 Some(_) => Box::pin(self.execute_command_depth(builtin_name, args, alias_depth)).await,
1494 None => Ok(ExecResult::failure(127, format!("command not found: {}", name))),
1495 };
1496 }
1497
1498 {
1500 let user_tools = self.user_tools.read().await;
1501 if let Some(tool_def) = user_tools.get(name) {
1502 let tool_def = tool_def.clone();
1503 drop(user_tools);
1504 return self.execute_user_tool(tool_def, args).await;
1505 }
1506 }
1507
1508 let tool = match self.tools.get(name) {
1510 Some(t) => t,
1511 None => {
1512 if let Some(result) = self.try_execute_script(name, args).await? {
1514 return Ok(result);
1515 }
1516 if let Some(result) = self.try_execute_external(name, args).await? {
1518 return Ok(result);
1519 }
1520
1521 let backend = self.exec_ctx.read().await.backend.clone();
1526 let tool_schema = backend.get_tool(name).await.ok().flatten().map(|t| {
1527 let mut s = t.schema;
1528 s.map_positionals = true;
1529 s
1530 });
1531 let tool_args = self.build_args_async(args, tool_schema.as_ref()).await?;
1532 let mut ctx = self.exec_ctx.write().await;
1533 {
1534 let scope = self.scope.read().await;
1535 ctx.scope = scope.clone();
1536 }
1537 let backend = ctx.backend.clone();
1538 match backend.call_tool(name, tool_args, &mut ctx).await {
1539 Ok(tool_result) => {
1540 let mut scope = self.scope.write().await;
1541 *scope = ctx.scope.clone();
1542 let mut exec = ExecResult::from_output(
1543 tool_result.code as i64, tool_result.stdout, tool_result.stderr,
1544 );
1545 exec.output = tool_result.output;
1546 return Ok(exec);
1547 }
1548 Err(BackendError::ToolNotFound(_)) => {
1549 }
1551 Err(e) => {
1552 tracing::debug!("backend error for {name}: {e}");
1555 }
1556 }
1557
1558 return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
1559 }
1560 };
1561
1562 let schema = tool.schema();
1564 let mut tool_args = self.build_args_async(args, Some(&schema)).await?;
1565 let output_format = extract_output_format(&mut tool_args, Some(&schema));
1566
1567 let schema_claims = |flag: &str| -> bool {
1569 let bare = flag.trim_start_matches('-');
1570 schema.params.iter().any(|p| p.matches_flag(flag) || p.matches_flag(bare))
1571 };
1572 let wants_help =
1573 (tool_args.flags.contains("help") && !schema_claims("help"))
1574 || (tool_args.flags.contains("h") && !schema_claims("-h"));
1575 if wants_help {
1576 let help_topic = crate::help::HelpTopic::Tool(name.to_string());
1577 let ctx = self.exec_ctx.read().await;
1578 let content = crate::help::get_help(&help_topic, &ctx.tool_schemas);
1579 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(content)));
1580 }
1581
1582 let mut ctx = self.exec_ctx.write().await;
1584 {
1585 let scope = self.scope.read().await;
1586 ctx.scope = scope.clone();
1587 }
1588
1589 let result = tool.execute(tool_args, &mut ctx).await;
1590
1591 {
1593 let mut scope = self.scope.write().await;
1594 *scope = ctx.scope.clone();
1595 }
1596
1597 let result = match output_format {
1598 Some(format) => apply_output_format(result, format),
1599 None => result,
1600 };
1601
1602 Ok(result)
1603 }
1604
1605 async fn build_args_async(&self, args: &[Arg], schema: Option<&crate::tools::ToolSchema>) -> Result<ToolArgs> {
1609 let mut tool_args = ToolArgs::new();
1610 let param_lookup = schema.map(schema_param_lookup).unwrap_or_default();
1611
1612 let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
1614 let mut past_double_dash = false;
1615
1616 let positional_indices: Vec<usize> = args.iter().enumerate()
1618 .filter_map(|(i, a)| matches!(a, Arg::Positional(_)).then_some(i))
1619 .collect();
1620
1621 let mut i = 0;
1622 while i < args.len() {
1623 match &args[i] {
1624 Arg::DoubleDash => {
1625 past_double_dash = true;
1626 }
1627 Arg::Positional(expr) => {
1628 if !consumed.contains(&i) {
1629 if let Expr::GlobPattern(pattern) = expr {
1631 let glob_enabled = {
1632 let scope = self.scope.read().await;
1633 scope.glob_enabled()
1634 };
1635 if glob_enabled {
1636 let (paths, cwd) = {
1637 let ctx = self.exec_ctx.read().await;
1638 let paths = ctx.expand_glob(pattern).await
1639 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
1640 let cwd = ctx.resolve_path(".");
1641 (paths, cwd)
1642 };
1643 if paths.is_empty() {
1644 return Err(anyhow::anyhow!("no matches: {}", pattern));
1645 }
1646 for path in paths {
1647 let display = if !pattern.starts_with('/') {
1648 path.strip_prefix(&cwd)
1649 .unwrap_or(&path)
1650 .to_string_lossy().into_owned()
1651 } else {
1652 path.to_string_lossy().into_owned()
1653 };
1654 tool_args.positional.push(Value::String(display));
1655 }
1656 i += 1;
1657 continue;
1658 }
1659 }
1660 let value = self.eval_expr_async(expr).await?;
1661 let value = apply_tilde_expansion(value);
1662 tool_args.positional.push(value);
1663 }
1664 }
1665 Arg::Named { key, value } => {
1666 let val = self.eval_expr_async(value).await?;
1667 let val = apply_tilde_expansion(val);
1668 tool_args.named.insert(key.clone(), val);
1669 }
1670 Arg::ShortFlag(name) => {
1671 if past_double_dash {
1672 tool_args.positional.push(Value::String(format!("-{name}")));
1673 } else if name.len() == 1 {
1674 let flag_name = name.as_str();
1675 let lookup = param_lookup.get(flag_name);
1676 let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1677
1678 if is_bool {
1679 tool_args.flags.insert(flag_name.to_string());
1680 } else {
1681 let canonical = lookup.map(|(name, _)| *name).unwrap_or(flag_name);
1683 let next_pos = positional_indices.iter()
1684 .find(|idx| **idx > i && !consumed.contains(idx));
1685
1686 if let Some(&pos_idx) = next_pos {
1687 if let Arg::Positional(expr) = &args[pos_idx] {
1688 let value = self.eval_expr_async(expr).await?;
1689 let value = apply_tilde_expansion(value);
1690 tool_args.named.insert(canonical.to_string(), value);
1691 consumed.insert(pos_idx);
1692 }
1693 } else {
1694 tool_args.flags.insert(flag_name.to_string());
1695 }
1696 }
1697 } else if let Some(&(canonical, typ)) = param_lookup.get(name.as_str()) {
1698 if is_bool_type(typ) {
1700 tool_args.flags.insert(canonical.to_string());
1701 } else {
1702 let next_pos = positional_indices.iter()
1703 .find(|idx| **idx > i && !consumed.contains(idx));
1704 if let Some(&pos_idx) = next_pos {
1705 if let Arg::Positional(expr) = &args[pos_idx] {
1706 let value = self.eval_expr_async(expr).await?;
1707 let value = apply_tilde_expansion(value);
1708 tool_args.named.insert(canonical.to_string(), value);
1709 consumed.insert(pos_idx);
1710 }
1711 } else {
1712 tool_args.flags.insert(name.clone());
1713 }
1714 }
1715 } else {
1716 for c in name.chars() {
1718 tool_args.flags.insert(c.to_string());
1719 }
1720 }
1721 }
1722 Arg::LongFlag(name) => {
1723 if past_double_dash {
1724 tool_args.positional.push(Value::String(format!("--{name}")));
1725 } else {
1726 let lookup = param_lookup.get(name.as_str());
1727 let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1728
1729 if is_bool {
1730 tool_args.flags.insert(name.clone());
1731 } else {
1732 let canonical = lookup.map(|(name, _)| *name).unwrap_or(name.as_str());
1733 let next_pos = positional_indices.iter()
1734 .find(|idx| **idx > i && !consumed.contains(idx));
1735
1736 if let Some(&pos_idx) = next_pos {
1737 if let Arg::Positional(expr) = &args[pos_idx] {
1738 let value = self.eval_expr_async(expr).await?;
1739 let value = apply_tilde_expansion(value);
1740 tool_args.named.insert(canonical.to_string(), value);
1741 consumed.insert(pos_idx);
1742 }
1743 } else {
1744 tool_args.flags.insert(name.clone());
1745 }
1746 }
1747 }
1748 }
1749 }
1750 i += 1;
1751 }
1752
1753 if let Some(schema) = schema.filter(|s| s.map_positionals) {
1758 let pre_dash_count = if past_double_dash {
1759 let dash_pos = args.iter().position(|a| matches!(a, Arg::DoubleDash)).unwrap_or(args.len());
1760 positional_indices.iter()
1761 .filter(|idx| **idx < dash_pos && !consumed.contains(idx))
1762 .count()
1763 } else {
1764 tool_args.positional.len()
1765 };
1766
1767 let mut remaining = Vec::new();
1768 let mut positional_iter = tool_args.positional.drain(..).enumerate();
1769
1770 for param in &schema.params {
1771 if tool_args.named.contains_key(¶m.name) || tool_args.flags.contains(¶m.name) {
1772 continue;
1773 }
1774 if is_bool_type(¶m.param_type) {
1775 continue;
1776 }
1777 loop {
1778 match positional_iter.next() {
1779 Some((idx, val)) if idx < pre_dash_count => {
1780 tool_args.named.insert(param.name.clone(), val);
1781 break;
1782 }
1783 Some((_, val)) => {
1784 remaining.push(val);
1785 }
1786 None => break,
1787 }
1788 }
1789 }
1790
1791 remaining.extend(positional_iter.map(|(_, v)| v));
1792 tool_args.positional = remaining;
1793 }
1794
1795 Ok(tool_args)
1796 }
1797
1798 async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
1808 let mut argv = Vec::new();
1809 for arg in args {
1810 match arg {
1811 Arg::Positional(expr) => {
1812 if let Expr::GlobPattern(pattern) = expr {
1814 let glob_enabled = {
1815 let scope = self.scope.read().await;
1816 scope.glob_enabled()
1817 };
1818 if glob_enabled {
1819 let (paths, cwd) = {
1820 let ctx = self.exec_ctx.read().await;
1821 let paths = ctx.expand_glob(pattern).await
1822 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
1823 let cwd = ctx.resolve_path(".");
1824 (paths, cwd)
1825 };
1826 if paths.is_empty() {
1827 return Err(anyhow::anyhow!("no matches: {}", pattern));
1828 }
1829 for path in paths {
1830 let display = if !pattern.starts_with('/') {
1831 path.strip_prefix(&cwd)
1832 .unwrap_or(&path)
1833 .to_string_lossy().into_owned()
1834 } else {
1835 path.to_string_lossy().into_owned()
1836 };
1837 argv.push(display);
1838 }
1839 continue;
1840 }
1841 }
1842 let value = self.eval_expr_async(expr).await?;
1843 let value = apply_tilde_expansion(value);
1844 argv.push(value_to_string(&value));
1845 }
1846 Arg::Named { key, value } => {
1847 let val = self.eval_expr_async(value).await?;
1848 let val = apply_tilde_expansion(val);
1849 argv.push(format!("{}={}", key, value_to_string(&val)));
1850 }
1851 Arg::ShortFlag(name) => {
1852 argv.push(format!("-{}", name));
1854 }
1855 Arg::LongFlag(name) => {
1856 argv.push(format!("--{}", name));
1858 }
1859 Arg::DoubleDash => {
1860 argv.push("--".to_string());
1862 }
1863 }
1864 }
1865 Ok(argv)
1866 }
1867
1868 fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
1873 Box::pin(async move {
1874 match expr {
1875 Expr::Literal(value) => Ok(value.clone()),
1876 Expr::VarRef(path) => {
1877 let scope = self.scope.read().await;
1878 scope.resolve_path(path)
1879 .ok_or_else(|| anyhow::anyhow!("undefined variable"))
1880 }
1881 Expr::Interpolated(parts) => {
1882 let mut result = String::new();
1883 for part in parts {
1884 result.push_str(&self.eval_string_part_async(part).await?);
1885 }
1886 Ok(Value::String(result))
1887 }
1888 Expr::BinaryOp { left, op, right } => {
1889 match op {
1890 BinaryOp::And => {
1891 let left_val = self.eval_expr_async(left).await?;
1892 if !is_truthy(&left_val) {
1893 return Ok(left_val);
1894 }
1895 self.eval_expr_async(right).await
1896 }
1897 BinaryOp::Or => {
1898 let left_val = self.eval_expr_async(left).await?;
1899 if is_truthy(&left_val) {
1900 return Ok(left_val);
1901 }
1902 self.eval_expr_async(right).await
1903 }
1904 _ => {
1905 let left_val = self.eval_expr_async(left).await?;
1907 let right_val = self.eval_expr_async(right).await?;
1908 let resolved = Expr::BinaryOp {
1909 left: Box::new(Expr::Literal(left_val)),
1910 op: *op,
1911 right: Box::new(Expr::Literal(right_val)),
1912 };
1913 let mut scope = self.scope.write().await;
1914 eval_expr(&resolved, &mut scope).map_err(|e| anyhow::anyhow!("{}", e))
1915 }
1916 }
1917 }
1918 Expr::CommandSubst(pipeline) => {
1919 let saved_scope = { self.scope.read().await.clone() };
1922 let saved_cwd = {
1923 let ec = self.exec_ctx.read().await;
1924 (ec.cwd.clone(), ec.prev_cwd.clone())
1925 };
1926
1927 let run_result = self.execute_pipeline(pipeline).await;
1929
1930 {
1932 let mut scope = self.scope.write().await;
1933 *scope = saved_scope;
1934 if let Ok(ref r) = run_result {
1935 scope.set_last_result(r.clone());
1936 }
1937 }
1938 {
1939 let mut ec = self.exec_ctx.write().await;
1940 ec.cwd = saved_cwd.0;
1941 ec.prev_cwd = saved_cwd.1;
1942 }
1943
1944 let result = run_result?;
1946
1947 if let Some(data) = &result.data {
1949 Ok(data.clone())
1950 } else if let Some(ref output) = result.output {
1951 if output.is_flat() && !output.is_simple_text() && !output.root.is_empty() {
1953 let items: Vec<serde_json::Value> = output.root.iter()
1954 .map(|n| serde_json::Value::String(n.display_name().to_string()))
1955 .collect();
1956 Ok(Value::Json(serde_json::Value::Array(items)))
1957 } else {
1958 Ok(Value::String(result.text_out().trim_end().to_string()))
1959 }
1960 } else {
1961 Ok(Value::String(result.text_out().trim_end().to_string()))
1963 }
1964 }
1965 Expr::Test(test_expr) => {
1966 Ok(Value::Bool(self.eval_test_async(test_expr).await?))
1967 }
1968 Expr::Positional(n) => {
1969 let scope = self.scope.read().await;
1970 match scope.get_positional(*n) {
1971 Some(s) => Ok(Value::String(s.to_string())),
1972 None => Ok(Value::String(String::new())),
1973 }
1974 }
1975 Expr::AllArgs => {
1976 let scope = self.scope.read().await;
1977 Ok(Value::String(scope.all_args().join(" ")))
1978 }
1979 Expr::ArgCount => {
1980 let scope = self.scope.read().await;
1981 Ok(Value::Int(scope.arg_count() as i64))
1982 }
1983 Expr::VarLength(name) => {
1984 let scope = self.scope.read().await;
1985 match scope.get(name) {
1986 Some(value) => Ok(Value::Int(value_to_string(value).len() as i64)),
1987 None => Ok(Value::Int(0)),
1988 }
1989 }
1990 Expr::VarWithDefault { name, default } => {
1991 let scope = self.scope.read().await;
1992 let use_default = match scope.get(name) {
1993 Some(value) => value_to_string(value).is_empty(),
1994 None => true,
1995 };
1996 drop(scope); if use_default {
1998 self.eval_string_parts_async(default).await.map(Value::String)
2000 } else {
2001 let scope = self.scope.read().await;
2002 scope.get(name).cloned().ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))
2003 }
2004 }
2005 Expr::Arithmetic(expr_str) => {
2006 let scope = self.scope.read().await;
2007 crate::arithmetic::eval_arithmetic(expr_str, &scope)
2008 .map(Value::Int)
2009 .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
2010 }
2011 Expr::Command(cmd) => {
2012 let result = self.execute_command(&cmd.name, &cmd.args).await?;
2014 Ok(Value::Bool(result.code == 0))
2015 }
2016 Expr::LastExitCode => {
2017 let scope = self.scope.read().await;
2018 Ok(Value::Int(scope.last_result().code))
2019 }
2020 Expr::CurrentPid => {
2021 let scope = self.scope.read().await;
2022 Ok(Value::Int(scope.pid() as i64))
2023 }
2024 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
2025 }
2026 })
2027 }
2028
2029 fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
2031 Box::pin(async move {
2032 let mut result = String::new();
2033 for part in parts {
2034 result.push_str(&self.eval_string_part_async(part).await?);
2035 }
2036 Ok(result)
2037 })
2038 }
2039
2040 fn eval_test_async<'a>(&'a self, test_expr: &'a TestExpr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + Send + 'a>> {
2044 Box::pin(async move {
2045 match test_expr {
2046 TestExpr::FileTest { op, path } => {
2047 let path_value = self.eval_expr_async(path).await?;
2048 let path_str = value_to_string(&path_value);
2049 let backend = self.exec_ctx.read().await.backend.clone();
2050 let entry = backend.stat(std::path::Path::new(&path_str)).await.ok();
2051 Ok(match op {
2052 FileTestOp::Exists => entry.is_some(),
2053 FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
2054 FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
2055 FileTestOp::Readable => entry.is_some(),
2056 FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
2057 e.permissions.is_none_or(|p| p & 0o222 != 0)
2058 }),
2059 FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
2060 e.permissions.is_some_and(|p| p & 0o111 != 0)
2061 }),
2062 })
2063 }
2064 TestExpr::StringTest { op, value } => {
2065 let val = self.eval_expr_async(value).await?;
2066 let s = value_to_string(&val);
2067 Ok(match op {
2068 crate::ast::StringTestOp::IsEmpty => s.is_empty(),
2069 crate::ast::StringTestOp::IsNonEmpty => !s.is_empty(),
2070 })
2071 }
2072 TestExpr::Comparison { left, op, right } => {
2073 let left_val = self.eval_expr_async(left).await?;
2075 let right_val = self.eval_expr_async(right).await?;
2076 let resolved = TestExpr::Comparison {
2077 left: Box::new(Expr::Literal(left_val)),
2078 op: *op,
2079 right: Box::new(Expr::Literal(right_val)),
2080 };
2081 let expr = Expr::Test(Box::new(resolved));
2082 let mut scope = self.scope.write().await;
2083 let value = eval_expr(&expr, &mut scope)
2084 .map_err(|e| anyhow::anyhow!("{}", e))?;
2085 Ok(value_to_bool(&value))
2086 }
2087 TestExpr::And { left, right } => {
2088 if !self.eval_test_async(left).await? {
2089 Ok(false)
2090 } else {
2091 self.eval_test_async(right).await
2092 }
2093 }
2094 TestExpr::Or { left, right } => {
2095 if self.eval_test_async(left).await? {
2096 Ok(true)
2097 } else {
2098 self.eval_test_async(right).await
2099 }
2100 }
2101 TestExpr::Not { expr } => {
2102 Ok(!self.eval_test_async(expr).await?)
2103 }
2104 }
2105 })
2106 }
2107
2108 fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
2109 Box::pin(async move {
2110 match part {
2111 StringPart::Literal(s) => Ok(s.clone()),
2112 StringPart::Var(path) => {
2113 let scope = self.scope.read().await;
2114 match scope.resolve_path(path) {
2115 Some(value) => Ok(value_to_string(&value)),
2116 None => Ok(String::new()), }
2118 }
2119 StringPart::VarWithDefault { name, default } => {
2120 let scope = self.scope.read().await;
2121 let use_default = match scope.get(name) {
2122 Some(value) => value_to_string(value).is_empty(),
2123 None => true,
2124 };
2125 drop(scope); if use_default {
2127 self.eval_string_parts_async(default).await
2129 } else {
2130 let scope = self.scope.read().await;
2131 Ok(value_to_string(scope.get(name).ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))?))
2132 }
2133 }
2134 StringPart::VarLength(name) => {
2135 let scope = self.scope.read().await;
2136 match scope.get(name) {
2137 Some(value) => Ok(value_to_string(value).len().to_string()),
2138 None => Ok("0".to_string()),
2139 }
2140 }
2141 StringPart::Positional(n) => {
2142 let scope = self.scope.read().await;
2143 match scope.get_positional(*n) {
2144 Some(s) => Ok(s.to_string()),
2145 None => Ok(String::new()),
2146 }
2147 }
2148 StringPart::AllArgs => {
2149 let scope = self.scope.read().await;
2150 Ok(scope.all_args().join(" "))
2151 }
2152 StringPart::ArgCount => {
2153 let scope = self.scope.read().await;
2154 Ok(scope.arg_count().to_string())
2155 }
2156 StringPart::Arithmetic(expr) => {
2157 let scope = self.scope.read().await;
2158 match crate::arithmetic::eval_arithmetic(expr, &scope) {
2159 Ok(value) => Ok(value.to_string()),
2160 Err(_) => Ok(String::new()),
2161 }
2162 }
2163 StringPart::CommandSubst(pipeline) => {
2164 let saved_scope = { self.scope.read().await.clone() };
2167 let saved_cwd = {
2168 let ec = self.exec_ctx.read().await;
2169 (ec.cwd.clone(), ec.prev_cwd.clone())
2170 };
2171
2172 let run_result = self.execute_pipeline(pipeline).await;
2174
2175 {
2177 let mut scope = self.scope.write().await;
2178 *scope = saved_scope;
2179 if let Ok(ref r) = run_result {
2180 scope.set_last_result(r.clone());
2181 }
2182 }
2183 {
2184 let mut ec = self.exec_ctx.write().await;
2185 ec.cwd = saved_cwd.0;
2186 ec.prev_cwd = saved_cwd.1;
2187 }
2188
2189 let result = run_result?;
2191
2192 Ok(result.text_out().trim_end_matches('\n').to_string())
2193 }
2194 StringPart::LastExitCode => {
2195 let scope = self.scope.read().await;
2196 Ok(scope.last_result().code.to_string())
2197 }
2198 StringPart::CurrentPid => {
2199 let scope = self.scope.read().await;
2200 Ok(scope.pid().to_string())
2201 }
2202 }
2203 })
2204 }
2205
2206 async fn update_last_result(&self, result: &ExecResult) {
2208 let mut scope = self.scope.write().await;
2209 scope.set_last_result(result.clone());
2210 }
2211
2212 async fn drain_stderr_into(&self, result: &mut ExecResult) {
2218 let drained = {
2219 let mut receiver = self.stderr_receiver.lock().await;
2220 receiver.drain_lossy()
2221 };
2222 if !drained.is_empty() {
2223 if !result.err.is_empty() && !result.err.ends_with('\n') {
2224 result.err.push('\n');
2225 }
2226 result.err.push_str(&drained);
2227 }
2228 }
2229
2230 async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
2236 let tool_args = self.build_args_async(args, None).await?;
2238
2239 {
2241 let mut scope = self.scope.write().await;
2242 scope.push_frame();
2243 }
2244
2245 let saved_positional = {
2247 let mut scope = self.scope.write().await;
2248 let saved = scope.save_positional();
2249
2250 let positional_args: Vec<String> = tool_args.positional
2252 .iter()
2253 .map(value_to_string)
2254 .collect();
2255 scope.set_positional(&def.name, positional_args);
2256
2257 saved
2258 };
2259
2260 let mut accumulated_out = String::new();
2263 let mut accumulated_err = String::new();
2264 let mut last_code = 0i64;
2265 let mut last_data: Option<Value> = None;
2266
2267 let mut exec_error: Option<anyhow::Error> = None;
2269 let mut exit_code: Option<i64> = None;
2270
2271 for stmt in &def.body {
2272 match self.execute_stmt_flow(stmt).await {
2273 Ok(flow) => {
2274 let drained = {
2276 let mut receiver = self.stderr_receiver.lock().await;
2277 receiver.drain_lossy()
2278 };
2279 if !drained.is_empty() {
2280 accumulated_err.push_str(&drained);
2281 }
2282
2283 match flow {
2284 ControlFlow::Normal(r) => {
2285 accumulated_out.push_str(&r.out);
2286 accumulated_err.push_str(&r.err);
2287 last_code = r.code;
2288 last_data = r.data;
2289 }
2290 ControlFlow::Return { value } => {
2291 accumulated_out.push_str(&value.out);
2292 accumulated_err.push_str(&value.err);
2293 last_code = value.code;
2294 last_data = value.data;
2295 break;
2296 }
2297 ControlFlow::Exit { code } => {
2298 exit_code = Some(code);
2299 break;
2300 }
2301 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
2302 accumulated_out.push_str(&r.out);
2303 accumulated_err.push_str(&r.err);
2304 last_code = r.code;
2305 last_data = r.data;
2306 }
2307 }
2308 }
2309 Err(e) => {
2310 exec_error = Some(e);
2311 break;
2312 }
2313 }
2314 }
2315
2316 {
2318 let mut scope = self.scope.write().await;
2319 scope.pop_frame();
2320 scope.set_positional(saved_positional.0, saved_positional.1);
2321 }
2322
2323 if let Some(e) = exec_error {
2325 return Err(e);
2326 }
2327 if let Some(code) = exit_code {
2328 return Ok(ExecResult {
2329 code,
2330 out: accumulated_out,
2331 err: accumulated_err,
2332 data: last_data,
2333 output: None,
2334 did_spill: false,
2335 original_code: None,
2336 });
2337 }
2338
2339 Ok(ExecResult {
2340 code: last_code,
2341 out: accumulated_out,
2342 err: accumulated_err,
2343 data: last_data,
2344 output: None,
2345 did_spill: false,
2346 original_code: None,
2347 })
2348 }
2349
2350 async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
2355 let tool_args = self.build_args_async(args, None).await?;
2357 let path = match tool_args.positional.first() {
2358 Some(Value::String(s)) => s.clone(),
2359 Some(v) => value_to_string(v),
2360 None => {
2361 return Ok(ExecResult::failure(1, "source: missing filename"));
2362 }
2363 };
2364
2365 let full_path = {
2367 let ctx = self.exec_ctx.read().await;
2368 if path.starts_with('/') {
2369 std::path::PathBuf::from(&path)
2370 } else {
2371 ctx.cwd.join(&path)
2372 }
2373 };
2374
2375 let content = {
2377 let ctx = self.exec_ctx.read().await;
2378 match ctx.backend.read(&full_path, None).await {
2379 Ok(bytes) => {
2380 String::from_utf8(bytes).map_err(|e| {
2381 anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
2382 })?
2383 }
2384 Err(e) => {
2385 return Ok(ExecResult::failure(
2386 1,
2387 format!("source: {}: {}", path, e),
2388 ));
2389 }
2390 }
2391 };
2392
2393 let program = match crate::parser::parse(&content) {
2395 Ok(p) => p,
2396 Err(errors) => {
2397 let msg = errors
2398 .iter()
2399 .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
2400 .collect::<Vec<_>>()
2401 .join("\n");
2402 return Ok(ExecResult::failure(1, format!("source: {}", msg)));
2403 }
2404 };
2405
2406 let mut result = ExecResult::success("");
2408 for stmt in program.statements {
2409 if matches!(stmt, crate::ast::Stmt::Empty) {
2410 continue;
2411 }
2412
2413 match self.execute_stmt_flow(&stmt).await {
2414 Ok(flow) => {
2415 self.drain_stderr_into(&mut result).await;
2416 match flow {
2417 ControlFlow::Normal(r) => {
2418 result = r.clone();
2419 self.update_last_result(&r).await;
2420 }
2421 ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
2422 return Err(anyhow::anyhow!(
2423 "source: {}: unexpected break/continue outside loop",
2424 path
2425 ));
2426 }
2427 ControlFlow::Return { value } => {
2428 return Ok(value);
2429 }
2430 ControlFlow::Exit { code } => {
2431 result.code = code;
2432 return Ok(result);
2433 }
2434 }
2435 }
2436 Err(e) => {
2437 return Err(e.context(format!("source: {}", path)));
2438 }
2439 }
2440 }
2441
2442 Ok(result)
2443 }
2444
2445 async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
2450 let path_value = {
2452 let scope = self.scope.read().await;
2453 scope
2454 .get("PATH")
2455 .map(value_to_string)
2456 .unwrap_or_else(|| "/bin".to_string())
2457 };
2458
2459 for dir in path_value.split(':') {
2461 if dir.is_empty() {
2462 continue;
2463 }
2464
2465 let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
2467
2468 let exists = {
2470 let ctx = self.exec_ctx.read().await;
2471 ctx.backend.exists(&script_path).await
2472 };
2473
2474 if !exists {
2475 continue;
2476 }
2477
2478 let content = {
2480 let ctx = self.exec_ctx.read().await;
2481 match ctx.backend.read(&script_path, None).await {
2482 Ok(bytes) => match String::from_utf8(bytes) {
2483 Ok(s) => s,
2484 Err(e) => {
2485 return Ok(Some(ExecResult::failure(
2486 1,
2487 format!("{}: invalid UTF-8: {}", script_path.display(), e),
2488 )));
2489 }
2490 },
2491 Err(e) => {
2492 return Ok(Some(ExecResult::failure(
2493 1,
2494 format!("{}: {}", script_path.display(), e),
2495 )));
2496 }
2497 }
2498 };
2499
2500 let program = match crate::parser::parse(&content) {
2502 Ok(p) => p,
2503 Err(errors) => {
2504 let msg = errors
2505 .iter()
2506 .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
2507 .collect::<Vec<_>>()
2508 .join("\n");
2509 return Ok(Some(ExecResult::failure(1, msg)));
2510 }
2511 };
2512
2513 let tool_args = self.build_args_async(args, None).await?;
2515
2516 let mut isolated_scope = Scope::new();
2518
2519 let positional_args: Vec<String> = tool_args.positional
2521 .iter()
2522 .map(value_to_string)
2523 .collect();
2524 isolated_scope.set_positional(name, positional_args);
2525
2526 let original_scope = {
2528 let mut scope = self.scope.write().await;
2529 std::mem::replace(&mut *scope, isolated_scope)
2530 };
2531
2532 let mut result = ExecResult::success("");
2534 let mut exec_error: Option<anyhow::Error> = None;
2535 let mut exit_code: Option<i64> = None;
2536
2537 for stmt in program.statements {
2538 if matches!(stmt, crate::ast::Stmt::Empty) {
2539 continue;
2540 }
2541
2542 match self.execute_stmt_flow(&stmt).await {
2543 Ok(flow) => {
2544 match flow {
2545 ControlFlow::Normal(r) => result = r,
2546 ControlFlow::Return { value } => {
2547 result = value;
2548 break;
2549 }
2550 ControlFlow::Exit { code } => {
2551 exit_code = Some(code);
2552 break;
2553 }
2554 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
2555 result = r;
2556 }
2557 }
2558 }
2559 Err(e) => {
2560 exec_error = Some(e);
2561 break;
2562 }
2563 }
2564 }
2565
2566 {
2568 let mut scope = self.scope.write().await;
2569 *scope = original_scope;
2570 }
2571
2572 if let Some(e) = exec_error {
2574 return Err(e.context(format!("script: {}", script_path.display())));
2575 }
2576 if let Some(code) = exit_code {
2577 result.code = code;
2578 return Ok(Some(result));
2579 }
2580
2581 return Ok(Some(result));
2582 }
2583
2584 Ok(None)
2586 }
2587
2588 #[tracing::instrument(level = "debug", skip(self, args), fields(command = %name))]
2602 async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
2603 if !self.allow_external_commands {
2604 return Ok(None);
2605 }
2606
2607 let real_cwd = {
2612 let ctx = self.exec_ctx.read().await;
2613 match ctx.backend.resolve_real_path(&ctx.cwd) {
2614 Some(p) => p,
2615 None => return Ok(None),
2616 }
2617 };
2618
2619 let executable = if name.contains('/') {
2620 let resolved = if std::path::Path::new(name).is_absolute() {
2622 std::path::PathBuf::from(name)
2623 } else {
2624 real_cwd.join(name)
2625 };
2626 if !resolved.exists() {
2627 return Ok(Some(ExecResult::failure(
2628 127,
2629 format!("{}: No such file or directory", name),
2630 )));
2631 }
2632 if !resolved.is_file() {
2633 return Ok(Some(ExecResult::failure(
2634 126,
2635 format!("{}: Is a directory", name),
2636 )));
2637 }
2638 #[cfg(unix)]
2639 {
2640 use std::os::unix::fs::PermissionsExt;
2641 let mode = std::fs::metadata(&resolved)
2642 .map(|m| m.permissions().mode())
2643 .unwrap_or(0);
2644 if mode & 0o111 == 0 {
2645 return Ok(Some(ExecResult::failure(
2646 126,
2647 format!("{}: Permission denied", name),
2648 )));
2649 }
2650 }
2651 resolved.to_string_lossy().into_owned()
2652 } else {
2653 let path_var = {
2655 let scope = self.scope.read().await;
2656 scope
2657 .get("PATH")
2658 .map(value_to_string)
2659 .unwrap_or_else(|| std::env::var("PATH").unwrap_or_default())
2660 };
2661
2662 match resolve_in_path(name, &path_var) {
2664 Some(path) => path,
2665 None => return Ok(None), }
2667 };
2668
2669 tracing::debug!(executable = %executable, "resolved external command");
2670
2671 let argv = self.build_args_flat(args).await?;
2673
2674 let stdin_data = {
2676 let mut ctx = self.exec_ctx.write().await;
2677 ctx.take_stdin()
2678 };
2679
2680 use tokio::process::Command;
2682
2683 let mut cmd = Command::new(&executable);
2684 cmd.args(&argv);
2685 cmd.current_dir(&real_cwd);
2686
2687 cmd.stdin(if stdin_data.is_some() {
2689 std::process::Stdio::piped()
2690 } else if self.interactive {
2691 std::process::Stdio::inherit()
2692 } else {
2693 std::process::Stdio::null()
2694 });
2695
2696 let pipeline_position = {
2700 let ctx = self.exec_ctx.read().await;
2701 ctx.pipeline_position
2702 };
2703 let inherit_output = self.interactive
2704 && matches!(pipeline_position, PipelinePosition::Only | PipelinePosition::Last);
2705
2706 if inherit_output {
2707 cmd.stdout(std::process::Stdio::inherit());
2708 cmd.stderr(std::process::Stdio::inherit());
2709 } else {
2710 cmd.stdout(std::process::Stdio::piped());
2711 cmd.stderr(std::process::Stdio::piped());
2712 }
2713
2714 #[cfg(unix)]
2718 if self.terminal_state.is_some() && inherit_output {
2719 #[allow(unsafe_code)]
2721 unsafe {
2722 cmd.pre_exec(|| {
2723 nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))
2725 .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
2726 use nix::libc::{sigaction, SIGTSTP, SIGTTOU, SIGTTIN, SIGINT, SIG_DFL};
2728 let mut sa: nix::libc::sigaction = std::mem::zeroed();
2729 sa.sa_sigaction = SIG_DFL;
2730 sigaction(SIGTSTP, &sa, std::ptr::null_mut());
2731 sigaction(SIGTTOU, &sa, std::ptr::null_mut());
2732 sigaction(SIGTTIN, &sa, std::ptr::null_mut());
2733 sigaction(SIGINT, &sa, std::ptr::null_mut());
2734 Ok(())
2735 });
2736 }
2737 }
2738
2739 let mut child = match cmd.spawn() {
2741 Ok(child) => child,
2742 Err(e) => {
2743 return Ok(Some(ExecResult::failure(
2744 127,
2745 format!("{}: {}", name, e),
2746 )));
2747 }
2748 };
2749
2750 if let Some(data) = stdin_data
2752 && let Some(mut stdin) = child.stdin.take()
2753 {
2754 use tokio::io::AsyncWriteExt;
2755 if let Err(e) = stdin.write_all(data.as_bytes()).await {
2756 return Ok(Some(ExecResult::failure(
2757 1,
2758 format!("{}: failed to write stdin: {}", name, e),
2759 )));
2760 }
2761 }
2763
2764 if inherit_output {
2765 #[cfg(unix)]
2767 if let Some(ref term) = self.terminal_state {
2768 let child_id = child.id().unwrap_or(0);
2769 let pid = nix::unistd::Pid::from_raw(child_id as i32);
2770 let pgid = pid; if let Err(e) = term.give_terminal_to(pgid) {
2774 tracing::warn!("failed to give terminal to child: {}", e);
2775 }
2776
2777 let term_clone = term.clone();
2778 let cmd_name = name.to_string();
2779 let cmd_display = format!("{} {}", name, argv.join(" "));
2780 let jobs = self.jobs.clone();
2781
2782 let code = tokio::task::block_in_place(move || {
2783 let result = term_clone.wait_for_foreground(pid);
2784
2785 if let Err(e) = term_clone.reclaim_terminal() {
2787 tracing::warn!("failed to reclaim terminal: {}", e);
2788 }
2789
2790 match result {
2791 crate::terminal::WaitResult::Exited(code) => code as i64,
2792 crate::terminal::WaitResult::Signaled(sig) => 128 + sig as i64,
2793 crate::terminal::WaitResult::Stopped(_sig) => {
2794 let rt = tokio::runtime::Handle::current();
2796 let job_id = rt.block_on(jobs.register_stopped(
2797 cmd_display,
2798 child_id,
2799 child_id, ));
2801 eprintln!("\n[{}]+ Stopped\t{}", job_id, cmd_name);
2802 148 }
2804 }
2805 });
2806
2807 return Ok(Some(ExecResult::from_output(code, String::new(), String::new())));
2808 }
2809
2810 let status = match child.wait().await {
2812 Ok(s) => s,
2813 Err(e) => {
2814 return Ok(Some(ExecResult::failure(
2815 1,
2816 format!("{}: failed to wait: {}", name, e),
2817 )));
2818 }
2819 };
2820
2821 let code = status.code().unwrap_or_else(|| {
2822 #[cfg(unix)]
2823 {
2824 use std::os::unix::process::ExitStatusExt;
2825 128 + status.signal().unwrap_or(0)
2826 }
2827 #[cfg(not(unix))]
2828 {
2829 -1
2830 }
2831 }) as i64;
2832
2833 Ok(Some(ExecResult::from_output(code, String::new(), String::new())))
2835 } else {
2836 let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2838 let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2839
2840 let stdout_pipe = child.stdout.take();
2841 let stderr_pipe = child.stderr.take();
2842
2843 let stdout_clone = stdout_stream.clone();
2844 let stderr_clone = stderr_stream.clone();
2845
2846 let stdout_task = stdout_pipe.map(|pipe| {
2847 tokio::spawn(async move {
2848 drain_to_stream(pipe, stdout_clone).await;
2849 })
2850 });
2851
2852 let stderr_task = stderr_pipe.map(|pipe| {
2853 tokio::spawn(async move {
2854 drain_to_stream(pipe, stderr_clone).await;
2855 })
2856 });
2857
2858 let status = match child.wait().await {
2859 Ok(s) => s,
2860 Err(e) => {
2861 return Ok(Some(ExecResult::failure(
2862 1,
2863 format!("{}: failed to wait: {}", name, e),
2864 )));
2865 }
2866 };
2867
2868 if let Some(task) = stdout_task {
2869 let _ = task.await;
2871 }
2872 if let Some(task) = stderr_task {
2873 let _ = task.await;
2874 }
2875
2876 let code = status.code().unwrap_or_else(|| {
2877 #[cfg(unix)]
2878 {
2879 use std::os::unix::process::ExitStatusExt;
2880 128 + status.signal().unwrap_or(0)
2881 }
2882 #[cfg(not(unix))]
2883 {
2884 -1
2885 }
2886 }) as i64;
2887
2888 let stdout = stdout_stream.read_string().await;
2889 let stderr = stderr_stream.read_string().await;
2890
2891 Ok(Some(ExecResult::from_output(code, stdout, stderr)))
2892 }
2893 }
2894
2895 pub async fn get_var(&self, name: &str) -> Option<Value> {
2899 let scope = self.scope.read().await;
2900 scope.get(name).cloned()
2901 }
2902
2903 #[cfg(test)]
2905 pub async fn error_exit_enabled(&self) -> bool {
2906 let scope = self.scope.read().await;
2907 scope.error_exit_enabled()
2908 }
2909
2910 pub async fn set_var(&self, name: &str, value: Value) {
2912 let mut scope = self.scope.write().await;
2913 scope.set(name.to_string(), value);
2914 }
2915
2916 pub async fn set_positional(&self, script_name: impl Into<String>, args: Vec<String>) {
2918 let mut scope = self.scope.write().await;
2919 scope.set_positional(script_name, args);
2920 }
2921
2922 pub async fn list_vars(&self) -> Vec<(String, Value)> {
2924 let scope = self.scope.read().await;
2925 scope.all()
2926 }
2927
2928 pub async fn cwd(&self) -> PathBuf {
2932 self.exec_ctx.read().await.cwd.clone()
2933 }
2934
2935 pub async fn set_cwd(&self, path: PathBuf) {
2937 let mut ctx = self.exec_ctx.write().await;
2938 ctx.set_cwd(path);
2939 }
2940
2941 pub async fn last_result(&self) -> ExecResult {
2945 let scope = self.scope.read().await;
2946 scope.last_result().clone()
2947 }
2948
2949 pub async fn has_function(&self, name: &str) -> bool {
2953 self.user_tools.read().await.contains_key(name)
2954 }
2955
2956 pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
2958 self.tools.schemas()
2959 }
2960
2961 pub fn jobs(&self) -> Arc<JobManager> {
2965 self.jobs.clone()
2966 }
2967
2968 pub fn vfs(&self) -> Arc<VfsRouter> {
2972 self.vfs.clone()
2973 }
2974
2975 pub async fn reset(&self) -> Result<()> {
2982 {
2983 let mut scope = self.scope.write().await;
2984 *scope = Scope::new();
2985 }
2986 {
2987 let mut ctx = self.exec_ctx.write().await;
2988 ctx.cwd = PathBuf::from("/");
2989 }
2990 Ok(())
2991 }
2992
2993 pub async fn shutdown(self) -> Result<()> {
2995 self.jobs.wait_all().await;
2997 Ok(())
2998 }
2999
3000 async fn dispatch_command(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
3011 {
3013 let mut scope = self.scope.write().await;
3014 *scope = ctx.scope.clone();
3015 }
3016 {
3017 let mut ec = self.exec_ctx.write().await;
3018 ec.cwd = ctx.cwd.clone();
3019 ec.prev_cwd = ctx.prev_cwd.clone();
3020 ec.stdin = ctx.stdin.take();
3021 ec.stdin_data = ctx.stdin_data.take();
3022 ec.aliases = ctx.aliases.clone();
3023 ec.ignore_config = ctx.ignore_config.clone();
3024 ec.output_limit = ctx.output_limit.clone();
3025 ec.pipeline_position = ctx.pipeline_position;
3026 }
3027
3028 let result = self.execute_command(&cmd.name, &cmd.args).await?;
3030
3031 {
3033 let scope = self.scope.read().await;
3034 ctx.scope = scope.clone();
3035 }
3036 {
3037 let ec = self.exec_ctx.read().await;
3038 ctx.cwd = ec.cwd.clone();
3039 ctx.prev_cwd = ec.prev_cwd.clone();
3040 ctx.aliases = ec.aliases.clone();
3041 ctx.ignore_config = ec.ignore_config.clone();
3042 ctx.output_limit = ec.output_limit.clone();
3043 }
3044
3045 Ok(result)
3046 }
3047}
3048
3049#[async_trait]
3050impl CommandDispatcher for Kernel {
3051 async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
3057 self.dispatch_command(cmd, ctx).await
3058 }
3059}
3060
3061fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
3067 if accumulated.out.is_empty() {
3071 if let Some(ref output) = accumulated.output {
3072 accumulated.out = output.to_canonical_string();
3073 accumulated.output = None;
3074 }
3075 }
3076 let new_text = new.text_out();
3077 if !accumulated.out.is_empty() && !new_text.is_empty() && !accumulated.out.ends_with('\n') {
3078 accumulated.out.push('\n');
3079 }
3080 accumulated.out.push_str(&new_text);
3081 if !accumulated.err.is_empty() && !new.err.is_empty() && !accumulated.err.ends_with('\n') {
3082 accumulated.err.push('\n');
3083 }
3084 accumulated.err.push_str(&new.err);
3085 accumulated.code = new.code;
3086 accumulated.data = new.data.clone();
3087}
3088
3089fn is_truthy(value: &Value) -> bool {
3091 match value {
3092 Value::Null => false,
3093 Value::Bool(b) => *b,
3094 Value::Int(i) => *i != 0,
3095 Value::Float(f) => *f != 0.0,
3096 Value::String(s) => !s.is_empty(),
3097 Value::Json(json) => match json {
3098 serde_json::Value::Null => false,
3099 serde_json::Value::Array(arr) => !arr.is_empty(),
3100 serde_json::Value::Object(obj) => !obj.is_empty(),
3101 serde_json::Value::Bool(b) => *b,
3102 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
3103 serde_json::Value::String(s) => !s.is_empty(),
3104 },
3105 Value::Blob(_) => true, }
3107}
3108
3109fn apply_tilde_expansion(value: Value) -> Value {
3113 match value {
3114 Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s)),
3115 _ => value,
3116 }
3117}
3118
3119#[cfg(test)]
3120mod tests {
3121 use super::*;
3122
3123 #[tokio::test]
3124 async fn test_kernel_transient() {
3125 let kernel = Kernel::transient().expect("failed to create kernel");
3126 assert_eq!(kernel.name(), "transient");
3127 }
3128
3129 #[tokio::test]
3130 async fn test_kernel_execute_echo() {
3131 let kernel = Kernel::transient().expect("failed to create kernel");
3132 let result = kernel.execute("echo hello").await.expect("execution failed");
3133 assert!(result.ok());
3134 assert_eq!(result.out.trim(), "hello");
3135 }
3136
3137 #[tokio::test]
3138 async fn test_multiple_statements_accumulate_output() {
3139 let kernel = Kernel::transient().expect("failed to create kernel");
3140 let result = kernel
3141 .execute("echo one\necho two\necho three")
3142 .await
3143 .expect("execution failed");
3144 assert!(result.ok());
3145 assert!(result.out.contains("one"), "missing 'one': {}", result.out);
3147 assert!(result.out.contains("two"), "missing 'two': {}", result.out);
3148 assert!(result.out.contains("three"), "missing 'three': {}", result.out);
3149 }
3150
3151 #[tokio::test]
3152 async fn test_and_chain_accumulates_output() {
3153 let kernel = Kernel::transient().expect("failed to create kernel");
3154 let result = kernel
3155 .execute("echo first && echo second")
3156 .await
3157 .expect("execution failed");
3158 assert!(result.ok());
3159 assert!(result.out.contains("first"), "missing 'first': {}", result.out);
3160 assert!(result.out.contains("second"), "missing 'second': {}", result.out);
3161 }
3162
3163 #[tokio::test]
3164 async fn test_for_loop_accumulates_output() {
3165 let kernel = Kernel::transient().expect("failed to create kernel");
3166 let result = kernel
3167 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
3168 .await
3169 .expect("execution failed");
3170 assert!(result.ok());
3171 assert!(result.out.contains("item: a"), "missing 'item: a': {}", result.out);
3172 assert!(result.out.contains("item: b"), "missing 'item: b': {}", result.out);
3173 assert!(result.out.contains("item: c"), "missing 'item: c': {}", result.out);
3174 }
3175
3176 #[tokio::test]
3177 async fn test_while_loop_accumulates_output() {
3178 let kernel = Kernel::transient().expect("failed to create kernel");
3179 let result = kernel
3180 .execute(r#"
3181 N=3
3182 while [[ ${N} -gt 0 ]]; do
3183 echo "N=${N}"
3184 N=$((N - 1))
3185 done
3186 "#)
3187 .await
3188 .expect("execution failed");
3189 assert!(result.ok());
3190 assert!(result.out.contains("N=3"), "missing 'N=3': {}", result.out);
3191 assert!(result.out.contains("N=2"), "missing 'N=2': {}", result.out);
3192 assert!(result.out.contains("N=1"), "missing 'N=1': {}", result.out);
3193 }
3194
3195 #[tokio::test]
3196 async fn test_kernel_set_var() {
3197 let kernel = Kernel::transient().expect("failed to create kernel");
3198
3199 kernel.execute("X=42").await.expect("set failed");
3200
3201 let value = kernel.get_var("X").await;
3202 assert_eq!(value, Some(Value::Int(42)));
3203 }
3204
3205 #[tokio::test]
3206 async fn test_kernel_var_expansion() {
3207 let kernel = Kernel::transient().expect("failed to create kernel");
3208
3209 kernel.execute("NAME=\"world\"").await.expect("set failed");
3210 let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
3211
3212 assert!(result.ok());
3213 assert_eq!(result.out.trim(), "hello world");
3214 }
3215
3216 #[tokio::test]
3217 async fn test_kernel_last_result() {
3218 let kernel = Kernel::transient().expect("failed to create kernel");
3219
3220 kernel.execute("echo test").await.expect("echo failed");
3221
3222 let last = kernel.last_result().await;
3223 assert!(last.ok());
3224 assert_eq!(last.out.trim(), "test");
3225 }
3226
3227 #[tokio::test]
3228 async fn test_kernel_tool_not_found() {
3229 let kernel = Kernel::transient().expect("failed to create kernel");
3230
3231 let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
3232 assert!(!result.ok());
3233 assert_eq!(result.code, 127);
3234 assert!(result.err.contains("command not found"));
3235 }
3236
3237 #[tokio::test]
3238 async fn test_external_command_true() {
3239 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
3241
3242 let result = kernel.execute("true").await.expect("execution failed");
3244 assert!(result.ok(), "true should succeed: {:?}", result);
3246 }
3247
3248 #[tokio::test]
3249 async fn test_external_command_basic() {
3250 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
3252
3253 let path_var = std::env::var("PATH").unwrap_or_default();
3258 eprintln!("System PATH: {}", path_var);
3259
3260 kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
3262
3263 let result = kernel.execute("uname").await.expect("execution failed");
3266 eprintln!("uname result: {:?}", result);
3267 assert!(result.ok() || result.code == 127, "uname: {:?}", result);
3269 }
3270
3271 #[tokio::test]
3272 async fn test_kernel_reset() {
3273 let kernel = Kernel::transient().expect("failed to create kernel");
3274
3275 kernel.execute("X=1").await.expect("set failed");
3276 assert!(kernel.get_var("X").await.is_some());
3277
3278 kernel.reset().await.expect("reset failed");
3279 assert!(kernel.get_var("X").await.is_none());
3280 }
3281
3282 #[tokio::test]
3283 async fn test_kernel_cwd() {
3284 let kernel = Kernel::transient().expect("failed to create kernel");
3285
3286 let cwd = kernel.cwd().await;
3288 let home = std::env::var("HOME")
3289 .map(PathBuf::from)
3290 .unwrap_or_else(|_| PathBuf::from("/"));
3291 assert_eq!(cwd, home);
3292
3293 kernel.set_cwd(PathBuf::from("/tmp")).await;
3294 assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
3295 }
3296
3297 #[tokio::test]
3298 async fn test_kernel_list_vars() {
3299 let kernel = Kernel::transient().expect("failed to create kernel");
3300
3301 kernel.execute("A=1").await.ok();
3302 kernel.execute("B=2").await.ok();
3303
3304 let vars = kernel.list_vars().await;
3305 assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
3306 assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
3307 }
3308
3309 #[tokio::test]
3310 async fn test_is_truthy() {
3311 assert!(!is_truthy(&Value::Null));
3312 assert!(!is_truthy(&Value::Bool(false)));
3313 assert!(is_truthy(&Value::Bool(true)));
3314 assert!(!is_truthy(&Value::Int(0)));
3315 assert!(is_truthy(&Value::Int(1)));
3316 assert!(!is_truthy(&Value::String("".into())));
3317 assert!(is_truthy(&Value::String("x".into())));
3318 }
3319
3320 #[tokio::test]
3321 async fn test_jq_in_pipeline() {
3322 let kernel = Kernel::transient().expect("failed to create kernel");
3323 let result = kernel
3325 .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
3326 .await
3327 .expect("execution failed");
3328 assert!(result.ok(), "jq pipeline failed: {}", result.err);
3329 assert_eq!(result.out.trim(), "Alice");
3330 }
3331
3332 #[tokio::test]
3333 async fn test_user_defined_tool() {
3334 let kernel = Kernel::transient().expect("failed to create kernel");
3335
3336 kernel
3338 .execute(r#"greet() { echo "Hello, $1!" }"#)
3339 .await
3340 .expect("function definition failed");
3341
3342 let result = kernel
3344 .execute(r#"greet "World""#)
3345 .await
3346 .expect("function call failed");
3347
3348 assert!(result.ok(), "greet failed: {}", result.err);
3349 assert_eq!(result.out.trim(), "Hello, World!");
3350 }
3351
3352 #[tokio::test]
3353 async fn test_user_tool_positional_args() {
3354 let kernel = Kernel::transient().expect("failed to create kernel");
3355
3356 kernel
3358 .execute(r#"greet() { echo "Hi $1" }"#)
3359 .await
3360 .expect("function definition failed");
3361
3362 let result = kernel
3364 .execute(r#"greet "Amy""#)
3365 .await
3366 .expect("function call failed");
3367
3368 assert!(result.ok(), "greet failed: {}", result.err);
3369 assert_eq!(result.out.trim(), "Hi Amy");
3370 }
3371
3372 #[tokio::test]
3373 async fn test_function_shared_scope() {
3374 let kernel = Kernel::transient().expect("failed to create kernel");
3375
3376 kernel
3378 .execute(r#"SECRET="hidden""#)
3379 .await
3380 .expect("set failed");
3381
3382 kernel
3384 .execute(r#"access_parent() {
3385 echo "${SECRET}"
3386 SECRET="modified"
3387 }"#)
3388 .await
3389 .expect("function definition failed");
3390
3391 let result = kernel.execute("access_parent").await.expect("function call failed");
3393
3394 assert!(
3396 result.out.contains("hidden"),
3397 "Function should access parent scope, got: {}",
3398 result.out
3399 );
3400
3401 let secret = kernel.get_var("SECRET").await;
3403 assert_eq!(
3404 secret,
3405 Some(Value::String("modified".into())),
3406 "Function should modify parent scope"
3407 );
3408 }
3409
3410 #[tokio::test]
3411 async fn test_exec_builtin() {
3412 let kernel = Kernel::transient().expect("failed to create kernel");
3413 let result = kernel
3415 .execute(r#"exec command="/bin/echo" argv="hello world""#)
3416 .await
3417 .expect("exec failed");
3418
3419 assert!(result.ok(), "exec failed: {}", result.err);
3420 assert_eq!(result.out.trim(), "hello world");
3421 }
3422
3423 #[tokio::test]
3424 async fn test_while_false_never_runs() {
3425 let kernel = Kernel::transient().expect("failed to create kernel");
3426
3427 let result = kernel
3429 .execute(r#"
3430 while false; do
3431 echo "should not run"
3432 done
3433 "#)
3434 .await
3435 .expect("while false failed");
3436
3437 assert!(result.ok());
3438 assert!(result.out.is_empty(), "while false should not execute body: {}", result.out);
3439 }
3440
3441 #[tokio::test]
3442 async fn test_while_string_comparison() {
3443 let kernel = Kernel::transient().expect("failed to create kernel");
3444
3445 kernel.execute(r#"FLAG="go""#).await.expect("set failed");
3447
3448 let result = kernel
3451 .execute(r#"
3452 while [[ ${FLAG} == "go" ]]; do
3453 FLAG="stop"
3454 echo "running"
3455 done
3456 "#)
3457 .await
3458 .expect("while with string cmp failed");
3459
3460 assert!(result.ok());
3461 assert!(result.out.contains("running"), "should have run once: {}", result.out);
3462
3463 let flag = kernel.get_var("FLAG").await;
3465 assert_eq!(flag, Some(Value::String("stop".into())));
3466 }
3467
3468 #[tokio::test]
3469 async fn test_while_numeric_comparison() {
3470 let kernel = Kernel::transient().expect("failed to create kernel");
3471
3472 kernel.execute("N=5").await.expect("set failed");
3474
3475 let result = kernel
3477 .execute(r#"
3478 while [[ ${N} -gt 3 ]]; do
3479 N=3
3480 echo "N was greater"
3481 done
3482 "#)
3483 .await
3484 .expect("while with > failed");
3485
3486 assert!(result.ok());
3487 assert!(result.out.contains("N was greater"), "should have run once: {}", result.out);
3488 }
3489
3490 #[tokio::test]
3491 async fn test_break_in_while_loop() {
3492 let kernel = Kernel::transient().expect("failed to create kernel");
3493
3494 let result = kernel
3495 .execute(r#"
3496 I=0
3497 while true; do
3498 I=1
3499 echo "before break"
3500 break
3501 echo "after break"
3502 done
3503 "#)
3504 .await
3505 .expect("while with break failed");
3506
3507 assert!(result.ok());
3508 assert!(result.out.contains("before break"), "should see before break: {}", result.out);
3509 assert!(!result.out.contains("after break"), "should not see after break: {}", result.out);
3510
3511 let i = kernel.get_var("I").await;
3513 assert_eq!(i, Some(Value::Int(1)));
3514 }
3515
3516 #[tokio::test]
3517 async fn test_continue_in_while_loop() {
3518 let kernel = Kernel::transient().expect("failed to create kernel");
3519
3520 let result = kernel
3525 .execute(r#"
3526 STATE="start"
3527 AFTER_CONTINUE="no"
3528 while [[ ${STATE} != "done" ]]; do
3529 if [[ ${STATE} == "start" ]]; then
3530 STATE="middle"
3531 continue
3532 AFTER_CONTINUE="yes"
3533 fi
3534 if [[ ${STATE} == "middle" ]]; then
3535 STATE="done"
3536 fi
3537 done
3538 "#)
3539 .await
3540 .expect("while with continue failed");
3541
3542 assert!(result.ok());
3543
3544 let state = kernel.get_var("STATE").await;
3546 assert_eq!(state, Some(Value::String("done".into())));
3547
3548 let after = kernel.get_var("AFTER_CONTINUE").await;
3550 assert_eq!(after, Some(Value::String("no".into())));
3551 }
3552
3553 #[tokio::test]
3554 async fn test_break_with_level() {
3555 let kernel = Kernel::transient().expect("failed to create kernel");
3556
3557 let result = kernel
3562 .execute(r#"
3563 OUTER=0
3564 while true; do
3565 OUTER=1
3566 for X in "1 2"; do
3567 break 2
3568 done
3569 OUTER=2
3570 done
3571 "#)
3572 .await
3573 .expect("nested break failed");
3574
3575 assert!(result.ok());
3576
3577 let outer = kernel.get_var("OUTER").await;
3579 assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
3580 }
3581
3582 #[tokio::test]
3583 async fn test_return_from_tool() {
3584 let kernel = Kernel::transient().expect("failed to create kernel");
3585
3586 kernel
3588 .execute(r#"early_return() {
3589 if [[ $1 == 1 ]]; then
3590 return 42
3591 fi
3592 echo "not returned"
3593 }"#)
3594 .await
3595 .expect("function definition failed");
3596
3597 let result = kernel
3600 .execute("early_return 1")
3601 .await
3602 .expect("function call failed");
3603
3604 assert_eq!(result.code, 42);
3606 assert!(result.out.is_empty());
3608 }
3609
3610 #[tokio::test]
3611 async fn test_return_without_value() {
3612 let kernel = Kernel::transient().expect("failed to create kernel");
3613
3614 kernel
3616 .execute(r#"early_exit() {
3617 if [[ $1 == "stop" ]]; then
3618 return
3619 fi
3620 echo "continued"
3621 }"#)
3622 .await
3623 .expect("function definition failed");
3624
3625 let result = kernel
3627 .execute(r#"early_exit "stop""#)
3628 .await
3629 .expect("function call failed");
3630
3631 assert!(result.ok());
3632 assert!(result.out.is_empty() || result.out.trim().is_empty());
3633 }
3634
3635 #[tokio::test]
3636 async fn test_exit_stops_execution() {
3637 let kernel = Kernel::transient().expect("failed to create kernel");
3638
3639 kernel
3641 .execute(r#"
3642 BEFORE="yes"
3643 exit 0
3644 AFTER="yes"
3645 "#)
3646 .await
3647 .expect("execution failed");
3648
3649 let before = kernel.get_var("BEFORE").await;
3651 assert_eq!(before, Some(Value::String("yes".into())));
3652
3653 let after = kernel.get_var("AFTER").await;
3654 assert!(after.is_none(), "AFTER should not be set after exit");
3655 }
3656
3657 #[tokio::test]
3658 async fn test_exit_with_code() {
3659 let kernel = Kernel::transient().expect("failed to create kernel");
3660
3661 let result = kernel
3663 .execute("exit 42")
3664 .await
3665 .expect("exit failed");
3666
3667 assert_eq!(result.code, 42);
3668 assert!(result.out.is_empty(), "exit should not produce stdout");
3669 }
3670
3671 #[tokio::test]
3672 async fn test_set_e_stops_on_failure() {
3673 let kernel = Kernel::transient().expect("failed to create kernel");
3674
3675 kernel.execute("set -e").await.expect("set -e failed");
3677
3678 kernel
3680 .execute(r#"
3681 STEP1="done"
3682 false
3683 STEP2="done"
3684 "#)
3685 .await
3686 .expect("execution failed");
3687
3688 let step1 = kernel.get_var("STEP1").await;
3690 assert_eq!(step1, Some(Value::String("done".into())));
3691
3692 let step2 = kernel.get_var("STEP2").await;
3693 assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
3694 }
3695
3696 #[tokio::test]
3697 async fn test_set_plus_e_disables_error_exit() {
3698 let kernel = Kernel::transient().expect("failed to create kernel");
3699
3700 kernel.execute("set -e").await.expect("set -e failed");
3702 kernel.execute("set +e").await.expect("set +e failed");
3703
3704 kernel
3706 .execute(r#"
3707 STEP1="done"
3708 false
3709 STEP2="done"
3710 "#)
3711 .await
3712 .expect("execution failed");
3713
3714 let step1 = kernel.get_var("STEP1").await;
3716 assert_eq!(step1, Some(Value::String("done".into())));
3717
3718 let step2 = kernel.get_var("STEP2").await;
3719 assert_eq!(step2, Some(Value::String("done".into())));
3720 }
3721
3722 #[tokio::test]
3723 async fn test_set_ignores_unknown_options() {
3724 let kernel = Kernel::transient().expect("failed to create kernel");
3725
3726 let result = kernel
3728 .execute("set -e -u -o pipefail")
3729 .await
3730 .expect("set with unknown options failed");
3731
3732 assert!(result.ok(), "set should succeed with unknown options");
3733
3734 kernel
3736 .execute(r#"
3737 BEFORE="yes"
3738 false
3739 AFTER="yes"
3740 "#)
3741 .await
3742 .ok();
3743
3744 let after = kernel.get_var("AFTER").await;
3745 assert!(after.is_none(), "-e should be enabled despite unknown options");
3746 }
3747
3748 #[tokio::test]
3749 async fn test_set_no_args_shows_settings() {
3750 let kernel = Kernel::transient().expect("failed to create kernel");
3751
3752 kernel.execute("set -e").await.expect("set -e failed");
3754
3755 let result = kernel.execute("set").await.expect("set failed");
3757
3758 assert!(result.ok());
3759 assert!(result.out.contains("set -e"), "should show -e is enabled: {}", result.out);
3760 }
3761
3762 #[tokio::test]
3763 async fn test_set_e_in_pipeline() {
3764 let kernel = Kernel::transient().expect("failed to create kernel");
3765
3766 kernel.execute("set -e").await.expect("set -e failed");
3767
3768 kernel
3770 .execute(r#"
3771 BEFORE="yes"
3772 false | cat
3773 AFTER="yes"
3774 "#)
3775 .await
3776 .ok();
3777
3778 let before = kernel.get_var("BEFORE").await;
3779 assert_eq!(before, Some(Value::String("yes".into())));
3780
3781 }
3786
3787 #[tokio::test]
3788 async fn test_set_e_with_and_chain() {
3789 let kernel = Kernel::transient().expect("failed to create kernel");
3790
3791 kernel.execute("set -e").await.expect("set -e failed");
3792
3793 kernel
3796 .execute(r#"
3797 RESULT="initial"
3798 false && RESULT="chained"
3799 RESULT="continued"
3800 "#)
3801 .await
3802 .ok();
3803
3804 let result = kernel.get_var("RESULT").await;
3807 assert!(result.is_some(), "RESULT should be set");
3810 }
3811
3812 #[tokio::test]
3813 async fn test_set_e_exits_in_for_loop() {
3814 let kernel = Kernel::transient().expect("failed to create kernel");
3815
3816 kernel.execute("set -e").await.expect("set -e failed");
3817
3818 kernel
3819 .execute(r#"
3820 REACHED="no"
3821 for x in 1 2 3; do
3822 false
3823 REACHED="yes"
3824 done
3825 "#)
3826 .await
3827 .ok();
3828
3829 let reached = kernel.get_var("REACHED").await;
3831 assert_eq!(reached, Some(Value::String("no".into())),
3832 "set -e should exit on failure in for loop body");
3833 }
3834
3835 #[tokio::test]
3836 async fn test_for_loop_continues_without_set_e() {
3837 let kernel = Kernel::transient().expect("failed to create kernel");
3838
3839 kernel
3841 .execute(r#"
3842 COUNT=0
3843 for x in 1 2 3; do
3844 false
3845 COUNT=$((COUNT + 1))
3846 done
3847 "#)
3848 .await
3849 .ok();
3850
3851 let count = kernel.get_var("COUNT").await;
3852 let count_val = match &count {
3854 Some(Value::Int(n)) => *n,
3855 Some(Value::String(s)) => s.parse().unwrap_or(-1),
3856 _ => -1,
3857 };
3858 assert_eq!(count_val, 3,
3859 "without set -e, loop should complete all iterations (got {:?})", count);
3860 }
3861
3862 #[tokio::test]
3867 async fn test_source_sets_variables() {
3868 let kernel = Kernel::transient().expect("failed to create kernel");
3869
3870 kernel
3872 .execute(r#"write "/test.kai" 'FOO="bar"'"#)
3873 .await
3874 .expect("write failed");
3875
3876 let result = kernel
3878 .execute(r#"source "/test.kai""#)
3879 .await
3880 .expect("source failed");
3881
3882 assert!(result.ok(), "source should succeed");
3883
3884 let foo = kernel.get_var("FOO").await;
3886 assert_eq!(foo, Some(Value::String("bar".into())));
3887 }
3888
3889 #[tokio::test]
3890 async fn test_source_with_dot_alias() {
3891 let kernel = Kernel::transient().expect("failed to create kernel");
3892
3893 kernel
3895 .execute(r#"write "/vars.kai" 'X=42'"#)
3896 .await
3897 .expect("write failed");
3898
3899 let result = kernel
3901 .execute(r#". "/vars.kai""#)
3902 .await
3903 .expect(". failed");
3904
3905 assert!(result.ok(), ". should succeed");
3906
3907 let x = kernel.get_var("X").await;
3909 assert_eq!(x, Some(Value::Int(42)));
3910 }
3911
3912 #[tokio::test]
3913 async fn test_source_not_found() {
3914 let kernel = Kernel::transient().expect("failed to create kernel");
3915
3916 let result = kernel
3918 .execute(r#"source "/nonexistent.kai""#)
3919 .await
3920 .expect("source should not fail with error");
3921
3922 assert!(!result.ok(), "source of non-existent file should fail");
3923 assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
3924 }
3925
3926 #[tokio::test]
3927 async fn test_source_missing_filename() {
3928 let kernel = Kernel::transient().expect("failed to create kernel");
3929
3930 let result = kernel
3932 .execute("source")
3933 .await
3934 .expect("source should not fail with error");
3935
3936 assert!(!result.ok(), "source without filename should fail");
3937 assert!(result.err.contains("missing filename"), "error should mention missing filename");
3938 }
3939
3940 #[tokio::test]
3941 async fn test_source_executes_multiple_statements() {
3942 let kernel = Kernel::transient().expect("failed to create kernel");
3943
3944 kernel
3946 .execute(r#"write "/multi.kai" 'A=1
3947B=2
3948C=3'"#)
3949 .await
3950 .expect("write failed");
3951
3952 kernel
3954 .execute(r#"source "/multi.kai""#)
3955 .await
3956 .expect("source failed");
3957
3958 assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
3960 assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
3961 assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
3962 }
3963
3964 #[tokio::test]
3965 async fn test_source_can_define_functions() {
3966 let kernel = Kernel::transient().expect("failed to create kernel");
3967
3968 kernel
3970 .execute(r#"write "/functions.kai" 'greet() {
3971 echo "Hello, $1!"
3972}'"#)
3973 .await
3974 .expect("write failed");
3975
3976 kernel
3978 .execute(r#"source "/functions.kai""#)
3979 .await
3980 .expect("source failed");
3981
3982 let result = kernel
3984 .execute(r#"greet "World""#)
3985 .await
3986 .expect("greet failed");
3987
3988 assert!(result.ok());
3989 assert!(result.out.contains("Hello, World!"));
3990 }
3991
3992 #[tokio::test]
3993 async fn test_source_inherits_error_exit() {
3994 let kernel = Kernel::transient().expect("failed to create kernel");
3995
3996 kernel.execute("set -e").await.expect("set -e failed");
3998
3999 kernel
4001 .execute(r#"write "/fail.kai" 'BEFORE="yes"
4002false
4003AFTER="yes"'"#)
4004 .await
4005 .expect("write failed");
4006
4007 kernel
4009 .execute(r#"source "/fail.kai""#)
4010 .await
4011 .ok();
4012
4013 let before = kernel.get_var("BEFORE").await;
4015 assert_eq!(before, Some(Value::String("yes".into())));
4016
4017 }
4020
4021 #[tokio::test]
4026 async fn test_set_e_and_chain_left_fails() {
4027 let kernel = Kernel::transient().expect("failed to create kernel");
4029 kernel.execute("set -e").await.expect("set -e failed");
4030
4031 kernel
4032 .execute("false && echo hi; REACHED=1")
4033 .await
4034 .expect("execution failed");
4035
4036 let reached = kernel.get_var("REACHED").await;
4037 assert_eq!(
4038 reached,
4039 Some(Value::Int(1)),
4040 "set -e should not trigger on left side of &&"
4041 );
4042 }
4043
4044 #[tokio::test]
4045 async fn test_set_e_and_chain_right_fails() {
4046 let kernel = Kernel::transient().expect("failed to create kernel");
4048 kernel.execute("set -e").await.expect("set -e failed");
4049
4050 kernel
4051 .execute("true && false; REACHED=1")
4052 .await
4053 .expect("execution failed");
4054
4055 let reached = kernel.get_var("REACHED").await;
4056 assert!(
4057 reached.is_none(),
4058 "set -e should trigger when right side of && fails"
4059 );
4060 }
4061
4062 #[tokio::test]
4063 async fn test_set_e_or_chain_recovers() {
4064 let kernel = Kernel::transient().expect("failed to create kernel");
4066 kernel.execute("set -e").await.expect("set -e failed");
4067
4068 kernel
4069 .execute("false || echo recovered; REACHED=1")
4070 .await
4071 .expect("execution failed");
4072
4073 let reached = kernel.get_var("REACHED").await;
4074 assert_eq!(
4075 reached,
4076 Some(Value::Int(1)),
4077 "set -e should not trigger when || recovers the failure"
4078 );
4079 }
4080
4081 #[tokio::test]
4082 async fn test_set_e_or_chain_both_fail() {
4083 let kernel = Kernel::transient().expect("failed to create kernel");
4085 kernel.execute("set -e").await.expect("set -e failed");
4086
4087 kernel
4088 .execute("false || false; REACHED=1")
4089 .await
4090 .expect("execution failed");
4091
4092 let reached = kernel.get_var("REACHED").await;
4093 assert!(
4094 reached.is_none(),
4095 "set -e should trigger when || chain ultimately fails"
4096 );
4097 }
4098
4099 fn schedule_cancel(kernel: &Arc<Kernel>, delay: std::time::Duration) {
4106 let k = Arc::clone(kernel);
4107 std::thread::spawn(move || {
4108 std::thread::sleep(delay);
4109 k.cancel();
4110 });
4111 }
4112
4113 #[tokio::test]
4114 async fn test_cancel_interrupts_for_loop() {
4115 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
4116
4117 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
4119
4120 let result = kernel
4121 .execute("for i in $(seq 1 100000); do X=$i; done")
4122 .await
4123 .expect("execute failed");
4124
4125 assert_eq!(result.code, 130, "cancelled execution should exit with code 130");
4126
4127 let x = kernel.get_var("X").await;
4129 if let Some(Value::Int(n)) = x {
4130 assert!(n < 100000, "loop should have been interrupted before finishing, got X={n}");
4131 }
4132 }
4133
4134 #[tokio::test]
4135 async fn test_cancel_interrupts_while_loop() {
4136 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
4137 kernel.execute("COUNT=0").await.expect("init failed");
4138
4139 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
4140
4141 let result = kernel
4142 .execute("while true; do COUNT=$((COUNT + 1)); done")
4143 .await
4144 .expect("execute failed");
4145
4146 assert_eq!(result.code, 130);
4147
4148 let count = kernel.get_var("COUNT").await;
4149 if let Some(Value::Int(n)) = count {
4150 assert!(n > 0, "loop should have run at least once");
4151 }
4152 }
4153
4154 #[tokio::test]
4155 async fn test_reset_after_cancel() {
4156 let kernel = Kernel::transient().expect("failed to create kernel");
4158 kernel.cancel(); let result = kernel.execute("echo hello").await.expect("execute failed");
4161 assert!(result.ok(), "execute after cancel should succeed");
4162 assert_eq!(result.out.trim(), "hello");
4163 }
4164
4165 #[tokio::test]
4166 async fn test_cancel_interrupts_statement_sequence() {
4167 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
4168
4169 schedule_cancel(&kernel, std::time::Duration::from_millis(50));
4171
4172 let result = kernel
4173 .execute("STEP=1; sleep 5; STEP=2; sleep 5; STEP=3")
4174 .await
4175 .expect("execute failed");
4176
4177 assert_eq!(result.code, 130);
4178
4179 let step = kernel.get_var("STEP").await;
4181 assert_eq!(step, Some(Value::Int(1)), "cancel should stop before STEP=2");
4182 }
4183
4184 #[tokio::test]
4189 async fn test_case_simple_match() {
4190 let kernel = Kernel::transient().expect("failed to create kernel");
4191
4192 let result = kernel
4193 .execute(r#"
4194 case "hello" in
4195 hello) echo "matched hello" ;;
4196 world) echo "matched world" ;;
4197 esac
4198 "#)
4199 .await
4200 .expect("case failed");
4201
4202 assert!(result.ok());
4203 assert_eq!(result.out.trim(), "matched hello");
4204 }
4205
4206 #[tokio::test]
4207 async fn test_case_wildcard_match() {
4208 let kernel = Kernel::transient().expect("failed to create kernel");
4209
4210 let result = kernel
4211 .execute(r#"
4212 case "main.rs" in
4213 *.py) echo "Python" ;;
4214 *.rs) echo "Rust" ;;
4215 *) echo "Unknown" ;;
4216 esac
4217 "#)
4218 .await
4219 .expect("case failed");
4220
4221 assert!(result.ok());
4222 assert_eq!(result.out.trim(), "Rust");
4223 }
4224
4225 #[tokio::test]
4226 async fn test_case_default_match() {
4227 let kernel = Kernel::transient().expect("failed to create kernel");
4228
4229 let result = kernel
4230 .execute(r#"
4231 case "unknown.xyz" in
4232 *.py) echo "Python" ;;
4233 *.rs) echo "Rust" ;;
4234 *) echo "Default" ;;
4235 esac
4236 "#)
4237 .await
4238 .expect("case failed");
4239
4240 assert!(result.ok());
4241 assert_eq!(result.out.trim(), "Default");
4242 }
4243
4244 #[tokio::test]
4245 async fn test_case_no_match() {
4246 let kernel = Kernel::transient().expect("failed to create kernel");
4247
4248 let result = kernel
4250 .execute(r#"
4251 case "nope" in
4252 "yes") echo "yes" ;;
4253 "no") echo "no" ;;
4254 esac
4255 "#)
4256 .await
4257 .expect("case failed");
4258
4259 assert!(result.ok());
4260 assert!(result.out.is_empty(), "no match should produce empty output");
4261 }
4262
4263 #[tokio::test]
4264 async fn test_case_with_variable() {
4265 let kernel = Kernel::transient().expect("failed to create kernel");
4266
4267 kernel.execute(r#"LANG="rust""#).await.expect("set failed");
4268
4269 let result = kernel
4270 .execute(r#"
4271 case ${LANG} in
4272 python) echo "snake" ;;
4273 rust) echo "crab" ;;
4274 go) echo "gopher" ;;
4275 esac
4276 "#)
4277 .await
4278 .expect("case failed");
4279
4280 assert!(result.ok());
4281 assert_eq!(result.out.trim(), "crab");
4282 }
4283
4284 #[tokio::test]
4285 async fn test_case_multiple_patterns() {
4286 let kernel = Kernel::transient().expect("failed to create kernel");
4287
4288 let result = kernel
4289 .execute(r#"
4290 case "yes" in
4291 "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
4292 "n"|"no"|"N"|"NO") echo "negative" ;;
4293 esac
4294 "#)
4295 .await
4296 .expect("case failed");
4297
4298 assert!(result.ok());
4299 assert_eq!(result.out.trim(), "affirmative");
4300 }
4301
4302 #[tokio::test]
4303 async fn test_case_glob_question_mark() {
4304 let kernel = Kernel::transient().expect("failed to create kernel");
4305
4306 let result = kernel
4307 .execute(r#"
4308 case "test1" in
4309 test?) echo "matched test?" ;;
4310 *) echo "default" ;;
4311 esac
4312 "#)
4313 .await
4314 .expect("case failed");
4315
4316 assert!(result.ok());
4317 assert_eq!(result.out.trim(), "matched test?");
4318 }
4319
4320 #[tokio::test]
4321 async fn test_case_char_class() {
4322 let kernel = Kernel::transient().expect("failed to create kernel");
4323
4324 let result = kernel
4325 .execute(r#"
4326 case "Yes" in
4327 [Yy]*) echo "yes-like" ;;
4328 [Nn]*) echo "no-like" ;;
4329 esac
4330 "#)
4331 .await
4332 .expect("case failed");
4333
4334 assert!(result.ok());
4335 assert_eq!(result.out.trim(), "yes-like");
4336 }
4337
4338 #[tokio::test]
4343 async fn test_cat_from_pipeline() {
4344 let kernel = Kernel::transient().expect("failed to create kernel");
4345
4346 let result = kernel
4347 .execute(r#"echo "piped text" | cat"#)
4348 .await
4349 .expect("cat pipeline failed");
4350
4351 assert!(result.ok(), "cat failed: {}", result.err);
4352 assert_eq!(result.out.trim(), "piped text");
4353 }
4354
4355 #[tokio::test]
4356 async fn test_cat_from_pipeline_multiline() {
4357 let kernel = Kernel::transient().expect("failed to create kernel");
4358
4359 let result = kernel
4360 .execute(r#"echo "line1\nline2" | cat -n"#)
4361 .await
4362 .expect("cat pipeline failed");
4363
4364 assert!(result.ok(), "cat failed: {}", result.err);
4365 assert!(result.out.contains("1\t"), "output: {}", result.out);
4366 }
4367
4368 #[tokio::test]
4373 async fn test_heredoc_basic() {
4374 let kernel = Kernel::transient().expect("failed to create kernel");
4375
4376 let result = kernel
4377 .execute("cat <<EOF\nhello\nEOF")
4378 .await
4379 .expect("heredoc failed");
4380
4381 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
4382 assert_eq!(result.out.trim(), "hello");
4383 }
4384
4385 #[tokio::test]
4386 async fn test_arithmetic_in_string() {
4387 let kernel = Kernel::transient().expect("failed to create kernel");
4388
4389 let result = kernel
4390 .execute(r#"echo "result: $((1 + 2))""#)
4391 .await
4392 .expect("arithmetic in string failed");
4393
4394 assert!(result.ok(), "echo failed: {}", result.err);
4395 assert_eq!(result.out.trim(), "result: 3");
4396 }
4397
4398 #[tokio::test]
4399 async fn test_heredoc_multiline() {
4400 let kernel = Kernel::transient().expect("failed to create kernel");
4401
4402 let result = kernel
4403 .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
4404 .await
4405 .expect("heredoc failed");
4406
4407 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
4408 assert!(result.out.contains("line1"), "output: {}", result.out);
4409 assert!(result.out.contains("line2"), "output: {}", result.out);
4410 assert!(result.out.contains("line3"), "output: {}", result.out);
4411 }
4412
4413 #[tokio::test]
4414 async fn test_heredoc_variable_expansion() {
4415 let kernel = Kernel::transient().expect("failed to create kernel");
4417
4418 kernel.execute("GREETING=hello").await.expect("set var");
4419
4420 let result = kernel
4421 .execute("cat <<EOF\n$GREETING world\nEOF")
4422 .await
4423 .expect("heredoc expansion failed");
4424
4425 assert!(result.ok(), "heredoc expansion failed: {}", result.err);
4426 assert_eq!(result.out.trim(), "hello world");
4427 }
4428
4429 #[tokio::test]
4430 async fn test_heredoc_quoted_no_expansion() {
4431 let kernel = Kernel::transient().expect("failed to create kernel");
4433
4434 kernel.execute("GREETING=hello").await.expect("set var");
4435
4436 let result = kernel
4437 .execute("cat <<'EOF'\n$GREETING world\nEOF")
4438 .await
4439 .expect("quoted heredoc failed");
4440
4441 assert!(result.ok(), "quoted heredoc failed: {}", result.err);
4442 assert_eq!(result.out.trim(), "$GREETING world");
4443 }
4444
4445 #[tokio::test]
4446 async fn test_heredoc_default_value_expansion() {
4447 let kernel = Kernel::transient().expect("failed to create kernel");
4449
4450 let result = kernel
4451 .execute("cat <<EOF\n${UNSET:-fallback}\nEOF")
4452 .await
4453 .expect("heredoc default expansion failed");
4454
4455 assert!(result.ok(), "heredoc default expansion failed: {}", result.err);
4456 assert_eq!(result.out.trim(), "fallback");
4457 }
4458
4459 #[tokio::test]
4464 async fn test_read_from_pipeline() {
4465 let kernel = Kernel::transient().expect("failed to create kernel");
4466
4467 let result = kernel
4469 .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
4470 .await
4471 .expect("read pipeline failed");
4472
4473 assert!(result.ok(), "read failed: {}", result.err);
4474 assert!(result.out.contains("Hello, Alice"), "output: {}", result.out);
4475 }
4476
4477 #[tokio::test]
4478 async fn test_read_multiple_vars_from_pipeline() {
4479 let kernel = Kernel::transient().expect("failed to create kernel");
4480
4481 let result = kernel
4482 .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
4483 .await
4484 .expect("read pipeline failed");
4485
4486 assert!(result.ok(), "read failed: {}", result.err);
4487 assert!(result.out.contains("John is 42"), "output: {}", result.out);
4488 }
4489
4490 #[tokio::test]
4495 async fn test_posix_function_with_positional_params() {
4496 let kernel = Kernel::transient().expect("failed to create kernel");
4497
4498 kernel
4500 .execute(r#"greet() { echo "Hello, $1!" }"#)
4501 .await
4502 .expect("function definition failed");
4503
4504 let result = kernel
4506 .execute(r#"greet "Amy""#)
4507 .await
4508 .expect("function call failed");
4509
4510 assert!(result.ok(), "greet failed: {}", result.err);
4511 assert_eq!(result.out.trim(), "Hello, Amy!");
4512 }
4513
4514 #[tokio::test]
4515 async fn test_posix_function_multiple_args() {
4516 let kernel = Kernel::transient().expect("failed to create kernel");
4517
4518 kernel
4520 .execute(r#"add_greeting() { echo "$1 $2!" }"#)
4521 .await
4522 .expect("function definition failed");
4523
4524 let result = kernel
4526 .execute(r#"add_greeting "Hello" "World""#)
4527 .await
4528 .expect("function call failed");
4529
4530 assert!(result.ok(), "function failed: {}", result.err);
4531 assert_eq!(result.out.trim(), "Hello World!");
4532 }
4533
4534 #[tokio::test]
4535 async fn test_bash_function_with_positional_params() {
4536 let kernel = Kernel::transient().expect("failed to create kernel");
4537
4538 kernel
4540 .execute(r#"function greet { echo "Hi $1" }"#)
4541 .await
4542 .expect("function definition failed");
4543
4544 let result = kernel
4546 .execute(r#"greet "Bob""#)
4547 .await
4548 .expect("function call failed");
4549
4550 assert!(result.ok(), "greet failed: {}", result.err);
4551 assert_eq!(result.out.trim(), "Hi Bob");
4552 }
4553
4554 #[tokio::test]
4555 async fn test_shell_function_with_all_args() {
4556 let kernel = Kernel::transient().expect("failed to create kernel");
4557
4558 kernel
4560 .execute(r#"echo_all() { echo "args: $@" }"#)
4561 .await
4562 .expect("function definition failed");
4563
4564 let result = kernel
4566 .execute(r#"echo_all "a" "b" "c""#)
4567 .await
4568 .expect("function call failed");
4569
4570 assert!(result.ok(), "function failed: {}", result.err);
4571 assert_eq!(result.out.trim(), "args: a b c");
4572 }
4573
4574 #[tokio::test]
4575 async fn test_shell_function_with_arg_count() {
4576 let kernel = Kernel::transient().expect("failed to create kernel");
4577
4578 kernel
4580 .execute(r#"count_args() { echo "count: $#" }"#)
4581 .await
4582 .expect("function definition failed");
4583
4584 let result = kernel
4586 .execute(r#"count_args "x" "y" "z""#)
4587 .await
4588 .expect("function call failed");
4589
4590 assert!(result.ok(), "function failed: {}", result.err);
4591 assert_eq!(result.out.trim(), "count: 3");
4592 }
4593
4594 #[tokio::test]
4595 async fn test_shell_function_shared_scope() {
4596 let kernel = Kernel::transient().expect("failed to create kernel");
4597
4598 kernel
4600 .execute(r#"PARENT_VAR="visible""#)
4601 .await
4602 .expect("set failed");
4603
4604 kernel
4606 .execute(r#"modify_parent() {
4607 echo "saw: ${PARENT_VAR}"
4608 PARENT_VAR="changed by function"
4609 }"#)
4610 .await
4611 .expect("function definition failed");
4612
4613 let result = kernel.execute("modify_parent").await.expect("function failed");
4615
4616 assert!(
4617 result.out.contains("visible"),
4618 "Shell function should access parent scope, got: {}",
4619 result.out
4620 );
4621
4622 let var = kernel.get_var("PARENT_VAR").await;
4624 assert_eq!(
4625 var,
4626 Some(Value::String("changed by function".into())),
4627 "Shell function should modify parent scope"
4628 );
4629 }
4630
4631 #[tokio::test]
4636 async fn test_script_execution_from_path() {
4637 let kernel = Kernel::transient().expect("failed to create kernel");
4638
4639 kernel.execute(r#"mkdir "/bin""#).await.ok();
4641 kernel
4642 .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
4643 .await
4644 .expect("write script failed");
4645
4646 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
4648
4649 let result = kernel
4651 .execute("hello")
4652 .await
4653 .expect("script execution failed");
4654
4655 assert!(result.ok(), "script failed: {}", result.err);
4656 assert_eq!(result.out.trim(), "Hello from script!");
4657 }
4658
4659 #[tokio::test]
4660 async fn test_script_with_args() {
4661 let kernel = Kernel::transient().expect("failed to create kernel");
4662
4663 kernel.execute(r#"mkdir "/bin""#).await.ok();
4665 kernel
4666 .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
4667 .await
4668 .expect("write script failed");
4669
4670 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
4672
4673 let result = kernel
4675 .execute(r#"greet "World""#)
4676 .await
4677 .expect("script execution failed");
4678
4679 assert!(result.ok(), "script failed: {}", result.err);
4680 assert_eq!(result.out.trim(), "Hello, World!");
4681 }
4682
4683 #[tokio::test]
4684 async fn test_script_not_found() {
4685 let kernel = Kernel::transient().expect("failed to create kernel");
4686
4687 kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
4689
4690 let result = kernel
4692 .execute("noscript")
4693 .await
4694 .expect("execution failed");
4695
4696 assert!(!result.ok(), "should fail with command not found");
4697 assert_eq!(result.code, 127);
4698 assert!(result.err.contains("command not found"));
4699 }
4700
4701 #[tokio::test]
4702 async fn test_script_path_search_order() {
4703 let kernel = Kernel::transient().expect("failed to create kernel");
4704
4705 kernel.execute(r#"mkdir "/first""#).await.ok();
4708 kernel.execute(r#"mkdir "/second""#).await.ok();
4709 kernel
4710 .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
4711 .await
4712 .expect("write failed");
4713 kernel
4714 .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
4715 .await
4716 .expect("write failed");
4717
4718 kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
4720
4721 let result = kernel
4723 .execute("myscript")
4724 .await
4725 .expect("script execution failed");
4726
4727 assert!(result.ok(), "script failed: {}", result.err);
4728 assert_eq!(result.out.trim(), "from first");
4729 }
4730
4731 #[tokio::test]
4736 async fn test_last_exit_code_success() {
4737 let kernel = Kernel::transient().expect("failed to create kernel");
4738
4739 let result = kernel.execute("true; echo $?").await.expect("execution failed");
4741 assert!(result.out.contains("0"), "expected 0, got: {}", result.out);
4742 }
4743
4744 #[tokio::test]
4745 async fn test_last_exit_code_failure() {
4746 let kernel = Kernel::transient().expect("failed to create kernel");
4747
4748 let result = kernel.execute("false; echo $?").await.expect("execution failed");
4750 assert!(result.out.contains("1"), "expected 1, got: {}", result.out);
4751 }
4752
4753 #[tokio::test]
4754 async fn test_current_pid() {
4755 let kernel = Kernel::transient().expect("failed to create kernel");
4756
4757 let result = kernel.execute("echo $$").await.expect("execution failed");
4758 let pid: u32 = result.out.trim().parse().expect("PID should be a number");
4760 assert!(pid > 0, "PID should be positive");
4761 }
4762
4763 #[tokio::test]
4764 async fn test_unset_variable_expands_to_empty() {
4765 let kernel = Kernel::transient().expect("failed to create kernel");
4766
4767 let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
4769 assert_eq!(result.out.trim(), "prefix::suffix");
4770 }
4771
4772 #[tokio::test]
4773 async fn test_eq_ne_operators() {
4774 let kernel = Kernel::transient().expect("failed to create kernel");
4775
4776 let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
4778 assert_eq!(result.out.trim(), "eq works");
4779
4780 let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
4782 assert_eq!(result.out.trim(), "ne works");
4783
4784 let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
4786 assert_eq!(result.out.trim(), "correct");
4787 }
4788
4789 #[tokio::test]
4790 async fn test_escaped_dollar_in_string() {
4791 let kernel = Kernel::transient().expect("failed to create kernel");
4792
4793 let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
4795 assert_eq!(result.out.trim(), "$100");
4796 }
4797
4798 #[tokio::test]
4799 async fn test_special_vars_in_interpolation() {
4800 let kernel = Kernel::transient().expect("failed to create kernel");
4801
4802 let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
4804 assert_eq!(result.out.trim(), "exit: 0");
4805
4806 let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
4808 assert!(result.out.starts_with("pid: "), "unexpected output: {}", result.out);
4809 let pid_part = result.out.trim().strip_prefix("pid: ").unwrap();
4810 let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
4811 }
4812
4813 #[tokio::test]
4818 async fn test_command_subst_assignment() {
4819 let kernel = Kernel::transient().expect("failed to create kernel");
4820
4821 let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
4823 assert_eq!(result.out.trim(), "hello");
4824 }
4825
4826 #[tokio::test]
4827 async fn test_command_subst_with_args() {
4828 let kernel = Kernel::transient().expect("failed to create kernel");
4829
4830 let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
4832 assert_eq!(result.out.trim(), "a b c");
4833 }
4834
4835 #[tokio::test]
4836 async fn test_command_subst_nested_vars() {
4837 let kernel = Kernel::transient().expect("failed to create kernel");
4838
4839 let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
4841 assert_eq!(result.out.trim(), "hello world");
4842 }
4843
4844 #[tokio::test]
4845 async fn test_background_job_basic() {
4846 use std::time::Duration;
4847
4848 let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
4849
4850 let result = kernel.execute("echo hello &").await.expect("execution failed");
4852 assert!(result.ok(), "background command should succeed: {}", result.err);
4853 assert!(result.out.contains("[1]"), "should return job ID: {}", result.out);
4854
4855 tokio::time::sleep(Duration::from_millis(100)).await;
4857
4858 let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
4860 assert!(status.ok(), "status should succeed: {}", status.err);
4861 assert!(
4862 status.out.contains("done:") || status.out.contains("running"),
4863 "should have valid status: {}",
4864 status.out
4865 );
4866
4867 let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
4869 assert!(stdout.ok());
4870 assert!(stdout.out.contains("hello"));
4871 }
4872
4873 #[tokio::test]
4874 async fn test_heredoc_piped_to_command() {
4875 let kernel = Kernel::transient().expect("kernel");
4877 let result = kernel.execute("cat <<EOF | cat\nhello world\nEOF").await.expect("exec");
4878 assert!(result.ok(), "heredoc | cat failed: {}", result.err);
4879 assert_eq!(result.out.trim(), "hello world");
4880 }
4881
4882 #[tokio::test]
4883 async fn test_for_loop_glob_iterates() {
4884 let kernel = Kernel::transient().expect("kernel");
4886 let dir = format!("/tmp/kaish_test_glob_{}", std::process::id());
4887 kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4888 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4889 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
4890 let result = kernel.execute(&format!(r#"
4891 N=0
4892 for F in $(glob "{dir}/*.txt"); do
4893 N=$((N + 1))
4894 done
4895 echo $N
4896 "#)).await.unwrap();
4897 assert!(result.ok(), "for glob failed: {}", result.err);
4898 assert_eq!(result.out.trim(), "2", "Should iterate 2 files, got: {}", result.out);
4899 kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4900 kernel.execute(&format!("rm {dir}/b.txt")).await.unwrap();
4901 }
4902
4903 #[tokio::test]
4904 async fn test_bare_glob_expansion_echo() {
4905 let kernel = Kernel::transient().expect("kernel");
4906 let dir = format!("/tmp/kaish_test_bareglob_{}", std::process::id());
4907 kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4908 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4909 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
4910 kernel.execute(&format!("echo c > {dir}/c.rs")).await.unwrap();
4911 kernel.execute(&format!("cd {dir}")).await.unwrap();
4912 let result = kernel.execute("echo *.txt").await.unwrap();
4913 assert!(result.ok(), "echo *.txt failed: {}", result.err);
4914 let out = result.out.trim();
4915 assert!(out.contains("a.txt"), "missing a.txt in: {}", out);
4917 assert!(out.contains("b.txt"), "missing b.txt in: {}", out);
4918 assert!(!out.contains("c.rs"), "should not contain c.rs in: {}", out);
4919 kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4921 kernel.execute(&format!("rm {dir}/b.txt")).await.unwrap();
4922 kernel.execute(&format!("rm {dir}/c.rs")).await.unwrap();
4923 }
4924
4925 #[tokio::test]
4926 async fn test_bare_glob_no_matches_errors() {
4927 let kernel = Kernel::transient().expect("kernel");
4928 let dir = format!("/tmp/kaish_test_bareglob_nomatch_{}", std::process::id());
4929 kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4930 kernel.execute(&format!("cd {dir}")).await.unwrap();
4931 let result = kernel.execute("echo *.nonexistent").await;
4932 match &result {
4933 Ok(exec) => {
4934 assert!(!exec.ok(), "expected failure, got success: out={}, err={}", exec.out, exec.err);
4936 assert!(exec.err.contains("no matches"), "error should say no matches: {}", exec.err);
4937 }
4938 Err(e) => {
4939 assert!(e.to_string().contains("no matches"), "error should say no matches: {}", e);
4940 }
4941 }
4942 }
4943
4944 #[tokio::test]
4945 async fn test_bare_glob_disabled_with_set() {
4946 let kernel = Kernel::transient().expect("kernel");
4947 let dir = format!("/tmp/kaish_test_bareglob_noglob_{}", std::process::id());
4948 kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4949 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4950 kernel.execute(&format!("cd {dir}")).await.unwrap();
4951 kernel.execute("set +o glob").await.unwrap();
4953 let result = kernel.execute("echo *.txt").await.unwrap();
4954 assert!(result.ok(), "echo should succeed: {}", result.err);
4956 assert_eq!(result.out.trim(), "*.txt", "should be literal: {}", result.out);
4957 kernel.execute("set -o glob").await.unwrap();
4959 kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4960 }
4961
4962 #[tokio::test]
4963 async fn test_bare_glob_quoted_not_expanded() {
4964 let kernel = Kernel::transient().expect("kernel");
4965 let dir = format!("/tmp/kaish_test_bareglob_quoted_{}", std::process::id());
4966 kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4967 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4968 kernel.execute(&format!("cd {dir}")).await.unwrap();
4969 let result = kernel.execute("echo \"*.txt\"").await.unwrap();
4971 assert!(result.ok(), "echo should succeed: {}", result.err);
4972 assert_eq!(result.out.trim(), "*.txt", "quoted should be literal: {}", result.out);
4973 kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4975 }
4976
4977 #[tokio::test]
4978 async fn test_bare_glob_for_loop() {
4979 let kernel = Kernel::transient().expect("kernel");
4980 let dir = format!("/tmp/kaish_test_bareglob_forloop_{}", std::process::id());
4981 kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4982 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4983 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
4984 kernel.execute(&format!("cd {dir}")).await.unwrap();
4985 let result = kernel.execute(r#"
4986 N=0
4987 for f in *.txt; do
4988 N=$((N + 1))
4989 done
4990 echo $N
4991 "#).await.unwrap();
4992 assert!(result.ok(), "for loop failed: {}", result.err);
4993 assert_eq!(result.out.trim(), "2", "should iterate 2 files: {}", result.out);
4994 kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4996 kernel.execute(&format!("rm {dir}/b.txt")).await.unwrap();
4997 }
4998
4999 #[tokio::test]
5000 async fn test_glob_in_assignment_is_literal() {
5001 let kernel = Kernel::transient().expect("kernel");
5002 let result = kernel.execute("X=*.txt; echo $X").await.unwrap();
5003 assert!(result.ok());
5004 assert_eq!(result.out.trim(), "*.txt", "glob in assignment should be literal");
5005 }
5006
5007 #[tokio::test]
5008 async fn test_glob_in_test_expr_is_literal() {
5009 let kernel = Kernel::transient().expect("kernel");
5010 let result = kernel.execute(r#"
5011 if [[ *.txt == "*.txt" ]]; then
5012 echo "match"
5013 else
5014 echo "no"
5015 fi
5016 "#).await.unwrap();
5017 assert!(result.ok());
5018 assert_eq!(result.out.trim(), "match", "glob in test expr should be literal");
5019 }
5020
5021 #[tokio::test]
5022 async fn test_command_subst_echo_not_iterable() {
5023 let kernel = Kernel::transient().expect("kernel");
5025 let result = kernel.execute(r#"
5026 N=0
5027 for X in $(echo "a b c"); do N=$((N + 1)); done
5028 echo $N
5029 "#).await.unwrap();
5030 assert!(result.ok());
5031 assert_eq!(result.out.trim(), "1", "echo should be one item: {}", result.out);
5032 }
5033
5034 #[test]
5037 fn test_accumulate_no_double_newlines() {
5038 let mut acc = ExecResult::success("line1\n");
5040 let new = ExecResult::success("line2\n");
5041 accumulate_result(&mut acc, &new);
5042 assert_eq!(acc.out, "line1\nline2\n");
5043 assert!(!acc.out.contains("\n\n"), "should not have double newlines: {:?}", acc.out);
5044 }
5045
5046 #[test]
5047 fn test_accumulate_adds_separator_when_needed() {
5048 let mut acc = ExecResult::success("line1");
5050 let new = ExecResult::success("line2");
5051 accumulate_result(&mut acc, &new);
5052 assert_eq!(acc.out, "line1\nline2");
5053 }
5054
5055 #[test]
5056 fn test_accumulate_empty_into_nonempty() {
5057 let mut acc = ExecResult::success("");
5058 let new = ExecResult::success("hello\n");
5059 accumulate_result(&mut acc, &new);
5060 assert_eq!(acc.out, "hello\n");
5061 }
5062
5063 #[test]
5064 fn test_accumulate_nonempty_into_empty() {
5065 let mut acc = ExecResult::success("hello\n");
5066 let new = ExecResult::success("");
5067 accumulate_result(&mut acc, &new);
5068 assert_eq!(acc.out, "hello\n");
5069 }
5070
5071 #[test]
5072 fn test_accumulate_stderr_no_double_newlines() {
5073 let mut acc = ExecResult::failure(1, "err1\n");
5074 let new = ExecResult::failure(1, "err2\n");
5075 accumulate_result(&mut acc, &new);
5076 assert!(!acc.err.contains("\n\n"), "stderr should not have double newlines: {:?}", acc.err);
5077 }
5078
5079 #[tokio::test]
5080 async fn test_multiple_echo_no_blank_lines() {
5081 let kernel = Kernel::transient().expect("kernel");
5082 let result = kernel
5083 .execute("echo one\necho two\necho three")
5084 .await
5085 .expect("execution failed");
5086 assert!(result.ok());
5087 assert_eq!(result.out, "one\ntwo\nthree\n");
5088 }
5089
5090 #[tokio::test]
5091 async fn test_for_loop_no_blank_lines() {
5092 let kernel = Kernel::transient().expect("kernel");
5093 let result = kernel
5094 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
5095 .await
5096 .expect("execution failed");
5097 assert!(result.ok());
5098 assert_eq!(result.out, "item: a\nitem: b\nitem: c\n");
5099 }
5100
5101 #[tokio::test]
5102 async fn test_for_command_subst_no_blank_lines() {
5103 let kernel = Kernel::transient().expect("kernel");
5104 let result = kernel
5105 .execute(r#"for N in $(seq 1 3); do echo "n=${N}"; done"#)
5106 .await
5107 .expect("execution failed");
5108 assert!(result.ok());
5109 assert_eq!(result.out, "n=1\nn=2\nn=3\n");
5110 }
5111
5112}