From ee497fddbfb2b1090430678b06142f1247509991 Mon Sep 17 00:00:00 2001 From: Patrick Ferris Date: Fri, 16 Jan 2026 13:44:45 +0000 Subject: [PATCH] Non-posix &> redirection It seems many a script uses the shorthand &> to redirect stdout and stderr at the same time (usually to /dev/null). --- src/lib/ast.ml | 14 +++- src/lib/eunix.ml | 23 +++++++ src/lib/eval.ml | 84 +++++++++++++---------- src/lib/sast.ml | 1 + test/built_ins.t | 5 ++ test/simple.t | 7 +- vendor/morbig.0.11.0/src/CST.mli | 1 + vendor/morbig.0.11.0/src/parser.mly | 6 ++ vendor/morbig.0.11.0/src/prelexer.mll | 4 +- vendor/morbig.0.11.0/src/prelexerState.ml | 2 +- vendor/morbig.0.11.0/src/pretoken.ml | 4 +- vendor/morbig.0.11.0/src/token.ml | 1 + 12 files changed, 110 insertions(+), 42 deletions(-) diff --git a/src/lib/ast.ml b/src/lib/ast.ml index ba0d26e..366b555 100644 --- a/src/lib/ast.ml +++ b/src/lib/ast.ml @@ -476,6 +476,9 @@ and io_file : CST.io_file -> io_file = | IoFile_LessGreat_FileName a -> let a = filename a.value in (Io_op_lessgreat, a) + | IoFile_AndGreat_FileName a -> + let a = filename a.value in + (Io_op_andgreat, a) | IoFile_Clobber_FileName a -> let a = filename a.value in (Io_op_clobber, a) @@ -780,12 +783,19 @@ let has_async ast = | _, Some Ampersand -> true | _ -> super#complete_command v ctx - method! separator_list f l ctx = + method! clist l ctx = let v = Nlist.to_list l |> List.exists (function _, Ampersand -> true | _ -> false) in - if v then v else super#separator_list f l ctx + if v then v else super#clist l ctx + + method! term t ctx = + let v = + Nlist.to_list t + |> List.exists (function _, Ampersand -> true | _ -> false) + in + if v then v else super#term t ctx end in o#complete_command ast false diff --git a/src/lib/eunix.ml b/src/lib/eunix.ml index d15f673..80390a5 100644 --- a/src/lib/eunix.ml +++ b/src/lib/eunix.ml @@ -25,3 +25,26 @@ let make_process_group () = match setpgrp 0 0 with 0 -> () | n -> exit n let background () = let _pgrid = Unix.getpid () in () + +let fd_of_int (fd : int) : Unix.file_descr = Obj.magic fd + +let with_redirections (rdrs : Types.redirect list) fn = + let saved_stdin = Unix.dup Unix.stdin in + let saved_stdout = Unix.dup Unix.stdout in + let saved_stderr = Unix.dup Unix.stderr in + List.iter + (function + | Types.Redirect (i, fd, _) -> + Eio_unix.Fd.use_exn "with_redirections" fd @@ fun fd -> + Unix.dup2 fd (fd_of_int i) + | Types.Close fd -> Eio_unix.Fd.close fd) + rdrs; + Fun.protect + ~finally:(fun () -> + Unix.dup2 saved_stdin (fd_of_int 0); + Unix.dup2 saved_stdout (fd_of_int 1); + Unix.dup2 saved_stderr (fd_of_int 2); + Unix.close saved_stdin; + Unix.close saved_stdout; + Unix.close saved_stderr) + fn diff --git a/src/lib/eval.ml b/src/lib/eval.ml index f192690..be5efa9 100644 --- a/src/lib/eval.ml +++ b/src/lib/eval.ml @@ -108,20 +108,21 @@ module Make (S : Types.State) (E : Types.Exec) = struct Eio.Path.open_in ~sw (ctx.fs / Ast.word_components_to_string file) in let fd = Eio_unix.Resource.fd_opt r |> Option.get in - Some (Types.Redirect (n, fd, `Blocking)) + [ Types.Redirect (n, fd, `Blocking) ] | Io_op_lessand -> ( match file with | [ WordLiteral "-" ] -> - if n = 0 then Some (Types.Close Eio_unix.Fd.stdin) + if n = 0 then [ Types.Close Eio_unix.Fd.stdin ] else let fd = fd_of_int ~sw n in - Some (Types.Close fd) + [ Types.Close fd ] | [ WordLiteral m ] when Option.is_some (int_of_string_opt m) -> let m = int_of_string m in - Some - (Types.Redirect - (n, fd_of_int ~close_unix:false ~sw m, `Blocking)) - | _ -> None) + [ + Types.Redirect + (n, fd_of_int ~close_unix:false ~sw m, `Blocking); + ] + | _ -> []) | (Io_op_great | Io_op_dgreat) as v -> (* Simple file creation *) let append = v = Io_op_dgreat in @@ -130,20 +131,33 @@ module Make (S : Types.State) (E : Types.Exec) = struct (ctx.fs / Ast.word_components_to_string file) in let fd = Eio_unix.Resource.fd_opt w |> Option.get in - Some (Types.Redirect (n, fd, `Blocking)) + [ Types.Redirect (n, fd, `Blocking) ] | Io_op_greatand -> ( match file with | [ WordLiteral "-" ] -> - if n = 0 then Some (Types.Close Eio_unix.Fd.stdout) + if n = 0 then [ Types.Close Eio_unix.Fd.stdout ] else let fd = fd_of_int ~sw n in - Some (Types.Close fd) + [ Types.Close fd ] | [ WordLiteral m ] when Option.is_some (int_of_string_opt m) -> let m = int_of_string m in - Some - (Types.Redirect - (n, fd_of_int ~close_unix:false ~sw m, `Blocking)) - | _ -> None) + [ + Types.Redirect + (n, fd_of_int ~close_unix:false ~sw m, `Blocking); + ] + | _ -> []) + | Io_op_andgreat -> + (* Yesh, not very POSIX *) + (* Simple file creation *) + let w = + Eio.Path.open_out ~sw ~create:(`If_missing 0o644) + (ctx.fs / Ast.word_components_to_string file) + in + let fd = Eio_unix.Resource.fd_opt w |> Option.get in + [ + Types.Redirect (1, fd, `Blocking); + Types.Redirect (2, fd, `Blocking); + ] | Io_op_clobber -> Fmt.failwith ">| not supported yet." | Io_op_lessgreat -> Fmt.failwith "<> not support yet.") | Ast.IoRedirect_IoHere _ -> @@ -267,9 +281,18 @@ module Make (S : Types.State) (E : Types.Exec) = struct | `Global p -> (true, p) | `Local p -> (false, p) in + let rdrs = + List.fold_left + (fun acc -> function + | Ast.Suffix_word _ -> acc + | Ast.Suffix_redirect rdr -> + handle_one_redirection ~sw:pipeline_switch ctx rdr @ acc) + [] suffix + |> List.rev + in match Built_ins.of_args (executable :: args_as_strings) with | Some (Ok bi) -> - let ctx = handle_built_in ~stdout:some_write ctx bi in + let ctx = handle_built_in ~rdrs ~stdout:some_write ctx bi in close_stdout ~is_global some_write; let built_in = ctx >|= fun _ -> () in let job = handle_job ~pgid job (`Built_in built_in) in @@ -301,29 +324,17 @@ module Make (S : Types.State) (E : Types.Exec) = struct in loop saved_ctx job (pgid, some_read) rest | None -> ( - let redirect = - List.fold_left - (fun acc -> function - | Ast.Suffix_word _ -> acc - | Ast.Suffix_redirect rdr -> - handle_one_redirection ~sw:pipeline_switch ctx - rdr - :: acc) - [] suffix - |> List.rev |> List.filter_map Fun.id - in match stdout_of_previous with | None -> let ctx, job = - exec_process ctx job ~fds:redirect - ~stdout:some_write ~pgid executable - args_as_strings + exec_process ctx job ~fds:rdrs ~stdout:some_write + ~pgid executable args_as_strings in close_stdout ~is_global some_write; loop ctx job (pgid, some_read) rest | Some stdout -> let ctx, job = - exec_process ctx job ~fds:redirect ~stdin:stdout + exec_process ctx job ~fds:rdrs ~stdin:stdout ~stdout:some_write ~pgid executable args_as_strings in @@ -724,8 +735,8 @@ module Make (S : Types.State) (E : Types.Exec) = struct (ctx, acc @ word_glob_expand ctx cst)) (ctx, []) swc - and handle_built_in ~(stdout : Eio_unix.sink_ty Eio.Flow.sink) (ctx : ctx) = - function + and handle_built_in ~rdrs ~(stdout : Eio_unix.sink_ty Eio.Flow.sink) + (ctx : ctx) = function | Built_ins.Cd { path } -> let cwd = S.cwd ctx.state in let+ state = @@ -740,9 +751,12 @@ module Make (S : Types.State) (E : Types.Exec) = struct in { ctx with state } | Pwd -> - Eio.Flow.copy_string - (Fmt.str "%a\n%!" Fpath.pp (S.cwd ctx.state)) - stdout; + let () = + Eunix.with_redirections rdrs @@ fun () -> + Eio.Flow.copy_string + (Fmt.str "%a\n%!" Fpath.pp (S.cwd ctx.state)) + stdout + in Exit.zero ctx | Exit n -> let should_exit = diff --git a/src/lib/sast.ml b/src/lib/sast.ml index 441fb1c..d53f0b2 100644 --- a/src/lib/sast.ml +++ b/src/lib/sast.ml @@ -99,6 +99,7 @@ and io_op = | Io_op_greatand | Io_op_dgreat | Io_op_lessgreat + | Io_op_andgreat | Io_op_clobber and io_file = io_op * filename diff --git a/test/built_ins.t b/test/built_ins.t index 29079e3..b2b3dda 100644 --- a/test/built_ins.t +++ b/test/built_ins.t @@ -143,3 +143,8 @@ Testing some shell built-ins test $ msh -c "FOO=\$(pwd | rev | rev); echo \$(basename \$FOO)" test + + $ sh -c "pwd > out.txt; cat out.txt | xargs -- basename" + test + $ msh -c "pwd > out.txt; cat out.txt | xargs -- basename" + test diff --git a/test/simple.t b/test/simple.t index 363e3d5..3775c53 100644 --- a/test/simple.t +++ b/test/simple.t @@ -2,7 +2,7 @@ A series of simple shell scripting tests for msh. 1. Variables and Parameters -1.1 Variable Subtitution +1.1 Variable Substitution $ cat >test.sh < P="hello world" @@ -171,6 +171,11 @@ Redirection of fds $ cat out.txt hello +NON-POSIX redirection of stderr and stdout. + + $ msh -c "ls -j &> /dev/null" + [2] + 2.7 Sequences A simple, semicolon sequence. diff --git a/vendor/morbig.0.11.0/src/CST.mli b/vendor/morbig.0.11.0/src/CST.mli index 532bdcc..0fc7727 100644 --- a/vendor/morbig.0.11.0/src/CST.mli +++ b/vendor/morbig.0.11.0/src/CST.mli @@ -272,6 +272,7 @@ and io_file = | IoFile_GreatAnd_FileName of filename' | IoFile_DGreat_FileName of filename' | IoFile_LessGreat_FileName of filename' + | IoFile_AndGreat_FileName of filename' | IoFile_Clobber_FileName of filename' and filename = diff --git a/vendor/morbig.0.11.0/src/parser.mly b/vendor/morbig.0.11.0/src/parser.mly index 6eba91a..fa706f6 100644 --- a/vendor/morbig.0.11.0/src/parser.mly +++ b/vendor/morbig.0.11.0/src/parser.mly @@ -63,6 +63,9 @@ %token DGREAT LESSAND GREATAND LESSGREAT (* '>>' '<&' '>&' '<>' *) +%token ANDGREAT +(* '&>' ... I know, not very POSIX *) + %token CLOBBER (* '>|' *) @@ -452,6 +455,9 @@ io_file : LESS f=located(filename) { | LESSGREAT f=located(filename) { IoFile_LessGreat_FileName f } +| ANDGREAT f=located(filename) { + IoFile_AndGreat_FileName f +} | CLOBBER f=located(filename) { IoFile_Clobber_FileName f } diff --git a/vendor/morbig.0.11.0/src/prelexer.mll b/vendor/morbig.0.11.0/src/prelexer.mll index d6589b9..db8949e 100644 --- a/vendor/morbig.0.11.0/src/prelexer.mll +++ b/vendor/morbig.0.11.0/src/prelexer.mll @@ -106,8 +106,8 @@ let newline = '\010' let blank = [' ' '\009' '\012'] -let operator = "&&" | "||" | ";;" | - "<<" | ">>" | "<&" | ">&" | "<>" | "<<-" | +let operator = "&&" | "||" | ";;" | "&>" | + "<<" | ">>" | "<&" | ">&" | "<>" | "<<-" | ">|" | "|" | "(" | ")" | "<" | ">" | ";" | "&" diff --git a/vendor/morbig.0.11.0/src/prelexerState.ml b/vendor/morbig.0.11.0/src/prelexerState.ml index 96bc890..c5ebcf9 100644 --- a/vendor/morbig.0.11.0/src/prelexerState.ml +++ b/vendor/morbig.0.11.0/src/prelexerState.ml @@ -365,7 +365,7 @@ let return ?(with_newline=false) lexbuf (current : prelexer_state) tokens = in let followed_by_redirection = Parser.(function | Pretoken.Operator (LESSAND | GREATAND | DGREAT | DLESS _ - | CLOBBER | LESS | GREAT | LESSGREAT) :: _ -> + | CLOBBER | LESS | GREAT | LESSGREAT | ANDGREAT) :: _ -> true | _ -> false diff --git a/vendor/morbig.0.11.0/src/pretoken.ml b/vendor/morbig.0.11.0/src/pretoken.ml index 94ba83c..6db9e9e 100644 --- a/vendor/morbig.0.11.0/src/pretoken.ml +++ b/vendor/morbig.0.11.0/src/pretoken.ml @@ -28,11 +28,12 @@ let string_of_pretoken = function let operators = Hashtbl.( let t = create 17 in List.iter (fun (sym, tok) -> add t sym tok) [ - "&&", AND_IF; "||", OR_IF; ";;", DSEMI; "<&", LESSAND; ">&", GREATAND; + "&>", ANDGREAT; + "&&", AND_IF; "<>", LESSGREAT; ">>", DGREAT; ">|", CLOBBER; @@ -64,6 +65,7 @@ let preword_of_operator = function | GREATAND -> ">&" | LESSGREAT -> "<>" | DGREAT -> ">>" + | ANDGREAT -> "&>" | CLOBBER -> ">|" | Pipe -> "|" | Lparen -> "(" diff --git a/vendor/morbig.0.11.0/src/token.ml b/vendor/morbig.0.11.0/src/token.ml index 74fed54..3fd7471 100644 --- a/vendor/morbig.0.11.0/src/token.ml +++ b/vendor/morbig.0.11.0/src/token.ml @@ -35,6 +35,7 @@ let string_of_token = function | LESSGREAT -> "LESSGREAT" | LESSAND -> "LESSAND" | DGREAT -> "DGREAT" + | ANDGREAT -> "ANDGREAT" | GREATAND -> "GREATAND" | WORD w -> Printf.sprintf "WORD(%s)" (unWord w) | ASSIGNMENT_WORD w -> -- 2.51.2