Parsing one's own command line

I am writing a program that has its own command line. Are there libraries to parse such custom command lines ? Or is there a way to use the usual command line parsing libraries (Arg, Cmdliner) to that effect ?

Maybe I am not being very clear… An example would be to program a shell in OCaml. I found the example GitHub - jsthomas/simple-ocaml-shell: Build a simple shell in OCaml. To parse its own command line, it uses Angstrom. Would there be a way to use, say, Cmdliner in that project instead ? Or is it anyway better to build your own parser for your own command line, for flexibility reasons ?

Also, if you know some examples, more developed than the one above, to take inspiration from, I’d be glad to be pointed to them.

Maybe you’re asking a different question? I think you’re asking: “are there libraries to aid in parsing languages that look like shell-languages?” That is to say, you want to write an interpreter for a language that looks like bash’s language, and are looking for what libraries might exist to help do it?

This is very different from using Arg/Cmdliner, b/c the language of bash (and of any shell) will have expressions that need to be interpreted before the resulting “value” (a list of strings) will be handed to exec(3). And I’d guess the answer is that nobody has done that, but it’d be a nice language project.

BTW, a few years ago there was also cash: GitHub - momohatt/cash: a toy shell written in OCaml 🐫🐚

1 Like

I’m also not entirely sure what you’re asking, but if the question is how to use Cmdliner with custom input, you can do something like the following:

open Cmdliner

let run opt_a opt_b =
    if opt_a then (
        print_endline "OPTION A"
    );
    if opt_b then (
        print_endline "OPTION B"
    );
    `Ok ()

let option_a =
    let doc = "this is option a" in
    Arg.(value & flag & info ["a"] ~doc)

let option_b =
    let doc = "this is option b" in
    Arg.(value & flag & info ["b"] ~doc)

let () =
    let doc = "Some command" in
    let info = Cmd.info "cmd" ~doc in
    let cmd = Cmd.v info Term.(ret (const run $ option_a $ option_b)) in

    while true do
        Printf.printf "> %!";
        let line = input_line stdin in
        let argv = String.split_on_char ' ' line |> Array.of_list in
        Printf.printf "Result %d\n\n%!" (Cmd.eval cmd ~argv)
    done

Then when you run the program you can do this:

$ ./_build/default/bin/main.exe 
> cmd -a
OPTION A
Result 0

> cmd -b
OPTION B
Result 0

> cmd -a -b
OPTION A
OPTION B
Result 0
1 Like

Thanks @Chet_Murthy and @OCamlUser. Both answers are useful, and I think that @OCamlUser answers my question (sorry for its vagueness…). I think I can replace in your example “while true” by

    Printf.printf "> %!";
    let line = ref (input_line stdin) in
    while not (!line = "exit") do
        Printf.printf "> %!";
        line := input_line stdin;
        ...

and work from there on adding new commands like your “cmd”.

Also: the arrows do not work when information is prompted (they write things like “^[[A”). Is there a library to make them work as intended (i.e., navigating the current line for left/right and navigating the history for up/down) ?

Yeah that looks like it would work fine. If you want a more “OCaml” solution you can use a recursive function:

  let rec loop () =
      Printf.printf "> %!";
      let line = input_line stdin in
      if line <> "exit" then (
          let argv = String.split_on_char ' ' line |> Array.of_list in
          Printf.printf "Result %d\n\n%!" (Cmd.eval cmd ~argv);
          loop()
      )
  in
  loop()

For arrow key support I would probably use a program called “rlwrap” if you’re on Linux or Mac it should be installable through your favorite package manager. Then you can use it like so:

rlwrap ./_build/default/bin/main.exe

and you’ll have arrows and standard commands like CTRL+A or CTRL+E working.

There are also some OCaml terminal I/O libraries around that you could use to get that stuff working, like GitHub - ocaml-community/lambda-term: Terminal manipulation library for OCaml but I’d say rlwrap is the easiest. I usually make a script that wraps it so I can use a single command to run it. For example in my ~/bin/ folder I’d have a file named “mycommand” with the contents:

#!/bin/sh
rlwrap ~/bin/mycommand.exe

Then I can run mycommand on the terminal and get the nicer input modes.

1 Like

There’s also the old “ledit” by Daniel de Rauglaudre, available via opam. It’s OCaml, so maybe easy to link into a program ? Never tried myself.

1 Like

For an interactive prompt with optional history I found linenoise useful:


let rec repl prompt cb =
  match LNoise.linenoise prompt with
  | None       -> ()
  | Some ""    -> repl prompt cb
  | Some input ->
      cb input;
      ignore @@ LNoise.history_add input;
      repl prompt cb

let main () =
  ignore @@ LNoise.history_set ~max_length:100;
  let calc input =
    try
      let lexbuf = Lexing.from_string input in
      let seconds = Tcalc.Parser.main Tcalc.Lexer.token lexbuf in
      result seconds
    with _ -> Printf.printf "syntax error in \"%s\"\n%!" input
  in
  repl "tcalc> " calc

Full example on github.com/lindig/tcalc.

6 Likes

Oh hm, this looks interesting: GitHub - colis-anr/morbig: A Static Parser for POSIX Shell

1 Like

After rereading your answer and Cmdliner’s help (Cmd (cmdliner.Cmdliner.Cmd)), I see that in the signature of Cmd.eval and related functions,

val eval : ?help:Stdlib.Format.formatter -> ?err:Stdlib.Format.formatter -> ?catch:bool ->
?env:(string -> string option) -> ?argv:string array -> ?term_err:Exit.code -> unit t -> Exit.code

the optional argument ?argv defaults to Sys.argv, and therefore it suffices to specify that argument to turn Cmdliner, usually used as a “the-command-line” parser, to a “custom-command-line” parser. Thanks for the replies in spite of the vague question.