forked from
patrick.sirref.org/merry
Shells in OCaml
1(*-----------------------------------------------------------------
2 Copyright (c) 2025 The merry programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 -----------------------------------------------------------------*)
5open Eio.Std
6open Import
7open Exit.Syntax
8
9(** An evaluator over the AST *)
10module Make (S : Types.State) (E : Types.Exec) = struct
11 (* What follows uses the POSIX definition of what a shell does ($ 2.1).
12
13 It starts from point (4), completing a series of expansions on the AST,
14 then redirection is setup, and finally functions/built-ins/commands are
15 executed. *)
16
17 module J = Job.Make (E)
18 module A = Arith.Make (S)
19
20 type signal_handler = { run : (unit -> unit) -> unit; sigint_set : bool }
21
22 type ctx = {
23 interactive : bool;
24 subshell : bool;
25 state : S.t;
26 local_state : (string * string) list;
27 executor : E.t;
28 fs : Eio.Fs.dir_ty Eio.Path.t;
29 options : Built_ins.Options.t;
30 stdin : Eio_unix.source_ty Eio.Flow.source;
31 stdout : Eio_unix.sink_ty Eio.Flow.sink;
32 background_jobs : J.t list;
33 last_background_process : string;
34 async_switch : Eio.Switch.t;
35 program : string;
36 argv : string array;
37 functions : (string * Ast.compound_command) list;
38 hash : Hash.t;
39 rdrs : Types.redirect list;
40 signal_handler : signal_handler;
41 exit_handler : (unit -> unit) option;
42 }
43
44 let _stdin ctx = ctx.stdin
45
46 let make_ctx ?(interactive = false) ?(subshell = false) ?(local_state = [])
47 ?(background_jobs = []) ?(last_background_process = "") ?(functions = [])
48 ?(rdrs = []) ?exit_handler ?(options = Built_ins.Options.default)
49 ?(hash = Hash.empty) ~fs ~stdin ~stdout ~async_switch ~program ~argv
50 ~signal_handler state executor =
51 let signal_handler = { run = signal_handler; sigint_set = false } in
52 {
53 interactive;
54 subshell;
55 state;
56 local_state;
57 executor;
58 fs;
59 options;
60 stdin;
61 stdout;
62 background_jobs;
63 last_background_process;
64 async_switch;
65 program;
66 argv;
67 functions;
68 hash;
69 rdrs;
70 signal_handler;
71 exit_handler;
72 }
73
74 let state ctx = ctx.state
75 let sigint_set ctx = ctx.signal_handler.sigint_set
76 let fs ctx = ctx.fs
77 let clear_local_state ctx = { ctx with local_state = [] }
78
79 let rec tilde_expansion ctx = function
80 | [] -> []
81 | Ast.WordTildePrefix _ :: rest ->
82 Ast.WordName (S.expand ctx.state `Tilde) :: tilde_expansion ctx rest
83 | v :: rest -> v :: tilde_expansion ctx rest
84
85 let arithmetic_expansion ctx expr =
86 let rec fold (ctx, cst) = function
87 | [] -> (ctx, cst)
88 | Ast.WordArithmeticExpression word :: rest ->
89 let expr = Ast.word_components_to_string word in
90 let aexpr =
91 Arith_parser.main Arith_lexer.read (Lexing.from_string expr)
92 in
93 let state, i = A.eval ctx.state aexpr in
94 fold
95 ({ ctx with state }, Ast.WordLiteral (string_of_int i) :: cst)
96 rest
97 | Ast.WordDoubleQuoted dq :: rest ->
98 let ctx, v = fold (ctx, []) dq in
99 fold (ctx, Ast.WordDoubleQuoted (List.rev v) :: cst) rest
100 | Ast.WordSingleQuoted dq :: rest ->
101 let ctx, v = fold (ctx, []) dq in
102 fold (ctx, Ast.WordSingleQuoted (List.rev v) :: cst) rest
103 | v :: rest -> fold (ctx, v :: cst) rest
104 in
105 let state, cst = fold (ctx, []) expr in
106 (state, List.rev cst)
107
108 let stdout_for_pipeline ~sw ctx = function
109 | [] -> (None, `Global ctx.stdout)
110 | _ ->
111 let r, w = Eio_unix.pipe sw in
112 (Some r, `Local (w :> Eio_unix.sink_ty Eio.Flow.sink))
113
114 let fd_of_int ?(close_unix = true) ~sw n =
115 Eio_unix.Fd.of_unix ~close_unix ~sw (Obj.magic n : Unix.file_descr)
116
117 let handle_one_redirection ~sw ctx = function
118 | Ast.IoRedirect_IoFile (n, (op, file)) -> (
119 match op with
120 | Io_op_less ->
121 (* Simple redirection for input *)
122 let r =
123 Eio.Path.open_in ~sw (ctx.fs / Ast.word_components_to_string file)
124 in
125 let fd = Eio_unix.Resource.fd_opt r |> Option.get in
126 [ Types.Redirect (n, fd, `Blocking) ]
127 | Io_op_lessand -> (
128 match file with
129 | [ WordLiteral "-" ] ->
130 if n = 0 then [ Types.Close Eio_unix.Fd.stdin ]
131 else
132 let fd = fd_of_int ~sw n in
133 [ Types.Close fd ]
134 | [ WordLiteral m ] when Option.is_some (int_of_string_opt m) ->
135 let m = int_of_string m in
136 [
137 Types.Redirect
138 (n, fd_of_int ~close_unix:false ~sw m, `Blocking);
139 ]
140 | _ -> [])
141 | (Io_op_great | Io_op_dgreat) as v ->
142 (* Simple file creation *)
143 let append = v = Io_op_dgreat in
144 let create =
145 if append then `Never
146 else if ctx.options.noclobber then `Exclusive 0o644
147 else `Or_truncate 0o644
148 in
149 let w =
150 Eio.Path.open_out ~sw ~append ~create
151 (ctx.fs / Ast.word_components_to_string file)
152 in
153 let fd = Eio_unix.Resource.fd_opt w |> Option.get in
154 [ Types.Redirect (n, fd, `Blocking) ]
155 | Io_op_greatand -> (
156 match file with
157 | [ WordLiteral "-" ] ->
158 if n = 0 then [ Types.Close Eio_unix.Fd.stdout ]
159 else
160 let fd = fd_of_int ~sw n in
161 [ Types.Close fd ]
162 | [ WordLiteral m ] when Option.is_some (int_of_string_opt m) ->
163 let m = int_of_string m in
164 [
165 Types.Redirect
166 (n, fd_of_int ~close_unix:false ~sw m, `Blocking);
167 ]
168 | _ -> [])
169 | Io_op_andgreat ->
170 (* Yesh, not very POSIX *)
171 (* Simple file creation *)
172 let w =
173 Eio.Path.open_out ~sw ~create:(`If_missing 0o644)
174 (ctx.fs / Ast.word_components_to_string file)
175 in
176 let fd = Eio_unix.Resource.fd_opt w |> Option.get in
177 [
178 Types.Redirect (1, fd, `Blocking);
179 Types.Redirect (2, fd, `Blocking);
180 ]
181 | Io_op_clobber ->
182 let w =
183 Eio.Path.open_out ~sw ~create:(`Or_truncate 0o644)
184 (ctx.fs / Ast.word_components_to_string file)
185 in
186 let fd = Eio_unix.Resource.fd_opt w |> Option.get in
187 [ Types.Redirect (n, fd, `Blocking) ]
188 | Io_op_lessgreat -> Fmt.failwith "<> not support yet.")
189 | Ast.IoRedirect_IoHere _ ->
190 Fmt.failwith "HERE documents not yet implemented!"
191
192 let handle_redirections ~sw ctx rdrs =
193 try Ok (List.concat_map (handle_one_redirection ~sw ctx) rdrs)
194 with Eio.Io (Eio.Fs.E (Already_exists _), _) ->
195 Fmt.epr "msh: cannot overwrite existing file\n%!";
196 Error ctx
197
198 let cwd_of_ctx ctx = S.cwd ctx.state |> Fpath.to_string |> ( / ) ctx.fs
199
200 let needs_glob_expansion : Ast.word_component -> bool = function
201 | WordGlobAll | WordGlobAny -> true
202 | _ -> false
203
204 let resolve_program ?(update = true) ctx name =
205 let v =
206 if not (String.contains name '/') then begin
207 S.lookup ctx.state ~param:"PATH"
208 |> Option.map Ast.word_components_to_string
209 |> Option.value ~default:"/bin:/usr/bin"
210 |> String.split_on_char ':'
211 |> List.find_map (fun dir ->
212 let p = Filename.concat dir name in
213 if Sys.file_exists p then Some p else None)
214 end
215 else if Sys.file_exists name then Some name
216 else None
217 in
218 match (update, v) with
219 | true, Some loc ->
220 let hash = Hash.add ~utility:name ~loc ctx.hash in
221 ({ ctx with hash }, Some loc)
222 | false, Some loc -> (ctx, Some loc)
223 | _, None -> (ctx, None)
224
225 let get_env ?(extra = []) ctx =
226 let extra =
227 extra
228 @ List.map (fun (k, v) -> (k, Ast.word_components_to_string v))
229 @@ S.exports ctx.state
230 in
231 let env = Eunix.env () in
232 List.fold_left (fun acc (k, _) -> List.remove_assoc k acc) env extra
233 |> List.append extra
234
235 let update ?export ?readonly ctx ~param v =
236 match S.update ?export ?readonly ctx.state ~param v with
237 | Ok state -> Exit.zero { ctx with state }
238 | Error msg ->
239 Fmt.epr "%s\n%!" msg;
240 Exit.nonzero ctx 1
241
242 let remove_quotes s =
243 let s_len = String.length s in
244 if s.[0] = '"' && s.[s_len - 1] = '"' then String.sub s 1 (s_len - 2) else s
245
246 let exit ctx code =
247 Option.iter (fun f -> f ()) ctx.exit_handler;
248 exit code
249
250 let rec handle_pipeline ~async initial_ctx p : ctx Exit.t =
251 let set_last_background ~async process ctx =
252 if async then
253 { ctx with last_background_process = string_of_int (E.pid process) }
254 else ctx
255 in
256 let on_process ?process ~async ctx =
257 let ctx = clear_local_state ctx in
258 match process with
259 | None -> ctx
260 | Some process -> set_last_background ~async process ctx
261 in
262 let handle_job j = function
263 | `Process p -> J.add_process p j
264 | `Rdr p -> J.add_rdr p j
265 | `Built_in p -> J.add_built_in p j
266 | `Error p -> J.add_error p j
267 | `Exit p -> J.add_exit p j
268 in
269 let close_stdout ~is_global some_write =
270 if not is_global then begin
271 Eio.Flow.close some_write
272 end
273 in
274 let exec_process ~sw ctx job ?fds ?stdin ~stdout ?pgid executable args =
275 let pgid = match pgid with None -> 0 | Some p -> p in
276 let reap = J.get_reaper job in
277 let mode = if async then Types.Async else Types.Switched sw in
278 let fds = ctx.rdrs @ Option.value ~default:[] fds in
279 let ctx, process =
280 match (executable, resolve_program ctx executable) with
281 | _, (ctx, None) | "", (ctx, _) ->
282 Eunix.with_redirections fds (fun () ->
283 Eio.Flow.copy_string
284 (Fmt.str "msh: command not found: %s\n" executable)
285 stdout);
286 (ctx, Error (127, `Not_found))
287 | _, (ctx, Some full_path) ->
288 ( ctx,
289 E.exec ctx.executor ~delay_reap:(fst reap) ~fds ?stdin ~stdout
290 ~pgid ~mode ~cwd:(cwd_of_ctx ctx)
291 ~env:(get_env ~extra:ctx.local_state ctx)
292 ~executable:full_path (executable :: args) )
293 in
294 match process with
295 | Error (n, _) ->
296 let job = handle_job job (`Error n) in
297 (on_process ~async ctx, job)
298 | Ok process ->
299 let pgid = if Int.equal pgid 0 then E.pid process else pgid in
300 let job = handle_job job (`Process process) |> J.set_id pgid in
301 (on_process ~async ~process ctx, job)
302 in
303 let job_pgid (t : J.t) = J.get_id t in
304 let rec loop pipeline_switch (ctx : ctx) (job : J.t)
305 (stdout_of_previous : Eio_unix.source_ty Eio_unix.source option) :
306 Ast.command list -> ctx * J.t =
307 fun c ->
308 let loop = loop pipeline_switch in
309 match c with
310 | Ast.SimpleCommand (Prefixed (prefix, None, _suffix)) :: rest ->
311 let ctx = collect_assignments ctx prefix in
312 let job = handle_job job (`Built_in (Exit.ignore ctx)) in
313 loop (Exit.value ctx) job stdout_of_previous rest
314 | Ast.SimpleCommand (Prefixed (prefix, Some executable, suffix)) :: rest
315 ->
316 let ctx = collect_assignments ~update:false ctx prefix in
317 let job = handle_job job (`Built_in (Exit.ignore ctx)) in
318 loop (Exit.value ctx) job stdout_of_previous
319 (Ast.SimpleCommand (Named (executable, suffix)) :: rest)
320 | Ast.SimpleCommand (Named (executable, suffix)) :: rest -> (
321 let ctx, executable = expand_cst ctx executable in
322 match ctx with
323 | Exit.Nonzero _ as ctx ->
324 let job = handle_job job (`Built_in (Exit.ignore ctx)) in
325 loop (Exit.value ctx) job stdout_of_previous rest
326 | Exit.Zero ctx -> (
327 let executable = handle_word_cst_subshell ctx executable in
328 let executable, extra_args =
329 (* This is a side-effect of the alias command with something like
330 alias ls="ls -la" *)
331 match executable with
332 | [ Ast.WordLiteral s ] as v -> (
333 match String.split_on_char ' ' (remove_quotes s) with
334 | exec :: args ->
335 ( [ Ast.WordName exec ],
336 List.map
337 (fun w -> Ast.Suffix_word [ Ast.WordName w ])
338 args )
339 | _ -> (v, []))
340 | v -> (v, [])
341 in
342 let executable = Ast.word_components_to_string executable in
343 let ctx, suffix =
344 match suffix with
345 | None -> (ctx, [])
346 | Some suffix -> expand_redirects (ctx, []) suffix
347 in
348 let ctx, args = args ctx (extra_args @ suffix) in
349 match ctx with
350 | Exit.Nonzero _ as ctx ->
351 let job = handle_job job (`Built_in (Exit.ignore ctx)) in
352 loop (Exit.value ctx) job stdout_of_previous rest
353 | Exit.Zero ctx -> (
354 let args_as_strings =
355 List.map Ast.word_components_to_string args
356 in
357 let some_read, some_write =
358 stdout_for_pipeline ~sw:pipeline_switch ctx rest
359 in
360 let is_global, some_write =
361 match some_write with
362 | `Global p -> (true, p)
363 | `Local p -> (false, p)
364 in
365 let rdrs =
366 List.fold_left
367 (fun acc -> function
368 | Ast.Suffix_word _ -> acc
369 | Ast.Suffix_redirect rdr -> rdr :: acc)
370 [] suffix
371 |> List.rev
372 in
373 match handle_redirections ~sw:pipeline_switch ctx rdrs with
374 | Error ctx -> (ctx, handle_job job (`Rdr (Exit.nonzero () 1)))
375 | Ok rdrs -> (
376 match
377 Built_ins.of_args (executable :: args_as_strings)
378 with
379 | Some (Error _) ->
380 (ctx, handle_job job (`Built_in (Exit.nonzero () 1)))
381 | (None | Some (Ok (Command _))) as v -> (
382 let is_command, command_args, print_command =
383 match v with
384 | Some (Ok (Command { print_command; args })) ->
385 (true, args, print_command)
386 | _ -> (false, [], false)
387 in
388 (* We handle the [export] built_in explicitly as we need access to the
389 raw CST *)
390 match executable with
391 | "export" ->
392 let updated =
393 handle_export_or_readonly `Export ctx args
394 in
395 let job =
396 handle_job job
397 (`Built_in (updated >|= fun _ -> ()))
398 in
399 loop (Exit.value updated) job stdout_of_previous
400 rest
401 | "readonly" ->
402 let updated =
403 handle_export_or_readonly `Readonly ctx args
404 in
405 let job =
406 handle_job job
407 (`Built_in (updated >|= fun _ -> ()))
408 in
409 loop (Exit.value updated) job stdout_of_previous
410 rest
411 | _ -> (
412 let saved_ctx = ctx in
413 let func_app =
414 if is_command then None
415 else
416 let ctx = { ctx with stdout = some_write } in
417 handle_function_application ctx
418 ~name:executable
419 (ctx.program :: args_as_strings)
420 in
421 match func_app with
422 | Some ctx ->
423 close_stdout ~is_global some_write;
424 (* TODO: Proper job stuff and redirects etc. *)
425 let job =
426 handle_job job (`Built_in (Exit.ignore ctx))
427 in
428 loop saved_ctx job some_read rest
429 | None -> (
430 match Built_ins.of_args command_args with
431 | Some (Error _) ->
432 ( ctx,
433 handle_job job
434 (`Built_in (Exit.nonzero () 1)) )
435 | Some (Ok bi) ->
436 let ctx =
437 handle_built_in ~rdrs ~stdout:some_write
438 ctx bi
439 in
440 let ctx =
441 ctx >|= fun ctx -> clear_local_state ctx
442 in
443 close_stdout ~is_global some_write;
444 let job =
445 match bi with
446 | Built_ins.Exit _ ->
447 let v_ctx = Exit.value ctx in
448 if not v_ctx.subshell then
449 exit v_ctx (Exit.code ctx)
450 else
451 handle_job job
452 (`Exit (Exit.ignore ctx))
453 | _ ->
454 handle_job job
455 (`Built_in (Exit.ignore ctx))
456 in
457 loop (Exit.value ctx) job some_read rest
458 | _ -> (
459 let exec_and_args =
460 if is_command then begin
461 match command_args with
462 | [] -> assert false
463 | x :: xs -> (
464 Eunix.with_redirections rdrs
465 @@ fun () ->
466 match
467 resolve_program ~update:false
468 ctx x
469 with
470 | _, None ->
471 Exit.nonzero ("", []) 1
472 | _, Some prog ->
473 if print_command then
474 Exit.zero ("echo", [ prog ])
475 else Exit.zero (x, xs))
476 end
477 else
478 Exit.zero (executable, args_as_strings)
479 in
480 match exec_and_args with
481 | Exit.Nonzero _ as v ->
482 let job =
483 handle_job job
484 (`Built_in (Exit.ignore v))
485 in
486 loop ctx job some_read rest
487 | Exit.Zero (executable, args) -> (
488 match stdout_of_previous with
489 | None ->
490 let ctx, job =
491 exec_process ~sw:pipeline_switch
492 ctx job ~fds:rdrs
493 ~stdout:some_write
494 ~pgid:(job_pgid job)
495 executable args
496 in
497 close_stdout ~is_global some_write;
498 loop ctx job some_read rest
499 | Some stdout ->
500 let ctx, job =
501 exec_process ~sw:pipeline_switch
502 ctx job ~fds:rdrs
503 ~stdin:stdout
504 ~stdout:some_write
505 ~pgid:(job_pgid job)
506 executable args_as_strings
507 in
508 close_stdout ~is_global some_write;
509 loop ctx job some_read rest)))))
510 | Some (Ok bi) ->
511 let ctx =
512 handle_built_in ~rdrs ~stdout:some_write ctx bi
513 in
514 let ctx = ctx >|= fun ctx -> clear_local_state ctx in
515 close_stdout ~is_global some_write;
516 let job =
517 match bi with
518 | Built_ins.Exit _ ->
519 let v_ctx = Exit.value ctx in
520 if not v_ctx.subshell then begin
521 if (Exit.value ctx).interactive then
522 Fmt.pr "exit\n%!";
523 exit v_ctx (Exit.code ctx)
524 end
525 else handle_job job (`Exit (Exit.ignore ctx))
526 | _ -> handle_job job (`Built_in (Exit.ignore ctx))
527 in
528 loop (Exit.value ctx) job some_read rest))))
529 | CompoundCommand (c, rdrs) :: rest -> (
530 match handle_redirections ~sw:pipeline_switch ctx rdrs with
531 | Error ctx -> (ctx, handle_job job (`Rdr (Exit.nonzero () 1)))
532 | Ok rdrs ->
533 (* TODO: No way this is right *)
534 let ctx = { ctx with rdrs } in
535 let ctx = handle_compound_command ctx c in
536 let job = handle_job job (`Built_in (ctx >|= fun _ -> ())) in
537 let actual_ctx = Exit.value ctx in
538 loop { actual_ctx with rdrs = [] } job None rest)
539 | FunctionDefinition (name, (body, _rdrs)) :: rest ->
540 let ctx = { ctx with functions = (name, body) :: ctx.functions } in
541 loop ctx job None rest
542 | [] -> (clear_local_state ctx, job)
543 in
544 Eio.Switch.run @@ fun sw ->
545 let initial_job = J.make 0 [] in
546 let saved_ctx = initial_ctx in
547 let subshell = saved_ctx.subshell || List.length p > 1 in
548 let ctx = { initial_ctx with subshell } in
549 let ctx, job = loop sw ctx initial_job None p in
550 match J.size job with
551 | 0 -> Exit.zero ctx
552 | _ ->
553 if not async then begin
554 J.await_exit ~pipefail:ctx.options.pipefail
555 ~interactive:ctx.interactive job
556 >|= fun () -> { ctx with subshell = saved_ctx.subshell }
557 end
558 else begin
559 Exit.zero
560 {
561 ctx with
562 background_jobs = job :: ctx.background_jobs;
563 subshell = saved_ctx.subshell;
564 }
565 end
566
567 and parameter_expansion' ctx ast =
568 let get_prefix ~pattern ~kind param =
569 let _, prefix =
570 String.fold_left
571 (fun (so_far, acc) c ->
572 match acc with
573 | Some s when kind = `Smallest -> (so_far, Some s)
574 | _ -> (
575 let s = so_far ^ String.make 1 c in
576 match Glob.tests ~pattern [ s ] with
577 | [ s ] -> (s, Some s)
578 | _ -> (s, acc)))
579 ("", None) param
580 in
581 prefix
582 in
583 let get_suffix ~pattern ~kind param =
584 let _, prefix =
585 String.fold_left
586 (fun (so_far, acc) c ->
587 match acc with
588 | Some s when kind = `Smallest -> (so_far, Some s)
589 | _ -> (
590 let s = String.make 1 c ^ so_far in
591 match Glob.tests ~pattern [ s ] with
592 | [ s ] -> (s, Some s)
593 | _ -> (s, acc)))
594 ("", None)
595 (String.fold_left (fun acc c -> String.make 1 c ^ acc) "" param)
596 in
597 prefix
598 in
599 let rec expand acc ctx = function
600 | [] -> (Exit.zero ctx, List.rev acc |> List.concat)
601 | Ast.WordVariable v :: rest -> (
602 match v with
603 | Ast.VariableAtom ("!", NoAttribute) ->
604 expand
605 ([ Ast.WordName ctx.last_background_process ] :: acc)
606 ctx rest
607 | Ast.VariableAtom (n, NoAttribute)
608 when Option.is_some (int_of_string_opt n) -> (
609 let n = int_of_string n in
610 match Array.get ctx.argv n with
611 | v -> expand ([ Ast.WordName v ] :: acc) ctx rest
612 | exception Invalid_argument _ ->
613 expand ([ Ast.WordName "" ] :: acc) ctx rest)
614 | Ast.VariableAtom (s, NoAttribute) -> (
615 match S.lookup ctx.state ~param:s with
616 | None ->
617 if ctx.options.no_unset then begin
618 ( Exit.nonzero_msg ctx ~exit_code:1 "%s: unbound variable" s,
619 List.rev acc |> List.concat )
620 end
621 else expand ([ Ast.WordName "" ] :: acc) ctx rest
622 | Some cst -> expand (cst :: acc) ctx rest)
623 | Ast.VariableAtom (s, ParameterLength) -> (
624 match S.lookup ctx.state ~param:s with
625 | None -> expand ([ Ast.WordLiteral "0" ] :: acc) ctx rest
626 | Some cst ->
627 expand
628 ([
629 Ast.WordLiteral
630 (string_of_int
631 (String.length (Ast.word_components_to_string cst)));
632 ]
633 :: acc)
634 ctx rest)
635 | Ast.VariableAtom (s, UseDefaultValues (_, cst)) -> (
636 match S.lookup ctx.state ~param:s with
637 | None -> expand (cst :: acc) ctx rest
638 | Some cst -> expand (cst :: acc) ctx rest)
639 | Ast.VariableAtom
640 ( s,
641 (( RemoveSmallestPrefixPattern cst
642 | RemoveLargestPrefixPattern cst ) as v) ) -> (
643 let ctx, spp = expand_cst ctx cst in
644 match ctx with
645 | Exit.Nonzero _ as ctx -> (ctx, List.rev acc |> List.concat)
646 | Exit.Zero ctx -> (
647 let pattern = Ast.word_components_to_string spp in
648 match S.lookup ctx.state ~param:s with
649 | None -> expand (cst :: acc) ctx rest
650 | Some cst -> (
651 let kind =
652 match v with
653 | RemoveSmallestPrefixPattern _ -> `Smallest
654 | RemoveLargestPrefixPattern _ -> `Largest
655 | _ -> assert false
656 in
657 let param = Ast.word_components_to_string cst in
658 let prefix = get_prefix ~pattern ~kind param in
659 match prefix with
660 | None -> expand ([ Ast.WordName param ] :: acc) ctx rest
661 | Some s -> (
662 match String.cut_prefix ~prefix:s param with
663 | Some s ->
664 expand ([ Ast.WordName s ] :: acc) ctx rest
665 | None ->
666 expand ([ Ast.WordName param ] :: acc) ctx rest)))
667 )
668 | Ast.VariableAtom
669 ( s,
670 (( RemoveSmallestSuffixPattern cst
671 | RemoveLargestSuffixPattern cst ) as v) ) -> (
672 let ctx, spp = expand_cst ctx cst in
673 let pattern = Ast.word_components_to_string spp in
674 match ctx with
675 | Exit.Nonzero _ as ctx -> (ctx, List.rev acc |> List.concat)
676 | Exit.Zero ctx -> (
677 match S.lookup ctx.state ~param:s with
678 | None -> expand (cst :: acc) ctx rest
679 | Some cst -> (
680 let kind =
681 match v with
682 | RemoveSmallestSuffixPattern _ -> `Smallest
683 | RemoveLargestSuffixPattern _ -> `Largest
684 | _ -> assert false
685 in
686 let param = Ast.word_components_to_string cst in
687 let suffix = get_suffix ~pattern ~kind param in
688 match suffix with
689 | None -> expand ([ Ast.WordName param ] :: acc) ctx rest
690 | Some s -> (
691 match String.cut_suffix ~suffix:s param with
692 | Some s ->
693 expand ([ Ast.WordName s ] :: acc) ctx rest
694 | None ->
695 expand ([ Ast.WordName param ] :: acc) ctx rest)))
696 )
697 | Ast.VariableAtom (s, UseAlternativeValue (_, alt)) -> (
698 match S.lookup ctx.state ~param:s with
699 | Some _ -> expand (alt :: acc) ctx rest
700 | None -> expand ([ Ast.WordEmpty ] :: acc) ctx rest)
701 | Ast.VariableAtom (s, AssignDefaultValues (_, value)) -> (
702 match S.lookup ctx.state ~param:s with
703 | Some cst -> expand (cst :: acc) ctx rest
704 | None -> (
705 match S.update ctx.state ~param:s value with
706 | Ok state ->
707 let new_ctx = { ctx with state } in
708 expand (value :: acc) new_ctx rest
709 | Error m ->
710 ( Exit.nonzero_msg ~exit_code:1 ctx "%s" m,
711 List.rev acc |> List.concat )))
712 | Ast.VariableAtom (_, IndicateErrorifNullorUnset (_, _)) ->
713 Fmt.failwith "TODO: Indicate Error")
714 | Ast.WordDoubleQuoted cst :: rest -> (
715 let new_ctx, cst_acc = expand [] ctx cst in
716 match new_ctx with
717 | Exit.Nonzero _ -> (new_ctx, cst_acc)
718 | Exit.Zero new_ctx ->
719 expand ([ Ast.WordDoubleQuoted cst_acc ] :: acc) new_ctx rest)
720 | Ast.WordSingleQuoted cst :: rest -> (
721 let new_ctx, cst_acc = expand [] ctx cst in
722 match new_ctx with
723 | Exit.Nonzero _ -> (new_ctx, cst_acc)
724 | Exit.Zero new_ctx ->
725 expand ([ Ast.WordSingleQuoted cst_acc ] :: acc) new_ctx rest)
726 | Ast.WordAssignmentWord (n, w) :: rest -> (
727 let new_ctx, cst_acc = expand [] ctx w in
728 match new_ctx with
729 | Exit.Nonzero _ -> (new_ctx, cst_acc)
730 | Exit.Zero new_ctx ->
731 expand
732 ([ Ast.WordAssignmentWord (n, cst_acc) ] :: acc)
733 new_ctx rest)
734 | v :: rest -> expand ([ v ] :: acc) ctx rest
735 in
736 expand [] ctx ast
737
738 and handle_export_or_readonly kind ctx (assignments : Ast.word_cst list) =
739 let flags, assignments =
740 List.fold_left
741 (fun (fs, args) -> function
742 | [ Ast.WordName v ] | [ Ast.WordLiteral v ] -> (
743 match Astring.String.cut ~sep:"-" v with
744 | Some ("", f) -> (f :: fs, args)
745 | _ -> (fs, [ Ast.WordName v ] :: args))
746 | v -> (fs, v :: args))
747 ([], []) assignments
748 in
749 let update =
750 match kind with
751 | `Export -> update ~export:true ~readonly:false
752 | `Readonly -> update ~export:false ~readonly:true
753 in
754 let rec loop acc_ctx = function
755 | [] -> Exit.zero acc_ctx
756 | Ast.WordAssignmentWord (Name param, v) :: rest ->
757 update acc_ctx ~param v >>= fun new_ctx -> loop new_ctx rest
758 | Ast.WordName param :: rest -> (
759 match S.lookup acc_ctx.state ~param with
760 | Some v ->
761 update acc_ctx ~param v >>= fun new_ctx -> loop new_ctx rest
762 | None -> loop acc_ctx rest)
763 | c :: _ ->
764 Exit.nonzero_msg acc_ctx "export weird arguments: %s\n"
765 (Ast.word_component_to_string c)
766 in
767 match flags with
768 | [] ->
769 List.fold_left
770 (fun ctx w -> match ctx with Exit.Zero ctx -> loop ctx w | _ -> ctx)
771 (Exit.zero ctx) assignments
772 | fs ->
773 if List.mem "p" fs then begin
774 match kind with
775 | `Readonly -> S.pp_readonly Fmt.stdout ctx.state
776 | `Export -> S.pp_export Fmt.stdout ctx.state
777 end;
778 Exit.zero ctx
779
780 and expand_cst (ctx : ctx) cst : ctx Exit.t * Ast.word_cst =
781 let cst = tilde_expansion ctx cst in
782 let ctx, cst = parameter_expansion' ctx cst in
783 match ctx with
784 | Exit.Nonzero _ as ctx -> (ctx, cst)
785 | Exit.Zero ctx ->
786 (* TODO: Propagate errors *)
787 let ctx, ast = arithmetic_expansion ctx cst in
788 (Exit.zero ctx, ast)
789
790 and expand_redirects ((ctx, acc) : ctx * Ast.cmd_suffix_item list)
791 (c : Ast.cmd_suffix_item list) =
792 match c with
793 | [] -> (ctx, List.rev acc)
794 | Ast.Suffix_redirect (IoRedirect_IoFile (num, (op, file))) :: rest -> (
795 let ctx, cst = expand_cst ctx file in
796 match ctx with
797 | Exit.Nonzero _ -> assert false
798 | Exit.Zero ctx ->
799 let cst = handle_subshell ctx cst in
800 let v = Ast.Suffix_redirect (IoRedirect_IoFile (num, (op, cst))) in
801 expand_redirects (ctx, v :: acc) rest)
802 | (Ast.Suffix_redirect _ as v) :: rest ->
803 expand_redirects (ctx, v :: acc) rest
804 | s :: rest -> expand_redirects (ctx, s :: acc) rest
805
806 and handle_and_or ~sw:_ ~async ctx c =
807 let pipeline = function
808 | Ast.Pipeline p -> (Fun.id, p)
809 | Ast.Pipeline_Bang p -> (Exit.not, p)
810 in
811
812 let rec fold :
813 Ast.and_or * ctx Exit.t -> Ast.pipeline Ast.and_or_list -> ctx Exit.t =
814 fun (sep, exit_so_far) pipe ->
815 match (sep, pipe) with
816 | And, Nlist.Singleton (p, _) -> (
817 match exit_so_far with
818 | Exit.Zero ctx ->
819 let f, p = pipeline p in
820 f @@ handle_pipeline ~async ctx p
821 | v -> v)
822 | Or, Nlist.Singleton (p, _) -> (
823 match exit_so_far with
824 | Exit.Zero _ as ctx -> ctx
825 | _ ->
826 let f, p = pipeline p in
827 f @@ handle_pipeline ~async ctx p)
828 | Noand_or, Nlist.Singleton (p, _) ->
829 let f, p = pipeline p in
830 f @@ handle_pipeline ~async ctx p
831 | Noand_or, Nlist.Cons ((p, next_sep), rest) ->
832 let f, p = pipeline p in
833 let exit_status = f (handle_pipeline ~async ctx p) in
834 fold (next_sep, exit_status) rest
835 | And, Nlist.Cons ((p, next_sep), rest) -> (
836 match exit_so_far with
837 | Exit.Zero ctx ->
838 let f, p = pipeline p in
839 fold (next_sep, f (handle_pipeline ~async ctx p)) rest
840 | Exit.Nonzero _ as v -> v)
841 | Or, Nlist.Cons ((p, next_sep), rest) -> (
842 match exit_so_far with
843 | Exit.Zero _ as exit_so_far -> fold (next_sep, exit_so_far) rest
844 | Exit.Nonzero _ ->
845 let f, p = pipeline p in
846 fold (next_sep, f (handle_pipeline ~async ctx p)) rest)
847 in
848 fold (Noand_or, Exit.zero ctx) c
849
850 and handle_for_clause ctx v : ctx Exit.t =
851 match v with
852 | Ast.For_Name_DoGroup (_, (term, sep)) -> exec ctx (term, Some sep)
853 | Ast.For_Name_In_WordList_DoGroup (Name name, wdlist, (term, sep)) ->
854 let wdlist = Nlist.flatten @@ Nlist.map (word_glob_expand ctx) wdlist in
855 Nlist.fold_left
856 (fun _ word ->
857 update ctx ~param:name word >>= fun ctx -> exec ctx (term, Some sep))
858 (Exit.zero ctx) wdlist
859
860 and handle_if_clause ctx = function
861 | Ast.If_then ((e1, sep1), (e2, sep2)) -> (
862 let ctx = exec ctx (e1, Some sep1) in
863 match ctx with
864 | Exit.Zero ctx -> exec ctx (e2, Some sep2)
865 | Exit.Nonzero { value = ctx; _ } -> Exit.zero ctx)
866 | Ast.If_then_else ((e1, sep1), (e2, sep2), else_part) -> (
867 let ctx = exec ctx (e1, Some sep1) in
868 match ctx with
869 | Exit.Zero ctx -> exec ctx (e2, Some sep2)
870 | Exit.Nonzero { value = ctx; _ } -> handle_else_part ctx else_part)
871
872 and handle_else_part ctx = function
873 | Ast.Else (c, sep) -> exec ctx (c, Some sep)
874 | Ast.Elif_then ((e1, sep1), (e2, sep2)) -> (
875 let ctx = exec ctx (e1, Some sep1) in
876 match ctx with
877 | Exit.Zero ctx -> exec ctx (e2, Some sep2)
878 | Exit.Nonzero { value = ctx; _ } -> Exit.zero ctx)
879 | Ast.Elif_then_else ((e1, sep1), (e2, sep2), else_part) -> (
880 let ctx = exec ctx (e1, Some sep1) in
881 match ctx with
882 | Exit.Zero ctx -> exec ctx (e2, Some sep2)
883 | Exit.Nonzero { value = ctx; _ } -> handle_else_part ctx else_part)
884
885 and handle_case_clause ctx = function
886 | Ast.Case _ -> Exit.zero ctx
887 | Cases (word, case_list) -> (
888 let ctx, word = expand_cst ctx word in
889 match ctx with
890 | Exit.Nonzero _ as ctx -> ctx
891 | Exit.Zero ctx -> (
892 let scrutinee = Ast.word_components_to_string word in
893 let res =
894 Nlist.fold_left
895 (fun acc pat ->
896 match acc with
897 | Some _ as ctx -> ctx
898 | None -> (
899 match pat with
900 | Ast.Case_pattern (p, sub) ->
901 Nlist.fold_left
902 (fun inner_acc pattern ->
903 match inner_acc with
904 | Some _ as v -> v
905 | None -> (
906 let ctx, pattern = expand_cst ctx pattern in
907 match ctx with
908 | Exit.Nonzero _ as ctx -> Some ctx
909 | Exit.Zero ctx ->
910 let pattern =
911 Ast.word_components_to_string pattern
912 in
913 if Glob.test ~pattern scrutinee then begin
914 match sub with
915 | Some sub ->
916 Some (exec_subshell ctx sub)
917 | None -> Some (Exit.zero ctx)
918 end
919 else inner_acc))
920 None p))
921 None case_list
922 in
923 match res with Some ctx -> ctx | None -> Exit.zero ctx))
924
925 and exec_subshell ctx (term, sep) =
926 let saved_ctx = ctx in
927 let e = exec ctx (term, Some sep) in
928 let v = e >|= fun _ -> saved_ctx in
929 v
930
931 and handle_while_clause ctx
932 (While ((term, sep), (term', sep')) : Ast.while_clause) =
933 let rec loop exit_so_far =
934 let running_ctx = Exit.value exit_so_far in
935 match exec running_ctx (term, Some sep) with
936 | Exit.Nonzero _ -> exit_so_far (* TODO: Context? *)
937 | Exit.Zero ctx -> loop (exec ctx (term', Some sep'))
938 in
939 loop (Exit.zero ctx)
940
941 and handle_until_clause ctx
942 (Until ((term, sep), (term', sep')) : Ast.until_clause) =
943 let rec loop exit_so_far =
944 let running_ctx = Exit.value exit_so_far in
945 match exec running_ctx (term, Some sep) with
946 | Exit.Zero _ -> exit_so_far (* TODO: Context? *)
947 | Exit.Nonzero { value = ctx; _ } -> loop (exec ctx (term', Some sep'))
948 in
949 loop (Exit.zero ctx)
950
951 and handle_compound_command ctx v : ctx Exit.t =
952 match v with
953 | Ast.ForClause fc -> handle_for_clause ctx fc
954 | Ast.IfClause if_ -> handle_if_clause ctx if_
955 | Ast.BraceGroup (term, sep) -> exec ctx (term, Some sep)
956 | Ast.Subshell s -> exec_subshell ctx s
957 | Ast.CaseClause cases -> handle_case_clause ctx cases
958 | Ast.WhileClause while_ -> handle_while_clause ctx while_
959 | Ast.UntilClause until -> handle_until_clause ctx until
960
961 and handle_function_application (ctx : ctx) ~name argv : ctx Exit.t option =
962 match List.assoc_opt name ctx.functions with
963 | None -> None
964 | Some commands ->
965 let ctx = { ctx with argv = Array.of_list argv } in
966 Option.some @@ (handle_compound_command ctx commands >|= fun _ -> ctx)
967
968 and needs_subshelling = function
969 | [] -> false
970 | Ast.WordSubshell _ :: _ -> true
971 | Ast.WordDoubleQuoted word :: rest ->
972 needs_subshelling word || needs_subshelling rest
973 | Ast.WordSingleQuoted word :: rest ->
974 needs_subshelling word || needs_subshelling rest
975 | _ -> false
976
977 and handle_subshell (ctx : ctx) wcs =
978 let exec_subshell ~sw ctx s =
979 let buf = Buffer.create 16 in
980 let stdout = Eio.Flow.buffer_sink buf in
981 let r, w = Eio_unix.pipe sw in
982 Eio.Fiber.fork ~sw (fun () -> Eio.Flow.copy r stdout);
983 let subshell_ctx = { ctx with stdout = w; subshell = true } in
984 let sub_ctx, _ = run (Exit.zero subshell_ctx) s in
985 Eio.Flow.close w;
986 ((sub_ctx >|= fun _ -> ctx), Buffer.contents buf)
987 in
988 let rec run_subshells ~sw ran_subshell = function
989 | [] -> []
990 | Ast.WordSubshell s :: rest ->
991 let _ctx, std = exec_subshell ~sw ctx s in
992 ran_subshell := true;
993 Ast.WordName (String.trim std) :: run_subshells ~sw ran_subshell rest
994 | Ast.WordDoubleQuoted word :: rest ->
995 let subshell_q = ref false in
996 let res = run_subshells ~sw subshell_q word in
997 if !subshell_q then res @ run_subshells ~sw subshell_q rest
998 else Ast.WordDoubleQuoted res :: run_subshells ~sw subshell_q rest
999 | Ast.WordSingleQuoted word :: rest ->
1000 let subshell_q = ref false in
1001 let res = run_subshells ~sw subshell_q word in
1002 if !subshell_q then res @ run_subshells ~sw subshell_q rest
1003 else Ast.WordSingleQuoted res :: run_subshells ~sw subshell_q rest
1004 | v :: rest -> v :: run_subshells ~sw ran_subshell rest
1005 in
1006 Eio.Switch.run @@ fun sw -> run_subshells ~sw (ref false) wcs
1007
1008 and handle_word_cst_subshell (ctx : ctx) wcs : Ast.word_cst =
1009 if needs_subshelling wcs then begin
1010 let wcs = handle_subshell ctx wcs in
1011 wcs
1012 end
1013 else wcs
1014
1015 and glob_expand ctx wc =
1016 let wc = handle_word_cst_subshell ctx wc in
1017 if Ast.has_glob wc && not ctx.options.no_path_expansion then
1018 Ast.word_components_to_string wc |> fun pattern ->
1019 Glob.glob_dir ~pattern (cwd_of_ctx ctx)
1020 |> List.map (fun w -> [ Ast.WordName w ])
1021 else [ wc ]
1022
1023 and word_glob_expand (ctx : ctx) wc : Ast.word_cst list =
1024 if List.exists needs_glob_expansion wc then glob_expand ctx wc
1025 else [ handle_word_cst_subshell ctx wc ]
1026
1027 and collect_assignments ?(update = true) ctx vs : ctx Exit.t =
1028 List.fold_left
1029 (fun ctx prefix ->
1030 match ctx with
1031 | Exit.Nonzero _ -> ctx
1032 | Exit.Zero ctx -> (
1033 match prefix with
1034 | Ast.Prefix_assignment (Name param, v) -> (
1035 (* Expand the values *)
1036 let ctx, v = expand_cst ctx v in
1037 match ctx with
1038 | Exit.Nonzero _ as ctx -> ctx
1039 | Exit.Zero ctx -> (
1040 let v = handle_subshell ctx v in
1041 let state =
1042 if update then S.update ctx.state ~param v
1043 else Ok ctx.state
1044 in
1045 match state with
1046 | Error message -> Exit.nonzero ~message ctx 1
1047 | Ok state ->
1048 Exit.zero
1049 {
1050 ctx with
1051 state;
1052 local_state =
1053 (param, Ast.word_components_to_string v)
1054 :: ctx.local_state;
1055 }))
1056 | _ -> Exit.zero ctx))
1057 (Exit.zero ctx) vs
1058
1059 and args ctx swc : ctx Exit.t * Ast.word_cst list =
1060 List.fold_left
1061 (fun (ctx, acc) -> function
1062 | Ast.Suffix_redirect _ -> (ctx, acc)
1063 | Suffix_word wc -> (
1064 match ctx with
1065 | Exit.Nonzero _ as ctx -> (ctx, acc)
1066 | Exit.Zero ctx -> (
1067 let ctx, cst = expand_cst ctx wc in
1068 match ctx with
1069 | Exit.Nonzero _ as ctx -> (ctx, acc)
1070 | Exit.Zero c as ctx -> (ctx, acc @ word_glob_expand c cst))))
1071 (Exit.zero ctx, [])
1072 swc
1073
1074 and handle_built_in ~rdrs ~(stdout : Eio_unix.sink_ty Eio.Flow.sink)
1075 (ctx : ctx) v =
1076 let rdrs = ctx.rdrs @ rdrs in
1077 Eunix.with_redirections rdrs @@ fun () ->
1078 match v with
1079 | Built_ins.Cd { path } ->
1080 let cwd = S.cwd ctx.state in
1081 let+ state =
1082 match path with
1083 | Some p ->
1084 let fp = Fpath.append cwd (Fpath.v p) in
1085 if Eio.Path.is_directory (ctx.fs / Fpath.to_string fp) then
1086 Exit.zero @@ S.set_cwd ctx.state fp
1087 else
1088 Exit.nonzero_msg ~exit_code:1 ctx.state
1089 "cd: not a directory: %a" Fpath.pp fp
1090 | None -> (
1091 match Eunix.find_env "HOME" with
1092 | None -> Exit.nonzero_msg ctx.state "HOME not set"
1093 | Some p -> Exit.zero (S.set_cwd ctx.state @@ Fpath.v p))
1094 in
1095 { ctx with state }
1096 | Pwd ->
1097 let () =
1098 Eio.Flow.copy_string
1099 (Fmt.str "%a\n%!" Fpath.pp (S.cwd ctx.state))
1100 stdout
1101 in
1102 Exit.zero ctx
1103 | Exit n ->
1104 let should_exit =
1105 { Exit.default_should_exit with interactive = `Yes }
1106 in
1107 Exit.nonzero ~should_exit ctx n
1108 | Set { update; print_options } ->
1109 let v =
1110 Exit.zero
1111 { ctx with options = Built_ins.Options.update ctx.options update }
1112 in
1113 if print_options then
1114 Eio.Flow.copy_string
1115 (Fmt.str "%a" Built_ins.Options.pp ctx.options)
1116 stdout;
1117 v
1118 | Wait i -> (
1119 match Unix.waitpid [] i with
1120 | _, WEXITED 0 -> Exit.zero ctx
1121 | _, (WEXITED n | WSIGNALED n | WSTOPPED n) -> Exit.nonzero ctx n)
1122 | Dot file -> (
1123 match resolve_program ctx file with
1124 | ctx, None -> Exit.nonzero ctx 127
1125 | ctx, Some f ->
1126 let program = Ast.of_file (ctx.fs / f) in
1127 let ctx, _ = run (Exit.zero ctx) program in
1128 ctx)
1129 | Unset names -> (
1130 match names with
1131 | `Variables names ->
1132 let state =
1133 List.fold_left
1134 (fun t param -> S.remove ~param t |> snd)
1135 ctx.state names
1136 in
1137 Exit.zero { ctx with state }
1138 | `Functions names ->
1139 let functions =
1140 List.fold_left
1141 (fun t param -> List.remove_assoc param t)
1142 ctx.functions names
1143 in
1144 Exit.zero { ctx with functions })
1145 | Hash v -> (
1146 match v with
1147 | Built_ins.Hash_remove -> Exit.zero { ctx with hash = Hash.empty }
1148 | Built_ins.Hash_stats ->
1149 Eio.Flow.copy_string (Fmt.str "%a" Hash.pp ctx.hash) stdout;
1150 Exit.zero ctx
1151 | _ -> assert false)
1152 | Alias | Unalias -> Exit.zero ctx (* Morbig handles this for us *)
1153 | Eval args ->
1154 let script = String.concat " " args in
1155 let ast = Ast.of_string script in
1156 let ctx, _ = run (Exit.zero ctx) ast in
1157 ctx
1158 | Echo args ->
1159 let str = String.concat " " args ^ "\n" in
1160 Eio.Flow.copy_string str stdout;
1161 Exit.zero ctx
1162 | Trap (action, signals) ->
1163 let saved_ctx = ctx in
1164 let action =
1165 match action with
1166 | Action m ->
1167 let ast = Ast.of_string m in
1168 let f _ =
1169 saved_ctx.signal_handler.run @@ fun () ->
1170 let _, _ = run (Exit.zero saved_ctx) ast in
1171 ()
1172 in
1173 Sys.Signal_handle f
1174 | Default -> Sys.Signal_default
1175 | Ignore -> Sys.Signal_ignore
1176 | Int _ -> assert false
1177 in
1178 Exit.zero
1179 @@ List.fold_left
1180 (fun ctx signal ->
1181 match signal with
1182 | `Exit ->
1183 let action =
1184 match action with
1185 | Sys.Signal_default | Sys.Signal_ignore -> None
1186 | Sys.Signal_handle f -> Some (fun () -> f 0)
1187 in
1188 { ctx with exit_handler = action }
1189 | `Signal signal ->
1190 let action =
1191 (* Handle sigint separately for interactive mode *)
1192 match (action, signal) with
1193 | Sys.Signal_default, Eunix.Signals.Interrupt ->
1194 if ctx.interactive then Sys.Signal_ignore else action
1195 | _ -> action
1196 in
1197 let setting_sigint =
1198 ctx.signal_handler.sigint_set = false
1199 &&
1200 match action with
1201 | Sys.Signal_handle _ -> true
1202 | _ -> false
1203 in
1204 Sys.set_signal (Eunix.Signals.to_int signal) action;
1205 {
1206 ctx with
1207 signal_handler =
1208 { ctx.signal_handler with sigint_set = setting_sigint };
1209 })
1210 ctx signals
1211 | Command _ ->
1212 (* Handled separately *)
1213 assert false
1214
1215 and exec initial_ctx ((command, sep) : Ast.complete_command) =
1216 let rec loop : Eio.Switch.t -> ctx -> Ast.clist -> ctx Exit.t =
1217 fun sw ctx -> function
1218 | Nlist.Singleton (c, sep) ->
1219 let async =
1220 match sep with Semicolon -> false | Ampersand -> true
1221 in
1222 handle_and_or ~sw ~async ctx c
1223 | Nlist.Cons ((c, sep), cs) -> (
1224 let async =
1225 match sep with Semicolon -> false | Ampersand -> true
1226 in
1227 match handle_and_or ~sw ~async ctx c with
1228 | Exit.Zero ctx -> loop sw ctx cs
1229 | v -> v)
1230 in
1231 match sep with
1232 | Some Semicolon | None ->
1233 Eio.Switch.run @@ fun sw -> loop sw initial_ctx command
1234 | Some Ampersand ->
1235 Fiber.fork ~sw:initial_ctx.async_switch (fun () ->
1236 Fiber.yield ();
1237 let _ : ctx Exit.t =
1238 loop initial_ctx.async_switch initial_ctx command
1239 in
1240 ());
1241 Exit.zero initial_ctx
1242
1243 and execute ctx ast = exec ctx ast
1244
1245 and run ctx ast =
1246 (* Make the shell its own process group *)
1247 Eunix.make_process_group ();
1248 let ctx, cs =
1249 let rec loop_commands (ctx, cs) (c : Ast.complete_commands) =
1250 match c with
1251 | [] -> (ctx, cs)
1252 | command :: commands -> (
1253 let ctx = Exit.value ctx in
1254 (* For our sanity *)
1255 let has_async = Ast.has_async command in
1256 if has_async && not ctx.options.async then begin
1257 Fmt.epr
1258 "You are using asynchronous operators and [set -o async] has \
1259 not been called.\n\
1260 %!";
1261 exit ctx 1
1262 end;
1263 let exit =
1264 try execute ctx command
1265 with
1266 | Eio.Io (Eio.Process.E (Eio.Process.Executable_not_found m), _ctx)
1267 ->
1268 Exit.nonzero_msg ctx ~exit_code:127 "command not found: %s" m
1269 in
1270 match exit with
1271 | Exit.Nonzero { exit_code; message; should_exit; _ } -> (
1272 Option.iter (Fmt.epr "%s\n%!") message;
1273 match
1274 ( should_exit.interactive,
1275 should_exit.non_interactive,
1276 ctx.subshell,
1277 ctx.interactive,
1278 commands )
1279 with
1280 | `Yes, _, false, true, [] | _, `Yes, false, false, [] ->
1281 if should_exit.interactive = `Yes then Fmt.epr "exit\n%!";
1282 Stdlib.exit exit_code
1283 | _ -> loop_commands (exit, c :: cs) commands)
1284 | Exit.Zero _ as ctx -> loop_commands (ctx, c :: cs) commands)
1285 in
1286 loop_commands (ctx, []) ast
1287 in
1288 (ctx, List.rev cs)
1289end