diff --git a/src/bin/main.ml b/src/bin/main.ml index efe9c35..cba35a2 100644 --- a/src/bin/main.ml +++ b/src/bin/main.ml @@ -4,58 +4,116 @@ module I = Merry.Interactive.Make (Merry_posix.State) (Merry_posix.Exec) (Merry.History.Prefix_search) -let sh ~command_flag ~dump ~file ~rest ~options env = +type shell = { + dump : bool; + command : bool; + arguments : string list; + after_dashes : string list; + options : Merry.Built_ins.Options.t; + verbose : int; +} + +let _pp_shell ppf = + let open Merry.Import in + Fmt.( + braces + @@ record + [ + Fmt.field "dump" (fun t -> t.dump) Fmt.bool; + Fmt.field "command" (fun t -> t.command) Fmt.bool; + Fmt.field "arguments" (fun t -> t.arguments) Fmt.(lst string); + Fmt.field "after_dashes" (fun t -> t.after_dashes) Fmt.(lst string); + ]) + ppf + +let default_shell = + { + dump = false; + command = false; + arguments = []; + after_dashes = []; + options = Merry.Built_ins.Options.default; + verbose = 0; + } + +let sh ~name:_ shell env = let executor = Merry_posix.Exec.{ mgr = env#process_mgr } in - let interactive = Option.is_none file && rest = [] in - let pos_zero = match file with Some f -> f | None -> "msh" in + let interactive = shell.arguments = [] in + let pos_zero = match shell.arguments with f :: _ -> f | [] -> "msh" in Eio.Switch.run ~name:"async" @@ fun async_switch -> let signal_handler f = Eio_posix.run @@ fun _ -> f () in - let ctx = + let ctx argv = C.make_ctx ~interactive (Merry_posix.State.make ~home:(Sys.getenv "HOME" ^ "/") (Fpath.v (Merry.Eunix.cwd ()))) executor ~fs:env#fs ~stdin:env#stdin ~stdout:env#stdout ~async_switch - ~options - ~argv:(Array.of_list (pos_zero :: (try List.tl rest with _ -> []))) - ~program:pos_zero ~signal_handler + ~options:shell.options ~argv ~program:pos_zero ~signal_handler in - match (file, command_flag) with - | None, false -> I.run (Merry.Exit.zero ctx) + match (shell.arguments, shell.command) with + | [], false -> + let argv = Array.of_list (pos_zero :: shell.arguments) in + I.run (Merry.Exit.zero (ctx argv)) | _ -> - let ast = - match (file, command_flag, rest) with - | None, false, _ -> assert false - | Some file, false, _ -> + let ast, rest = + match (shell.command, shell.arguments @ shell.after_dashes) with + | false, file :: rest -> Merry.Debug.Log.debug (fun f -> f "msh executing %s" file); - Merry.Ast.of_file Eio.Path.(env#fs / file) - | _, true, c :: _ -> Merry.Ast.of_string c - | _, b, cs -> Fmt.failwith "Bad usage: %b %a" b Fmt.(list string) cs + (Merry.Ast.of_file Eio.Path.(env#fs / file), rest) + | true, c :: rest -> (Merry.Ast.of_string c, rest) + | b, cs -> Fmt.failwith "Bad usage: %b %a" b Fmt.(list string) cs in - if dump then Merry.Ast.Dump.pp Fmt.stdout ast + if shell.dump then Merry.Ast.Dump.pp Fmt.stdout ast else - let _ctx, _ast = C.run (Merry.Exit.zero ctx) ast in + let args = + if shell.command then Array.of_list rest + else begin + let rest = + match shell.after_dashes with + | [] -> shell.arguments + | some -> shell.arguments @ ("--" :: some) + in + Array.of_list rest + end + in + let _ctx, _ast = C.run (Merry.Exit.zero (ctx args)) ast in () -open Cmdliner -open Cmdliner.Term.Syntax - -let command_flag = - let doc = "Run commands from the command-line" in - Arg.(value & flag & info [ "c" ] ~doc) +(* Command line parsing for a shell is really awkward + for reasons that I do not understand. But, it means + we have to do this by hand. *) -let file = - let doc = "The shell script to execute" in - Arg.(value & pos 0 (some string) None & info [] ~doc) +exception Unknown_flag of string -let dump = - let doc = - "dump an internal AST representation to JSON (does nothing in interactive \ - mode)." +let parse_arguments argv = + let rec loop acc = function + | "--" :: rest -> { acc with after_dashes = rest } + | "-c" :: rest -> loop { acc with command = true } rest + | "-d" :: rest | "--dump" :: rest -> loop { acc with dump = true } rest + | "-e" :: rest -> + loop + { + acc with + options = + Merry.Built_ins.Options.update acc.options [ (`Errexit, true) ]; + } + rest + | "-v" :: rest -> loop { acc with verbose = acc.verbose + 1 } rest + | arg :: rest -> + (* Arguments for a command or for a script to run *) + if acc.command || match acc.arguments with _ :: _ -> true | _ -> false + then begin + loop { acc with arguments = arg :: acc.arguments } rest + end + else if + String.starts_with ~prefix:"-" arg + || String.starts_with ~prefix:"--" arg + then raise (Unknown_flag arg) + else loop { acc with arguments = arg :: acc.arguments } rest + | [] -> acc in - Arg.(value & flag & info [ "d"; "D"; "dump" ] ~doc) - -let errexit = Merry.Built_ins.Set.errexit + let t = loop default_shell argv in + { t with arguments = List.rev t.arguments } let setup_log style_renderer level = Fmt_tty.setup_std_outputs ?style_renderer (); @@ -66,82 +124,19 @@ let setup_log style_renderer level = Logs.set_reporter (Logs_fmt.reporter ()); () -let setup_log = - let docs = Manpage.s_common_options in - Term.( - const setup_log $ Fmt_cli.style_renderer ~docs () $ Logs_cli.level ~docs ()) - -let rest = Arg.(value & pos_all string [] & info []) - -let cmd ~args ~other_flags env = - let doc = "Mere's shell." in - let man = - [ - `S Manpage.s_description; - `P - "$(cmd) is first and foremost a shell written in the OCaml programming \ - language. Like most shells, you can execute some series of commands \ - either by specify a shell script file to run, by passing commands \ - using the -c flag or by omitting both of these options and entering \ - interactive mode."; - `P - "$(cmd) exists thanks to the Morbig static parser for the POSIX shell \ - syntax."; - `S Manpage.s_bugs; - `P "Report bugs at https://tangled.org/patrick.sirref.org/merry/issues."; - `S Manpage.s_authors; - `P "Patrick Ferris "; - ] - in - Cmd.make (Cmd.info "msh" ~version:"v0.0.1" ~doc ~man) - @@ - let+ command_flag = command_flag - and+ () = setup_log - and+ dump = dump - and+ errexit = errexit - and+ file = file - (* We keep this just to consume them on the command line *) - and+ _rest = rest in - let remove_flag ~condition f vs = - if condition then List.filter (fun (_, c) -> not (List.mem c f)) vs else vs - in - let args = - remove_flag ~condition:command_flag [ "-c" ] args - |> remove_flag ~condition:dump [ "-d"; "--dump" ] - |> remove_flag ~condition:true [ "-e" ] - |> remove_flag ~condition:true [ "--" ] - |> remove_flag ~condition:true [ "-v" ] - in - let rest = - args @ other_flags - |> List.sort (fun (i, _) (j, _) -> Int.compare i j) - |> List.map snd - in - let options = Merry.Built_ins.Options.(with_options ~errexit default) in - sh ~command_flag ~dump ~file ~rest ~options env - let main () = Eio_posix.run @@ fun env -> - let allowed_args = [ "-c"; "-d"; "--dump"; "--"; "-v"; "-e" ] in - let is_allowed v = - List.mem v allowed_args || not (String.starts_with ~prefix:"-" v) - in - let args, others = - let i = ref (-1) in - Array.fold_left - (fun (allowed, other) v -> - incr i; - if is_allowed v then ((!i, v) :: allowed, other) - else (allowed, (!i, v) :: other)) - ([], []) - (Array.sub Sys.argv 1 (Array.length Sys.argv - 1)) - in - let other_flags = - List.filter (fun (_, c) -> String.starts_with ~prefix:"-" c) others + let name, args = + match Array.to_list Sys.argv with + | [ name ] -> (name, []) + | name :: rest -> (name, rest) + | [] -> Fmt.failwith "Not possible to have no name!" in - let argv = Sys.argv.(0) :: List.rev (List.map snd args) |> Array.of_list in - Cmd.eval ~argv (cmd ~args ~other_flags env) + let shell_options = parse_arguments args in + if shell_options.verbose >= 2 || (Option.is_some @@ Sys.getenv_opt "MSH_DEBUG") + then setup_log (Some `Ansi_tty) (Some Debug); + sh ~name shell_options env let () = Fmt_tty.setup_std_outputs (); - if !Sys.interactive then () else exit (main ()) + if !Sys.interactive then () else main () diff --git a/src/lib/built_ins.ml b/src/lib/built_ins.ml index 74b3d8c..ae35f18 100644 --- a/src/lib/built_ins.ml +++ b/src/lib/built_ins.ml @@ -71,10 +71,21 @@ module Options = struct Fmt.pf ppf "@[%a@]" Fmt.(list ~sep:nop pp_option) opts end -type set = { update : (Options.option * bool) list; print_options : bool } +type set = { + update : (Options.option * bool) list; + arguments : string list; + print_options : bool; +} + type hash = Hash_remove | Hash_stats | Hash_add of string list type trap = Int of int | Action of string | Ignore | Default +type getopts = { + optstring : [ `Flag of char | `Arg of char ] list; + name : string; + params : string list; +} + type t = (* Built-in Actions *) | Cd of { path : string option } @@ -97,6 +108,7 @@ type t = | Umask of int option | Shift of int option | Read of bool * string list + | Getopts of getopts let reserved = [ "fg"; "bg"; "jobs" ] let pp_args = Fmt.(list ~sep:(Fmt.any " ") string) @@ -135,6 +147,17 @@ let to_string = function | Set _ -> "set" | Read (backslash, vars) -> Fmt.str "read%s %a" (if backslash then " -r" else " ") pp_args vars + | Getopts { optstring; name; params } -> + let optstring = + let rec loop = function + | [] -> "" + | `Flag f :: rest -> String.make 1 f ^ loop rest + | `Arg v :: rest -> String.make 1 v ^ ":" ^ loop rest + in + loop optstring + in + let params = String.concat " " params in + Fmt.str "getopts %s %s %s" optstring name params (* Change Directory *) module Cd = struct @@ -240,7 +263,7 @@ module Set = struct let extra = if nounset then (`Nounset, true) :: extra else extra in let extra = if errexit then (`Errexit, true) :: extra else extra in let update = extra @ update in - let unset, _args = classify_args rest in + let unset, args = classify_args rest in let unset = List.filter_map (function @@ -253,9 +276,9 @@ module Set = struct unset in let update = update @ unset in - match update with - | [] -> Set { update = []; print_options = true } - | update -> Set { update; print_options = false } + match (update, args) with + | [], [] -> Set { update = []; arguments = []; print_options = true } + | update, arguments -> Set { update; arguments; print_options = false } in let term = Term.( @@ -566,6 +589,52 @@ module Read = struct Cmd.v info term end +module Getopts = struct + open Cmdliner + + let optstring = + let doc = "Optstring." in + Arg.(required & pos 0 (some string) None & info [] ~docv:"OPTSTRING" ~doc) + + let name = + let doc = "Name." in + Arg.(required & pos 1 (some string) None & info [] ~docv:"NAME" ~doc) + + let params = + let doc = "Arguments." in + Arg.(value & pos_right 1 string [] & info [] ~docv:"ARGS" ~doc) + + let t = + let make_getopts opts name params = + let optstring = String.split_on_char ':' opts in + let has_opt = match optstring with "" :: _ -> true | _ -> false in + let optstring = ref [] in + let i = ref (if has_opt then 1 else 0) in + while !i < String.length opts do + let c = String.get opts !i in + match String.get opts (!i + 1) with + | ':' -> + incr i; + incr i; + optstring := `Arg c :: !optstring + | _ -> + incr i; + optstring := `Flag c :: !optstring + | exception _ -> + incr i; + optstring := `Flag c :: !optstring + done; + let optstring = List.rev !optstring in + Getopts { optstring; name; params } + in + let term = Term.(const make_getopts $ optstring $ name $ params) in + let info = + let doc = "parse utility options" in + Cmd.info "getopts" ~doc + in + Cmd.v info term +end + let of_args (w : string list) = let open Cmdliner in let exec_cmd cmd v = @@ -598,6 +667,7 @@ let of_args (w : string list) = | "umask" :: _ as cmd -> exec_cmd cmd Umask.t | "shift" :: _ as cmd -> exec_cmd cmd Shift.t | "read" :: _ as cmd -> exec_cmd cmd Read.t + | "getopts" :: _ as cmd -> exec_cmd cmd Getopts.t | cmd :: _ -> if List.mem cmd reserved then begin Debug.Log.err (fun f -> f "Unimplemented built-in: %s" cmd); diff --git a/src/lib/built_ins.mli b/src/lib/built_ins.mli index 9207183..c3df6ba 100644 --- a/src/lib/built_ins.mli +++ b/src/lib/built_ins.mli @@ -30,10 +30,21 @@ module Options : sig val pp : t Fmt.t end -type set = { update : (Options.option * bool) list; print_options : bool } +type set = { + update : (Options.option * bool) list; + arguments : string list; + print_options : bool; +} + type hash = Hash_remove | Hash_stats | Hash_add of string list type trap = Int of int | Action of string | Ignore | Default +type getopts = { + optstring : [ `Arg of char | `Flag of char ] list; + name : string; + params : string list; +} + type t = | Cd of { path : string option } (** Change directory to a path (if [None] then it should be [HOME]) *) @@ -56,6 +67,7 @@ type t = | Umask of int option | Shift of int option | Read of bool * string list + | Getopts of getopts val to_string : t -> string (** Serialises a built-in to a string *) diff --git a/src/lib/eunix.ml b/src/lib/eunix.ml index 48a9bbe..3190f7a 100644 --- a/src/lib/eunix.ml +++ b/src/lib/eunix.ml @@ -38,12 +38,12 @@ external tcsetpgrp : Unix.file_descr -> int -> int = "caml_merry_tcsetpgrp" let delegate_control ~pgid fn = let shell_pid = getpgrp () in + Debug.Log.info (fun f -> f "Deletgating to %i shell pid is %i" pgid shell_pid); Fun.protect - ~finally:(fun () -> assert (Int.equal 0 @@ tcsetpgrp Unix.stdin shell_pid)) - (fun () -> - match tcsetpgrp Unix.stdin pgid with - | 0 -> fn () - | n -> Fmt.failwith "tcsetpgrp: %i" n) + ~finally:(fun () -> + let v = tcsetpgrp Unix.stdin shell_pid in + if not (Int.equal 0 v) then ()) + (fun () -> match tcsetpgrp Unix.stdin pgid with 0 -> fn () | _ -> fn ()) external setpgrp : int -> int -> int = "caml_merry_setpgid" diff --git a/src/lib/eval.ml b/src/lib/eval.ml index 9ca69c6..c03fa43 100644 --- a/src/lib/eval.ml +++ b/src/lib/eval.ml @@ -109,13 +109,15 @@ module Make (S : Types.State) (E : Types.Exec) = struct current_context : [ `Toplevel | `Function | `CompoundCommand ]; } - let should_exit ctx v = + let should_exit ?(with_errexit = true) v = match v with | Exit.Zero _ -> false - | Exit.Nonzero { should_exit = { interactive; non_interactive }; _ } -> + | Exit.Nonzero + { value = ctx; should_exit = { interactive; non_interactive }; _ } -> (ctx.interactive && interactive = `Yes) || (not ctx.interactive) && non_interactive = `Yes - && ctx.options.errexit + && (if with_errexit then ctx.options.errexit else true) + && not ctx.subshell let dump_ctx ppf = let pp_quotes ppf = function @@ -128,6 +130,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct [ Fmt.field "quotes" (fun t -> t.quotes) pp_quotes; Fmt.field "rdrs" (fun t -> t.rdrs) Fmt.(lst Types.pp_redirect); + Fmt.field "argv" (fun t -> Array.to_list t.argv) Fmt.(lst string); ]) ppf @@ -150,6 +153,18 @@ module Make (S : Types.State) (E : Types.Exec) = struct | Some _ as v -> v | None -> S.lookup ctx.state ~param + let get_optind ctx = + match lookup ~param:"OPTIND" ctx with + | None -> assert false + | Some [ { txt; _ } ] -> int_of_string txt + | Some _ -> assert false + + let set_optind ctx v = + let new_state = + S.update ctx.state ~param:"OPTIND" [ Ast.Fragment.make (string_of_int v) ] + in + { ctx with state = Result.get_ok new_state } + let in_double_quotes ctx = ctx.quotes = `Double let in_quotes ctx = ctx.quotes = `Single || ctx.quotes = `Double @@ -171,6 +186,8 @@ module Make (S : Types.State) (E : Types.Exec) = struct let signal_handler = { run = signal_handler; sigint_set = false } in let state = S.update state ~param:"IFS" [ Ast.Fragment.make " \t\n" ] |> Result.get_ok + |> fun s -> + S.update s ~param:"OPTIND" [ Ast.Fragment.make "1" ] |> Result.get_ok in { interactive; @@ -381,7 +398,9 @@ module Make (S : Types.State) (E : Types.Exec) = struct end else ctx in - let should_exit ctx = (not ctx.subshell) && List.length p = 1 in + let should_exit ?with_errexit ctx = + should_exit ?with_errexit ctx && List.length p = 1 + in let on_process ?process ~async ctx = let ctx = pop_local_state ctx in match process with @@ -698,8 +717,10 @@ module Make (S : Types.State) (E : Types.Exec) = struct | Built_ins.Exit _ -> let ctx = Promise.await_exn ctx in let v_ctx = Exit.value ctx in - if should_exit v_ctx then - exit v_ctx (Exit.code ctx) + if + should_exit ~with_errexit:false + ctx + then exit v_ctx (Exit.code ctx) else handle_job job (`Exit ctx) | _ -> handle_job job (`Built_in ctx) in @@ -808,7 +829,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct | Built_ins.Exit _ -> let ctx = Promise.await_exn ctx in let v_ctx = Exit.value ctx in - if should_exit v_ctx then begin + if should_exit ~with_errexit:false ctx then begin if (Exit.value ctx).interactive then Fmt.pr "exit\n%!"; exit v_ctx (Exit.code ctx) @@ -1179,6 +1200,10 @@ module Make (S : Types.State) (E : Types.Exec) = struct | Ast.VariableAtom (n, NoAttribute) when Option.is_some (int_of_string_opt n) -> ( let n = int_of_string n in + Debug.Log.info (fun f -> + f "Lookup %i from %a" n + Fmt.(lst string) + (Array.to_list ctx.argv)); match Array.get ~label:(Fmt.str "argv(%i)" n) ctx.argv n with | v -> ( Exit.zero ctx, @@ -1198,7 +1223,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct else (Exit.zero ctx, []) | Some [ Ast.{ txt = ""; _ } ] when not (in_quotes ctx) -> (Exit.zero ctx, []) - | None | Some [ Ast.{ txt = ""; _ } ] -> + | None | Some [] | Some [ Ast.{ txt = ""; _ } ] -> (Exit.zero ctx, [ Fragment.make "" ]) | Some cst -> ( Exit.zero ctx, @@ -1325,8 +1350,14 @@ module Make (S : Types.State) (E : Types.Exec) = struct (Exit.zero new_ctx, List.concat value) | Error m -> (Exit.nonzero_msg ~exit_code:1 ctx "%s" m, [])) | Some cst -> (new_ctx, cst)) - | Ast.VariableAtom (_, IndicateErrorifNullorUnset (_, _)) -> - Fmt.failwith "TODO: Indicate Error") + | Ast.VariableAtom (p, IndicateErrorifNullorUnset (_, msg)) -> ( + match lookup ~param:p ctx with + | None | Some [] | Some [ { txt = ""; _ } ] -> + let _ctx, fs = word_expansion ctx msg in + ( Exit.nonzero_msg ctx "msh err: %s: %s" p + (Ast.Fragment.join_list ~sep:"" (List.concat fs)), + [] ) + | Some f -> (Exit.zero ctx, f))) | Ast.WordDoubleQuoted [] -> (Exit.zero ctx, [ Ast.Fragment.empty ]) | Ast.WordDoubleQuoted cst -> ( let saved_dqoute = ctx.quotes in @@ -1363,7 +1394,9 @@ module Make (S : Types.State) (E : Types.Exec) = struct (* Command substitution *) let saved_ctx = ctx in let ctx = { ctx with quotes = `None } in + Debug.Log.info (fun f -> f "Subshell %a" Ast_pp.complete_commands sub); let s = command_substitution ctx sub in + Debug.Log.info (fun f -> f "Subshell done"); ( Exit.zero saved_ctx, [ Fragment.make ~splittable:(not (in_quotes saved_ctx)) ~join:`Yes s; @@ -1454,7 +1487,9 @@ module Make (S : Types.State) (E : Types.Exec) = struct let ctx = match (ctx, collect) with | _, None -> ctx - | Exit.Nonzero _, Some _ -> ctx + | Exit.Nonzero _, Some _ -> + Debug.Log.info (fun f -> f "All done"); + ctx | Exit.Zero ctx, Some param -> ( Debug.Log.debug (fun f -> f "collect assignment: %s is %a" param @@ -1835,6 +1870,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct in let run_subshells s = let _ctx, std = exec_subshell ctx s in + Debug.Log.info (fun f -> f "RS: %s" std); String.trim std in run_subshells cc @@ -1940,10 +1976,18 @@ module Make (S : Types.State) (E : Types.Exec) = struct | Return n -> raise (Return (Exit.nonzero ctx n)) | Break n -> raise (Break (n, ctx)) | Continue n -> raise (Continue (n, ctx)) - | Set { update; print_options } -> + | Set { update; arguments; print_options } -> + Debug.Log.info (fun f -> f "set args %a" Fmt.(lst string) arguments); let v = Exit.zero - { ctx with options = Built_ins.Options.update ctx.options update } + { + ctx with + options = Built_ins.Options.update ctx.options update; + argv = + (if arguments = [] then ctx.argv + else + Array.of_list (Array.get ~label:"set" ctx.argv 0 :: arguments)); + } in if print_options then Eio.Flow.copy_string @@ -2061,16 +2105,22 @@ module Make (S : Types.State) (E : Types.Exec) = struct Exit.zero ctx | Umask (Some i) -> Exit.zero { ctx with umask = i } | Shift n -> + (* Since we store $0 in argv, we need to shift everything + starting from position 1 *) let n = Option.value ~default:1 n in - let new_len = Array.length ctx.argv - n in - if new_len <= 0 then begin + let arg_len = Array.length ctx.argv - 1 in + let new_len = arg_len - n in + if new_len < 0 then begin Fmt.epr "Shift count out of range\n%!"; Exit.nonzero ctx 1 end else let argv = - Array.init new_len (fun i -> - Array.get ~label:"shift" ctx.argv (i + n)) + List.init new_len (fun i -> + Array.get ~label:"shift" ctx.argv (i + n + 1)) + in + let argv = + Array.get ~label:"shift-2" ctx.argv 0 :: argv |> Array.of_list in Exit.zero { ctx with argv } | Read (_backslash, vars) -> ( @@ -2117,6 +2167,56 @@ module Make (S : Types.State) (E : Types.Exec) = struct ctx.state vars in Exit.zero { ctx with state }) + | Getopts { optstring; name; params = _ } -> ( + let optind = get_optind ctx in + match Array.get ~label:"getopts" ctx.argv optind with + | exception Invalid_argument _ -> + let _, state = S.remove ~param:"OPTARG" ctx.state in + Exit.nonzero { ctx with state } 127 + | arg -> ( + let is_arg arg c = + let c = String.make 1 c in + String.equal arg ("-" ^ c) || String.equal arg ("--" ^ c) + in + let match_flag_or_arg arg = + List.find_map + (function + | `Flag f -> if is_arg arg f then Some (`Flag f) else None + | `Arg f -> if is_arg arg f then Some (`Arg f) else None) + optstring + in + match match_flag_or_arg arg with + | None -> + let _, state = S.remove ~param:"OPTARG" ctx.state in + Exit.nonzero { ctx with state } 127 + | Some (`Flag f) -> + let _, state = S.remove ~param:"OPTARG" ctx.state in + let state = + S.update state ~param:name + [ Ast.Fragment.make (String.make 1 f) ] + |> Result.get_ok + in + Exit.zero (set_optind { ctx with state } (optind + 1)) + | Some (`Arg f) -> + let argument = + match Array.get ~label:"getopts" ctx.argv (optind + 1) with + | arg -> Some arg + | exception _ -> None + in + let state = + S.update ctx.state ~param:name + [ Ast.Fragment.make (String.make 1 f) ] + |> Result.get_ok + in + let state = + match argument with + | Some argument -> + S.update state ~param:"OPTARG" + [ Ast.Fragment.make argument ] + |> Result.get_ok + | None -> state + in + Exit.zero (set_optind { ctx with state } (optind + 2)))) | Command _ -> (* Handled separately *) assert false @@ -2137,7 +2237,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct match handle_and_or ~sw ~async ctx c with | Exit.Zero ctx -> loop sw ctx cs | Exit.Nonzero { value = ctx; _ } as v -> - if should_exit ctx v then v else loop sw ctx cs) + if should_exit v then v else loop sw ctx cs) in match sep with | Some Semicolon | None -> @@ -2191,7 +2291,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct if should_exit.interactive = `Yes && ctx.interactive then Fmt.epr "exit\n%!"; Stdlib.exit exit_code - | _, `Yes, _, false, true, _ -> Stdlib.exit exit_code + | _, `Yes, false, false, true, _ -> Stdlib.exit exit_code | _ -> loop_commands (exit, c :: cs) commands) | Exit.Zero _ as ctx -> loop_commands (ctx, c :: cs) commands) in diff --git a/src/lib/job.ml b/src/lib/job.ml index fced84b..58f5dc5 100644 --- a/src/lib/job.ml +++ b/src/lib/job.ml @@ -60,15 +60,15 @@ module Make (E : Types.Exec) = struct let size t = List.length t.processes (* Section 2.9.2 https://pubs.opengroup.org/onlinepubs/9799919799/ *) - let await_exit ~default ~pipefail ~interactive t = + let await_exit ~default ~pipefail ~interactive:_ t = Eio.Promise.resolve (snd t.reap) (); Eio.Fiber.yield (); let await = function | `Process (c, p) -> let v = - if interactive then - Eunix.delegate_control ~pgid:t.id @@ fun () -> E.await p - else E.await p + (* if interactive then *) + Eunix.delegate_control ~pgid:t.id @@ fun () -> E.await p + (* else E.await p *) in Exit.map ~f:(fun () -> c) v | `Built_in b -> Eio.Promise.await_exn b diff --git a/test/built_ins.t b/test/built_ins.t index 8c220ce..a748b10 100644 --- a/test/built_ins.t +++ b/test/built_ins.t @@ -441,3 +441,24 @@ The parsing for eval might be a little broken w.r.t quotes. hey $ msh test.sh hey + +18. Set + + $ cat > test.sh << EOF + > set -- foo bar baz + > echo \$1 + > echo \$2 + > echo \$3 + > echo "no four: \$4" + > EOF + + $ sh test.sh a b c d + foo + bar + baz + no four: + $ msh test.sh a b c d + foo + bar + baz + no four: diff --git a/test/scripts/dune.inc b/test/scripts/dune.inc index 783ac8e..f4c8ae0 100644 --- a/test/scripts/dune.inc +++ b/test/scripts/dune.inc @@ -46,6 +46,22 @@ (action (diff funcdefs.expected funcdefs.actual))) +(rule + (alias runtest) + (deps %{bin:msh} getopts.sh) + (action + (with-stdout-to getopts.actual + (progn + (run sh ./getopts.sh) + (run sh -c "echo ====msh====") + (run msh ./getopts.sh) + )))) + +(rule + (alias runtest) + (action + (diff getopts.expected getopts.actual))) + (rule (alias runtest) (deps %{bin:msh} subshell.sh) diff --git a/test/scripts/getopts.expected b/test/scripts/getopts.expected new file mode 100644 index 0000000..0710e7f --- /dev/null +++ b/test/scripts/getopts.expected @@ -0,0 +1,11 @@ +8 +on +article-is-this +EN + +====msh==== +8 +on +article-is-this +EN + diff --git a/test/scripts/getopts.sh b/test/scripts/getopts.sh new file mode 100644 index 0000000..0397729 --- /dev/null +++ b/test/scripts/getopts.sh @@ -0,0 +1,29 @@ +i=0 +run () { + while getopts ':a:l:v' opt; do + case $opt in + (v) verbose="on";; + (a) article=$OPTARG;; + (l) lang=$OPTARG;; + (?) echo "hmmmmm:$OPTARG"; + (:) # "optional arguments" (missing option-argument handling) + case $OPTARG in + (a) exit 1;; # error, according to our syntax + (l) :;; # acceptable but does nothing + esac;; + esac + i=$((i + 1)) + + if [ $i = 100 ]; then + exit 100 + fi + done +} + +run -a article-is-this -l EN -v -v -v +echo $OPTIND +echo $verbose +echo $article +echo $lang +echo $OPTARG + diff --git a/test/simple.t b/test/simple.t index aa45082..a832243 100644 --- a/test/simple.t +++ b/test/simple.t @@ -199,11 +199,9 @@ Using `<&` to copy a file descriptor But also, some simple errors should work too. $ msh -c "cat <1" - msh: internal error, uncaught exception: - Eio.Io Fs Not_found Unix_error (No such file or directory, "openat", "1"), - opening - - [125] + Fatal error: exception Eio.Io Fs Not_found Unix_error (No such file or directory, "openat", "1"), + opening + [2] 2.6 Output Redirection