123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395(*
Copyright 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2017 Anton Lavrik
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*)(*
* Interpreting command-line arguments as Piq data
*)(* @doc
Piqi getopt uses different option syntax than Posix/GNU getopt, because their
syntax is way too relaxed and imprecise. These are examples of GNU getopt
options and their possible meanings:
--c-long=10 // c-long = 10
-c 10 // c, 10
-c10 // c = 10
-ac10 // a, c = 10
-ca10 // c = a10
In Piqi getopt, both short and long options are supported. Both type of
options must be seprated from a value by whitespace, e.g.
-c 10
--c-long 10
Short options start with '-' character followed by one or more letters. In
the latter case, each letter is treated as if it was specified separaterly.
For example,
-abc 10
is equivalent to
-a -b -c 10
'-' followed by a <number> is normally treated as a negative number, e.g.
-10
-0.nan
-0.0
-0.infinity
Words will be treated either as Piq strings or binaries or words, depending
on the expected type. Examples of words:
a
foo
Strings or binaries can be specified explicitly using Piq string syntax.
'"a"'
'"foo\u0000"'
'"\x00\n\r"'
Lists can be specified using regular Piq syntax, but '[' and ']' characters
can be specified as separate arguments and not as a part of other arguments.
Examples:
[]
[ a b ] // this is correct
[a b] // this is incorrect
[ a b 10 -1 ]
[ a b [ c d ] ]
Values for the arguments that start with '@' character will be loaded from a
file which names follows the '@' character. For example:
@foo // string or binary value will be loaded from file "foo"
TODO: @- // string or binary value will be loaded from stdin
*)moduleC=Piqi_commonopenC(*
* Set "alt-name" fields for Piqi options and fields based on "getopt-name"
* fields provided by user in the Piqi spec.
*
* "alt-name" field is specific to the library implementation while
* "getopt-name" field is a part of public Piqi specification.
*)letcheck_getopt_letters=leterrorerr=errors("invalid getopt-letter "^U.quotes^": "^err)in(* NOTE: getopt-letter is a Piq word and, therefore, it can't be empty -- so
* there's no need to check for that *)ifString.lengths>1thenerror"must contain exactly one letter";matchs.[0]with|'a'..'z'|'A'..'Z'->()|c->error"must be lower- or upper-case alphabet letter"letgetopt_name_fieldx=letopenFieldinletletter=x.getopt_letterinmatchletterwith|None->()|Somen->check_getopt_lettern;x.piq_alias<-letterletgetopt_name_optionx=letopenOptioninletletter=x.getopt_letterinmatchletterwith|None->()|Somen->check_getopt_lettern;x.piq_alias<-letter(* name fields and options *)letgetopt_name_recordx=List.itergetopt_name_fieldx.R.fieldletgetopt_name_variantx=List.itergetopt_name_optionx.V.optionletgetopt_name_enumx=List.itergetopt_name_optionx.E.optionletgetopt_name_typedef=function|`recordx->getopt_name_recordx|`variantx->getopt_name_variantx|`enumx->getopt_name_enumx|_->()letgetopt_name_defsdefs=(* name fields and options *)List.itergetopt_name_typedefdefsletgetopt_name_piqi_idtable(piqi:T.piqi)=letopenPingetopt_name_defspiqi.resolved_typedef(* NOTE: this function is called only in case if a getopt-related operation is
* performed (e.g. "piqi getopt" or "piqi call". We don't need this startup
* overhead otherwise *)letinit()=trace"init getopt\n";Piqi.register_processing_hookgetopt_name_piqi(**)(* fake filename for error reporting *)letgetopt_filename="argv"leterrors=(* using fake location here, the actual location (i.e. the index of the
* argument) will be correctly provided by the exception handler below *)letloc=(0,0)inraise(Piq_lexer.Error(s,loc))letparse_string_args=letlexbuf=Piq_lexer.init_from_stringsinlettoken()=tryPiq_lexer.tokenlexbufwithPiq_lexer.Error(err,_loc)->error(err^": "^s)inletres=token()inmatchreswith|Piq_lexer.String_->(* there must be no other literal after the string *)iftoken()=Piq_lexer.EOFthenreselse(* s is alread quoted *)error("trailing characters after string: "^s)|_->assertfalse(* something that starts with '"' have to be a string *)letparse_word_args=ifPiq_lexer.is_valid_wordsthenPiq_lexer.Wordselse(* Raw string -- just a sequence of bytes: may be parsed as binary or utf8
* string *)Piq_lexer.Raw_stringsletparse_name_args=(* cut the leading '-' and check if what we got is a valid Piq name *)letn=String.subs1(String.lengths-1)inifPiqi_name.is_valid_namen~allow:"."then(lets=Bytes.of_stringsinBytes.sets0'.';(* replace '-' with '.' to turn it into a Piq name *)Piq_lexer.Name(Bytes.unsafe_to_strings))elseerror("invalid name: "^U.quotes)letread_filefilename=letch=open_in_binfilenameinletlen=in_channel_lengthchinletbuf=Buffer.createleninBuffer.add_channelbufchlen;close_inch;Buffer.contentsbufletread_filefilename=tryread_filefilenamewithSys_errors->error("error reading file argument: "^s)letparse_args=letlen=String.lengthsinmatchswith(* NOTE: we don't support '(' and ')' and '[]' is handeled separately below *)|"["->Piq_lexer.Lbr|"]"->Piq_lexer.Rbr|swhens.[0]='"'->parse_string_args|swhens.[0]='@'->letfilename=String.subs1(len-1)inletcontent=read_filefilenamein(* Raw string -- just a sequence of bytes: may be parsed as either
* binary or utf8 string *)Piq_lexer.Raw_stringcontent(* parsing long options starting with "--"
*
* NOTE: it is safe to check s.[1] because a single '-' case is eliminated
* in the calling function *)|swhens.[0]='-'&&s.[1]='-'->letname=String.subs1(len-1)in(* skip first '-' *)parse_name_argname|swhens.[0]='.'->parse_name_args(* XXX: allowing Piq -style names *)(* XXX: support typenames and, possibly, other literals? *)|s->parse_word_argsletparse_argvstart=leterrorierr=C.error_at(getopt_filename,0,i)errinletmake_tokenitok=(* 1-based token position in the argv starting from the position after "--" *)letloc=(0,i-start+1)in(tok,loc)inletparse_make_argix=lettok=tryparse_argxwithPiq_lexer.Error(err,_loc)->errorierrinmake_tokenitokinletparse_letter_argsis=letlen=String.lengthsinletrecauxj=ifj=lenthen[](* end of string *)elseletc=s.[j]inmatchcwith(* only letters are allowed as single-letter options *)|'a'..'z'|'A'..'Z'->(* creating Piq name: '.' followed by the letter *)letword=Bytes.create2inBytes.setword0'.';Bytes.setword1c;lettok=Piq_lexer.Name(Bytes.unsafe_to_stringword)in(make_tokenitok)::(aux(j+1))|_->errori("invalid single-letter argument: "^Char.escapedc)inaux1(* start at position 1 skipping the leading '-' *)inletlen=Array.lengthSys.argvinletrecauxi=ifi>=lenthen[make_tokeniPiq_lexer.EOF]elseleta=Sys.argv.(i)inmatchawith|""->errori"empty argument"|"-"|"--"->errori("invalid argument: "^a)|"[]"->(* split it into two tokens '[' and ']' *)(parse_make_argi"[")::(parse_make_argi"]")::(aux(i+1))(* After skipping negative integers, and those arguments that start with
* '--', we end up having '-' followed by one or more characters. We
* treat those characters as single-letter arguments.
*
* NOTE: it is safe to check s.[1] because a single '-' case is
* eliminated above *)|swhens.[0]='-'&&s.[1]<>'-'&&(s.[1]<'0'||s.[1]>'9')->(parse_letter_argsis)@(aux(i+1))|s->(parse_make_argis)::(aux(i+1))inauxstart(* index of the "--" element in argv array *)letargv_start_index=ref0(* find the position of the first argument after "--" *)letrest_funarg=if!argv_start_index=0(* first argument after first occurrence of "--" *)thenargv_start_index:=!Arg.current+1else()letarg__rest="--",Arg.Restrest_fun,"separator between piqi command-line arguments and data arguments"letgetopt_piq():piq_astlist=letstart=if!argv_start_index=0(* "--" is not present in the list of arguments *)thenArray.lengthSys.argvelse!argv_start_indexinlettokens=parse_argvstartinletpiq_parser=Piq_parser.init_from_token_listgetopt_filenametokensinletpiq_ast_list=U.with_boolConfig.piq_relaxed_parsingtrue(fun()->Piq_parser.read_allpiq_parser)inpiq_ast_listletparse_args(piqtype:T.piqtype)(args:piq_astlist):Piqobj.obj=letast=matchargswith|[x]whennot(C.is_container_typepiqtype)->(* scalar type? *)x|l->letres=`listlin(* set the location *)letloc=(getopt_filename,0,1)inPiqloc.addlocretlocresinletast=Piq_parser.expandastinletpiqobj=U.with_boolConfig.piq_relaxed_parsingtrue(fun()->Piqobj_of_piq.parse_objpiqtypeast)inpiqobj