open Common
module Literal = struct
type _ t =
| Int: int -> int t
| String: string -> string t
| Bool: bool -> bool t
let to_shell: type a. a t -> string =
function
| Int i -> sprintf "%d" i
| String s ->
with_buffer begin fun str ->
String.iter s ~f:(fun c ->
Char.code c |> sprintf "%03o" |> str
);
end |> fst
| Bool true -> "true"
| Bool false -> "false"
module String = struct
let easy_to_escape s =
String.for_all s
~f:(function
| 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '-' | '_' | '*' | '&' | '^'
| '=' | '+' | '%' | '$' | '"' | '\'' | '/' | '#' | '@' | '!' | ' '
| '~' | '`' | '\\' | '|' | '?' | '>' | '<' | '.' | ',' | ':' | ';'
| '{' | '}' | '(' | ')' | '[' | ']' -> true
| other -> false)
let impossible_to_escape_for_variable = String.exists ~f:((=) '\x00')
end
end
type fd_redirection = {
take: int t;
redirect_to: [ `Path of string t | `Fd of int t (* | `Input_of of unit t *) ];
}
and _ t =
| Exec: string t list -> unit t
| Raw_cmd: string -> 'a t
| Bool_operator: bool t * [ `And | `Or ] * bool t -> bool t
| String_operator: string t * [ `Eq | `Neq ] * string t -> bool t
| Not: bool t -> bool t
| Returns: {expr: 'a t; value: int} -> bool t
| No_op: unit t
| If: bool t * unit t * unit t -> unit t
| Seq: unit t list -> unit t
| Literal: 'a Literal.t -> 'a t
| Output_as_string: unit t -> string t
| Redirect_output: unit t * fd_redirection list -> unit t
| Write_output: {
expr: unit t;
stdout: string t option;
stderr: string t option;
return_value: string t option;
} -> unit t
| Feed: string t * unit t -> unit t
| Pipe: unit t list -> unit t
| While: {condition: bool t; body: unit t} -> unit t
| Fail: unit t
| Int_to_string: int t -> string t
| String_to_int: string t -> int t
| Bool_to_string: bool t -> string t
| String_to_bool: string t -> bool t
| List_to_string: 'a list t * ('a t -> string t) -> string t
| String_to_list: string t * (string t -> 'a t) -> 'a list t
| List: 'a t list -> 'a list t
| String_concat: string list t -> string t
| List_append: ('a list t * 'a list t) -> 'a list t
| List_iter: 'a list t * ((unit -> 'a t) -> unit t) -> unit t
| Int_bin_op:
int t * [ `Plus | `Minus | `Mult | `Div | `Mod ] * int t -> int t
| Int_bin_comparison:
int t * [ `Eq | `Ne | `Gt | `Ge | `Lt | `Le ] * int t -> bool t
| Getenv: string t -> string t
| Setenv: string t * string t -> unit t
| With_signal: {
signal_name: string;
catch: unit t;
run: unit t -> unit t;
} -> unit t
module Construct = struct
let exec l = Exec (List.map l ~f:(fun s -> Literal (Literal.String s)))
let call l = Exec l
let (&&&) a b = Bool_operator (a, `And, b)
let (|||) a b = Bool_operator (a, `Or, b)
let (=$=) a b = String_operator (a, `Eq, b)
let (<$>) a b = String_operator (a, `Neq, b)
let returns expr ~value = Returns {expr; value}
let succeeds expr = returns expr ~value:0
let nop = No_op
let if_then_else a b c = If (a, b, c)
let if_then a b = if_then_else a b nop
let seq l = Seq l
let not t = Not t
let with_signal ?(signal_name = "USR2") ~catch run =
With_signal {signal_name; catch; run}
let fail = Fail
let make_switch: type a. (bool t * unit t) list -> default: unit t -> unit t =
fun conds ~default ->
List.fold_right conds ~init:default ~f:(fun (x, body) prev ->
if_then_else x body prev)
let write_output ?stdout ?stderr ?return_value expr =
Write_output {expr; stdout; stderr; return_value}
let write_stdout ~path expr = write_output expr ~stdout:path
let to_fd take fd = { take; redirect_to = `Fd fd }
let to_file take file = { take; redirect_to = `Path file }
let with_redirections cmd l =
Redirect_output (cmd, l)
let literal l = Literal l
let string s = Literal.String s |> literal
let int s = Literal.Int s |> literal
let bool t = Literal.Bool t |> literal
let file_exists p =
call [string "test"; string "-f"; p] |> succeeds
let getenv v = Getenv v
let setenv ~var v = Setenv (var, v)
let output_as_string e = Output_as_string e
let feed ~string e = Feed (string, e)
let (>>) string e = feed ~string e
let pipe l = Pipe l
let (||>) a b = Pipe [a; b]
let loop_while condition ~body = While {condition; body}
let list l = List l
let string_concat_list l = String_concat l
let list_append la lb = List_append (la, lb)
let list_iter l ~f = List_iter (l, f)
let list_to_string l ~f = List_to_string (l, f)
let list_of_string l ~f = String_to_list (l, f)
module Bool = struct
let of_string s = String_to_bool s
let to_string b = Bool_to_string b
end
module Integer = struct
let to_string i = Int_to_string i
let of_string s = String_to_int s
let bin_op a o b = Int_bin_op (a, o, b)
let add a b = bin_op a `Plus b
let (+) = add
let sub a b = bin_op a `Minus b
let (-) = sub
let mul a b = bin_op a `Mult b
let ( * ) = mul
let div a b = bin_op a `Div b
let (/) = div
let modulo a b = bin_op a `Mod b
let (mod) = modulo
let cmp op a b = Int_bin_comparison (a, op, b)
let eq = cmp `Eq
let ne = cmp `Ne
let lt = cmp `Lt
let le = cmp `Le
let ge = cmp `Ge
let gt = cmp `Gt
let (=) = eq
let (<>) = ne
let (<) = lt
let (<=) = le
let (>=) = ge
let (>) = gt
end
module Magic = struct
let unit s : unit t = Raw_cmd s
end
end
type output_parameters = {
statement_separator: string;
die_command: (string -> string) option;
}
let rec to_shell: type a. _ -> a t -> string =
fun params e ->
let continue e = to_shell params e in
let seq =
function
| [] -> ":"
| l -> String.concat ~sep:params.statement_separator l in
let die s =
match params.die_command with
| Some f -> f s
| None ->
ksprintf failwith
"Die command not set: you cannot use the `fail` construct \
together with the `~no_trap:true` option (error message was: %S)" s
in
let expand_octal s =
sprintf
{sh| printf -- "$(printf -- '%%s' %s | sed -e 's/\(.\{3\}\)/\\\1/g')" |sh}
s in
let to_argument varprefix =
let argument ?declaration ?variable_name argument =
object
method declaration = declaration
method export = Option.map ~f:(sprintf "export %s ; ") declaration
method variable_name = variable_name
method argument = argument
end in
function
| `String (Literal (Literal.String s)) when Literal.String.easy_to_escape s ->
argument (Filename.quote s)
| `String (Literal (Literal.String s)) when
Literal.String.impossible_to_escape_for_variable s ->
ksprintf failwith "to_shell: sorry literal %S is impossible to \
escape as `exec` argument" s
| `String v ->
let variable_name = Unique_name.variable varprefix in
let declaration =
sprintf "%s=$(%s; printf 'x')"
variable_name (continue v |> expand_octal)
in
argument ~variable_name ~declaration
(sprintf "\"${%s%%?}\"" variable_name)
| `Int (Literal (Literal.Int s)) -> argument (Int.to_string s)
| `Int other ->
let variable_name = Unique_name.variable varprefix in
let declaration = sprintf "%s=%s" variable_name (continue other) in
argument ~variable_name ~declaration
(sprintf "\"${%s%%?}\"" variable_name)
in
match e with
| Exec l ->
let variables = ref [] in
let args =
List.mapi l ~f:(fun index v ->
let varname = sprintf "argument_%d" index in
let arg = to_argument varname (`String v) in
match arg#declaration with
| None -> arg#argument
| Some vardef ->
variables := sprintf "%s ; " vardef :: !variables;
arg#argument) in
(List.rev !variables) @ args
|> String.concat ~sep:" "
|> sprintf " { %s ; } "
| Raw_cmd s -> s
| Returns {expr; value} ->
sprintf " { %s ; [ $? -eq %d ] ; }" (continue expr) value
| Bool_operator (a, op, b) ->
sprintf "{ %s %s %s ; }"
(continue a)
(match op with `And -> "&&" | `Or -> "||")
(continue b)
| String_operator (a, op, b) ->
sprintf "[ \"%s\" %s \"%s\" ]"
(continue a)
(match op with `Eq -> "=" | `Neq -> "!=")
(continue b)
| No_op -> ":"
| If (c, t, e) ->
seq [
sprintf "if { %s ; }" (continue c);
sprintf "then %s" (continue t);
sprintf "else %s" (continue e);
"fi";
]
| While {condition; body} ->
seq [
sprintf "while { %s ; }" (continue condition);
sprintf "do %s" (continue body);
"done"
]
| Seq l -> seq (List.map l ~f:continue)
| Not t ->
sprintf "! { %s ; }" (continue t)
| Redirect_output (unit_t, redirections) ->
(*
We're here compiling the redirections into `exec` statements which
set up global redirections; we limit their scope with `( .. )`.
E.g.
( exec 3>/tmp/output-of-ls ; exec 2>&3 ; exec 1>&2 ; ls ; ) ;
*)
let make_redirection { take; redirect_to } =
let takearg = to_argument "redirection_take" (`Int take) in
let retoarg =
to_argument "redirection_to"
(match redirect_to with `Fd i -> `Int i | `Path p -> `String p) in
let variables =
takearg#export :: retoarg#export :: [] |> List.filter_opt in
let exec =
sprintf "\"exec %%s>%s%%s\" %s %s"
(match redirect_to with `Fd _ -> "&" | `Path _ -> "")
takearg#argument
retoarg#argument
in
sprintf "%s eval \"$(printf -- %s)\" || { echo 'Exec %s failed' >&2 ; } "
(String.concat variables ~sep:"")
exec
exec
in
begin match redirections with
| [] -> continue unit_t
| one :: more ->
continue (Seq (
Raw_cmd (sprintf "( %s" (make_redirection one))
::
List.map more ~f:(fun r -> Raw_cmd (make_redirection r))
@ [unit_t]
@ [ Raw_cmd ")" ]
))
end
| Write_output { expr; stdout; stderr; return_value } ->
let ret_arg =
Option.map return_value ~f:(fun v -> to_argument "retval" (`String v))
in
let var =
Option.((ret_arg >>= fun ra -> ra#export) |> value ~default:"") in
let with_potential_return =
sprintf "%s { %s %s ; }" var (continue expr)
(Option.value_map ret_arg ~default:"" ~f:(fun r ->
sprintf "; printf -- \"$?\" > %s" r#argument))
in
let redirections =
let make fd =
Option.map
~f:(fun p -> {take = Construct.int fd; redirect_to = `Path p}) in
[make 1 stdout; make 2 stderr] |> List.filter_opt in
continue (Redirect_output (Raw_cmd with_potential_return, redirections))
| Literal lit ->
Literal.to_shell lit
| Output_as_string e ->
sprintf "\"$( { %s ; } | od -t o1 -An -v | tr -d ' \\n' )\"" (continue e)
| Int_to_string i ->
continue (Output_as_string (Raw_cmd (sprintf "printf -- '%%d' %s" (continue i))))
| String_to_int s ->
let var = Unique_name.variable "string_to_int" in
let value = sprintf "\"$%s\"" var in
(* We put the result of the string expression in a variable to
evaluate it once; then we test that the result is an integer
(i.e. ["test ... -eq ...."] parses it as an integer). *)
sprintf " $( %s=$( %s ) ; if [ %s -eq %s ] ; then printf -- %s ; else %s ; fi ; ) "
var
(continue s |> expand_octal)
value value value
(die (sprintf "String_to_int: error, $%s is not an integer" var))
| Bool_to_string b ->
continue (Output_as_string (Raw_cmd (sprintf "printf -- '%s'"
(continue b))))
| String_to_bool s ->
continue (
If (
(String_operator (s, `Eq, Literal (Literal.String "true"))),
(Raw_cmd "true"),
If (
(String_operator (s, `Eq, Literal (Literal.String "false"))),
(Raw_cmd "false"),
(Fail))
)
)
| List l ->
(* Lists are newline-separated internal represetations,
prefixed by `G`. *)
let output o = sprintf "printf -- 'G%%s' \"%s\"" (continue o) in
let outputs = List.map l ~f:output in
let rec build =
function
| [] -> []
| [one] -> [one]
| one :: two :: t ->
one :: "printf -- '\\n'" :: build (two :: t)
in
(seq (build outputs))
| List_to_string (l, f) ->
continue (Output_as_string (Raw_cmd (continue l)))
| String_to_list (s, f) ->
continue s |> expand_octal |> sprintf "printf -- '%%s' \"$(%s)\""
| String_concat sl ->
let outputing_list = continue sl in
sprintf "$( { %s ; } | tr -d 'G\\n' )" outputing_list
| List_append (la, lb) ->
seq (continue la :: "printf -- '\\n'" :: continue lb :: [])
| List_iter (l, f) ->
let variter = Unique_name.variable "list_iter_var" in
let varlist = Unique_name.variable "list_iter_list" in
let outputing_list = continue l in
seq [
sprintf "export %s=\"$(%s)\" " varlist outputing_list;
sprintf "for %s in ${%s} " variter varlist;
"do : "; (* we cannot put a `;` after do so the first command is no-op *)
continue (f (fun () ->
(* Here we remove the `G` from the internal represetation: *)
Raw_cmd (sprintf "${%s#G}" variter)));
"done";
]
| Int_bin_op (ia, op, ib) ->
sprintf "$(( %s %s %s ))"
(continue ia)
begin match op with
| `Div -> "/"
| `Minus -> "-"
| `Mult -> "*"
| `Plus -> "+"
| `Mod -> "%"
end
(continue ib)
| Int_bin_comparison (ia, op, ib) ->
sprintf "[ %s %s %s ]"
(continue ia)
begin match op with
| `Eq -> "-eq"
| `Ge -> "-ge"
| `Gt -> "-gt"
| `Le -> "-le"
| `Lt -> "-lt"
| `Ne -> "-ne"
end
(continue ib)
| Feed (string, e) ->
sprintf {sh| %s | %s |sh}
(continue string |> expand_octal) (continue e)
| Pipe [] -> ":"
| Pipe l ->
sprintf " %s "
(List.map l ~f:continue |> String.concat ~sep:" | ")
| Getenv s ->
let var = Unique_name.variable "getenv" in
let value = sprintf "\"$%s\"" var in
let cmd_outputs_value =
(* We need to get the output of the `string t` and then do a `$`
on it:
f () { printf "HOME" ;}
aa=$(printf "\${%s}" $(f)) ; eval "printf \"$aa\""
And the `` | tr -d '\\n' `` part is because `\n` in the variable name
just “cuts” it, it wouldn't fail and `${HOME\nBOUH}` would be
equal to `${HOME}`
*)
sprintf "{ %s=$(printf \\\"\\${%%s}\\\" $(%s | tr -d '\\n')) ; eval \"printf -- '%%s' %s\" ; } "
var (continue s |> expand_octal) value in
continue (Output_as_string (Raw_cmd cmd_outputs_value))
| Setenv (variable, value) ->
sprintf "export $(%s)=\"$(%s)\""
(continue variable |> expand_octal)
(continue value |> expand_octal)
| With_signal {signal_name; catch; run} ->
let var = Unique_name.variable "with_signal" in
let value = sprintf "\"$%s\"" var in
continue Construct.(seq [
Raw_cmd (sprintf "export %s=$$" var);
exec ["trap"; continue catch; signal_name];
exec [
(* We need the `sh -c ...` in order to properly create a subprocess,
if not we break when `With_signal` are enclosed, the kill
command does not wake up the right process. *)
"sh"; "-c";
run (Raw_cmd (sprintf " kill -s %s %s ; kill $$ " signal_name value))
|> continue
];
])
| Fail -> die "EDSL.fail called"
(*
POSIX does not have ["set -o pipefail"].
We implement it by killing the toplevel process with SIGUSR1, then we use
["trap"] to choose the exit status.
*)
let with_trap ~statement_separator ~exit_with script =
let variable_name = Unique_name.variable "genspio_trap" in
let die s =
sprintf " { printf -- '%%s\\n' \"%s\" >&2 ; kill -s USR1 ${%s} ; } " s variable_name in
String.concat ~sep:statement_separator [
sprintf "export %s=$$" variable_name;
sprintf "trap 'exit %d' USR1" exit_with;
script ~die;
]
let compile ~statement_separator ?(no_trap = false) e =
match no_trap with
| false ->
with_trap ~statement_separator ~exit_with:77
(fun ~die -> to_shell {statement_separator; die_command = Some die} e)
| true ->
to_shell {statement_separator; die_command = None} e
let to_one_liner ?no_trap e =
let statement_separator = " ; " in
compile ~statement_separator ?no_trap e
let to_many_lines ?no_trap e =
let statement_separator = " \n " in
compile ~statement_separator ?no_trap e