123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345openCommontype'at='aLanguage.ttypec_string=Language.c_stringtypebyte_array=Language.byte_arraytypefd_redirection=Language.fd_redirectionlet(//)=Caml.Filename.concatincludeLanguage.Constructletcaseconditionbody=`Case(condition,seqbody)letdefaultd=`Default(seqd)letswitchl=letdefault=refNoneinletcases=List.filter_mapl~f:(function|`Default_whenPoly.(!default<>None)->failwith"Cannot build switch with >1 defaults"|`Defaultd->default:=Somed;None|`Caset->Somet)inmake_switch~default:(Option.value~default:nop!default)cases(*
let string_list_to_string l =
Elist.to_string ~f:(fun e -> to_byte_array e) l |> to_c_string
let string_list_of_string s =
Elist.of_string ~f:(fun e -> to_c_string e) (to_byte_array s)
*)typefile=<get:byte_arrayt;get_c:c_stringt;set:byte_arrayt->unitt;set_c:c_stringt->unitt;append:byte_arrayt->unitt;delete:unitt;path:c_stringt>lettmp_file?tmp_dirname:file=letdefault_tmp_dir="/tmp"inletget_tmp_dir=Option.valuetmp_dir~default:(get_stdout((* https://en.wikipedia.org/wiki/TMPDIR *)if_then_elseC_string.(getenv(c_string"TMPDIR")<$>c_string"")(call[c_string"printf";c_string"%s";getenv(c_string"TMPDIR")])(exec["printf";"%s";default_tmp_dir]))|>to_c_string)inletpath=letclean=String.mapname~f:(function|('a'..'z'|'A'..'Z'|'0'..'9'|'_'|'-')asc->c|_->'_')inC_string.concat_list[get_tmp_dir;c_string"/";c_string(Fmt.str"genspio-tmp-file-%s-%s"cleanCaml.Digest.(stringname|>to_hex))]inlettmp=C_string.concat_list[path;string"-tmp"]inobject(self)methodget=get_stdout(call[string"cat";path])methodget_c=self#get|>to_c_stringmethodpath=pathmethodsetv=seq[(* call [string "echo"; string "Setting "; string name]; *)(* call [string "echo"; string "Setting "; path; string " to "; v]; *)(* call [string "echo"; tmp]; *)v>>exec["cat"]|>write_output~stdout:tmp;call[string"mv";string"-f";tmp;path]]methodset_cc=self#set(to_byte_arrayc)methodappendv=seq[seq[call[string"cat";path];v>>exec["cat"]]|>write_output~stdout:tmp;call[string"mv";string"-f";tmp;path]]methoddelete=call[string"rm";string"-f";path;tmp]endletif_seq~t?ec=matchewith|None->if_thenc(seqt)|Somef->if_then_elsec(seqt)(seqf)letprintffmtl=call(string"printf"::string"--"::fmt::l)leteprintffmtl=with_redirections(printffmtl)[to_fd(int1)(int2)]moduleCommand_line=structtype'acli_option={switches:stringlist;doc:string;default:'a}type_option_spec=|Opt_flag:booltcli_option->booltoption_spec|Opt_string:c_stringtcli_option->c_stringtoption_specand(_,_)cli_options=|Opt_end:string->('a,'a)cli_options|Opt_cons:'coption_spec*('a,'b)cli_options->('c->'a,'b)cli_optionsmoduleArg=structletstring?(default=string"")~docswitches=Opt_string{switches;doc;default}letflag?(default=boolfalse)~docswitches=Opt_flag{switches;doc;default}let(&)xy=Opt_cons(x,y)letusages=Opt_endsendletparse(options:('a,unitt)cli_options)(action:anon:c_stringlistt->'a):unitt=letprefix=Common.Unique_name.variable"getopts"inletvariable{switches;_}=Fmt.str"%s_%s"prefix(String.concat~sep:""switches|>Caml.Digest.string|>Caml.Digest.to_hex)inletinits=ref[]inletto_inits=inits:=s::!initsinletcases=ref[]inletto_cases=cases:=s::!casesinlethelp_intro=ref""inlethelp=ref[]inletto_helps=help:=s::!helpinletstring_of_varvar=getenv(stringvar)inletbool_of_varvar=getenv(stringvar)|>Bool.of_stringinletanon_tmp=Fmt.kstrtmp_file"parse-cli-%s"Caml.(Marshal.to_stringoptions[]|>Digest.string|>Digest.to_hex)inletanon=anon_tmp#get|>Elist.deserialize_to_c_string_listinletapplied_action=(*
The [loop] function below is building 3 pieces of Genspio code at once:
- variable initializations
- individual case statements (including variable assignments)
that are part of the ["while true { switch { .... } }"] loop
that incrementally interprets each command line argument.
- [applied_action] (of type [unit t]) is the
the result of applying the [action] function to all the elements of
[options] + the list of anonymous arguments.
It is hence the (user-provided) code that uses the parsed arguments.
The [loop] function builds the closure as the loop goes since
[options] is a “difference list”, see also:
{{:https://drup.github.io/2016/08/02/difflists/}Drup's blog post}.
The 2 first items are agglomerated in the [inits] and [cases]
references.
*)letrecloop:typeab.a->(a,b)cli_options->b=funf->function|Opt_enddoc->help_intro:=doc;f|Opt_cons(Opt_stringx,more)->letvar=variablexinto_init(setenv~var:(stringvar)x.default);to_case(case(List.fold~init:(boolfalse)x.switches~f:(funps->p|||C_string.(c_strings=$=getenv(c_string"1"))))[if_seqC_string.(getenv(string"2")=$=string"")~t:[eprintf(string"ERROR option '%s' requires an argument\\n")[getenv(string"1")];fail"Wrong command line"]~e:[setenv~var:(stringvar)(getenv(string"2"))];exec["shift"];exec["shift"]]);Fmt.kstrto_help"* `%s <string>`: %s"(String.concat~sep:","x.switches)x.doc;loop(f(string_of_varvar))more|Opt_cons(Opt_flagx,more)->letvar=variablexinto_init(setenv~var:(stringvar)(Bool.to_stringx.default));to_case(case(List.fold~init:(boolfalse)x.switches~f:(funps->p|||C_string.equals(strings)(getenv(string"1"))))[setenv~var:(stringvar)(Bool.to_string(booltrue));exec["shift"]]);Fmt.kstrto_help"* `%s`: %s"(String.concat~sep:","x.switches)x.doc;loop(f(bool_of_varvar))moreinloop(action~anon)optionsinlethelp_msg=Fmt.str"%s\n\nOptions:\n\n%s\n"!help_intro(String.concat~sep:"\n"(List.rev!help))inlethelp_flag_var=Fmt.kstrstring"%s_help"prefixinletwhile_loop=letbody=letappend_anon_arg_to_list=seq[anon_tmp#set(Elist.append(anon_tmp#get|>Elist.deserialize_to_byte_array_list)(Elist.make[getenv(string"1")|>C_string.to_byte_array])|>Elist.serialize_byte_array_list)]inlethelp_case=lethelp_switches=["-h";"-help";"--help"]incase(List.fold~init:(boolfalse)help_switches~f:(funps->p|||C_string.(c_strings=$=getenv(c_string"1"))))[setenv~var:help_flag_var(Bool.to_string(booltrue));byte_arrayhelp_msg>>exec["cat"];exec["break"]]inletdash_dash_case=caseC_string.(getenv(c_string"1")=$=c_string"--")[exec["shift"];loop_whileC_string.(getenv(c_string"#")<$>c_string"0")~body:(seq[append_anon_arg_to_list;exec["shift"]]);exec["break"]]inletanon_case=caseC_string.(getenv(c_string"#")<$>c_string"0")[append_anon_arg_to_list;exec["shift"]]inletdefault_case=default[exec["break"]]inletcases=(help_case::List.rev!cases)@[dash_dash_case;anon_case;default_case]inseq[switchcases]inloop_while(booltrue)~bodyinseq[setenv~var:help_flag_var(Bool.to_string(boolfalse));anon_tmp#set(Elist.serialize_byte_array_list(Elist.make[]));seq(List.rev!inits);while_loop;if_then_else(bool_of_var(Fmt.str"%s_help"prefix))nopapplied_action]endletloop_until_true?(attempts=20)?(sleep=2)?(on_failed_attempt=funnth->printf(string"%d.")[Integer.to_stringnth])cmd=letintvar=letvarname=string"C_ATTEMPTS"inobjectmethodsetv=setenv~var:varname(Integer.to_stringv)methodget=getenvvarname|>Integer.of_stringendinseq[intvar#set(int1);loop_while(Integer.(intvar#get<=intattempts)&&¬cmd)~body:(seq[on_failed_attemptintvar#get;intvar#setInteger.(intvar#get+int1);if_thenInteger.(intvar#get<=intattempts)(exec["sleep";Fmt.str"%d"sleep])]);exec["printf";"\\n"];if_then_elseInteger.(intvar#get>intattempts)(seq[(* Fmt.str "Command failed %d times!" attempts; *)exec["false"]])(seq[(* Fmt.str "Command failed %d times!" attempts; *)exec["true"]])]|>returns~value:0letsilentlyu=letdev_null=string"/dev/null"inwrite_output~stdout:dev_null~stderr:dev_nulluletsucceeds_silentlyu=silentlyu|>succeedsletseq_andl=List.foldl~init:(booltrue)~f:(funuv->u&&&succeedsv)letoutput_markdown_codetagf=seq[exec["printf";Fmt.str"``````````%s\\n"tag];f;exec["printf";Fmt.str"\\n``````````\\n"]]letcat_markdowntagfile=output_markdown_codetag@@call[string"cat";file]letfresh_namesuf=letx=objectmethodv=42endinFmt.str"g-%d-%d-%s"(Caml.Oo.idx)(Random.int100_000)sufletsanitize_namen=String.mapn~f:(function|('0'..'9'|'a'..'z'|'A'..'Z'|'-')asc->c|_->'_')letdefault_on_failure~step:(i,_)~stdout~stderr=seq[printf(Fmt.kstrc_string"Step '%s' FAILED:\\n"i)[];cat_markdown"stdout"stdout;cat_markdown"stderr"stderr;exec["false"]]letcheck_sequence?(verbosity=`Announce">> ")?(on_failure=default_on_failure)?(on_success=fun~step:_~stdout:_~stderr:_->nop)?(tmpdir="/tmp")cmds=lettmp_prefix=fresh_name"-cmd"inlettmpoutwhichid=c_string(tmpdir//Fmt.str"genspio-check-sequence-%s-%s-%s"tmp_prefixwhich(sanitize_nameid))inletstdoutid=tmpout"stdout"idinletstderrid=tmpout"stderr"idinletlogidu=matchverbositywith|`Silent->write_output~stdout:(stdoutid)~stderr:(stderrid)u|`Announceprompt->seq[printf(Fmt.kstrc_string"%s %s\\n"promptid)[];write_output~stdout:(stdoutid)~stderr:(stderrid)u]|`Output_all->uinletcheckidx(nam,u)next=letid=Fmt.str"%d. %s"idxnaminif_seq(logidu|>succeeds)~t:[on_success~step:(id,u)~stdout:(stdoutid)~stderr:(stderrid);next]~e:[on_failure~step:(id,u)~stdout:(stdoutid)~stderr:(stderrid)]inletrecloopi=function|one::more->checkione(loop(i+1)more)|[]->exec["true"]inloop1cmdsleton_stdin_linesbody=letfresh=Common.Unique_name.variable"read_stdin"inloop_while(exec["read";"-r";fresh]|>succeeds)~body:(seq[exec["export";fresh];body(getenv(stringfresh))])