Shells in OCaml
at main 126 lines 4.4 kB view raw
1let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty 2 3module Make (S : Types.State) (E : Types.Exec) (H : Types.History) = struct 4 module Eval = Eval.Make (S) (E) 5 6 let pp_colored c pp fmt v = Fmt.pf fmt "%a" (Fmt.styled (`Fg c) pp) v 7 8 let subst_tilde path = 9 match Eunix.find_env "HOME" with 10 | None -> path 11 | Some home -> ( 12 match Fpath.rem_prefix (Fpath.v home) path with 13 | Some rel -> Fpath.(v "~" // rel) 14 | None -> path) 15 16 let default_prompt (ctx : Eval.ctx Exit.t) = 17 let state = 18 match ctx with 19 | Exit.Zero ctx | Exit.Nonzero { value = ctx; _ } -> Eval.state ctx 20 in 21 let pp_status ppf = function 22 | Exit.Zero _ -> () 23 | Exit.Nonzero { exit_code; _ } -> 24 Fmt.pf ppf "[%a] " (pp_colored `Red Fmt.int) exit_code 25 in 26 Fmt.pf Format.str_formatter "%a%a:%s >\n%!" pp_status ctx 27 Fmt.(pp_colored `Yellow string) 28 (Eunix.get_user_and_host ()) 29 (Fpath.normalize @@ S.cwd state |> subst_tilde |> Fpath.to_string); 30 Format.flush_str_formatter () 31 32 let resolve_programs name = 33 if not (String.contains name '/') then begin 34 Eunix.find_env "PATH" 35 |> Option.value ~default:"/bin:/usr/bin" 36 |> String.split_on_char ':' 37 |> List.concat_map (fun dir -> 38 let files = try Sys.readdir dir |> Array.to_list with _ -> [] in 39 List.filter (fun f -> String.starts_with ~prefix:name f) files) 40 end 41 else if Sys.file_exists name then [ name ] 42 else [] 43 44 let complete path = 45 let rest, last_arg = 46 String.split_on_char ' ' path |> List.rev |> function 47 | [] -> ([], `None) 48 | [ prog ] -> ([], `Prog prog) 49 | x :: rest -> (List.rev rest, `Path x) 50 in 51 let completions from_path basename = 52 match (Unix.stat from_path).st_kind with 53 | S_DIR -> 54 let entries = Sys.readdir from_path |> Array.to_list in 55 List.filter_map 56 (fun e -> 57 match basename with 58 | None -> Some e 59 | Some prefix -> 60 if String.starts_with ~prefix e then Some e else None) 61 entries 62 |> List.map (fun e -> Filename.concat from_path e) 63 |> List.map (fun p -> String.concat " " (rest @ [ p ])) 64 | S_REG -> [ String.concat " " (rest @ [ path ]) ] 65 | _ -> [] 66 | exception Unix.Unix_error (Unix.ENOENT, _, _) -> [] 67 in 68 match last_arg with 69 | `None -> [] 70 | `Prog prog -> resolve_programs prog 71 | `Path path -> ( 72 match Unix.(stat path).st_kind with 73 | exception Unix.Unix_error (Unix.ENOENT, _, _) -> 74 let dirname = Filename.dirname path in 75 let basename = 76 match Filename.basename path with "." | "" -> None | p -> Some p 77 in 78 completions dirname basename 79 | S_DIR -> completions path None 80 | _ -> []) 81 82 let run ?(prompt = default_prompt) initial_ctx = 83 Sys.set_signal Sys.sigttou Sys.Signal_ignore; 84 Sys.set_signal Sys.sigttin Sys.Signal_ignore; 85 Sys.set_signal Sys.sigtstp Sys.Signal_ignore; 86 Sys.set_signal Sys.sigint Sys.Signal_ignore; 87 let xdg = Xdge.create (Eval.fs (Exit.value initial_ctx)) "merry" in 88 let history = Eio.Path.(Xdge.data_dir xdg / ".merry_history") in 89 let initial_history = try H.load history with _ -> H.empty in 90 let h = ref initial_history in 91 let add_history c = 92 let c = String.trim c in 93 h := H.add (H.make_entry c) !h 94 in 95 let rec loop (ctx : Eval.ctx Exit.t) = 96 Option.iter (Fmt.epr "%s%!") 97 (S.lookup (Exit.value ctx |> Eval.state) ~param:"PS1" 98 |> Option.map Ast.word_components_to_string); 99 let p = prompt ctx in 100 Fmt.pr "%s\r%!" p; 101 match 102 Bruit.bruit 103 ~history:(fun command -> H.history ~command !h |> H.commands) 104 ~complete "% " 105 with 106 | String None -> 107 Fmt.pr "exit\n%!"; 108 exit 0 109 | String (Some c) -> 110 let ast = Ast.of_string (String.trim c) in 111 Fmt.pr "\n%!"; 112 let ctx', _ast = Eval.run ctx ast in 113 add_history c; 114 H.save !h history; 115 loop ctx' 116 | Ctrl_c -> 117 let c = Exit.value ctx in 118 Eunix.Signals.(raise Interrupt); 119 if Eval.sigint_set c then loop (Exit.zero c) 120 else begin 121 Fmt.pr "\n%!"; 122 loop (Exit.nonzero c 130) 123 end 124 in 125 loop initial_ctx 126end