#!/usr/bin/env ocaml

(*****************************************************************************)
(* Prelude *)
(*****************************************************************************)
(* Toy hello-world style script written in OCaml showing it's actually
 * possible to write scripts in OCaml!
 *
 * It also shows how to leverage a few useful libraries and ppx extensions
 * for scripting:
 *  - topfind (to easily load packages)
 *  - ppx_deriving (who doesn't like deriving show?)
 *  - ppx_subliner (cmdliner for mere mortals)
 *  - feather (shell DSL)
 *  - some Semgrep libraries! (I need my Common)
 *
 * usage:
 *   $ ./hello_script.ml --debug world
 *   [DEBUG]: params = { Hello_script.username = "pad"; verbose = true; command = "world" }
 *   [INFO]: Hello world from pad
 *   PAD      1869469  0.0  0.1 194628 32616 PTS/0    SL+  17:03   0:00 /HOME/PAD/.OPAM/4.14.0/BIN/OCAMLRUN /HOME/PAD/.OPAM/4.14.0/BIN/OCAML ./SCRIPTS/HELLO_SCRIPT.ML --VERBOSE WORLD
 *   PAD      1869485  0.0  0.0   6512  2572 PTS/0    S+   17:03   0:00 GREP OCAML
 *
 * alternatives:
 *  - ocamlscript?
 *
 * references:
 *  - https://discuss.ocaml.org/t/how-to-run-ml-ocaml-file-without-compiling/4311/21
 *  - https://discuss.ocaml.org/t/running-ocaml-scripts/10228
 *  - https://discuss.ocaml.org/t/ocaml-scripting-and-ppx-support/10638
 *)

(*****************************************************************************)
(* Imports *)
(*****************************************************************************)

(* This directive allows then to use #require to load opam/findlib packages. *)
#use "topfind"

(* You need first to 'opam install ppx_deriving'. *)
(* TODO: #require "ppx_deriving.show" *)

(* You need first to 'opam install ppx_subliner'. *)
(* alt: use ppx_deriving_cmdliner, available at
 * https://github.com/hammerlab/ppx_deriving_cmdliner
 * which interacts better with ppx_deriving.show but it has not been updated
 * in 2 years and conflicts with the need for a more recent ppxlib by other
 * librairies in Semgrep.
 *)
#require "cmdliner"

#require "ppx_subliner"

#load "unix.cma"

#load "threads/threads.cma"

(* You need first to 'opam install feather'.
 * For some unknown reasons, requiring feather does not load the unix and
 * threads dependencies, so I had to do it manually above.
 *)
#require "feather"

(* commons below uses timedesc, hence the requires below.
 * see https://github.com/daypack-dev/timere#using-timedesc-in-utop *)
#require "timedesc-tzdb.full"

#require "timedesc-tzlocal.unix"

#require "timedesc"

(* This assumes you did a 'make install-semgrep-libs' so that
 * the semgrep libraries are available from topfind.
 *)
#require "commons"

open Common
module Arg = Cmdliner.Arg
module Term = Cmdliner.Term

(* We don't use Cmd here so we can also use semgrep/libs/commons/Cmd.ml *)
module Cmd_ = Cmdliner.Cmd
module F = Feather
open Feather (* for |. *)
open Feather.Infix (* for >, <, ||., etc. *)

(*****************************************************************************)
(* Types *)
(*****************************************************************************)

type conf = {
  username : string; [@default "pad"]
  debug : bool; [@defaul false]
  command : string; [@pos 0] [@doc "CMD"]
}
[@@deriving subliner]
(* TODO: add show too *)

(* Unfortunately, we can't currently use both subliner and show :(
 * See https://github.com/bn-d/ppx_subliner/issues/33
 *)
let show_conf (conf : conf) =
  spf "{username = \"%s\"; debug = %b; command = \"%s\"}" conf.username
    conf.debug conf.command

(*****************************************************************************)
(* Helpers *)
(*****************************************************************************)

(* see https://github.com/charlesetc/feather *)
let simple_shell_programming_demo () =
  let out =
    F.process "ps" [ "-aux" ]
    |. F.map_lines ~f:String.uppercase_ascii
    |. F.process "grep" [ "OCAML" ]
    (* alt: |. F.grep "OCAML" *)
    |> F.collect F.stdout
  in
  UCommon.pr out

(*****************************************************************************)
(* Entry point *)
(*****************************************************************************)

let run (caps : Cap.all_caps) (conf : conf) =
  Logs_.setup ~level:(if conf.debug then Some Logs.Debug else Some Logs.Info) ();
  Logs.debug (fun m -> m "params = %s" (show_conf conf));
  Logs.info (fun m -> m "Hello %s from %s" conf.command conf.username);
  simple_shell_programming_demo ();
  ()

(*****************************************************************************)
(* Cmdliner boilerplate *)
(*****************************************************************************)
(* this is now autogenerated by ppx_subliner *)
(*
let conf_cmdliner_term : conf Term.t =
  let o_username : string Term.t =
    Arg.(value & opt string "pad" & info [ "u"; "user" ] ~doc:"")
  in
  let o_debug : bool Term.t =
    Arg.(value & flag & info [ "debug" ] ~doc:"")
  in
  let o_command : string Term.t =
    Arg.(value & pos 0 string "<nocommand>" & info [] ~docv:"CMD" ~doc:"")
  in
  let combine command debug username  = { username; debug; command } in
  Term.(const combine $ o_command $ o_debug $ o_username)
*)

let main () =
  Cap.main (fun (caps : Cap.all_caps) ->
      let info = Cmd_.info Sys.argv.(0) in
      let term = Term.(const (run caps) $ conf_cmdliner_term ()) in
      let cmd = Cmd_.v info term in
      CapStdlib.exit caps#exit (Cmd_.eval cmd))

let () = main ()
