From 6c4b2348ace6b082d25591dca80cca94fb062587 Mon Sep 17 00:00:00 2001 From: Hexagon Date: Fri, 15 Mar 2024 01:47:33 +0100 Subject: [PATCH] Add ArgsParser --- .github/workflows/bun.yml | 14 +++++ .github/workflows/node.yml | 22 ++++++++ deno.json | 2 +- mod.ts | 2 +- utils/ansi.test.ts | 21 ++++++- utils/ansi.ts | 15 +++++ utils/args.test.ts | 113 +++++++++++++++++++++++++++++++++++++ utils/args.ts | 105 ++++++++++++++++++++++++++++++++++ 8 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/bun.yml create mode 100644 .github/workflows/node.yml create mode 100644 utils/args.test.ts diff --git a/.github/workflows/bun.yml b/.github/workflows/bun.yml new file mode 100644 index 0000000..a82b62f --- /dev/null +++ b/.github/workflows/bun.yml @@ -0,0 +1,14 @@ +name: Bun CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: antongolub/action-setup-bun@v1.12.8 + with: + bun-version: v1.x # Uses latest bun 1 + - run: bun x jsr add @cross/test @std/assert # Installs dependencies + - run: bun test # Runs the tests \ No newline at end of file diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml new file mode 100644 index 0000000..87b5a41 --- /dev/null +++ b/.github/workflows/node.yml @@ -0,0 +1,22 @@ +name: Node.js CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 21.x] + + steps: + - uses: actions/checkout@v3 + - run: npx jsr add @cross/test @std/assert + - run: "echo '{ \"type\": \"module\" }' > package.json" # Needed for tsx to work + - run: npx --yes tsx --test \ No newline at end of file diff --git a/deno.json b/deno.json index 8460704..ebc4842 100644 --- a/deno.json +++ b/deno.json @@ -8,7 +8,7 @@ "./exit": "./utils/exit.ts" }, "imports": { - "@cross/runtime": "jsr:@cross/runtime@^0.0.16", + "@cross/runtime": "jsr:@cross/runtime@^0.0.17", "@cross/test": "jsr:@cross/test@^0.0.8", "@std/assert": "jsr:@std/assert@^0.219.1" } diff --git a/mod.ts b/mod.ts index fde4c9f..b5f1a08 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,3 @@ export { exit } from "./utils/exit.ts"; -export { args } from "./utils/args.ts"; +export { args, ArgsParser } from "./utils/args.ts"; export { Colors, Cursor, stripAnsi } from "./utils/ansi.ts"; diff --git a/utils/ansi.test.ts b/utils/ansi.test.ts index 42ec141..1a6f20d 100644 --- a/utils/ansi.test.ts +++ b/utils/ansi.test.ts @@ -1,7 +1,7 @@ import { test } from "@cross/test"; import { assertEquals } from "@std/assert"; -import { stripAnsi } from "./ansi.ts"; +import { Colors, Cursor, stripAnsi } from "./ansi.ts"; test("Strip ansi characters", () => { const text = @@ -9,3 +9,22 @@ test("Strip ansi characters", () => { const strippedText = stripAnsi(text); assertEquals(strippedText, "This is colored and some more text"); }); + +test("Apply bold formatting", () => { + assertEquals(Colors.bold("hello"), "\x1b[1mhello\x1b[0m"); +}); + +test("Set foreground color (RGB)", () => { + assertEquals( + Colors.rgb(255, 128, 0, "orange"), + "\x1b[38;2;255;128;0morange\x1b[0m", + ); +}); + +test("Set background color (green)", () => { + assertEquals(Colors.bgGreen("grass"), "\x1b[42mgrass\x1b[0m"); +}); + +test("Move cursor up", () => { + assertEquals(Cursor.up(3), "\x1b[3A"); +}); diff --git a/utils/ansi.ts b/utils/ansi.ts index e27c02d..5d91169 100644 --- a/utils/ansi.ts +++ b/utils/ansi.ts @@ -1,5 +1,11 @@ +/** + * This module provides utilities for styling console output and manipulating the cursor, including + * ANSI color codes, text formatting, cursor movement and screen clearing. + */ + /** * ANSI escape codes for styling text. + * @private */ const enum AnsiCodes { Reset = "\x1b[0m", @@ -31,6 +37,10 @@ const enum AnsiCodes { } export class Colors { + /** + * Contains methods for applying various ANSI text styles to console output. + */ + /** * Applies bold formatting to text. * @param {string} text The text to format. @@ -252,6 +262,11 @@ export class Colors { } export class Cursor { + /** + * Contains methods for controlling cursor behavior in the console, like movement, visibility, + * or clearing the screen. + */ + /** * Moves the cursor up a specified number of lines. * @param {number} lines The number of lines to move up (default: 1). diff --git a/utils/args.test.ts b/utils/args.test.ts new file mode 100644 index 0000000..2090152 --- /dev/null +++ b/utils/args.test.ts @@ -0,0 +1,113 @@ +import { test } from "@cross/test"; +import { assertEquals } from "@std/assert"; + +import { ArgsParser } from "./args.ts"; + +test("Parse arguments using space as separator", () => { + const cmdArgs = ["--port", "8080", "-v", "--configFile", "app.config"]; + const parsedArgs = ArgsParser.parseArgs(cmdArgs); + + assertEquals(parsedArgs, { + args: { + port: ["8080"], + v: [true], + configFile: ["app.config"], + }, + loose: [], + }); +}); + +test("Parse arguments using equal sign as separator", () => { + const cmdArgs = ["--arg=asd"]; + const parsedArgs = ArgsParser.parseArgs(cmdArgs); + + assertEquals(parsedArgs, { + args: { + arg: ["asd"], + }, + loose: [], + }); +}); + +test("Handle flags with no values", () => { + const cmdArgs = ["-v", "--debug"]; + const parsedArgs = ArgsParser.parseArgs(cmdArgs); + + assertEquals(parsedArgs, { + args: { + v: [true], + debug: [true], + }, + loose: [], + }); +}); + +test("Handle an argument at the end", () => { + const cmdArgs = ["--port", "8080", "app.config"]; + const parsedArgs = ArgsParser.parseArgs(cmdArgs); + + assertEquals(parsedArgs, { + args: { + port: ["8080"], + }, + loose: ["app.config"], + }); +}); + +test("Handle empty arguments", () => { + const cmdArgs = ["--flag", ""]; + const parsedArgs = ArgsParser.parseArgs(cmdArgs); + + assertEquals(parsedArgs, { + args: { + flag: [""], + }, + loose: [], + }); +}); + +test("Handle arguments with embedded equals signs", () => { + const cmdArgs = ["--path", "/my/path=with/equals"]; + const parsedArgs = ArgsParser.parseArgs(cmdArgs); + + assertEquals(parsedArgs, { + args: { + path: ["/my/path=with/equals"], + }, + loose: [], + }); +}); + +test("Handle multiple occurrences of a flag", () => { + const cmdArgs = ["-v", "-v", "--config", "prod.config"]; + const parsedArgs = ArgsParser.parseArgs(cmdArgs); + + assertEquals(parsedArgs, { + args: { + v: [true, true], + config: ["prod.config"], + }, + loose: [], + }); +}); + +test("Test ArgsParser methods", () => { + const cmdArgs = [ + "-v", + "-v", + "--port", + "8080", + "--config-file", + "prod.config", + "file.txt", + ]; + const parser = new ArgsParser(cmdArgs); + + assertEquals(parser.getArray("v"), [true, true]); + assertEquals(parser.get("port"), "8080"); + assertEquals(parser.count("config-file"), 1); + assertEquals(parser.count("nonexistent"), 0); + + // Add a method to get loose arguments for completeness (optional) + assertEquals(parser.getLoose(), ["file.txt"]); +}); diff --git a/utils/args.ts b/utils/args.ts index 7a81b57..c2c4b6b 100644 --- a/utils/args.ts +++ b/utils/args.ts @@ -42,3 +42,108 @@ export function args(all: boolean = false): string[] { return []; } } + +export class ArgsParser { + private readonly parsedArgs: Record; + private readonly looseArgs: string[]; + + /** + * Parses command-line arguments. + * + * @param {string[]} cmdArgs The array of command-line arguments. + * + * @returns {Object} An object with the following properties: + * - args: An object where keys represent argument names and values are their corresponding values. + * - loose: An array of loose arguments. + */ + public static parseArgs( + cmdArgs: string[], + ): { args: Record; loose: string[] } { + const parsedArgs: Record = {}; + const looseArgs: string[] = []; + + for (let i = 0; i < cmdArgs.length; i++) { + const arg = cmdArgs[i]; + if (arg.startsWith("--") || arg.startsWith("-")) { + const parts = arg.slice(arg.startsWith("--") ? 2 : 1).split("="); + const key = parts[0]; + let value: string | boolean = true; // Default to boolean for flags + + if (parts.length > 1) { + value = parts[1]; + } else if (i + 1 < cmdArgs.length && !cmdArgs[i + 1].startsWith("-")) { + value = cmdArgs[i + 1]; + i++; + } + + // Handle multiple values for a flag + if (key in parsedArgs) { + const existingValue = parsedArgs[key]; + parsedArgs[key] = Array.isArray(existingValue) + ? [...existingValue, value] + : [existingValue, value]; + } else { + parsedArgs[key] = [value]; + } + } else { + looseArgs.push(arg); + } + } + + return { args: parsedArgs, loose: looseArgs }; + } + + constructor(cmdArgs: string[]) { + const result = ArgsParser.parseArgs(cmdArgs); + this.parsedArgs = result.args; + this.looseArgs = result.loose; + } + + /** + * Retrieves an array of values associated with a given argument name. + * + * @param {string} argName The argument name. + * @returns {(string | boolean)[]} An array of values. Returns an empty array if the argument is not found. + */ + getArray(argName: string): (string | boolean)[] { + return this.parsedArgs[argName] || []; + } + + /** + * Retrieves the first value associated with a given argument name. + * + * @param {string} argName The argument name. + * @returns {string|boolean|undefined} The first value, or undefined if the argument is not found. + */ + get(argName: string): string | boolean | undefined { + const value = this.parsedArgs[argName]; + return Array.isArray(value) ? value[0] : value; + } + + /** + * Counts the occurrences of a given argument name. + * + * @param {string} argName The argument name. + * @returns {number} The number of occurrences (0 if not found). + */ + count(argName: string): number { + const value = this.parsedArgs[argName]; + return Array.isArray(value) ? value.length : (value ? 1 : 0); + } + + /** + * Returns an array of loose arguments + * @returns {string[]} An array of loose arguments + */ + getLoose(): string[] { + return this.looseArgs; // Assuming looseArgs is a private array + } + + /** + * Counts the number of loose arguments + * @returns {number} The number of loose arguments + */ + countLoose(): number { + return this.looseArgs.length; + } +}