Select the types of activity you want to include in your feed.
Handle argument expansion better
Still not happy with this implementation, but it is closer to the specification in terms of field-splitting and handling of awkward parameters like $@.
···55 Merry.Interactive.Make (Merry_posix.State) (Merry_posix.Exec)
66 (Merry.History.Prefix_search)
7788-let sh ~command ~dump ~file ~rest env =
88+let sh ~command_flag ~dump ~file ~rest env =
99 let executor = Merry_posix.Exec.{ mgr = env#process_mgr } in
1010- let interactive = Option.is_none file && Option.is_none command in
1010+ let interactive = Option.is_none file && rest = [] in
1111 let pos_zero = match file with Some f -> f | None -> "msh" in
1212 Eio.Switch.run @@ fun async_switch ->
1313 let signal_handler f = Eio_posix.run @@ fun _ -> f () in
···1717 ~home:(Sys.getenv "HOME" ^ "/")
1818 (Fpath.v (Merry.Eunix.cwd ())))
1919 executor ~fs:env#fs ~stdin:env#stdin ~stdout:env#stdout ~async_switch
2020- ~argv:(Array.of_list (pos_zero :: rest))
2020+ ~argv:(Array.of_list (pos_zero :: (try List.tl rest with _ -> [])))
2121 ~program:pos_zero ~signal_handler
2222 in
2323- match (file, command) with
2424- | None, None -> I.run (Merry.Exit.zero ctx)
2323+ match (file, command_flag) with
2424+ | None, false -> I.run (Merry.Exit.zero ctx)
2525 | _ ->
2626 let ast =
2727- match (file, command) with
2828- | None, None -> assert false
2929- | Some file, None -> Merry.Ast.of_file Eio.Path.(env#fs / file)
3030- | _, Some c -> Merry.Ast.of_string c
2727+ match (file, command_flag, rest) with
2828+ | None, false, _ -> assert false
2929+ | Some file, false, _ -> Merry.Ast.of_file Eio.Path.(env#fs / file)
3030+ | _, true, c :: _ -> Merry.Ast.of_string c
3131+ | _, b, cs -> Fmt.failwith "Bad usage: %b %a" b Fmt.(list string) cs
3132 in
3233 if dump then Merry.Ast.Dump.pp Fmt.stdout ast
3334 else
···3738open Cmdliner
3839open Cmdliner.Term.Syntax
39404040-let command =
4141- let doc = "command to run" in
4242- Arg.(value & opt (some string) None & info [ "c"; "C" ] ~doc)
4141+let command_flag =
4242+ let doc = "Run commands from the command-line" in
4343+ Arg.(value & flag & info [ "c" ] ~doc)
43444445let file =
4546 let doc = "The shell script to execute" in
···5253 in
5354 Arg.(value & flag & info [ "d"; "D"; "dump" ] ~doc)
54555555-let rest = Arg.(value & pos_right 0 string [] & info [])
5656+let rest = Arg.(value & pos_all string [] & info [])
56575758let cmd env =
5859 let doc = "Mere's shell." in
···7475 in
7576 Cmd.make (Cmd.info "msh" ~version:"v0.0.1" ~doc ~man)
7677 @@
7777- let+ command = command and+ dump = dump and+ file = file and+ rest = rest in
7878- sh ~command ~dump ~file ~rest env
7878+ let+ command_flag = command_flag
7979+ and+ dump = dump
8080+ and+ file = file
8181+ and+ rest = rest in
8282+ sh ~command_flag ~dump ~file ~rest env
79838084let main () = Eio_posix.run @@ fun env -> Cmd.eval (cmd env)
8185
+2-3
src/lib/arith.ml
···2727 let eval initial_state expr =
2828 let lookup state s =
2929 match S.lookup state ~param:s with
3030- | Some [ Ast.WordLiteral n ] when Option.is_some (int_of_string_opt n) ->
3131- int_of_string n
3030+ | Some n when Option.is_some (int_of_string_opt n) -> int_of_string n
3231 | _ -> 0
3332 in
3433 let update state s i =
3535- match S.update state ~param:s [ Ast.WordLiteral (string_of_int i) ] with
3434+ match S.update state ~param:s (string_of_int i) with
3635 | Ok s -> s
3736 | Error m -> failwith m
3837 in
+51-11
src/lib/ast.ml
···748748 let fname = Eio.Path.native_exn path in
749749 Eio.Path.load path |> of_string ~filename:fname
750750751751-let rec word_component_to_string : word_component -> string = function
752752- | WordName s -> s
753753- | WordLiteral s -> s
754754- | WordDoubleQuoted s -> word_components_to_string s
755755- | WordSingleQuoted s -> word_components_to_string s
756756- | WordGlobAll -> "*"
757757- | WordGlobAny -> "?"
758758- | WordEmpty -> ""
759759- | WordAssignmentWord (Name p, v) -> p ^ "=" ^ word_components_to_string v
751751+let rec word_component_to_string :
752752+ ?field_splitting:bool -> word_component -> string list =
753753+ fun ?(field_splitting = true) -> function
754754+ | WordName s -> [ s ]
755755+ | WordLiteral s -> [ s ]
756756+ | WordDoubleQuoted s -> word_components_to_strings ~field_splitting:false s
757757+ | WordSingleQuoted s -> word_components_to_strings ~field_splitting:false s
758758+ | WordGlobAll -> [ "*" ]
759759+ | WordGlobAny -> [ "?" ]
760760+ | WordEmpty -> [ "" ]
761761+ | WordAssignmentWord (Name p, v) ->
762762+ p :: "=" :: word_components_to_strings ~field_splitting v
760763 | WordSubshell _ ->
761764 Fmt.failwith
762765 "This is an error in Merry, subshells should already have been \
···765768 Fmt.failwith "Conversion of %a" Yojson.Safe.pp
766769 (word_component_to_yojson v)
767770768768-and word_components_to_string ws =
769769- String.concat "" (List.map word_component_to_string ws)
771771+and word_components_to_strings ?(field_splitting = true) ws =
772772+ if field_splitting then
773773+ List.concat_map (word_component_to_string ~field_splitting) ws
774774+ else
775775+ [
776776+ String.concat ""
777777+ (List.concat_map (word_component_to_string ~field_splitting) ws);
778778+ ]
770779771780class check_ast =
772781 object (_)
773782 inherit [bool] Sast.fold
774783 method int _ ctx = ctx
784784+ method bool _ ctx = ctx
775785 method string _ ctx = ctx
776786 method char _ ctx = ctx
777787 method option f v ctx = Option.fold ~none:ctx ~some:(fun i -> f i ctx) v
···822832 end
823833 in
824834 o#word_cst ast false
835835+836836+module Fragment = struct
837837+ let make ?(splittable = false) ?(globbable = false) ?(join = `No) txt =
838838+ { txt; splittable; join; globbable }
839839+840840+ let empty = make ""
841841+ let to_string { txt; _ } = txt
842842+ let join ~sep f1 f2 = { f1 with txt = f1.txt ^ sep ^ f2.txt }
843843+ let join_list ~sep fs = List.fold_left (join ~sep) empty fs |> to_string
844844+845845+ let pp_join ppf = function
846846+ | `No -> Fmt.pf ppf "no"
847847+ | `With_previous -> Fmt.pf ppf "with-previous"
848848+ | `With_next -> Fmt.pf ppf "with-next"
849849+850850+ let pp ppf { txt; join; splittable; globbable } =
851851+ Fmt.pf ppf "{ txt = %s; join = %a; splittable = %b; globbable = %b }" txt
852852+ pp_join join splittable globbable
853853+854854+ let handle_joins cst =
855855+ let rec loop = function
856856+ | [] -> []
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
861861+ | x :: xs -> x :: loop xs
862862+ in
863863+ loop cst
864864+end
+21-2
src/lib/ast.mli
···19192020(* class map : Ppxlib_traverse_builtins.map *)
21212222-val word_component_to_string : word_component -> string
2323-val word_components_to_string : word_cst -> string
2222+val word_component_to_string :
2323+ ?field_splitting:bool -> word_component -> string list
2424+2525+val word_components_to_strings :
2626+ ?field_splitting:bool -> word_cst -> string list
24272528val has_async : complete_command -> bool
2629(** Checks, recursively, the command to see if there is any use of the async
···28312932val has_glob : word_cst -> bool
3033(** Checks whether or not any glob patterns exist in a given word_cst *)
3434+3535+module Fragment : sig
3636+ val make :
3737+ ?splittable:bool ->
3838+ ?globbable:bool ->
3939+ ?join:[ `No | `With_next | `With_previous ] ->
4040+ string ->
4141+ fragment
4242+4343+ val empty : fragment
4444+ val to_string : fragment -> string
4545+ val join : sep:string -> fragment -> fragment -> fragment
4646+ val join_list : sep:string -> fragment list -> string
4747+ val handle_joins : fragment list -> fragment list
4848+ val pp : fragment Fmt.t
4949+end
31503251module Dump : sig
3352 val pp : t Fmt.t
+318-264
src/lib/eval.ml
···3939 rdrs : Types.redirect list;
4040 signal_handler : signal_handler;
4141 exit_handler : (unit -> unit) option;
4242+ in_double_quotes : bool;
4243 }
43444445 let _stdin ctx = ctx.stdin
···4647 let make_ctx ?(interactive = false) ?(subshell = false) ?(local_state = [])
4748 ?(background_jobs = []) ?(last_background_process = "") ?(functions = [])
4849 ?(rdrs = []) ?exit_handler ?(options = Built_ins.Options.default)
4949- ?(hash = Hash.empty) ~fs ~stdin ~stdout ~async_switch ~program ~argv
5050- ~signal_handler state executor =
5050+ ?(hash = Hash.empty) ?(in_double_quotes = false) ~fs ~stdin ~stdout
5151+ ~async_switch ~program ~argv ~signal_handler state executor =
5152 let signal_handler = { run = signal_handler; sigint_set = false } in
5253 {
5354 interactive;
···6970 rdrs;
7071 signal_handler;
7172 exit_handler;
7373+ in_double_quotes;
7274 }
73757476 let state ctx = ctx.state
···7678 let fs ctx = ctx.fs
7779 let clear_local_state ctx = { ctx with local_state = [] }
78807979- let rec tilde_expansion ctx = function
8080- | [] -> []
8181- | Ast.WordTildePrefix _ :: rest ->
8282- Ast.WordName (S.expand ctx.state `Tilde) :: tilde_expansion ctx rest
8383- | v :: rest -> v :: tilde_expansion ctx rest
8181+ let tilde_expansion ctx = function
8282+ | Ast.WordTildePrefix _ -> Ast.WordLiteral (S.expand ctx.state `Tilde)
8383+ | v -> v
8484+8585+ let word_cst_to_string ?field_splitting v =
8686+ Ast.word_components_to_strings ?field_splitting v |> String.concat ""
84878585- let arithmetic_expansion ctx expr =
8686- let rec fold (ctx, cst) = function
8787- | [] -> (ctx, cst)
8888- | Ast.WordArithmeticExpression word :: rest ->
8989- let expr = Ast.word_components_to_string word in
9090- let aexpr =
9191- Arith_parser.main Arith_lexer.read (Lexing.from_string expr)
9292- in
9393- let state, i = A.eval ctx.state aexpr in
9494- fold
9595- ({ ctx with state }, Ast.WordLiteral (string_of_int i) :: cst)
9696- rest
9797- | Ast.WordDoubleQuoted dq :: rest ->
9898- let ctx, v = fold (ctx, []) dq in
9999- fold (ctx, Ast.WordDoubleQuoted (List.rev v) :: cst) rest
100100- | Ast.WordSingleQuoted dq :: rest ->
101101- let ctx, v = fold (ctx, []) dq in
102102- fold (ctx, Ast.WordSingleQuoted (List.rev v) :: cst) rest
103103- | v :: rest -> fold (ctx, v :: cst) rest
104104- in
105105- let state, cst = fold (ctx, []) expr in
106106- (state, List.rev cst)
8888+ let arithmetic_expansion ctx word =
8989+ let expr = word_cst_to_string word in
9090+ let aexpr = Arith_parser.main Arith_lexer.read (Lexing.from_string expr) in
9191+ let state, i = A.eval ctx.state aexpr in
9292+ ({ ctx with state }, Ast.Fragment.make (string_of_int i))
1079310894 let stdout_for_pipeline ~sw ctx = function
10995 | [] -> (None, `Global ctx.stdout)
···119105 match op with
120106 | Io_op_less ->
121107 (* Simple redirection for input *)
122122- let r =
123123- Eio.Path.open_in ~sw (ctx.fs / Ast.word_components_to_string file)
124124- in
108108+ let r = Eio.Path.open_in ~sw (ctx.fs / word_cst_to_string file) in
125109 let fd = Eio_unix.Resource.fd_opt r |> Option.get in
126110 [ Types.Redirect (n, fd, `Blocking) ]
127111 | Io_op_lessand -> (
···148132 in
149133 let w =
150134 Eio.Path.open_out ~sw ~append ~create
151151- (ctx.fs / Ast.word_components_to_string file)
135135+ (ctx.fs / word_cst_to_string file)
152136 in
153137 let fd = Eio_unix.Resource.fd_opt w |> Option.get in
154138 [ Types.Redirect (n, fd, `Blocking) ]
···171155 (* Simple file creation *)
172156 let w =
173157 Eio.Path.open_out ~sw ~create:(`If_missing 0o644)
174174- (ctx.fs / Ast.word_components_to_string file)
158158+ (ctx.fs / word_cst_to_string file)
175159 in
176160 let fd = Eio_unix.Resource.fd_opt w |> Option.get in
177161 [
···181165 | Io_op_clobber ->
182166 let w =
183167 Eio.Path.open_out ~sw ~create:(`Or_truncate 0o644)
184184- (ctx.fs / Ast.word_components_to_string file)
168168+ (ctx.fs / word_cst_to_string file)
185169 in
186170 let fd = Eio_unix.Resource.fd_opt w |> Option.get in
187171 [ Types.Redirect (n, fd, `Blocking) ]
···197181198182 let cwd_of_ctx ctx = S.cwd ctx.state |> Fpath.to_string |> ( / ) ctx.fs
199183200200- let needs_glob_expansion : Ast.word_component -> bool = function
201201- | WordGlobAll | WordGlobAny -> true
202202- | _ -> false
203203-204184 let resolve_program ?(update = true) ctx name =
205185 let v =
206186 if not (String.contains name '/') then begin
207187 S.lookup ctx.state ~param:"PATH"
208208- |> Option.map Ast.word_components_to_string
209188 |> Option.value ~default:"/bin:/usr/bin"
210189 |> String.split_on_char ':'
211190 |> List.find_map (fun dir ->
···224203225204 let get_env ?(extra = []) ctx =
226205 let extra =
227227- extra
228228- @ List.map (fun (k, v) -> (k, Ast.word_components_to_string v))
229229- @@ S.exports ctx.state
206206+ extra @ List.map (fun (k, v) -> (k, v)) @@ S.exports ctx.state
230207 in
231208 let env = Eunix.env () in
232209 List.fold_left (fun acc (k, _) -> List.remove_assoc k acc) env extra
···241218242219 let remove_quotes s =
243220 let s_len = String.length s in
244244- if s.[0] = '"' && s.[s_len - 1] = '"' then String.sub s 1 (s_len - 2) else s
221221+ let s = if s.[0] = '"' then String.sub s 1 (s_len - 1) else s in
222222+ let s_len = String.length s in
223223+ if s.[s_len - 1] = '"' then String.sub s 0 (s_len - 1) else s
245224246225 let exit ctx code =
247226 Option.iter (fun f -> f ()) ctx.exit_handler;
···318297 loop (Exit.value ctx) job stdout_of_previous
319298 (Ast.SimpleCommand (Named (executable, suffix)) :: rest)
320299 | Ast.SimpleCommand (Named (executable, suffix)) :: rest -> (
321321- let ctx, executable = expand_cst ctx executable in
300300+ let ctx, executable = word_expansion ctx executable in
322301 match ctx with
323302 | Exit.Nonzero _ as ctx ->
324303 let job = handle_job job (`Built_in (Exit.ignore ctx)) in
325304 loop (Exit.value ctx) job stdout_of_previous rest
326305 | Exit.Zero ctx -> (
327327- let executable = handle_word_cst_subshell ctx executable in
328306 let executable, extra_args =
329307 (* This is a side-effect of the alias command with something like
330308 alias ls="ls -la" *)
331331- match executable with
332332- | [ Ast.WordLiteral s ] as v -> (
333333- match String.split_on_char ' ' (remove_quotes s) with
334334- | exec :: args ->
335335- ( [ Ast.WordName exec ],
336336- List.map
337337- (fun w -> Ast.Suffix_word [ Ast.WordName w ])
338338- args )
339339- | _ -> (v, []))
340340- | v -> (v, [])
309309+ match
310310+ Ast.Fragment.join_list ~sep:"" executable
311311+ |> String.split_on_char ' ' |> List.map Ast.Fragment.make
312312+ with
313313+ | [] -> ("", [])
314314+ | exec :: args ->
315315+ ( remove_quotes exec.txt,
316316+ List.map
317317+ (fun v ->
318318+ Ast.Suffix_word
319319+ [ Ast.WordLiteral (remove_quotes v.Ast.txt) ])
320320+ args )
341321 in
342342- let executable = Ast.word_components_to_string executable in
343322 let ctx, suffix =
344323 match suffix with
345324 | None -> (ctx, [])
···351330 let job = handle_job job (`Built_in (Exit.ignore ctx)) in
352331 loop (Exit.value ctx) job stdout_of_previous rest
353332 | Exit.Zero ctx -> (
354354- let args_as_strings =
355355- List.map Ast.word_components_to_string args
356356- in
357333 let some_read, some_write =
358334 stdout_for_pipeline ~sw:pipeline_switch ctx rest
359335 in
···373349 match handle_redirections ~sw:pipeline_switch ctx rdrs with
374350 | Error ctx -> (ctx, handle_job job (`Rdr (Exit.nonzero () 1)))
375351 | Ok rdrs -> (
376376- match
377377- Built_ins.of_args (executable :: args_as_strings)
378378- with
352352+ match Built_ins.of_args (executable :: args) with
379353 | Some (Error _) ->
380354 (ctx, handle_job job (`Built_in (Exit.nonzero () 1)))
381355 | (None | Some (Ok (Command _))) as v -> (
···415389 else
416390 let ctx = { ctx with stdout = some_write } in
417391 handle_function_application ctx
418418- ~name:executable
419419- (ctx.program :: args_as_strings)
392392+ ~name:executable (ctx.program :: args)
420393 in
421394 match func_app with
422395 | Some ctx ->
···474447 Exit.zero ("echo", [ prog ])
475448 else Exit.zero (x, xs))
476449 end
477477- else
478478- Exit.zero (executable, args_as_strings)
450450+ else Exit.zero (executable, args)
479451 in
480452 match exec_and_args with
481453 | Exit.Nonzero _ as v ->
···503475 ~stdin:stdout
504476 ~stdout:some_write
505477 ~pgid:(job_pgid job)
506506- executable args_as_strings
478478+ executable args
507479 in
508480 close_stdout ~is_global some_write;
509481 loop ctx job some_read rest)))))
···564536 }
565537 end
566538567567- and parameter_expansion' ctx ast =
539539+ and parameter_expansion ctx ast : ctx Exit.t * Ast.fragment list list =
568540 let get_prefix ~pattern ~kind param =
569541 let _, prefix =
570542 String.fold_left
···596568 in
597569 prefix
598570 in
599599- let rec expand acc ctx = function
600600- | [] -> (Exit.zero ctx, List.rev acc |> List.concat)
601601- | Ast.WordVariable v :: rest -> (
571571+ let tl_or_empty v = try List.tl v with _ -> [] in
572572+ let expand ctx v : ctx Exit.t * Ast.fragment list list =
573573+ let module Fragment = struct
574574+ include Ast.Fragment
575575+576576+ let make ?(join = if ctx.in_double_quotes then `With_previous else `No)
577577+ ?globbable ?splittable v =
578578+ Ast.Fragment.make ~join ?splittable ?globbable v
579579+ end in
580580+ match v with
581581+ | Ast.WordVariable v -> (
602582 match v with
603583 | Ast.VariableAtom ("!", NoAttribute) ->
604604- expand
605605- ([ Ast.WordName ctx.last_background_process ] :: acc)
606606- ctx rest
584584+ (Exit.zero ctx, [ [ Fragment.make ctx.last_background_process ] ])
607585 | Ast.VariableAtom ("-", NoAttribute) ->
608586 let i = if ctx.interactive then "i" else "" in
609609- expand
610610- ([ Ast.WordName (Built_ins.Options.to_letters ctx.options ^ i) ]
611611- :: acc)
612612- ctx rest
587587+ ( Exit.zero ctx,
588588+ [
589589+ [
590590+ Fragment.make (Built_ins.Options.to_letters ctx.options ^ i);
591591+ ];
592592+ ] )
593593+ | Ast.VariableAtom ("@", NoAttribute) ->
594594+ let args = tl_or_empty @@ Array.to_list ctx.argv in
595595+ let args =
596596+ if not ctx.in_double_quotes then
597597+ List.map
598598+ (fun v -> [ Fragment.make ~join:`No ~splittable:true v ])
599599+ args
600600+ else
601601+ let l = List.length args in
602602+ List.mapi
603603+ (fun idx arg ->
604604+ if idx = 0 then [ Fragment.make ~join:`With_previous arg ]
605605+ else if idx = l - 1 then
606606+ [ Fragment.make ~join:`With_next arg ]
607607+ else [ Fragment.make ~join:`No arg ])
608608+ args
609609+ in
610610+611611+ (Exit.zero ctx, args)
612612+ | Ast.VariableAtom ("#", NoAttribute) ->
613613+ ( Exit.zero ctx,
614614+ [
615615+ [
616616+ Fragment.make
617617+ (string_of_int
618618+ (List.length @@ tl_or_empty (Array.to_list ctx.argv)));
619619+ ];
620620+ ] )
613621 | Ast.VariableAtom (n, NoAttribute)
614622 when Option.is_some (int_of_string_opt n) -> (
615623 let n = int_of_string n in
616624 match Array.get ctx.argv n with
617617- | v -> expand ([ Ast.WordName v ] :: acc) ctx rest
625625+ | v -> (Exit.zero ctx, [ [ Fragment.make v ] ])
618626 | exception Invalid_argument _ ->
619619- expand ([ Ast.WordName "" ] :: acc) ctx rest)
627627+ (Exit.zero ctx, [ [ Fragment.make "" ] ]))
620628 | Ast.VariableAtom (s, NoAttribute) -> (
621629 match S.lookup ctx.state ~param:s with
622630 | None ->
623631 if ctx.options.no_unset then begin
624632 ( Exit.nonzero_msg ctx ~exit_code:1 "%s: unbound variable" s,
625625- List.rev acc |> List.concat )
633633+ [ [ Fragment.make "" ] ] )
626634 end
627627- else expand ([ Ast.WordName "" ] :: acc) ctx rest
628628- | Some cst -> expand (cst :: acc) ctx rest)
635635+ else (Exit.zero ctx, [ [ Fragment.make "" ] ])
636636+ | Some cst ->
637637+ ( Exit.zero ctx,
638638+ [
639639+ [
640640+ Fragment.make ~splittable:(not ctx.in_double_quotes) cst;
641641+ ];
642642+ ] ))
629643 | Ast.VariableAtom (s, ParameterLength) -> (
630644 match S.lookup ctx.state ~param:s with
631631- | None -> expand ([ Ast.WordLiteral "0" ] :: acc) ctx rest
645645+ | None -> (Exit.zero ctx, [ [ Fragment.make "0" ] ])
632646 | Some cst ->
633633- expand
634634- ([
635635- Ast.WordLiteral
636636- (string_of_int
637637- (String.length (Ast.word_components_to_string cst)));
638638- ]
639639- :: acc)
640640- ctx rest)
647647+ ( Exit.zero ctx,
648648+ [ [ Fragment.make (string_of_int (String.length cst)) ] ] ))
641649 | Ast.VariableAtom (s, UseDefaultValues (_, cst)) -> (
642650 match S.lookup ctx.state ~param:s with
643643- | None -> expand (cst :: acc) ctx rest
644644- | Some cst -> expand (cst :: acc) ctx rest)
651651+ | None ->
652652+ (Exit.zero ctx, [ [ Fragment.make (word_cst_to_string cst) ] ])
653653+ | Some cst -> (Exit.zero ctx, [ [ Fragment.make cst ] ]))
645654 | Ast.VariableAtom
646655 ( s,
647656 (( RemoveSmallestPrefixPattern cst
648657 | RemoveLargestPrefixPattern cst ) as v) ) -> (
649649- let ctx, spp = expand_cst ctx cst in
658658+ let ctx, spp = word_expansion ctx cst in
650659 match ctx with
651651- | Exit.Nonzero _ as ctx -> (ctx, List.rev acc |> List.concat)
660660+ | Exit.Nonzero _ as ctx -> (ctx, [ [ Fragment.make "" ] ])
652661 | Exit.Zero ctx -> (
653653- let pattern = Ast.word_components_to_string spp in
662662+ let pattern = Fragment.join_list ~sep:"" spp in
654663 match S.lookup ctx.state ~param:s with
655655- | None -> expand (cst :: acc) ctx rest
664664+ | None ->
665665+ ( Exit.zero ctx,
666666+ [ [ Fragment.make (word_cst_to_string cst) ] ] )
656667 | Some cst -> (
657668 let kind =
658669 match v with
···660671 | RemoveLargestPrefixPattern _ -> `Largest
661672 | _ -> assert false
662673 in
663663- let param = Ast.word_components_to_string cst in
674674+ let param = cst in
664675 let prefix = get_prefix ~pattern ~kind param in
665676 match prefix with
666666- | None -> expand ([ Ast.WordName param ] :: acc) ctx rest
677677+ | None -> (Exit.zero ctx, [ [ Fragment.make param ] ])
667678 | Some s -> (
668679 match String.cut_prefix ~prefix:s param with
669669- | Some s ->
670670- expand ([ Ast.WordName s ] :: acc) ctx rest
671671- | None ->
672672- expand ([ Ast.WordName param ] :: acc) ctx rest)))
673673- )
680680+ | Some s -> (Exit.zero ctx, [ [ Fragment.make s ] ])
681681+ | None -> (Exit.zero ctx, [ [ Fragment.make param ] ])
682682+ ))))
674683 | Ast.VariableAtom
675684 ( s,
676685 (( RemoveSmallestSuffixPattern cst
677686 | RemoveLargestSuffixPattern cst ) as v) ) -> (
678678- let ctx, spp = expand_cst ctx cst in
679679- let pattern = Ast.word_components_to_string spp in
687687+ let ctx, spp = word_expansion ctx cst in
688688+ let pattern = Fragment.join_list ~sep:"" spp in
680689 match ctx with
681681- | Exit.Nonzero _ as ctx -> (ctx, List.rev acc |> List.concat)
690690+ | Exit.Nonzero _ as ctx -> (ctx, [ [ Fragment.empty ] ])
682691 | Exit.Zero ctx -> (
683692 match S.lookup ctx.state ~param:s with
684684- | None -> expand (cst :: acc) ctx rest
693693+ | None ->
694694+ ( Exit.zero ctx,
695695+ [ [ Fragment.make (word_cst_to_string cst) ] ] )
685696 | Some cst -> (
686697 let kind =
687698 match v with
···689700 | RemoveLargestSuffixPattern _ -> `Largest
690701 | _ -> assert false
691702 in
692692- let param = Ast.word_components_to_string cst in
703703+ let param = cst in
693704 let suffix = get_suffix ~pattern ~kind param in
694705 match suffix with
695695- | None -> expand ([ Ast.WordName param ] :: acc) ctx rest
706706+ | None -> (Exit.zero ctx, [ [ Fragment.make param ] ])
696707 | Some s -> (
697708 match String.cut_suffix ~suffix:s param with
698698- | Some s ->
699699- expand ([ Ast.WordName s ] :: acc) ctx rest
700700- | None ->
701701- expand ([ Ast.WordName param ] :: acc) ctx rest)))
702702- )
709709+ | Some s -> (Exit.zero ctx, [ [ Fragment.make s ] ])
710710+ | None -> (Exit.zero ctx, [ [ Fragment.make param ] ])
711711+ ))))
703712 | Ast.VariableAtom (s, UseAlternativeValue (_, alt)) -> (
704713 match S.lookup ctx.state ~param:s with
705705- | Some _ -> expand (alt :: acc) ctx rest
706706- | None -> expand ([ Ast.WordEmpty ] :: acc) ctx rest)
714714+ | Some _ ->
715715+ (Exit.zero ctx, [ [ Fragment.make (word_cst_to_string alt) ] ])
716716+ | None -> (Exit.zero ctx, [ [ Fragment.empty ] ]))
707717 | Ast.VariableAtom (s, AssignDefaultValues (_, value)) -> (
708718 match S.lookup ctx.state ~param:s with
709709- | Some cst -> expand (cst :: acc) ctx rest
719719+ | Some cst -> (Exit.zero ctx, [ [ Fragment.make cst ] ])
710720 | None -> (
711711- match S.update ctx.state ~param:s value with
721721+ match
722722+ S.update ctx.state ~param:s (word_cst_to_string value)
723723+ with
712724 | Ok state ->
713725 let new_ctx = { ctx with state } in
714714- expand (value :: acc) new_ctx rest
726726+ ( Exit.zero new_ctx,
727727+ [ [ Fragment.make (word_cst_to_string value) ] ] )
715728 | Error m ->
716729 ( Exit.nonzero_msg ~exit_code:1 ctx "%s" m,
717717- List.rev acc |> List.concat )))
730730+ [ [ Fragment.empty ] ] )))
718731 | Ast.VariableAtom (_, IndicateErrorifNullorUnset (_, _)) ->
719732 Fmt.failwith "TODO: Indicate Error")
720720- | Ast.WordDoubleQuoted cst :: rest -> (
721721- let new_ctx, cst_acc = expand [] ctx cst in
733733+ | Ast.WordDoubleQuoted cst -> (
734734+ let ctx = { ctx with in_double_quotes = true } in
735735+ let new_ctx, cst_acc = word_expansion ctx cst in
736736+ (* We now do any joining for $@... *)
737737+ let cst_acc = Fragment.handle_joins cst_acc in
738738+ let new_ctx =
739739+ Exit.map
740740+ ~f:(fun ctx -> { ctx with in_double_quotes = false })
741741+ new_ctx
742742+ in
722743 match new_ctx with
723723- | Exit.Nonzero _ -> (new_ctx, cst_acc)
724724- | Exit.Zero new_ctx ->
725725- expand ([ Ast.WordDoubleQuoted cst_acc ] :: acc) new_ctx rest)
726726- | Ast.WordSingleQuoted cst :: rest -> (
727727- let new_ctx, cst_acc = expand [] ctx cst in
744744+ | Exit.Nonzero _ -> (new_ctx, [ cst_acc ])
745745+ | Exit.Zero new_ctx -> (Exit.zero new_ctx, [ cst_acc ]))
746746+ | Ast.WordSingleQuoted cst -> (
747747+ let ctx = { ctx with in_double_quotes = true } in
748748+ let new_ctx, cst_acc = word_expansion ctx cst in
749749+ let new_ctx =
750750+ Exit.map
751751+ ~f:(fun ctx -> { ctx with in_double_quotes = false })
752752+ new_ctx
753753+ in
728754 match new_ctx with
729729- | Exit.Nonzero _ -> (new_ctx, cst_acc)
730730- | Exit.Zero new_ctx ->
731731- expand ([ Ast.WordSingleQuoted cst_acc ] :: acc) new_ctx rest)
732732- | Ast.WordAssignmentWord (n, w) :: rest -> (
733733- let new_ctx, cst_acc = expand [] ctx w in
755755+ | Exit.Nonzero _ -> (new_ctx, [ cst_acc ])
756756+ | Exit.Zero new_ctx -> (Exit.zero new_ctx, [ cst_acc ]))
757757+ | Ast.WordAssignmentWord (Name n, w) -> (
758758+ let new_ctx, cst_acc = word_expansion ctx w in
734759 match new_ctx with
735735- | Exit.Nonzero _ -> (new_ctx, cst_acc)
736736- | Exit.Zero new_ctx ->
737737- expand
738738- ([ Ast.WordAssignmentWord (n, cst_acc) ] :: acc)
739739- new_ctx rest)
740740- | v :: rest -> expand ([ v ] :: acc) ctx rest
760760+ | Exit.Nonzero _ -> (new_ctx, [ cst_acc ])
761761+ | Exit.Zero _ ->
762762+ ( new_ctx,
763763+ [
764764+ [
765765+ Fragment.make
766766+ (n ^ "="
767767+ ^ Fragment.join_list ~sep:"" (List.concat [ cst_acc ]));
768768+ ];
769769+ ] ))
770770+ | Ast.WordSubshell sub ->
771771+ (* Command substitution *)
772772+ let s = command_substitution ctx sub in
773773+ (Exit.zero ctx, [ [ Fragment.make s ] ])
774774+ | Ast.WordArithmeticExpression cst ->
775775+ arithmetic_expansion ctx cst |> fun (ctx, v) ->
776776+ (Exit.zero ctx, [ [ v ] ])
777777+ | Ast.WordName s -> (Exit.zero ctx, [ [ Fragment.make s ] ])
778778+ | Ast.WordLiteral s -> (Exit.zero ctx, [ [ Fragment.make s ] ])
779779+ | Ast.WordGlobAll ->
780780+ (Exit.zero ctx, [ [ Fragment.make ~globbable:true "*" ] ])
781781+ | Ast.WordGlobAny ->
782782+ (Exit.zero ctx, [ [ Fragment.make ~globbable:true "?" ] ])
783783+ | v ->
784784+ Fmt.failwith "TODO: expansion of %a" yojson_pp
785785+ (Ast.word_component_to_yojson v)
741786 in
742742- expand [] ctx ast
787787+ expand ctx ast
788788+789789+ and field_splitting ctx = function
790790+ | [] -> []
791791+ | Ast.{ splittable = true; txt; _ } :: rest ->
792792+ (String.split_on_char ' ' txt |> List.map Ast.Fragment.make)
793793+ @ field_splitting ctx rest
794794+ | txt :: rest -> txt :: field_splitting ctx rest
743795744744- and handle_export_or_readonly kind ctx (assignments : Ast.word_cst list) =
796796+ and word_expansion' ctx cst : ctx Exit.t * Ast.fragments list =
797797+ let cst = tilde_expansion ctx cst in
798798+ parameter_expansion ctx cst
799799+800800+ and word_expansion ctx cst : ctx Exit.t * Ast.fragment list =
801801+ let rec aux ctx = function
802802+ | [] -> (ctx, []) (* one empty word *)
803803+ | c :: rest ->
804804+ let new_ctx, l = word_expansion' (Exit.value ctx) c in
805805+ let next_ctx, r = aux new_ctx rest in
806806+ let combined = l @ r in
807807+ (next_ctx, combined)
808808+ in
809809+ let ctx, cst = aux (Exit.zero ctx) cst in
810810+ match ctx with
811811+ | Exit.Nonzero _ -> (ctx, List.concat cst)
812812+ | Exit.Zero ctx ->
813813+ let fields = cst in
814814+ let fields = List.map (field_splitting ctx) fields in
815815+ let (ctx, cst) : ctx * Ast.fragments list =
816816+ begin
817817+ let glob = Ast.Fragment.join_list ~sep:"" (List.concat fields) in
818818+ let vs : Ast.fragments list =
819819+ let has_glob =
820820+ List.exists
821821+ (fun (f : Ast.fragment) -> f.globbable)
822822+ (List.concat fields)
823823+ in
824824+ let _new_ctx, s =
825825+ if (not ctx.options.no_path_expansion) && has_glob then
826826+ glob_expand ctx glob
827827+ else if ctx.options.no_path_expansion && has_glob then
828828+ (ctx, [ Ast.Fragment.make glob ])
829829+ else (ctx, List.concat fields)
830830+ in
831831+ [ s ]
832832+ in
833833+ (ctx, vs)
834834+ end
835835+ in
836836+ (Exit.zero ctx, List.concat cst)
837837+838838+ and handle_export_or_readonly kind ctx (assignments : string list) =
745839 let flags, assignments =
746840 List.fold_left
747747- (fun (fs, args) -> function
748748- | [ Ast.WordName v ] | [ Ast.WordLiteral v ] -> (
749749- match Astring.String.cut ~sep:"-" v with
750750- | Some ("", f) -> (f :: fs, args)
751751- | _ -> (fs, [ Ast.WordName v ] :: args))
752752- | v -> (fs, v :: args))
841841+ (fun (fs, args) v ->
842842+ match Astring.String.cut ~sep:"-" v with
843843+ | Some ("", f) -> (f :: fs, args)
844844+ | _ -> (fs, v :: args))
753845 ([], []) assignments
754846 in
755847 let update =
···757849 | `Export -> update ~export:true ~readonly:false
758850 | `Readonly -> update ~export:false ~readonly:true
759851 in
760760- let rec loop acc_ctx = function
761761- | [] -> Exit.zero acc_ctx
762762- | Ast.WordAssignmentWord (Name param, v) :: rest ->
763763- update acc_ctx ~param v >>= fun new_ctx -> loop new_ctx rest
764764- | Ast.WordName param :: rest -> (
852852+ let read_arg acc_ctx param =
853853+ (* TODO: quoting? *)
854854+ match Astring.String.cut ~sep:"=" param with
855855+ | Some (param, v) -> update acc_ctx ~param v
856856+ | None -> (
765857 match S.lookup acc_ctx.state ~param with
766766- | Some v ->
767767- update acc_ctx ~param v >>= fun new_ctx -> loop new_ctx rest
768768- | None -> loop acc_ctx rest)
769769- | c :: _ ->
770770- Exit.nonzero_msg acc_ctx "export weird arguments: %s\n"
771771- (Ast.word_component_to_string c)
858858+ | Some v -> update acc_ctx ~param v
859859+ | None -> Exit.zero acc_ctx)
772860 in
773861 match flags with
774862 | [] ->
775863 List.fold_left
776776- (fun ctx w -> match ctx with Exit.Zero ctx -> loop ctx w | _ -> ctx)
864864+ (fun ctx w ->
865865+ match ctx with Exit.Zero ctx -> read_arg ctx w | _ -> ctx)
777866 (Exit.zero ctx) assignments
778867 | fs ->
779868 if List.mem "p" fs then begin
···783872 end;
784873 Exit.zero ctx
785874786786- and expand_cst (ctx : ctx) cst : ctx Exit.t * Ast.word_cst =
787787- let cst = tilde_expansion ctx cst in
788788- let ctx, cst = parameter_expansion' ctx cst in
789789- match ctx with
790790- | Exit.Nonzero _ as ctx -> (ctx, cst)
791791- | Exit.Zero ctx ->
792792- (* TODO: Propagate errors *)
793793- let ctx, ast = arithmetic_expansion ctx cst in
794794- (Exit.zero ctx, ast)
795795-796875 and expand_redirects ((ctx, acc) : ctx * Ast.cmd_suffix_item list)
797876 (c : Ast.cmd_suffix_item list) =
798877 match c with
799878 | [] -> (ctx, List.rev acc)
800879 | Ast.Suffix_redirect (IoRedirect_IoFile (num, (op, file))) :: rest -> (
801801- let ctx, cst = expand_cst ctx file in
880880+ let ctx, cst = word_expansion ctx file in
802881 match ctx with
803803- | Exit.Nonzero _ -> assert false
882882+ | Exit.Nonzero _ -> Fmt.failwith "Redirect expansion"
804883 | Exit.Zero ctx ->
805805- let cst = handle_subshell ctx cst in
884884+ let cst =
885885+ List.map (fun Ast.{ txt; _ } -> Ast.WordLiteral txt) cst
886886+ in
806887 let v = Ast.Suffix_redirect (IoRedirect_IoFile (num, (op, cst))) in
807888 expand_redirects (ctx, v :: acc) rest)
808889 | (Ast.Suffix_redirect _ as v) :: rest ->
···857938 match v with
858939 | Ast.For_Name_DoGroup (_, (term, sep)) -> exec ctx (term, Some sep)
859940 | Ast.For_Name_In_WordList_DoGroup (Name name, wdlist, (term, sep)) ->
860860- let wdlist = Nlist.flatten @@ Nlist.map (word_glob_expand ctx) wdlist in
941941+ let wdlist = Nlist.map (word_expansion ctx) wdlist in
861942 Nlist.fold_left
862862- (fun _ word ->
863863- update ctx ~param:name word >>= fun ctx -> exec ctx (term, Some sep))
943943+ (fun _ (_, words) ->
944944+ List.fold_left
945945+ (fun _ word ->
946946+ update ctx ~param:name word.Ast.txt >>= fun ctx ->
947947+ exec ctx (term, Some sep))
948948+ (Exit.zero ctx) words)
864949 (Exit.zero ctx) wdlist
865950866951 and handle_if_clause ctx = function
···891976 and handle_case_clause ctx = function
892977 | Ast.Case _ -> Exit.zero ctx
893978 | Cases (word, case_list) -> (
894894- let ctx, word = expand_cst ctx word in
979979+ let ctx, word = word_expansion ctx word in
895980 match ctx with
896981 | Exit.Nonzero _ as ctx -> ctx
897982 | Exit.Zero ctx -> (
898898- let scrutinee = Ast.word_components_to_string word in
983983+ let scrutinee = Ast.Fragment.join_list ~sep:"" word in
899984 let res =
900985 Nlist.fold_left
901986 (fun acc pat ->
···908993 (fun inner_acc pattern ->
909994 match inner_acc with
910995 | Some _ as v -> v
911911- | None -> (
912912- let ctx, pattern = expand_cst ctx pattern in
913913- match ctx with
914914- | Exit.Nonzero _ as ctx -> Some ctx
915915- | Exit.Zero ctx ->
916916- let pattern =
917917- Ast.word_components_to_string pattern
918918- in
919919- if Glob.test ~pattern scrutinee then begin
920920- match sub with
921921- | Some sub ->
922922- Some (exec_subshell ctx sub)
923923- | None -> Some (Exit.zero ctx)
924924- end
925925- else inner_acc))
996996+ | None ->
997997+ let pattern = word_cst_to_string pattern in
998998+ if Glob.test ~pattern scrutinee then begin
999999+ match sub with
10001000+ | Some sub -> Some (exec_subshell ctx sub)
10011001+ | None -> Some (Exit.zero ctx)
10021002+ end
10031003+ else inner_acc)
9261004 None p))
9271005 None case_list
9281006 in
···9711049 let ctx = { ctx with argv = Array.of_list argv } in
9721050 Option.some @@ (handle_compound_command ctx commands >|= fun _ -> ctx)
9731051974974- and needs_subshelling = function
975975- | [] -> false
976976- | Ast.WordSubshell _ :: _ -> true
977977- | Ast.WordDoubleQuoted word :: rest ->
978978- needs_subshelling word || needs_subshelling rest
979979- | Ast.WordSingleQuoted word :: rest ->
980980- needs_subshelling word || needs_subshelling rest
981981- | _ -> false
982982-983983- and handle_subshell (ctx : ctx) wcs =
984984- let exec_subshell ~sw ctx s =
10521052+ and command_substitution (ctx : ctx) (cc : Ast.complete_commands) =
10531053+ let exec_subshell ctx s =
9851054 let buf = Buffer.create 16 in
9861055 let stdout = Eio.Flow.buffer_sink buf in
987987- let r, w = Eio_unix.pipe sw in
988988- Eio.Fiber.fork ~sw (fun () -> Eio.Flow.copy r stdout);
989989- let subshell_ctx = { ctx with stdout = w; subshell = true } in
990990- let sub_ctx, _ = run (Exit.zero subshell_ctx) s in
991991- Eio.Flow.close w;
10561056+ let sub_ctx =
10571057+ Eio.Switch.run @@ fun sw ->
10581058+ let r, w = Eio_unix.pipe sw in
10591059+ Eio.Fiber.fork ~sw (fun () -> Eio.Flow.copy r stdout);
10601060+ let subshell_ctx = { ctx with stdout = w; subshell = true } in
10611061+ let sub_ctx, _ = run (Exit.zero subshell_ctx) s in
10621062+ Eio.Flow.close w;
10631063+ sub_ctx
10641064+ in
9921065 ((sub_ctx >|= fun _ -> ctx), Buffer.contents buf)
9931066 in
994994- let rec run_subshells ~sw ran_subshell = function
995995- | [] -> []
996996- | Ast.WordSubshell s :: rest ->
997997- let _ctx, std = exec_subshell ~sw ctx s in
998998- ran_subshell := true;
999999- Ast.WordName (String.trim std) :: run_subshells ~sw ran_subshell rest
10001000- | Ast.WordDoubleQuoted word :: rest ->
10011001- let subshell_q = ref false in
10021002- let res = run_subshells ~sw subshell_q word in
10031003- if !subshell_q then res @ run_subshells ~sw subshell_q rest
10041004- else Ast.WordDoubleQuoted res :: run_subshells ~sw subshell_q rest
10051005- | Ast.WordSingleQuoted word :: rest ->
10061006- let subshell_q = ref false in
10071007- let res = run_subshells ~sw subshell_q word in
10081008- if !subshell_q then res @ run_subshells ~sw subshell_q rest
10091009- else Ast.WordSingleQuoted res :: run_subshells ~sw subshell_q rest
10101010- | v :: rest -> v :: run_subshells ~sw ran_subshell rest
10671067+ let run_subshells s =
10681068+ let _ctx, std = exec_subshell ctx s in
10691069+ String.trim std
10111070 in
10121012- Eio.Switch.run @@ fun sw -> run_subshells ~sw (ref false) wcs
10131013-10141014- and handle_word_cst_subshell (ctx : ctx) wcs : Ast.word_cst =
10151015- if needs_subshelling wcs then begin
10161016- let wcs = handle_subshell ctx wcs in
10171017- wcs
10181018- end
10191019- else wcs
10201020-10211021- and glob_expand ctx wc =
10221022- let wc = handle_word_cst_subshell ctx wc in
10231023- if Ast.has_glob wc && not ctx.options.no_path_expansion then
10241024- Ast.word_components_to_string wc |> fun pattern ->
10251025- Glob.glob_dir ~pattern (cwd_of_ctx ctx)
10261026- |> List.map (fun w -> [ Ast.WordName w ])
10271027- else [ wc ]
10711071+ run_subshells cc
1028107210291029- and word_glob_expand (ctx : ctx) wc : Ast.word_cst list =
10301030- if List.exists needs_glob_expansion wc then glob_expand ctx wc
10311031- else [ handle_word_cst_subshell ctx wc ]
10731073+ and glob_expand ctx pattern : ctx * Ast.fragment list =
10741074+ ( ctx,
10751075+ match Glob.glob_dir ~pattern (cwd_of_ctx ctx) with
10761076+ | [] -> [ Ast.Fragment.make pattern ]
10771077+ | exception _ -> [ Ast.Fragment.make pattern ]
10781078+ | xs -> List.map Ast.Fragment.make xs )
1032107910331080 and collect_assignments ?(update = true) ctx vs : ctx Exit.t =
10341081 List.fold_left
···10391086 match prefix with
10401087 | Ast.Prefix_assignment (Name param, v) -> (
10411088 (* Expand the values *)
10421042- let ctx, v = expand_cst ctx v in
10891089+ let ctx, v = word_expansion ctx v in
10431090 match ctx with
10441091 | Exit.Nonzero _ as ctx -> ctx
10451092 | Exit.Zero ctx -> (
10461046- let v = handle_subshell ctx v in
10471093 let state =
10481048- if update then S.update ctx.state ~param v
10941094+ if update then
10951095+ S.update ctx.state ~param
10961096+ (Ast.Fragment.join_list ~sep:"" v)
10491097 else Ok ctx.state
10501098 in
10511099 match state with
···10561104 ctx with
10571105 state;
10581106 local_state =
10591059- (param, Ast.word_components_to_string v)
11071107+ (param, Ast.Fragment.join_list ~sep:"" v)
10601108 :: ctx.local_state;
10611109 }))
10621110 | _ -> Exit.zero ctx))
10631111 (Exit.zero ctx) vs
1064111210651065- and args ctx swc : ctx Exit.t * Ast.word_cst list =
10661066- List.fold_left
10671067- (fun (ctx, acc) -> function
10681068- | Ast.Suffix_redirect _ -> (ctx, acc)
10691069- | Suffix_word wc -> (
10701070- match ctx with
10711071- | Exit.Nonzero _ as ctx -> (ctx, acc)
10721072- | Exit.Zero ctx -> (
10731073- let ctx, cst = expand_cst ctx wc in
10741074- match ctx with
10751075- | Exit.Nonzero _ as ctx -> (ctx, acc)
10761076- | Exit.Zero c as ctx -> (ctx, acc @ word_glob_expand c cst))))
10771077- (Exit.zero ctx, [])
10781078- swc
11131113+ and args ctx swc : ctx Exit.t * string list =
11141114+ let ctx, fs =
11151115+ List.fold_left
11161116+ (fun (ctx, acc) -> function
11171117+ | Ast.Suffix_redirect _ -> (ctx, acc)
11181118+ | Suffix_word wc -> (
11191119+ match ctx with
11201120+ | Exit.Nonzero _ as ctx -> (ctx, acc)
11211121+ | Exit.Zero ctx -> (
11221122+ let ctx, cst = word_expansion ctx wc in
11231123+ let cst = Ast.Fragment.handle_joins cst in
11241124+ (* Fmt.pr "Expanding: %a\n%!" Fmt.(list Ast.Fragment.pp) cst; *)
11251125+ match ctx with
11261126+ | Exit.Nonzero _ as ctx -> (ctx, acc)
11271127+ | Exit.Zero _ as ctx -> (ctx, acc @ cst))))
11281128+ (Exit.zero ctx, [])
11291129+ swc
11301130+ in
11311131+ (* Fmt.pr "Arguments: %a\n%!" Fmt.(list Ast.Fragment.pp) fs; *)
11321132+ (ctx, List.map Ast.Fragment.to_string fs)
1079113310801134 and handle_built_in ~rdrs ~(stdout : Eio_unix.sink_ty Eio.Flow.sink)
10811135 (ctx : ctx) v =
+6
src/lib/eval.mli
···2121 ?exit_handler:(unit -> unit) ->
2222 ?options:Built_ins.Options.t ->
2323 ?hash:Hash.t ->
2424+ ?in_double_quotes:bool ->
2425 fs:Eio.Fs.dir_ty Eio.Path.t ->
2526 stdin:Eio_unix.source_ty r ->
2627 stdout:Eio_unix.sink_ty r ->
···43444445 val run : ctx Exit.t -> Ast.t -> ctx Exit.t * Ast.t list
4546 (** [run ctx ast] evaluates [ast] using the initial [ctx]. *)
4747+4848+ (** {2 Private} *)
4949+5050+ val word_expansion : ctx -> Ast.word_cst -> ctx Exit.t * Ast.fragment list
5151+ (* Mostly for testing purposes, this exposes the logic for expanding words. *)
4652end
+1-2
src/lib/interactive.ml
···9494 in
9595 let rec loop (ctx : Eval.ctx Exit.t) =
9696 Option.iter (Fmt.epr "%s%!")
9797- (S.lookup (Exit.value ctx |> Eval.state) ~param:"PS1"
9898- |> Option.map Ast.word_components_to_string);
9797+ (S.lookup (Exit.value ctx |> Eval.state) ~param:"PS1");
9998 let p = prompt ctx in
10099 Fmt.pr "%s\r%!" p;
101100 let hint command =
···126126 (* Empty CST. Useful to represent the absence of relevant CSTs. *)
127127 | WordEmpty
128128129129+and fragment = {
130130+ txt : string;
131131+ splittable : bool;
132132+ globbable : bool;
133133+ join : [ `No | `With_previous | `With_next ]; (* Used for "args: [$@]" *)
134134+}
135135+(** Post expansion representation of strings ready for possible field splitting
136136+ and globbing. *)
137137+138138+and fragments = fragment list
139139+129140and bracket_expression =
130141 | BracketExpression_LBRACKET_MatchingList_RBRACKET of matching_list
131142 | BracketExpression_LBRACKET_NonMatchingList_RBRACKET of nonmatching_list
+4-4
src/lib/types.ml
···2020 val expand : t -> [ `Tilde ] -> string
2121 (** Expansions *)
22222323- val lookup : t -> param:string -> Ast.word_cst option
2323+ val lookup : t -> param:string -> string option
2424 (** Parameter lookup. [None] means [unset]. *)
25252626 val update :
···2828 ?readonly:bool ->
2929 t ->
3030 param:string ->
3131- Ast.word_cst ->
3131+ string ->
3232 (t, string) result
3333 (** Update the state with a new parameter mapping and whether or not it should
3434 exported to the environment (default false). *)
···3737 (** [remove ~param t] removes [param] from [t] if it exists. [bool] is [true]
3838 if a removal took place. *)
39394040- val exports : t -> (string * Ast.word_cst) list
4040+ val exports : t -> (string * string) list
4141 (** All of the variables that must be exported to the environment *)
42424343- val readonly : t -> (string * Ast.word_cst) list
4343+ val readonly : t -> (string * string) list
4444 (** All of the variables that must be exported to the environment *)
45454646 val pp_readonly : t Fmt.t
+67
src/lib/util.ml
···11+(* Some random utils for debugging *)
22+open Eio.Std
33+44+let traced_sink tag
55+ (Eio.Resource.T (t, handler) : Eio_unix.sink_ty Eio.Flow.sink) :
66+ Eio_unix.sink_ty r =
77+ let module Sink = (val Eio.Resource.get handler Eio.Flow.Pi.Sink) in
88+ let close = Eio.Resource.get handler Eio.Resource.Close in
99+ let buf = Cstruct.create 4096 in
1010+ let copy () ~src =
1111+ try
1212+ while true do
1313+ match Eio.Flow.single_read src buf with
1414+ | i ->
1515+ Eio.traceln ">>>>> %s Single read: %s" tag (Cstruct.to_string buf);
1616+ let bufs = [ Cstruct.sub buf 0 i ] in
1717+ Sink.copy ~src:(Eio.Flow.cstruct_source bufs) t
1818+ done
1919+ with End_of_file -> Eio.traceln ">>>>>> EOF"
2020+ in
2121+ let single_write () x =
2222+ Eio.traceln ">>>>> single write: %s" (Cstruct.concat x |> Cstruct.to_string);
2323+ Sink.single_write t x
2424+ in
2525+ let module T = struct
2626+ type t = unit
2727+2828+ let single_write = single_write
2929+ let copy = copy
3030+ end in
3131+ let t =
3232+ Eio.Resource.handler
3333+ [
3434+ H (Eio.Flow.Pi.Sink, (module T));
3535+ H (Eio.Resource.Close, fun () -> close t);
3636+ ]
3737+ in
3838+ Eio.Resource.T ((), t)
3939+4040+let traced_sink_flow tag
4141+ (Eio.Resource.T (t, handler) : Eio.Flow.sink_ty Eio.Flow.sink) :
4242+ Eio.Flow.sink_ty r =
4343+ let module Sink = (val Eio.Resource.get handler Eio.Flow.Pi.Sink) in
4444+ let buf = Cstruct.create 4096 in
4545+ let copy () ~src =
4646+ try
4747+ while true do
4848+ match Eio.Flow.single_read src buf with
4949+ | i ->
5050+ Eio.traceln ">>>>> %s Single read: %s" tag (Cstruct.to_string buf);
5151+ let bufs = [ Cstruct.sub buf 0 i ] in
5252+ Sink.copy ~src:(Eio.Flow.cstruct_source bufs) t
5353+ done
5454+ with End_of_file -> Eio.traceln ">>>>>> EOF"
5555+ in
5656+ let single_write () x =
5757+ Eio.traceln ">>>>> single write: %s" (Cstruct.concat x |> Cstruct.to_string);
5858+ Sink.single_write t x
5959+ in
6060+ let module T = struct
6161+ type t = unit
6262+6363+ let single_write = single_write
6464+ let copy = copy
6565+ end in
6666+ let t = Eio.Resource.handler [ H (Eio.Flow.Pi.Sink, (module T)) ] in
6767+ Eio.Resource.T ((), t)
···11+Test cases from the mind of Ryan Gibb; only those that were designed to torture
22+shell users and shell authors alike.
33+44+ $ cat > test.sh << EOF
55+ >
66+ > for arg in \$@; do
77+ > echo "No quote: \$arg"
88+ > done
99+ >
1010+ > for arg in "\$@"; do
1111+ > echo "In quotes quote: \$arg"
1212+ > done
1313+ > EOF
1414+1515+ $ sh test.sh a b "c d"
1616+ No quote: a
1717+ No quote: b
1818+ No quote: c
1919+ No quote: d
2020+ In quotes quote: a
2121+ In quotes quote: b
2222+ In quotes quote: c d
2323+ $ msh test.sh a b "c d"
2424+ No quote: a
2525+ No quote: b
2626+ No quote: c
2727+ No quote: d
2828+ In quotes quote: a
2929+ In quotes quote: b
3030+ In quotes quote: c d
3131+
+20
test/simple.t
···7878 $ msh test.sh
7979 /bin:/usr/bin
80808181+Some variable expansions:
8282+8383+ $ cat > test.sh << EOF
8484+ > echo "Got \$# arguments: [\$@]"
8585+ > for arg in "\$@"; do
8686+ > echo "[\$arg]"
8787+ > done
8888+ > EOF
8989+9090+ $ sh test.sh hello world "from the shell"
9191+ Got 3 arguments: [hello world from the shell]
9292+ [hello]
9393+ [world]
9494+ [from the shell]
9595+ $ msh test.sh hello world "from the shell"
9696+ Got 3 arguments: [hello world from the shell]
9797+ [hello]
9898+ [world]
9999+ [from the shell]
100100+811012. Pipelines with And|Or
82102831032.1 Simple Or
+124
test/wordexp.ml
···11+(* Thorough testing of word expansion *)
22+open Merry
33+module C = Merry.Eval.Make (Merry_posix.State) (Merry_posix.Exec)
44+55+let expand ctx cst = C.word_expansion ctx cst |> snd
66+let fragment = Alcotest.of_pp Merry.Ast.Fragment.pp
77+let fragments = Alcotest.list fragment
88+let frags = List.map Ast.Fragment.make
99+1010+let with_default_ctx ?(args = []) ?(params = []) env fn =
1111+ let executor = Merry_posix.Exec.{ mgr = env#process_mgr } in
1212+ let interactive = false in
1313+ let pos_zero = "msh" in
1414+ Eio.Switch.run @@ fun async_switch ->
1515+ let signal_handler f = Eio_posix.run @@ fun _ -> f () in
1616+ let state =
1717+ Merry_posix.State.make
1818+ ~home:(Sys.getenv "HOME" ^ "/")
1919+ (Fpath.v (Merry.Eunix.cwd ()))
2020+ in
2121+ let state =
2222+ List.fold_left
2323+ (fun s (k, v) -> Merry_posix.State.update s ~param:k v |> Result.get_ok)
2424+ state params
2525+ in
2626+ let ctx =
2727+ C.make_ctx ~interactive state executor ~fs:env#fs ~stdin:env#stdin
2828+ ~stdout:env#stdout ~async_switch ~argv:(Array.of_list args)
2929+ ~program:pos_zero ~signal_handler
3030+ in
3131+ fn ctx
3232+3333+module W = struct
3434+ let name s = Ast.WordName s
3535+ let lit s = Ast.WordLiteral s
3636+ let dquote c = Ast.WordDoubleQuoted c
3737+ let glob_all = Ast.WordGlobAll
3838+3939+ (* let squote c = Ast.WordSingleQuoted c *)
4040+ (* let arith a = Ast.WordArithmeticExpression a *)
4141+ let var v = Ast.WordVariable (Ast.VariableAtom (v, Ast.NoAttribute))
4242+end
4343+4444+let test_no_expansions env () =
4545+ let args = [ "echo"; "hello" ] in
4646+ let cargs = W.[ name "echo"; lit "hello" ] in
4747+ with_default_ctx ~args env @@ fun ctx ->
4848+ let expected = frags args in
4949+ let actual = expand ctx cargs in
5050+ Alcotest.check fragments "same fragments" expected actual
5151+5252+let test_dquote env () =
5353+ let args = [ "echo"; "\"hello there\"" ] in
5454+ let cargs = W.[ name "echo"; dquote [ lit "hello there" ] ] in
5555+ with_default_ctx ~args env @@ fun ctx ->
5656+ let expected =
5757+ Ast.
5858+ [ Fragment.make "echo"; Fragment.make ~join:`With_previous "hello there" ]
5959+ in
6060+ let actual = expand ctx cargs in
6161+ Alcotest.check fragments "same fragments" expected actual
6262+6363+let test_dquote_expansion env () =
6464+ let args = [ "echo"; "\"hello $FOO...\"" ] in
6565+ let cargs =
6666+ W.[ name "echo"; dquote [ lit "hello "; var "FOO"; lit "..." ] ]
6767+ in
6868+ with_default_ctx ~args ~params:[ ("FOO", "there") ] env @@ fun ctx ->
6969+ let expected =
7070+ Ast.Fragment.[ make "echo"; make ~join:`With_previous "hello there..." ]
7171+ in
7272+ let actual = expand ctx cargs in
7373+ Alcotest.check fragments "same fragments" expected actual
7474+7575+let test_single_expansion env () =
7676+ let args = [ "echo"; "$FOO" ] in
7777+ let cargs = W.[ name "echo"; var "FOO" ] in
7878+ with_default_ctx ~args ~params:[ ("FOO", "bar") ] env @@ fun ctx ->
7979+ let expected = [ Ast.Fragment.make "echo"; Ast.Fragment.make "bar" ] in
8080+ let actual = expand ctx cargs in
8181+ Alcotest.check fragments "same fragments" expected actual
8282+8383+let test_argv_expansion env () =
8484+ let cargs = W.[ name "echo"; var "@" ] in
8585+ with_default_ctx ~args:[ "echo"; "a"; "b"; "c" ] env @@ fun ctx ->
8686+ let expected = frags [ "echo"; "a"; "b"; "c" ] in
8787+ let actual = expand ctx cargs in
8888+ Alcotest.check fragments "same fragments" expected actual
8989+9090+let test_argv_in_quotes_expansion env () =
9191+ let cargs = W.[ name "echo"; dquote [ lit "got ["; var "@"; lit "]" ] ] in
9292+ with_default_ctx ~args:[ "echo"; "a"; "b"; "c" ] env @@ fun ctx ->
9393+ let expected =
9494+ Ast.Fragment.
9595+ [
9696+ make "echo";
9797+ make ~join:`With_previous "got [a";
9898+ make "b";
9999+ make ~join:`With_next "c]";
100100+ ]
101101+ in
102102+ let actual = expand ctx cargs in
103103+ Alcotest.check fragments "same fragments" expected actual
104104+105105+let test_glob env () =
106106+ let cargs = W.[ glob_all; lit ".ml" ] in
107107+ with_default_ctx ~args:[ "*.ml" ] env @@ fun ctx ->
108108+ let expected = Ast.Fragment.[ make "test_merry.ml"; make "wordexp.ml" ] in
109109+ let actual = expand ctx cargs in
110110+ Alcotest.check fragments "same fragments" expected actual
111111+112112+let simple env =
113113+ [
114114+ ("no expansions", `Quick, test_no_expansions env);
115115+ ("double quote", `Quick, test_dquote env);
116116+ ("double quote expansion", `Quick, test_dquote_expansion env);
117117+ ("single expansion", `Quick, test_single_expansion env);
118118+ ("argv expansion", `Quick, test_argv_expansion env);
119119+ ("argv expansion dquote", `Quick, test_argv_in_quotes_expansion env);
120120+ ("glob all", `Quick, test_glob env);
121121+ ]
122122+123123+let () =
124124+ Eio_posix.run @@ fun env -> Alcotest.run "wordexp" [ ("simple", simple env) ]