Select the types of activity you want to include in your feed.
Improve exit code handling
This introduces a `'a Exit.t` type for handling exit codes during the execution of a series of commands. This is useful because unlike a `Result.t` a non-zero exit code does not always mean _unrecoverable error_.
···11(* A shell... one day *)
22-open Merry.Import
32module C = Merry.Eval.Make (Merry_posix.State) (Merry_posix.Exec)
43module I = Merry.Interactive.Make (Merry_posix.State) (Merry_posix.Exec)
5465let sh ~command ~dump ~file env =
76 let executor = Merry_posix.Exec.{ mgr = env#process_mgr } in
77+ let interactive = Option.is_none file && Option.is_none command in
88 let ctx =
99 C.
1010 {
1111+ interactive;
1112 state =
1213 Merry_posix.State.make
1314 ~home:(Sys.getenv "HOME" ^ "/")
1414- (Fpath.v (Eunix.cwd ()));
1515+ (Fpath.v (Merry.Eunix.cwd ()));
1516 executor;
1617 fs = env#fs;
1718 options = Merry.Eval.Options.default;
1819 }
1920 in
2021 match (file, command) with
2121- | None, None -> I.run ctx
2222+ | None, None -> I.run (Merry.Exit.zero ctx)
2223 | _ ->
2324 let ast =
2425 match (file, command) with
···2829 in
2930 if dump then Merry.Ast.Dump.pp Fmt.stdout ast
3031 else
3131- let _ctx, _ast = C.run ctx ast in
3232+ let _ctx, _ast = C.run (Merry.Exit.zero ctx) ast in
3233 ()
33343435open Cmdliner
···7374 sh ~command ~dump ~file env
74757576let main () = Eio_posix.run @@ fun env -> Cmd.eval (cmd env)
7676-let () = if !Sys.interactive then () else exit (main ())
7777+7878+let () =
7979+ Fmt_tty.setup_std_outputs ();
8080+ if !Sys.interactive then () else exit (main ())
+27
src/lib/eunix.ml
···11+let cwd () = Eio_unix.run_in_systhread ~label:"cwd" @@ fun () -> Unix.getcwd ()
22+33+let chdir p =
44+ try
55+ let dir =
66+ Eio_unix.run_in_systhread ~label:"chdir" @@ fun () -> Unix.chdir p
77+ in
88+ Exit.zero dir
99+ with Unix.Unix_error (Unix.ENOENT, _, _) ->
1010+ Exit.nonzero_msg () "no such file or directory: %s" p
1111+1212+let env () =
1313+ Eio_unix.run_in_systhread ~label:"env" @@ fun () ->
1414+ Unix.environment ()
1515+ |> Array.map (Astring.String.cut ~sep:"=")
1616+ |> Array.to_list |> List.filter_map Fun.id
1717+1818+let find_env k = env () |> List.assoc_opt k
1919+2020+let put_env ~key ~value =
2121+ Eio_unix.run_in_systhread ~label:"put_env" @@ fun () -> Unix.putenv key value
2222+2323+let get_user_and_host () =
2424+ Eio_unix.run_in_systhread ~label:"get_user_and_host" @@ fun () ->
2525+ let name = Unix.getlogin () in
2626+ let host = Unix.gethostname () in
2727+ Fmt.str "%s@%s" name host
+51-39
src/lib/eval.ml
···33 SPDX-License-Identifier: ISC
44 -----------------------------------------------------------------*)
55open Import
66+open Exit.Syntax
6778module Options = struct
89 type t = { noclobber : bool }
···3435 end
35363637 type ctx = {
3838+ interactive : bool;
3739 state : S.t;
3840 executor : E.t;
3941 fs : Eio.Fs.dir_ty Eio.Path.t;
···177179 let handle_built_in (ctx : ctx) = function
178180 | Built_ins.Cd { path } ->
179181 let cwd = S.cwd ctx.state in
180180- let new_cwd =
182182+ let+ state =
181183 match path with
182184 | Some p ->
183185 let fp = Fpath.append cwd (Fpath.v p) in
184184- Eunix.chdir p;
185185- fp
186186- | None -> Fpath.v ""
186186+ Exit.map' (Eunix.chdir p)
187187+ ~zero:(fun () -> S.set_cwd ctx.state fp)
188188+ ~nonzero:(fun () -> ctx.state)
189189+ | None -> (
190190+ match Eunix.find_env "HOME" with
191191+ | None -> Exit.nonzero_msg ctx.state "HOME not set"
192192+ | Some p -> Exit.zero (S.set_cwd ctx.state @@ Fpath.v p))
187193 in
188188- let state = S.set_cwd ctx.state new_cwd in
189189- ({ ctx with state }, Some 0)
194194+ { ctx with state }
190195 | Pwd ->
191196 Fmt.pr "%s\n%!" (Eunix.cwd ());
192192- (ctx, Some 0)
197197+ Exit.zero ctx
193198194199 let cwd_of_ctx ctx = S.cwd ctx.state |> Fpath.to_string |> ( / ) ctx.fs
195200196201 let execute_commands initial_ctx local_switch p =
197197- let rec loop ctx (status_of_previous, stdout_of_previous) :
198198- Ast.command list -> ctx * int option = function
202202+ let rec loop ctx
203203+ ((status_of_previous, stdout_of_previous) :
204204+ ctx Exit.t * Eio_unix.source_ty Eio_unix.source option) :
205205+ Ast.command list -> ctx Exit.t = function
199206 | Ast.SimpleCommand (Prefixed _) :: next ->
200207 loop ctx (status_of_previous, stdout_of_previous) next
201208 | Ast.SimpleCommand (Named (executable, None)) :: rest -> (
···209216 in
210217 match stdout_of_previous with
211218 | None ->
212212- ( ctx,
219219+ let+ () =
213220 E.exec ctx.executor ?stdout:some_write ~cwd:(cwd_of_ctx ctx)
214221 (List.map Ast.word_component_to_string executable)
215215- |> Option.some )
222222+ in
223223+ ctx
216224 | Some stdout ->
217225 let res =
218226 E.exec ctx.executor ~stdin:stdout ?stdout:some_write
219227 ~cwd:(cwd_of_ctx ctx)
220228 (List.map Ast.word_component_to_string executable)
229229+ >|= fun () -> ctx
221230 in
222231 Option.iter Eio.Flow.close some_write;
223223- loop ctx (Some res, some_read) rest))
232232+ loop ctx (res, some_read) rest))
224233 | Ast.SimpleCommand (Named (executable, Some suffix)) :: rest -> (
225234 let args =
226235 List.filter_map
···255264 E.exec ~fds:redirect ctx.executor ?stdout:some_write
256265 ~cwd:(cwd_of_ctx ctx)
257266 (List.map Ast.word_component_to_string executable @ args)
258258- |> Option.some
267267+ >|= fun () -> ctx
259268 in
260269 Option.iter Eio.Flow.close some_write;
261270 loop ctx (res, some_read) rest
···264273 E.exec ~fds:redirect ctx.executor ~stdin:stdout
265274 ~cwd:(cwd_of_ctx ctx) ?stdout:some_write
266275 (List.map Ast.word_component_to_string executable @ args)
267267- |> Option.some
276276+ >|= fun () -> ctx
268277 in
269278 Option.iter Eio.Flow.close some_write;
270279 loop ctx (res, some_read) rest))
271280 | v :: _ ->
272281 Fmt.epr "TODO: %a" Yojson.Safe.pp (Ast.command_to_yojson v);
273282 failwith "Err"
274274- | [] -> (ctx, status_of_previous)
283283+ | [] -> status_of_previous
275284 in
276276- loop initial_ctx (None, None) p
285285+ loop initial_ctx (Exit.zero initial_ctx, None) p
277286278287 let handle_single_pipeline ~sw ctx c =
279288 let pipeline = function
280289 | Ast.Pipeline p -> (Fun.id, p)
281281- | Ast.Pipeline_Bang p ->
282282- ( (fun (ctx, v) ->
283283- (ctx, Option.map (fun i -> if Int.equal i 0 then -1 else 0) v)),
284284- p )
290290+ | Ast.Pipeline_Bang p -> (Exit.not, p)
285291 in
286292287293 let rec fold :
288288- Ast.and_or * (ctx * int option) ->
289289- Ast.pipeline Ast.and_or_list ->
290290- ctx * int option =
291291- fun (sep, (ctx, exit_so_far)) pipe ->
294294+ Ast.and_or * ctx Exit.t -> Ast.pipeline Ast.and_or_list -> ctx Exit.t =
295295+ fun (sep, exit_so_far) pipe ->
292296 match (sep, pipe) with
293297 | And, Nlist.Singleton (p, _) -> (
294298 match exit_so_far with
295295- | Some 0 ->
299299+ | Exit.Zero ctx ->
296300 let f, p = pipeline p in
297301 f @@ execute_commands ctx sw p
298298- | v -> (ctx, v))
302302+ | v -> v)
299303 | Or, Nlist.Singleton (p, _) -> (
300304 match exit_so_far with
301301- | Some 0 -> (ctx, Some 0)
305305+ | Exit.Zero _ as ctx -> ctx
302306 | _ ->
303307 let f, p = pipeline p in
304308 f @@ execute_commands ctx sw p)
···310314 fold (next_sep, f (execute_commands ctx sw p)) rest
311315 | And, Nlist.Cons ((p, next_sep), rest) -> (
312316 match exit_so_far with
313313- | Some 0 ->
317317+ | Exit.Zero ctx ->
314318 let f, p = pipeline p in
315319 fold (next_sep, f (execute_commands ctx sw p)) rest
316316- | (None | Some _) as v -> (ctx, v))
320320+ | Exit.Nonzero _ as v -> v)
317321 | Or, Nlist.Cons ((p, next_sep), rest) -> (
318322 match exit_so_far with
319319- | Some 0 -> fold (next_sep, (ctx, exit_so_far)) rest
320320- | None | Some _ ->
323323+ | Exit.Zero _ as exit_so_far -> fold (next_sep, exit_so_far) rest
324324+ | Exit.Nonzero _ ->
321325 let f, p = pipeline p in
322326 fold (next_sep, f (execute_commands ctx sw p)) rest)
323327 in
324324- fold (Noand_or, (ctx, None)) c
328328+ fold (Noand_or, Exit.zero ctx) c
325329326330 let exec initial_ctx (ast : Ast.complete_command) =
327331 let command, _ = ast in
328328- let rec loop : Eio.Switch.t -> ctx -> Ast.clist -> ctx * int option =
332332+ let rec loop : Eio.Switch.t -> ctx -> Ast.clist -> ctx Exit.t =
329333 fun sw ctx -> function
330334 | Nlist.Singleton (c, _) -> handle_single_pipeline ~sw ctx c
331335 | Nlist.Cons ((c, (Semicolon | Nosep)), cs) -> (
332336 match handle_single_pipeline ~sw ctx c with
333333- | ctx, Some 0 -> loop sw ctx cs
337337+ | Exit.Zero ctx -> loop sw ctx cs
334338 | v -> v)
335339 | _ -> Fmt.failwith "Background tasks not implemented yet!"
336340 in
···351355 let ctx, cs =
352356 List.fold_left
353357 (fun (ctx, cs) command ->
354354- let (ctx, exit_code), ast =
355355- expand ctx command ||> redirect ||> execute
356356- in
357357- Option.iter (function 0 -> () | n -> exit n) exit_code;
358358- (ctx, ast :: cs))
358358+ let ctx = Exit.value ctx in
359359+ let exit, ast = expand ctx command ||> redirect ||> execute in
360360+ match exit with
361361+ | Exit.Nonzero { exit_code; message; should_exit; _ } -> (
362362+ Option.iter (Fmt.epr "%s\n%!") message;
363363+ match
364364+ ( should_exit.interactive,
365365+ should_exit.non_interactive,
366366+ ctx.interactive )
367367+ with
368368+ | `Yes, _, true | _, `Yes, false -> Stdlib.exit exit_code
369369+ | _ -> (exit, ast :: cs))
370370+ | Exit.Zero _ as ctx -> (ctx, ast :: cs))
359371 (ctx, []) ast
360372 in
361373 (ctx, List.rev cs)
+59
src/lib/exit.ml
···11+(* The shell exit applicative? This is very like the result monad,
22+ except modelled around a shell's exit status codes *)
33+44+type should_exit = {
55+ interactive : [ `Yes | `No ];
66+ non_interactive : [ `Yes | `No ];
77+}
88+99+let default_should_exit = { interactive = `No; non_interactive = `Yes }
1010+1111+type 'a t =
1212+ | Zero of 'a
1313+ | Nonzero of {
1414+ value : 'a;
1515+ exit_code : int;
1616+ message : string option;
1717+ should_exit : should_exit;
1818+ }
1919+2020+let value = function Zero v -> v | Nonzero { value; _ } -> value
2121+2222+let not = function
2323+ | Zero value ->
2424+ Nonzero
2525+ {
2626+ value;
2727+ exit_code = 1;
2828+ message = None;
2929+ should_exit = default_should_exit;
3030+ }
3131+ | Nonzero { value; _ } -> Zero value
3232+3333+let zero v = Zero v
3434+3535+let nonzero ?message ?(should_exit = default_should_exit) value exit_code =
3636+ Nonzero { value; exit_code; message; should_exit }
3737+3838+let nonzero_msg ?(exit_code = 1) ?(should_exit = default_should_exit) value fmt
3939+ =
4040+ Fmt.kstr
4141+ (fun message ->
4242+ Nonzero { value; exit_code; message = Some message; should_exit })
4343+ fmt
4444+4545+let map ~f = function
4646+ | Zero v -> Zero (f v)
4747+ | Nonzero ({ value; _ } as v) -> Nonzero { v with value = f value }
4848+4949+let map' ~zero ~nonzero = function
5050+ | Zero v -> Zero (zero v)
5151+ | Nonzero v -> Nonzero { v with value = nonzero v.value }
5252+5353+let is_nonzero = function Zero _ -> false | Nonzero _ -> true
5454+let is_zero = function Zero _ -> true | Nonzero _ -> false
5555+5656+module Syntax = struct
5757+ let ( >|= ) x f = map ~f x
5858+ let ( let+ ) = ( >|= )
5959+end
-8
src/lib/import.ml
···5252end
53535454let yojson_pp = Yojson.Safe.pretty_print ~std:true
5555-5656-module Eunix = struct
5757- let cwd () =
5858- Eio_unix.run_in_systhread ~label:"cwd" @@ fun () -> Unix.getcwd ()
5959-6060- let chdir p =
6161- Eio_unix.run_in_systhread ~label:"chdir" @@ fun () -> Unix.chdir p
6262-end
+29-4
src/lib/interactive.ml
···11+let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
22+13module Make (S : Types.State) (E : Types.Exec) = struct
24 module Eval = Eval.Make (S) (E)
3544- let default_prompt s =
55- Fmt.str "%s > %!" (Fpath.normalize @@ S.cwd s |> Fpath.to_string)
66+ let pp_colored c pp fmt v = Fmt.pf fmt "%a" (Fmt.styled (`Fg c) pp) v
77+88+ let subst_tilde path =
99+ match Eunix.find_env "HOME" with
1010+ | None -> path
1111+ | Some home -> (
1212+ match Fpath.rem_prefix (Fpath.v home) path with
1313+ | Some rel -> Fpath.(v "~" // rel)
1414+ | None -> path)
1515+1616+ let default_prompt (ctx : Eval.ctx Exit.t) =
1717+ let state =
1818+ match ctx with
1919+ | Exit.Zero ctx | Exit.Nonzero { value = ctx; _ } -> ctx.state
2020+ in
2121+ let pp_status ppf = function
2222+ | Exit.Zero _ -> ()
2323+ | Exit.Nonzero { exit_code; _ } ->
2424+ Fmt.pf ppf "[%a] " (pp_colored `Red Fmt.int) exit_code
2525+ in
2626+ Fmt.pf Format.str_formatter "%a%a:%s > %!" pp_status ctx
2727+ Fmt.(pp_colored `Yellow string)
2828+ (Eunix.get_user_and_host ())
2929+ (Fpath.normalize @@ S.cwd state |> subst_tilde |> Fpath.to_string);
3030+ Format.flush_str_formatter ()
631732 let with_stdin_in_raw_mode fn =
833 let saved_tio = Unix.tcgetattr Unix.stdin in
···4065 fn
41664267 let run ?(prompt = default_prompt) initial_ctx =
4343- let rec loop (ctx : Eval.ctx) =
4444- let p = prompt ctx.state in
6868+ let rec loop (ctx : Eval.ctx Exit.t) =
6969+ let p = prompt ctx in
4570 match LNoise.linenoise p with
4671 | None -> loop ctx
4772 | Some c ->