diff --git a/src/lib/built_ins.ml b/src/lib/built_ins.ml index 8e5a863..760250e 100644 --- a/src/lib/built_ins.ml +++ b/src/lib/built_ins.ml @@ -94,6 +94,7 @@ type t = | Return of int | Umask of int option | Shift of int option + | Read of bool * string list let reserved = [ "fg"; "bg"; "jobs" ] let pp_args = Fmt.(list ~sep:(Fmt.any " ") string) @@ -126,6 +127,8 @@ let to_string = function | Ignore -> "ignore" | Default -> "default") | Set _ -> "set" + | Read (backslash, vars) -> + Fmt.str "read%s %a" (if backslash then " -r" else " ") pp_args vars (* Change Directory *) module Cd = struct @@ -495,6 +498,30 @@ module Shift = struct Cmd.v info term end +module Read = struct + open Cmdliner + + let r = + let doc = + "Do not treat character in any special way. Consider each \ + to be part of the input line." + in + Arg.(value & flag & info [ "r" ] ~doc) + + let args = + let doc = "Names of existing or non-existing shell variables." in + Arg.(value & pos_all string [] & info [] ~docv:"var" ~doc) + + let t = + let make_read r args = Read (r, args) in + let term = Term.(const make_read $ r $ args) in + let info = + let doc = "Read from standard input into shell variables." in + Cmd.info "read" ~doc + in + Cmd.v info term +end + let of_args (w : string list) = let open Cmdliner in let exec_cmd cmd v = @@ -524,6 +551,7 @@ let of_args (w : string list) = | "return" :: _ as cmd -> exec_cmd cmd Return.t | "umask" :: _ as cmd -> exec_cmd cmd Umask.t | "shift" :: _ as cmd -> exec_cmd cmd Shift.t + | "read" :: _ as cmd -> exec_cmd cmd Read.t | cmd :: _ -> if List.mem cmd reserved then begin Debug.Log.err (fun f -> f "Unimplemented built-in: %s" cmd); diff --git a/src/lib/built_ins.mli b/src/lib/built_ins.mli index 9a68473..f79487b 100644 --- a/src/lib/built_ins.mli +++ b/src/lib/built_ins.mli @@ -53,6 +53,7 @@ type t = | Return of int | Umask of int option | Shift of int option + | Read of bool * string list val to_string : t -> string (** Serialises a built-in to a string *) diff --git a/src/lib/eval.ml b/src/lib/eval.ml index aa4db48..806b87a 100644 --- a/src/lib/eval.ml +++ b/src/lib/eval.ml @@ -1365,6 +1365,42 @@ module Make (S : Types.State) (E : Types.Exec) = struct assert (new_len >= 0); let argv = Array.init new_len (fun i -> Array.get ctx.argv (i + n)) in Exit.zero { ctx with argv } + | Read (_backslash, vars) -> ( + let line = + let buf = Cstruct.create 1 in + let rec loop acc = + match + Eio.Flow.read_exact ctx.stdin buf; + Cstruct.to_string buf + with + | "\n" -> Some acc + | c -> loop (acc ^ c) + | exception End_of_file -> None + in + loop "" + in + let rec loop acc = function + | v :: vars, Ast.{ txt; _ } :: fs -> loop ((v, txt) :: acc) (vars, fs) + | _, [] -> List.rev acc + | [], lines -> + let last_var, last_line = List.hd acc in + List.rev + ((last_var, last_line ^ Ast.Fragment.join_list ~sep:"" lines) + :: acc) + in + let fields = + Option.map (fun s -> field_splitting ctx [ Ast.Fragment.make s ]) line + in + match fields with + | None -> Exit.nonzero ctx 1 + | Some fs -> + let vars = loop [] (vars, fs) in + let state = + List.fold_left + (fun st (k, v) -> S.update st ~param:k v |> Result.get_ok) + ctx.state vars + in + Exit.zero { ctx with state }) | Command _ -> (* Handled separately *) assert false diff --git a/test/built_ins.t b/test/built_ins.t index af57b73..8a92ae8 100644 --- a/test/built_ins.t +++ b/test/built_ins.t @@ -328,3 +328,38 @@ Some bits of `exec` (in particular redirects) are supported. args: a b c d args 2: c d args 2: 2 + +15. Read + + + $ cat > test.sh << EOF + > echo "hello world" | read FOO BAR + > echo "FOO is \$FOO" + > echo "BAR is \$BAR" + > EOF + + $ sh test.sh + FOO is + BAR is + $ msh test.sh + FOO is + BAR is + + $ cat > test.sh << EOF + > echo hello > hello.txt + > echo world >> hello.txt + > echo finished >> hello.txt + > + > while read line; do + > echo "Got line: \$line" + > done < hello.txt + > EOF + + $ sh test.sh + Got line: hello + Got line: world + Got line: finished + $ msh test.sh + Got line: hello + Got line: world + Got line: finished