diff --git a/src/browser/dune b/src/browser/dune index 1e6a33f..73f6750 100644 --- a/src/browser/dune +++ b/src/browser/dune @@ -1,7 +1,7 @@ (executable (modes js) (name index) - (libraries merry ansi bruit.brr eio_mem) + (libraries merry merry.utils ansi bruit.brr eio_mem) (js_of_ocaml (flags (:standard --enable=effects)))) diff --git a/src/browser/index.ml b/src/browser/index.ml index 663ff7d..57421f5 100644 --- a/src/browser/index.ml +++ b/src/browser/index.ml @@ -191,7 +191,7 @@ let make_passwd fs = Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 (fs / "/etc"); Eio.Path.save ~create:(`If_missing 0o644) (fs / "/etc/passwd") "root:x:0:0:System administrator:/:/run/current-system/sw/bin/bash\n"; - Eio.Path.save ~create:(`If_missing 0o644) (fs / "/etc/passwd") + Eio.Path.save ~append:true ~create:(`If_missing 0o644) (fs / "/etc/passwd") "msh:x:1:100::/home/msh:/run/current-system/sw/bin/msh\n" let stdin_flow : Eio_unix.source_ty Eio.Flow.source = @@ -219,7 +219,7 @@ let stdout_flow : Eio_unix.sink_ty Eio.Flow.sink = let buf = Cstruct.concat bufs |> Cstruct.to_string in if buf = "\r" then 1 else begin - let el = El.pre [] in + let el = El.code [] in html el buf; El.append_children stdout [ el ]; String.length buf @@ -254,6 +254,20 @@ let fd_opt r = | None -> None | Some fd -> Some (Merry.Types.make_packed_fd (module Fd) fd) +let run_built_in args ctx = + match args with + | "ls" :: _ -> + let env = C.env ctx in + let term = Merry_utils.Ls.term env in + let res = Cmdliner.Cmd.eval ~argv:(Array.of_list args) term in + if res = 0 then Merry.Exit.zero ctx else Merry.Exit.nonzero ctx res + | "cat" :: _ -> + let env = C.env ctx in + let term = Merry_utils.Cat.term env in + let res = Cmdliner.Cmd.eval ~argv:(Array.of_list args) term in + if res = 0 then Merry.Exit.zero ctx else Merry.Exit.nonzero ctx res + | _ -> Merry.Exit.zero ctx + let () = let () = Logs.set_level (Some Logs.Debug) in Eio_js_backend.start @@ fun () -> @@ -291,9 +305,10 @@ let () = method secure_random = secure_random end in + let built_ins = ([ "ls"; "cat" ], run_built_in) in Eio.Switch.run ~name:"async" @@ fun async_switch -> let ctx = - C.make_ctx ~interactive:true + C.make_ctx ~built_ins ~interactive:true (Merry.State.make ~variables:Merry.State.Variables.empty ~home path (Fpath.v "/")) executor fd_table ~async_switch ~fd_opt diff --git a/src/lib_utils/cat.ml b/src/lib_utils/cat.ml new file mode 100644 index 0000000..875acc5 --- /dev/null +++ b/src/lib_utils/cat.ml @@ -0,0 +1,33 @@ +open Cmdliner + +let paths = + let doc = + "A pathname of an input file. If no file operands are specified, the \ + standard input shall be used. If a file is '-', the cat utility shall \ + read from the standard input at that point in the sequence. The cat \ + utility shall not close and reopen standard input when it is referenced \ + in this way, but shall accept multiple occurrences of '-' as a file \ + operand." + in + Arg.(value & pos_all string [] & info [] ~doc) + +let term env = + let make paths = + match paths with + | [] -> Eio.Flow.copy env#stdin env#stdout + | _ -> + List.iter + (fun path -> + Eio.Path.(with_open_in (env#fs / path)) @@ fun src -> + Eio.Flow.copy src env#stdout) + paths + in + let term = Term.(const make $ paths) in + let info = + let doc = + "The cat utility shall read files in sequence and shall write their \ + contents to the standard output in the same sequence." + in + Cmd.info "cat" ~doc + in + Cmd.v info term diff --git a/src/lib_utils/dune b/src/lib_utils/dune new file mode 100644 index 0000000..8944ba2 --- /dev/null +++ b/src/lib_utils/dune @@ -0,0 +1,4 @@ +(library + (name merry_utils) + (public_name merry.utils) + (libraries cmdliner eio)) diff --git a/src/lib_utils/ls.ml b/src/lib_utils/ls.ml new file mode 100644 index 0000000..3b350d2 --- /dev/null +++ b/src/lib_utils/ls.ml @@ -0,0 +1,118 @@ +module Config = struct + type t = { all_directory_entries : bool; long_form : bool } + + let default = { all_directory_entries = false; long_form = false } + + let make ?(all_directory_entries = default.all_directory_entries) + ?(long_form = default.long_form) () = + { all_directory_entries; long_form } + + open Cmdliner + + let all_directory_entries = + let doc = + "Write out all directory entries, including those whose names begin with \ + a period but excluding dot and dot-dot." + in + Arg.(value & flag & info [ "a" ] ~doc) + + let long_form = + let doc = "Write out in long format." in + Arg.(value & flag & info [ "l" ] ~doc) +end + +let flow_as_formatter stdout = + let outfns : Format.formatter_out_functions = + { + out_string = + (fun s i len -> Eio.Flow.copy_string (String.sub s i len) stdout); + out_width = Format.utf_8_scalar_width; + out_flush = (fun () -> ()); + out_newline = (fun () -> Eio.Flow.copy_string "\n" stdout); + out_spaces = (fun n -> Eio.Flow.copy_string (String.make n ' ') stdout); + out_indent = (fun n -> Eio.Flow.copy_string (String.make n ' ') stdout); + } + in + Format.formatter_of_out_functions outfns + +let pp_file_mode ppf (kind, mode) = + let kind_letter = + match kind with + | `Directory -> "d" + | `Block_device -> "b" + | `Character_special -> "c" + | `Symbolic_link -> "l" + | `Socket -> "s" + | `Regular_file -> "-" + | _ -> "o" + in + let check_perm perm mask c = if perm land mask <> 0 then c else "-" in + let permissions pos ppf perm = + Fmt.pf ppf "%s%s%s" + (check_perm perm (0o400 lsr pos) "r") + (check_perm perm (0o200 lsr pos) "w") + (check_perm perm (0o100 lsr pos) "x") + in + Fmt.pf ppf "%s%a%a%a" kind_letter (permissions 0) mode (permissions 3) mode + (permissions 6) mode + +let pp_long_form' ppf ((stat, name) : Eio.File.Stat.t * string) = + match stat.kind with + | `Character_special | `Block_device -> + Fmt.pf ppf "%a %Ld %Ld %Ld %Ld %s %s" pp_file_mode (stat.kind, stat.perm) + stat.nlink stat.uid stat.gid stat.dev "" name + | _ -> + Fmt.pf ppf "%a %Ld %Ld %Ld %a %s %s" pp_file_mode (stat.kind, stat.perm) + stat.nlink stat.uid stat.gid Optint.Int63.pp stat.size "" name + +let pp_long_form ppf paths = + Fmt.pf ppf "%a\n%!" Fmt.(list ~sep:(Fmt.any "\n") pp_long_form') paths + +let pp ppf paths = + Fmt.pf ppf "%a\n%!" Fmt.(list ~sep:(Fmt.any " ") Fmt.string) paths + +let ls_path ~config stdout path = + let d = Eio.Path.read_dir_entries path |> List.map snd in + if config.Config.long_form then + let d = + List.map + (fun s -> + let fpath = Eio.Path.(path / s) in + (Eio.Path.stat ~follow:false fpath, s)) + d + in + pp_long_form stdout d + else pp stdout d + +let ls ?(config = Config.default) env paths = + let stdout = Eio.Stdenv.stdout env |> flow_as_formatter in + match paths with + | [ path ] -> ls_path ~config stdout path + | _ -> ls_path ~config stdout env#fs + +open Cmdliner + +let paths = + let doc = "Files -- pathnames to files." in + Arg.(value & pos_all string [] & info [] ~doc) + +let term env = + let make all_directory_entries long_form paths = + let config = Config.make ~all_directory_entries ~long_form () in + ls ~config env (List.map (Eio.Path.( / ) env#fs) paths) + in + let term = + Term.(const make $ Config.all_directory_entries $ Config.long_form $ paths) + in + let info = + let doc = + "For each operand that names a file of a type other than directory or \ + symbolic link to a directory, ls shall write the name of the file as \ + well as any requested, associated information. For each operand that \ + names a file of type directory, ls shall write the names of files \ + contained within the directory as well as any requested, associated \ + information." + in + Cmd.info "ls" ~doc + in + Cmd.v info term diff --git a/src/lib_utils/merry_utils.ml b/src/lib_utils/merry_utils.ml new file mode 100644 index 0000000..58eea20 --- /dev/null +++ b/src/lib_utils/merry_utils.ml @@ -0,0 +1,2 @@ +module Cat = Cat +module Ls = Ls diff --git a/src/lib_utils/types.ml b/src/lib_utils/types.ml new file mode 100644 index 0000000..8a77a30 --- /dev/null +++ b/src/lib_utils/types.ml @@ -0,0 +1,3 @@ +module type Util = sig + val term : int Cmdliner.Term.t +end