diff --git a/dune-project b/dune-project index 599ad44..784292c 100644 --- a/dune-project +++ b/dune-project @@ -44,6 +44,7 @@ (= 1.3)) (cmdliner (>= 1.3.0)) + (alcotest :with-test) ocaml) (tags ("add topics" "to describe" your project))) diff --git a/merry.opam b/merry.opam index 9e02443..12b06ef 100644 --- a/merry.opam +++ b/merry.opam @@ -23,6 +23,7 @@ depends: [ "eio_posix" {= "1.3"} "eio" {= "1.3"} "cmdliner" {>= "1.3.0"} + "alcotest" {with-test} "ocaml" "odoc" {with-doc} ] diff --git a/src/bin/main.ml b/src/bin/main.ml index e08f14c..e168f9f 100644 --- a/src/bin/main.ml +++ b/src/bin/main.ml @@ -5,9 +5,9 @@ module I = Merry.Interactive.Make (Merry_posix.State) (Merry_posix.Exec) (Merry.History.Prefix_search) -let sh ~command ~dump ~file ~rest env = +let sh ~command_flag ~dump ~file ~rest env = let executor = Merry_posix.Exec.{ mgr = env#process_mgr } in - let interactive = Option.is_none file && Option.is_none command in + let interactive = Option.is_none file && rest = [] in let pos_zero = match file with Some f -> f | None -> "msh" in Eio.Switch.run @@ fun async_switch -> let signal_handler f = Eio_posix.run @@ fun _ -> f () in @@ -17,17 +17,18 @@ let sh ~command ~dump ~file ~rest env = ~home:(Sys.getenv "HOME" ^ "/") (Fpath.v (Merry.Eunix.cwd ()))) executor ~fs:env#fs ~stdin:env#stdin ~stdout:env#stdout ~async_switch - ~argv:(Array.of_list (pos_zero :: rest)) + ~argv:(Array.of_list (pos_zero :: (try List.tl rest with _ -> []))) ~program:pos_zero ~signal_handler in - match (file, command) with - | None, None -> I.run (Merry.Exit.zero ctx) + match (file, command_flag) with + | None, false -> I.run (Merry.Exit.zero ctx) | _ -> let ast = - match (file, command) with - | None, None -> assert false - | Some file, None -> Merry.Ast.of_file Eio.Path.(env#fs / file) - | _, Some c -> Merry.Ast.of_string c + match (file, command_flag, rest) with + | None, false, _ -> assert false + | Some file, false, _ -> 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 in if dump then Merry.Ast.Dump.pp Fmt.stdout ast else @@ -37,9 +38,9 @@ let sh ~command ~dump ~file ~rest env = open Cmdliner open Cmdliner.Term.Syntax -let command = - let doc = "command to run" in - Arg.(value & opt (some string) None & info [ "c"; "C" ] ~doc) +let command_flag = + let doc = "Run commands from the command-line" in + Arg.(value & flag & info [ "c" ] ~doc) let file = let doc = "The shell script to execute" in @@ -52,7 +53,7 @@ let dump = in Arg.(value & flag & info [ "d"; "D"; "dump" ] ~doc) -let rest = Arg.(value & pos_right 0 string [] & info []) +let rest = Arg.(value & pos_all string [] & info []) let cmd env = let doc = "Mere's shell." in @@ -74,8 +75,11 @@ let cmd env = in Cmd.make (Cmd.info "msh" ~version:"v0.0.1" ~doc ~man) @@ - let+ command = command and+ dump = dump and+ file = file and+ rest = rest in - sh ~command ~dump ~file ~rest env + let+ command_flag = command_flag + and+ dump = dump + and+ file = file + and+ rest = rest in + sh ~command_flag ~dump ~file ~rest env let main () = Eio_posix.run @@ fun env -> Cmd.eval (cmd env) diff --git a/src/lib/arith.ml b/src/lib/arith.ml index 76e866b..6e6d06b 100644 --- a/src/lib/arith.ml +++ b/src/lib/arith.ml @@ -27,12 +27,11 @@ module Make (S : Types.State) = struct let eval initial_state expr = let lookup state s = match S.lookup state ~param:s with - | Some [ Ast.WordLiteral n ] when Option.is_some (int_of_string_opt n) -> - int_of_string n + | Some n when Option.is_some (int_of_string_opt n) -> int_of_string n | _ -> 0 in let update state s i = - match S.update state ~param:s [ Ast.WordLiteral (string_of_int i) ] with + match S.update state ~param:s (string_of_int i) with | Ok s -> s | Error m -> failwith m in diff --git a/src/lib/ast.ml b/src/lib/ast.ml index ab80dbd..d53eb7b 100644 --- a/src/lib/ast.ml +++ b/src/lib/ast.ml @@ -748,15 +748,18 @@ let of_file path = let fname = Eio.Path.native_exn path in Eio.Path.load path |> of_string ~filename:fname -let rec word_component_to_string : word_component -> string = function - | WordName s -> s - | WordLiteral s -> s - | WordDoubleQuoted s -> word_components_to_string s - | WordSingleQuoted s -> word_components_to_string s - | WordGlobAll -> "*" - | WordGlobAny -> "?" - | WordEmpty -> "" - | WordAssignmentWord (Name p, v) -> p ^ "=" ^ word_components_to_string v +let rec word_component_to_string : + ?field_splitting:bool -> word_component -> string list = + fun ?(field_splitting = true) -> function + | WordName s -> [ s ] + | WordLiteral s -> [ s ] + | WordDoubleQuoted s -> word_components_to_strings ~field_splitting:false s + | WordSingleQuoted s -> word_components_to_strings ~field_splitting:false s + | WordGlobAll -> [ "*" ] + | WordGlobAny -> [ "?" ] + | WordEmpty -> [ "" ] + | WordAssignmentWord (Name p, v) -> + p :: "=" :: word_components_to_strings ~field_splitting v | WordSubshell _ -> Fmt.failwith "This is an error in Merry, subshells should already have been \ @@ -765,13 +768,20 @@ let rec word_component_to_string : word_component -> string = function Fmt.failwith "Conversion of %a" Yojson.Safe.pp (word_component_to_yojson v) -and word_components_to_string ws = - String.concat "" (List.map word_component_to_string ws) +and word_components_to_strings ?(field_splitting = true) ws = + if field_splitting then + List.concat_map (word_component_to_string ~field_splitting) ws + else + [ + String.concat "" + (List.concat_map (word_component_to_string ~field_splitting) ws); + ] class check_ast = object (_) inherit [bool] Sast.fold method int _ ctx = ctx + method bool _ ctx = ctx method string _ ctx = ctx method char _ ctx = ctx method option f v ctx = Option.fold ~none:ctx ~some:(fun i -> f i ctx) v @@ -822,3 +832,33 @@ let has_glob ast = end in o#word_cst ast false + +module Fragment = struct + let make ?(splittable = false) ?(globbable = false) ?(join = `No) txt = + { txt; splittable; join; globbable } + + let empty = make "" + let to_string { txt; _ } = txt + let join ~sep f1 f2 = { f1 with txt = f1.txt ^ sep ^ f2.txt } + let join_list ~sep fs = List.fold_left (join ~sep) empty fs |> to_string + + let pp_join ppf = function + | `No -> Fmt.pf ppf "no" + | `With_previous -> Fmt.pf ppf "with-previous" + | `With_next -> Fmt.pf ppf "with-next" + + let pp ppf { txt; join; splittable; globbable } = + Fmt.pf ppf "{ txt = %s; join = %a; splittable = %b; globbable = %b }" txt + pp_join join splittable globbable + + let handle_joins cst = + let rec loop = function + | [] -> [] + | x :: { txt; join = `With_previous; _ } :: rest -> + loop ({ x with txt = x.txt ^ txt } :: rest) + | { txt; join = `With_next; _ } :: y :: rest -> + { y with txt = txt ^ y.txt } :: loop rest + | x :: xs -> x :: loop xs + in + loop cst +end diff --git a/src/lib/ast.mli b/src/lib/ast.mli index b85b3da..cb9ce2c 100644 --- a/src/lib/ast.mli +++ b/src/lib/ast.mli @@ -19,8 +19,11 @@ val of_file : _ Eio.Path.t -> t (* class map : Ppxlib_traverse_builtins.map *) -val word_component_to_string : word_component -> string -val word_components_to_string : word_cst -> string +val word_component_to_string : + ?field_splitting:bool -> word_component -> string list + +val word_components_to_strings : + ?field_splitting:bool -> word_cst -> string list val has_async : complete_command -> bool (** Checks, recursively, the command to see if there is any use of the async @@ -29,6 +32,22 @@ val has_async : complete_command -> bool val has_glob : word_cst -> bool (** Checks whether or not any glob patterns exist in a given word_cst *) +module Fragment : sig + val make : + ?splittable:bool -> + ?globbable:bool -> + ?join:[ `No | `With_next | `With_previous ] -> + string -> + fragment + + val empty : fragment + val to_string : fragment -> string + val join : sep:string -> fragment -> fragment -> fragment + val join_list : sep:string -> fragment list -> string + val handle_joins : fragment list -> fragment list + val pp : fragment Fmt.t +end + module Dump : sig val pp : t Fmt.t (** Dump the program *) diff --git a/src/lib/eval.ml b/src/lib/eval.ml index 73a80fe..ee7f53e 100644 --- a/src/lib/eval.ml +++ b/src/lib/eval.ml @@ -39,6 +39,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct rdrs : Types.redirect list; signal_handler : signal_handler; exit_handler : (unit -> unit) option; + in_double_quotes : bool; } let _stdin ctx = ctx.stdin @@ -46,8 +47,8 @@ module Make (S : Types.State) (E : Types.Exec) = struct let make_ctx ?(interactive = false) ?(subshell = false) ?(local_state = []) ?(background_jobs = []) ?(last_background_process = "") ?(functions = []) ?(rdrs = []) ?exit_handler ?(options = Built_ins.Options.default) - ?(hash = Hash.empty) ~fs ~stdin ~stdout ~async_switch ~program ~argv - ~signal_handler state executor = + ?(hash = Hash.empty) ?(in_double_quotes = false) ~fs ~stdin ~stdout + ~async_switch ~program ~argv ~signal_handler state executor = let signal_handler = { run = signal_handler; sigint_set = false } in { interactive; @@ -69,6 +70,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct rdrs; signal_handler; exit_handler; + in_double_quotes; } let state ctx = ctx.state @@ -76,34 +78,18 @@ module Make (S : Types.State) (E : Types.Exec) = struct let fs ctx = ctx.fs let clear_local_state ctx = { ctx with local_state = [] } - let rec tilde_expansion ctx = function - | [] -> [] - | Ast.WordTildePrefix _ :: rest -> - Ast.WordName (S.expand ctx.state `Tilde) :: tilde_expansion ctx rest - | v :: rest -> v :: tilde_expansion ctx rest + let tilde_expansion ctx = function + | Ast.WordTildePrefix _ -> Ast.WordLiteral (S.expand ctx.state `Tilde) + | v -> v - let arithmetic_expansion ctx expr = - let rec fold (ctx, cst) = function - | [] -> (ctx, cst) - | Ast.WordArithmeticExpression word :: rest -> - let expr = Ast.word_components_to_string word in - let aexpr = - Arith_parser.main Arith_lexer.read (Lexing.from_string expr) - in - let state, i = A.eval ctx.state aexpr in - fold - ({ ctx with state }, Ast.WordLiteral (string_of_int i) :: cst) - rest - | Ast.WordDoubleQuoted dq :: rest -> - let ctx, v = fold (ctx, []) dq in - fold (ctx, Ast.WordDoubleQuoted (List.rev v) :: cst) rest - | Ast.WordSingleQuoted dq :: rest -> - let ctx, v = fold (ctx, []) dq in - fold (ctx, Ast.WordSingleQuoted (List.rev v) :: cst) rest - | v :: rest -> fold (ctx, v :: cst) rest - in - let state, cst = fold (ctx, []) expr in - (state, List.rev cst) + let word_cst_to_string ?field_splitting v = + Ast.word_components_to_strings ?field_splitting v |> String.concat "" + + let arithmetic_expansion ctx word = + let expr = word_cst_to_string word in + let aexpr = Arith_parser.main Arith_lexer.read (Lexing.from_string expr) in + let state, i = A.eval ctx.state aexpr in + ({ ctx with state }, Ast.Fragment.make (string_of_int i)) let stdout_for_pipeline ~sw ctx = function | [] -> (None, `Global ctx.stdout) @@ -119,9 +105,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct match op with | Io_op_less -> (* Simple redirection for input *) - let r = - Eio.Path.open_in ~sw (ctx.fs / Ast.word_components_to_string file) - in + let r = Eio.Path.open_in ~sw (ctx.fs / word_cst_to_string file) in let fd = Eio_unix.Resource.fd_opt r |> Option.get in [ Types.Redirect (n, fd, `Blocking) ] | Io_op_lessand -> ( @@ -148,7 +132,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct in let w = Eio.Path.open_out ~sw ~append ~create - (ctx.fs / Ast.word_components_to_string file) + (ctx.fs / word_cst_to_string file) in let fd = Eio_unix.Resource.fd_opt w |> Option.get in [ Types.Redirect (n, fd, `Blocking) ] @@ -171,7 +155,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct (* Simple file creation *) let w = Eio.Path.open_out ~sw ~create:(`If_missing 0o644) - (ctx.fs / Ast.word_components_to_string file) + (ctx.fs / word_cst_to_string file) in let fd = Eio_unix.Resource.fd_opt w |> Option.get in [ @@ -181,7 +165,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct | Io_op_clobber -> let w = Eio.Path.open_out ~sw ~create:(`Or_truncate 0o644) - (ctx.fs / Ast.word_components_to_string file) + (ctx.fs / word_cst_to_string file) in let fd = Eio_unix.Resource.fd_opt w |> Option.get in [ Types.Redirect (n, fd, `Blocking) ] @@ -197,15 +181,10 @@ module Make (S : Types.State) (E : Types.Exec) = struct let cwd_of_ctx ctx = S.cwd ctx.state |> Fpath.to_string |> ( / ) ctx.fs - let needs_glob_expansion : Ast.word_component -> bool = function - | WordGlobAll | WordGlobAny -> true - | _ -> false - let resolve_program ?(update = true) ctx name = let v = if not (String.contains name '/') then begin S.lookup ctx.state ~param:"PATH" - |> Option.map Ast.word_components_to_string |> Option.value ~default:"/bin:/usr/bin" |> String.split_on_char ':' |> List.find_map (fun dir -> @@ -224,9 +203,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct let get_env ?(extra = []) ctx = let extra = - extra - @ List.map (fun (k, v) -> (k, Ast.word_components_to_string v)) - @@ S.exports ctx.state + extra @ List.map (fun (k, v) -> (k, v)) @@ S.exports ctx.state in let env = Eunix.env () in List.fold_left (fun acc (k, _) -> List.remove_assoc k acc) env extra @@ -241,7 +218,9 @@ module Make (S : Types.State) (E : Types.Exec) = struct let remove_quotes s = let s_len = String.length s in - if s.[0] = '"' && s.[s_len - 1] = '"' then String.sub s 1 (s_len - 2) else s + let s = if s.[0] = '"' then String.sub s 1 (s_len - 1) else s in + let s_len = String.length s in + if s.[s_len - 1] = '"' then String.sub s 0 (s_len - 1) else s let exit ctx code = Option.iter (fun f -> f ()) ctx.exit_handler; @@ -318,28 +297,28 @@ module Make (S : Types.State) (E : Types.Exec) = struct loop (Exit.value ctx) job stdout_of_previous (Ast.SimpleCommand (Named (executable, suffix)) :: rest) | Ast.SimpleCommand (Named (executable, suffix)) :: rest -> ( - let ctx, executable = expand_cst ctx executable in + let ctx, executable = word_expansion ctx executable in match ctx with | Exit.Nonzero _ as ctx -> let job = handle_job job (`Built_in (Exit.ignore ctx)) in loop (Exit.value ctx) job stdout_of_previous rest | Exit.Zero ctx -> ( - let executable = handle_word_cst_subshell ctx executable in let executable, extra_args = (* This is a side-effect of the alias command with something like alias ls="ls -la" *) - match executable with - | [ Ast.WordLiteral s ] as v -> ( - match String.split_on_char ' ' (remove_quotes s) with - | exec :: args -> - ( [ Ast.WordName exec ], - List.map - (fun w -> Ast.Suffix_word [ Ast.WordName w ]) - args ) - | _ -> (v, [])) - | v -> (v, []) + match + Ast.Fragment.join_list ~sep:"" executable + |> String.split_on_char ' ' |> List.map Ast.Fragment.make + with + | [] -> ("", []) + | exec :: args -> + ( remove_quotes exec.txt, + List.map + (fun v -> + Ast.Suffix_word + [ Ast.WordLiteral (remove_quotes v.Ast.txt) ]) + args ) in - let executable = Ast.word_components_to_string executable in let ctx, suffix = match suffix with | None -> (ctx, []) @@ -351,9 +330,6 @@ module Make (S : Types.State) (E : Types.Exec) = struct let job = handle_job job (`Built_in (Exit.ignore ctx)) in loop (Exit.value ctx) job stdout_of_previous rest | Exit.Zero ctx -> ( - let args_as_strings = - List.map Ast.word_components_to_string args - in let some_read, some_write = stdout_for_pipeline ~sw:pipeline_switch ctx rest in @@ -373,9 +349,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct match handle_redirections ~sw:pipeline_switch ctx rdrs with | Error ctx -> (ctx, handle_job job (`Rdr (Exit.nonzero () 1))) | Ok rdrs -> ( - match - Built_ins.of_args (executable :: args_as_strings) - with + match Built_ins.of_args (executable :: args) with | Some (Error _) -> (ctx, handle_job job (`Built_in (Exit.nonzero () 1))) | (None | Some (Ok (Command _))) as v -> ( @@ -415,8 +389,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct else let ctx = { ctx with stdout = some_write } in handle_function_application ctx - ~name:executable - (ctx.program :: args_as_strings) + ~name:executable (ctx.program :: args) in match func_app with | Some ctx -> @@ -474,8 +447,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct Exit.zero ("echo", [ prog ]) else Exit.zero (x, xs)) end - else - Exit.zero (executable, args_as_strings) + else Exit.zero (executable, args) in match exec_and_args with | Exit.Nonzero _ as v -> @@ -503,7 +475,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct ~stdin:stdout ~stdout:some_write ~pgid:(job_pgid job) - executable args_as_strings + executable args in close_stdout ~is_global some_write; loop ctx job some_read rest))))) @@ -564,7 +536,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct } end - and parameter_expansion' ctx ast = + and parameter_expansion ctx ast : ctx Exit.t * Ast.fragment list list = let get_prefix ~pattern ~kind param = let _, prefix = String.fold_left @@ -596,63 +568,102 @@ module Make (S : Types.State) (E : Types.Exec) = struct in prefix in - let rec expand acc ctx = function - | [] -> (Exit.zero ctx, List.rev acc |> List.concat) - | Ast.WordVariable v :: rest -> ( + let tl_or_empty v = try List.tl v with _ -> [] in + let expand ctx v : ctx Exit.t * Ast.fragment list list = + let module Fragment = struct + include Ast.Fragment + + let make ?(join = if ctx.in_double_quotes then `With_previous else `No) + ?globbable ?splittable v = + Ast.Fragment.make ~join ?splittable ?globbable v + end in + match v with + | Ast.WordVariable v -> ( match v with | Ast.VariableAtom ("!", NoAttribute) -> - expand - ([ Ast.WordName ctx.last_background_process ] :: acc) - ctx rest + (Exit.zero ctx, [ [ Fragment.make ctx.last_background_process ] ]) | Ast.VariableAtom ("-", NoAttribute) -> let i = if ctx.interactive then "i" else "" in - expand - ([ Ast.WordName (Built_ins.Options.to_letters ctx.options ^ i) ] - :: acc) - ctx rest + ( Exit.zero ctx, + [ + [ + Fragment.make (Built_ins.Options.to_letters ctx.options ^ i); + ]; + ] ) + | Ast.VariableAtom ("@", NoAttribute) -> + let args = tl_or_empty @@ Array.to_list ctx.argv in + let args = + if not ctx.in_double_quotes then + List.map + (fun v -> [ Fragment.make ~join:`No ~splittable:true v ]) + args + else + let l = List.length args in + List.mapi + (fun idx arg -> + if idx = 0 then [ Fragment.make ~join:`With_previous arg ] + else if idx = l - 1 then + [ Fragment.make ~join:`With_next arg ] + else [ Fragment.make ~join:`No arg ]) + args + in + + (Exit.zero ctx, args) + | Ast.VariableAtom ("#", NoAttribute) -> + ( Exit.zero ctx, + [ + [ + Fragment.make + (string_of_int + (List.length @@ tl_or_empty (Array.to_list ctx.argv))); + ]; + ] ) | Ast.VariableAtom (n, NoAttribute) when Option.is_some (int_of_string_opt n) -> ( let n = int_of_string n in match Array.get ctx.argv n with - | v -> expand ([ Ast.WordName v ] :: acc) ctx rest + | v -> (Exit.zero ctx, [ [ Fragment.make v ] ]) | exception Invalid_argument _ -> - expand ([ Ast.WordName "" ] :: acc) ctx rest) + (Exit.zero ctx, [ [ Fragment.make "" ] ])) | Ast.VariableAtom (s, NoAttribute) -> ( match S.lookup ctx.state ~param:s with | None -> if ctx.options.no_unset then begin ( Exit.nonzero_msg ctx ~exit_code:1 "%s: unbound variable" s, - List.rev acc |> List.concat ) + [ [ Fragment.make "" ] ] ) end - else expand ([ Ast.WordName "" ] :: acc) ctx rest - | Some cst -> expand (cst :: acc) ctx rest) + else (Exit.zero ctx, [ [ Fragment.make "" ] ]) + | Some cst -> + ( Exit.zero ctx, + [ + [ + Fragment.make ~splittable:(not ctx.in_double_quotes) cst; + ]; + ] )) | Ast.VariableAtom (s, ParameterLength) -> ( match S.lookup ctx.state ~param:s with - | None -> expand ([ Ast.WordLiteral "0" ] :: acc) ctx rest + | None -> (Exit.zero ctx, [ [ Fragment.make "0" ] ]) | Some cst -> - expand - ([ - Ast.WordLiteral - (string_of_int - (String.length (Ast.word_components_to_string cst))); - ] - :: acc) - ctx rest) + ( Exit.zero ctx, + [ [ Fragment.make (string_of_int (String.length cst)) ] ] )) | Ast.VariableAtom (s, UseDefaultValues (_, cst)) -> ( match S.lookup ctx.state ~param:s with - | None -> expand (cst :: acc) ctx rest - | Some cst -> expand (cst :: acc) ctx rest) + | None -> + (Exit.zero ctx, [ [ Fragment.make (word_cst_to_string cst) ] ]) + | Some cst -> (Exit.zero ctx, [ [ Fragment.make cst ] ])) | Ast.VariableAtom ( s, (( RemoveSmallestPrefixPattern cst | RemoveLargestPrefixPattern cst ) as v) ) -> ( - let ctx, spp = expand_cst ctx cst in + let ctx, spp = word_expansion ctx cst in match ctx with - | Exit.Nonzero _ as ctx -> (ctx, List.rev acc |> List.concat) + | Exit.Nonzero _ as ctx -> (ctx, [ [ Fragment.make "" ] ]) | Exit.Zero ctx -> ( - let pattern = Ast.word_components_to_string spp in + let pattern = Fragment.join_list ~sep:"" spp in match S.lookup ctx.state ~param:s with - | None -> expand (cst :: acc) ctx rest + | None -> + ( Exit.zero ctx, + [ [ Fragment.make (word_cst_to_string cst) ] ] ) | Some cst -> ( let kind = match v with @@ -660,28 +671,28 @@ module Make (S : Types.State) (E : Types.Exec) = struct | RemoveLargestPrefixPattern _ -> `Largest | _ -> assert false in - let param = Ast.word_components_to_string cst in + let param = cst in let prefix = get_prefix ~pattern ~kind param in match prefix with - | None -> expand ([ Ast.WordName param ] :: acc) ctx rest + | None -> (Exit.zero ctx, [ [ Fragment.make param ] ]) | Some s -> ( match String.cut_prefix ~prefix:s param with - | Some s -> - expand ([ Ast.WordName s ] :: acc) ctx rest - | None -> - expand ([ Ast.WordName param ] :: acc) ctx rest))) - ) + | Some s -> (Exit.zero ctx, [ [ Fragment.make s ] ]) + | None -> (Exit.zero ctx, [ [ Fragment.make param ] ]) + )))) | Ast.VariableAtom ( s, (( RemoveSmallestSuffixPattern cst | RemoveLargestSuffixPattern cst ) as v) ) -> ( - let ctx, spp = expand_cst ctx cst in - let pattern = Ast.word_components_to_string spp in + let ctx, spp = word_expansion ctx cst in + let pattern = Fragment.join_list ~sep:"" spp in match ctx with - | Exit.Nonzero _ as ctx -> (ctx, List.rev acc |> List.concat) + | Exit.Nonzero _ as ctx -> (ctx, [ [ Fragment.empty ] ]) | Exit.Zero ctx -> ( match S.lookup ctx.state ~param:s with - | None -> expand (cst :: acc) ctx rest + | None -> + ( Exit.zero ctx, + [ [ Fragment.make (word_cst_to_string cst) ] ] ) | Some cst -> ( let kind = match v with @@ -689,67 +700,148 @@ module Make (S : Types.State) (E : Types.Exec) = struct | RemoveLargestSuffixPattern _ -> `Largest | _ -> assert false in - let param = Ast.word_components_to_string cst in + let param = cst in let suffix = get_suffix ~pattern ~kind param in match suffix with - | None -> expand ([ Ast.WordName param ] :: acc) ctx rest + | None -> (Exit.zero ctx, [ [ Fragment.make param ] ]) | Some s -> ( match String.cut_suffix ~suffix:s param with - | Some s -> - expand ([ Ast.WordName s ] :: acc) ctx rest - | None -> - expand ([ Ast.WordName param ] :: acc) ctx rest))) - ) + | Some s -> (Exit.zero ctx, [ [ Fragment.make s ] ]) + | None -> (Exit.zero ctx, [ [ Fragment.make param ] ]) + )))) | Ast.VariableAtom (s, UseAlternativeValue (_, alt)) -> ( match S.lookup ctx.state ~param:s with - | Some _ -> expand (alt :: acc) ctx rest - | None -> expand ([ Ast.WordEmpty ] :: acc) ctx rest) + | Some _ -> + (Exit.zero ctx, [ [ Fragment.make (word_cst_to_string alt) ] ]) + | None -> (Exit.zero ctx, [ [ Fragment.empty ] ])) | Ast.VariableAtom (s, AssignDefaultValues (_, value)) -> ( match S.lookup ctx.state ~param:s with - | Some cst -> expand (cst :: acc) ctx rest + | Some cst -> (Exit.zero ctx, [ [ Fragment.make cst ] ]) | None -> ( - match S.update ctx.state ~param:s value with + match + S.update ctx.state ~param:s (word_cst_to_string value) + with | Ok state -> let new_ctx = { ctx with state } in - expand (value :: acc) new_ctx rest + ( Exit.zero new_ctx, + [ [ Fragment.make (word_cst_to_string value) ] ] ) | Error m -> ( Exit.nonzero_msg ~exit_code:1 ctx "%s" m, - List.rev acc |> List.concat ))) + [ [ Fragment.empty ] ] ))) | Ast.VariableAtom (_, IndicateErrorifNullorUnset (_, _)) -> Fmt.failwith "TODO: Indicate Error") - | Ast.WordDoubleQuoted cst :: rest -> ( - let new_ctx, cst_acc = expand [] ctx cst in + | Ast.WordDoubleQuoted cst -> ( + let ctx = { ctx with in_double_quotes = true } in + let new_ctx, cst_acc = word_expansion ctx cst in + (* We now do any joining for $@... *) + let cst_acc = Fragment.handle_joins cst_acc in + let new_ctx = + Exit.map + ~f:(fun ctx -> { ctx with in_double_quotes = false }) + new_ctx + in match new_ctx with - | Exit.Nonzero _ -> (new_ctx, cst_acc) - | Exit.Zero new_ctx -> - expand ([ Ast.WordDoubleQuoted cst_acc ] :: acc) new_ctx rest) - | Ast.WordSingleQuoted cst :: rest -> ( - let new_ctx, cst_acc = expand [] ctx cst in + | Exit.Nonzero _ -> (new_ctx, [ cst_acc ]) + | Exit.Zero new_ctx -> (Exit.zero new_ctx, [ cst_acc ])) + | Ast.WordSingleQuoted cst -> ( + let ctx = { ctx with in_double_quotes = true } in + let new_ctx, cst_acc = word_expansion ctx cst in + let new_ctx = + Exit.map + ~f:(fun ctx -> { ctx with in_double_quotes = false }) + new_ctx + in match new_ctx with - | Exit.Nonzero _ -> (new_ctx, cst_acc) - | Exit.Zero new_ctx -> - expand ([ Ast.WordSingleQuoted cst_acc ] :: acc) new_ctx rest) - | Ast.WordAssignmentWord (n, w) :: rest -> ( - let new_ctx, cst_acc = expand [] ctx w in + | Exit.Nonzero _ -> (new_ctx, [ cst_acc ]) + | Exit.Zero new_ctx -> (Exit.zero new_ctx, [ cst_acc ])) + | Ast.WordAssignmentWord (Name n, w) -> ( + let new_ctx, cst_acc = word_expansion ctx w in match new_ctx with - | Exit.Nonzero _ -> (new_ctx, cst_acc) - | Exit.Zero new_ctx -> - expand - ([ Ast.WordAssignmentWord (n, cst_acc) ] :: acc) - new_ctx rest) - | v :: rest -> expand ([ v ] :: acc) ctx rest + | Exit.Nonzero _ -> (new_ctx, [ cst_acc ]) + | Exit.Zero _ -> + ( new_ctx, + [ + [ + Fragment.make + (n ^ "=" + ^ Fragment.join_list ~sep:"" (List.concat [ cst_acc ])); + ]; + ] )) + | Ast.WordSubshell sub -> + (* Command substitution *) + let s = command_substitution ctx sub in + (Exit.zero ctx, [ [ Fragment.make s ] ]) + | Ast.WordArithmeticExpression cst -> + arithmetic_expansion ctx cst |> fun (ctx, v) -> + (Exit.zero ctx, [ [ v ] ]) + | Ast.WordName s -> (Exit.zero ctx, [ [ Fragment.make s ] ]) + | Ast.WordLiteral s -> (Exit.zero ctx, [ [ Fragment.make s ] ]) + | Ast.WordGlobAll -> + (Exit.zero ctx, [ [ Fragment.make ~globbable:true "*" ] ]) + | Ast.WordGlobAny -> + (Exit.zero ctx, [ [ Fragment.make ~globbable:true "?" ] ]) + | v -> + Fmt.failwith "TODO: expansion of %a" yojson_pp + (Ast.word_component_to_yojson v) + in + expand ctx ast + + and field_splitting ctx = function + | [] -> [] + | Ast.{ splittable = true; txt; _ } :: rest -> + (String.split_on_char ' ' txt |> List.map Ast.Fragment.make) + @ field_splitting ctx rest + | txt :: rest -> txt :: field_splitting ctx rest + + and word_expansion' ctx cst : ctx Exit.t * Ast.fragments list = + let cst = tilde_expansion ctx cst in + parameter_expansion ctx cst + + and word_expansion ctx cst : ctx Exit.t * Ast.fragment list = + let rec aux ctx = function + | [] -> (ctx, []) (* one empty word *) + | c :: rest -> + let new_ctx, l = word_expansion' (Exit.value ctx) c in + let next_ctx, r = aux new_ctx rest in + let combined = l @ r in + (next_ctx, combined) in - expand [] ctx ast + let ctx, cst = aux (Exit.zero ctx) cst in + match ctx with + | Exit.Nonzero _ -> (ctx, List.concat cst) + | Exit.Zero ctx -> + let fields = cst in + let fields = List.map (field_splitting ctx) fields in + let (ctx, cst) : ctx * Ast.fragments list = + begin + let glob = Ast.Fragment.join_list ~sep:"" (List.concat fields) in + let vs : Ast.fragments list = + let has_glob = + List.exists + (fun (f : Ast.fragment) -> f.globbable) + (List.concat fields) + in + let _new_ctx, s = + if (not ctx.options.no_path_expansion) && has_glob then + glob_expand ctx glob + else if ctx.options.no_path_expansion && has_glob then + (ctx, [ Ast.Fragment.make glob ]) + else (ctx, List.concat fields) + in + [ s ] + in + (ctx, vs) + end + in + (Exit.zero ctx, List.concat cst) - and handle_export_or_readonly kind ctx (assignments : Ast.word_cst list) = + and handle_export_or_readonly kind ctx (assignments : string list) = let flags, assignments = List.fold_left - (fun (fs, args) -> function - | [ Ast.WordName v ] | [ Ast.WordLiteral v ] -> ( - match Astring.String.cut ~sep:"-" v with - | Some ("", f) -> (f :: fs, args) - | _ -> (fs, [ Ast.WordName v ] :: args)) - | v -> (fs, v :: args)) + (fun (fs, args) v -> + match Astring.String.cut ~sep:"-" v with + | Some ("", f) -> (f :: fs, args) + | _ -> (fs, v :: args)) ([], []) assignments in let update = @@ -757,23 +849,20 @@ module Make (S : Types.State) (E : Types.Exec) = struct | `Export -> update ~export:true ~readonly:false | `Readonly -> update ~export:false ~readonly:true in - let rec loop acc_ctx = function - | [] -> Exit.zero acc_ctx - | Ast.WordAssignmentWord (Name param, v) :: rest -> - update acc_ctx ~param v >>= fun new_ctx -> loop new_ctx rest - | Ast.WordName param :: rest -> ( + let read_arg acc_ctx param = + (* TODO: quoting? *) + match Astring.String.cut ~sep:"=" param with + | Some (param, v) -> update acc_ctx ~param v + | None -> ( match S.lookup acc_ctx.state ~param with - | Some v -> - update acc_ctx ~param v >>= fun new_ctx -> loop new_ctx rest - | None -> loop acc_ctx rest) - | c :: _ -> - Exit.nonzero_msg acc_ctx "export weird arguments: %s\n" - (Ast.word_component_to_string c) + | Some v -> update acc_ctx ~param v + | None -> Exit.zero acc_ctx) in match flags with | [] -> List.fold_left - (fun ctx w -> match ctx with Exit.Zero ctx -> loop ctx w | _ -> ctx) + (fun ctx w -> + match ctx with Exit.Zero ctx -> read_arg ctx w | _ -> ctx) (Exit.zero ctx) assignments | fs -> if List.mem "p" fs then begin @@ -783,26 +872,18 @@ module Make (S : Types.State) (E : Types.Exec) = struct end; Exit.zero ctx - and expand_cst (ctx : ctx) cst : ctx Exit.t * Ast.word_cst = - let cst = tilde_expansion ctx cst in - let ctx, cst = parameter_expansion' ctx cst in - match ctx with - | Exit.Nonzero _ as ctx -> (ctx, cst) - | Exit.Zero ctx -> - (* TODO: Propagate errors *) - let ctx, ast = arithmetic_expansion ctx cst in - (Exit.zero ctx, ast) - and expand_redirects ((ctx, acc) : ctx * Ast.cmd_suffix_item list) (c : Ast.cmd_suffix_item list) = match c with | [] -> (ctx, List.rev acc) | Ast.Suffix_redirect (IoRedirect_IoFile (num, (op, file))) :: rest -> ( - let ctx, cst = expand_cst ctx file in + let ctx, cst = word_expansion ctx file in match ctx with - | Exit.Nonzero _ -> assert false + | Exit.Nonzero _ -> Fmt.failwith "Redirect expansion" | Exit.Zero ctx -> - let cst = handle_subshell ctx cst in + let cst = + List.map (fun Ast.{ txt; _ } -> Ast.WordLiteral txt) cst + in let v = Ast.Suffix_redirect (IoRedirect_IoFile (num, (op, cst))) in expand_redirects (ctx, v :: acc) rest) | (Ast.Suffix_redirect _ as v) :: rest -> @@ -857,10 +938,14 @@ module Make (S : Types.State) (E : Types.Exec) = struct match v with | Ast.For_Name_DoGroup (_, (term, sep)) -> exec ctx (term, Some sep) | Ast.For_Name_In_WordList_DoGroup (Name name, wdlist, (term, sep)) -> - let wdlist = Nlist.flatten @@ Nlist.map (word_glob_expand ctx) wdlist in + let wdlist = Nlist.map (word_expansion ctx) wdlist in Nlist.fold_left - (fun _ word -> - update ctx ~param:name word >>= fun ctx -> exec ctx (term, Some sep)) + (fun _ (_, words) -> + List.fold_left + (fun _ word -> + update ctx ~param:name word.Ast.txt >>= fun ctx -> + exec ctx (term, Some sep)) + (Exit.zero ctx) words) (Exit.zero ctx) wdlist and handle_if_clause ctx = function @@ -891,11 +976,11 @@ module Make (S : Types.State) (E : Types.Exec) = struct and handle_case_clause ctx = function | Ast.Case _ -> Exit.zero ctx | Cases (word, case_list) -> ( - let ctx, word = expand_cst ctx word in + let ctx, word = word_expansion ctx word in match ctx with | Exit.Nonzero _ as ctx -> ctx | Exit.Zero ctx -> ( - let scrutinee = Ast.word_components_to_string word in + let scrutinee = Ast.Fragment.join_list ~sep:"" word in let res = Nlist.fold_left (fun acc pat -> @@ -908,21 +993,14 @@ module Make (S : Types.State) (E : Types.Exec) = struct (fun inner_acc pattern -> match inner_acc with | Some _ as v -> v - | None -> ( - let ctx, pattern = expand_cst ctx pattern in - match ctx with - | Exit.Nonzero _ as ctx -> Some ctx - | Exit.Zero ctx -> - let pattern = - Ast.word_components_to_string pattern - in - if Glob.test ~pattern scrutinee then begin - match sub with - | Some sub -> - Some (exec_subshell ctx sub) - | None -> Some (Exit.zero ctx) - end - else inner_acc)) + | None -> + let pattern = word_cst_to_string pattern in + if Glob.test ~pattern scrutinee then begin + match sub with + | Some sub -> Some (exec_subshell ctx sub) + | None -> Some (Exit.zero ctx) + end + else inner_acc) None p)) None case_list in @@ -971,64 +1049,33 @@ module Make (S : Types.State) (E : Types.Exec) = struct let ctx = { ctx with argv = Array.of_list argv } in Option.some @@ (handle_compound_command ctx commands >|= fun _ -> ctx) - and needs_subshelling = function - | [] -> false - | Ast.WordSubshell _ :: _ -> true - | Ast.WordDoubleQuoted word :: rest -> - needs_subshelling word || needs_subshelling rest - | Ast.WordSingleQuoted word :: rest -> - needs_subshelling word || needs_subshelling rest - | _ -> false - - and handle_subshell (ctx : ctx) wcs = - let exec_subshell ~sw ctx s = + and command_substitution (ctx : ctx) (cc : Ast.complete_commands) = + let exec_subshell ctx s = let buf = Buffer.create 16 in let stdout = Eio.Flow.buffer_sink buf in - let r, w = Eio_unix.pipe sw in - Eio.Fiber.fork ~sw (fun () -> Eio.Flow.copy r stdout); - let subshell_ctx = { ctx with stdout = w; subshell = true } in - let sub_ctx, _ = run (Exit.zero subshell_ctx) s in - Eio.Flow.close w; + let sub_ctx = + Eio.Switch.run @@ fun sw -> + let r, w = Eio_unix.pipe sw in + Eio.Fiber.fork ~sw (fun () -> Eio.Flow.copy r stdout); + let subshell_ctx = { ctx with stdout = w; subshell = true } in + let sub_ctx, _ = run (Exit.zero subshell_ctx) s in + Eio.Flow.close w; + sub_ctx + in ((sub_ctx >|= fun _ -> ctx), Buffer.contents buf) in - let rec run_subshells ~sw ran_subshell = function - | [] -> [] - | Ast.WordSubshell s :: rest -> - let _ctx, std = exec_subshell ~sw ctx s in - ran_subshell := true; - Ast.WordName (String.trim std) :: run_subshells ~sw ran_subshell rest - | Ast.WordDoubleQuoted word :: rest -> - let subshell_q = ref false in - let res = run_subshells ~sw subshell_q word in - if !subshell_q then res @ run_subshells ~sw subshell_q rest - else Ast.WordDoubleQuoted res :: run_subshells ~sw subshell_q rest - | Ast.WordSingleQuoted word :: rest -> - let subshell_q = ref false in - let res = run_subshells ~sw subshell_q word in - if !subshell_q then res @ run_subshells ~sw subshell_q rest - else Ast.WordSingleQuoted res :: run_subshells ~sw subshell_q rest - | v :: rest -> v :: run_subshells ~sw ran_subshell rest + let run_subshells s = + let _ctx, std = exec_subshell ctx s in + String.trim std in - Eio.Switch.run @@ fun sw -> run_subshells ~sw (ref false) wcs - - and handle_word_cst_subshell (ctx : ctx) wcs : Ast.word_cst = - if needs_subshelling wcs then begin - let wcs = handle_subshell ctx wcs in - wcs - end - else wcs - - and glob_expand ctx wc = - let wc = handle_word_cst_subshell ctx wc in - if Ast.has_glob wc && not ctx.options.no_path_expansion then - Ast.word_components_to_string wc |> fun pattern -> - Glob.glob_dir ~pattern (cwd_of_ctx ctx) - |> List.map (fun w -> [ Ast.WordName w ]) - else [ wc ] + run_subshells cc - and word_glob_expand (ctx : ctx) wc : Ast.word_cst list = - if List.exists needs_glob_expansion wc then glob_expand ctx wc - else [ handle_word_cst_subshell ctx wc ] + and glob_expand ctx pattern : ctx * Ast.fragment list = + ( ctx, + match Glob.glob_dir ~pattern (cwd_of_ctx ctx) with + | [] -> [ Ast.Fragment.make pattern ] + | exception _ -> [ Ast.Fragment.make pattern ] + | xs -> List.map Ast.Fragment.make xs ) and collect_assignments ?(update = true) ctx vs : ctx Exit.t = List.fold_left @@ -1039,13 +1086,14 @@ module Make (S : Types.State) (E : Types.Exec) = struct match prefix with | Ast.Prefix_assignment (Name param, v) -> ( (* Expand the values *) - let ctx, v = expand_cst ctx v in + let ctx, v = word_expansion ctx v in match ctx with | Exit.Nonzero _ as ctx -> ctx | Exit.Zero ctx -> ( - let v = handle_subshell ctx v in let state = - if update then S.update ctx.state ~param v + if update then + S.update ctx.state ~param + (Ast.Fragment.join_list ~sep:"" v) else Ok ctx.state in match state with @@ -1056,26 +1104,32 @@ module Make (S : Types.State) (E : Types.Exec) = struct ctx with state; local_state = - (param, Ast.word_components_to_string v) + (param, Ast.Fragment.join_list ~sep:"" v) :: ctx.local_state; })) | _ -> Exit.zero ctx)) (Exit.zero ctx) vs - and args ctx swc : ctx Exit.t * Ast.word_cst list = - List.fold_left - (fun (ctx, acc) -> function - | Ast.Suffix_redirect _ -> (ctx, acc) - | Suffix_word wc -> ( - match ctx with - | Exit.Nonzero _ as ctx -> (ctx, acc) - | Exit.Zero ctx -> ( - let ctx, cst = expand_cst ctx wc in - match ctx with - | Exit.Nonzero _ as ctx -> (ctx, acc) - | Exit.Zero c as ctx -> (ctx, acc @ word_glob_expand c cst)))) - (Exit.zero ctx, []) - swc + and args ctx swc : ctx Exit.t * string list = + let ctx, fs = + List.fold_left + (fun (ctx, acc) -> function + | Ast.Suffix_redirect _ -> (ctx, acc) + | Suffix_word wc -> ( + match ctx with + | Exit.Nonzero _ as ctx -> (ctx, acc) + | Exit.Zero ctx -> ( + let ctx, cst = word_expansion ctx wc in + let cst = Ast.Fragment.handle_joins cst in + (* Fmt.pr "Expanding: %a\n%!" Fmt.(list Ast.Fragment.pp) cst; *) + match ctx with + | Exit.Nonzero _ as ctx -> (ctx, acc) + | Exit.Zero _ as ctx -> (ctx, acc @ cst)))) + (Exit.zero ctx, []) + swc + in + (* Fmt.pr "Arguments: %a\n%!" Fmt.(list Ast.Fragment.pp) fs; *) + (ctx, List.map Ast.Fragment.to_string fs) and handle_built_in ~rdrs ~(stdout : Eio_unix.sink_ty Eio.Flow.sink) (ctx : ctx) v = diff --git a/src/lib/eval.mli b/src/lib/eval.mli index 0135d26..7b282ab 100644 --- a/src/lib/eval.mli +++ b/src/lib/eval.mli @@ -21,6 +21,7 @@ module Make (S : Types.State) (E : Types.Exec) : sig ?exit_handler:(unit -> unit) -> ?options:Built_ins.Options.t -> ?hash:Hash.t -> + ?in_double_quotes:bool -> fs:Eio.Fs.dir_ty Eio.Path.t -> stdin:Eio_unix.source_ty r -> stdout:Eio_unix.sink_ty r -> @@ -43,4 +44,9 @@ module Make (S : Types.State) (E : Types.Exec) : sig val run : ctx Exit.t -> Ast.t -> ctx Exit.t * Ast.t list (** [run ctx ast] evaluates [ast] using the initial [ctx]. *) + + (** {2 Private} *) + + val word_expansion : ctx -> Ast.word_cst -> ctx Exit.t * Ast.fragment list + (* Mostly for testing purposes, this exposes the logic for expanding words. *) end diff --git a/src/lib/interactive.ml b/src/lib/interactive.ml index 2dee885..b06fc62 100644 --- a/src/lib/interactive.ml +++ b/src/lib/interactive.ml @@ -94,8 +94,7 @@ module Make (S : Types.State) (E : Types.Exec) (H : Types.History) = struct in let rec loop (ctx : Eval.ctx Exit.t) = Option.iter (Fmt.epr "%s%!") - (S.lookup (Exit.value ctx |> Eval.state) ~param:"PS1" - |> Option.map Ast.word_components_to_string); + (S.lookup (Exit.value ctx |> Eval.state) ~param:"PS1"); let p = prompt ctx in Fmt.pr "%s\r%!" p; let hint command = diff --git a/src/lib/posix/state.ml b/src/lib/posix/state.ml index 08e7fb6..3c2289f 100644 --- a/src/lib/posix/state.ml +++ b/src/lib/posix/state.ml @@ -10,7 +10,7 @@ type t = { root : int; outermost : bool; home : string; - variables : (attributes * Merry.Ast.word_cst) Variables.t; + variables : (attributes * string) Variables.t; } let update ?(export = false) ?(readonly = false) t ~param v = @@ -26,9 +26,7 @@ let seed_env () = let env = Merry.Eunix.env () in List.fold_left (fun vars (param, v) -> - Variables.add param - ({ default_attribute with export = true }, [ Merry.Ast.WordName v ]) - vars) + Variables.add param ({ default_attribute with export = true }, v) vars) Variables.empty env let make ?(functions = []) ?(root = 0) ?(outermost = true) ?(home = "/root") @@ -60,27 +58,17 @@ let readonly t = let pp_readonly fmt t = let rs = readonly t in - let rs = - List.map - (fun (p, cst) -> - ("readonly " ^ p, Merry.Ast.word_components_to_string cst)) - rs - in + let rs = List.map (fun (p, cst) -> ("readonly " ^ p, cst)) rs in Fmt.(list ~sep:(Fmt.any "\n") (pair ~sep:(Fmt.any "=") string (quote string))) fmt rs let pp_export fmt t = let rs = exports t in - let rs = - List.map - (fun (p, cst) -> ("export " ^ p, Merry.Ast.word_components_to_string cst)) - rs - in + let rs = List.map (fun (p, cst) -> ("export " ^ p, cst)) rs in Fmt.(list ~sep:(Fmt.any "\n") (pair ~sep:(Fmt.any "=") string (quote string))) fmt rs let dump ppf s = Fmt.pf ppf "Variables:[%a]" - Fmt.(list ~sep:Fmt.comma (pair string Yojson.Safe.pp)) - (Variables.to_list s.variables - |> List.map (fun (s, (_, v)) -> (s, Merry.Ast.word_cst_to_yojson v))) + Fmt.(list ~sep:Fmt.comma (pair string string)) + (Variables.to_list s.variables |> List.map (fun (s, (_, v)) -> (s, v))) diff --git a/src/lib/sast.ml b/src/lib/sast.ml index 701c60c..c1a9349 100644 --- a/src/lib/sast.ml +++ b/src/lib/sast.ml @@ -126,6 +126,17 @@ and word_component = (* Empty CST. Useful to represent the absence of relevant CSTs. *) | WordEmpty +and fragment = { + txt : string; + splittable : bool; + globbable : bool; + join : [ `No | `With_previous | `With_next ]; (* Used for "args: [$@]" *) +} +(** Post expansion representation of strings ready for possible field splitting + and globbing. *) + +and fragments = fragment list + and bracket_expression = | BracketExpression_LBRACKET_MatchingList_RBRACKET of matching_list | BracketExpression_LBRACKET_NonMatchingList_RBRACKET of nonmatching_list diff --git a/src/lib/types.ml b/src/lib/types.ml index 009ef10..36355d6 100644 --- a/src/lib/types.ml +++ b/src/lib/types.ml @@ -20,7 +20,7 @@ module type State = sig val expand : t -> [ `Tilde ] -> string (** Expansions *) - val lookup : t -> param:string -> Ast.word_cst option + val lookup : t -> param:string -> string option (** Parameter lookup. [None] means [unset]. *) val update : @@ -28,7 +28,7 @@ module type State = sig ?readonly:bool -> t -> param:string -> - Ast.word_cst -> + string -> (t, string) result (** Update the state with a new parameter mapping and whether or not it should exported to the environment (default false). *) @@ -37,10 +37,10 @@ module type State = sig (** [remove ~param t] removes [param] from [t] if it exists. [bool] is [true] if a removal took place. *) - val exports : t -> (string * Ast.word_cst) list + val exports : t -> (string * string) list (** All of the variables that must be exported to the environment *) - val readonly : t -> (string * Ast.word_cst) list + val readonly : t -> (string * string) list (** All of the variables that must be exported to the environment *) val pp_readonly : t Fmt.t diff --git a/src/lib/util.ml b/src/lib/util.ml new file mode 100644 index 0000000..7c47666 --- /dev/null +++ b/src/lib/util.ml @@ -0,0 +1,67 @@ +(* Some random utils for debugging *) +open Eio.Std + +let traced_sink tag + (Eio.Resource.T (t, handler) : Eio_unix.sink_ty Eio.Flow.sink) : + Eio_unix.sink_ty r = + let module Sink = (val Eio.Resource.get handler Eio.Flow.Pi.Sink) in + let close = Eio.Resource.get handler Eio.Resource.Close in + let buf = Cstruct.create 4096 in + let copy () ~src = + try + while true do + match Eio.Flow.single_read src buf with + | i -> + Eio.traceln ">>>>> %s Single read: %s" tag (Cstruct.to_string buf); + let bufs = [ Cstruct.sub buf 0 i ] in + Sink.copy ~src:(Eio.Flow.cstruct_source bufs) t + done + with End_of_file -> Eio.traceln ">>>>>> EOF" + in + let single_write () x = + Eio.traceln ">>>>> single write: %s" (Cstruct.concat x |> Cstruct.to_string); + Sink.single_write t x + in + let module T = struct + type t = unit + + let single_write = single_write + let copy = copy + end in + let t = + Eio.Resource.handler + [ + H (Eio.Flow.Pi.Sink, (module T)); + H (Eio.Resource.Close, fun () -> close t); + ] + in + Eio.Resource.T ((), t) + +let traced_sink_flow tag + (Eio.Resource.T (t, handler) : Eio.Flow.sink_ty Eio.Flow.sink) : + Eio.Flow.sink_ty r = + let module Sink = (val Eio.Resource.get handler Eio.Flow.Pi.Sink) in + let buf = Cstruct.create 4096 in + let copy () ~src = + try + while true do + match Eio.Flow.single_read src buf with + | i -> + Eio.traceln ">>>>> %s Single read: %s" tag (Cstruct.to_string buf); + let bufs = [ Cstruct.sub buf 0 i ] in + Sink.copy ~src:(Eio.Flow.cstruct_source bufs) t + done + with End_of_file -> Eio.traceln ">>>>>> EOF" + in + let single_write () x = + Eio.traceln ">>>>> single write: %s" (Cstruct.concat x |> Cstruct.to_string); + Sink.single_write t x + in + let module T = struct + type t = unit + + let single_write = single_write + let copy = copy + end in + let t = Eio.Resource.handler [ H (Eio.Flow.Pi.Sink, (module T)) ] in + Eio.Resource.T ((), t) diff --git a/src/lib/wordexp.ml b/src/lib/wordexp.ml new file mode 100644 index 0000000..5d1e4e1 --- /dev/null +++ b/src/lib/wordexp.ml @@ -0,0 +1 @@ +(* Word expansion. *) diff --git a/test/debootstrap/Dockerfile b/test/debootstrap/Dockerfile index 69b251f..e72fb62 100644 --- a/test/debootstrap/Dockerfile +++ b/test/debootstrap/Dockerfile @@ -10,4 +10,7 @@ RUN opam exec -- dune build --profile=release FROM debian:13 COPY --from=builder /home/opam/src/_build/default/src/bin/main.exe /bin/msh +RUN ln -sf /bin/msh /bin/sh +RUN apt-get update \ + && apt-get install --no-install-recommends --assume-yes debootstrap ENTRYPOINT [ "msh" ] diff --git a/test/dune b/test/dune index 944df3e..f82971c 100644 --- a/test/dune +++ b/test/dune @@ -5,3 +5,7 @@ (test (name test_merry) (libraries eio morbig)) + +(test + (name wordexp) + (libraries eio_posix merry merry.posix alcotest)) diff --git a/test/fields.t b/test/fields.t new file mode 100644 index 0000000..e69de29 diff --git a/test/ryan.t b/test/ryan.t new file mode 100644 index 0000000..c6ea2d6 --- /dev/null +++ b/test/ryan.t @@ -0,0 +1,31 @@ +Test cases from the mind of Ryan Gibb; only those that were designed to torture +shell users and shell authors alike. + + $ cat > test.sh << EOF + > + > for arg in \$@; do + > echo "No quote: \$arg" + > done + > + > for arg in "\$@"; do + > echo "In quotes quote: \$arg" + > done + > EOF + + $ sh test.sh a b "c d" + No quote: a + No quote: b + No quote: c + No quote: d + In quotes quote: a + In quotes quote: b + In quotes quote: c d + $ msh test.sh a b "c d" + No quote: a + No quote: b + No quote: c + No quote: d + In quotes quote: a + In quotes quote: b + In quotes quote: c d + diff --git a/test/simple.t b/test/simple.t index 39e7e4c..8051067 100644 --- a/test/simple.t +++ b/test/simple.t @@ -78,6 +78,26 @@ Trying a common pattern for path-like expansion: $ msh test.sh /bin:/usr/bin +Some variable expansions: + + $ cat > test.sh << EOF + > echo "Got \$# arguments: [\$@]" + > for arg in "\$@"; do + > echo "[\$arg]" + > done + > EOF + + $ sh test.sh hello world "from the shell" + Got 3 arguments: [hello world from the shell] + [hello] + [world] + [from the shell] + $ msh test.sh hello world "from the shell" + Got 3 arguments: [hello world from the shell] + [hello] + [world] + [from the shell] + 2. Pipelines with And|Or 2.1 Simple Or diff --git a/test/wordexp.ml b/test/wordexp.ml new file mode 100644 index 0000000..13c6613 --- /dev/null +++ b/test/wordexp.ml @@ -0,0 +1,124 @@ +(* Thorough testing of word expansion *) +open Merry +module C = Merry.Eval.Make (Merry_posix.State) (Merry_posix.Exec) + +let expand ctx cst = C.word_expansion ctx cst |> snd +let fragment = Alcotest.of_pp Merry.Ast.Fragment.pp +let fragments = Alcotest.list fragment +let frags = List.map Ast.Fragment.make + +let with_default_ctx ?(args = []) ?(params = []) env fn = + let executor = Merry_posix.Exec.{ mgr = env#process_mgr } in + let interactive = false in + let pos_zero = "msh" in + Eio.Switch.run @@ fun async_switch -> + let signal_handler f = Eio_posix.run @@ fun _ -> f () in + let state = + Merry_posix.State.make + ~home:(Sys.getenv "HOME" ^ "/") + (Fpath.v (Merry.Eunix.cwd ())) + in + let state = + List.fold_left + (fun s (k, v) -> Merry_posix.State.update s ~param:k v |> Result.get_ok) + state params + in + let ctx = + C.make_ctx ~interactive state executor ~fs:env#fs ~stdin:env#stdin + ~stdout:env#stdout ~async_switch ~argv:(Array.of_list args) + ~program:pos_zero ~signal_handler + in + fn ctx + +module W = struct + let name s = Ast.WordName s + let lit s = Ast.WordLiteral s + let dquote c = Ast.WordDoubleQuoted c + let glob_all = Ast.WordGlobAll + + (* let squote c = Ast.WordSingleQuoted c *) + (* let arith a = Ast.WordArithmeticExpression a *) + let var v = Ast.WordVariable (Ast.VariableAtom (v, Ast.NoAttribute)) +end + +let test_no_expansions env () = + let args = [ "echo"; "hello" ] in + let cargs = W.[ name "echo"; lit "hello" ] in + with_default_ctx ~args env @@ fun ctx -> + let expected = frags args in + let actual = expand ctx cargs in + Alcotest.check fragments "same fragments" expected actual + +let test_dquote env () = + let args = [ "echo"; "\"hello there\"" ] in + let cargs = W.[ name "echo"; dquote [ lit "hello there" ] ] in + with_default_ctx ~args env @@ fun ctx -> + let expected = + Ast. + [ Fragment.make "echo"; Fragment.make ~join:`With_previous "hello there" ] + in + let actual = expand ctx cargs in + Alcotest.check fragments "same fragments" expected actual + +let test_dquote_expansion env () = + let args = [ "echo"; "\"hello $FOO...\"" ] in + let cargs = + W.[ name "echo"; dquote [ lit "hello "; var "FOO"; lit "..." ] ] + in + with_default_ctx ~args ~params:[ ("FOO", "there") ] env @@ fun ctx -> + let expected = + Ast.Fragment.[ make "echo"; make ~join:`With_previous "hello there..." ] + in + let actual = expand ctx cargs in + Alcotest.check fragments "same fragments" expected actual + +let test_single_expansion env () = + let args = [ "echo"; "$FOO" ] in + let cargs = W.[ name "echo"; var "FOO" ] in + with_default_ctx ~args ~params:[ ("FOO", "bar") ] env @@ fun ctx -> + let expected = [ Ast.Fragment.make "echo"; Ast.Fragment.make "bar" ] in + let actual = expand ctx cargs in + Alcotest.check fragments "same fragments" expected actual + +let test_argv_expansion env () = + let cargs = W.[ name "echo"; var "@" ] in + with_default_ctx ~args:[ "echo"; "a"; "b"; "c" ] env @@ fun ctx -> + let expected = frags [ "echo"; "a"; "b"; "c" ] in + let actual = expand ctx cargs in + Alcotest.check fragments "same fragments" expected actual + +let test_argv_in_quotes_expansion env () = + let cargs = W.[ name "echo"; dquote [ lit "got ["; var "@"; lit "]" ] ] in + with_default_ctx ~args:[ "echo"; "a"; "b"; "c" ] env @@ fun ctx -> + let expected = + Ast.Fragment. + [ + make "echo"; + make ~join:`With_previous "got [a"; + make "b"; + make ~join:`With_next "c]"; + ] + in + let actual = expand ctx cargs in + Alcotest.check fragments "same fragments" expected actual + +let test_glob env () = + let cargs = W.[ glob_all; lit ".ml" ] in + with_default_ctx ~args:[ "*.ml" ] env @@ fun ctx -> + let expected = Ast.Fragment.[ make "test_merry.ml"; make "wordexp.ml" ] in + let actual = expand ctx cargs in + Alcotest.check fragments "same fragments" expected actual + +let simple env = + [ + ("no expansions", `Quick, test_no_expansions env); + ("double quote", `Quick, test_dquote env); + ("double quote expansion", `Quick, test_dquote_expansion env); + ("single expansion", `Quick, test_single_expansion env); + ("argv expansion", `Quick, test_argv_expansion env); + ("argv expansion dquote", `Quick, test_argv_in_quotes_expansion env); + ("glob all", `Quick, test_glob env); + ] + +let () = + Eio_posix.run @@ fun env -> Alcotest.run "wordexp" [ ("simple", simple env) ]