diff --git a/src/bin/main.ml b/src/bin/main.ml index 80e0a63..7405f16 100644 --- a/src/bin/main.ml +++ b/src/bin/main.ml @@ -27,6 +27,7 @@ let sh ~command ~dump ~file ~rest env = argv = Array.of_list (pos_zero :: rest); program = pos_zero; functions = []; + hash = Merry.Hash.empty; } in match (file, command) with diff --git a/src/lib/built_ins.ml b/src/lib/built_ins.ml index 28bda36..d2cb40a 100644 --- a/src/lib/built_ins.ml +++ b/src/lib/built_ins.ml @@ -34,6 +34,7 @@ module Options = struct end type set = { update : Options.option list; print_options : bool } +type hash = Hash_remove | Hash_stats | Hash_add of string list (* Built-in Actions *) type t = @@ -44,6 +45,7 @@ type t = | Wait of int | Dot of string (* a.k.a source *) | Unset of [ `Variables of string list | `Functions of string list ] + | Hash of hash (* Change Directory *) module Cd = struct @@ -159,8 +161,32 @@ module Unset = struct in let term = Term.(const make_unset $ kind $ names) in let info = - let doc = "Wait for a particular PID (default is 0)" in - Cmd.info "wait" ~doc + let doc = "Unset names of variables or functions." in + Cmd.info "unset" ~doc + in + Cmd.v info term +end + +module Hash = struct + open Cmdliner + + let remove = + let doc = "Empty the location table." in + Arg.(value & flag & info [ "r" ] ~docv:"REMOVE" ~doc) + + let utilities = + let doc = "Utilities to search for and add to the location table." in + Arg.(value & pos_all string [] & info [] ~docv:"UTILITIES" ~doc) + + let t = + let make_hash remove pos_all = + if remove then Hash Hash_remove + else match pos_all with [] -> Hash Hash_stats | us -> Hash (Hash_add us) + in + let term = Term.(const make_hash $ remove $ utilities) in + let info = + let doc = "Remember or report utility locations." in + Cmd.info "hash" ~doc in Cmd.v info term end @@ -212,4 +238,5 @@ let of_args (w : string list) = | "source" :: _ as cmd -> exec_cmd cmd Source.t | "." :: _ as cmd -> exec_cmd cmd Dot.t | "unset" :: _ as cmd -> exec_cmd cmd Unset.t + | "hash" :: _ as cmd -> exec_cmd cmd Hash.t | _ -> None diff --git a/src/lib/built_ins.mli b/src/lib/built_ins.mli index 49f5e7d..d8892ff 100644 --- a/src/lib/built_ins.mli +++ b/src/lib/built_ins.mli @@ -11,6 +11,7 @@ module Options : sig end type set = { update : Options.option list; print_options : bool } +type hash = Hash_remove | Hash_stats | Hash_add of string list type t = | Cd of { path : string option } @@ -21,6 +22,7 @@ type t = | Wait of int | Dot of string | Unset of [ `Variables of string list | `Functions of string list ] + | Hash of hash val of_args : string list -> (t, string) result option (** Parses a command-line to the built-ins, errors are returned if parsing. *) diff --git a/src/lib/eval.ml b/src/lib/eval.ml index 2bf3ad8..f192690 100644 --- a/src/lib/eval.ml +++ b/src/lib/eval.ml @@ -43,6 +43,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct program : string; argv : string array; functions : (string * Ast.compound_command) list; + hash : Hash.t; } let clear_local_state ctx = { ctx with local_state = [] } @@ -157,6 +158,25 @@ module Make (S : Types.State) (E : Types.Exec) = struct let apply_pair (a, b) f = f a b let ( ||> ) = apply_pair + let resolve_program ?(update = true) ctx name = + let v = + if not (String.contains name '/') then + Sys.getenv_opt "PATH" + |> Option.value ~default:"/bin:/usr/bin" + |> String.split_on_char ':' + |> List.find_map (fun dir -> + let p = Filename.concat dir name in + if Sys.file_exists p then Some p else None) + else if Sys.file_exists name then Some name + else None + in + match (update, v) with + | true, Some loc -> + let hash = Hash.add ~utility:name ~loc ctx.hash in + ({ ctx with hash }, Some loc) + | false, Some loc -> (ctx, Some loc) + | _, None -> (ctx, None) + let get_env ?(extra = []) ctx = let extra = extra @@ -193,12 +213,18 @@ module Make (S : Types.State) (E : Types.Exec) = struct Eio.Flow.close some_write end in - let exec_process ctx job ?fds ?stdin ~stdout ~pgid args = - let process = - E.exec ctx.executor ?fds ?stdin ~stdout ~pgid ~mode - ~cwd:(cwd_of_ctx ctx) - ~env:(get_env ~extra:ctx.local_state ctx) - args + let exec_process ctx job ?fds ?stdin ~stdout ~pgid executable args = + let ctx, process = + match resolve_program ctx executable with + | ctx, None -> + Fmt.epr "msh: command not found: %s\n%!" executable; + (ctx, Error (127, `Not_found)) + | ctx, Some full_path -> + ( ctx, + E.exec ctx.executor ?fds ?stdin ~stdout ~pgid ~mode + ~cwd:(cwd_of_ctx ctx) + ~env:(get_env ~extra:ctx.local_state ctx) + ~executable:full_path (executable :: args) ) in match process with | Error (n, _) -> @@ -243,10 +269,11 @@ module Make (S : Types.State) (E : Types.Exec) = struct in match Built_ins.of_args (executable :: args_as_strings) with | Some (Ok bi) -> - let ctx = handle_built_in ctx bi in + let ctx = handle_built_in ~stdout:some_write ctx bi in + close_stdout ~is_global some_write; let built_in = ctx >|= fun _ -> () in let job = handle_job ~pgid job (`Built_in built_in) in - loop (Exit.value ctx) job (pgid, stdout_of_previous) rest + loop (Exit.value ctx) job (pgid, some_read) rest | Some (Error _) -> (ctx, handle_job ~pgid job (`Built_in (Exit.nonzero () 1))) | None -> ( @@ -289,16 +316,16 @@ module Make (S : Types.State) (E : Types.Exec) = struct | None -> let ctx, job = exec_process ctx job ~fds:redirect - ~stdout:some_write ~pgid - (executable :: args_as_strings) + ~stdout:some_write ~pgid executable + args_as_strings in close_stdout ~is_global some_write; loop ctx job (pgid, some_read) rest | Some stdout -> let ctx, job = exec_process ctx job ~fds:redirect ~stdin:stdout - ~stdout:some_write ~pgid - (executable :: args_as_strings) + ~stdout:some_write ~pgid executable + args_as_strings in close_stdout ~is_global some_write; loop ctx job (pgid, some_read) rest)))) @@ -320,14 +347,17 @@ module Make (S : Types.State) (E : Types.Exec) = struct process that last just until all of the processes are setup. *) let ctx, job = let ghost_process = - E.exec ~mode:(Types.Switched pipeline_switch) ~pgid:0 - ~cwd:(cwd_of_ctx initial_ctx) initial_ctx.executor - [ "sleep"; "99999999" ] - |> function - | Ok p -> p - | Error (n, `Not_found) -> - Fmt.epr "Interal error ghost process: not found"; - exit n + match resolve_program ~update:false initial_ctx "sleep" with + | _, None -> Fmt.failwith "Sleep not found\n%!" + | ctx, Some sleep -> ( + E.exec ~mode:(Types.Switched pipeline_switch) ~pgid:0 + ~cwd:(cwd_of_ctx ctx) ctx.executor ~executable:sleep + [ "sleep"; "99999999" ] + |> function + | Ok p -> p + | Error (n, `Not_found) -> + Fmt.epr "Interal error ghost process: not found"; + exit n) in loop initial_ctx None (E.pid ghost_process, None) p in @@ -672,6 +702,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct | Ast.Prefix_assignment (Name param, v) -> (* Expand the values *) let ctx, v = expand_cst ctx v in + let v = handle_subshell ctx v in let state = if update then S.update ctx.state ~param v else ctx.state in @@ -693,7 +724,8 @@ module Make (S : Types.State) (E : Types.Exec) = struct (ctx, acc @ word_glob_expand ctx cst)) (ctx, []) swc - and handle_built_in (ctx : ctx) = function + and handle_built_in ~(stdout : Eio_unix.sink_ty Eio.Flow.sink) (ctx : ctx) = + function | Built_ins.Cd { path } -> let cwd = S.cwd ctx.state in let+ state = @@ -708,7 +740,9 @@ module Make (S : Types.State) (E : Types.Exec) = struct in { ctx with state } | Pwd -> - Fmt.pr "%a\n%!" Fpath.pp (S.cwd ctx.state); + Eio.Flow.copy_string + (Fmt.str "%a\n%!" Fpath.pp (S.cwd ctx.state)) + stdout; Exit.zero ctx | Exit n -> let should_exit = @@ -720,27 +754,19 @@ module Make (S : Types.State) (E : Types.Exec) = struct Exit.zero { ctx with options = Built_ins.Options.update ctx.options update } in - if print_options then Fmt.pr "%a%!" Built_ins.Options.pp ctx.options; + if print_options then + Eio.Flow.copy_string + (Fmt.str "%a" Built_ins.Options.pp ctx.options) + stdout; v | Wait i -> ( match Unix.waitpid [] i with | _, WEXITED 0 -> Exit.zero ctx | _, (WEXITED n | WSIGNALED n | WSTOPPED n) -> Exit.nonzero ctx n) | Dot file -> ( - let resolve_program name = - if not (String.contains name '/') then - Sys.getenv_opt "PATH" - |> Option.value ~default:"/bin:/usr/bin" - |> String.split_on_char ':' - |> List.find_map (fun dir -> - let p = Filename.concat dir name in - if Sys.file_exists p then Some p else None) - else if Sys.file_exists name then Some name - else None - in - match resolve_program file with - | None -> Exit.nonzero ctx 127 - | Some f -> + match resolve_program ctx file with + | ctx, None -> Exit.nonzero ctx 127 + | ctx, Some f -> let program = Ast.of_file (ctx.fs / f) in let ctx, _ = run (Exit.zero ctx) program in ctx) @@ -760,6 +786,13 @@ module Make (S : Types.State) (E : Types.Exec) = struct ctx.functions names in Exit.zero { ctx with functions }) + | Hash v -> ( + match v with + | Built_ins.Hash_remove -> Exit.zero { ctx with hash = Hash.empty } + | Built_ins.Hash_stats -> + Eio.Flow.copy_string (Fmt.str "%a" Hash.pp ctx.hash) stdout; + Exit.zero ctx + | _ -> assert false) and exec initial_ctx ((command, sep) : Ast.complete_command) = let rec loop : Eio.Switch.t -> ctx -> Ast.clist -> ctx Exit.t = diff --git a/src/lib/hash.ml b/src/lib/hash.ml new file mode 100644 index 0000000..073c2f3 --- /dev/null +++ b/src/lib/hash.ml @@ -0,0 +1,25 @@ +(* The hash table for utility locations *) + +module M = Map.Make (String) + +type entry = { hits : int; loc : string } +type t = entry M.t + +let empty = M.empty + +let add ~utility ~loc t = + match M.find_opt utility t with + | Some { hits; loc = loc' } when String.equal loc loc' -> + M.add utility { hits = hits + 1; loc } t + | None | Some _ -> M.add utility { hits = 1; loc } t + +let lookup ~utility t = M.find_opt utility t |> Option.map (fun v -> v.loc) + +let pp ppf t = + let entries = M.to_list t in + match entries with + | [] -> () + | _ -> + let pp_entry ppf (_, { hits; loc }) = Fmt.pf ppf "%-7i %s@." hits loc in + let pp_header ppf () = Fmt.pf ppf "%-7s %s@." "hits" "command" in + Fmt.pf ppf "@[%a%a@]" pp_header () Fmt.(list pp_entry) entries diff --git a/src/lib/hash.mli b/src/lib/hash.mli new file mode 100644 index 0000000..f6ebff6 --- /dev/null +++ b/src/lib/hash.mli @@ -0,0 +1,14 @@ +type t +(** A lookup table for utilities *) + +val empty : t +(** The empty table *) + +val add : utility:string -> loc:string -> t -> t +(** [add ~utility ~loc t] adds the [utility] with [loc] location. *) + +val lookup : utility:string -> t -> string option +(** [lookup ~utility t] will try to find [utility] in [t]. *) + +val pp : t Fmt.t +(** A pretty printer *) diff --git a/src/lib/merry.ml b/src/lib/merry.ml index d98fd00..175ae93 100644 --- a/src/lib/merry.ml +++ b/src/lib/merry.ml @@ -1,4 +1,5 @@ module Import = Import +module Hash = Hash module Exit = Exit module Eunix = Eunix module Ast = Ast diff --git a/src/lib/merry.mli b/src/lib/merry.mli index 9a7e5f8..b5df63f 100644 --- a/src/lib/merry.mli +++ b/src/lib/merry.mli @@ -1,4 +1,5 @@ module Ast = Ast +module Hash = Hash module Exit = Exit module Eunix = Eunix module Types = Types diff --git a/src/lib/posix/exec.ml b/src/lib/posix/exec.ml index 2cd616c..24a0883 100644 --- a/src/lib/posix/exec.ml +++ b/src/lib/posix/exec.ml @@ -128,17 +128,6 @@ let process = let handler = Eio.Process.Pi.process (module Process_impl) in fun proc -> Eio.Resource.T (proc, handler) -let resolve_program name = - if not (String.contains name '/') then - Sys.getenv_opt "PATH" - |> Option.value ~default:"/bin:/usr/bin" - |> String.split_on_char ':' - |> List.find_map (fun dir -> - let p = Filename.concat dir name in - if Sys.file_exists p then Some p else None) - else if Sys.file_exists name then Some name - else None - let read_of_fd ~mode ~default ~to_close v = match (mode, v) with | Merry.Types.Async, _ | _, None -> default @@ -186,10 +175,7 @@ let get_executable ~args = function | None -> ( match args with | [] -> invalid_arg "Arguments list is empty and no executable given!" - | x :: _ -> ( - match resolve_program x with - | Some x -> x - | None -> raise (Eio.Process.err (Executable_not_found x)))) + | x :: _ -> x) let get_env = function Some e -> e | None -> Unix.environment () diff --git a/src/lib/posix/merry_posix.ml b/src/lib/posix/merry_posix.ml index a1ae698..2f3cacd 100644 --- a/src/lib/posix/merry_posix.ml +++ b/src/lib/posix/merry_posix.ml @@ -16,7 +16,7 @@ module Exec = struct | `Signaled n -> Merry.Exit.nonzero () n let exec ?(fork_actions = []) ?(fds = []) ?stdin ?stdout ?stderr ?env ~mode - ~pgid ~cwd t args = + ~pgid ~cwd ~executable t args = let env = Option.map (fun lst -> List.map (fun (a, b) -> a ^ "=" ^ b) lst |> Array.of_list) @@ -25,7 +25,7 @@ module Exec = struct try Ok (Exec.run ~fork_actions ~mode ~fds ~pgid ~cwd ?stdin ?stdout ?stderr - ?env t args) + ?env t ~executable args) with Eio.Io (Eio.Process.E (Eio.Process.Executable_not_found m), _ctx) -> Fmt.epr "msh: command not found: %s\n%!" m; Error (127, `Not_found) diff --git a/src/lib/types.ml b/src/lib/types.ml index bcdab91..3dac835 100644 --- a/src/lib/types.ml +++ b/src/lib/types.ml @@ -70,6 +70,7 @@ module type Exec = sig mode:exec_mode -> pgid:int -> cwd:Eio.Fs.dir_ty Eio.Path.t -> + executable:string -> t -> string list -> (process, int * [ `Not_found ]) result diff --git a/test/built_ins.t b/test/built_ins.t index abe31c4..29079e3 100644 --- a/test/built_ins.t +++ b/test/built_ins.t @@ -93,3 +93,53 @@ Testing some shell built-ins HEY msh: command not found: shout [127] + +5. Hash + + $ cat > test.sh << EOF + > reproducible_hash () { + > hash | sed 's/|/ /' | awk '{print \$1, \$8}' + > } + > ls + > ls + > reproducible_hash + > hash -r + > reproducible_hash + > EOF + + $ sh test.sh + hello.txt + run.sh + test.sh + test_bad.sh + test_good.sh + testing + hello.txt + run.sh + test.sh + test_bad.sh + test_good.sh + testing + hits + 2 + $ msh test.sh + hello.txt + run.sh + test.sh + test_bad.sh + test_good.sh + testing + hello.txt + run.sh + test.sh + test_bad.sh + test_good.sh + testing + hits + 2 +6. Built-in redirection and pipelining + + $ sh -c "FOO=\$(pwd | rev | rev); echo \$(basename \$FOO)" + test + $ msh -c "FOO=\$(pwd | rev | rev); echo \$(basename \$FOO)" + test