├── .gitignore ├── .haxerc ├── .travis.yml ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── dev.hxml ├── examples ├── haxe │ ├── Haxe.hx │ ├── README.md │ ├── haxe.sh │ └── run.sh └── sub │ ├── Example.hx │ ├── README.md │ └── run.sh ├── extraParams.hxml ├── haxe_libraries ├── ansi.hxml ├── hxcpp.hxml ├── hxnodejs.hxml ├── tink_await.hxml ├── tink_chunk.hxml ├── tink_cli.hxml ├── tink_core.hxml ├── tink_io.hxml ├── tink_macro.hxml ├── tink_priority.hxml ├── tink_streams.hxml ├── tink_stringly.hxml ├── tink_syntaxhub.hxml ├── tink_testrunner.hxml ├── tink_unittest.hxml └── travix.hxml ├── haxelib.json ├── package.json ├── playground.hxml ├── src └── tink │ ├── Cli.hx │ └── cli │ ├── DocFormatter.hx │ ├── Macro.hx │ ├── Prompt.hx │ ├── Rest.hx │ ├── Result.hx │ ├── Router.hx │ ├── doc │ └── DefaultFormatter.hx │ ├── macro │ └── Router.hx │ └── prompt │ ├── DefaultPrompt.hx │ ├── IoPrompt.hx │ ├── NodePrompt.hx │ ├── RetryPrompt.hx │ └── SysPrompt.hx ├── tests.hxml ├── tests ├── DebugCommand.hx ├── Playground.hx ├── RunTests.hx ├── TestAliasDisabled.hx ├── TestCommand.hx ├── TestFlag.hx ├── TestOptional.hx ├── TestPrompt.hx └── TestPromptAndRest.hx └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | dump 3 | node_modules 4 | *.n -------------------------------------------------------------------------------- /.haxerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.0.0", 3 | "resolveLibs": "scoped" 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: xenial 3 | 4 | stages: 5 | - test 6 | - deploy 7 | 8 | language: node_js 9 | node_js: 10 10 | 11 | cache: 12 | directories: 13 | - $HOME/haxe 14 | 15 | os: 16 | - linux 17 | # - osx 18 | 19 | env: 20 | - HAXE_VERSION=3.4.7 21 | - HAXE_VERSION=latest 22 | 23 | install: 24 | - npm i -g lix 25 | - lix install haxe $HAXE_VERSION 26 | - lix download 27 | 28 | script: 29 | - lix run travix node 30 | # - lix run travix cs # type paramters... 31 | # - lix run travix java # type paramters... 32 | - lix run travix python 33 | - lix run travix php 34 | - lix run travix cpp 35 | - lix run travix lua 36 | - lix run travix neko 37 | - lix run travix interp 38 | 39 | 40 | jobs: 41 | include: 42 | # - stage: test # should uncomment this when there is no matrix above (e.g. only one os, one env, etc) 43 | - stage: deploy 44 | os: linux 45 | install: 46 | - npm i -g lix 47 | - lix download 48 | script: skip 49 | env: 50 | secure: ZHfmyegShsvV61rIuLfu9YyhXULmwR7ax7+vhWt57dtStppbetaIFtAz+iJwXDHdReH8Z+1tPIXNrSjz0pEn+dp+3ov+TwJJXP0vPeEOvyoMI3pcNKAT3IP0nGBUf0xoOh/9g9RBTPgS5nH7wVNu33uk7Eh2HW/TfMYVlGxiuvJ74fhsA2UgD9PWIeTnC9LW7AxdAzhXz26a70fHj30bHAG8jKJZcLYSCl7Vb7+BkYx+WUonpt9lXeOz34QQTMlf/k2EVRU5r97Qnv8F9x+qCS/zI/H7qZp2+VrbfdnUbBfHgOgRpiow0sSj9Z3I/NsvO7v5nAnASSnZb4FmFty9iy8AetleUv8q27VUqlolzSQ83SipjjG4VwbB4ZVqfYb2uzWBmB3TZUyHSBvp9JWc4etd5EhaNWvQ4FVkC8kKHECPBfCarA36XS1DHpj+6rVj3iNnQkfKXFCCv1AhP318sSByETZIMEhTU8h3zXY5D5SL7A8nIMDCyPSREnhwt7J0YA/tuCO7/EZ67lDKixWyoGWZs3pj5yyAhiXd1bfJ2Cdv2bYRFA94BjGzIEfVepdJo/Toda8Ort8VadsGJSI8DEOrd2JcdlbRBCiA/+Tu5Jf3knsFo/3ug4rvTK7Vg/lo9YwYpneTjPhLUx6SLpUwBD1wnSoum84rf6wh6B5J7x4= 51 | after_success: 52 | - lix run travix install 53 | - lix run travix release 54 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // These are configurations used for haxe completion. 3 | // 4 | // Each configuration is an array of arguments that will be passed to the Haxe completion server, 5 | // they should only contain arguments and/or hxml files that are needed for completion, 6 | // such as -cp, -lib, target output settings and defines. 7 | "haxe.displayConfigurations": [ 8 | ["dev.hxml"], // if a hxml file is safe to use, we can just pass it as argument 9 | // you can add more than one configuration and switch between them 10 | ["playground.hxml"] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "lix", 4 | "args": ["run", "travix","node"], 5 | "problemMatcher": "$haxe", 6 | "group": { 7 | "kind": "build", 8 | "isDefault": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kevin Leung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tink_cli 2 | 3 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/haxetink/public) 4 | 5 | Write command line tools with ~~ease~~ Haxe. 6 | 7 | ## Usage 8 | 9 | To illustrate the usage, let's look at the follow quick mock-up of the Haxe command line. 10 | 11 | ```haxe 12 | static function main() { 13 | Cli.process(Sys.args(), new MockupHaxe()).handle(Cli.exit); 14 | } 15 | 16 | @:alias(false) 17 | class MockupHaxe { 18 | @:flag('-js') 19 | public var js:String; 20 | 21 | @:flag('-lib') 22 | public var lib:Array = null; 23 | 24 | @:flag('-main') 25 | public var main:String; 26 | 27 | @:flag('-D') 28 | public var defines:Array = null; 29 | 30 | public var help:Bool; 31 | 32 | @:flag('help-defines') 33 | public var helpDefines:Bool; 34 | 35 | public function new() {} 36 | 37 | @:defaultCommand 38 | public function run(rest:Rest) { 39 | Sys.println('js: $js'); 40 | Sys.println('lib: $lib'); 41 | Sys.println('main: $main'); 42 | Sys.println('defines: $defines'); 43 | Sys.println('help: $help'); 44 | Sys.println('helpDefines: $helpDefines'); 45 | Sys.println('rest: $rest'); 46 | } 47 | } 48 | ``` 49 | 50 | And then run with: 51 | 52 | `./haxe.sh -js bin/run.js -D release -D mobile -lib tink_core -lib tink_json Main` 53 | 54 | Gives you: 55 | 56 | ``` 57 | js: bin/run.js 58 | lib: [tink_core,tink_json] 59 | main: null 60 | defines: [release,mobile] 61 | help: null 62 | helpDefines: null 63 | rest: [Main] 64 | ``` 65 | 66 | or: 67 | `./haxe.sh --help-defines` 68 | 69 | ``` 70 | js: null 71 | lib: null 72 | main: null 73 | defines: null 74 | help: null 75 | helpDefines: true 76 | rest: [] 77 | ``` 78 | 79 | Check out the examples folder for the complete code. 80 | 81 | ### Flags 82 | Every `public var` in the class will be treated as a cli flag. (To override this behavior, tag a field with `@:flag(false)`) 83 | 84 | For example `public var flag:String` will be set to value `` by the cli switch `--flag `. 85 | Also, the framework will also recognize the first letter of the flag name as alias. So in this case 86 | `-f ` will do the same. 87 | 88 | You can use metadata data to govern the flag name (`@:flag('my-custom-flag-name')`) and alias (`@:alias('a')`). 89 | Note that you can only use a single character for alias. 90 | 91 | The reason for a single-char restriction for alias is that you can use a condensed alias format, for example: 92 | `-abcdefg` is actually equivalent to `-a -b -c -d -e -f -g`. 93 | 94 | If you specify a flag name starting with a single dash (e.g. `@:flag('-flag')`), it will be respected but then 95 | alias support will be automatically disabled. You can also use `@:alias(false)` to manually disable alias, 96 | which works on both field level and class level. 97 | 98 | ### Commands 99 | Public methods tagged with `@:command` will be treated as a runnable command. 100 | 101 | For example `@:command public function run() {}` can be triggered by `./yourscript run`. In case you want to 102 | run a function under your binary name, you can tag a function with `@:defaultCommand`, then you will be able 103 | to run the function with `./yourscript`. 104 | 105 | By default the framework infers the command name from the method name, 106 | you can provide a parameter to the metadata like `@:command('my-cmd')` to change that. 107 | 108 | Also, if you tag a `public var` with @:command, it will be treated as a sub-command. For instance: 109 | 110 | ```haxe 111 | @:command 112 | public var sub:AnotherCommand; 113 | ``` 114 | 115 | In this case, when the program is called with `./yourscript sub -a`, 116 | the default command in `AnotherCommand` will run, with the argument `-a` 117 | 118 | ### Data Types 119 | 120 | By default, Bool, Int, Float and String are supported. Furthermore, you can use any abstract that is castable from String 121 | (i.e. `from String` or `@:from public static function ofString(v:String)`) as the data type. 122 | 123 | For example, to use a Map: 124 | 125 | ```haxe 126 | 127 | @:forward 128 | abstract CustomMap(StringMap) from StringMap to StringMap { 129 | @:from 130 | public static function fromString(v:String):CustomMap { 131 | var map = new StringMap(); 132 | for(i in v.split(',')) switch i.split('=') { 133 | case [key, value]: map.set(key, (value:tink.Stringly)); 134 | default: throw 'Invalid format'; 135 | } 136 | return map; 137 | } 138 | } 139 | ``` 140 | 141 | Also, note that if a flag is of type Bool, the flag will be set to true without considering the "switch argument", 142 | For example, `--force` is used instead of `--force true` to set `force = true`. In fact in the latter case, the `true` 143 | string is considered as a Rest argument. 144 | 145 | Rest argument is a list of strings which are not consumed by the flag parser. You can capture it in a command with 146 | `Rest`. For example: 147 | 148 | ```haxe 149 | @:defaultCommand 150 | public function run(rest:Rest) {} 151 | ``` 152 | 153 | Note that at most one Rest argument may appear in the list. 154 | 155 | ### User Input 156 | 157 | Besides a non-interaction tool as described above. One can also build an interactive tool, by utilizing the `Prompt` interface. 158 | 159 | It is basically: 160 | 161 | ```haxe 162 | interface Prompt { 163 | function prompt(type:PromptType):Promise; 164 | } 165 | 166 | enum PromptType { 167 | Simple(prompt:String); 168 | MultipleChoices(prompt:String, choices:Array); 169 | } 170 | ``` 171 | 172 | Basically you ask for an input from the user, and then you will get a promised result. It is just an interface 173 | so you can basically implement any mechanism of input, from simple text input to "GUI" input with arrow movements, etc. 174 | 175 | For now there is a `SimplePrompt` which basically read from the stdin and take in anything the user gives. 176 | And in case of multiple choice prompt, `RetryPrompt` will make sure the user are choosing from 177 | the provided list of choices, and fail after certain number of retries. 178 | 179 | First, you set a `Prompt` instance in `Cli.process(args, command, ?prompt)` 180 | (defaults to a `SimplePrompt` instance if omitted) 181 | and then you can later obtain it back from a command's function like so: 182 | 183 | ```haxe 184 | @:command 185 | public function run(prompt:tink.cli.Prompt) { 186 | prompt.prompt('Input your name: ').handle(...); 187 | } 188 | ``` 189 | 190 | ### Documentations 191 | 192 | ```haxe 193 | var doc = Cli.getDoc(myCommand); 194 | Sys.println(doc); 195 | ``` 196 | -------------------------------------------------------------------------------- /dev.hxml: -------------------------------------------------------------------------------- 1 | tests.hxml 2 | 3 | -js bin/node/tests.js 4 | 5 | -lib hxnodejs 6 | -lib tink_cli 7 | -lib tink_unittest 8 | -lib travix 9 | 10 | -dce full -------------------------------------------------------------------------------- /examples/haxe/Haxe.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.cli.*; 4 | import tink.Cli; 5 | 6 | class Haxe { 7 | static function main() { 8 | Cli.process(Sys.args(), new Command()).handle(Cli.exit); 9 | } 10 | } 11 | 12 | @:alias(false) 13 | class Command { 14 | @:flag('-js') 15 | public var js:String; 16 | 17 | @:flag('-lib') 18 | public var lib:Array = null; 19 | 20 | @:flag('-main') 21 | public var main:String; 22 | 23 | @:flag('-D') 24 | public var defines:Array = null; 25 | 26 | public var help:Bool; 27 | 28 | @:flag('help-defines') 29 | public var helpDefines:Bool; 30 | 31 | public function new() {} 32 | 33 | @:defaultCommand 34 | public function run(rest:Rest) { 35 | Sys.println('js: $js'); 36 | Sys.println('lib: $lib'); 37 | Sys.println('main: $main'); 38 | Sys.println('defines: $defines'); 39 | Sys.println('help: $help'); 40 | Sys.println('helpDefines: $helpDefines'); 41 | Sys.println('rest: $rest'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/haxe/README.md: -------------------------------------------------------------------------------- 1 | # Mockup Command Line Tool for the Haxe Complier 2 | 3 | ### Basic 4 | `./haxe.sh -js bin/run.js -main Main` 5 | ``` 6 | js: bin/run.js 7 | lib: null 8 | main: Main 9 | defines: null 10 | help: null 11 | helpDefines: null 12 | rest: [] 13 | ``` 14 | 15 | ### Without main 16 | `./haxe.sh -js bin/run.js Api` 17 | ``` 18 | js: bin/run.js 19 | lib: null 20 | main: null 21 | defines: null 22 | help: null 23 | helpDefines: null 24 | rest: [Api] 25 | ``` 26 | 27 | ### With libaries and defines 28 | `./haxe.sh -js bin/run.js -D release -D mobile -lib tink_core -lib tink_json -main Main` 29 | ``` 30 | js: bin/run.js 31 | lib: [tink_core,tink_json] 32 | main: Main 33 | defines: [release,mobile] 34 | help: null 35 | helpDefines: null 36 | rest: [] 37 | ``` 38 | 39 | ### Flags 40 | `./haxe.sh --help` 41 | ``` 42 | js: null 43 | lib: null 44 | main: null 45 | defines: null 46 | help: true 47 | helpDefines: null 48 | rest: [] 49 | ``` 50 | `./haxe.sh --help-defines` 51 | ``` 52 | js: null 53 | lib: null 54 | main: null 55 | defines: null 56 | help: null 57 | helpDefines: true 58 | rest: [] 59 | ``` -------------------------------------------------------------------------------- /examples/haxe/haxe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | haxe -lib tink_cli --run Haxe $@ -------------------------------------------------------------------------------- /examples/haxe/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | echo "### Basic" 6 | ./haxe.sh -js bin/run.js -main Main 7 | 8 | echo "### Without main" 9 | ./haxe.sh -js bin/run.js Api 10 | 11 | echo "### With libaries and defines" 12 | ./haxe.sh -js bin/run.js -D release -D mobile -lib tink_core -lib tink_json -main Main 13 | 14 | echo "### Flags" 15 | ./haxe.sh --help 16 | ./haxe.sh --help-defines -------------------------------------------------------------------------------- /examples/sub/Example.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.cli.*; 4 | import tink.Cli; 5 | 6 | class Example { 7 | static function main() { 8 | Cli.process(Sys.args(), new Command()).handle(function(o) {}); 9 | } 10 | } 11 | 12 | class Command { 13 | @:command 14 | public var sub = new SubCommand(); 15 | 16 | public function new() {} 17 | 18 | @:defaultCommand 19 | public function run(rest:Rest) { 20 | Sys.println('main $rest'); 21 | } 22 | } 23 | 24 | class SubCommand { 25 | public function new() {} 26 | 27 | @:defaultCommand 28 | public function run(rest:Rest) { 29 | Sys.println('sub $rest'); 30 | } 31 | } -------------------------------------------------------------------------------- /examples/sub/README.md: -------------------------------------------------------------------------------- 1 | # Sub-Command Demo 2 | 3 | #### Example 1 (with main class) 4 | `./run.sh foo bar` 5 | 6 | prints: 7 | 8 | ``` 9 | main [foo,bar] 10 | ``` 11 | 12 | #### Example 2 (without main class) 13 | `./run.sh sub foo bar` 14 | 15 | prints: 16 | 17 | ``` 18 | sub [foo,bar] 19 | ``` -------------------------------------------------------------------------------- /examples/sub/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | haxe -lib tink_cli --run Example $@ -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | # Make sure docs are generated 2 | -D use-rtti-doc -------------------------------------------------------------------------------- /haxe_libraries/ansi.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download haxelib:ansi#1.0.0 into ansi/1.0.0/haxelib 2 | -D ansi=1.0.0 3 | -cp ${HAXESHIM_LIBCACHE}/ansi/1.0.0/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/hxcpp.hxml: -------------------------------------------------------------------------------- 1 | -D hxcpp=4.0.8 2 | # @install: lix --silent download "haxelib:/hxcpp#4.0.8" into hxcpp/4.0.8/haxelib 3 | # @run: haxelib run-dir hxcpp ${HAXE_LIBCACHE}/hxcpp/4.0.8/haxelib 4 | -cp ${HAXE_LIBCACHE}/hxcpp/4.0.8/haxelib/ 5 | -------------------------------------------------------------------------------- /haxe_libraries/hxnodejs.hxml: -------------------------------------------------------------------------------- 1 | -D hxnodejs=6.9.1 2 | # @install: lix --silent download "haxelib:/hxnodejs#6.9.1" into hxnodejs/6.9.1/haxelib 3 | -cp ${HAXE_LIBCACHE}/hxnodejs/6.9.1/haxelib/src 4 | --macro allowPackage('sys') 5 | # should behave like other target defines and not be defined in macro context 6 | --macro define('nodejs') 7 | -------------------------------------------------------------------------------- /haxe_libraries/tink_await.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download haxelib:tink_await#0.1.7 into tink_await/0.1.7/haxelib 2 | -D tink_await=0.1.7 3 | -cp ${HAXESHIM_LIBCACHE}/tink_await/0.1.7/haxelib/src 4 | --macro tink.await.Await.use() 5 | -lib tink_macro 6 | -lib tink_core 7 | -lib tink_syntaxhub -------------------------------------------------------------------------------- /haxe_libraries/tink_chunk.hxml: -------------------------------------------------------------------------------- 1 | -D tink_chunk=0.3.1 2 | # @install: lix --silent download "haxelib:/tink_chunk#0.3.1" into tink_chunk/0.3.1/haxelib 3 | -cp ${HAXE_LIBCACHE}/tink_chunk/0.3.1/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_cli.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download https://github.com/haxetink/tink_cli/archive/5207b1c4c5126ea866ee5b1e8714f6ee658d2888.tar.gz into tink_cli/0.2.1/github/5207b1c4c5126ea866ee5b1e8714f6ee658d2888 2 | -D tink_cli=0.2.1 3 | -cp src 4 | # Make sure docs are generated 5 | -D use-rtti-doc 6 | -lib tink_io 7 | -lib tink_stringly 8 | -lib tink_macro -------------------------------------------------------------------------------- /haxe_libraries/tink_core.hxml: -------------------------------------------------------------------------------- 1 | -D tink_core=1.24.0 2 | # @install: lix --silent download "haxelib:/tink_core#1.24.0" into tink_core/1.24.0/haxelib 3 | -cp ${HAXE_LIBCACHE}/tink_core/1.24.0/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_io.hxml: -------------------------------------------------------------------------------- 1 | -D tink_io=0.7.1 2 | # @install: lix --silent download "haxelib:/tink_io#0.7.1" into tink_io/0.7.1/haxelib 3 | -lib tink_chunk 4 | -lib tink_streams 5 | -cp ${HAXE_LIBCACHE}/tink_io/0.7.1/haxelib/src 6 | -------------------------------------------------------------------------------- /haxe_libraries/tink_macro.hxml: -------------------------------------------------------------------------------- 1 | -D tink_macro=0.18.0 2 | # @install: lix --silent download "haxelib:/tink_macro#0.18.0" into tink_macro/0.18.0/haxelib 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_macro/0.18.0/haxelib/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_priority.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download haxelib:tink_priority#0.1.3 into tink_priority/0.1.3/haxelib 2 | -D tink_priority=0.1.3 3 | -cp ${HAXESHIM_LIBCACHE}/tink_priority/0.1.3/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_streams.hxml: -------------------------------------------------------------------------------- 1 | -D tink_streams=0.3.2 2 | # @install: lix --silent download "haxelib:/tink_streams#0.3.2" into tink_streams/0.3.2/haxelib 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_streams/0.3.2/haxelib/src 5 | # temp for development, delete this file when pure branch merged 6 | -D pure -------------------------------------------------------------------------------- /haxe_libraries/tink_stringly.hxml: -------------------------------------------------------------------------------- 1 | -D tink_stringly=0.4.0 2 | # @install: lix --silent download "haxelib:/tink_stringly#0.4.0" into tink_stringly/0.4.0/haxelib 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_stringly/0.4.0/haxelib/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_syntaxhub.hxml: -------------------------------------------------------------------------------- 1 | -D tink_syntaxhub=0.4.3 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_syntaxhub#8b928af11fb39170dcb7254d02923777cddcc678" into tink_syntaxhub/0.4.3/github/8b928af11fb39170dcb7254d02923777cddcc678 3 | -lib tink_priority 4 | -lib tink_macro 5 | -cp ${HAXE_LIBCACHE}/tink_syntaxhub/0.4.3/github/8b928af11fb39170dcb7254d02923777cddcc678/src 6 | --macro tink.SyntaxHub.use() -------------------------------------------------------------------------------- /haxe_libraries/tink_testrunner.hxml: -------------------------------------------------------------------------------- 1 | -D tink_testrunner=0.6.4 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_testrunner#e1c0a48571812797811d8dde86889f72414980ad" into tink_testrunner/0.6.4/github/e1c0a48571812797811d8dde86889f72414980ad 3 | -lib ansi 4 | -lib tink_macro 5 | -lib tink_streams 6 | -cp ${HAXE_LIBCACHE}/tink_testrunner/0.6.4/github/e1c0a48571812797811d8dde86889f72414980ad/src 7 | -------------------------------------------------------------------------------- /haxe_libraries/tink_unittest.hxml: -------------------------------------------------------------------------------- 1 | -D tink_unittest=0.5.6 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_unittest#fc3cd348015b58df10d417d5fc3ffaa45eb99d30" into tink_unittest/0.5.6/github/fc3cd348015b58df10d417d5fc3ffaa45eb99d30 3 | -lib tink_syntaxhub 4 | -lib tink_testrunner 5 | -cp ${HAXE_LIBCACHE}/tink_unittest/0.5.6/github/fc3cd348015b58df10d417d5fc3ffaa45eb99d30/src 6 | --macro tink.unit.AssertionBufferInjector.use() -------------------------------------------------------------------------------- /haxe_libraries/travix.hxml: -------------------------------------------------------------------------------- 1 | -D travix=0.12.2 2 | # @install: lix --silent download "gh://github.com/back2dos/travix#d9f15d14098deea42a89334b0473f088e670e7de" into travix/0.12.2/github/d9f15d14098deea42a89334b0473f088e670e7de 3 | # @post-install: cd ${HAXE_LIBCACHE}/travix/0.12.2/github/d9f15d14098deea42a89334b0473f088e670e7de && haxe -cp src --run travix.PostDownload 4 | # @run: haxelib run-dir travix ${HAXE_LIBCACHE}/travix/0.12.2/github/d9f15d14098deea42a89334b0473f088e670e7de 5 | -lib tink_cli 6 | -cp ${HAXE_LIBCACHE}/travix/0.12.2/github/d9f15d14098deea42a89334b0473f088e670e7de/src 7 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tink_cli", 3 | "license": "MIT", 4 | "tags": [ 5 | "cross", 6 | "cli" 7 | ], 8 | "classPath": "src", 9 | "contributors": [ 10 | "kevinresol", 11 | "back2dos" 12 | ], 13 | "releasenote": "Fix rest argument handling when `prompt` argument is present", 14 | "version": "0.5.1", 15 | "url": "https://github.com/haxetink/tink_cli/", 16 | "dependencies": { 17 | "tink_io": "", 18 | "tink_stringly": "", 19 | "tink_macro": "" 20 | } 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tink_cli", 3 | "version": "0.0.0", 4 | "description": "", 5 | "scripts": { 6 | "postinstall": "lix download" 7 | }, 8 | "dependencies": {}, 9 | "devDependencies": { 10 | "haxe-travix": "^0.11.2", 11 | "lix": "^15.3.0-alpha.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /playground.hxml: -------------------------------------------------------------------------------- 1 | -cp tests 2 | -main Playground 3 | 4 | -neko bin/neko/playround.n 5 | # -js bin/node/playground.js 6 | # -lib hxnodejs 7 | 8 | 9 | -lib tink_cli -------------------------------------------------------------------------------- /src/tink/Cli.hx: -------------------------------------------------------------------------------- 1 | package tink; 2 | 3 | import haxe.macro.Expr; 4 | import haxe.macro.Context; 5 | 6 | import tink.cli.Prompt; 7 | import tink.cli.Result; 8 | import tink.cli.DocFormatter; 9 | 10 | using tink.CoreApi; 11 | #if macro 12 | using tink.MacroApi; 13 | #end 14 | 15 | class Cli { 16 | public static macro function process(args:ExprOf>, target:ExprOf, ?prompt:ExprOf):ExprOf { 17 | var ct = Context.toComplexType(Context.typeof(target)); 18 | prompt = prompt.ifNull(macro new tink.cli.prompt.RetryPrompt(5)); 19 | return macro new tink.cli.macro.Router<$ct>($target, $prompt).process($args); 20 | } 21 | 22 | public static macro function getDoc(target:ExprOf, ?formatter:ExprOf>):ExprOf { 23 | formatter = formatter.ifNull(macro new tink.cli.doc.DefaultFormatter()); 24 | var doc = tink.cli.Macro.buildDoc(Context.typeof(target), target.pos); 25 | return macro $formatter.format($doc); 26 | } 27 | 28 | public static function exit(result:Outcome) { 29 | switch result { 30 | case Success(_): Sys.exit(0); 31 | case Failure(e): 32 | var message = e.message; 33 | if(e.data != null) message += ', ${e.data}'; 34 | Sys.println(message); Sys.exit(e.code); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/tink/cli/DocFormatter.hx: -------------------------------------------------------------------------------- 1 | package tink.cli; 2 | 3 | interface DocFormatter { 4 | function format(spec:DocSpec):T; 5 | } 6 | 7 | typedef DocSpec = { 8 | doc:String, // class-level doc 9 | commands:Array, 10 | flags:Array, 11 | } 12 | 13 | typedef DocCommand = { 14 | isDefault:Bool, 15 | isSub:Bool, 16 | names:Array, 17 | doc:String, 18 | } 19 | 20 | typedef DocFlag = { 21 | aliases:Array, 22 | names:Array, 23 | doc:String, 24 | } -------------------------------------------------------------------------------- /src/tink/cli/Macro.hx: -------------------------------------------------------------------------------- 1 | package tink.cli; 2 | 3 | import haxe.macro.Expr; 4 | import haxe.macro.Context; 5 | import haxe.macro.Type; 6 | import tink.macro.TypeMap; // https://github.com/haxetink/tink_macro/pull/11 7 | 8 | using Lambda; 9 | using tink.MacroApi; 10 | using tink.CoreApi; 11 | 12 | class Macro { 13 | 14 | static var counter = 0; 15 | static var counters = new TypeMap(); 16 | static var infoCache = new TypeMap(); 17 | static var routerCache = new TypeMap(); 18 | static var docCache = new TypeMap(); 19 | static var TYPE_STRING = Context.getType('String'); 20 | static var TYPE_STRINGLY = Context.getType('tink.Stringly'); 21 | 22 | public static function build() { 23 | switch Context.getLocalType() { 24 | case TInst(_, [type]): 25 | if(!routerCache.exists(type)) routerCache.set(type, buildClass(type)); 26 | return routerCache.get(type); 27 | default: throw 'assert'; 28 | } 29 | } 30 | 31 | public static function buildDoc(type:Type, pos) { 32 | 33 | if(!docCache.exists(type)) { 34 | 35 | var info = preprocess(type, pos); 36 | 37 | var commands = []; 38 | var flags = []; 39 | 40 | function s2e(v:String) return macro $v{v}; 41 | function f2e(fields) return EObjectDecl(fields).at(); 42 | 43 | for(command in info.commands) { 44 | commands.push([ 45 | {field: 'isDefault', expr: macro $v{command.isDefault}}, 46 | {field: 'isSub', expr: macro $v{command.isSub}}, 47 | {field: 'names', expr: macro $a{command.names.map(s2e)}}, 48 | {field: 'doc', expr: macro $v{getCommandDoc(command.field)}}, 49 | ]); 50 | } 51 | 52 | for(flag in info.flags) { 53 | flags.push([ 54 | {field: 'names', expr: macro $a{flag.names.map(s2e)}}, 55 | {field: 'aliases', expr: macro $a{flag.aliases.map(String.fromCharCode).map(s2e)}}, 56 | {field: 'doc', expr: macro $v{flag.field.doc}}, 57 | ]); 58 | } 59 | 60 | var clsname = 'Doc' + getCounter(type); 61 | var def = macro class $clsname { 62 | 63 | static var doc:tink.cli.DocFormatter.DocSpec; 64 | 65 | public static function get() { 66 | if(doc == null) 67 | doc = ${ 68 | EObjectDecl([ 69 | {field: 'doc', expr: macro $v{info.cls.doc}}, 70 | {field: 'commands', expr: macro $a{commands.map(f2e)}}, 71 | {field: 'flags', expr: macro $a{flags.map(f2e)}}, 72 | ]).at() 73 | } 74 | return doc; 75 | } 76 | } 77 | 78 | def.pack = ['tink', 'cli']; 79 | 80 | Context.defineType(def); 81 | 82 | docCache.set(type, macro $p{['tink', 'cli', clsname]}.get()); 83 | 84 | } 85 | 86 | return docCache.get(type); 87 | } 88 | 89 | static function buildClass(type:Type) { 90 | 91 | var info = preprocess(type, Context.currentPos()); 92 | var cls = info.cls; 93 | 94 | // commands 95 | var defCommandCall = switch info.commands.find(function(c) return c.isDefault) { 96 | case null: Context.error('Default command not found, tag a function with @:defaultCommand', cls.pos); 97 | case cmd: buildCommandCall(cmd); 98 | } 99 | 100 | var cmdCases = [{ 101 | values: [macro null], 102 | guard: null, 103 | expr: defCommandCall, 104 | }]; 105 | var fields = []; 106 | for(command in info.commands) { 107 | if(!command.isDefault) cmdCases.push({ 108 | values: [for(name in command.names) macro $v{name}], 109 | guard:null, 110 | expr: buildCommandCall(command), 111 | }); 112 | fields.push(buildCommandField(command)); 113 | } 114 | 115 | // flags 116 | var flagCases = []; 117 | var aliasCases = []; 118 | for(flag in info.flags) { 119 | var name = flag.field.name; 120 | var pos = flag.field.pos; 121 | var access = macro command.$name; 122 | 123 | function getAssignment(type:Type) { 124 | return switch type { 125 | case _.getID() => 'Bool': 126 | macro @:pos(pos) $access = true; 127 | case TInst(_.get() => {pack: [], name: 'Array'}, _): 128 | macro @:pos(pos) { 129 | if($access == null) $access = []; 130 | $access.push((args[++current]:tink.Stringly)); 131 | } 132 | case TLazy(f): 133 | getAssignment(f()); 134 | default: 135 | macro @:pos(pos) $access = (args[++current]:tink.Stringly); 136 | } 137 | } 138 | 139 | var assignment = getAssignment(flag.field.type); 140 | 141 | if(flag.names.length > 0) flagCases.push({ 142 | values: [for(name in flag.names) macro @:pos(pos) $v{name}], 143 | guard: null, 144 | expr: assignment, 145 | }); 146 | 147 | if(flag.aliases.length > 0) aliasCases.push({ 148 | values: [for(alias in flag.aliases) macro @:pos(pos) $v{alias}], 149 | guard: null, 150 | expr: assignment, 151 | }); 152 | } 153 | 154 | // prompt required flags 155 | var requiredFlags = info.flags.filter(function(f) return f.isRequired); 156 | requiredFlags.reverse(); // we build the prompt loop inside out 157 | var promptRequired = requiredFlags 158 | .fold(function(flag, prev) { 159 | var display = flag.names[0]; 160 | var i = 0; 161 | while(display.charCodeAt(i) == '-'.code) i++; 162 | display = display.substr(i); 163 | 164 | var name = flag.field.name; 165 | var access = macro command.$name; 166 | 167 | return macro { 168 | var next = 169 | if($access == null) 170 | prompt.prompt($v{display}) 171 | .next(function(v) { 172 | $access = v; 173 | return tink.core.Noise.Noise.Noise; 174 | }); 175 | else 176 | tink.core.Future.sync(tink.core.Outcome.Success(tink.core.Noise.Noise.Noise)); 177 | 178 | next.handle(function(o) switch o { 179 | case Success(_): $prev; 180 | case Failure(_): cb(o); 181 | }); 182 | } 183 | }, macro cb(tink.core.Outcome.Success(tink.core.Noise.Noise.Noise))); 184 | 185 | // build the type 186 | var path = cls.module.split('.'); 187 | if(path[path.length - 1] != cls.name) path.push(cls.name); 188 | var ct = TPath(path.join('.').asTypePath()); 189 | var clsname = 'Router' + getCounter(type); 190 | var def = macro class $clsname extends tink.cli.Router<$ct> { 191 | 192 | public function new(command, prompt) { 193 | super(command, prompt, $v{info.flags.length > 0}); 194 | } 195 | 196 | override function process(args:Array):tink.cli.Result { 197 | return ${ESwitch(macro args[0], cmdCases, defCommandCall).at()} 198 | } 199 | 200 | override function processFlag(args:Array, index:Int) { 201 | var current = index; 202 | ${ESwitch(macro args[index], flagCases, macro return -1).at()} 203 | return current - index; 204 | } 205 | 206 | override function processAlias(args:Array, index:Int) { 207 | var current = index; 208 | var str = args[index]; 209 | for(i in 1...str.length) 210 | ${ESwitch(macro str.charCodeAt(i), aliasCases, macro throw "Invalid alias '-" + str.charAt(i) + "'").at()} 211 | 212 | return current - index; 213 | } 214 | 215 | override function promptRequired():tink.core.Promise { 216 | return tink.core.Future.async(function(cb) $promptRequired); 217 | } 218 | 219 | } 220 | 221 | def.fields = def.fields.concat(fields); 222 | def.pack = ['tink', 'cli']; 223 | 224 | if(info.aliasDisabled) def.fields.remove(def.fields.find(function(f) return f.name == 'processAlias')); 225 | 226 | Context.defineType(def); 227 | 228 | return TPath('tink.cli.$clsname'.asTypePath()); 229 | } 230 | 231 | static function preprocess(type:Type, pos:Position):ClassInfo { 232 | 233 | if(!infoCache.exists(type)) { 234 | 235 | var cls = switch type { 236 | case TInst(_.get() => cls, _): cls; 237 | default: pos.error('Expected a class instance but got ${type.getName()}'); 238 | } 239 | 240 | var info:ClassInfo = { 241 | aliasDisabled: switch cls.meta.extract(':alias') { 242 | case [{params: [macro false]}]: true; 243 | default: false; 244 | }, 245 | flags: [], 246 | commands: [], 247 | cls: cls, 248 | } 249 | 250 | for(field in cls.fields.get()) if(field.isPublic) { 251 | function addCommand(names:Array, isDefault:Bool, isSub:Bool, skipFlags:SkipFlags) { 252 | for(command in info.commands) { 253 | for(n in names) if(command.names.indexOf(n) != -1) 254 | field.pos.makeFailure('Duplicate command: $n').sure(); 255 | } 256 | info.commands.push({names: names, isDefault: isDefault, isSub: isSub, skipFlags: skipFlags, field: field}); 257 | } 258 | 259 | function addFlag(names:Array, aliases:Array, isRequired:Bool) { 260 | field.meta.remove(':flag'); 261 | field.meta.remove(':alias'); 262 | var usedName = null; 263 | var usedAlias = null; 264 | for(flag in info.flags) { 265 | for(n in names) if(flag.names.indexOf(n) != -1) { 266 | usedName = n; 267 | break; 268 | } 269 | 270 | if(!info.aliasDisabled) for(a in aliases) if(flag.aliases.indexOf(a) != -1) { 271 | usedAlias = a; 272 | break; 273 | } 274 | } 275 | switch [usedName, usedAlias] { 276 | case [null, null]: info.flags.push({names: names, aliases: aliases, isRequired: isRequired, field: field}); 277 | case [null, v]: field.pos.makeFailure('Duplicate flag alias: "-' + String.fromCharCode(v) + '". By default tink_cli uses the first letter of the flag Rename the alias with @:alias() or disable alias with @:alias(false).').sure(); 278 | case [v, _]: field.pos.makeFailure('Duplicate flag name: $v').sure(); 279 | } 280 | } 281 | 282 | // process commands 283 | 284 | var commands = []; 285 | var isDefault = false; 286 | 287 | switch field.meta.extract(':defaultCommand') { 288 | case [v]: isDefault = true; 289 | default: 290 | } 291 | 292 | switch field.meta.extract(':command') { 293 | case []: // not command 294 | case [{params: []}]: 295 | commands.push(field.name); 296 | case [{params: params}]: 297 | for(p in params) commands.push(p.getString().sure()); 298 | case v: 299 | v[1].pos.makeFailure('Multiple @:command meta is not allowed').sure(); 300 | } 301 | 302 | var skipFlags = switch field.meta.extract(':skipFlags') { 303 | case []: 304 | Nil; 305 | case [{params: []}]: 306 | All; 307 | case [{params: params, pos: pos}]: 308 | pos.makeFailure('@:skipFlags with arguments is not supported yet').sure(); 309 | // Some([for(p in params) p.getString().sure()]); 310 | case v: 311 | v[1].pos.makeFailure('Multiple @:skipFlags meta is not allowed').sure(); 312 | } 313 | 314 | if(commands.length > 0 || isDefault) { 315 | addCommand(commands, isDefault, field.kind.match(FVar(_)), skipFlags); 316 | continue; 317 | } 318 | 319 | // process flags 320 | 321 | switch field.kind { 322 | case FVar(_): 323 | var flags = []; 324 | 325 | // determine flag name 326 | switch field.meta.extract(':flag') { 327 | case []: 328 | flags.push('--' + field.name); 329 | case [{params: [macro false]}]: 330 | continue; 331 | case [{params: params}]: 332 | for(p in params) { 333 | var flag = p.getString().sure(); 334 | switch [flag.charCodeAt(0), flag.charCodeAt(1)] { 335 | case ['-'.code, '-'.code]: flags.push(flag); 336 | case ['-'.code, _]: flags.push(flag); info.aliasDisabled = true; 337 | default: flags.push('--$flag'); 338 | } 339 | } 340 | case v: 341 | v[1].pos.makeFailure('Only a single @:flag meta is allowed').sure(); 342 | } 343 | 344 | // determine aliases 345 | var aliases = []; 346 | switch field.meta.extract(':alias') { 347 | case []: 348 | for(flag in flags) 349 | for(i in 0...flag.length) 350 | switch flag.charCodeAt(i) { 351 | case '-'.code: // continue 352 | case v: 353 | if(aliases.indexOf(v) == -1) aliases.push(v); 354 | break; 355 | } 356 | case [{params: params}]: 357 | for(p in params) { 358 | switch p.getIdent() { 359 | case Success('false'): 360 | aliases = []; 361 | break; 362 | default: 363 | var v = p.getString().sure(); 364 | if(v.length == 1) aliases.push(v.charCodeAt(0)); 365 | else p.pos.makeFailure('Alias must be a single letter').sure(); 366 | } 367 | } 368 | case v: 369 | v[1].pos.makeFailure('Only a single @:alias meta is allowed').sure(); 370 | } 371 | 372 | // flag is marked as "required" (will be prompted when missing) if there is no default expr 373 | var isRequired = field.expr() == null && !field.meta.has(':optional'); 374 | if(isRequired && !TYPE_STRINGLY.unifiesWith(field.type) && !TYPE_STRING.unifiesWith(field.type)) { 375 | var type = field.type.toComplex().toString(); 376 | field.pos.error('$type is not supported. Please use a custom abstract to handle it. See https://github.com/haxetink/tink_cli#data-types'); 377 | } 378 | 379 | addFlag(flags, aliases, isRequired); 380 | 381 | case FMethod(_): 382 | } 383 | } 384 | 385 | infoCache.set(type, info); 386 | } 387 | 388 | return infoCache.get(type); 389 | } 390 | 391 | static function buildCommandCall(command:Command) { 392 | var call = macro $i{'run_' + command.field.name}; 393 | if(command.isSub) return macro $call(args.slice(1)); 394 | 395 | var args = command.isDefault ? macro args : macro args.slice(1); 396 | 397 | return macro { 398 | var args = switch processArgs($args) { 399 | case Success(args): args; 400 | case Failure(f): return f; 401 | } 402 | ${switch command.skipFlags { 403 | case Nil: macro promptRequired().next(function(_) return $call(args)); 404 | case All: macro $call(args); 405 | }} 406 | } 407 | } 408 | 409 | static function buildCommandField(command:Command):Field { 410 | return { 411 | access: [], 412 | name: 'run_' + command.field.name, 413 | kind: FFun({ 414 | args: [{ 415 | name: 'args', 416 | type: macro:Array, 417 | }], 418 | ret: macro:tink.cli.Result, 419 | expr: buildCommandForwardCall(command), 420 | }), 421 | pos: command.field.pos, 422 | } 423 | } 424 | 425 | static function buildCommandForwardCall(command:Command) { 426 | var name = command.field.name; 427 | var pos = command.field.pos; 428 | return if(command.isSub) { 429 | macro return tink.Cli.process(args, command.$name); 430 | } else { 431 | function process(type:Type) { 432 | return switch type { 433 | case TLazy(f): process(f()); 434 | case TFun(args, ret): 435 | var expr = switch args { 436 | case []: 437 | macro command.$name(); 438 | default: 439 | var requiredParams = args.length; 440 | var restLocation = -1; 441 | var promptLocation = -1; 442 | 443 | for(i in 0...args.length) { 444 | var arg = args[i]; 445 | switch arg.t.reduce() { 446 | case TAbstract(_.get() => {pack: ['tink', 'cli'], name: 'Rest'}, _): 447 | if(restLocation != -1) command.field.pos.makeFailure('A command can only accept at most one Rest argument').sure(); 448 | requiredParams--; 449 | restLocation = i; 450 | 451 | case _.getID() => 'tink.cli.Prompt': 452 | if(promptLocation != -1) command.field.pos.makeFailure('A command can only accept at most one "prompt" argument').sure(); 453 | requiredParams--; 454 | promptLocation = i; 455 | 456 | default: 457 | 458 | } 459 | } 460 | 461 | 462 | var cargs = []; 463 | var cargsNum = args.length; 464 | if(promptLocation != -1) cargsNum--; 465 | 466 | var expr = macro @:pos(pos) if(args.length < $v{requiredParams}) return tink.core.Outcome.Failure(new tink.core.Error('Insufficient arguments. Expected: ' + $v{requiredParams} + ', Got: ' + args.length)); 467 | 468 | if(restLocation == -1) { 469 | 470 | for(i in 0...cargsNum) cargs.push(macro @:pos(pos) args[$v{i}]); 471 | 472 | } else { 473 | 474 | var breakpoint = restLocation; 475 | var remaining = args.length - restLocation - 1; 476 | if(promptLocation != -1) { 477 | if(restLocation > promptLocation) 478 | breakpoint -= 1; 479 | else 480 | remaining -= 1; 481 | } 482 | 483 | for(i in 0...breakpoint) cargs.push(macro @:pos(pos) args[$v{i}]); 484 | 485 | cargs.push(macro @:pos(pos) args.slice($v{breakpoint}, args.length - $v{remaining})); 486 | 487 | for(i in 0...remaining) cargs.push(macro @:pos(pos) args[args.length - $v{remaining - i}]); 488 | 489 | } 490 | 491 | if(promptLocation != -1) cargs.insert(promptLocation, macro prompt); 492 | 493 | expr = expr.concat(macro command.$name($a{cargs})); 494 | } 495 | 496 | var ret = switch ret.reduce() { 497 | case TAbstract(_.get() => {pack: ['tink', 'core'], name: 'Promise'}, [t]) 498 | | TAbstract(_.get() => {pack: ['tink', 'core'], name: 'Future'}, [TEnum(_.get() => {pack: ['tink', 'core'], name: 'Outcome'}, [t, _])]) 499 | | TEnum(_.get() => {pack: ['tink', 'core'], name: 'Outcome'}, [t, _]) 500 | | TAbstract(_.get() => {pack: ['tink', 'core'], name: 'Future'}, [t]): t; 501 | case t: t; 502 | } 503 | 504 | switch ret { 505 | case v if(v.getID() == 'Void'): 506 | expr = expr.concat(macro tink.core.Noise.Noise.Noise); 507 | case v if(v.getID() == 'tink.core.Noise'): 508 | // ok 509 | case TAnonymous(_): 510 | var ct = ret.toComplex(); 511 | expr = macro ($expr:tink.core.Promise<$ct>); 512 | case _.getID() => id: 513 | var ct = id == null ? macro:tink.core.Noise : ret.toComplex(); 514 | expr = macro ($expr:tink.core.Promise<$ct>); 515 | } 516 | 517 | macro return $expr; 518 | 519 | default: throw 'assert'; 520 | } 521 | } 522 | process(command.field.type); 523 | } 524 | } 525 | 526 | static function getCounter(type:Type) { 527 | if(!counters.exists(type)) counters.set(type, counter++); 528 | return counters.get(type); 529 | } 530 | 531 | static function getCommandDoc(field:ClassField) { 532 | if(field.doc != null) return field.doc; 533 | switch(field.type) { 534 | case TInst(t, params): return t.get().doc; 535 | case _: return null; 536 | } 537 | } 538 | } 539 | 540 | typedef ClassInfo = { 541 | commands:Array, 542 | flags:Array, 543 | aliasDisabled:Bool, 544 | cls:ClassType, 545 | } 546 | 547 | typedef Command = { 548 | names:Array, 549 | isDefault:Bool, 550 | isSub:Bool, 551 | field:ClassField, 552 | skipFlags:SkipFlags, 553 | } 554 | 555 | typedef Flag = { 556 | names:Array, 557 | aliases:Array, 558 | isRequired:Bool, 559 | field:ClassField, 560 | } 561 | 562 | enum SkipFlags { 563 | Nil; 564 | // Some(v:Array), 565 | All; 566 | } 567 | -------------------------------------------------------------------------------- /src/tink/cli/Prompt.hx: -------------------------------------------------------------------------------- 1 | package tink.cli; 2 | 3 | import tink.Stringly; 4 | using tink.CoreApi; 5 | 6 | interface Prompt { 7 | function print(v:String):Promise; 8 | function println(v:String):Promise; 9 | function prompt(type:PromptType):Promise; 10 | } 11 | 12 | abstract PromptType(PromptTypeBase) from PromptTypeBase to PromptTypeBase { 13 | @:from 14 | public static inline function ofString(v:String):PromptType 15 | return Simple(v); 16 | } 17 | 18 | enum PromptTypeBase { 19 | Simple(prompt:String); 20 | MultipleChoices(prompt:String, choices:Array); 21 | Secure(prompt:String); 22 | } -------------------------------------------------------------------------------- /src/tink/cli/Rest.hx: -------------------------------------------------------------------------------- 1 | package tink.cli; 2 | 3 | @:forward 4 | abstract Rest(Array) from Array to Array { 5 | @:to 6 | public inline function asArray():Array 7 | return this; 8 | } -------------------------------------------------------------------------------- /src/tink/cli/Result.hx: -------------------------------------------------------------------------------- 1 | package tink.cli; 2 | 3 | using tink.CoreApi; 4 | 5 | typedef Result = Promise -------------------------------------------------------------------------------- /src/tink/cli/Router.hx: -------------------------------------------------------------------------------- 1 | package tink.cli; 2 | 3 | using tink.CoreApi; 4 | 5 | class Router { 6 | var command:T; 7 | var prompt:Prompt; 8 | var hasFlags:Bool; 9 | 10 | public function new(command, prompt, hasFlags) { 11 | this.command = command; 12 | this.prompt = prompt; 13 | this.hasFlags = hasFlags; 14 | } 15 | 16 | public function process(args:Array):Result { 17 | return Noise; 18 | } 19 | 20 | function processArgs(args:Array):Outcome, Error> { 21 | return 22 | if(!hasFlags) 23 | Success(args); 24 | else 25 | Error.catchExceptions(function() { 26 | var args = expandAssignments(args); 27 | var rest = []; 28 | var i = 0; 29 | var flagsEnded = false; 30 | while(i < args.length) { 31 | var arg = args[i]; 32 | 33 | if(arg == '--') { 34 | flagsEnded = true; 35 | i++; 36 | } else if(!flagsEnded && arg.charCodeAt(0) == '-'.code) { 37 | switch processFlag(args, i) { 38 | case -1: // unrecognized flag 39 | if(arg.charCodeAt(1) != '-'.code) { 40 | switch processAlias(args, i) { 41 | case -1: throw 'Unrecognized alias "$arg"'; 42 | case v: i += v + 1; 43 | } 44 | } else { 45 | throw 'Unrecognized flag "$arg"'; 46 | } 47 | 48 | case v: 49 | i += v + 1; 50 | } 51 | } else { 52 | rest.push(arg); 53 | i++; 54 | } 55 | } 56 | return rest; 57 | }); 58 | } 59 | 60 | function processFlag(args:Array, index:Int) { 61 | return -1; 62 | } 63 | 64 | function processAlias(args:Array, index:Int) { 65 | return -1; 66 | } 67 | 68 | function promptRequired():Promise { 69 | return Noise; 70 | } 71 | 72 | static function expandAssignments(args:Array):Array { 73 | var ret = []; 74 | var flags = true; 75 | for(arg in args) { 76 | if(arg == '--') flags = false; 77 | if(!flags) 78 | ret.push(arg); 79 | else 80 | switch [arg.charCodeAt(0), arg.charCodeAt(1), arg.indexOf('=')] { 81 | case ['-'.code, '-'.code, i] if(i != -1): 82 | ret.push(arg.substr(0, i)); 83 | ret.push(arg.substr(i + 1)); 84 | case _: 85 | ret.push(arg); 86 | } 87 | } 88 | return ret; 89 | } 90 | } -------------------------------------------------------------------------------- /src/tink/cli/doc/DefaultFormatter.hx: -------------------------------------------------------------------------------- 1 | package tink.cli.doc; 2 | 3 | import tink.cli.DocFormatter; 4 | 5 | using Lambda; 6 | using StringTools; 7 | 8 | class DefaultFormatter implements DocFormatter { 9 | 10 | var root:String; 11 | 12 | public function new(?root) { 13 | this.root = root; 14 | } 15 | 16 | public function format(spec:DocSpec):String { 17 | var out = new StringBuf(); 18 | inline function addLine(v:String) out.add(v + '\n'); 19 | 20 | // title 21 | addLine(''); 22 | switch formatDoc(spec.doc) { 23 | case null: 24 | case doc: addLine('$doc\n'); 25 | } 26 | 27 | var subs = spec.commands.filter(function(c) return !c.isDefault); 28 | var flags = []; 29 | 30 | if(root != null) addLine(' Usage: $root'); 31 | 32 | switch spec.commands.find(function(c) return c.isDefault) { 33 | case null: 34 | case defaultCommand: 35 | switch formatDoc(defaultCommand.doc) { 36 | case null: 37 | case doc: addLine(indent(doc, 4) + '\n'); 38 | } 39 | } 40 | 41 | if(subs.length > 0) { 42 | var maxCommandLength = subs.fold(function(command, max) { 43 | for(name in command.names) if(name.length > max) max = name.length; 44 | return max; 45 | }, 0); 46 | 47 | if(root != null) addLine(' Usage: $root '); 48 | addLine(' Subcommands:'); 49 | 50 | function addCommand(name:String, doc:String) { 51 | if(doc == null) doc = '(doc missing)'; 52 | addLine(indent(name.lpad(' ', maxCommandLength) + ' : ' + indent(doc, maxCommandLength + 3).trim(), 6)); 53 | } 54 | 55 | for(command in subs) { 56 | var name = command.names[0]; 57 | addCommand(name, formatDoc(command.doc)); 58 | 59 | if(command.names.length > 1) 60 | for(i in 1...command.names.length) 61 | addCommand(command.names[i], 'Alias of $name'); 62 | } 63 | } 64 | 65 | if(spec.flags.length > 0) { 66 | function nameOf(flag:DocFlag) { 67 | var variants = flag.names.join(', '); 68 | if(flag.aliases.length > 0) variants += ', ' + flag.aliases.map(function(a) return '-$a').join(', '); 69 | return variants; 70 | } 71 | 72 | var maxFlagLength = spec.flags.fold(function(flag, max) { 73 | var name = nameOf(flag); 74 | if(name.length > max) max = name.length; 75 | return max; 76 | }, 0); 77 | 78 | function addFlag(name:String, doc:String) { 79 | if(doc == null) doc = ''; 80 | addLine(indent(name.lpad(' ', maxFlagLength) + ' : ' + indent(doc, maxFlagLength + 3).trim(), 6)); 81 | } 82 | 83 | addLine(''); 84 | addLine(' Flags:'); 85 | 86 | for(flag in spec.flags) { 87 | addFlag(nameOf(flag), formatDoc(flag.doc)); 88 | } 89 | } 90 | 91 | return out.toString(); 92 | } 93 | 94 | function indent(v:String, level:Int) { 95 | return v.split('\n').map(function(v) return ''.lpad(' ', level) + v).join('\n'); 96 | } 97 | 98 | var re = ~/^\s*\*?\s{0,2}(.*)$/; 99 | function formatDoc(doc:String) { 100 | if(doc == null) return null; 101 | var lines = doc.split('\n').map(StringTools.trim); 102 | 103 | // remove empty lines at the beginning and end 104 | while(lines[0] == '') lines = lines.slice(1); 105 | while(lines[lines.length - 1] == '') lines.pop(); 106 | 107 | return lines 108 | .map(function(line) return if(re.match(line)) re.matched(1) else line) // trim off leading asterisks 109 | .join('\n'); 110 | } 111 | } -------------------------------------------------------------------------------- /src/tink/cli/macro/Router.hx: -------------------------------------------------------------------------------- 1 | package tink.cli.macro; 2 | 3 | @:genericBuild(tink.cli.Macro.build()) 4 | class Router {} -------------------------------------------------------------------------------- /src/tink/cli/prompt/DefaultPrompt.hx: -------------------------------------------------------------------------------- 1 | package tink.cli.prompt; 2 | 3 | typedef DefaultPrompt = 4 | #if nodejs 5 | NodePrompt 6 | #elseif sys 7 | SysPrompt 8 | #else 9 | #error "Not Implemented" 10 | #end 11 | ; -------------------------------------------------------------------------------- /src/tink/cli/prompt/IoPrompt.hx: -------------------------------------------------------------------------------- 1 | package tink.cli.prompt; 2 | 3 | import haxe.io.Bytes; 4 | import tink.Stringly; 5 | import tink.io.Sink; 6 | import tink.cli.Prompt; 7 | 8 | using tink.io.Source; 9 | using tink.CoreApi; 10 | 11 | class IoPrompt implements Prompt { 12 | 13 | var source:RealSource; 14 | var sink:RealSink; 15 | 16 | public function new(source, sink) { 17 | this.source = source; 18 | this.sink = sink; 19 | } 20 | 21 | public function print(v:String):Promise { 22 | return (v:IdealSource).pipeTo(sink).map(function(r) return switch r { 23 | case AllWritten: Success(Noise); 24 | default: Failure(Error.withData('Pipe Error', r)); 25 | }); 26 | } 27 | 28 | public function println(v:String):Promise { 29 | return print('$v\n'); 30 | } 31 | 32 | public function prompt(type:PromptType):Promise { 33 | 34 | var secure = false; 35 | var display = switch type { 36 | case Simple(v): '$v: '; 37 | case Secure(v): secure = true; '$v: '; 38 | case MultipleChoices(v, c): '$v [${c.join('/')}]: '; 39 | } 40 | 41 | return (display:IdealSource).pipeTo(sink).flatMap(function(o):Promise return switch o { 42 | case AllWritten: 43 | if(secure) secureInput(display); 44 | else { 45 | var split = source.split('\n'); 46 | source = split.after; 47 | split.before.all() 48 | .next(function(chunk) { 49 | var s = chunk.toString(); 50 | return s.charCodeAt(s.length - 1) == '\r'.code ? s.substr(0, s.length - 1) : s; 51 | }); 52 | } 53 | default: 54 | new Error(''); 55 | }); 56 | } 57 | 58 | function secureInput(prompt:String):Promise 59 | throw 'not implemented'; 60 | } -------------------------------------------------------------------------------- /src/tink/cli/prompt/NodePrompt.hx: -------------------------------------------------------------------------------- 1 | package tink.cli.prompt; 2 | 3 | import tink.io.Sink; 4 | import tink.io.Source; 5 | import tink.Stringly; 6 | import tink.cli.Prompt; 7 | import js.Node.*; 8 | import js.node.Buffer; 9 | 10 | using tink.CoreApi; 11 | 12 | class NodePrompt extends IoPrompt { 13 | public function new() { 14 | super( 15 | Source.ofNodeStream('stdin', process.stdin), 16 | Sink.ofNodeStream('stdout', process.stdout) 17 | ); 18 | } 19 | 20 | override function secureInput(prompt:String):Promise { 21 | return Future.async(function(cb) { 22 | 23 | var rl:Dynamic = js.node.Readline.createInterface({ 24 | input: process.stdin, 25 | output: process.stdout 26 | }); 27 | 28 | function hidden(query, callback) { 29 | var stdin = untyped process.openStdin(); 30 | process.stdin.on('data', function(buf:Buffer) { 31 | var char = buf.toString(); 32 | switch (char) { 33 | case '\n' | '\r' | '\u0004': 34 | stdin.pause(); 35 | default: 36 | process.stdout.write('\033[2K\033[200D' + query); 37 | } 38 | }); 39 | 40 | rl.question(query, function(value) { 41 | rl.history = rl.history.slice(1); 42 | callback(value); 43 | }); 44 | } 45 | 46 | hidden(prompt, function(password:Stringly) cb(Success(password))); 47 | }); 48 | } 49 | } -------------------------------------------------------------------------------- /src/tink/cli/prompt/RetryPrompt.hx: -------------------------------------------------------------------------------- 1 | package tink.cli.prompt; 2 | 3 | import tink.Stringly; 4 | import tink.cli.Prompt; 5 | 6 | using tink.CoreApi; 7 | 8 | class RetryPrompt implements Prompt { 9 | var trials:Int; 10 | var proxy:Prompt; 11 | 12 | public function new(trials, ?proxy:Prompt) { 13 | this.trials = trials; 14 | this.proxy = proxy == null ? new DefaultPrompt() : proxy; 15 | } 16 | 17 | public function print(v:String):Promise { 18 | return proxy.print(v); 19 | } 20 | 21 | public function println(v:String):Promise { 22 | return proxy.println(v); 23 | } 24 | 25 | public function prompt(type:PromptType):Promise { 26 | return switch type { 27 | case Simple(_) | Secure(_): 28 | proxy.prompt(type); 29 | case MultipleChoices(v, c): 30 | Future.async(function(cb) { 31 | var remaining = trials; 32 | function next() { 33 | remaining--; 34 | function retry() { 35 | if(remaining > 0) next(); 36 | else cb(Failure(new Error('Maximum retries reached'))); 37 | } 38 | 39 | proxy.prompt(type).handle(function(o) switch o { 40 | case Success(result): 41 | if(c.indexOf(result) == -1) 42 | retry(); 43 | else 44 | cb(Success(result)); 45 | case Failure(f): 46 | retry(); 47 | }); 48 | } 49 | next(); 50 | }); 51 | } 52 | 53 | } 54 | } -------------------------------------------------------------------------------- /src/tink/cli/prompt/SysPrompt.hx: -------------------------------------------------------------------------------- 1 | package tink.cli.prompt; 2 | 3 | import tink.io.Worker; 4 | import tink.Stringly; 5 | import tink.cli.Prompt; 6 | 7 | using tink.CoreApi; 8 | 9 | class SysPrompt implements Prompt { 10 | 11 | var worker:Worker; 12 | 13 | public function new(?worker:Worker) { 14 | this.worker = worker.ensure(); 15 | } 16 | 17 | public inline function print(v:String):Promise { 18 | return worker.work(function() { 19 | Sys.print(v); 20 | return Success(Noise); 21 | }); 22 | } 23 | 24 | public inline function println(v:String):Promise { 25 | return worker.work(function() { 26 | Sys.println(v); 27 | return Success(Noise); 28 | }); 29 | } 30 | 31 | public function prompt(type:PromptType):Promise { 32 | return worker.work(function() { 33 | return switch type { 34 | case Simple(v): 35 | Sys.print('$v: '); 36 | Success(Sys.stdin().readLine()); 37 | 38 | case Secure(v): 39 | Sys.print('$v: '); 40 | var s = []; 41 | do switch Sys.getChar(false) { 42 | case 10 | 13: 43 | Sys.println(''); 44 | break; 45 | case 3 | 4: // ctrl+C, ctrl+D 46 | Sys.println(''); 47 | Sys.exit(1); 48 | case 127: 49 | s.pop(); 50 | case c if(c >= 0x20): 51 | s.push(c); 52 | case c: 53 | Sys.println(''); 54 | return Failure(new Error('Invalid char $c')); 55 | } while(true); 56 | Success(s.map(String.fromCharCode).join('')); 57 | 58 | case MultipleChoices(v, c): 59 | Sys.print('$v [${c.join('/')}]: '); 60 | Success(Sys.stdin().readLine()); 61 | } 62 | }); 63 | } 64 | } -------------------------------------------------------------------------------- /tests.hxml: -------------------------------------------------------------------------------- 1 | -cp tests 2 | -main RunTests 3 | -lib tink_unittest 4 | 5 | -dce full 6 | -D no-deprecation-warnings -------------------------------------------------------------------------------- /tests/DebugCommand.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | class DebugCommand { 4 | var debug:String; 5 | public function new() {} 6 | public function result() return debug; 7 | } -------------------------------------------------------------------------------- /tests/Playground.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.Cli; 4 | import tink.cli.Prompt; 5 | 6 | using tink.CoreApi; 7 | 8 | class Playground { 9 | static function main() { 10 | Cli.process(Sys.args(), new Playground()).handle(Cli.exit); 11 | } 12 | 13 | public function new() {} 14 | 15 | @:command("ss") public var sub:Sub = new Sub(); 16 | 17 | @:command 18 | public function simple(prompt:Prompt) { 19 | return prompt.prompt(Simple('Input')) 20 | .next(function(input) { 21 | trace(input); 22 | return Noise; 23 | }); 24 | } 25 | 26 | @:command 27 | public function multiple(prompt:Prompt) { 28 | return prompt.prompt(MultipleChoices('Choose one:', ['y','n'])) 29 | .next(function(input) { 30 | trace(input); 31 | return Noise; 32 | }); 33 | } 34 | 35 | @:command 36 | public function secure(prompt:Prompt) { 37 | return prompt.prompt(Secure('Password')) 38 | .next(function(input) { 39 | trace(input); 40 | return Noise; 41 | }); 42 | } 43 | 44 | @:defaultCommand 45 | public function help() { 46 | trace('missing command (simple/multiple/secure)'); 47 | } 48 | } 49 | 50 | class Sub { 51 | public function new() {} 52 | 53 | @:defaultCommand 54 | public function foo() { 55 | trace('foo'); 56 | return Noise; 57 | } 58 | } -------------------------------------------------------------------------------- /tests/RunTests.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | 3 | import tink.testrunner.*; 4 | import tink.unit.*; 5 | 6 | class RunTests { 7 | 8 | static function main() { 9 | 10 | Runner.run(TestBatch.make([ 11 | new TestCommand(), 12 | new TestFlag(), 13 | new TestAliasDisabled(), 14 | new TestPrompt(), 15 | new TestPromptAndRest(), 16 | new TestOptional(), 17 | ])).handle(Runner.exit); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/TestAliasDisabled.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.cli.*; 4 | import tink.Cli; 5 | import tink.unit.Assert.*; 6 | import haxe.ds.StringMap; 7 | 8 | using tink.CoreApi; 9 | 10 | @:asserts 11 | class TestAliasDisabled { 12 | public function new() {} 13 | 14 | @:describe('Single Dash Flag') 15 | public function testSingleDash() { 16 | var command = new AliasCommand(); 17 | return Cli.process(['-path', 'mypath', 'myarg'], command) 18 | .map(function(code) { 19 | asserts.assert('mypath' == command.path); 20 | asserts.assert('run myarg' == command.result()); 21 | return asserts.done(); 22 | }); 23 | } 24 | 25 | @:describe('Alias Disabled') 26 | public function testAliasDisabled() { 27 | return Cli.process(['-p', 'mypath', 'myarg'], new AliasCommand()) 28 | .and(Cli.process(['-n', 'myname', 'myarg'], new AliasCommand())) 29 | .map(function(result) { 30 | asserts.assert(!result.a.isSuccess()); 31 | asserts.assert(!result.b.isSuccess()); 32 | return asserts.done(); 33 | }); 34 | } 35 | 36 | } 37 | 38 | class AliasCommand extends DebugCommand { 39 | 40 | public var name:String = null; 41 | 42 | @:flag('-path') 43 | public var path:String = null; 44 | 45 | @:defaultCommand 46 | public function run(args:Rest) { 47 | debug = 'run ' + args.join(','); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/TestCommand.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.cli.*; 4 | import tink.Cli; 5 | import tink.unit.Assert.*; 6 | 7 | using tink.CoreApi; 8 | 9 | @:asserts 10 | class TestCommand { 11 | public function new() {} 12 | 13 | 14 | @:describe('Default Command') 15 | public function testDefault() { 16 | var command = new EntryCommand(); 17 | 18 | return Cli.process(['arg', 'other'], command) 19 | .map(function(code) return assert(command.result() == 'defaultAction arg,other')); 20 | } 21 | 22 | @:describe('Unnamed Command') 23 | public function testUnnamed() { 24 | var command = new EntryCommand(); 25 | return Cli.process(['install', 'mypath'], command) 26 | .map(function(code) return assert(command.result() == 'install mypath')); 27 | } 28 | 29 | @:describe('Named Command') 30 | public function testNamed() { 31 | var command = new EntryCommand(); 32 | return Cli.process(['uninst', 'mypath', '3'], command) 33 | .map(function(code) return assert(command.result() == 'uninstall mypath 3')); 34 | } 35 | 36 | @:describe('Sub Command') 37 | public function testSub() { 38 | var command = new EntryCommand(); 39 | return Cli.process(['init', 'a', 'b', 'c'], command) 40 | .map(function(code) return assert(command.init.result() == 'defaultInit a,b,c')); 41 | } 42 | 43 | @:describe('Multi Name') 44 | @:variant('multi1') 45 | @:variant('multi2') 46 | public function testMultiName(cmd:String) { 47 | var command = new EntryCommand(); 48 | return Cli.process([cmd], command) 49 | .map(function(_) return assert(command.result() == 'multi')); 50 | } 51 | 52 | @:describe('Rest Arguments') 53 | @:variant(['rest', 'a', 'b'], null) 54 | @:variant(['rest', 'a', 'b', 'c'], 'rest a b c') 55 | @:variant(['rest', 'a', 'b', 'c', 'd'], 'rest a b c d') 56 | @:variant(['rest', 'a', 'b', 'c', 'd', 'e'], 'rest a b c,d e') 57 | public function testRest(cmd:Array, result:String) { 58 | var command = new EntryCommand(); 59 | return Cli.process(cmd, command) 60 | .map(function(_) return assert(command.result() == result)); 61 | } 62 | 63 | @:describe('Const Result') 64 | public function testConst() { 65 | var command = new EntryCommand(); 66 | return Cli.process(['const'], command) 67 | .map(function(o) return assert(o.isSuccess())); 68 | } 69 | 70 | @:describe('Success Result') 71 | public function testSuccess() { 72 | var command = new EntryCommand(); 73 | return Cli.process(['success'], command) 74 | .map(function(o) return assert(o.isSuccess())); 75 | } 76 | 77 | @:describe('Failure Result') 78 | public function testFailure() { 79 | var command = new EntryCommand(); 80 | return Cli.process(['failure'], command) 81 | .map(function(result) return assert(!result.isSuccess())); 82 | } 83 | 84 | @:describe('Future Const Result') 85 | public function testFutureConst() { 86 | var command = new EntryCommand(); 87 | return Cli.process(['futureConst'], command) 88 | .map(function(o) return assert(o.isSuccess())); 89 | } 90 | 91 | @:describe('Future Success Result') 92 | public function testFutureSuccess() { 93 | var command = new EntryCommand(); 94 | return Cli.process(['futureSuccess'], command) 95 | .map(function(o) return assert(o.isSuccess())); 96 | } 97 | 98 | @:describe('Future Failure Result') 99 | public function testFutureFailure() { 100 | var command = new EntryCommand(); 101 | return Cli.process(['futureFailure'], command) 102 | .map(function(result) return assert(!result.isSuccess())); 103 | } 104 | 105 | } 106 | 107 | class EntryCommand extends DebugCommand { 108 | 109 | @:command('init') 110 | public var init = new InitCommand(); 111 | 112 | @:command 113 | public function install(path:String) { 114 | debug = 'install $path'; 115 | } 116 | 117 | @:command('uninst') 118 | public function uninstall(path:String, retries:Int) { 119 | debug = 'uninstall $path $retries'; 120 | } 121 | 122 | @:command('multi1', 'multi2') 123 | public function multi() { 124 | debug = 'multi'; 125 | } 126 | 127 | @:command 128 | public function rest(a:String, b:String, c:Rest, d:String) { 129 | debug = 'rest $a $b ${c.join(',')} $d'; 130 | } 131 | 132 | @:defaultCommand 133 | public function defaultAction(args:Rest) { 134 | debug = 'defaultAction ' + args.join(','); 135 | } 136 | 137 | 138 | @:command public function const() return 'Done'; 139 | @:command public function success() return Success('Done'); 140 | @:command public function failure() return Failure(new Error('Errored')); 141 | @:command public function futureConst() return Future.sync('Done'); 142 | @:command public function futureSuccess() return Future.sync(Success('Done')); 143 | @:command public function futureFailure() return Future.sync(Failure(new Error('Errored'))); 144 | } 145 | 146 | class InitCommand extends DebugCommand{ 147 | @:defaultCommand 148 | public function defaultInit(args:Rest) { 149 | debug = 'defaultInit ' + args.join(','); 150 | } 151 | } -------------------------------------------------------------------------------- /tests/TestFlag.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.cli.*; 4 | import tink.Cli; 5 | import tink.unit.Assert.*; 6 | import haxe.ds.StringMap; 7 | 8 | using tink.CoreApi; 9 | 10 | @:asserts 11 | class TestFlag { 12 | public function new() {} 13 | 14 | @:variant('Flag' (['--name', 'myname', 'myarg'], 'myname', 'run myarg')) 15 | @:variant('Alias' (['-n', 'myname', 'myarg'], 'myname', 'run myarg')) 16 | @:variant('Argument before Flag' (['myarg', '--name', 'myname'], 'myname', 'run myarg')) 17 | @:variant('Argument before Alias' (['myarg', '-n', 'myname'], 'myname', 'run myarg')) 18 | @:variant('Assignment' (['myarg', '--name=myname'], 'myname', 'run myarg')) 19 | @:variant('Double Dashes end Flags' (['--name=myname', '--', '--myarg=foo'], 'myname', 'run --myarg=foo')) 20 | public function flags(args:Array, name:String, result:String) { 21 | var command = new FlagCommand(); 22 | return Cli.process(args, command) 23 | .map(function(code) { 24 | asserts.assert(name == command.name); 25 | asserts.assert(result == command.result()); 26 | return asserts.done(); 27 | }); 28 | } 29 | 30 | @:variant('Renamed' (['--another-name', 'mypath', 'myarg'], 'mypath', 'run myarg')) 31 | @:variant('Renamed Alias' (['-a', 'mypath', 'myarg'], 'mypath', 'run myarg')) 32 | public function renamed(args:Array, path:String, result:String) { 33 | var command = new FlagCommand(); 34 | return Cli.process(args, command) 35 | .map(function(code) { 36 | asserts.assert(path == command.path); 37 | asserts.assert(result == command.result()); 38 | return asserts.done(); 39 | }); 40 | } 41 | 42 | 43 | @:describe('Combined Alias') 44 | @:variant(['-an', 'mypath', 'myname', 'myarg'], 'mypath', 'myname', 'run myarg') 45 | @:variant(['-na', 'myname', 'mypath', 'myarg'], 'mypath', 'myname', 'run myarg') 46 | public function testCombinedAlias(args, path, name, result) { 47 | var command = new FlagCommand(); 48 | return Cli.process(args, command) 49 | .map(function(_) { 50 | asserts.assert(path == command.path); 51 | asserts.assert(name == command.name); 52 | asserts.assert(result == command.result()); 53 | return asserts.done(); 54 | }); 55 | } 56 | 57 | @:describe('Bool Flag') 58 | public function testBool() { 59 | var command = new FlagCommand(); 60 | return Cli.process(['--force', 'myarg'], command) 61 | .map(function(code) { 62 | asserts.assert(command.force); 63 | asserts.assert('run myarg' == command.result()); 64 | return asserts.done(); 65 | }); 66 | } 67 | 68 | @:describe('Int Flag') 69 | public function testInt() { 70 | var command = new FlagCommand(); 71 | return Cli.process(['--int', '123','myarg'], command) 72 | .map(function(code) { 73 | asserts.assert(123 == command.int); 74 | asserts.assert('run myarg' == command.result()); 75 | return asserts.done(); 76 | }); 77 | } 78 | 79 | @:describe('Float Flag') 80 | public function testFloat() { 81 | var command = new FlagCommand(); 82 | return Cli.process(['--float', '1.23', 'myarg'], command) 83 | .map(function(code) { 84 | asserts.assert(1.23 == command.float); 85 | asserts.assert('run myarg' == command.result()); 86 | return asserts.done(); 87 | }); 88 | } 89 | 90 | @:describe('Int Array Flag') 91 | public function testInts() { 92 | var command = new FlagCommand(); 93 | return Cli.process(['--ints', '123', '--ints', '234', '--ints', '456', 'myarg'], command) 94 | .map(function(code) { 95 | asserts.assert('[123,234,456]' == haxe.Json.stringify(command.ints)); 96 | asserts.assert('run myarg' == command.result()); 97 | return asserts.done(); 98 | }); 99 | } 100 | 101 | @:describe('Float Array Flag') 102 | public function testFloats() { 103 | var command = new FlagCommand(); 104 | return Cli.process(['--floats', '1.23', '--floats', '2.34', '--floats', '3.45', 'myarg'], command) 105 | .map(function(code) { 106 | asserts.assert('[1.23,2.34,3.45]' == haxe.Json.stringify(command.floats)); 107 | asserts.assert('run myarg' == command.result()); 108 | return asserts.done(); 109 | }); 110 | } 111 | 112 | @:describe('String Array Flag') 113 | public function testStrings() { 114 | var command = new FlagCommand(); 115 | return Cli.process(['--strings', 'a', '--strings', 'b', '--strings', 'c', 'myarg'], command) 116 | .map(function(code) { 117 | asserts.assert('["a","b","c"]' == haxe.Json.stringify(command.strings)); 118 | asserts.assert('run myarg' == command.result()); 119 | return asserts.done(); 120 | }); 121 | } 122 | 123 | @:describe('Custom Map') 124 | public function testCustomMap() { 125 | var command = new FlagCommand(); 126 | return Cli.process(['--map', 'a=1,b=2,c=3', 'myarg'], command) 127 | .map(function(code) { 128 | asserts.assert('a=>1,b=>2,c=>3' == command.map.toString()); 129 | asserts.assert('run myarg' == command.result()); 130 | return asserts.done(); 131 | }); 132 | } 133 | 134 | @:describe('Multiple Flag Names') 135 | @:variant('--multi1') 136 | @:variant('--multi2') 137 | @:variant('-m') 138 | public function testMultipleFlagNames(cmd) { 139 | var command = new FlagCommand(); 140 | return Cli.process([cmd, 'multi', 'myarg'], command) 141 | .map(function(_) { 142 | asserts.assert('multi' == command.multi); 143 | asserts.assert('run myarg' == command.result()); 144 | return asserts.done(); 145 | }); 146 | } 147 | 148 | @:describe('Multiple Aliases') 149 | @:variant('-x') 150 | @:variant('-y') 151 | @:variant('-z') 152 | public function testMultipleAliases(flag) { 153 | var command = new FlagCommand(); 154 | return Cli.process([flag, 'multi', 'myarg'], command) 155 | .map(function(_) { 156 | asserts.assert('multi' == command.multiAlias); 157 | asserts.assert('run myarg' == command.result()); 158 | return asserts.done(); 159 | }); 160 | } 161 | 162 | 163 | 164 | @:describe('No Alias') 165 | public function testNoAlias() { 166 | var command = new FlagCommand(); 167 | return Cli.process(['-w', 'multi', 'myarg'], command) 168 | .map(function(result) return assert(!result.isSuccess())); 169 | } 170 | } 171 | 172 | class FlagCommand extends DebugCommand { 173 | 174 | public var name:String = null; 175 | 176 | @:flag('another-name') 177 | public var path:String = null; 178 | 179 | @:flag('multi1', 'multi2') 180 | public var multi:String = null; 181 | 182 | @:alias('x', 'y', 'z') 183 | public var multiAlias:String = null; 184 | 185 | @:alias('b') 186 | public var force:Bool = false; 187 | 188 | public var int:Int = 0; 189 | public var float:Float = 0; 190 | 191 | @:flag(false) 192 | public var notFlag:String; 193 | 194 | @:alias('j') 195 | public var ints:Array = null; 196 | @:alias('k') 197 | public var floats:Array = null; 198 | public var strings:Array = null; 199 | 200 | // public var rstrings:Array; 201 | 202 | @:alias('o') 203 | public var map:CustomMap = null; 204 | 205 | @:alias(false) 206 | public var withoutalias:String = null; 207 | 208 | @:defaultCommand 209 | public function run(args:Rest) { 210 | debug = 'run ' + args.join(','); 211 | } 212 | } 213 | 214 | @:forward 215 | abstract CustomMap(StringMap) from StringMap to StringMap { 216 | @:from 217 | public static function fromString(v:String):CustomMap { 218 | var map = new StringMap(); 219 | for(i in v.split(',')) switch i.split('=') { 220 | case [key, value]: map.set(key, (value:tink.Stringly)); 221 | default: throw 'Invalid format'; 222 | } 223 | return map; 224 | } 225 | 226 | public function toString() { 227 | var keys = [for(key in this.keys()) key]; 228 | keys.sort(Reflect.compare); 229 | return [for(key in keys) '$key=>' + this.get(key)].join(','); 230 | } 231 | } -------------------------------------------------------------------------------- /tests/TestOptional.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.Cli; 4 | import tink.cli.*; 5 | import tink.cli.prompt.*; 6 | import tink.io.Sink; 7 | import tink.unit.Assert.*; 8 | import haxe.ds.StringMap; 9 | 10 | using tink.CoreApi; 11 | 12 | @:asserts 13 | class TestOptional { 14 | public function new() {} 15 | 16 | @:describe('Missing mandatory flags should trigger prompt') 17 | public function mandatory() { 18 | var command = new OptionalCommand(); 19 | var random = StringTools.hex(Std.random(0xffff), 4).toLowerCase(); 20 | 21 | return Cli.process([], command, new IoPrompt('$random\n', Sink.BLACKHOLE)) 22 | .map(function(code) { 23 | asserts.assert(command.mandatory == random); 24 | asserts.assert(command.optional == 'default'); 25 | asserts.assert(command.result() == 'mandatory:$random,optional:default'); 26 | return asserts.done(); 27 | }); 28 | } 29 | 30 | @:describe('Missing optional flags should be filled automatically (initialized)') 31 | public function initialized() { 32 | var command = new OptionalCommand(); 33 | var random = StringTools.hex(Std.random(0xffff), 4).toLowerCase(); 34 | 35 | return Cli.process(['--mandatory', random], command) 36 | .map(function(code) { 37 | asserts.assert(command.mandatory == random); 38 | asserts.assert(command.optional == 'default'); 39 | asserts.assert(command.result() == 'mandatory:$random,optional:default'); 40 | return asserts.done(); 41 | }); 42 | } 43 | 44 | @:describe('Missing optional flags should be filled automatically (meta)') 45 | public function meta() { 46 | var command = new MetaOptionalCommand(); 47 | var random = StringTools.hex(Std.random(0xffff), 4).toLowerCase(); 48 | 49 | return Cli.process(['--mandatory', random], command) 50 | .map(function(code) { 51 | asserts.assert(command.mandatory == random); 52 | asserts.assert(command.optional == null); 53 | asserts.assert(command.result() == 'mandatory:$random,optional:null'); 54 | return asserts.done(); 55 | }); 56 | } 57 | } 58 | 59 | class OptionalCommand extends DebugCommand { 60 | 61 | public var mandatory:String; 62 | public var optional:String = 'default'; 63 | 64 | @:defaultCommand 65 | public function run() { 66 | debug = 'mandatory:$mandatory,optional:$optional'; 67 | } 68 | } 69 | 70 | class MetaOptionalCommand extends DebugCommand { 71 | 72 | public var mandatory:String; 73 | @:optional public var optional:String; 74 | 75 | @:defaultCommand 76 | public function run() { 77 | debug = 'mandatory:$mandatory,optional:${optional == null ? 'null' : optional}'; 78 | } 79 | } -------------------------------------------------------------------------------- /tests/TestPrompt.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.io.Source; 4 | import tink.io.Sink; 5 | import tink.cli.Prompt; 6 | import tink.cli.prompt.*; 7 | import tink.unit.Assert.*; 8 | 9 | using tink.CoreApi; 10 | 11 | class TestPrompt { 12 | public function new() {} 13 | 14 | @:describe('Basic Input') 15 | public function testBasic() { 16 | var command = new PromptCommand(); 17 | var prompt = new FakePrompt('y\n'); 18 | return tink.Cli.process(['hi'], command, prompt) 19 | .map(function(_) return assert('y' == command.result())); 20 | } 21 | } 22 | 23 | 24 | class PromptCommand extends DebugCommand { 25 | 26 | @:defaultCommand 27 | public function run(prompt:Prompt):Promise { 28 | var result = prompt.prompt(MultipleChoices('Install?', ['y','n'])); 29 | result.handle(function(o) switch o { 30 | case Success(result): debug = result; 31 | case Failure(e): 32 | }); 33 | return result; 34 | } 35 | } 36 | 37 | class FakePrompt extends IoPrompt { 38 | public function new(src) { 39 | super(src, Sink.BLACKHOLE); 40 | } 41 | } -------------------------------------------------------------------------------- /tests/TestPromptAndRest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.io.Source; 4 | import tink.io.Sink; 5 | import tink.cli.Prompt; 6 | import tink.cli.Rest; 7 | import tink.cli.prompt.*; 8 | import tink.unit.Assert.*; 9 | 10 | using tink.CoreApi; 11 | 12 | class TestPromptAndRest { 13 | public function new() {} 14 | 15 | @:variant('a1 foo bar baz', 'a1:foo,bar,baz') 16 | @:variant('a2 foo bar baz', 'a2:baz,foo,bar') 17 | @:variant('a3 foo bar baz', 'a3:baz,foo,bar') 18 | @:variant('b1 foo bar baz', 'b1:foo,bar,baz') 19 | @:variant('b2 foo bar baz', 'b2:foo,bar,baz') 20 | @:variant('b3 foo bar baz', 'b3:baz,foo,bar') 21 | public function mixed(args:String, result:String) { 22 | var command = new PromptRestCommand(); 23 | return tink.Cli.process(args.split(' '), command) 24 | .map(function(_) return assert(result == command.result())); 25 | } 26 | } 27 | 28 | 29 | class PromptRestCommand extends DebugCommand { 30 | 31 | @:defaultCommand 32 | public function run(prompt:Prompt):Promise return Noise; 33 | 34 | @:command public function a1(a:String, rest:Rest, prompt:Prompt):Promise return handle('a1', a, rest); 35 | @:command public function a2(rest:Rest, a:String, prompt:Prompt):Promise return handle('a2', a, rest); 36 | @:command public function a3(rest:Rest, prompt:Prompt, a:String):Promise return handle('a3', a, rest); 37 | 38 | @:command public function b1(b:String, prompt:Prompt, rest:Rest):Promise return handle('b1', b, rest); 39 | @:command public function b2(prompt:Prompt, b:String, rest:Rest):Promise return handle('b2', b, rest); 40 | @:command public function b3(prompt:Prompt, rest:Rest, b:String):Promise return handle('b3', b, rest); 41 | 42 | function handle(cmd:String, s:String, rest:Rest) { 43 | debug = '$cmd:'; 44 | debug += s; 45 | for(v in rest) debug += ',$v'; 46 | return Noise; 47 | } 48 | } -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | haxe-travix@^0.11.2: 6 | version "0.11.2" 7 | resolved "https://registry.yarnpkg.com/haxe-travix/-/haxe-travix-0.11.2.tgz#8cbe13a3cc178cdc1f0d10f17c5740b998303a00" 8 | 9 | lix@^15.3.0-alpha.1: 10 | version "15.3.0-alpha.1" 11 | resolved "https://registry.yarnpkg.com/lix/-/lix-15.3.0-alpha.1.tgz#8eb1523ee48c7bc1856852be049e2a669c99fdc0" 12 | --------------------------------------------------------------------------------