···77 let interactive = Option.is_none file && Option.is_none command in
88 let pos_zero = match file with Some f -> f | None -> "msh" in
99 Eio.Switch.run @@ fun async_switch ->
1010+ let signal_handler f = Eio_posix.run @@ fun _ -> f () in
1011 let ctx =
1112 C.
1213 {
···3031 functions = [];
3132 hash = Merry.Hash.empty;
3233 rdrs = [];
3434+ signal_handler = { run = signal_handler; sigint_set = false };
3535+ exit_handler = None;
3336 }
3437 in
3538 match (file, command) with
+61
src/lib/built_ins.ml
···59596060type set = { update : Options.option list; print_options : bool }
6161type hash = Hash_remove | Hash_stats | Hash_add of string list
6262+type trap = Int of int | Action of string | Ignore | Default
62636364type t =
6465 (* Built-in Actions *)
···7576 | Unalias
7677 | Eval of string list
7778 | Echo of string list
7979+ | Trap of trap * [ `Signal of Eunix.Signals.t | `Exit ] list
78807981(* Change Directory *)
8082module Cd = struct
···316318 Cmd.v info term
317319end
318320321321+module Trap = struct
322322+ open Cmdliner
323323+324324+ let action =
325325+ let doc =
326326+ "Either an integer number or an action to invoke on a particular set of \
327327+ signals."
328328+ in
329329+ let action_conv =
330330+ let parser s =
331331+ match int_of_string_opt s with
332332+ | Some n -> Ok (Int n)
333333+ | None -> (
334334+ match s with
335335+ | "\"\"" -> Ok Ignore
336336+ | "-" -> Ok Default
337337+ | s -> Ok (Action s))
338338+ in
339339+ let pp ppf v =
340340+ match v with
341341+ | Int i -> Fmt.pf ppf "%i" i
342342+ | Action p -> Fmt.pf ppf "%s" p
343343+ | Ignore -> Fmt.pf ppf "\"\""
344344+ | Default -> Fmt.pf ppf "-"
345345+ in
346346+ Arg.Conv.make ~docv:"ACTION" ~parser ~pp ()
347347+ in
348348+ Arg.(required & pos 0 (some action_conv) None & info [] ~docv:"ACTION" ~doc)
349349+350350+ let conditions =
351351+ let doc = "Conditions to run the action on." in
352352+ let cond_conv =
353353+ let parser s =
354354+ let s = String.lowercase_ascii s in
355355+ try
356356+ match int_of_string_opt s with
357357+ | Some n -> Ok (`Signal (Eunix.Signals.of_int n))
358358+ | None -> (
359359+ match s with
360360+ | "exit" -> Ok `Exit
361361+ | s -> Ok (`Signal (Eunix.Signals.of_string s)))
362362+ with Invalid_argument m -> Error m
363363+ in
364364+ let pp _ppf _ = () in
365365+ Arg.Conv.make ~docv:"CONDITION" ~parser ~pp ()
366366+ in
367367+ Arg.(value & pos_right 0 cond_conv [] & info [] ~docv:"CONDITIONS" ~doc)
368368+369369+ let t =
370370+ let make_trap action sigs = Trap (action, sigs) in
371371+ let term = Term.(const make_trap $ action $ conditions) in
372372+ let info =
373373+ let doc = "Display a line of text." in
374374+ Cmd.info "echo" ~doc
375375+ in
376376+ Cmd.v info term
377377+end
378378+319379module Source = Make_dot (struct
320380 let name = "source"
321381end)
···349409 | "unalias" :: _ -> Some (Ok Unalias)
350410 | "eval" :: _ as cmd -> exec_cmd cmd Eval.t
351411 | "echo" :: _ as cmd -> exec_cmd cmd Echo.t
412412+ | "trap" :: _ as cmd -> exec_cmd cmd Trap.t
352413 | _ -> None
+2
src/lib/built_ins.mli
···28282929type set = { update : Options.option list; print_options : bool }
3030type hash = Hash_remove | Hash_stats | Hash_add of string list
3131+type trap = Int of int | Action of string | Ignore | Default
31323233type t =
3334 | Cd of { path : string option }
···4445 | Unalias
4546 | Eval of string list
4647 | Echo of string list
4848+ | Trap of trap * [ `Signal of Eunix.Signals.t | `Exit ] list
47494850val of_args : string list -> (t, string) result option
4951(** Parses a command-line to the built-ins, errors are returned if parsing. *)
+49
src/lib/eunix.ml
···9595 Fun.protect
9696 ~finally:(fun () -> Unix.tcsetattr Unix.stdin TCSADRAIN saved_tio)
9797 fn
9898+9999+module Signals = struct
100100+ type t =
101101+ | Interrupt
102102+ | Quit
103103+ | Abort
104104+ | Kill
105105+ | Alarm
106106+ | Terminate
107107+ | Exit
108108+ | Stop
109109+ | Hup
110110+ [@@deriving to_yojson]
111111+112112+ let of_int = function
113113+ | i when Int.equal i Sys.sigint -> Interrupt
114114+ | i when Int.equal i Sys.sigquit -> Quit
115115+ | i when Int.equal i Sys.sigabrt -> Abort
116116+ | i when Int.equal i Sys.sigkill -> Kill
117117+ | i when Int.equal i Sys.sigalrm -> Alarm
118118+ | i when Int.equal i Sys.sigterm -> Terminate
119119+ | i when Int.equal i Sys.sigstop -> Stop
120120+ | i when Int.equal i Sys.sighup -> Hup
121121+ | m -> Fmt.invalid_arg "Signal %i not supported yet." m
122122+123123+ let to_int = function
124124+ | Interrupt -> Sys.sigint
125125+ | Quit -> Sys.sigquit
126126+ | Abort -> Sys.sigabrt
127127+ | Kill -> Sys.sigkill
128128+ | Alarm -> Sys.sigalrm
129129+ | Terminate | Exit -> Sys.sigterm
130130+ | Stop -> Sys.sigstop
131131+ | Hup -> Sys.sighup
132132+133133+ let of_string s =
134134+ match String.uppercase_ascii s with
135135+ | "SIGINT" | "INT" -> Interrupt
136136+ | "SIGQUIT" | "QUIT" -> Quit
137137+ | "SIGABRT" | "ABRT" -> Abort
138138+ | "SIGKILL" | "KILL" -> Kill
139139+ | "SIGALRM" | "ALRM" -> Alarm
140140+ | "SIGTERM" | "TERM" -> Terminate
141141+ | "SIGSTOP" | "STOP" -> Stop
142142+ | "SIGHUP" | "HUP" -> Hup
143143+ | m -> Fmt.invalid_arg "Signal %s not supported or recognised." m
144144+145145+ let raise v = Unix.kill (Unix.getpid ()) (to_int v)
146146+end
+95-13
src/lib/eval.ml
···2929 method list f t = List.map f t
3030 end
31313232+ type signal_handler = { run : (unit -> unit) -> unit; sigint_set : bool }
3333+3234 type ctx = {
3335 interactive : bool;
3436 subshell : bool;
···4749 functions : (string * Ast.compound_command) list;
4850 hash : Hash.t;
4951 rdrs : Types.redirect list;
5252+ signal_handler : signal_handler;
5353+ exit_handler : (unit -> unit) option;
5054 }
51555256 let clear_local_state ctx = { ctx with local_state = [] }
···257261 let s_len = String.length s in
258262 if s.[0] = '"' && s.[s_len - 1] = '"' then String.sub s 1 (s_len - 2) else s
259263264264+ let exit ctx code =
265265+ Option.iter (fun f -> f ()) ctx.exit_handler;
266266+ exit code
267267+260268 let rec handle_pipeline ~async initial_ctx p : ctx Exit.t =
261269 let set_last_background ~async process ctx =
262270 if async then
···274282 | `Rdr p -> J.add_rdr p j
275283 | `Built_in p -> J.add_built_in p j
276284 | `Error p -> J.add_error p j
285285+ | `Exit p -> J.add_exit p j
277286 in
278287 let close_stdout ~is_global some_write =
279288 if not is_global then begin
···452461 ctx >|= fun ctx -> clear_local_state ctx
453462 in
454463 close_stdout ~is_global some_write;
455455- let built_in = ctx >|= fun _ -> () in
456464 let job =
457457- handle_job job (`Built_in built_in)
465465+ match bi with
466466+ | Built_ins.Exit _ ->
467467+ let v_ctx = Exit.value ctx in
468468+ if not v_ctx.subshell then
469469+ exit v_ctx (Exit.code ctx)
470470+ else
471471+ handle_job job
472472+ (`Exit (Exit.ignore ctx))
473473+ | _ ->
474474+ handle_job job
475475+ (`Built_in (Exit.ignore ctx))
458476 in
459477 loop (Exit.value ctx) job some_read rest
460478 | _ -> (
···515533 in
516534 let ctx = ctx >|= fun ctx -> clear_local_state ctx in
517535 close_stdout ~is_global some_write;
518518- let built_in = ctx >|= fun _ -> () in
519519- let job = handle_job job (`Built_in built_in) in
536536+ let job =
537537+ match bi with
538538+ | Built_ins.Exit _ ->
539539+ let v_ctx = Exit.value ctx in
540540+ if not v_ctx.subshell then begin
541541+ if (Exit.value ctx).interactive then
542542+ Fmt.pr "exit\n%!";
543543+ exit v_ctx (Exit.code ctx)
544544+ end
545545+ else handle_job job (`Exit (Exit.ignore ctx))
546546+ | _ -> handle_job job (`Built_in (Exit.ignore ctx))
547547+ in
520548 loop (Exit.value ctx) job some_read rest))))
521549 | CompoundCommand (c, rdrs) :: rest -> (
522550 match handle_redirections ~sw:pipeline_switch ctx rdrs with
···533561 loop ctx job None rest
534562 | [] -> (clear_local_state ctx, job)
535563 in
536536- (* HACK: when running the pipeline, we need a process group to
537537- put everything in. Eio's model of execution is nice, but we cannot
538538- safely delay execution of a process. So instead we create a ghost
539539- process that last just until all of the processes are setup. *)
540564 Eio.Switch.run @@ fun sw ->
541565 let initial_job = J.make 0 [] in
542542- let ctx, job = loop sw initial_ctx initial_job None p in
566566+ let saved_ctx = initial_ctx in
567567+ let subshell = saved_ctx.subshell || List.length p > 1 in
568568+ let ctx = { initial_ctx with subshell } in
569569+ let ctx, job = loop sw ctx initial_job None p in
543570 match job.processes with
544571 | [] -> Exit.zero ctx
545572 | _ :: _ ->
546573 if not async then begin
547547- J.await_exit ~pipefail:false ~interactive:ctx.interactive job
548548- >|= fun () -> ctx
574574+ J.await_exit ~pipefail:ctx.options.pipefail
575575+ ~interactive:ctx.interactive job
576576+ >|= fun () -> { ctx with subshell = saved_ctx.subshell }
549577 end
550578 else begin
551551- Exit.zero { ctx with background_jobs = job :: ctx.background_jobs }
579579+ Exit.zero
580580+ {
581581+ ctx with
582582+ background_jobs = job :: ctx.background_jobs;
583583+ subshell = saved_ctx.subshell;
584584+ }
552585 end
553586554587 and parameter_expansion' ctx ast =
···11461179 let str = String.concat " " args ^ "\n" in
11471180 Eio.Flow.copy_string str stdout;
11481181 Exit.zero ctx
11821182+ | Trap (action, signals) ->
11831183+ let saved_ctx = ctx in
11841184+ let action =
11851185+ match action with
11861186+ | Action m ->
11871187+ let ast = Ast.of_string m in
11881188+ let f _ =
11891189+ saved_ctx.signal_handler.run @@ fun () ->
11901190+ let _, _ = run (Exit.zero saved_ctx) ast in
11911191+ ()
11921192+ in
11931193+ Sys.Signal_handle f
11941194+ | Default -> Sys.Signal_default
11951195+ | Ignore -> Sys.Signal_ignore
11961196+ | Int _ -> assert false
11971197+ in
11981198+ Exit.zero
11991199+ @@ List.fold_left
12001200+ (fun ctx signal ->
12011201+ match signal with
12021202+ | `Exit ->
12031203+ let action =
12041204+ match action with
12051205+ | Sys.Signal_default | Sys.Signal_ignore -> None
12061206+ | Sys.Signal_handle f -> Some (fun () -> f 0)
12071207+ in
12081208+ { ctx with exit_handler = action }
12091209+ | `Signal signal ->
12101210+ let action =
12111211+ (* Handle sigint separately for interactive mode *)
12121212+ match (action, signal) with
12131213+ | Sys.Signal_default, Eunix.Signals.Interrupt ->
12141214+ if ctx.interactive then Sys.Signal_ignore else action
12151215+ | _ -> action
12161216+ in
12171217+ let setting_sigint =
12181218+ ctx.signal_handler.sigint_set = false
12191219+ &&
12201220+ match action with
12211221+ | Sys.Signal_handle _ -> true
12221222+ | _ -> false
12231223+ in
12241224+ Sys.set_signal (Eunix.Signals.to_int signal) action;
12251225+ {
12261226+ ctx with
12271227+ signal_handler =
12281228+ { ctx.signal_handler with sigint_set = setting_sigint };
12291229+ })
12301230+ ctx signals
11491231 | Command _ ->
11501232 (* Handled separately *)
11511233 assert false
···11961278 "You are using asynchronous operators and [set -o async] has \
11971279 not been called.\n\
11981280 %!";
11991199- exit 1
12811281+ exit ctx 1
12001282 end;
12011283 let exit =
12021284 try execute ctx command
···8282 Sys.set_signal Sys.sigttou Sys.Signal_ignore;
8383 Sys.set_signal Sys.sigttin Sys.Signal_ignore;
8484 Sys.set_signal Sys.sigtstp Sys.Signal_ignore;
8585+ Sys.set_signal Sys.sigint Sys.Signal_ignore;
8586 let rec loop (ctx : Eval.ctx Exit.t) =
8687 Option.iter (Fmt.epr "%s%!")
8788 (S.lookup (Exit.value ctx).state ~param:"PS1"
···9495 exit 0
9596 | String (Some c) ->
9697 let ast = Ast.of_string (String.trim c) in
9898+ Fmt.pr "\n%!";
9799 let ctx', _ast = Eval.run ctx ast in
98100 add_history c;
99101 loop ctx'
100102 | Ctrl_c ->
101103 let c = Exit.value ctx in
102102- loop (Exit.nonzero c 130)
104104+ Eunix.Signals.(raise Interrupt);
105105+ if c.signal_handler.sigint_set then loop (Exit.zero c)
106106+ else begin
107107+ Fmt.pr "\n%!";
108108+ loop (Exit.nonzero c 130)
109109+ end
103110 in
104111 loop initial_ctx
105112end
+3-1
src/lib/job.ml
···77 processes :
88 [ `Process of E.process
99 | `Built_in of unit Exit.t
1010+ | `Exit of unit Exit.t
1011 | `Rdr of unit Exit.t
1112 | `Error of int ]
1213 list;
···26272728 let add_error b t = { t with processes = List.cons (`Error b) t.processes }
2829 let add_rdr b t = { t with processes = List.cons (`Rdr b) t.processes }
3030+ let add_exit b t = { t with processes = List.cons (`Exit b) t.processes }
29313032 (* Section 2.9.2 https://pubs.opengroup.org/onlinepubs/9799919799/ *)
3133 let await_exit ~pipefail ~interactive t =
···3638 if interactive then
3739 Eunix.delegate_control ~pgid:t.id @@ fun () -> E.await p
3840 else E.await p
3939- | `Built_in b | `Rdr b -> b
4141+ | `Built_in b | `Exit b | `Rdr b -> b
4042 | `Error n -> Exit.nonzero () n
4143 in
4244 match (pipefail, t.processes) with