diff --git a/.vscode/settings.json b/.vscode/settings.json index 6de865a..502b715 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { "deno.enable": true, + "editor.formatOnSave": true } \ No newline at end of file diff --git a/README.md b/README.md index 5e655fe..e7559e8 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,7 @@ _Denomander_ is a solution for [Deno](https://deno.land) command-line interfaces. It is inspired from [commander.js](https://github.com/tj/commander.js) by [tj](https://github.com/tj) which is the node's version. -> [__Lizard__ 🦎](https://github.com/siokas/denomander/wiki/Lizard-%F0%9F%A6%8E): There is a new, much simpler and much cleaner, way to define you cli commands and options. It is called [__Lizard__ 🦎](https://github.com/siokas/denomander/wiki/Lizard-%F0%9F%A6%8E) and it is inspired by Laravel's Artisan commands. [For more, follow the instructions...](https://github.com/siokas/denomander/wiki/Lizard-%F0%9F%A6%8E) . - -> Denomander is a [Deno](https://deno.land) project so it needs to have deno installed in your system. +> Denomander is a [Deno](https://deno.land) project so it needs to have deno installed in your system. > If you don't there is a Dockerfile in the root of the project to create an image running deno > To use it just build the Docker file `docker build -t deno .` > Now you can run all the deno commands `docker run --rm -v $PWD:/app/ deno test` @@ -17,33 +15,34 @@ _Denomander_ is a solution for [Deno](https://deno.land) command-line interfaces ## Installation Using Nest Land + ```javascript import Denomander from "https://x.nest.land/denomander@0.7.0/mod.ts"; ``` Using Deno Land + ```javascript import Denomander from "https://deno.land/x/denomander/mod.ts"; ``` ## Usage example -At first initialize the app and optionally you may pass the name, description and version of the app. If not you can change them afterwards by setting the __app_name__, __app_description__ and __app_version__ variables. +At first initialize the app and optionally you may pass the name, description and version of the app. If not you can change them afterwards by setting the **app_name**, **app_description** and **app_version** variables. ```javascript -const program = new Denomander( - { - app_name: "My MY App", - app_description: "My MY Description", - app_version: "1.0.1" - } -); +const program = new Denomander({ + app_name: "My MY App", + app_description: "My MY Description", + app_version: "1.0.1", +}); ``` -There are three option types: __commands__, __options__ and __required options__. +There are three option types: **commands**, **options** and **required options**. ### Options -To set an option just call the __option()__ method passing __a) the sort and the long flag__ seperated by space and __b) the description__. The value can be accessed as properties. + +To set an option just call the **option()** method passing **a) the sort and the long flag** seperated by space and **b) the description**. The value can be accessed as properties. ```javascript program @@ -52,13 +51,13 @@ program .option("-p --port", "Define the port") .parse(Deno.args); - if(program.address){ - const port = program.port || "8000"; - console.log(`Server is running on ${program.address}:${port}`); - } +if (program.address) { + const port = program.port || "8000"; + console.log(`Server is running on ${program.address}:${port}`); +} ``` -__You may define the option's short and long flags by seperating them with either with a) space, b) comma, or c) | (vertical bar or "pipe")__ +**You may define the option's short and long flags by seperating them with either with a) space, b) comma, or c) | (vertical bar or "pipe")** ```javascript program @@ -71,7 +70,8 @@ console.log(`Server is running on ${program.address}:${program.port}`); ``` ### Required Options -The implementation of required option is exactly same as the optional option but you have to call the __requiredOption()__ method instead. + +The implementation of required option is exactly same as the optional option but you have to call the **requiredOption()** method instead. ```javascript program @@ -80,11 +80,13 @@ program .option("-a --address", "Define the address") .parse(Deno.args); - // The port is required so it must have a value - let address = program.address || "localhost"; - console.log(`Server run on ${address}:${program.port}`); +// The port is required so it must have a value +let address = program.address || "localhost"; +console.log(`Server run on ${address}:${program.port}`); ``` + ### Global Options and Base Command Options + You have the option to define options which belong to all commands (global option) and options which belong to no command (base command option ex. --help, --version) ```javascript @@ -124,20 +126,21 @@ program ``` ### Commands -There are two ways to implement the commands. The first is to use an action handler by calling the __action()__ method immediately after the command definition passing the callback function and the second is with custom one-line implementation. __Multiple command arguments are now supported!__ + +There are two ways to implement the commands. The first is to use an action handler by calling the **action()** method immediately after the command definition passing the callback function and the second is with custom one-line implementation. **Multiple command arguments are now supported!** To define a command just call the .command() method and pass the command name (optionally you may also pass the description and a callback function but if not you may define them afterwards in their own methods). After the command you have the option to declare argument(s) inside brackets []. If you want a not required argument just append a question mark (?) after the name of the argument. ```javascript program - .command("mv [from] [to] [message?]", "Start the server") - .action(({from, to, message}:any)=>{ - // Do your actions here - console.log(`File is moved from ${from} to ${to}`); - if(message){ - console.log("message") - } - }); + .command("mv [from] [to] [message?]", "Start the server") + .action(({ from, to, message }: any) => { + // Do your actions here + console.log(`File is moved from ${from} to ${to}`); + if (message) { + console.log("message"); + } + }); program.parse(Deno.args); @@ -152,7 +155,7 @@ program.parse(Deno.args); program .command("clone [foldername]") .description("clone a repo") - .action(({foldername}:any) => { + .action(({ foldername }: any) => { console.log("The repo is cloned into: " + foldername); }); @@ -160,10 +163,11 @@ program.parse(Deno.args); ``` #### Custom Implementation + ```javascript program.command("serve", "Start the server"); -if(program.serve){ +if (program.serve) { console.log("The server has started..."); } @@ -176,54 +180,51 @@ After the command declaration you have the option to declare as many aliases as ```javascript program - .command("serve", "Start the server") - .alias("server", "start-server") - .action(()=>{ - console.log("the server is started"); - }); + .command("serve", "Start the server") + .alias("server", "start-server") + .action(() => { + console.log("the server is started"); + }); program.parse(Deno.args); // Command action calback is called in all 3 command names (actual command and two aliases) ``` - ### Option to change default commands (help, version) -In order to change the default commands (help, version) just call the corresponding method. In case of help pass the command and the description but in case of version you may also pass the actual version of the app and after that the command and the description. +In order to change the default commands (help, version) just call the corresponding method. In case of help pass the command and the description but in case of version you may also pass the actual version of the app and after that the command and the description. ```javascript - program.setVersion( - "1.8.1", - "-x --xversion", - "Display the version of the app" - ); - - program.parse(args); +program.setVersion("1.8.1", "-x --xversion", "Display the version of the app"); + +program.parse(args); ``` ## Customize error messages There are two ways to change the error messages. You may pass a fourth argument in new Denomander() constructor (errors object) or you may call the .errorMessages() method again passing the error messages in object. + 1. + ```javascript -const program = new Denomander( - { - app_name: "My MY App", - app_description: "My MY Description", - app_version: "1.0.1", - errors: { - INVALID_RULE: "Invalid Rule", - OPTION_NOT_FOUND: "Option not found!", - COMMAND_NOT_FOUND: "Command not found!", - REQUIRED_OPTION_NOT_FOUND: "Required option is not specified!", - REQUIRED_VALUE_NOT_FOUND: "Required command value is not specified!", - TOO_MANY_PARAMS: "You have passed too many parameters", - } +const program = new Denomander({ + app_name: "My MY App", + app_description: "My MY Description", + app_version: "1.0.1", + errors: { + INVALID_RULE: "Invalid Rule", + OPTION_NOT_FOUND: "Option not found!", + COMMAND_NOT_FOUND: "Command not found!", + REQUIRED_OPTION_NOT_FOUND: "Required option is not specified!", + REQUIRED_VALUE_NOT_FOUND: "Required command value is not specified!", + TOO_MANY_PARAMS: "You have passed too many parameters", }, -); +}); ``` + 2. + ```javascript program.errorMessages({ INVALID_RULE: "Invalid Rule", @@ -235,25 +236,38 @@ program.errorMessages({ }); ``` +### Improved error experience. Option to throw the errors + +From v0.8 by default Denomander app does not throw the errors but instead it outputs the error message in the console and exits the app. If you want to throw all the errors just pass the `throw_errors: true` option inside the AppDetails in Denomander constructor + +```javascript +const program = new Denomander({ + app_name: "My App Name", + app_description: "My App Description", + app_version: "1.0.1", + throw_errors: true, +}); +``` + ## ToDo -- [X] Custom option processing -- [ ] More examples -- [ ] More tests -- [X] Easy Error Customization -- [ ] Documentation -- [ ] Chanage --help default output -- [X] Command with multiple arguments +- [x] Custom option processing +- [ ] More examples +- [ ] More tests +- [x] Easy Error Customization +- [ ] Documentation +- [ ] Chanage --help default output +- [x] Command with multiple arguments ## Used -- [Deno](https://deno.land) -- [Deno STD Libraries](https://deno.land/std/) -- [FlatIcon](https://www.flaticon.com/) for the logo +- [Deno](https://deno.land) +- [Deno STD Libraries](https://deno.land/std/) +- [FlatIcon](https://www.flaticon.com/) for the logo ## Meta -Apostolos Siokas – [@siokas_](https://twitter.com/siokas_) – apostolossiokas@gmail.com +Apostolos Siokas – [@siokas\_](https://twitter.com/siokas_) – apostolossiokas@gmail.com ## Contributing @@ -261,6 +275,6 @@ Any kind of contribution is welcome! ## License -Distributed under the [MIT License](https://github.com/siokas/denomander/blob/master/LICENSE). +Distributed under the [MIT License](https://github.com/siokas/denomander/blob/master/LICENSE). -[https://github.com/siokas/denomander](https://github.com/siokas/denomander) \ No newline at end of file +[https://github.com/siokas/denomander](https://github.com/siokas/denomander) diff --git a/deps.ts b/deps.ts index 601a95f..5eb6fc2 100644 --- a/deps.ts +++ b/deps.ts @@ -1,15 +1,15 @@ export const test = Deno.test; -export { parse } from "https://deno.land/std@0.81.0/flags/mod.ts"; +export { parse } from "https://deno.land/std@0.83.0/flags/mod.ts"; export { blue, bold, green, red, yellow, -} from "https://deno.land/std@0.81.0/fmt/colors.ts"; +} from "https://deno.land/std@0.83.0/fmt/colors.ts"; export { assert, assertEquals, assertThrows, assertThrowsAsync, -} from "https://deno.land/std@0.81.0/testing/asserts.ts"; +} from "https://deno.land/std@0.83.0/testing/asserts.ts"; diff --git a/src/Command.ts b/src/Command.ts index a7dede3..2f01db9 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -118,9 +118,12 @@ export class Command { return this.aliases.length > 0; } + public requiredCommandArguments(): Array { + return this.command_arguments.filter((commandArg) => commandArg.isRequired); + } + public countRequiredCommandArguments(): number { - return this.command_arguments.filter((commandArg) => commandArg.isRequired) - .length; + return this.requiredCommandArguments().length; } public hasRequiredArguments(): boolean { diff --git a/src/Executor.ts b/src/Executor.ts index 95d6e54..d879f6c 100644 --- a/src/Executor.ts +++ b/src/Executor.ts @@ -9,6 +9,9 @@ import { Validator } from "./Validator.ts"; /** It is responsible for generating the app variables and running the necessary callback functions */ export class Executor { + /** User have the option to throw the errors */ + public throw_errors: boolean; + /** The Arguments instance holding all the arguments passed by the user */ protected args: Arguments; @@ -16,9 +19,10 @@ export class Executor { protected app: Kernel; /** Constructor of Executor object. */ - constructor(app: Kernel, args: Arguments) { + constructor(app: Kernel, args: Arguments, throw_errors: boolean) { this.app = app; this.args = args; + this.throw_errors = throw_errors; } /** It prints the help screen and creates public app properties based on the name of the option */ @@ -53,6 +57,7 @@ export class Executor { rules: [ ValidationRules.REQUIRED_VALUES, ], + throw_errors: this.throw_errors, }).validate(); this.args.commands.forEach((arg: string, key: number) => { @@ -86,6 +91,7 @@ export class Executor { ValidationRules.NON_DECLEARED_ARGS, ValidationRules.REQUIRED_OPTIONS, ], + throw_errors: this.throw_errors, }).validate(); for (const key in this.args.options) { const command: Command | undefined = Util.findCommandFromArgs( @@ -119,6 +125,7 @@ export class Executor { rules: [ ValidationRules.ON_COMMANDS, ], + throw_errors: this.throw_errors, }).validate(); this.app.on_commands.forEach((onCommand) => { diff --git a/src/Kernel.ts b/src/Kernel.ts index 6ba9d91..322c097 100644 --- a/src/Kernel.ts +++ b/src/Kernel.ts @@ -8,6 +8,7 @@ import { AppDetails, CustomArgs, DenomanderErrors, + KernelAppDetails, OnCommand, OptionBuilder, ValidationRules, @@ -37,6 +38,7 @@ export abstract class Kernel { /** Holds all the available .on() commands */ public on_commands: Array = []; + /** Holds all the command aliases */ public aliases: Array = []; /** If the user has defined a custom help */ @@ -46,17 +48,18 @@ export abstract class Kernel { public isVersionConfigured = false; /** The arguments object instance */ - public args: Arguments | undefined; + public args?: Arguments; /** The name of the app */ - public _app_name: string; + public _app_name?: string; /** The description of the app */ - public _app_description: string; + public _app_description?: string; /** The version of the app */ - public _app_version: string; + public _app_version?: string; + /** The version Option instance */ public versionOption: Option; /** The base command is needed to hold the default options like --help, --version */ @@ -67,6 +70,7 @@ export abstract class Kernel { /** Arguments passed by the user during runtime */ protected _args: CustomArgs = {}; + /** Default errors if no errors passed by user */ public errors: DenomanderErrors = { INVALID_RULE: "Invalid Rule", OPTION_NOT_FOUND: "Option not found!", @@ -76,8 +80,11 @@ export abstract class Kernel { TOO_MANY_PARAMS: "You have passed too many parameters", }; + /** User have the option to throw the errors. by default it is not enabled */ + public throw_errors = false; + /** Constructor of AppDetails object */ - constructor(app_details?: AppDetails) { + constructor(app_details?: KernelAppDetails) { if (app_details) { this._app_name = app_details.app_name; this._app_description = app_details.app_description; @@ -85,10 +92,9 @@ export abstract class Kernel { if (app_details.errors) { this.errors = app_details.errors; } - } else { - this._app_name = "My App"; - this._app_description = "My Description"; - this._app_version = "0.0.1"; + if (app_details.throw_errors) { + this.throw_errors = app_details.throw_errors; + } } this.versionOption = this.BASE_COMMAND.addOption( @@ -98,7 +104,7 @@ export abstract class Kernel { /** Getter of the app name */ public get app_name(): string { - return this._app_name; + return this._app_name || "My App"; } /** Setter of the app name */ @@ -108,7 +114,7 @@ export abstract class Kernel { /** Getter of the app description*/ public get app_description(): string { - return this._app_description; + return this._app_description || "My Description"; } /** Setter of the app description */ @@ -118,7 +124,7 @@ export abstract class Kernel { /** Getter of the app version */ public get app_version(): string { - return this._app_version; + return this._app_version || "0.0.1"; } /** Setter of the app version */ @@ -142,7 +148,7 @@ export abstract class Kernel { /** Executes default commands (--help, --version) */ protected execute(): Kernel { if (this.args) { - new Executor(this, this.args) + new Executor(this, this.args, this.throw_errors) .onCommands() .defaultCommands() .commandValues() @@ -155,9 +161,9 @@ export abstract class Kernel { /** Passes the details and commands to prints the help screen */ protected printDefaultHelp(): void { const app_details: AppDetails = { - app_name: this._app_name, - app_description: this._app_description, - app_version: this._app_version, + app_name: this.app_name, + app_description: this.app_description, + app_version: this.app_version, }; Util.print_help(app_details, this.commands, this.BASE_COMMAND); @@ -202,6 +208,7 @@ export abstract class Kernel { rules: [ ValidationRules.BASE_COMMAND_OPTIONS, ], + throw_errors: this.throw_errors, }, ).validate(); diff --git a/src/Logger.ts b/src/Logger.ts new file mode 100644 index 0000000..0d70700 --- /dev/null +++ b/src/Logger.ts @@ -0,0 +1,45 @@ +import { blue, green, red, yellow } from "../deps.ts"; + +export function log(text: string) { + return colored_output(text, "normal"); +} + +export function error_log(text: string, command?: string) { + if (command) { + const error_text = `❌ Error: ${command} | ${text}`; + return colored_output(error_text, "red"); + } + return colored_output("❌ " + text, "red"); +} + +export function success_log(text: string) { + return colored_output(text, "green"); +} + +export function warning_log(text: string) { + return colored_output("⚠️" + text, "yellow"); +} + +function colored_output(text: string, color: string = "normal") { + switch (color) { + case "red": + console.log(red(text)); + break; + + case "green": + console.log(green(text)); + break; + + case "yellow": + console.log(yellow(text)); + break; + + case "blue": + console.log(blue(text)); + break; + + default: + console.log(text); + break; + } +} diff --git a/src/Validator.ts b/src/Validator.ts index a0b4c1d..361dc44 100644 --- a/src/Validator.ts +++ b/src/Validator.ts @@ -2,6 +2,7 @@ import { Util } from "./Util.ts"; import { Arguments } from "./Arguments.ts"; import { Kernel } from "./Kernel.ts"; import { Command } from "./Command.ts"; +import { error_log } from "./Logger.ts"; import { ValidatorContract } from "./interfaces.ts"; import { OnCommand, @@ -24,18 +25,28 @@ export class Validator implements ValidatorContract { /** The array of rules for validation */ public rules: Array; + /** User have the option to throw the errors */ + public throw_errors: boolean; + /** Constructor of the Validator object */ constructor(options: ValidatorOptions) { this.app = options.app; this.args = options.args; this.rules = options.rules; + this.throw_errors = options.throw_errors; } /** It starts the validation process and throws the first error */ public validate() { const failed = this.failed(); if (failed.length) { - throw failed[0].error; + if (this.throw_errors) { + throw failed[0].error; + } + const error_message = failed[0].error?.message || ""; + const error_command = failed[0].command; + error_log(error_message, error_command); + Deno.exit(1); } } @@ -79,11 +90,19 @@ export class Validator implements ValidatorContract { .nonDeclearedOptionArgs(); if (commandArgs.error) { - return { passed: false, error: commandArgs.error }; + return { + passed: false, + error: commandArgs.error, + command: commandArgs.command || "", + }; } if (optionArgs.error) { - return { passed: false, error: optionArgs.error }; + return { + passed: false, + error: optionArgs.error, + command: optionArgs.command || "", + }; } return { passed: true }; @@ -100,14 +119,15 @@ export class Validator implements ValidatorContract { ); if (command && command.hasRequiredOptions()) { - const found = command.requiredOptions.filter((option: Option) => { - return Util.optionIsInArgs(option, this.args); + const notFound = command.requiredOptions.filter((option: Option) => { + return !Util.optionIsInArgs(option, this.args); }); - if (!found.length) { + if (notFound.length) { result = { passed: false, error: new Error(this.app.errors.REQUIRED_OPTION_NOT_FOUND), + command: `(${notFound[0]?.flags})`, }; } } @@ -127,12 +147,16 @@ export class Validator implements ValidatorContract { ); if (command && command.hasRequiredArguments()) { + const commandRequiredArgs = command.requiredCommandArguments(); if ( - command.countRequiredCommandArguments() >= this.args.commands.length + commandRequiredArgs.length >= this.args.commands.length ) { + const command = + commandRequiredArgs[commandRequiredArgs.length - 1].argument; result = { passed: false, error: new Error(this.app.errors.REQUIRED_VALUE_NOT_FOUND), + command: `[${command}]`, }; } } @@ -161,6 +185,7 @@ export class Validator implements ValidatorContract { result = { passed: false, error: new Error(this.app.errors.COMMAND_NOT_FOUND), + command: onCommand.arg, }; } }); @@ -202,6 +227,7 @@ export class Validator implements ValidatorContract { result = { passed: false, error: new Error(this.app.errors.COMMAND_NOT_FOUND), + command: arg, }; } }); @@ -229,11 +255,11 @@ export class Validator implements ValidatorContract { command.options, key, ); - if (!found_in_base_commands && !found_in_all_commands) { result = { passed: false, error: new Error(this.app.errors.OPTION_NOT_FOUND), + command: `(${key})`, }; } } @@ -256,6 +282,7 @@ export class Validator implements ValidatorContract { result = { passed: false, error: new Error(this.app.errors.OPTION_NOT_FOUND), + command: `(${key})`, }; } } diff --git a/src/types.ts b/src/types.ts index 60c4789..b61a25e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,12 +2,22 @@ import { Command } from "./Command.ts"; import { Kernel } from "./Kernel.ts"; import { Arguments } from "./Arguments.ts"; +/** Defines the app detail types. Applied only in Kernel constuctor */ +export type KernelAppDetails = { + app_name?: string; + app_description?: string; + app_version?: string; + errors?: DenomanderErrors; + throw_errors?: boolean; +}; + /** Defines the app detail types */ export type AppDetails = { app_name: string; app_description: string; app_version: string; errors?: DenomanderErrors; + throw_errors?: boolean; }; /** Defines the .on() command options */ @@ -44,6 +54,7 @@ export type CommandTypes = { export type ValidationResult = { passed: boolean; error?: Error; + command?: string; }; /* Defines the validator options for the constructor */ @@ -51,6 +62,7 @@ export type ValidatorOptions = { app: Kernel; args: Arguments; rules: Array; + throw_errors: boolean; }; /** Defines the Command constactor options */ diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 7fd88e1..f78b625 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -57,15 +57,3 @@ test("arrays_have_matching_command", function () { true, ); }); - -// test("contain_command_in_on_commands_array", function () { -// const helpCommand = new Command( -// { value: "-h --help", description: "Helper of the app" }, -// ); - -// const array1: Array = [ -// { command: helpCommand, callback: () => {} }, -// ]; - -// assertEquals(Util.containCommandInOnCommandArray(helpCommand, array1), true); -// }); diff --git a/tests/validations.test.ts b/tests/validations.test.ts index 0c5f830..fcd9287 100644 --- a/tests/validations.test.ts +++ b/tests/validations.test.ts @@ -1,8 +1,8 @@ -import { assertThrows, test } from "../deps.ts"; +import { assertEquals, assertThrows, test } from "../deps.ts"; import { Denomander } from "../src/Denomander.ts"; -test("validation_option_not_found", function () { - const program = new Denomander(); +test("validation_option_not_found_throws_error", function () { + const program = new Denomander({ throw_errors: true }); const optionArgs = ["serve", "-a", "127.0.0.1"]; assertThrows( @@ -17,8 +17,8 @@ test("validation_option_not_found", function () { ); }); -test("validation_command_not_found", function () { - const program = new Denomander(); +test("validation_command_not_found_throws_error", function () { + const program = new Denomander({ throw_errors: true }); const optionArgs = ["wrongCommand", "-p", "80"]; assertThrows( @@ -33,8 +33,8 @@ test("validation_command_not_found", function () { ); }); -test("validation_required_option", function () { - const program = new Denomander(); +test("validation_required_option_throws_error", function () { + const program = new Denomander({ throw_errors: true }); const args = ["serve"]; assertThrows( @@ -47,8 +47,8 @@ test("validation_required_option", function () { ); }); -test("validation_command_with_required_argument", function () { - const program = new Denomander(); +test("validation_command_with_required_argument_throws_error", function () { + const program = new Denomander({ throw_errors: true }); const args = ["clone"]; assertThrows(