From 751cd63bc1c2145bd2980f0ab528443190659d8c Mon Sep 17 00:00:00 2001 From: Benjamin Dasnois Date: Sun, 17 Sep 2023 16:27:46 +0200 Subject: [PATCH] Proper publication --- LICENSE.txt | 7 +++ README.md | 78 +++++++++++++++++++++++++++++ haxelib.json | 13 +++++ src/epikowa/cli/Cli.hx | 108 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 haxelib.json create mode 100644 src/epikowa/cli/Cli.hx diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..87300c1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2023 Benjamin Dasnois + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..43010be --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Introduction +This library helps you create command line utilities by providing a command line parser and handler. +It takes inspiration from [tink_cli](https://www.github.com/haxetink/tink_cli) but does not provide prompts nor (currently) use building macros. + +## Why re-build with less features? +Simply because I tried adding tink_cli to one of my projects and it started to severely impair building and VS Code integration. +It may not really have been tink_cli's bad but it felt to me like it was too much overhead for what I needed. + +# How to use +Your users invoke your application and specify an action to run. +You simply write a handler class and mark functions that should serve as action and the one that should serve as the default. +Use `@defaultCommand` and `@command` annotation to do so : + +```haxe +class CliManager { + public function new() { + } + + @defaultCommand + public function startServer() { + } + + @command + public function addUser() { + } + + @command + public function addOrganisation() { + } +} +``` + +Once you've done that, call `epikowa.cli.Cli.parse`: + +```haxe +class Main { + static function main() { + Cli.parse(Sys.args(), new CliManager()); + } +} +``` + +Your users can then call: + +```bash +% yourapp # Runs CliManager.startServer +% yourapp addUser # Run CliManager.addUser +% yourapp addOrganisation # CliManager.addOrganisation +``` + +## Flags and parameters +By setting a flag on the command line, your user can set a value on your handler before the action is executed. +For example, if you have the following: + +```haxe +class CliManager { + public function new() { + } + + @flag + var username:String; + + @defaultCommand + public function sayHello() { + Sys.println('Hello ${username}'); + } +} +``` + +you user may run: + +```sh +% yourapp --username Benjamin +``` + +and sayHello will be called with username set to `Benjamin`. + +___At the moment, only strings are supported for flag's value. They have to be provided. No shorthands or aliases are supported at the moment.___ \ No newline at end of file diff --git a/haxelib.json b/haxelib.json new file mode 100644 index 0000000..3654fb2 --- /dev/null +++ b/haxelib.json @@ -0,0 +1,13 @@ +{ + "name": "epikowa_cli", + "url" : "https://gitlab2.latearrivalstud.io/epikowa/epikowa_cli", + "license": "CECILL-B", + "tags": ["cli"], + "description": "Parse and handle CLI commands.", + "version": "0.5.0", + "classPath": "src/", + "releasenote": "Initial release", + "contributors": ["Benjamin Dasnois"], + "dependencies": { + } + } \ No newline at end of file diff --git a/src/epikowa/cli/Cli.hx b/src/epikowa/cli/Cli.hx new file mode 100644 index 0000000..fb5b18d --- /dev/null +++ b/src/epikowa/cli/Cli.hx @@ -0,0 +1,108 @@ +package epikowa.cli; + +import haxe.macro.PositionTools; +import haxe.macro.Expr.Position; +import haxe.rtti.Meta; + +typedef Error = haxe.macro.Expr.Error; +/** + This class offers CLI tools such as command-line parsing & terminal based UIs +**/ +@:nullSafety(Strict) +class Cli { + static var noOp:Void->Void = () -> { + }; + + /** + Parses a command and runs associated functions. + **/ + public static function parse(params:Array, cliHandler:Any) { + var action:Null = null; + var params = Lambda.array(params); + + var handlerClass = getHandlerClass(cliHandler); + while (params.length > 0) { + var param = params.shift(); + if (param == null) { + throw new Error( 'Param can\'t be null', PositionTools.here()); + } + if (param.indexOf('-') == 0) { + handleFlag(param, params, cliHandler); + } else { + if (action != null) { + throw new Error('Only one action should be provided', PositionTools.here()); + } + + action = param; + } + } + var meta = Meta.getFields(handlerClass); + trace(meta); + if (action == null) { + action = findDefaultCommand(meta); + } + + if (action == null) { + throw new Error('No action provided and no default command set', PositionTools.here()); + } + + if (cliHandler == null) { + throw new Error('cliHandler must not be null', PositionTools.here()); + } + + if (!(Reflect.hasField(meta, action) && (Reflect.hasField(Reflect.field(meta, action), 'command') || Reflect.hasField(Reflect.field(meta, action), 'defaultCommand')))) { + throw new Error('this action does not exist', PositionTools.here()); + } + + Reflect.callMethod(cliHandler, Reflect.field(cliHandler, action ?? '') ?? noOp, []); + } + + static function getFlags(meta:Dynamic>>) { + var flags:Array = []; + for (fieldName in Reflect.fields(meta)) { + var field = Reflect.field(meta, fieldName); + if (Reflect.hasField(field, 'flag')) { + flags.push(fieldName); + } + } + + return flags; + } + + static function handleFlag(param:String, params:Array, cliHandler:Any) { + if (param.indexOf('--') == 0) { + trace('--field ${param}'); + var paramName = param.substr(2); + trace(paramName); + Reflect.setProperty(cliHandler, paramName, params.shift()); + } else if (param.indexOf('-') == 0) { + trace('-field ${param}'); + throw new Error('shorthand params are not supported yet', PositionTools.here()); + } else { + trace('field ${param}'); + throw new Error('param has an unexpected value', PositionTools.here()); + } + } + + static function getHandlerClass(cliHandler:Any) { + var handlerClass:Null> = Type.getClass(cliHandler ?? new Cli()); + if (handlerClass == null || handlerClass == Cli) { + throw new Error('cliHandler has to be an instance of a class', PositionTools.here()); + } + + return handlerClass; + } + + static function findDefaultCommand(meta:Dynamic>>):Null { + for (fieldName in Reflect.fields(meta)) { + var field = Reflect.field(meta, fieldName); + if (Reflect.hasField(field, 'defaultCommand')) { + return fieldName; + } + } + + return null; + } + + public function new() {} +} \ No newline at end of file