Proper publication

This commit is contained in:
Benjamin Dasnois 2023-09-17 16:27:46 +02:00
commit 751cd63bc1
4 changed files with 206 additions and 0 deletions

7
LICENSE.txt Normal file
View File

@ -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.

78
README.md Normal file
View File

@ -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.___

13
haxelib.json Normal file
View File

@ -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": {
}
}

108
src/epikowa/cli/Cli.hx Normal file
View File

@ -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<String>, cliHandler:Any) {
var action:Null<String> = 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<Dynamic<Array<Dynamic>>>) {
var flags:Array<String> = [];
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<String>, 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<Class<Any>> = 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<Dynamic<Array<Dynamic>>>):Null<String> {
for (fieldName in Reflect.fields(meta)) {
var field = Reflect.field(meta, fieldName);
if (Reflect.hasField(field, 'defaultCommand')) {
return fieldName;
}
}
return null;
}
public function new() {}
}