Shells in OCaml
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Subshells and variables

Unfortunately this work got complicated quickly and resulted in more
fixes and features than I wished. Primarily, we now have subshell
support. This also meant rejigging how we expand and collect variable
assignments. There is a new, rather janky, concept of local_state which
is something we clear immediately after executing a command. This might
need some more work in the future.

+284 -115
+2
src/bin/main.ml
··· 13 13 Merry_posix.State.make 14 14 ~home:(Sys.getenv "HOME" ^ "/") 15 15 (Fpath.v (Merry.Eunix.cwd ())); 16 + local_state = []; 16 17 executor; 17 18 fs = env#fs; 18 19 options = Merry.Eval.Options.default; 20 + stdout = None; 19 21 } 20 22 in 21 23 match (file, command) with
+8 -11
src/lib/ast.ml
··· 516 516 and word_component : CST.word_component -> word_component = 517 517 fun x -> 518 518 match x with 519 - | WordSubshell (a, b) -> 520 - let a = subshell_kind a in 519 + | WordSubshell (_a, b) -> 521 520 let b = program b.value in 522 - WordSubshell (a, b) 521 + WordSubshell b 523 522 | WordName a -> WordName a 524 523 | WordAssignmentWord a -> 525 524 let a = assignment_word a in ··· 701 700 let a = word a in 702 701 RemoveLargestPrefixPattern a 703 702 704 - and subshell_kind : CST.subshell_kind -> subshell_kind = function 705 - | SubShellKindBackQuote -> SubShellKindBackQuote 706 - | SubShellKindParentheses -> SubShellKindParentheses 707 - 708 703 and name : CST.name -> name = fun x -> match x with Name a -> Name a 709 704 710 705 and assignment_word : CST.assignment_word -> assignment_word = ··· 733 728 let rec word_component_to_string : word_component -> string = function 734 729 | WordName s -> s 735 730 | WordLiteral s -> s 736 - | WordDoubleQuoted s -> 737 - String.concat " " (List.map word_component_to_string s) 738 - | WordSingleQuoted s -> 739 - String.concat " " (List.map word_component_to_string s) 731 + | WordDoubleQuoted s -> String.concat "" (List.map word_component_to_string s) 732 + | WordSingleQuoted s -> String.concat "" (List.map word_component_to_string s) 740 733 | WordGlobAll -> "*" 741 734 | WordGlobAny -> "?" 735 + | WordSubshell _ -> 736 + Fmt.failwith 737 + "This is an error in Merry, subshells should already have been \ 738 + expanded by now!" 742 739 | v -> 743 740 Fmt.failwith "Conversion of %a" Yojson.Safe.pp 744 741 (word_component_to_yojson v)
+181 -96
src/lib/eval.ml
··· 37 37 type ctx = { 38 38 interactive : bool; 39 39 state : S.t; 40 + local_state : (string * string) list; 40 41 executor : E.t; 41 42 fs : Eio.Fs.dir_ty Eio.Path.t; 42 43 options : Options.t; 44 + stdout : Eio_unix.sink_ty Eio.Flow.sink option; 43 45 } 46 + 47 + let clear_local_state ctx = { ctx with local_state = [] } 44 48 45 49 class default_ctx_fold = 46 50 object (_) ··· 78 82 in 79 83 o 80 84 81 - let tilde_expansion ctx ast = 82 - ( ctx, 83 - map_word_components 84 - (function 85 - | Ast.WordTildePrefix _ -> Ast.WordName (S.expand ctx.state `Tilde) 86 - | s -> s) 87 - ast ) 88 - 89 - let parameter_expansion' ?skip_for_clauses ctx = 90 - ( ctx, 91 - map_words ?skip_for_clauses 92 - (List.concat_map (function 93 - | Ast.WordVariable v -> ( 94 - match v with 95 - | Ast.VariableAtom (s, NoAttribute) -> ( 96 - match S.lookup ctx.state ~param:s with 97 - | None -> [ Ast.WordName "" ] 98 - | Some cst -> cst) 99 - | _ -> Fmt.failwith "No support for variable attributes yet!") 100 - | s -> [ s ])) ) 101 - 102 - let parameter_expansion ctx ast = 103 - let ctx, o = parameter_expansion' ctx in 104 - (ctx, o#complete_command ast) 105 - 106 - let assignments ctx ast = 107 - let o = 108 - object 109 - inherit default_ctx_fold 85 + let rec tilde_expansion ctx = function 86 + | [] -> [] 87 + | Ast.WordTildePrefix _ :: rest -> 88 + Ast.WordName (S.expand ctx.state `Tilde) :: tilde_expansion ctx rest 89 + | v :: rest -> v :: tilde_expansion ctx rest 110 90 111 - method! simple_command ast ctx = 112 - match ast with 113 - | Ast.Prefixed (cmd_prefix, _, _) -> 114 - List.fold_left 115 - (fun ctx -> function 116 - | Ast.Prefix_assignment (Name param, v) -> 117 - let state = S.update ctx.state ~param v in 118 - { ctx with state } 119 - | _ -> ctx) 120 - ctx cmd_prefix 121 - | _ -> ctx 122 - end 91 + let parameter_expansion' ctx = 92 + let rec expand = function 93 + | [] -> [] 94 + | Ast.WordVariable v :: rest -> ( 95 + match v with 96 + | Ast.VariableAtom (s, NoAttribute) -> ( 97 + match S.lookup ctx.state ~param:s with 98 + | None -> Ast.WordName "" :: expand rest 99 + | Some cst -> cst @ expand rest) 100 + | _ -> Fmt.failwith "No support for variable attributes yet!") 101 + | Ast.WordDoubleQuoted cst :: rest -> 102 + Ast.WordDoubleQuoted (expand cst) :: expand rest 103 + | Ast.WordSingleQuoted cst :: rest -> 104 + Ast.WordSingleQuoted (expand cst) :: expand rest 105 + | v :: rest -> v :: expand rest 123 106 in 124 - (o#complete_command ast ctx, ast) 107 + (ctx, expand) 125 108 126 - let stdout_for_pipeline ~sw = function 127 - | [] -> (None, None) 109 + let stdout_for_pipeline ~sw ctx = function 110 + | [] -> (None, ctx.stdout) 128 111 | _ -> 129 112 let r, w = Eio_unix.pipe sw in 130 113 (Some r, Some (w :> Eio_unix.sink_ty Eio.Flow.sink)) ··· 213 196 | WordGlobAll | WordGlobAny -> true 214 197 | _ -> false 215 198 216 - let glob_expand wc = 217 - Ast.word_components_to_string wc |> Globlon.glob |> Array.to_list 218 - 219 - let word_glob_expand wc = 220 - if List.exists needs_glob_expansion wc then glob_expand wc 221 - else [ Ast.word_components_to_string wc ] 222 - 223 - let args swc = 224 - List.concat_map 225 - (function 226 - | Ast.Suffix_redirect _ -> [] | Suffix_word wc -> word_glob_expand wc) 227 - swc 199 + let apply_pair (a, b) f = f a b 200 + let ( ||> ) = apply_pair 201 + let get_env ?(extra = []) () = Eunix.env () @ extra 228 202 229 203 let rec execute_commands initial_ctx local_switch p = 230 - let rec loop ctx 231 - ((status_of_previous, stdout_of_previous) : 232 - ctx Exit.t * Eio_unix.source_ty Eio_unix.source option) : 233 - Ast.command list -> ctx Exit.t = function 234 - | Ast.SimpleCommand (Prefixed _) :: next -> 235 - loop ctx (status_of_previous, stdout_of_previous) next 204 + let rec loop (exit_ctx : ctx Exit.t) 205 + (stdout_of_previous : Eio_unix.source_ty Eio_unix.source option) : 206 + Ast.command list -> ctx Exit.t = 207 + fun c -> 208 + let ctx = Exit.value exit_ctx in 209 + match c with 210 + | Ast.SimpleCommand (Prefixed (prefix, None, _suffix)) :: rest -> 211 + let ctx = collect_assignments ctx prefix in 212 + loop (Exit.zero ctx) stdout_of_previous rest 213 + | Ast.SimpleCommand (Prefixed (prefix, Some executable, suffix)) :: rest 214 + -> 215 + let ctx = collect_assignments ~update:false ctx prefix in 216 + loop (Exit.zero ctx) stdout_of_previous 217 + (Ast.SimpleCommand (Named (executable, suffix)) :: rest) 236 218 | Ast.SimpleCommand (Named (executable, None)) :: rest -> ( 219 + let ctx, executable = expand_cst ctx executable in 237 220 match 238 - Built_ins.of_args [ Ast.word_components_to_string executable ] 221 + Built_ins.of_args 222 + [ handle_word_components_to_string ctx executable ] 239 223 with 240 224 | Some bi -> handle_built_in ctx bi 241 225 | None -> ( 242 226 let some_read, some_write = 243 - stdout_for_pipeline ~sw:local_switch rest 227 + stdout_for_pipeline ctx ~sw:local_switch rest 244 228 in 245 229 match stdout_of_previous with 246 230 | None -> 247 - let+ () = 231 + let executable = 232 + handle_word_components_to_string ctx executable 233 + in 234 + let res = 248 235 E.exec ctx.executor ?stdout:some_write ~cwd:(cwd_of_ctx ctx) 249 - (List.map Ast.word_component_to_string executable) 236 + ~env:(get_env ~extra:ctx.local_state ()) 237 + [ executable ] 238 + >|= fun () -> clear_local_state ctx 250 239 in 251 - ctx 240 + Option.iter Eio.Flow.close some_write; 241 + loop res some_read rest 252 242 | Some stdout -> 243 + let executable = 244 + handle_word_components_to_string ctx executable 245 + in 253 246 let res = 254 247 E.exec ctx.executor ~stdin:stdout ?stdout:some_write 255 - ~cwd:(cwd_of_ctx ctx) 256 - (List.map Ast.word_component_to_string executable) 257 - >|= fun () -> ctx 248 + ~env:(get_env ~extra:ctx.local_state ()) 249 + ~cwd:(cwd_of_ctx ctx) [ executable ] 250 + >|= fun () -> clear_local_state ctx 258 251 in 259 252 Option.iter Eio.Flow.close some_write; 260 - loop ctx (res, some_read) rest)) 253 + loop res some_read rest)) 261 254 | Ast.SimpleCommand (Named (executable, Some suffix)) :: rest -> ( 262 - let args = args suffix in 255 + let ctx, executable = expand_cst ctx executable in 256 + let ctx, suffix = expand_redirects (ctx, []) suffix in 257 + let args = args ctx suffix in 263 258 match 264 - Built_ins.of_args (Ast.word_components_to_string executable :: args) 259 + Built_ins.of_args 260 + (handle_word_components_to_string ctx executable :: args) 265 261 with 266 262 | Some bi -> handle_built_in ctx bi 267 263 | None -> ( ··· 275 271 |> List.rev |> List.filter_map Fun.id 276 272 in 277 273 let some_read, some_write = 278 - stdout_for_pipeline ~sw:local_switch rest 274 + stdout_for_pipeline ~sw:local_switch ctx rest 279 275 in 280 276 match stdout_of_previous with 281 277 | None -> 282 278 let res = 283 279 E.exec ~fds:redirect ctx.executor ?stdout:some_write 284 280 ~cwd:(cwd_of_ctx ctx) 285 - (List.map Ast.word_component_to_string executable @ args) 286 - >|= fun () -> ctx 281 + ~env:(get_env ~extra:ctx.local_state ()) 282 + (handle_word_components_to_string ctx executable :: args) 283 + >|= fun () -> clear_local_state ctx 287 284 in 288 285 Option.iter Eio.Flow.close some_write; 289 - loop ctx (res, some_read) rest 286 + loop res some_read rest 290 287 | Some stdout -> 291 288 let res = 292 289 E.exec ~fds:redirect ctx.executor ~stdin:stdout 293 290 ~cwd:(cwd_of_ctx ctx) ?stdout:some_write 294 - (List.map Ast.word_component_to_string executable @ args) 295 - >|= fun () -> ctx 291 + ~env:(get_env ~extra:ctx.local_state ()) 292 + (handle_word_components_to_string ctx executable :: args) 293 + >|= fun () -> clear_local_state ctx 296 294 in 297 295 Option.iter Eio.Flow.close some_write; 298 - loop ctx (res, some_read) rest)) 296 + loop res some_read rest)) 299 297 | CompoundCommand (c, rdrs) :: _rest -> 300 298 let _rdrs = 301 299 List.map (handle_one_redirection ~sw:local_switch ctx) rdrs ··· 305 303 | v :: _ -> 306 304 Fmt.epr "TODO: %a" Yojson.Safe.pp (Ast.command_to_yojson v); 307 305 failwith "Err" 308 - | [] -> status_of_previous 306 + | [] -> exit_ctx 309 307 in 310 - loop initial_ctx (Exit.zero initial_ctx, None) p 308 + loop (Exit.zero initial_ctx) None p 309 + 310 + and expand_cst (ctx : ctx) cst = 311 + let cst = tilde_expansion ctx cst in 312 + let _, o = parameter_expansion' ctx in 313 + (ctx, o cst) 314 + 315 + and expand_redirects ((ctx, acc) : ctx * Ast.cmd_suffix_item list) 316 + (c : Ast.cmd_suffix_item list) = 317 + match c with 318 + | [] -> (ctx, List.rev acc) 319 + | Ast.Suffix_redirect (IoRedirect_IoFile (num, (op, file))) :: rest -> 320 + let ctx, cst = expand_cst ctx file in 321 + let cst = handle_subshell ctx cst in 322 + let v = Ast.Suffix_redirect (IoRedirect_IoFile (num, (op, cst))) in 323 + expand_redirects (ctx, v :: acc) rest 324 + | (Ast.Suffix_redirect _ as v) :: rest -> 325 + expand_redirects (ctx, v :: acc) rest 326 + | s :: rest -> expand_redirects (ctx, s :: acc) rest 311 327 312 328 and handle_single_pipeline ~sw ctx c = 313 329 let pipeline = function ··· 355 371 and handle_for_clause ctx = function 356 372 | Ast.For_Name_DoGroup (_, (term, sep)) -> exec ctx (term, Some sep) 357 373 | Ast.For_Name_In_WordList_DoGroup (Name name, wdlist, (term, sep)) -> 358 - let wdlist = Nlist.flatten @@ Nlist.map word_glob_expand wdlist in 374 + let wdlist = Nlist.flatten @@ Nlist.map (word_glob_expand ctx) wdlist in 359 375 (* Fmt.pr "List [%a]\n%!" Fmt.(list (list ~sep:Fmt.comma string)) (Nlist.to_list wdlist); *) 360 376 Nlist.fold_left 361 377 (fun _ word -> 362 378 (* let words = List.map (fun s -> Ast.WordLiteral s) words in *) 363 379 let s = S.update ctx.state ~param:name [ Ast.WordLiteral word ] in 364 380 let ctx = { ctx with state = s } in 365 - let ctx, o = parameter_expansion' ~skip_for_clauses:false ctx in 366 - let term = o#term term in 367 381 exec ctx (term, Some sep)) 368 382 (Exit.zero ctx) wdlist 369 383 ··· 373 387 Fmt.failwith "Compound command not supported: %a" yojson_pp 374 388 (Ast.compound_command_to_yojson c) 375 389 390 + and needs_subshelling = function 391 + | [] -> false 392 + | Ast.WordSubshell _ :: _ -> true 393 + | Ast.WordDoubleQuoted word :: rest -> 394 + needs_subshelling word || needs_subshelling rest 395 + | Ast.WordSingleQuoted word :: rest -> 396 + needs_subshelling word || needs_subshelling rest 397 + | _ -> false 398 + 399 + and handle_subshell (ctx : ctx) wcs = 400 + let exec_subshell ~sw ctx s = 401 + let buf = Buffer.create 16 in 402 + let stdout = Eio.Flow.buffer_sink buf in 403 + let r, w = Eio_unix.pipe sw in 404 + Eio.Fiber.fork ~sw (fun () -> Eio.Flow.copy r stdout); 405 + let subshell_ctx = { ctx with stdout = Some w } in 406 + let _ = run (Exit.zero subshell_ctx) s in 407 + (ctx, Buffer.contents buf) 408 + in 409 + let rec run_subshells ~sw ran_subshell = function 410 + | [] -> [] 411 + | Ast.WordSubshell s :: rest -> 412 + let _ctx, std = exec_subshell ~sw ctx s in 413 + ran_subshell := true; 414 + Ast.WordName (String.trim std) :: run_subshells ~sw ran_subshell rest 415 + | Ast.WordDoubleQuoted word :: rest -> 416 + let subshell_q = ref false in 417 + let res = run_subshells ~sw subshell_q word in 418 + if !subshell_q then res @ run_subshells ~sw subshell_q rest 419 + else Ast.WordDoubleQuoted res :: run_subshells ~sw subshell_q rest 420 + | Ast.WordSingleQuoted word :: rest -> 421 + let subshell_q = ref false in 422 + let res = run_subshells ~sw subshell_q word in 423 + if !subshell_q then res @ run_subshells ~sw subshell_q rest 424 + else Ast.WordSingleQuoted res :: run_subshells ~sw subshell_q rest 425 + | v :: rest -> v :: run_subshells ~sw ran_subshell rest 426 + in 427 + Eio.Switch.run @@ fun sw -> run_subshells ~sw (ref false) wcs 428 + 429 + and handle_word_components_to_string (ctx : ctx) wcs : string = 430 + if needs_subshelling wcs then begin 431 + let wcs = handle_subshell ctx wcs in 432 + Ast.word_components_to_string wcs 433 + end 434 + else Ast.word_components_to_string wcs 435 + 436 + and glob_expand ctx wc = 437 + handle_word_components_to_string ctx wc |> Globlon.glob |> Array.to_list 438 + 439 + and word_glob_expand (ctx : ctx) wc = 440 + if List.exists needs_glob_expansion wc then glob_expand ctx wc 441 + else [ handle_word_components_to_string ctx wc ] 442 + 443 + and collect_assignments ?(update = true) ctx = 444 + List.fold_left 445 + (fun ctx -> function 446 + | Ast.Prefix_assignment (Name param, v) -> 447 + (* Expand the values *) 448 + let ctx, v = expand_cst ctx v in 449 + let state = 450 + if update then S.update ctx.state ~param v else ctx.state 451 + in 452 + { 453 + ctx with 454 + state; 455 + local_state = 456 + (param, Ast.word_components_to_string v) :: ctx.local_state; 457 + } 458 + | _ -> ctx) 459 + ctx 460 + 461 + and args ctx swc = 462 + List.concat_map 463 + (function 464 + | Ast.Suffix_redirect _ -> [] 465 + | Suffix_word wc -> 466 + let ctx, cst = expand_cst ctx wc in 467 + word_glob_expand ctx cst) 468 + swc 469 + 376 470 and exec initial_ctx (ast : Ast.complete_command) = 377 471 let command, _ = ast in 378 472 let rec loop : Eio.Switch.t -> ctx -> Ast.clist -> ctx Exit.t = ··· 386 480 in 387 481 Eio.Switch.run @@ fun sw -> loop sw initial_ctx command 388 482 389 - let apply_pair (a, b) f = f a b 390 - let ( ||> ) = apply_pair 391 - 392 - let rec expand ctx (ast : Ast.complete_command) : ctx * Ast.complete_command = 393 - tilde_expansion ctx ast ||> parameter_expansion 394 - 395 - and redirect ctx (ast : Ast.complete_command) : ctx * Ast.complete_command = 396 - (ctx, ast) 397 - 398 - and execute ctx ast = assignments ctx ast ||> exec 483 + and execute ctx ast = exec ctx ast 399 484 400 485 and run ctx ast = 401 486 let ctx, cs = 402 487 List.fold_left 403 488 (fun (ctx, cs) command -> 404 489 let ctx = Exit.value ctx in 405 - let exit = expand ctx command ||> redirect ||> execute in 490 + let exit = execute ctx command in 406 491 match exit with 407 492 | Exit.Nonzero { exit_code; message; should_exit; _ } -> ( 408 493 Option.iter (Fmt.epr "%s\n%!") message;
+8 -2
src/lib/posix/merry_posix.ml
··· 6 6 type t = { mgr : Eio_unix.Process.mgr_ty Eio_unix.Process.mgr } 7 7 type fork_action = unit 8 8 9 - let exec ?fork_actions:_ ?(fds = []) ?stdin ?stdout ?stderr ~cwd t args = 9 + let exec ?fork_actions:_ ?(fds = []) ?stdin ?stdout ?stderr ?env ~cwd t args = 10 10 Eio.Switch.run @@ fun sw -> 11 - Exec.run ~sw ~fds ~cwd ?stdin ?stdout ?stderr t args |> Eio.Process.await 11 + let env = 12 + Option.map 13 + (fun lst -> List.map (fun (a, b) -> a ^ "=" ^ b) lst |> Array.of_list) 14 + env 15 + in 16 + Exec.run ~sw ~fds ~cwd ?stdin ?stdout ?stderr ?env t args 17 + |> Eio.Process.await 12 18 |> function 13 19 | `Exited 0 -> Merry.Exit.zero () 14 20 | `Exited n -> Merry.Exit.nonzero () n
+6
src/lib/posix/state.ml
··· 21 21 let update t ~param v = 22 22 let variables' = Variables.add param v t.variables in 23 23 { t with variables = variables' } 24 + 25 + let dump ppf s = 26 + Fmt.pf ppf "Variables:[%a]" 27 + Fmt.(list ~sep:Fmt.comma (pair string Yojson.Safe.pp)) 28 + (Variables.to_list s.variables 29 + |> List.map (fun (s, v) -> (s, Merry.Ast.word_cst_to_yojson v)))
+1 -2
src/lib/sast.ml
··· 114 114 and word_cst = word_component list 115 115 116 116 and word_component = 117 - | WordSubshell of subshell_kind * complete_commands 117 + | WordSubshell of complete_commands 118 118 | WordName of string 119 119 | WordAssignmentWord of assignment_word 120 120 | WordDoubleQuoted of word ··· 192 192 | RemoveSmallestPrefixPattern of word 193 193 | RemoveLargestPrefixPattern of word 194 194 195 - and subshell_kind = SubShellKindBackQuote | SubShellKindParentheses 196 195 and name = Name of string 197 196 and assignment_word = name * word 198 197 and io_number = int
+6 -3
src/lib/types.ml
··· 25 25 26 26 val update : t -> param:string -> Ast.word_cst -> t 27 27 (** Update the state with a new parameter mapping *) 28 + 29 + val dump : t Fmt.t 28 30 end 29 31 30 32 type redirect = ··· 42 44 val exec : 43 45 ?fork_actions:fork_action list -> 44 46 ?fds:redirect list -> 45 - ?stdin:_ Eio_unix.source -> 46 - ?stdout:_ Eio_unix.sink -> 47 - ?stderr:_ Eio_unix.sink -> 47 + ?stdin:_ Eio.Flow.source -> 48 + ?stdout:_ Eio.Flow.sink -> 49 + ?stderr:_ Eio.Flow.sink -> 50 + ?env:(string * string) list -> 48 51 cwd:Eio.Fs.dir_ty Eio.Path.t -> 49 52 t -> 50 53 string list ->
-1
test.sh
··· 1 - echo one; (sleep 1; echo two) & echo three
+20
test/forloops.t
··· 26 26 hello.txt 27 27 world.txt 28 28 29 + 1.4 Subshells and stuff! 30 + 31 + $ cat >test.sh << EOF 32 + > echo hello > abc.md 33 + > echo world > def.md 34 + > for f in *.md; do 35 + > cmarkit html --unsafe -e -c -h -k "\$f" > "\$(basename -- "\$f" .md).html" 36 + > done 37 + > EOF 38 + 39 + $ osh test.sh 40 + $ ls 41 + abc.html 42 + abc.md 43 + def.html 44 + def.md 45 + hello.txt 46 + test.sh 47 + world.txt 48 +
+38
test/simple.t
··· 12 12 $ osh test.sh 13 13 hello world 14 14 15 + $ cat >test.sh <<EOF 16 + > FOO=bar 17 + > echo "\$FOO" 18 + > EOF 19 + $ osh test.sh 20 + bar 21 + 22 + $ osh -c "FOO=bar echo \$FOO" 23 + 24 + $ osh -c "FOO=bar; echo \$FOO" 25 + bar 26 + 27 + $ osh -c "FOO=~/foo; echo \$FOO" 28 + /home/patrick/foo 29 + 30 + $ osh -c "FOO=~/foo env | grep FOO" 31 + FOO=/home/patrick/foo 32 + 33 + $ cat >test.sh << EOF 34 + > VAR1=hello 35 + > VAR2=world; VAR_UNSET=check echo \$VAR1 36 + > echo "VAR2: \$VAR2 (\$VAR_UNSET)" 37 + > EOF 38 + 39 + $ sh test.sh 40 + hello 41 + VAR2: world () 42 + $ osh test.sh 43 + hello 44 + VAR2: world () 45 + 46 + $ sh -c "FOO=abc env | grep FOO; env | grep FOO" 47 + FOO=abc 48 + [1] 49 + $ osh -c "FOO=abc env | grep FOO; env | grep FOO" 50 + FOO=abc 51 + [1] 52 + 15 53 2. Pipelines with And|Or 16 54 17 55 2.1 Simple Or
+14
test/subshell.t
··· 1 + Subshells! 2 + 3 + A simple test. 4 + 5 + $ cat > test.md << EOF 6 + > Just some text here 7 + > EOF 8 + $ osh -c "cat '$(echo test.md)'" 9 + Just some text here 10 + 11 + $ osh -c "'$(which echo)' hello" 12 + hello 13 + $ osh -c "`which echo` hello" 14 + hello