···11-let () = print_endline "Hello, World!"
11+(* A shell... one day *)
22+33+module Shell = Merry.Eval.Make (Merry_posix.State) (Merry_posix.Exec)
44+55+let sh ~command ~dump ~file env =
66+ let ast =
77+ match (file, command) with
88+ | None, None ->
99+ Fmt.pr "Expected either a file path or a command to run...";
1010+ exit 1
1111+ | Some file, None -> Merry.of_file Eio.Path.(env#fs / file)
1212+ | _, Some c -> Merry.of_string c
1313+ in
1414+ if dump then Merry.Ast.Dump.pp Fmt.stdout ast
1515+ else
1616+ let _ctx, _ast =
1717+ let executor =
1818+ Merry_posix.Exec.
1919+ {
2020+ mgr =
2121+ (env#process_mgr
2222+ :> [ `Generic ] Eio.Process.mgr_ty Eio.Process.mgr);
2323+ }
2424+ in
2525+ Shell.run
2626+ Shell.
2727+ {
2828+ state = Merry_posix.State.make ~home:(Sys.getenv "HOME") ();
2929+ executor;
3030+ }
3131+ ast
3232+ in
3333+ ()
3434+3535+open Cmdliner
3636+open Cmdliner.Term.Syntax
3737+3838+let command =
3939+ let doc = "command to run" in
4040+ Arg.(value & opt (some string) None & info [ "c"; "C" ] ~doc)
4141+4242+let file =
4343+ let doc = "The shell script to execute" in
4444+ Arg.(value & pos 0 (some string) None & info [] ~doc)
4545+4646+let dump =
4747+ let doc =
4848+ "dump an internal AST representation to JSON (does nothing in interactive \
4949+ mode)."
5050+ in
5151+ Arg.(value & flag & info [ "d"; "D"; "dump" ] ~doc)
5252+5353+let cmd env =
5454+ let doc = "A shell." in
5555+ let man =
5656+ [
5757+ `S Manpage.s_description;
5858+ `P
5959+ "$(cmd) is first a foremost a shell written completely in the OCaml \
6060+ programming language";
6161+ `S Manpage.s_bugs;
6262+ `P "Report bugs at https://tangled.org/patrick.sirref.org/merry/issues.";
6363+ ]
6464+ in
6565+ Cmd.make (Cmd.info "osh" ~version:"v0.0.1" ~doc ~man)
6666+ @@
6767+ let+ command = command and+ dump = dump and+ file = file in
6868+ sh ~command ~dump ~file env
6969+7070+let main () = Eio_posix.run @@ fun env -> Cmd.eval (cmd env)
7171+let () = if !Sys.interactive then () else exit (main ())
+8-10
src/lib/ast.ml
···4949 let a = and_or a.value in
5050 Nslist.singleton Nosep a
51515252-and and_or : CST.and_or -> pipeline and_or_list =
5353- fun x ->
5252+and and_or : ?sep:and_or -> CST.and_or -> pipeline and_or_list =
5353+ fun ?(sep = Noand_or) x ->
5454 match x with
5555 | AndOr_Pipeline a ->
5656 let a = pipeline a.value in
5757- Nslist.singleton Noand_or a
5757+ Nslist.singleton sep a
5858 | AndOr_AndOr_AndIf_LineBreak_Pipeline (a, _, c) ->
5959- let rest = and_or a.value in
5959+ let rest = and_or ~sep:And a.value in
6060 let c = pipeline c.value in
6161- Nslist.cons And c rest
6161+ Nslist.append rest (Nlist.Singleton (c, sep))
6262 | AndOr_AndOr_OrIf_LineBreak_Pipeline (a, _, c) ->
6363- let rest = and_or a.value in
6363+ let rest = and_or ~sep:Or a.value in
6464 let c = pipeline c.value in
6565- Nslist.cons Or c rest
6565+ Nslist.append rest (Nlist.Singleton (c, sep))
66666767and pipeline : CST.pipeline -> pipeline =
6868 fun x ->
···514514 | SequentialSep_Semicolon_LineBreak _ -> Semicolon
515515 | SequentialSep_NewLineList _ -> Newline
516516517517-and word : CST.word -> word =
518518- fun x -> match x with Word (a, b) -> (a, word_cst b)
519519-517517+and word : CST.word -> word = fun x -> match x with Word (_, b) -> word_cst b
520518and word_cst : CST.word_cst -> word_cst = fun v -> List.map word_component v
521519522520and word_component : CST.word_component -> word_component =
+4
src/lib/ast.mli
···99val of_program : Morbig.CST.program -> t
1010(** An AST from a Morbig program *)
11111212+(** {2 Utilities} *)
1313+1414+(* class map : Ppxlib_traverse_builtins.map *)
1515+1216module Dump : sig
1317 val pp : t Fmt.t
1418 (** Dump the program *)
···11+(*-----------------------------------------------------------------
22+ Copyright (c) 2025 The merry programmers. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ -----------------------------------------------------------------*)
55+open Import
66+77+(** An evaluator over the AST *)
88+module Make (S : Types.State) (E : Types.Exec) = struct
99+ (* What follows uses the POSIX definition of what a shell does ($ 2.1).
1010+1111+ It starts from point (4), completing a series of expansions on the AST,
1212+ then redirection is setup, and finally functions/built-ins/commands are
1313+ executed. *)
1414+1515+ let word_component_to_string : Ast.word_component -> string = function
1616+ | WordName s -> s
1717+ | WordLiteral s -> s
1818+ | v ->
1919+ Fmt.failwith "Conversion of %a" Yojson.Safe.pp
2020+ (Ast.word_component_to_yojson v)
2121+2222+ class default_map =
2323+ object (_)
2424+ inherit Ast.map
2525+ method string (s : string) = s
2626+ method int (i : int) = i
2727+ method char c = c
2828+ method option f v = Option.map f v
2929+ method nlist__t f t = Nlist.map f t
3030+ method nslist__t f t = Nslist.map f t
3131+ method list f t = List.map f t
3232+ end
3333+3434+ type ctx = { state : S.t; executor : E.t }
3535+3636+ class default_ctx_fold =
3737+ object (_)
3838+ inherit [ctx] Ast.fold
3939+ method string _ ctx = ctx
4040+ method char _ ctx = ctx
4141+ method option f v ctx = Option.fold ~none:ctx ~some:(fun i -> f i ctx) v
4242+ method nlist__t f v ctx = Nlist.fold_left (fun acc i -> f i acc) ctx v
4343+4444+ method nslist__t f g v ctx =
4545+ Nslist.fold_left (fun acc a b -> f a acc |> g b) ctx v
4646+4747+ method list f v ctx = List.fold_left (fun acc i -> f i acc) ctx v
4848+ end
4949+5050+ let map_word_components f ast =
5151+ let o =
5252+ object (_)
5353+ inherit default_map
5454+ method! word_component cst = f cst
5555+ end
5656+ in
5757+ o#complete_command ast
5858+5959+ let map_words f ast =
6060+ let o =
6161+ object (_)
6262+ inherit default_map
6363+ method! word cst = f cst
6464+ end
6565+ in
6666+ o#complete_command ast
6767+6868+ let tilde_expansion ctx ast =
6969+ ( ctx,
7070+ map_word_components
7171+ (function
7272+ | Ast.WordTildePrefix _ -> Ast.WordName (S.expand ctx.state `Tilde)
7373+ | s -> s)
7474+ ast )
7575+7676+ let parameter_expansion ctx ast =
7777+ ( ctx,
7878+ map_words
7979+ (List.concat_map (function
8080+ | Ast.WordVariable v -> (
8181+ match v with
8282+ | Ast.VariableAtom (s, NoAttribute) -> (
8383+ match S.lookup ctx.state ~param:s with
8484+ | None -> [ Ast.WordName "" ]
8585+ | Some cst -> cst)
8686+ | _ -> Fmt.failwith "No support for variable attributes yet!")
8787+ | s -> [ s ]))
8888+ ast )
8989+9090+ let assignments ctx ast =
9191+ let o =
9292+ object
9393+ inherit default_ctx_fold
9494+9595+ method! simple_command ast ctx =
9696+ match ast with
9797+ | Ast.Prefixed (cmd_prefix, _, _) ->
9898+ List.fold_left
9999+ (fun ctx -> function
100100+ | Ast.Prefix_assignment (Name param, v) ->
101101+ let state = S.update ctx.state ~param v in
102102+ { ctx with state }
103103+ | _ -> ctx)
104104+ ctx cmd_prefix
105105+ | _ -> ctx
106106+ end
107107+ in
108108+ (o#complete_command ast ctx, ast)
109109+110110+ let exec ctx (ast : Ast.complete_command) =
111111+ let command, _ = ast in
112112+ let execute_command p =
113113+ let h = List.hd p in
114114+ match h with
115115+ | Ast.SimpleCommand (Prefixed _) -> None
116116+ | Ast.SimpleCommand (Named (executable, None)) ->
117117+ E.exec ctx.executor (List.map word_component_to_string executable)
118118+ |> Option.some
119119+ | Ast.SimpleCommand (Named (executable, Some suffix)) ->
120120+ let args =
121121+ List.filter_map
122122+ (function
123123+ | Ast.Suffix_word w ->
124124+ Some (List.map word_component_to_string w)
125125+ | Ast.Suffix_redirect _ -> None)
126126+ suffix
127127+ in
128128+ E.exec ctx.executor
129129+ (List.map word_component_to_string executable @ List.concat args)
130130+ |> Option.some
131131+ | v ->
132132+ Fmt.epr "TODO: %a" Yojson.Safe.pp (Ast.command_to_yojson v);
133133+ failwith "Err"
134134+ in
135135+ let pipeline = function
136136+ | Ast.Pipeline p -> (Fun.id, p)
137137+ | Ast.Pipeline_Bang p ->
138138+ (Option.map (fun i -> if Int.equal i 0 then -1 else 0), p)
139139+ in
140140+ let loop : Ast.clist -> int option = function
141141+ | Nlist.Singleton (c, _) ->
142142+ let rec fold :
143143+ Ast.and_or * int option ->
144144+ Ast.pipeline Ast.and_or_list ->
145145+ int option =
146146+ fun (sep, exit_so_far) pipe ->
147147+ match (sep, pipe) with
148148+ | And, Nlist.Singleton (p, _) -> (
149149+ match exit_so_far with
150150+ | Some 0 ->
151151+ let f, p = pipeline p in
152152+ f @@ execute_command p
153153+ | v -> v)
154154+ | Or, Nlist.Singleton (p, _) -> (
155155+ match exit_so_far with
156156+ | Some 0 -> Some 0
157157+ | _ ->
158158+ let f, p = pipeline p in
159159+ f @@ execute_command p)
160160+ | Noand_or, Nlist.Cons ((p, next_sep), rest) ->
161161+ let f, p = pipeline p in
162162+ fold (next_sep, f (execute_command p)) rest
163163+ | And, Nlist.Cons ((p, next_sep), rest) -> (
164164+ match exit_so_far with
165165+ | Some 0 ->
166166+ let f, p = pipeline p in
167167+ fold (next_sep, f (execute_command p)) rest
168168+ | (None | Some _) as v -> v)
169169+ | Or, Nlist.Cons ((p, next_sep), rest) -> (
170170+ match exit_so_far with
171171+ | Some 0 -> fold (next_sep, exit_so_far) rest
172172+ | None | Some _ ->
173173+ let f, p = pipeline p in
174174+ fold (next_sep, f (execute_command p)) rest)
175175+ | _ -> assert false
176176+ in
177177+ fold (Noand_or, None) c
178178+ | _ -> Fmt.failwith "TODO!!!"
179179+ in
180180+ (loop command, ctx, ast)
181181+182182+ let apply_pair (a, b) f = f a b
183183+ let ( ||> ) = apply_pair
184184+185185+ let rec expand ctx (ast : Ast.complete_command) : ctx * Ast.complete_command =
186186+ tilde_expansion ctx ast ||> parameter_expansion
187187+188188+ and redirect ctx (ast : Ast.complete_command) : ctx * Ast.complete_command =
189189+ (ctx, ast)
190190+191191+ and execute ctx ast = assignments ctx ast ||> exec
192192+193193+ and run ctx ast =
194194+ let ctx, cs =
195195+ List.fold_left
196196+ (fun (ctx, cs) command ->
197197+ let exit_code, ctx, ast =
198198+ expand ctx command ||> redirect ||> execute
199199+ in
200200+ Option.iter (function 0 -> () | n -> exit n) exit_code;
201201+ (ctx, ast :: cs))
202202+ (ctx, []) ast
203203+ in
204204+ (ctx, List.rev cs)
205205+end
+10
src/lib/import.ml
···2424 in
2525 loop v1
26262727+ let fold_left f acc v = to_list v |> List.fold_left f acc
2728 let ( @ ) = append
28292930 let to_yojson f v : Yojson.Safe.t =
3031 let lst = to_list v in
3132 `List (List.map (fun i -> f i) lst)
3333+3434+ let pp elt ppf v = to_list v |> Fmt.(list elt) ppf
3235end
33363437(* Nonempty, separated lists *)
···37403841 let singleton sep i = Nlist.Singleton (i, sep)
3942 let cons sep i v = Nlist.cons (i, sep) v
4343+ let append a b = Nlist.append a b
4444+4545+ let map f g (v : ('a, 'b) t) : ('c, 'd) t =
4646+ Nlist.map (fun (a, b) -> (f a, g b)) v
4747+4848+ let fold_left f acc (vs : ('a, 'b) t) =
4949+ Nlist.fold_left (fun acc (a, b) -> f acc a b) acc vs
4050end
+19
src/lib/merry.ml
···11module Ast = Ast
22+module Types = Types
33+module Eval = Eval
44+55+let of_string s = Morbig.parse_string "-" s |> Ast.of_program
2637let of_file path =
48 let fname = Eio.Path.native_exn path in
59 Eio.Path.load path |> Morbig.parse_string fname |> Ast.of_program
1010+1111+module Variable = struct
1212+ type t
1313+ (** A name for a {! Parameter.t} *)
1414+end
1515+1616+module Function = struct
1717+ type t
1818+ (** Function definitions *)
1919+end
2020+2121+module Trap = struct
2222+ type t
2323+ (** Traps *)
2424+end
+21
src/lib/merry.mli
···11module Ast = Ast
22+module Types = Types
33+44+val of_string : string -> Ast.t
55+(** Construct an AST from a string *)
2637val of_file : _ Eio.Path.t -> Ast.t
48(** Construct an AST from a file path. *)
99+1010+module Eval = Eval
1111+1212+module Variable : sig
1313+ type t
1414+ (** A name for a {! Parameter.t} *)
1515+end
1616+1717+module Function : sig
1818+ type t
1919+ (** Function definitions *)
2020+end
2121+2222+module Trap : sig
2323+ type t
2424+ (** Traps *)
2525+end
···11+(* A set of modules for writing shell's that adhere, as past they can,
22+ to the POSIX standard: https://pubs.opengroup.org/onlinepubs/9699919799/ *)
33+module State = State
44+55+module Exec = struct
66+ type t = { mgr : [ `Generic ] Eio.Process.mgr_ty Eio.Process.mgr }
77+ type fork_action = unit
88+99+ let exec ?fork_actions:_ t s =
1010+ Eio.Switch.run @@ fun sw ->
1111+ Eio.Process.spawn ~sw t.mgr s |> Eio.Process.await |> function
1212+ | `Exited n -> n
1313+ | `Signaled n -> n
1414+end
+22
src/lib/posix/state.ml
···11+module Variables = Map.Make (String)
22+33+type t = {
44+ cwd : string;
55+ functions : Merry.Function.t list;
66+ root : int;
77+ outermost : bool;
88+ home : string;
99+ variables : Merry.Ast.word_cst Variables.t;
1010+}
1111+1212+let make ?(cwd = ".") ?(functions = []) ?(root = 0) ?(outermost = true)
1313+ ?(home = "/root") ?(variables = Variables.empty) () =
1414+ { cwd; functions; root; outermost; home; variables }
1515+1616+let default = make ()
1717+let expand t = function `Tilde -> t.home
1818+let lookup t ~param = Variables.find_opt param t.variables
1919+2020+let update t ~param v =
2121+ let variables' = Variables.add param v t.variables in
2222+ { t with variables = variables' }
+4-2
src/lib/sast.ml
···1313 on being executable rather than capturing notions like position, linebreaks etc. *)
1414open Import
15151616-type complete_commands = complete_command list [@@deriving to_yojson]
1616+type complete_commands = complete_command list
1717+[@@deriving to_yojson, traverse_map, traverse_fold]
1818+1719and complete_command = clist * separator option
1820and and_or = And | Or | Noand_or
1921and 'v separator_list = ('v, separator) Nslist.t
···112114and here_end = HereEnd_Word of word
113115and separator = Uppersand | Semicolon | Nosep
114116and sequential_sep = Semicolon | Newline
115115-and word = string * word_cst
117117+and word = word_cst
116118and word_cst = word_component list
117119118120and word_component =
+34
src/lib/types.ml
···11+module Parameter = struct
22+ type t =
33+ | String of string
44+ | Number of string
55+ | Special of string
66+ | Null (** Possible shell parameters *)
77+end
88+99+module type State = sig
1010+ type t
1111+ (** State for the shell and operating system that is carried from one
1212+ evaluation step to the next. *)
1313+1414+ val expand : t -> [ `Tilde ] -> string
1515+ (** Expansions *)
1616+1717+ val lookup : t -> param:string -> Ast.word_cst option
1818+ (** Parameter lookup. [None] means [unset]. *)
1919+2020+ val update : t -> param:string -> Ast.word_cst -> t
2121+ (** Update the state with a new parameter mapping *)
2222+end
2323+2424+module type Exec = sig
2525+ type t
2626+ (** An executor for commands *)
2727+2828+ type fork_action
2929+ (** A fork action is a piece of C-code to run inbetween the fork and the exec
3030+ *)
3131+3232+ val exec : ?fork_actions:fork_action list -> t -> string list -> int
3333+ (** Run a command in a child process *)
3434+end