├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── extraParams.hxml ├── haxelib.json ├── mcli ├── CommandLine.hx ├── Decoder.hx ├── Dispatch.hx ├── DispatchError.hx ├── Types.hx └── internal │ ├── Data.hx │ ├── Macro.hx │ └── Tools.hx ├── samples ├── 00-helloworld │ ├── Test.hx │ └── build.hxml ├── 01-ant │ ├── 01-simple.hxproj │ ├── Test.hx │ └── build.hxml ├── 02-ls │ ├── Test.hx │ └── build.hxml ├── 03-git │ ├── Test.hx │ └── build.hxml └── 04-complex │ ├── Test.hx │ └── build.hxml └── test ├── build.hxml └── src ├── Test.hx └── tests └── McliTests.hx /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | tags 3 | *.n 4 | *.swp 5 | *.swo 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # example travis.yml haxe configuration 2 | language: c # change this to objective-c to test on a mac machine 3 | 4 | env: 5 | global: 6 | # - OS=mac # add this too to let the script know that the OS is a mac 7 | # - ARCH=i686 # add this to run in 32-bit mode. See availability at README 8 | # SAUCE_ACCESS_KEY 9 | - secure: "YOUR_ENCRYPTED_SAUCE_ACCESS_KEY" # if you want to use SauceLabs 10 | # SAUCE_USERNAME 11 | - secure: "YOUR_ENCRYPTED_SAUCE_USERNAME" # if you want to use SauceLabs 12 | matrix: 13 | - TARGET=neko 14 | # optional: FILENAME 15 | - TARGET=interp 16 | - TARGET=macro 17 | # optional: MACROFLAGS: specify the flags that are unique to building/running with --macro arguments 18 | - TARGET=js TOOLCHAIN=default # target is tested by node.js 19 | # optional: FILENAME 20 | # optional: NODECMD: set the command to be run by nodejs 21 | - TARGET=js TOOLCHAIN=browser # target is tested by browsers / phantomjs 22 | # optional: FILENAME 23 | # optional: SAUCE_BROWSERS: specify the .json file that specifies the SauceLabs browsers to test. Defaults to `.sauce-browsers.json` 24 | - TARGET=php 25 | # optional: FILENAME 26 | - TARGET=cpp 27 | # optional: FILENAME 28 | - TARGET=swf 29 | # optional: FILENAME 30 | - TARGET=as3 31 | # optional: FILENAME 32 | - TARGET=java 33 | # optional: FILENAME 34 | - TARGET=cs 35 | # optional: FILENAME 36 | - TARGET=python 37 | # optional: FILENAME 38 | # optional: PYTHONCMD 39 | 40 | matrix: 41 | fast_finish: true 42 | # allow_failures: 43 | # - env: TARGET=python 44 | 45 | before_install: # clone travis-hx repo 46 | - travis_retry git clone --depth=50 --branch=master git://github.com/waneck/travis-hx.git ~/travis-hx 47 | 48 | install: # setup the target 49 | - ~/travis-hx/setup.sh 50 | - haxelib install utest 51 | - haxelib dev mcli $TRAVIS_BUILD_DIR 52 | 53 | script: 54 | - cd $TRAVIS_BUILD_DIR/test 55 | # build the target. This will call haxe with the HXFLAGS and HXFLAGS_EXTRA environment variables 56 | - HXFLAGS="-cp src -main Test -lib utest -lib mcli -D travis" ~/travis-hx/build.sh 57 | # run the tests 58 | - ~/travis-hx/runtests.sh $FILENAME # this will set the $FILENAME defined on the environment variable to run the tests 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/waneck/mcli.svg?branch=master)](https://travis-ci.org/waneck/mcli) 2 | 3 | #mcli 4 | 5 | ## Description 6 | mcli is a simple, opinionated and type-safe way to create command line interfaces. 7 | It will map a class definition into expected command-line arguments in an intuitive and straight-forward way. 8 | 9 | ## Features 10 | * Easy to use 11 | * Public variables and functions become options 12 | * Can work with any type, not just basic types 13 | * Support for Haxe's Map<> for key=value definitions 14 | * Extensible 15 | * Public domain license 16 | 17 | ## Example 18 | 19 | ``` 20 | /** 21 | Say hello. 22 | Example inspired by ruby's "executable" lib example 23 | **/ 24 | class HelloWorld extends mcli.CommandLine 25 | { 26 | /** 27 | Say it in uppercase? 28 | **/ 29 | public var loud:Bool; 30 | 31 | /** 32 | Show this message. 33 | **/ 34 | public function help() 35 | { 36 | Sys.println(this.showUsage()); 37 | Sys.exit(0); 38 | } 39 | 40 | public function runDefault(?name:String) 41 | { 42 | if(name == null) 43 | name = "World"; 44 | var msg = 'Hello, $name!'; 45 | if (loud) 46 | msg = msg.toUpperCase(); 47 | Sys.println(msg); 48 | } 49 | 50 | public static function main() 51 | { 52 | new mcli.Dispatch(Sys.args()).dispatch(new HelloWorld()); 53 | } 54 | 55 | } 56 | ``` 57 | 58 | Compiling this with 59 | ``` 60 | -neko hello.n 61 | -main HelloWorld 62 | -lib mcli 63 | ``` 64 | 65 | Will render the following program: 66 | ``` 67 | $ neko hello 68 | Hello, World! 69 | $ neko hello Caue 70 | Hello, Caue! 71 | $ neko hello --loud Caue 72 | HELLO, CAUE! 73 | $ neko hello Caue --loud 74 | HELLO, CAUE! 75 | ``` 76 | 77 | You can also generate help for commands: 78 | ``` 79 | $ neko hello --help 80 | Say hello. 81 | Example inspired by ruby's `executable` lib example 82 | 83 | --loud Say it in uppercase? 84 | --help Show this message. 85 | ``` 86 | 87 | You can see more complex examples looking at the samples provided 88 | 89 | ## License 90 | As stated above, the mcli library is in public domain 91 | 92 | ## Status 93 | Everything should be working, but it's currently in beta status. 94 | After some tests it will get a major release. You may use github's issues list to add feature requests and bug reports. 95 | Pull requests for new features are welcome, provided it won't hurt backwards compatibility and existing features and follow mcli's minimalist approach 96 | -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | -D use_rtti_doc 2 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcli", 3 | "url": "https://github.com/waneck/mcli", 4 | "license": "Public", 5 | "tags": ["cross","cli"], 6 | "description": "A simple command-line object mapper. See more at https://github.com/waneck/mcli", 7 | "version": "0.1.7", 8 | "releasenote": "Haxe 4 update", 9 | "contributors": ["waneck"] 10 | } 11 | -------------------------------------------------------------------------------- /mcli/CommandLine.hx: -------------------------------------------------------------------------------- 1 | package mcli; 2 | 3 | @:autoBuild(mcli.internal.Macro.build()) 4 | @:keepSub 5 | class CommandLine 6 | { 7 | private var _preventDefault:Bool = false; 8 | 9 | public function new() 10 | { 11 | } 12 | 13 | /** 14 | This function will be auto-generated by the build macro 15 | **/ 16 | public function getArguments():Array 17 | { 18 | return []; 19 | } 20 | 21 | /** 22 | Do not run default (`runDefault`) 23 | **/ 24 | public function preventDefault() 25 | { 26 | this._preventDefault = true; 27 | } 28 | 29 | public function showUsage():String 30 | { 31 | return mcli.Dispatch.showUsageOf(getArguments()); 32 | } 33 | 34 | public function toString():String 35 | { 36 | return showUsage(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mcli/Decoder.hx: -------------------------------------------------------------------------------- 1 | package mcli; 2 | 3 | /** 4 | In order to support custom types, one must provide a custom Decoder implementation 5 | **/ 6 | typedef Decoder = 7 | { 8 | function fromString(s:String):T; 9 | } 10 | -------------------------------------------------------------------------------- /mcli/Dispatch.hx: -------------------------------------------------------------------------------- 1 | package mcli; 2 | import mcli.DispatchError; 3 | import mcli.internal.Data; 4 | #if macro 5 | import haxe.macro.Expr; 6 | import haxe.macro.Type in MType; 7 | import haxe.macro.Context; 8 | import haxe.macro.TypeTools; 9 | #end 10 | #if haxe4 11 | import haxe.Constraints.IMap; 12 | #else 13 | import Map.IMap; 14 | #end 15 | using mcli.internal.Tools; 16 | using Lambda; 17 | 18 | @:access(mcli.CommandLine) class Dispatch 19 | { 20 | 21 | /** 22 | Formats an argument definition to String. 23 | [argSize] maximum argument string length 24 | [screenSize] maxium characters until a line break should be forced 25 | **/ 26 | public static function argToString(arg:Argument, argSize=30, ?screenSize) 27 | { 28 | if (screenSize == null) 29 | screenSize = getScreenSize(); 30 | var postfix = getPostfix(arg); 31 | var versions = getAliases(arg); 32 | 33 | if (versions.length == 0) 34 | if (arg.description != null) 35 | return arg.description; 36 | else 37 | return ""; 38 | versions.sort(function(s1,s2) return Reflect.compare(s1.length, s2.length)); 39 | 40 | var desc = (arg.description != null ? arg.description : ""); 41 | 42 | var ret = new StringBuf(); 43 | ret.add(" "); 44 | var argsTxt = StringTools.rpad(versions.map(function(v) return v).join(", ") + postfix, " ", argSize); 45 | ret.add(argsTxt); 46 | if (argsTxt.length > argSize) 47 | { 48 | ret.add("\n"); 49 | for (i in 0...argSize) 50 | ret.add(" "); 51 | } 52 | 53 | ret.add(" "); 54 | if (arg.description != null) 55 | ret.add(arg.description); 56 | var consolidated = ret.toString(); 57 | var inNewline = false; 58 | if (consolidated.length > screenSize) 59 | { 60 | ret = new StringBuf(); 61 | var c = consolidated.split(" "), ccount = 0; 62 | for (word in c) 63 | { 64 | if (inNewline && word == '') 65 | continue; 66 | else 67 | inNewline = false; 68 | ccount += word.length + 1; 69 | if (ccount >= screenSize) 70 | { 71 | ret.addChar("\n".code); 72 | for (i in 0...(argSize + 7)) 73 | ret.add(" "); 74 | ccount = word.length + 1 + argSize + 8; 75 | inNewline = true; 76 | if (word == '') continue; 77 | } 78 | ret.add(word); 79 | ret.add(" "); 80 | } 81 | return ret.toString(); 82 | } else { 83 | return consolidated; 84 | } 85 | } 86 | 87 | /** 88 | With an argument definition array, it formats to show the standard usage help screen 89 | [screenSize] maximum number of characters before a line break is forced 90 | **/ 91 | public static function showUsageOf(args:Array, ?screenSize):String 92 | { 93 | if (screenSize == null) 94 | screenSize = getScreenSize(); 95 | var maxSize = 0; 96 | for (arg in args) 97 | { 98 | if (arg.name == "runDefault") continue; 99 | var postfixSize = getPostfix(arg).length; 100 | var size = arg.command.length + postfixSize + 3; 101 | if (arg.aliases != null) for (a in arg.aliases) 102 | { 103 | size += a.length + 3; 104 | } 105 | 106 | if (size > maxSize) 107 | maxSize = size; 108 | } 109 | 110 | if (maxSize > (screenSize / 2.5)) maxSize = Std.int(screenSize / 2.5); 111 | var buf = new StringBuf(); 112 | for (arg in args) 113 | { 114 | if (arg.name == "runDefault") continue; 115 | var str = argToString(arg, maxSize, screenSize); 116 | if (str.length > 0) 117 | { 118 | buf.add(str); 119 | buf.addChar('\n'.code); 120 | } 121 | } 122 | return buf.toString(); 123 | } 124 | 125 | private static function getScreenSize(defaultSize=80) 126 | { 127 | #if sys 128 | var cols:Null = null; 129 | cols = Std.parseInt(Sys.getEnv("COLUNNS")); 130 | if (cols != null) 131 | return cols; 132 | try 133 | { 134 | var proc = new sys.io.Process('resize',[]); 135 | var i = proc.stdout; 136 | try 137 | { 138 | while(true) 139 | { 140 | var ln = StringTools.trim(i.readLine()); 141 | if (StringTools.startsWith(ln,"COLUMNS=")) 142 | { 143 | cols = Std.parseInt(ln.split('=')[1]); 144 | break; 145 | } 146 | } 147 | } 148 | catch(e:haxe.io.Eof) { 149 | } 150 | proc.close(); 151 | } 152 | catch(e:Dynamic) 153 | { 154 | } 155 | if (cols == null) 156 | return defaultSize; 157 | else 158 | return cols; 159 | #else 160 | return defaultSize; 161 | #end 162 | } 163 | 164 | private static function getAliases(arg:Argument) 165 | { 166 | var versions = arg.aliases != null ? arg.aliases.concat([arg.command]) : [arg.command]; 167 | versions = versions.filter(function(s) return s != null && s != ""); 168 | 169 | var prefix = "-"; 170 | if (arg.kind == SubDispatch || arg.kind == Message) 171 | prefix = ""; 172 | return [ for (v in versions) (v.length == 1) ? prefix + v.toDashSep() : prefix + prefix + v.toDashSep() ]; 173 | } 174 | 175 | private static function getPostfix(arg:Argument) 176 | { 177 | return switch(arg.kind) 178 | { 179 | case VarHash(k,v,_): 180 | " " + k.name + "[=" + v.name +"]"; 181 | case Var(_): 182 | " <" + arg.name + ">"; 183 | case Function(args,vargs): 184 | var postfix = ""; 185 | for (arg in args) 186 | postfix += (arg.opt ? " [" : " <") + arg.name.toDashSep() + (arg.opt ? "]" : ">"); 187 | if (vargs != null) 188 | postfix += " [arg1 [arg2 ...[argN]]]"; 189 | postfix; 190 | default: 191 | ""; 192 | }; 193 | } 194 | 195 | private static var decoders:Map>; 196 | 197 | /** 198 | Registers a custom Decoder that will be used to decode 'T' types. 199 | This function is type-checked and calling it will avoid the 'no Decoder was declared' warnings. 200 | 201 | IMPORTANT: this function must be called before the first .dispatch() that uses the custom type is called 202 | **/ 203 | macro public static function addDecoder(decoder:ExprOf>) 204 | { 205 | var t = Context.typeof(decoder); 206 | var field = null; 207 | switch(Context.follow(t)) 208 | { 209 | case TInst(c,_): 210 | for (f in c.get().fields.get()) 211 | { 212 | if (f.name == "fromString") 213 | { 214 | field = f; 215 | break; 216 | } 217 | } 218 | case TAnonymous(a): 219 | for(f in a.get().fields) 220 | { 221 | if (f.name == "fromString") 222 | { 223 | field =f; 224 | break; 225 | } 226 | } 227 | default: 228 | throw new Error("Unsupported decoder type :" + TypeTools.toString(t), decoder.pos); 229 | } 230 | if (field == null) 231 | throw new Error("The type '" + TypeTools.toString(t) + "' is not compatible with a Decoder type", decoder.pos); 232 | var type = switch(Context.follow(field.type)) 233 | { 234 | case TFun([arg],ret): //TODO test arg for string 235 | ret; 236 | default: 237 | throw new Error("The type '" + TypeTools.toString(field.type) + "' is not compatible with a Decoder type", decoder.pos); 238 | }; 239 | 240 | var name = mcli.internal.Macro.convertType(type, decoder.pos); 241 | mcli.internal.Macro.registerDecoder(name); 242 | var name = { expr:EConst(CString(name)), pos: decoder.pos }; 243 | 244 | return macro mcli.Dispatch.addDecoderRuntime($name, $decoder); 245 | } 246 | 247 | public static function addDecoderRuntime(name:String, d:Decoder):Void 248 | { 249 | if (decoders == null) 250 | decoders = new Map(); 251 | decoders.set(name,d); 252 | } 253 | 254 | static function decode(a:String, type:String):Dynamic 255 | { 256 | return switch(type) 257 | { 258 | case "Int": 259 | var ret = Std.parseInt(a); 260 | if (ret == null) throw ArgumentFormatError(type,a); 261 | ret; 262 | case "Float": 263 | var ret = Std.parseFloat(a); 264 | if (Math.isNaN(ret)) 265 | throw ArgumentFormatError(type,a); 266 | ret; 267 | case "String": 268 | a; 269 | default: 270 | var d = decoders != null ? decoders.get(type) : null; 271 | if (d == null) 272 | { 273 | var dt = Type.resolveClass(type); 274 | if (dt != null && Reflect.hasField(dt, "fromString")) 275 | d = cast dt; 276 | } 277 | if (d == null) 278 | { 279 | var dt2 = Type.resolveClass(type + "Decoder"); 280 | if (dt2 != null && Reflect.hasField(dt2, "fromString")) 281 | d = cast dt2; 282 | } 283 | if (d == null) 284 | { 285 | var e = Type.resolveEnum(type); 286 | if (e != null) 287 | { 288 | var all = Type.allEnums(e); 289 | if (all.length > 0 && all.length == Type.getEnumConstructs(e).length) 290 | { 291 | for (v in all) 292 | { 293 | if (a == Std.string(v).toDashSep()) 294 | return v; 295 | } 296 | throw ArgumentFormatError(type,a); 297 | } 298 | } 299 | } 300 | 301 | if (d == null) throw DecoderNotFound(type); 302 | d.fromString(a); 303 | }; 304 | } 305 | 306 | public var args(default,null):Array; 307 | var depth:Int; 308 | 309 | public function new(args:Array) 310 | { 311 | this.args = args.copy(); 312 | this.args.reverse(); 313 | this.depth = 0; 314 | } 315 | 316 | private function errln(s:String) 317 | { 318 | #if sys 319 | Sys.stderr().writeString(s + "\n"); 320 | #else 321 | haxe.Log.trace(s,null); 322 | #end 323 | } 324 | 325 | private function println(s:String) 326 | { 327 | #if sys 328 | Sys.println(s); 329 | #else 330 | haxe.Log.trace(s,null); 331 | #end 332 | } 333 | 334 | private static function isArgument(str:String) 335 | { 336 | if (str.charCodeAt(0) == '-'.code) 337 | { 338 | var code = str.charCodeAt(1); 339 | if (code >= '0'.code && code <= '9'.code || code == '.'.code) 340 | return false; 341 | else 342 | return true; 343 | } 344 | return false; 345 | } 346 | 347 | public function dispatch(v:mcli.CommandLine, handleExceptions = true):Void 348 | { 349 | this.depth++; 350 | try 351 | { 352 | _dispatch(v,handleExceptions); 353 | this.depth--; 354 | } 355 | catch(e:Dynamic) 356 | { 357 | this.depth--; 358 | #if cpp 359 | cpp.Lib.rethrow(e); 360 | #elseif neko 361 | neko.Lib.rethrow(e); 362 | #elseif cs 363 | cs.Lib.rethrow(e); 364 | #else 365 | throw e; 366 | #end 367 | } 368 | } 369 | 370 | private function _dispatch(v:mcli.CommandLine, handleExceptions:Bool):Void 371 | { 372 | if (handleExceptions) 373 | { 374 | try 375 | { 376 | _dispatch(v,false); 377 | } 378 | catch(e:DispatchError) 379 | { 380 | switch(e) 381 | { 382 | case UnknownArgument(a): 383 | errln('ERROR: Unknown argument: $a'); 384 | case ArgumentFormatError(t,p): 385 | errln('ERROR: Unrecognized format for $t. Passed $p'); 386 | case DecoderNotFound(t): 387 | errln('[mcli error] No Decoder found for type $t'); 388 | case MissingOptionArgument(opt,name) if (opt == "--run-default"): 389 | errln('ERROR: The argument $name is required'); 390 | case MissingOptionArgument(opt,name): 391 | name = name != null ? " (" + name + ")" : ""; 392 | errln('ERROR: The option $opt requires an argument $name, but no argument was passed'); 393 | case MissingArgument: 394 | errln('ERROR: Missing arguments'); 395 | case TooManyArguments: 396 | errln('ERROR: Too many arguments'); 397 | } 398 | println(v.showUsage()); 399 | #if sys 400 | Sys.exit(1); 401 | #end 402 | } 403 | 404 | return; 405 | } 406 | 407 | var defs = v.getArguments(); 408 | var names = new Map(); 409 | for (arg in defs) 410 | for (a in getAliases(arg)) 411 | names.set(a, arg); 412 | 413 | var didCall = false, defaultRan = false; 414 | var delays = []; 415 | function runArgument(arg:String, argDef:Argument) 416 | { 417 | switch(argDef.kind) 418 | { 419 | case Flag: 420 | Reflect.setProperty(v, argDef.name, true); 421 | case VarHash(key,val,arr): 422 | var map:IMap = Reflect.getProperty(v, argDef.name); 423 | var n = args.pop(); 424 | var toAdd = []; 425 | while(n != null && isArgument(n)) 426 | { 427 | toAdd.push(n); 428 | n = args.pop(); 429 | } 430 | if (n == null) 431 | throw MissingOptionArgument(arg, key.name); 432 | var kv = n.split("="); 433 | var k = decode(kv[0], key.t); 434 | var v = null; 435 | if (kv[1] != null) 436 | v = decode(kv[1], val.t); 437 | var oldv = map.get(k); 438 | if (oldv != null) 439 | { 440 | if (arr) 441 | oldv.push(v); 442 | // else //TODO 443 | // throw RepeatedArgument(arg 444 | } else { 445 | if (arr) 446 | map.set(k, [v]); 447 | else 448 | map.set(k,v); 449 | } 450 | if (toAdd.length > 0) 451 | { 452 | toAdd.reverse(); 453 | args = args.concat(toAdd); 454 | } 455 | case Var(t): 456 | var n = args.pop(); 457 | var toAdd = []; 458 | while(n != null && isArgument(n)) 459 | { 460 | toAdd.push(n); 461 | n = args.pop(); 462 | } 463 | if (n == null) 464 | throw MissingOptionArgument(arg); 465 | var val = decode(n, t); 466 | Reflect.setProperty(v, argDef.name, val); 467 | if (toAdd.length > 0) 468 | { 469 | toAdd.reverse(); 470 | args = args.concat(toAdd); 471 | } 472 | case Function(fargs,varArg): 473 | didCall = true; 474 | var applied:Array = []; 475 | var toAdd = []; 476 | var origArg = arg; 477 | for (fa in fargs) 478 | { 479 | arg = args.pop(); 480 | while (arg != null && isArgument(arg)) 481 | { 482 | toAdd.push(arg); 483 | arg = args.pop(); 484 | } 485 | if (arg == null && !fa.opt) 486 | throw MissingOptionArgument(origArg, fa.name); 487 | applied.push(decode(arg, fa.t)); 488 | } 489 | if (varArg != null) 490 | { 491 | var va = []; 492 | while (args.length > 0) 493 | { 494 | var arg = args.pop(); 495 | if (isArgument(arg)) 496 | { 497 | args.push(arg); 498 | break; 499 | } else { 500 | va.push(decode(arg,varArg)); 501 | } 502 | } 503 | applied.push(va); 504 | } 505 | delays.push(function() Reflect.callMethod(v, Reflect.field(v, argDef.name), applied)); 506 | if (toAdd.length != 0) 507 | { 508 | toAdd.reverse(); 509 | args = args.concat(toAdd); 510 | } 511 | case SubDispatch: 512 | didCall = true; 513 | for (d in delays) d(); 514 | delays = []; 515 | Reflect.callMethod(v, Reflect.field(v, argDef.name), [this]); 516 | case Message: 517 | throw UnknownArgument(arg); 518 | } 519 | } 520 | 521 | function getDefaultAlias() { 522 | return 523 | if (names.exists("--run-default")) "--run-default"; 524 | else if (names.exists("run-default")) "run-default"; 525 | else ""; 526 | } 527 | 528 | while (args.length > 0) 529 | { 530 | var arg = args.pop(); 531 | var argDef = names.get(arg); 532 | if (argDef == null) 533 | { 534 | if (!isArgument(arg)) 535 | { 536 | if (!defaultRan && !v._preventDefault) 537 | { 538 | argDef = names.get(getDefaultAlias()); 539 | if (argDef != null) 540 | defaultRan = true; 541 | args.push(arg); 542 | } 543 | } else if (arg.length > 2 && arg.charCodeAt(1) != '-'.code) { 544 | var a = arg.substr(1).split('').map(function(v) return '-' + v); 545 | a.reverse(); 546 | args = args.concat(a); 547 | continue; 548 | } 549 | } 550 | if (argDef == null) 551 | if (arg != null) { 552 | if ( (didCall == false && !v._preventDefault) || depth == 1 ) 553 | { 554 | throw UnknownArgument(arg); 555 | } else { 556 | args.push(arg); 557 | break; 558 | } 559 | } 560 | else 561 | throw MissingArgument; 562 | 563 | runArgument(arg, argDef); 564 | } 565 | 566 | var defaultAlias = getDefaultAlias(); 567 | var argDef = names.get(defaultAlias); 568 | 569 | for (d in delays) d(); 570 | delays = []; 571 | if (argDef == null) 572 | { 573 | if (!didCall) 574 | throw MissingArgument; 575 | } else { 576 | if (!didCall && !v._preventDefault) 577 | { 578 | runArgument(defaultAlias, argDef); 579 | } else if (!defaultRan && !v._preventDefault) switch(argDef.kind) { 580 | case Function(args,_) if (!args.exists(function(a) return !a.opt)): 581 | runArgument(defaultAlias, argDef); //only run default if compatible 582 | default: 583 | } 584 | } 585 | for (d in delays) d(); 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /mcli/DispatchError.hx: -------------------------------------------------------------------------------- 1 | package mcli; 2 | 3 | enum DispatchError 4 | { 5 | UnknownArgument(arg:String); 6 | ArgumentFormatError(type:String, passed:String); 7 | DecoderNotFound(type:String); 8 | MissingOptionArgument(opt:String, ?name:String); 9 | MissingArgument; 10 | TooManyArguments; 11 | } 12 | -------------------------------------------------------------------------------- /mcli/Types.hx: -------------------------------------------------------------------------------- 1 | package mcli; 2 | -------------------------------------------------------------------------------- /mcli/internal/Data.hx: -------------------------------------------------------------------------------- 1 | package mcli.internal; 2 | 3 | typedef Argument = 4 | { 5 | name:String, 6 | command:String, 7 | aliases:Null>, 8 | description:Null, 9 | kind:Kind 10 | } 11 | 12 | enum Kind 13 | { 14 | //stub 15 | Message; 16 | //variable 17 | Flag; 18 | VarHash(key:{ name:String, t:CType }, value:{ name:String, t:CType }, ?valueIsArray:Bool); 19 | Var(t:CType); 20 | //function 21 | Function(args:Array<{name:String, opt:Bool, t:CType}>, ?varArgs:Null); 22 | SubDispatch; 23 | } 24 | 25 | typedef CType = String; 26 | -------------------------------------------------------------------------------- /mcli/internal/Macro.hx: -------------------------------------------------------------------------------- 1 | package mcli.internal; 2 | import haxe.macro.*; 3 | import haxe.macro.Type; 4 | import haxe.macro.Expr; 5 | import mcli.internal.Data; 6 | using haxe.macro.TypeTools; 7 | using haxe.macro.ComplexTypeTools; 8 | using Lambda; 9 | 10 | class Macro 11 | { 12 | private static var types:Map, parentType:String }> = new Map(); 13 | private static var usedTypes:Map> = new Map(); 14 | private static var once = false; 15 | 16 | private static function ensureArgs(name, args, nargs, p) 17 | { 18 | if (args.length != nargs) 19 | throw new Error("Invalid number of type parameters for $name. Expected $nargs", p); 20 | } 21 | 22 | public static function convertType(t:haxe.macro.Type, pos:Position):mcli.internal.CType 23 | { 24 | return switch(Context.follow(t)) 25 | { 26 | case TInst(c,_): c.toString(); 27 | case TEnum(e,_): e.toString(); 28 | case TAbstract(a,_): a.toString(); 29 | default: 30 | throw new Error("The type " + t.toString() + " is not supported by the CLI dispatcher", pos); 31 | } 32 | } 33 | 34 | public static function registerUse(t:String, declaredPos:Position, parentType:String) 35 | { 36 | switch(t) 37 | { 38 | case "Int", "Float", "String", "Bool": return; 39 | default: 40 | } 41 | var g = types.get(t); 42 | if (g == null) 43 | { 44 | types.set(t, { found:false, declaredPos:declaredPos, parentType:parentType }); 45 | } 46 | } 47 | 48 | public static function registerDecoder(t:String) 49 | { 50 | var g = types.get(t); 51 | if (g == null) 52 | { 53 | types.set(t, { found:true, declaredPos:null, parentType:null }); 54 | } else { 55 | g.found = true; 56 | } 57 | } 58 | 59 | public static function build() 60 | { 61 | //reset statics for each reused context 62 | if (!once) 63 | { 64 | if (!Context.defined("use_rtti_doc")) 65 | throw new Error("mini cli will only work when -D use_rtti_doc is defined", Context.currentPos()); 66 | #if !haxe4 67 | Context.onMacroContextReused(function() 68 | { 69 | resetContext(); 70 | return true; 71 | }); 72 | #end 73 | resetContext(); 74 | once = true; 75 | } 76 | var cls = Context.getLocalClass().get(); 77 | var clsname = Context.getLocalClass().toString(); 78 | var lastCtor = getLastCtor(cls); 79 | if (cls.params.length != 0) 80 | throw new Error("Unsupported type parameters for Command Line macros", cls.pos); 81 | 82 | function convert(t:haxe.macro.Type, pos:Position):mcli.internal.CType 83 | { 84 | var ret = Macro.convertType(t,pos); 85 | registerUse(ret, pos, clsname); 86 | return ret; 87 | } 88 | //collect all @:arg members, and add a static 89 | var fields = Context.getBuildFields(); 90 | var ctor = null, setters = []; 91 | var arguments = []; 92 | 93 | if (cls.doc != null) 94 | { 95 | var parsed = Tools.parseComments(cls.doc); 96 | if (parsed[0] != null && parsed[0].tag == null) 97 | arguments.push(Context.makeExpr({ name:"", command:"", aliases:null, description:parsed[0].contents + '\n', kind:mcli.internal.Data.Kind.Message }, cls.pos)); 98 | } 99 | for (f in fields) 100 | { 101 | var name = f.name; 102 | if (f.name == "new") 103 | { 104 | ctor = f; 105 | continue; 106 | } 107 | //no statics / private allowed 108 | if (f.access.has(AStatic) || !f.access.has(APublic)) continue; 109 | 110 | var skip = false; 111 | for (m in f.meta) if (m.name == ":msg") 112 | { 113 | var descr = m.params[0]; 114 | arguments.push(macro { name:"", command:"", aliases:null, description:$descr, kind:mcli.internal.Data.Kind.Message }); 115 | } else if (m.name == ":skip") { 116 | skip = true; 117 | break; 118 | } 119 | if (skip) continue; 120 | 121 | var doc = f.doc; 122 | var parsed = (f.doc != null ? Tools.parseComments(doc) : []); 123 | 124 | var type = switch(f.kind) 125 | { 126 | case FVar(t, e), FProp(_, _, t, e): 127 | if (e != null) 128 | { 129 | f.kind = switch (f.kind) 130 | { 131 | case FVar(t,e): 132 | setters.push(macro this.$name = $e); 133 | FVar(t,null); 134 | case FProp(get,set,t,e): 135 | setters.push(macro this.$name = $e); 136 | FProp(get,set,t,null); 137 | default: throw "assert"; 138 | }; 139 | } 140 | 141 | if (t == null) 142 | { 143 | if (e == null) throw new Error("A field must either be fully typed, or be initialized with a typed expression", f.pos); 144 | try 145 | { 146 | Context.typeof(e); 147 | } 148 | catch(d:Dynamic) 149 | { 150 | throw new Error("Dispatch field cannot build with error: $d . Consider using a constant, or a simple expression", f.pos); 151 | } 152 | } else { 153 | t.toType(); 154 | } 155 | case FFun(fn): 156 | if (fn.params.length > 0) throw new Error("Unsupported function with parameters as an argument", f.pos); 157 | var fn = { ret : null, params: [], expr: macro {}, args: fn.args }; 158 | Context.typeof({ expr: EFunction(null,fn), pos: f.pos }); 159 | }; 160 | var command = name; 161 | var description = null, aliases = [], command = name, key = null, value = null; 162 | for (p in parsed) 163 | { 164 | p.contents = StringTools.replace(p.contents, "\n", ""); 165 | if (p.tag == null) 166 | { 167 | description = p.contents; 168 | } else switch (p.tag) { 169 | case "region": 170 | arguments.push(Context.makeExpr({ name:"", command:"", aliases:null, description:p.contents, kind:mcli.internal.Data.Kind.Message }, f.pos)); 171 | case "alias": 172 | aliases.push(StringTools.trim(p.contents)); 173 | case "command": 174 | command = StringTools.trim(p.contents); 175 | case "key": 176 | key = StringTools.trim(p.contents); 177 | case "value": 178 | value = StringTools.trim(p.contents); 179 | default: 180 | } 181 | } 182 | if (key == null) key = "key"; 183 | if (value == null) value = "value"; 184 | 185 | var kind = switch(Context.follow(type)) 186 | { 187 | case TAbstract(a,[p1,p2]) if (a.toString() == "Map" || a.toString() == 'haxe.ds.Map'): 188 | var arr = arrayType(p2); 189 | if (arr != null) p2 = arr; 190 | VarHash({ name:key, t:convert(p1, f.pos) }, { name:value, t:convert(p2, f.pos) }, arr != null); 191 | case TInst(c,[p1]) if (c.toString() == "haxe.ds.StringMap" || c.toString() == "haxe.ds.IntMap"): 192 | var arr = arrayType(p1); 193 | if (arr != null) p1 = arr; 194 | var t = c.toString() == "haxe.ds.StringMap" ? "String":"Int"; 195 | VarHash( { name:key, t:t }, {name:value, t:convert(p1, f.pos) }, arr != null ); 196 | case TAbstract(a,[]) if (a.toString() == "Bool"): 197 | Flag; 198 | case TFun([arg],ret) if (isDispatch(arg.t)): 199 | SubDispatch; 200 | case TFun(args,ret): 201 | var args = args.copy(); 202 | var last = args.pop(); 203 | var varArg = null; 204 | if (last != null && (last.name == "varArgs" || last.name == "rest")) 205 | { 206 | switch(Context.follow(last.t)) 207 | { 208 | case TInst(a,[t]) if (a.toString() == "Array"): 209 | varArg = convert(t, f.pos); 210 | default: 211 | args.push(last); 212 | } 213 | } else if (last != null) { 214 | args.push(last); 215 | } 216 | Function(args.map(function(a) return { name: a.name, opt: a.opt, t: convert(a.t, f.pos) }), varArg); 217 | default: 218 | Var( convert(type, f.pos) ); 219 | }; 220 | arguments.push(Context.makeExpr({ name:name, command:command, aliases:aliases, description:description, kind:kind }, f.pos)); 221 | } 222 | 223 | if (setters.length != 0) 224 | { 225 | if (ctor != null) 226 | { 227 | switch(ctor.kind) 228 | { 229 | case FFun(f): 230 | if (f.expr != null) 231 | { 232 | setters.push(f.expr); 233 | } 234 | f.expr = { expr: EBlock(setters), pos: ctor.pos }; 235 | default: throw "assert"; 236 | } 237 | } else { 238 | var bsuper = []; 239 | var args = []; 240 | var access = []; 241 | if (lastCtor != null) 242 | { 243 | if (lastCtor.isPublic) access.push(APublic); 244 | switch(Context.follow(lastCtor.type)) 245 | { 246 | case TFun(_args,_): 247 | args = _args.map(function(arg) return { 248 | value:null, 249 | type:null, 250 | opt:arg.opt, 251 | name:arg.name 252 | #if (haxe_ver >= 3.3) 253 | ,meta:null 254 | #end 255 | }); 256 | bsuper.push({ expr:ECall(macro super, args.map(function(arg) return { expr:EConst(CIdent(arg.name)), pos:Context.currentPos() })), pos: Context.currentPos() }); 257 | default: throw "assert"; 258 | } 259 | } 260 | 261 | ctor = { pos: Context.currentPos(), name:"new", meta: [], doc:null, access:access, kind:FFun({ 262 | ret: null, 263 | params: [], 264 | expr: { expr : EBlock(setters.concat(bsuper)), pos: Context.currentPos() }, 265 | args: args 266 | }) }; 267 | fields.push(ctor); 268 | } 269 | } 270 | 271 | if (arguments.length != 0) 272 | { 273 | fields.push({ 274 | pos: Context.currentPos(), 275 | name:"ARGUMENTS", 276 | meta: [], 277 | doc:null, 278 | access: [AStatic], 279 | kind:FVar(null, { expr: EArrayDecl(arguments), pos: Context.currentPos() }) 280 | }); 281 | if (!fields.exists(function(f) return f.name == "getArguments")) 282 | fields.push({ 283 | pos: Context.currentPos(), 284 | name:"getArguments", 285 | meta:[], 286 | doc:null, 287 | access:[APublic,AOverride], 288 | kind:FFun({ 289 | ret: null, 290 | params: [], 291 | expr: { expr: EReturn(macro ARGUMENTS.concat(super.getArguments())), pos: Context.currentPos() }, 292 | args: [] 293 | }) 294 | }); 295 | } 296 | return fields; 297 | } 298 | 299 | private static function getLastCtor(c:ClassType) 300 | { 301 | if (c.superClass == null) return null; 302 | var s = c.superClass.t.get(); 303 | if (s.constructor != null) return s.constructor.get(); 304 | return getLastCtor(s); 305 | } 306 | 307 | private static function isDispatch(t) 308 | { 309 | return switch(Context.follow(t)) 310 | { 311 | case TInst(c,_): 312 | if (c.toString() == "mcli.Dispatch") 313 | true; 314 | else if (c.get().superClass == null) 315 | false; 316 | else { 317 | var sc = c.get().superClass.t; 318 | var dyn = Context.getType('Dynamic'); 319 | isDispatch(TInst(c.get().superClass.t,[ for (param in sc.get().params) dyn ])); 320 | } 321 | default: false; 322 | } 323 | } 324 | 325 | private static function arrayType(t) 326 | { 327 | return switch(Context.follow(t)) 328 | { 329 | case TInst(c,[p]) if (c.toString() == "Array"): p; 330 | default: null; 331 | } 332 | } 333 | 334 | private static function getName(t:haxe.macro.Type) 335 | { 336 | return switch(Context.follow(t)) 337 | { 338 | case TInst(c,_): c.toString(); 339 | case TEnum(e,_): e.toString(); 340 | case TAbstract(a,_): a.toString(); 341 | default: null; 342 | } 343 | } 344 | 345 | private static function conformsToDecoder(t:haxe.macro.Type):Bool 346 | { 347 | switch(t) 348 | { 349 | case TInst(c,_): 350 | var c = c.get(); 351 | //TODO: test actual type 352 | return c.statics.get().exists(function(cf) return cf.name == "fromString"); 353 | default: return false; 354 | } 355 | } 356 | 357 | private static function resetContext() 358 | { 359 | usedTypes = new Map(); 360 | Context.onGenerate(function(btypes) 361 | { 362 | //see if all types dependencies are met 363 | for (k in types.keys()) 364 | { 365 | switch(k) 366 | { 367 | case "String", "Int", "Float", "Bool": continue; 368 | default: 369 | } 370 | var t = types.get(k); 371 | if (t != null && !t.found) 372 | { 373 | //check if the declared type is declared and conforms to the Decoder typedef 374 | for (bt in btypes) 375 | { 376 | if (getName(bt) == k) 377 | { 378 | switch(bt) 379 | { 380 | case TEnum(e,_): 381 | var e = e.get(); 382 | var simple = true; 383 | for (c in e.constructs) 384 | { 385 | switch(Context.follow(c.type)) 386 | { 387 | case TEnum(_,_): 388 | default: 389 | simple = false; 390 | break; 391 | } 392 | } 393 | if (simple) 394 | t.found = true; 395 | default: 396 | } 397 | } 398 | 399 | if ( !t.found && (getName(bt) == k || getName(bt) == k + "Decoder") && conformsToDecoder(bt)) 400 | { 401 | t.found = true; 402 | } 403 | } 404 | 405 | if (!t.found) 406 | { 407 | if (t.declaredPos == null) throw "assert"; //should never happen; declaredPos is null only for found=true 408 | Context.warning('The type $k is used by a mini cli Dispatcher but no Decoder was declared', t.declaredPos); 409 | //FIXME: for subsequent compiles using the compile server, this information will not show up 410 | var usedAt = usedTypes.get(t.parentType); 411 | if (usedAt != null) 412 | { 413 | for (p in usedAt) 414 | Context.warning("Last warning's type used here", p); 415 | } 416 | } 417 | } 418 | } 419 | }); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /mcli/internal/Tools.hx: -------------------------------------------------------------------------------- 1 | package mcli.internal; 2 | using StringTools; 3 | 4 | class Tools 5 | { 6 | public static function toDashSep(s:String):String 7 | { 8 | if (s.length <= 1) return s; //allow upper-case aliases 9 | var buf = new StringBuf(); 10 | var first = true; 11 | for (i in 0...s.length) 12 | { 13 | var chr = s.charCodeAt(i); 14 | if (chr >= 'A'.code && chr <= 'Z'.code) 15 | { 16 | if (!first) 17 | buf.addChar('-'.code); 18 | buf.addChar( chr - ('A'.code - 'a'.code) ); 19 | first = true; 20 | } else { 21 | buf.addChar(chr); 22 | first = false; 23 | } 24 | } 25 | 26 | return buf.toString(); 27 | } 28 | 29 | public static function parseComments(c:String):Array<{ tag:Null, contents:String }> 30 | { 31 | var ret = []; 32 | var curTag = null; 33 | var txt = new StringBuf(); 34 | for (ln in c.split("\n")) 35 | { 36 | var i = 0, len = ln.length; 37 | var foundTab = false; 38 | while (i < len) 39 | { 40 | switch(ln.fastCodeAt(i)) 41 | { 42 | case '\t'.code: 43 | i++; 44 | foundTab = true; 45 | case ' '.code if(!foundTab || (i > 0 && ln.fastCodeAt(i-1) == '*'.code)): 46 | i++; 47 | case '*'.code: 48 | i++; 49 | case '@'.code: //found a tag 50 | var t = txt.toString(); 51 | txt = new StringBuf(); 52 | if (curTag != null || t.length > 0) 53 | { 54 | ret.push({ tag:curTag, contents:t }); 55 | } 56 | var begin = ++i; 57 | while(i < len) 58 | { 59 | switch(ln.fastCodeAt(i)) 60 | { 61 | case ' '.code, '\t'.code: 62 | break; 63 | default: i++; 64 | } 65 | } 66 | curTag = ln.substr(begin, i - begin); 67 | break; 68 | default: break; 69 | } 70 | } 71 | if (i < len) 72 | { 73 | txt.add(ln.substr(i).replace("\r", "").rtrim()); 74 | txt.addChar(' '.code); 75 | } 76 | txt.addChar('\n'.code); 77 | } 78 | 79 | var t = txt.toString().trim(); 80 | if (curTag != null || t.length > 0) 81 | ret.push({ tag:curTag, contents: t }); 82 | 83 | return ret; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /samples/00-helloworld/Test.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | import mcli.Dispatch; 3 | 4 | /** 5 | Say hello. 6 | Example inspired by ruby's `executable` lib example 7 | **/ 8 | class Test extends mcli.CommandLine 9 | { 10 | /** 11 | Say it in uppercase? 12 | **/ 13 | public var loud:Bool; 14 | 15 | /** 16 | Show this message. 17 | **/ 18 | public function help() 19 | { 20 | Sys.println(this.showUsage()); 21 | } 22 | 23 | public function runDefault(?name:String) 24 | { 25 | if(name == null) 26 | name = "World"; 27 | var msg = 'Hello, $name!'; 28 | if (loud) 29 | msg = msg.toUpperCase(); 30 | Sys.println(msg); 31 | } 32 | 33 | public static function main() 34 | { 35 | new Dispatch(Sys.args()).dispatch(new Test()); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /samples/00-helloworld/build.hxml: -------------------------------------------------------------------------------- 1 | -cp ../.. 2 | -main Test 3 | -neko app.n 4 | -D use_rtti_doc 5 | -------------------------------------------------------------------------------- /samples/01-ant/01-simple.hxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /samples/01-ant/Test.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | import mcli.Dispatch; 3 | 4 | class Test 5 | { 6 | 7 | public function new() 8 | { 9 | 10 | } 11 | 12 | public static function main() 13 | { 14 | new Dispatch(Sys.args()).dispatch(new Ant()); 15 | } 16 | 17 | } 18 | 19 | //replicating http://commons.apache.org/proper/commons-cli/usage.html 20 | /** 21 | a Java based make tool 22 | **/ 23 | class Ant extends mcli.CommandLine 24 | { 25 | /** 26 | be extra quiet 27 | **/ 28 | public var quiet:Bool = false; 29 | 30 | /** 31 | be extra verbose 32 | **/ 33 | public var verbose:Bool = false; 34 | 35 | /** 36 | print debugging information 37 | **/ 38 | public var debug:Bool = false; 39 | 40 | /** 41 | produce logging information without adornments 42 | **/ 43 | public var emacs:Bool = false; 44 | 45 | /** 46 | use value for given property 47 | 48 | @key property 49 | @value value 50 | **/ 51 | public var D:Map = new Map(); 52 | 53 | /** 54 | print this message 55 | **/ 56 | public function help() 57 | { 58 | Sys.println(this.toString()); 59 | } 60 | 61 | /** 62 | print project help information 63 | **/ 64 | public function projectHelp() 65 | { 66 | 67 | } 68 | 69 | /** 70 | print the version information and exit 71 | **/ 72 | public function version() 73 | { 74 | 75 | } 76 | 77 | /** 78 | use given file for log 79 | **/ 80 | public function logfile(file:String) 81 | { 82 | 83 | } 84 | 85 | /** 86 | the class which is to perform logging 87 | **/ 88 | public function logger(clsname:String) 89 | { 90 | 91 | } 92 | 93 | /** 94 | add an instance of class as a project listener 95 | **/ 96 | public function listener(clsname:String) 97 | { 98 | 99 | } 100 | 101 | /** 102 | use given buildfile 103 | **/ 104 | public function buildFile(file:String) 105 | { 106 | 107 | } 108 | 109 | /** 110 | search for buildfile towards the root of the filesystem and use it 111 | **/ 112 | public function find(file:String) 113 | { 114 | 115 | } 116 | 117 | public function runDefault(varArgs:Array) 118 | { 119 | 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /samples/01-ant/build.hxml: -------------------------------------------------------------------------------- 1 | -cp ../.. 2 | -main Test 3 | -neko app.n 4 | -D use_rtti_doc 5 | -dce no 6 | -------------------------------------------------------------------------------- /samples/02-ls/Test.hx: -------------------------------------------------------------------------------- 1 | class Test 2 | { 3 | 4 | public static function main() 5 | { 6 | new mcli.Dispatch(Sys.args()).dispatch(new Ls()); 7 | } 8 | 9 | } 10 | 11 | //after example at http://commons.apache.org/proper/commons-cli/usage.html 12 | class Ls extends mcli.CommandLine 13 | { 14 | /** 15 | do not hide entries starting with . 16 | @alias a 17 | **/ 18 | public var all:Bool; 19 | 20 | /** 21 | do not list implied . and .. 22 | @alias A 23 | **/ 24 | public var almostAll:Bool; //conversion to almost-all is implied 25 | 26 | /** 27 | print octal escapes for nongraphic characters 28 | @alias b 29 | **/ 30 | public var escape:Bool; 31 | 32 | /** 33 | use SIZE-byte blocks 34 | **/ 35 | public var blockSize:Int; 36 | 37 | /** 38 | do not list implied entries ending with - 39 | @alias B 40 | **/ 41 | public var ignoreBackups:Bool; 42 | 43 | /** 44 | with -lt: 45 | sort by, and show, ctime (time of last modification of file status information) 46 | with -l: 47 | show ctime and sort by name 48 | otherwise: 49 | sort by ctime 50 | 51 | @command 52 | @alias c 53 | **/ 54 | public var ctime:Bool; 55 | 56 | /** 57 | list entries by columns 58 | @command 59 | @alias C 60 | **/ 61 | public var columns:Bool; 62 | 63 | public function runDefault() 64 | { 65 | for (v in Type.getInstanceFields(Ls)) 66 | { 67 | var f = Reflect.field(this,v); 68 | if (!Reflect.isFunction(f)) 69 | trace('$v : $f'); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /samples/02-ls/build.hxml: -------------------------------------------------------------------------------- 1 | -cp ../.. 2 | -D use_rtti_doc 3 | -main Test 4 | -neko app.n 5 | -------------------------------------------------------------------------------- /samples/03-git/Test.hx: -------------------------------------------------------------------------------- 1 | import mcli.CommandLine; 2 | import mcli.Dispatch; 3 | 4 | class Test { 5 | 6 | static function main() 7 | { 8 | Dispatch.addDecoder(new TestingDecoder()); 9 | new Dispatch(Sys.args()).dispatch(new Git()); 10 | } 11 | 12 | } 13 | 14 | //this will demonstrate how to add a custom type to be decoded 15 | class Testing 16 | { 17 | public var str:String; 18 | 19 | public function new(str) 20 | { 21 | this.str = str; 22 | } 23 | } 24 | 25 | class TestingDecoder 26 | { 27 | public function new() 28 | { 29 | 30 | } 31 | 32 | public function fromString(s:String):Testing 33 | { 34 | return new Testing(s + "."); 35 | } 36 | } 37 | 38 | class Git extends CommandLine 39 | { 40 | /** 41 | [region] The most commonly used git commands are: 42 | Add file contents to the index 43 | **/ 44 | public function add(d:Dispatch) 45 | { 46 | //re-dispatch here 47 | trace("git add called"); 48 | d.dispatch(new GitAdd()); 49 | trace("HERE"); 50 | } 51 | 52 | /** 53 | Find by binary search the change that introduced the bug 54 | **/ 55 | public function bisect(d:Dispatch) 56 | { 57 | 58 | } 59 | 60 | /** 61 | List, create, or delete branches 62 | **/ 63 | public function branch(d:Dispatch) 64 | { 65 | 66 | } 67 | 68 | /** 69 | Checkout a branch or paths to the working tree 70 | **/ 71 | public function checkout(d:Dispatch) 72 | { 73 | 74 | } 75 | 76 | /** 77 | Record changes to the repository 78 | **/ 79 | public function commit(d:Dispatch) 80 | { 81 | trace("commit"); 82 | d.dispatch(new GitCommit()); 83 | } 84 | 85 | /** 86 | Testing argument 87 | **/ 88 | public function testingArgument(arg:Testing) 89 | { 90 | trace("called testing " + arg.str); 91 | 92 | } 93 | } 94 | 95 | class GitCommand extends CommandLine 96 | { 97 | /** 98 | be verbose 99 | @alias v 100 | **/ 101 | public var verbose:Bool; 102 | 103 | /** 104 | print this message 105 | @command 106 | @alias h 107 | **/ 108 | public function help() 109 | { 110 | Sys.println(this.toString()); 111 | } 112 | } 113 | 114 | class GitCommit extends GitCommand 115 | { 116 | /** 117 | commit all changed files 118 | @alias a 119 | **/ 120 | public var all:Bool; 121 | 122 | /** 123 | commit message 124 | @alias m 125 | **/ 126 | public var message:String; 127 | 128 | public function runDefault() 129 | { 130 | 131 | } 132 | } 133 | 134 | class GitAdd extends GitCommand 135 | { 136 | /** 137 | dry run 138 | @alias n 139 | **/ 140 | public var dryRun:Bool; 141 | 142 | /** 143 | interactive picking 144 | @alias i 145 | **/ 146 | public var interactive:Bool; 147 | 148 | /** 149 | select hunks interactively 150 | @alias p 151 | **/ 152 | public var patch:Bool; 153 | 154 | /** 155 | edit current diff and apply 156 | @alias e 157 | **/ 158 | public var edit:Bool; 159 | 160 | /** 161 | allow adding otherwise ignored files 162 | @alias f 163 | **/ 164 | public var force:Bool; 165 | 166 | /** 167 | update tracked files 168 | @alias u 169 | **/ 170 | public var update:Bool; 171 | 172 | /** 173 | don't add, only refresh the index 174 | **/ 175 | public var refresh:Bool; 176 | 177 | /** 178 | just skip files which cannot be added because of errors 179 | **/ 180 | public var ignoreErrors:Bool; 181 | 182 | /** 183 | check if - even missing - files are ignored in dry run 184 | **/ 185 | public var ignoreMissing:Bool; 186 | 187 | public function runDefault(arg1:String) 188 | { 189 | trace(arg1); 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /samples/03-git/build.hxml: -------------------------------------------------------------------------------- 1 | -cp ../.. 2 | -D use_rtti_doc 3 | -main Test 4 | -neko app.n 5 | -------------------------------------------------------------------------------- /samples/04-complex/Test.hx: -------------------------------------------------------------------------------- 1 | import mcli.Dispatch; 2 | 3 | class Test 4 | { 5 | static function main() 6 | { 7 | new Dispatch(Sys.args()).dispatch(new Complex()); 8 | 9 | } 10 | } 11 | 12 | class Complex extends mcli.CommandLine 13 | { 14 | //test stacked aliases 15 | /** 16 | a simple flag 17 | @alias f 18 | **/ 19 | public var simpleFlag:Bool = false; 20 | 21 | /** 22 | a type var 23 | @alias t 24 | **/ 25 | public var typeVar:String = "defaultVal"; 26 | 27 | /** 28 | a map 29 | @alias D 30 | **/ 31 | public var defines:Map = new Map(); 32 | 33 | /** 34 | an enum 35 | @alias e 36 | **/ 37 | public var anEnum:SimpleEnum; 38 | 39 | /** 40 | run quietly 41 | @alias q 42 | **/ 43 | public var quiet:Bool = false; 44 | 45 | @:skip public var arg:SimpleEnum; 46 | @:skip public var hasRun:Bool = false; 47 | 48 | /** 49 | a function with no args 50 | @alias n 51 | **/ 52 | public function noArgs() 53 | { 54 | if (!quiet) 55 | trace('no args called'); 56 | } 57 | 58 | /** 59 | a function with an argument 60 | @alias a 61 | **/ 62 | public function args(arg:SimpleEnum) 63 | { 64 | if (!quiet) 65 | trace(arg); 66 | this.arg = arg; 67 | } 68 | 69 | /** 70 | runs a mcli test 71 | **/ 72 | public function test() 73 | { 74 | var c = new Complex(); 75 | new Dispatch(["-qftDena","type","x=y","two","three"]).dispatch(c); 76 | if (c.arg != Three) 77 | trace("error"); 78 | if (!c.hasRun) 79 | trace("error"); 80 | if (c.defines.get("x") != "y") 81 | trace("error"); 82 | if (c.anEnum != Two) 83 | trace("error"); 84 | trace("tests finished"); 85 | } 86 | 87 | public function runDefault() 88 | { 89 | if (!quiet) 90 | trace([simpleFlag, typeVar, defines.toString(), anEnum]); 91 | hasRun = true; 92 | } 93 | 94 | /** 95 | shows this message 96 | @alias h 97 | **/ 98 | public function help() 99 | { 100 | Sys.println(this.showUsage()); 101 | } 102 | } 103 | 104 | enum SimpleEnum 105 | { 106 | ValueOne; 107 | Two; 108 | Three; 109 | } 110 | -------------------------------------------------------------------------------- /samples/04-complex/build.hxml: -------------------------------------------------------------------------------- 1 | -cp ../.. 2 | -dce no 3 | -main Test 4 | -neko app.n 5 | -D use_rtti_doc 6 | -------------------------------------------------------------------------------- /test/build.hxml: -------------------------------------------------------------------------------- 1 | -cp src 2 | -lib utest 3 | -lib mcli 4 | -main Test 5 | -D travis 6 | 7 | -js bin/js.js 8 | #-python bin/py.py 9 | #-php bin/php 10 | #-cmd python3 bin/py.py 11 | -------------------------------------------------------------------------------- /test/src/Test.hx: -------------------------------------------------------------------------------- 1 | import utest.*; 2 | import utest.ui.Report; 3 | import tests.*; 4 | 5 | class Test 6 | { 7 | static function main() 8 | { 9 | var runner = new Runner(); 10 | 11 | runner.addCase(new McliTests()); 12 | Report.create(runner); 13 | 14 | var r:TestResult = null; 15 | runner.onProgress.add(function(o) if (o.done == o.totals) r = o.result); 16 | runner.run(); 17 | 18 | #if sys 19 | if (r.allOk()) 20 | Sys.exit(0); 21 | else 22 | Sys.exit(1); 23 | #end 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/src/tests/McliTests.hx: -------------------------------------------------------------------------------- 1 | package tests; 2 | import utest.Assert.*; 3 | import mcli.*; 4 | 5 | class McliTests 6 | { 7 | public function new() 8 | { 9 | } 10 | 11 | public function test_basictypes() 12 | { 13 | var bt = new BasicTypes(); 14 | inline function dispatch(args:Array) 15 | new Dispatch(args).dispatch(bt,false); 16 | 17 | equals(bt.bool,false); 18 | equals(bt.string,'hello'); 19 | equals(bt.ivar,0); 20 | equals(bt.fvar,0); 21 | 22 | dispatch(['--bool']); 23 | isTrue(bt.didRunDefault); 24 | equals(bt.bool,true); 25 | bt.bool = false; 26 | dispatch(['-b']); 27 | equals(bt.bool,true); 28 | bt.bool = false; 29 | 30 | dispatch(['--string','hi']); 31 | equals(bt.string,'hi'); 32 | bt.string = null; 33 | dispatch(['-s','hi!']); 34 | equals(bt.string,'hi!'); 35 | bt.string = null; 36 | 37 | dispatch(['--ivar','42']); 38 | equals(bt.ivar,42); 39 | bt.ivar = 0; 40 | dispatch(['-i','44']); 41 | equals(bt.ivar,44); 42 | bt.ivar = 0; 43 | 44 | // negative 45 | dispatch(['--ivar','-42']); 46 | equals(bt.ivar,-42); 47 | bt.ivar = 0; 48 | dispatch(['-i','-1']); 49 | equals(bt.ivar,-1); 50 | bt.ivar = 0; 51 | 52 | dispatch(['--fvar','4.2']); 53 | equals(bt.fvar,4.2); 54 | bt.fvar = 0; 55 | dispatch(['-f','.44']); 56 | equals(bt.fvar,.44); 57 | bt.fvar = 0; 58 | 59 | // negative 60 | dispatch(['--fvar','-.42']); 61 | equals(bt.fvar,-.42); 62 | bt.fvar = 0; 63 | dispatch(['-f','-1.2']); 64 | equals(bt.fvar,-1.2); 65 | bt.fvar = 0; 66 | 67 | dispatch(['--map','v1','--map','v2=test']); 68 | equals(bt.map['v1'],null); 69 | isTrue(bt.map.exists('v1')); 70 | equals(bt.map['v2'],'test'); 71 | 72 | bt.map = new Map(); 73 | dispatch(['-m','v1=','-m','v2=test2']); 74 | equals(bt.map['v1'],''); 75 | equals(bt.map['v2'],'test2'); 76 | 77 | // many 78 | bt = new BasicTypes(); 79 | dispatch(['-sbfim','thestring','42.2','29','key=value']); 80 | equals(bt.string,'thestring'); 81 | equals(bt.fvar,42.2); 82 | equals(bt.bool,true); 83 | equals(bt.ivar,29); 84 | equals(bt.map['key'],'value'); 85 | } 86 | 87 | public function test_funcs() 88 | { 89 | var d = new WithSubDispatch(); 90 | inline function dispatch(args:Array) 91 | new Dispatch(args).dispatch(d,false); 92 | 93 | dispatch(['--one-arg','12']); 94 | equals(d.i,12); 95 | dispatch(['--two-args','15','10.2']); 96 | equals(d.i,15); 97 | equals(d.f,10.2); 98 | 99 | // order of function calls 100 | dispatch(['-OT','11','14','10.1']); 101 | equals(d.i,14); 102 | equals(d.f,10.1); 103 | dispatch(['-TO','1','2.2','3']); 104 | equals(d.i,3); 105 | equals(d.f,2.2); 106 | 107 | // error when cannot run 108 | raises(function() dispatch(['something'])); 109 | raises(function() dispatch(['something','else'])); 110 | raises(function() dispatch(['sub','something'])); 111 | raises(function() dispatch(['sub','something','else'])); 112 | 113 | d = new WithSubDispatchAndDefault(); 114 | dispatch(['something']); 115 | same(d.varArgs,['something']); 116 | dispatch(['something','else']); 117 | same(d.varArgs,['something','else']); 118 | dispatch(['sub','something']); 119 | same(d.varArgs,['something']); 120 | dispatch(['sub','something','else']); 121 | same(d.varArgs,['something','else']); 122 | 123 | // sub dispatch 124 | for (disp in [new WithSubDispatch(), new WithSubDispatchAndDefault()]) 125 | { 126 | d = disp; 127 | dispatch(['sub','-m','v1=something','-sbf','string','1.1']); 128 | equals(d.bt.map['v1'],'something'); 129 | equals(d.bt.string,'string'); 130 | equals(d.bt.fvar,1.1); 131 | isTrue(d.bt.didRunDefault); 132 | 133 | // fallback to upper dispatch 134 | d.bt.didRunDefault = false; 135 | dispatch(['sub','-s','thestring','sub','-s','otherstring']); 136 | isTrue(d.bt.didRunDefault); 137 | equals(d.bt.string,'otherstring'); 138 | 139 | d.bt.didRunDefault = false; 140 | d.bt.preventDefault(); 141 | dispatch(['sub','-s','thestring']); 142 | isFalse(d.bt.didRunDefault); 143 | equals(d.bt.string,'thestring'); 144 | 145 | // fallback to upper dispatch 146 | dispatch(['sub','-s','thestring','sub','-ss','otherstring','anotherstring']); 147 | isFalse(d.bt.didRunDefault); 148 | equals(d.bt.string,'anotherstring'); 149 | } 150 | } 151 | 152 | public static function raises(fn:Void->Void) 153 | { 154 | var didFail = false; 155 | try 156 | { 157 | fn(); 158 | utest.Assert.fail(); 159 | } 160 | catch(e:DispatchError) 161 | { 162 | didFail = true; 163 | } 164 | utest.Assert.isTrue(didFail); 165 | } 166 | } 167 | 168 | class BasicTypes extends mcli.CommandLine 169 | { 170 | /** 171 | @alias b 172 | **/ 173 | public var bool:Bool = false; 174 | /** 175 | @alias s 176 | **/ 177 | public var string:String = 'hello'; 178 | 179 | /** 180 | @alias i 181 | **/ 182 | public var ivar:Int = 0; 183 | 184 | /** 185 | @alias f 186 | **/ 187 | public var fvar:Float = 0; 188 | 189 | /** 190 | @alias m 191 | **/ 192 | public var map:Map = new Map(); 193 | 194 | @:skip public var didRunDefault:Bool; 195 | public function runDefault() 196 | { 197 | this.didRunDefault = true; 198 | } 199 | } 200 | 201 | class WithSubDispatch extends CommandLine 202 | { 203 | @:skip public var i:Int; 204 | /** 205 | @alias O 206 | **/ 207 | public function oneArg(i:Int) 208 | { 209 | this.i = i; 210 | } 211 | 212 | @:skip public var f:Float; 213 | /** 214 | @alias T 215 | **/ 216 | public function twoArgs(i:Int,f:Float) 217 | { 218 | this.i = i; 219 | this.f = f; 220 | } 221 | 222 | @:skip public var bt:BasicTypes = new BasicTypes(); 223 | public function sub(d:Dispatch) 224 | { 225 | d.dispatch(bt,false); 226 | } 227 | 228 | @:skip public var varArgs:Array; 229 | } 230 | 231 | class WithSubDispatchAndDefault extends WithSubDispatch 232 | { 233 | public function runDefault(varArgs:Array) 234 | { 235 | this.varArgs = varArgs; 236 | } 237 | } 238 | --------------------------------------------------------------------------------