···2525 let ast =
2626 match (file, command_flag, rest) with
2727 | None, false, _ -> assert false
2828- | Some file, false, _ -> Merry.Ast.of_file Eio.Path.(env#fs / file)
2828+ | Some file, false, _ ->
2929+ Merry.Debug.Log.debug (fun f -> f "msh executing %s" file);
3030+ Merry.Ast.of_file Eio.Path.(env#fs / file)
2931 | _, true, c :: _ -> Merry.Ast.of_string c
3032 | _, b, cs -> Fmt.failwith "Bad usage: %b %a" b Fmt.(list string) cs
3133 in
···5557let setup_log style_renderer level =
5658 Fmt_tty.setup_std_outputs ?style_renderer ();
5759 Logs.set_level level;
6060+ let override = Sys.getenv_opt "MSH_DEBUG" in
6161+ let level = match override with Some _ -> Some Logs.Debug | None -> level in
5862 Logs.Src.set_level Merry.Debug.src level;
5963 Logs.set_reporter (Logs_fmt.reporter ());
6064 ()
+16-6
src/lib/ast.ml
···765765 "This is an error in Merry, subshells should already have been \
766766 expanded by now!"
767767 | v ->
768768- Fmt.failwith "Conversion of %a" Yojson.Safe.pp
768768+ Fmt.failwith "conversion of %a" Yojson.Safe.pp
769769 (word_component_to_yojson v)
770770771771and word_components_to_strings ?(field_splitting = true) ws =
···839839840840 let empty = make ""
841841 let to_string { txt; _ } = txt
842842- let join ~sep f1 f2 = { f1 with txt = f1.txt ^ sep ^ f2.txt }
842842+843843+ let join ~sep f1 f2 =
844844+ {
845845+ f1 with
846846+ txt = f1.txt ^ sep ^ f2.txt;
847847+ globbable = f1.globbable || f2.globbable;
848848+ }
849849+843850 let join_list ~sep fs = List.fold_left (join ~sep) empty fs |> to_string
844851845852 let pp_join ppf = function
···854861 let handle_joins cst =
855862 let rec loop = function
856863 | [] -> []
857857- | x :: { txt; join = `With_previous; _ } :: rest ->
858858- loop ({ x with txt = x.txt ^ txt } :: rest)
859859- | { txt; join = `With_next; _ } :: y :: rest ->
860860- { y with txt = txt ^ y.txt } :: loop rest
864864+ | x :: { txt; join = `With_previous; globbable; _ } :: rest ->
865865+ loop
866866+ ({ x with txt = x.txt ^ txt; globbable = x.globbable || globbable }
867867+ :: rest)
868868+ | { txt; join = `With_next; globbable; _ } :: y :: rest ->
869869+ { y with txt = txt ^ y.txt; globbable = globbable || y.globbable }
870870+ :: loop rest
861871 | x :: xs -> x :: loop xs
862872 in
863873 loop cst
+58-1
src/lib/built_ins.ml
···9292 | Echo of string list
9393 | Trap of trap * [ `Signal of Eunix.Signals.t | `Exit ] list
9494 | Return of int
9595+ | Umask of int option
9696+ | Shift of int option
95979898+let reserved = [ "fg"; "bg"; "jobs" ]
9699let pp_args = Fmt.(list ~sep:(Fmt.any " ") string)
9710098101let to_string = function
···111114 | Unalias -> "unalias"
112115 | Eval s -> Fmt.str "eval %a" pp_args s
113116 | Echo s -> Fmt.str "echo %a" pp_args s
114114- | Trap _ -> "trap"
117117+ | Umask None -> "umask"
118118+ | Umask (Some i) -> Fmt.str "umask %o" i
119119+ | Shift None -> "shift"
120120+ | Shift (Some i) -> Fmt.str "shift %o" i
121121+ | Trap (trap, _) ->
122122+ Fmt.str "trap %s"
123123+ (match trap with
124124+ | Int i -> string_of_int i
125125+ | Action a -> a
126126+ | Ignore -> "ignore"
127127+ | Default -> "default")
115128 | Set _ -> "set"
116129117130(* Change Directory *)
···446459 Cmd.v info term
447460end
448461462462+module Umask = struct
463463+ open Cmdliner
464464+465465+ let mask =
466466+ let doc = "Mask for file creation." in
467467+ Arg.(value & pos 0 (some string) None & info [] ~docv:"MASK" ~doc)
468468+469469+ let t =
470470+ let make_umask i =
471471+ Umask (Option.map (fun i -> Scanf.sscanf i "%o" Fun.id) i)
472472+ in
473473+ let term = Term.(const make_umask $ mask) in
474474+ let info =
475475+ let doc = "Get or set the file mode creation mask." in
476476+ Cmd.info "umask" ~doc
477477+ in
478478+ Cmd.v info term
479479+end
480480+481481+module Shift = struct
482482+ open Cmdliner
483483+484484+ let mask =
485485+ let doc = "shift positional parameters by n." in
486486+ Arg.(value & pos 0 (some int) None & info [] ~docv:"N" ~doc)
487487+488488+ let t =
489489+ let make_shift i = Shift i in
490490+ let term = Term.(const make_shift $ mask) in
491491+ let info =
492492+ let doc = "Shift positional parameters." in
493493+ Cmd.info "shift" ~doc
494494+ in
495495+ Cmd.v info term
496496+end
497497+449498let of_args (w : string list) =
450499 let open Cmdliner in
451500 let exec_cmd cmd v =
···473522 | "echo" :: _ as cmd -> exec_cmd cmd Echo.t
474523 | "trap" :: _ as cmd -> exec_cmd cmd Trap.t
475524 | "return" :: _ as cmd -> exec_cmd cmd Return.t
525525+ | "umask" :: _ as cmd -> exec_cmd cmd Umask.t
526526+ | "shift" :: _ as cmd -> exec_cmd cmd Shift.t
527527+ | cmd :: _ ->
528528+ if List.mem cmd reserved then begin
529529+ Debug.Log.err (fun f -> f "Unimplemented built-in: %s" cmd);
530530+ Some (Error (Fmt.str "Unimplemented built-in: %s" cmd))
531531+ end
532532+ else None
476533 | _ -> None
+2
src/lib/built_ins.mli
···5151 | Echo of string list
5252 | Trap of trap * [ `Signal of Eunix.Signals.t | `Exit ] list
5353 | Return of int
5454+ | Umask of int option
5555+ | Shift of int option
54565557val to_string : t -> string
5658(** Serialises a built-in to a string *)
+15-4
src/lib/eunix.ml
···1111let put_env ~key ~value =
1212 Eio_unix.run_in_systhread ~label:"put_env" @@ fun () -> Unix.putenv key value
13131414-let get_user_and_host () =
1515- Eio_unix.run_in_systhread ~label:"get_user_and_host" @@ fun () ->
1616- let name =
1717- try Unix.getlogin () with Unix.Unix_error (Unix.ENOENT, _, _) -> "root"
1414+let get_user_and_host fs =
1515+ let passwd =
1616+ Eio.Path.(load (fs / "/etc/passwd"))
1717+ |> String.split_on_char '\n'
1818+ |> List.map (String.split_on_char ':')
1919+ in
2020+ let uid = Unix.getuid () in
2121+ let username =
2222+ List.find_map
2323+ (function
2424+ | name :: _ :: m :: _ ->
2525+ if int_of_string m = uid then Some name else None
2626+ | _ -> None)
2727+ passwd
1828 in
2929+ let name = Option.value ~default:"?" username in
1930 let host = Unix.gethostname () in
2031 Fmt.str "%s@%s" name host
2132
+163-92
src/lib/eval.ml
···66open Import
77open Exit.Syntax
8899+let pp_args = Fmt.(list ~sep:(Fmt.any " ") string)
1010+1111+let pp_fs_create ppf (v : Eio.Fs.create) =
1212+ match v with
1313+ | `If_missing o -> Fmt.pf ppf "if-missing %o" o
1414+ | `Exclusive o -> Fmt.pf ppf "exclusive %o" o
1515+ | `Or_truncate o -> Fmt.pf ppf "or-truncate %o" o
1616+ | `Never -> Fmt.pf ppf "never"
1717+918(** An evaluator over the AST *)
1019module Make (S : Types.State) (E : Types.Exec) = struct
1120 (* What follows uses the POSIX definition of what a shell does ($ 2.1).
···4049 signal_handler : signal_handler;
4150 exit_handler : (unit -> unit) option;
4251 in_double_quotes : bool;
5252+ umask : int;
4353 }
44544555 let _stdin ctx = ctx.stdin
···4757 let make_ctx ?(interactive = false) ?(subshell = false) ?(local_state = [])
4858 ?(background_jobs = []) ?(last_background_process = "") ?(functions = [])
4959 ?(rdrs = []) ?exit_handler ?(options = Built_ins.Options.default)
5050- ?(hash = Hash.empty) ?(in_double_quotes = false) ~fs ~stdin ~stdout
5151- ~async_switch ~program ~argv ~signal_handler state executor =
6060+ ?(hash = Hash.empty) ?(in_double_quotes = false) ?(umask = 0o22) ~fs
6161+ ~stdin ~stdout ~async_switch ~program ~argv ~signal_handler state executor
6262+ =
5263 let signal_handler = { run = signal_handler; sigint_set = false } in
5364 {
5465 interactive;
···7182 signal_handler;
7283 exit_handler;
7384 in_double_quotes;
8585+ umask;
7486 }
75877688 let state ctx = ctx.state
···100112 let fd_of_int ?(close_unix = true) ~sw n =
101113 Eio_unix.Fd.of_unix ~close_unix ~sw (Obj.magic n : Unix.file_descr)
102114103103- let handle_one_redirection ~sw ctx = function
104104- | Ast.IoRedirect_IoFile (n, (op, file)) -> (
105105- match op with
106106- | Io_op_less ->
107107- (* Simple redirection for input *)
108108- let r = Eio.Path.open_in ~sw (ctx.fs / word_cst_to_string file) in
109109- let fd = Eio_unix.Resource.fd_opt r |> Option.get in
110110- [ Types.Redirect (n, fd, `Blocking) ]
111111- | Io_op_lessand -> (
112112- match file with
113113- | [ WordLiteral "-" ] ->
114114- if n = 0 then [ Types.Close Eio_unix.Fd.stdin ]
115115- else
116116- let fd = fd_of_int ~sw n in
117117- [ Types.Close fd ]
118118- | [ WordLiteral m ] when Option.is_some (int_of_string_opt m) ->
119119- let m = int_of_string m in
120120- [
121121- Types.Redirect
122122- (n, fd_of_int ~close_unix:false ~sw m, `Blocking);
123123- ]
124124- | _ -> [])
125125- | (Io_op_great | Io_op_dgreat) as v ->
126126- (* Simple file creation *)
127127- let append = v = Io_op_dgreat in
128128- let create =
129129- if append then `Never
130130- else if ctx.options.noclobber then `Exclusive 0o644
131131- else `Or_truncate 0o644
132132- in
133133- let w =
134134- Eio.Path.open_out ~sw ~append ~create
135135- (ctx.fs / word_cst_to_string file)
136136- in
137137- let fd = Eio_unix.Resource.fd_opt w |> Option.get in
138138- [ Types.Redirect (n, fd, `Blocking) ]
139139- | Io_op_greatand -> (
140140- match file with
141141- | [ WordLiteral "-" ] ->
142142- if n = 0 then [ Types.Close Eio_unix.Fd.stdout ]
143143- else
144144- let fd = fd_of_int ~sw n in
145145- [ Types.Close fd ]
146146- | [ WordLiteral m ] when Option.is_some (int_of_string_opt m) ->
147147- let m = int_of_string m in
148148- [
149149- Types.Redirect
150150- (n, fd_of_int ~close_unix:false ~sw m, `Blocking);
151151- ]
152152- | _ -> [])
153153- | Io_op_andgreat ->
154154- (* Yesh, not very POSIX *)
155155- (* Simple file creation *)
156156- let w =
157157- Eio.Path.open_out ~sw ~create:(`If_missing 0o644)
158158- (ctx.fs / word_cst_to_string file)
159159- in
160160- let fd = Eio_unix.Resource.fd_opt w |> Option.get in
161161- [
162162- Types.Redirect (1, fd, `Blocking);
163163- Types.Redirect (2, fd, `Blocking);
164164- ]
165165- | Io_op_clobber ->
166166- let w =
167167- Eio.Path.open_out ~sw ~create:(`Or_truncate 0o644)
168168- (ctx.fs / word_cst_to_string file)
169169- in
170170- let fd = Eio_unix.Resource.fd_opt w |> Option.get in
171171- [ Types.Redirect (n, fd, `Blocking) ]
172172- | Io_op_lessgreat -> Fmt.failwith "<> not support yet.")
173173- | Ast.IoRedirect_IoHere _ ->
174174- Fmt.failwith "HERE documents not yet implemented!"
175175-176176- let handle_redirections ~sw ctx rdrs =
177177- try Ok (List.concat_map (handle_one_redirection ~sw ctx) rdrs)
178178- with Eio.Io (Eio.Fs.E (Already_exists _), _) ->
179179- Fmt.epr "msh: cannot overwrite existing file\n%!";
180180- Error ctx
181181-115115+ let file_creation_mode ctx = 0o666 - ctx.umask
182116 let cwd_of_ctx ctx = S.cwd ctx.state |> Fpath.to_string |> ( / ) ctx.fs
183117184118 let resolve_program ?(update = true) ctx name =
···265199 (ctx, Error (127, `Not_found))
266200 | _, (ctx, Some full_path) ->
267201 Debug.Log.debug (fun f ->
268268- f "executing %a" Fmt.(list string) (executable :: args));
202202+ f "executing %a"
203203+ Fmt.(list ~sep:(Fmt.any " ") string)
204204+ (full_path :: args));
269205 ( ctx,
270206 E.exec ctx.executor ~delay_reap:(fst reap) ~fds ?stdin ~stdout
271207 ~pgid ~mode ~cwd:(cwd_of_ctx ctx)
···351287 [] suffix
352288 |> List.rev
353289 in
354354- match handle_redirections ~sw:pipeline_switch ctx rdrs with
290290+ match handle_redirections ~sw:ctx.async_switch ctx rdrs with
355291 | Error ctx -> (ctx, handle_job job (`Rdr (Exit.nonzero () 1)))
356292 | Ok rdrs -> (
357293 match Built_ins.of_args (executable :: args) with
···375311 handle_job job
376312 (`Built_in (updated >|= fun _ -> ()))
377313 in
314314+ Debug.Log.debug (fun f ->
315315+ f "export %a" pp_args args);
378316 loop (Exit.value updated) job stdout_of_previous
379317 rest
380318 | "readonly" ->
···385323 handle_job job
386324 (`Built_in (updated >|= fun _ -> ()))
387325 in
326326+ Debug.Log.debug (fun f ->
327327+ f "readonly %a" pp_args args);
388328 loop (Exit.value updated) job stdout_of_previous
389329 rest
390330 | "local" ->
···397337 in
398338 loop (Exit.value updated) job stdout_of_previous
399339 rest
340340+ | "exec" ->
341341+ Debug.Log.debug (fun f ->
342342+ f "exec [%a] [%a]" pp_args args
343343+ Fmt.(list Types.pp_redirect)
344344+ rdrs);
345345+ if args <> [] then
346346+ Fmt.invalid_arg
347347+ "Exec with args not yet supported...";
348348+ ({ ctx with rdrs }, job)
400349 | _ -> (
401350 let saved_ctx = ctx in
402351 let func_app =
···551500 }
552501 end
553502503503+ and handle_one_redirection ~sw ctx = function
504504+ | Ast.IoRedirect_IoFile (n, (op, file)) -> (
505505+ let _ctx, file = word_expansion ctx file in
506506+ let file = Ast.Fragment.join_list ~sep:"" file in
507507+ match op with
508508+ | Io_op_less ->
509509+ (* Simple redirection for input *)
510510+ let r = Eio.Path.open_in ~sw (ctx.fs / file) in
511511+ let fd = Eio_unix.Resource.fd_opt r |> Option.get in
512512+ [ Types.Redirect (n, fd, `Blocking) ]
513513+ | Io_op_lessand -> (
514514+ match file with
515515+ | "-" ->
516516+ if n = 0 then [ Types.Close Eio_unix.Fd.stdin ]
517517+ else
518518+ let fd = fd_of_int ~sw n in
519519+ [ Types.Close fd ]
520520+ | m when Option.is_some (int_of_string_opt m) ->
521521+ let m = int_of_string m in
522522+ [
523523+ Types.Redirect
524524+ (n, fd_of_int ~close_unix:false ~sw m, `Blocking);
525525+ ]
526526+ | _ -> [])
527527+ | (Io_op_great | Io_op_dgreat) as v ->
528528+ (* Simple file creation *)
529529+ let append = v = Io_op_dgreat in
530530+ let create =
531531+ if append then `If_missing (file_creation_mode ctx)
532532+ else if ctx.options.noclobber then
533533+ `Exclusive (file_creation_mode ctx)
534534+ else `Or_truncate (file_creation_mode ctx)
535535+ in
536536+ Debug.Log.debug (fun f ->
537537+ f "Creating file (append:%b, %a): %s" append pp_fs_create create
538538+ file);
539539+ let w = Eio.Path.open_out ~sw ~append ~create (ctx.fs / file) in
540540+ let fd = Eio_unix.Resource.fd_opt w |> Option.get in
541541+ [ Types.Redirect (n, fd, `Blocking) ]
542542+ | Io_op_greatand -> (
543543+ match file with
544544+ | "-" ->
545545+ if n = 0 then [ Types.Close Eio_unix.Fd.stdout ]
546546+ else
547547+ let fd = fd_of_int ~sw n in
548548+ [ Types.Close fd ]
549549+ | m when Option.is_some (int_of_string_opt m) ->
550550+ let m = int_of_string m in
551551+ [
552552+ Types.Redirect
553553+ (n, fd_of_int ~close_unix:false ~sw m, `Blocking);
554554+ ]
555555+ | _ -> [])
556556+ | Io_op_andgreat ->
557557+ (* Yesh, not very POSIX *)
558558+ (* Simple file creation *)
559559+ let w =
560560+ Eio.Path.open_out ~sw
561561+ ~create:(`If_missing (file_creation_mode ctx))
562562+ (ctx.fs / file)
563563+ in
564564+ let fd = Eio_unix.Resource.fd_opt w |> Option.get in
565565+ [
566566+ Types.Redirect (1, fd, `Blocking);
567567+ Types.Redirect (2, fd, `Blocking);
568568+ ]
569569+ | Io_op_clobber ->
570570+ let w =
571571+ Eio.Path.open_out ~sw
572572+ ~create:(`Or_truncate (file_creation_mode ctx))
573573+ (ctx.fs / file)
574574+ in
575575+ let fd = Eio_unix.Resource.fd_opt w |> Option.get in
576576+ [ Types.Redirect (n, fd, `Blocking) ]
577577+ | Io_op_lessgreat -> Fmt.failwith "<> not support yet.")
578578+ | Ast.IoRedirect_IoHere _ ->
579579+ Fmt.failwith "HERE documents not yet implemented!"
580580+581581+ and handle_redirections ~sw ctx rdrs =
582582+ try Ok (List.concat_map (handle_one_redirection ~sw ctx) rdrs)
583583+ with Eio.Io (Eio.Fs.E (Already_exists _), _) ->
584584+ Fmt.epr "msh: cannot overwrite existing file\n%!";
585585+ Error ctx
586586+554587 and parameter_expansion ctx ast : ctx Exit.t * Ast.fragment list list =
555588 let get_prefix ~pattern ~kind param =
556589 let _, prefix =
···801834 in
802835 expand ctx ast
803836837837+ and split_fields ifs s =
838838+ let v, ls =
839839+ String.fold_left
840840+ (fun (so_far, ls) c ->
841841+ if String.contains ifs c then ("", so_far :: ls)
842842+ else (so_far ^ String.make 1 c, ls))
843843+ ("", []) s
844844+ in
845845+ List.rev (v :: ls)
846846+804847 and field_splitting ctx = function
805848 | [] -> []
806806- | Ast.{ splittable = true; txt; _ } :: rest ->
807807- (String.split_on_char ' ' txt |> List.map Ast.Fragment.make)
808808- @ field_splitting ctx rest
849849+ | Ast.{ splittable = true; txt; globbable; _ } :: rest -> (
850850+ match S.lookup ctx.state ~param:"IFS" with
851851+ | Some "" -> [ Ast.Fragment.make ~globbable txt ]
852852+ | (None | Some _) as ifs ->
853853+ let ifs = Option.value ~default:" \t\n" ifs in
854854+ (split_fields ifs txt |> List.map (Ast.Fragment.make ~globbable))
855855+ @ field_splitting ctx rest)
809856 | txt :: rest -> txt :: field_splitting ctx rest
810857811858 and word_expansion' ctx cst : ctx Exit.t * Ast.fragments list =
···10651112 match List.assoc_opt name ctx.functions with
10661113 | None -> None
10671114 | Some commands ->
11151115+ Debug.Log.debug (fun f ->
11161116+ f "function enter: %s [%a]" name
11171117+ Fmt.(list ~sep:Fmt.(any " ") string)
11181118+ argv);
10681119 let ctx = { ctx with argv = Array.of_list argv } in
10691069- Option.some @@ (handle_compound_command ctx commands >|= fun _ -> ctx)
11201120+ let v =
11211121+ Option.some @@ (handle_compound_command ctx commands >|= fun _ -> ctx)
11221122+ in
11231123+ Debug.Log.debug (fun f -> f "function leave: %s" name);
11241124+ v
1070112510711126 and command_substitution (ctx : ctx) (cc : Ast.complete_commands) =
10721127 let exec_subshell ctx s =
···12031258 | Dot file -> (
12041259 match resolve_program ctx file with
12051260 | ctx, None -> Exit.nonzero ctx 127
12061206- | ctx, Some f ->
12071207- let program = Ast.of_file (ctx.fs / f) in
12081208- let ctx, _ = run (Exit.zero ctx) program in
12611261+ | ctx, Some fname ->
12621262+ Debug.Log.debug (fun f -> f "sourcing...");
12631263+ let program = Ast.of_file (ctx.fs / fname) in
12641264+ let ctx, _ =
12651265+ run' ~make_process_group:false (Exit.zero ctx) program
12661266+ in
12671267+ Debug.Log.debug (fun f -> f "finished sourcing %s" fname);
12091268 ctx)
12101269 | Unset names -> (
12111270 match names with
···12891348 { ctx.signal_handler with sigint_set = setting_sigint };
12901349 })
12911350 ctx signals
13511351+ | Umask None ->
13521352+ let str = Fmt.str "0%o\n" ctx.umask in
13531353+ Eio.Flow.copy_string str stdout;
13541354+ Exit.zero ctx
13551355+ | Umask (Some i) -> Exit.zero { ctx with umask = i }
13561356+ | Shift n ->
13571357+ let n = Option.value ~default:1 n in
13581358+ let new_len = Array.length ctx.argv - n in
13591359+ assert (new_len >= 0);
13601360+ let argv = Array.init new_len (fun i -> Array.get ctx.argv (i + n)) in
13611361+ Exit.zero { ctx with argv }
12921362 | Command _ ->
12931363 (* Handled separately *)
12941364 assert false
···13221392 Exit.zero initial_ctx
1323139313241394 and execute ctx ast = exec ctx ast
13951395+ and run ctx ast = run' ~make_process_group:true ctx ast
1325139613261326- and run ctx ast =
13971397+ and run' ?(make_process_group = true) ctx ast =
13271398 (* Make the shell its own process group *)
13281328- Eunix.make_process_group ();
13991399+ if make_process_group then Eunix.make_process_group ();
13291400 let ctx, cs =
13301401 let rec loop_commands (ctx, cs) (c : Ast.complete_commands) =
13311402 match c with