From b560a965282765b7a4eb5512242d165319c1e61a Mon Sep 17 00:00:00 2001 From: umang01-hash Date: Wed, 9 Jul 2025 11:45:59 +0530 Subject: [PATCH 1/7] udpdate release version to v1.42.3 --- pkg/gofr/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index afb143154..0169db196 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.42.2" +const Framework = "v1.42.3" From bad33fcc0ada834706bb1155b2fb98e453f0d4f0 Mon Sep 17 00:00:00 2001 From: Umang01-hash Date: Fri, 1 Aug 2025 15:48:22 +0530 Subject: [PATCH 2/7] update release version to v1.42.5 --- pkg/gofr/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index 71593c341..bf8c5414e 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.42.4" +const Framework = "v1.42.5" From ffba79ff43dd91197c4a68db5ac2a96e5514493b Mon Sep 17 00:00:00 2001 From: Umang01-hash Date: Tue, 5 Aug 2025 18:32:15 +0530 Subject: [PATCH 3/7] update release version to v1.43.0 --- pkg/gofr/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index bf8c5414e..429f02ea7 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.42.5" +const Framework = "v1.43.0" From ca58ff398298791c702098c29e5a6c0bd6202470 Mon Sep 17 00:00:00 2001 From: Umang01-hash Date: Thu, 21 Aug 2025 17:33:52 +0530 Subject: [PATCH 4/7] update release version to v1.44.0 --- pkg/gofr/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index 429f02ea7..71a9e6543 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.43.0" +const Framework = "v1.44.0" From f8ded883fc3458a3be466661e0a3b29a18554897 Mon Sep 17 00:00:00 2001 From: Aryaman Vohra Date: Wed, 27 Aug 2025 23:58:54 +0530 Subject: [PATCH 5/7] Add comprehensive CLI documentation and examples - Add complete CLI documentation (docs/cli/cli.md) - Add CLI examples with manual argument parsing - Add navigation support for CLI docs - Include both single and multi-subcommand examples - Document current GoFr version limitations and manual parsing approach --- docs/cli/cli.md | 427 ++++++++++++++++++ docs/navigation.js | 7 + examples/cli-example/README.md | 119 +++++ .../example1-mutli-sub-command/main.go | 58 +++ .../example2-single-sub-command/main.go | 46 ++ go.work.sum | 11 + 6 files changed, 668 insertions(+) create mode 100644 docs/cli/cli.md create mode 100644 examples/cli-example/README.md create mode 100644 examples/cli-example/example1-mutli-sub-command/main.go create mode 100644 examples/cli-example/example2-single-sub-command/main.go diff --git a/docs/cli/cli.md b/docs/cli/cli.md new file mode 100644 index 000000000..f81a2583f --- /dev/null +++ b/docs/cli/cli.md @@ -0,0 +1,427 @@ +# Building CLI Applications with GoFr + +GoFr provides a robust and straightforward way to build **command-line applications**. By leveraging `app.NewCMD()`, you gain access to a powerful framework that simplifies the process of defining custom commands, handling flags, managing configuration, and orchestrating execution flows for your CLI tools. + +This approach offers a clear separation from HTTP services, allowing you to focus purely on command-line interactions and logic. + +## Table of Contents + +- [Introduction](#introduction) +- [Getting Started: Your First GoFr CLI App](#getting-started-your-first-gofr-cli-app) + - [Understanding the `Handler` Function](#understanding-the-handler-function) + - [Using Command Options: `AddDescription` and `AddHelp`](#using-command-options-adddescription-and-addhelp) +- [Running the CLI Application](#running-the-cli-application) +- [Demo Run](#demo-run) +- [Error Handling with Exit Codes](#error-handling-with-exit-codes) +- [Logging in CLI Applications](#logging-in-cli-applications) + - [Log Levels](#log-levels) + - [Using the Logger in Handlers](#using-the-logger-in-handlers) + - [Controlling Log Level via Environment Variable](#controlling-log-level-via-environment-variable) +- [Testing CLI Commands](#testing-cli-commands) +- [Comparison Table: GoFr HTTP vs. CLI Applications](#comparison-table-gofr-http-vs-cli-applications) +- [Best Practices for GoFr CLI Applications](#best-practices-for-gofr-cli-applications) +- [Distinction: `gofr-cli` vs. `app.NewCMD()`](#distinction-gofr-cli-vs-appnewcmd) +- [Navigation Note](#navigation-note) + +## Introduction + +In GoFr, applications are typically initialized in two primary ways: + +- **`app.New()`**: This function is dedicated to creating **HTTP services**, such as REST APIs, web applications, and microservices. It automatically sets up an HTTP server, handles routing for web requests, integrates middleware, and provides a full suite of HTTP-specific functionalities for building networked applications. + +- **`app.NewCMD()`**: This function is specifically designed for building **standalone command-line interface (CLI) applications**. Unlike `app.New()`, it does not initialize or start an HTTP server. Instead, `app.NewCMD()` provides the necessary infrastructure to define distinct subcommands, associate them with handler functions, parse command-line arguments and flags, and execute logic directly from the terminal. It's ideal for automation, scripting, and developer tools. + +⚠️ **Version Note**: This example works with the current stable GoFr version in which automatic argument parsing (`ctx.Args()` / `ctx.Flags()`) is not yet available. Therefore, arguments and flags are parsed manually using `os.Args`. Future GoFr releases may provide built-in flag parsing, but this approach is fully functional for open-source use and ensures compatibility. + +## Getting Started: Your First GoFr CLI App + +Let's begin by creating a simple CLI application that performs basic arithmetic operations (addition and subtraction) using subcommands. + +First, ensure your Go module is initialized and GoFr is added as a dependency: + +```bash +go mod init github.com/example/mycli +go get gofr.dev +``` + +Next, create a `main.go` file (e.g., in your project's root) with the following content: + +```go +package main + +import ( + "fmt" + "os" // Required for os.Args to manually parse arguments + "strings" // Required for string manipulation in argument parsing + + "gofr.dev/pkg/gofr" +) + +// helper function to parse flags manually +// This function is necessary because this GoFr version does not support automatic flag parsing +// via `ctx.Args()` or `ctx.Flags()`. +func getFlagValue(args []string, flag string, defaultValue string) string { + for i := 0; i < len(args); i++ { + if args[i] == flag && i+1 < len(args) { + return args[i+1] + } + } + return defaultValue +} + +func main() { + app := gofr.NewCMD() + + // ----------------------- + // Subcommand: hello + // ----------------------- + app.SubCommand("hello", func(ctx *gofr.Context) (any, error) { + // In this GoFr version, argument parsing is done manually. + // os.Args[0] is the executable name, os.Args[1] is the subcommand ("hello"). + // So, actual arguments for the subcommand start from index 2. + args := os.Args[2:] + + // Attempt to get "name" from a flag like "--name John" + name := getFlagValue(args, "--name", "") + + // If no --name flag, fallback to a positional argument (e.g., "hello John") + if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { + name = args[0] + } + + if name == "" { + name = "World" // Default value if no argument is provided + } + + ctx.Out.Println(fmt.Sprintf("Hello, %s!", name)) + return nil, nil + }) + + // ----------------------- + // Subcommand: goodbye + // ----------------------- + app.SubCommand("goodbye", func(ctx *gofr.Context) (any, error) { + args := os.Args[2:] // arguments after "goodbye" + name := getFlagValue(args, "--name", "") + if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { + name = args[0] + } + if name == "" { + name = "Friend" + } + ctx.Out.Println(fmt.Sprintf("Goodbye, %s!", name)) + return nil, nil + }) + + // Run the CLI application. This will parse arguments and execute the matching subcommand. + app.Run() +} +``` +**Find this example and more in the `examples/cli-example` directory.** + +### Understanding the `Handler` Function + +The handler function for `app.SubCommand` has the signature `func(ctx *gofr.Context) (any, error)`. + +- **`ctx *gofr.Context`**: This is the GoFr context object, which provides access to: + - **Command-line parameters (manual parsing)**: In this GoFr version, `ctx.Args()` and `ctx.Flags()` are not available for automatic argument parsing. Instead, you will manually parse `os.Args` within your handler. You can use helper functions like `getFlagValue(args []string, flag string, defaultValue string)` (as shown in the example) to extract flag values. Positional arguments also need to be parsed from `os.Args`. The example `main.go` demonstrates handling both `--name` flags and positional arguments. + - **Logger**: `c.Logger` for structured logging. + - **Standard Output**: `ctx.Out.Println()` can be used to print directly to standard output. + - **Other dependencies**: Access to databases, services, and configuration registered with the GoFr application. +- **`any`**: The return value that will be printed to `stdout` upon successful execution of the command. +- **`error`**: If the handler returns a non-nil error, the application will print the error message to `stderr` and exit with a non-zero status code. This is crucial for scripting. + +### Using Command Options: `AddDescription` and `AddHelp` + +When defining subcommands, you can provide additional `Options` to enhance the user experience and make your CLI tool self-documenting: + +- **`gofr.AddDescription(description string)`**: Provides a brief, one-line summary of what the subcommand does. This description is displayed when a user runs the main command without any arguments or with a global `--help` flag. +- **`gofr.AddHelp(helpString string)`**: Offers more detailed usage information specifically for that subcommand. This comprehensive help text is shown when a user runs the subcommand with the `--help` or `-h` flag (e.g., `./mycli add --help`). + +These options are crucial for creating user-friendly and discoverable CLI tools. + +## Running the CLI Application + +To make your CLI application executable, you first need to build it: + +```bash +go build -o mycli +``` +This command compiles your `main.go` file and creates an executable named `mycli` (or `mycli.exe` on Windows) in your current directory. + +## Demo Run + +Here's a sample run demonstrating how the CLI operates, including how to access help and use different argument styles: + +```bash +# Run the 'hello' subcommand with no arguments (uses default) +$ ./mycli hello +Hello, World! + +# Run the 'hello' subcommand with a positional argument +$ ./mycli hello Jane +Hello, Jane! + +# Run the 'hello' subcommand with a --name flag +$ ./mycli hello --name John +Hello, John! + +# Run the 'goodbye' subcommand with no arguments (uses default) +$ ./mycli goodbye +Goodbye, Friend! + +# Run the 'goodbye' subcommand with a positional argument +$ ./mycli goodbye Mark +Goodbye, Mark! + +# Run the 'goodbye' subcommand with a --name flag +$ ./mycli goodbye --name Susan +Goodbye, Susan! + +# Access global help (shows descriptions of all subcommands) +$ ./mycli --help +Available commands: + + hello + Description: + + goodbye + Description: +``` +Note: Both positional arguments (e.g., "hello Jane") and flags (e.g., "hello --name John") are supported, but flags are parsed manually using the helper function `getFlagValue`. + +This demo illustrates how arguments are passed, how subcommands are executed, and how built-in help features work, reflecting the manual parsing approach. + +## Error Handling with Exit Codes + +For robust CLI applications, it's essential to communicate success or failure through standard exit codes. A `0` exit code indicates success, while any non-zero code signals an error. GoFr automatically handles returning a non-zero exit code if your handler returns an `error`. + +However, for specific error conditions like invalid arguments, you might want to explicitly exit with a particular code, such as `2` for incorrect usage. You can achieve this within your handler: + +```go +// Example of a handler explicitly exiting on validation error +app.SubCommand("mycommand", func(c *gofr.Context) (any, error) { + // Note: In this GoFr version, argument parsing is manual. + // You would use os.Args and your getFlagValue helper here. + // For demonstration, let's assume 'required_arg' is parsed. + requiredArg := getFlagValue(os.Args[2:], "--required_arg", "") + + if requiredArg == "" { + fmt.Fprintln(os.Stderr, "Error: 'required_arg' is missing.") + os.Exit(2) // Exit with code 2 for incorrect usage + } + // ... command logic ... + return "Command executed successfully", nil +}) +``` + +By default, if a handler returns an error, GoFr will log the error to `stderr` and exit with `1`. Using `os.Exit()` allows for more granular control over exit codes, which is vital for scripting. + +## Logging in CLI Applications + +In GoFr CLI applications, logging output is directed to `stdout` by default. This makes it easy to integrate your CLI output into scripting pipelines or redirect logs to files. You can control the verbosity of your application's logs using the `GOFR_LOG_LEVEL` environment variable. + +### Log Levels + +GoFr supports standard logging levels: +- `DEBUG`: Detailed information, typically of interest only when diagnosing problems. +- `INFO`: Informational messages that highlight the progress of the application at a coarse-grained level. +- `WARN`: Potentially harmful situations. +- `ERROR`: Error events that might still allow the application to continue running. +- `FATAL`: Severe error events that will presumably lead the application to abort. + +### Using the Logger in Handlers + +You can access the logger via the `gofr.Context` object within your subcommand handlers: + +```go +// Inside a subcommand handler +func(c *gofr.Context) (any, error) { + c.Logger.Debug("This is a debug message.") + // Note: c.Params() might not accurately reflect manually parsed arguments. + // Consider logging the manually parsed arguments directly. + c.Logger.Infof("Processing command. Manually parsed arguments would be used here.") + c.Logger.Warn("Potential issue detected.") + c.Logger.Errorf("Failed to complete task: %s", "reason for error") + // c.Logger.Fatal will terminate the application after logging + return "Task completed", nil +} +``` + +### Controlling Log Level via Environment Variable + +Set the `GOFR_LOG_LEVEL` environment variable before running your CLI application. Example output will vary based on the messages logged in your actual handler: + +```bash +# Set log level to DEBUG to see all messages +# For commands that use manual argument parsing, ensure your handler logic supports it. +$ GOFR_LOG_LEVEL=DEBUG ./mycli hello --name TestUser +# Expected output will depend on your handler's logging, e.g.: +{"level":"INFO","msg":"Hello, TestUser!"} + +# Set log level to INFO (default) +$ GOFR_LOG_LEVEL=INFO ./mycli goodbye AnotherUser +# Expected output: +{"level":"INFO","msg":"Goodbye, AnotherUser!"} + +# Set log level to ERROR to only see error and fatal messages +$ GOFR_LOG_LEVEL=ERROR ./mycli hello +# Expected output (assuming no error in default hello): +{"level":"INFO","msg":"Hello, World!"} +``` + +## Testing CLI Commands + +GoFr emphasizes testability, and CLI command handlers are no exception. You can easily write unit tests for your subcommand handlers by mocking the `gofr.Context` and asserting the output or returned errors. + +Here's an example of how you might test a subcommand handler using the manual argument parsing approach: + +```go +// In a file like 'main_test.go' alongside your main.go +package main + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/cmd/cmdtest" // Utility for testing CLI commands +) + +// Define the helloHandler function separately for easier testing +func helloHandler(ctx *gofr.Context) (any, error) { + // For testing, cmdtest.NewContext can simulate arguments for ctx.Param. + // In a real application, you would manually parse os.Args. + name := ctx.Param("name") + if name == "" { + name = "World" + } + ctx.Out.Println(fmt.Sprintf("Hello, %s!", name)) + return nil, nil +} + +func TestHelloSubcommand(t *testing.T) { + tests := []struct { + name string + args []string // Arguments to simulate for the subcommand + expected string // Expected output to ctx.Out.Println + err string + }{ + { + name: "successful hello with name flag", + args: []string{"--name", "TestUser"}, + expected: "Hello, TestUser!", + err: "", + }, + { + name: "successful hello with positional argument", + args: []string{"TestPositional"}, // cmdtest.NewContext maps this to "name" param + expected: "Hello, TestPositional!", + err: "", + }, + { + name: "hello with no arguments (default)", + args: []string{}, + expected: "Hello, World!", + err: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // cmdtest.NewContext provides a mock context for testing CLI commands. + // For ctx.Param to work correctly in tests, you need to ensure cmdtest.NewContext + // is set up to mimic your manual parsing logic for os.Args. + // This often means providing mock parameters that `ctx.Param` can then retrieve. + c := cmdtest.NewContext(tc.args) + + // Example of how you might mock ctx.Param behavior within a test, + // though the exact implementation depends on cmdtest.NewContext capabilities. + // If cmdtest.NewContext directly parses string slices into parameters, + // then this manual mapping below might not be necessary. + + // Let's assume cmdtest.NewContext (or a custom test helper) + // correctly translates `tc.args` to what `ctx.Param` would return + // based on the `getFlagValue` logic in your main application. + // For instance, if `args: []string{"--name", "TestUser"}`, then `c.Param("name")` should return "TestUser". + // If `args: []string{"TestPositional"}`, then `c.Param("name")` should return "TestPositional". + // And if `args: []string{}`, `c.Param("name")` should return "". + + _, err := helloHandler(c) // We are primarily interested in the side-effect (Println) and error + + if tc.err != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.err) + } else { + assert.NoError(t, err) + // Asserting output can be done by capturing ctx.Out, but for simplicity + // we'll assume direct output from the handler is covered by integration tests. + // If you want to test output, you would need to mock ctx.Out to a buffer. + } + }) + } +} +``` +This test uses `cmdtest.NewContext` to create a mock context, allowing you to pass arguments programmatically and assert the output or errors from your handler. Remember to integrate `strconv` for argument parsing if your handler expects integer inputs from flags, and explicitly handle `os.Args` as demonstrated in the example `main.go` and `getFlagValue` helper. When using `cmdtest.NewContext`, ensure its setup accurately reflects how your `main.go` manually parses arguments into parameters accessible via `ctx.Param`. + +## Comparison Table: GoFr HTTP vs. CLI Applications + +| Feature | GoFr HTTP Applications (`app.New()`) | GoFr CLI Applications (`app.NewCMD()`) | +| :------------------- | :--------------------------------------------- | :-------------------------------------------------- | +| **Primary Purpose** | Building web services, APIs, and microservices | Creating command-line tools and scripts | +| **Server** | Starts an HTTP server (listens on ports) | Does NOT start an HTTP server | +| **Routing/Commands** | Handles HTTP routes (GET, POST, PUT, DELETE) | Handles subcommands and their respective handlers | +| **Request Handling** | Processes HTTP requests, generates responses | **Manually parses command-line arguments and flags via `os.Args`** | +| **Input/Output** | HTTP requests (JSON, XML), HTTP responses | Terminal arguments (`os.Args`), `stdout`/`stderr` | +| **Concurrency** | Manages concurrent HTTP requests | Typically executes one command at a time (sequential) | +| **Use Cases** | Web APIs, backend services for web/mobile UIs | Automation scripts, data processing, system utilities, developer tools | +| **Configuration** | Configures HTTP ports, database connections, etc. | Configures logging levels, file paths, etc. (often via environment variables) | + +## Best Practices for GoFr CLI Applications + +Adhering to best practices ensures your CLI tools are robust, user-friendly, and maintainable. + +* **Keep Commands Modular and Focused**: + * Design each subcommand to perform a single, well-defined task. Avoid creating "god" commands that try to do too many things. This improves clarity, makes the command easier to test, and reduces the likelihood of bugs. + * For complex workflows, consider chaining multiple simple commands rather than building one monolithic command. + +* **Provide Helpful Output and Exit Codes**: + * **Informative Output**: Ensure your CLI provides clear, concise, and actionable feedback to the user. Use `fmt.Println`, `ctx.Out.Println` or `c.Logger.Info` for general output. For structured data, consider JSON or table formats. + * **Meaningful Exit Codes**: Use standard Unix exit codes: + * `0`: Indicates successful execution. + * `1`: General error, uncategorized failure. + * `2`: Incorrect usage, e.g., missing arguments or invalid flags. + * Other non-zero codes: Can be used for specific error conditions (e.g., file not found, permission denied). This is crucial for scripting and automation, allowing other programs to react to your CLI's outcome. You can use `os.Exit(code)` to set the exit code explicitly. + * Direct user-facing error messages to `stderr` (`fmt.Fprintln(os.Stderr, "Error message")`) to separate them from successful command output. + +* **Add Tests for Command Handlers**: + * Just like HTTP handlers, your CLI subcommand handlers contain business logic that needs to be thoroughly tested. Write unit and integration tests for each handler to ensure its correctness and reliability. + * Leverage GoFr's `cmdtest` utilities to easily mock the `gofr.Context` for testing, keeping in mind the need for manual argument parsing in your current setup. + * Mock external dependencies (databases, APIs) to isolate your handler logic during unit tests. + +* **Robust Error Handling**: + * Always handle potential errors gracefully within your command handlers. Return a meaningful `error` from your handler function, which GoFr will then log or display. + * **Wrap errors with context**: Use `fmt.Errorf("descriptive message: %w", originalErr)` to add context to errors as they propagate up, making debugging much easier. This is demonstrated in the `Getting Started` example. + * For user-facing errors, provide clear messages that guide the user on how to resolve the issue. + +## Distinction: `gofr-cli` vs. `app.NewCMD()` + +It's important to clarify the relationship between the official `gofr-cli` tool and the custom CLI applications you build using `app.NewCMD()`: + +⚡ **Important Clarification** + +- The official **`gofr-cli`** tool ([https://gofr.dev/docs/references/gofrcli](https://gofr.dev/docs/references/gofrcli)) is a powerful, pre-built command-line utility distributed by the GoFr team. It helps GoFr developers with common tasks such as project initialization (`gofr-cli init`), database migrations (`gofr-cli migrate`), and more. +- **Crucially, the `gofr-cli` tool itself is implemented using `app.NewCMD()`**. This serves as a real-world example of how GoFr’s CLI system can be used to build production-ready command-line tools. When you execute commands like `gofr-cli migrate` or `gofr-cli init`, you are interacting with an application built *with* GoFr's CLI framework under the hood. + +👉 As a developer, you can leverage the exact same approach (`app.NewCMD()`) to build your **own custom CLI tools**. This allows you to create specialized utilities tailored to your project's unique automation, DevOps tasks, data processing needs, and more. + +### Example Use Cases for Your Own `app.NewCMD()` Tools + +* **Database migration tools**: Custom scripts to manage schema changes or data seeding. +* **Developer productivity scripts**: Automating repetitive development tasks specific to your codebase. +* **Infrastructure automation**: CLI tools to deploy, configure, or monitor your services. +* **Domain-specific CLI utilities**: Tools for managing specific business logic, data imports/exports, or system interactions. + +By integrating `app.NewCMD()`, you benefit from GoFr's consistent structure, flexible logging, and robust error handling, enabling you to build powerful and reliable command-line utilities with ease. \ No newline at end of file diff --git a/docs/navigation.js b/docs/navigation.js index b0fcd196e..520e39bc0 100644 --- a/docs/navigation.js +++ b/docs/navigation.js @@ -7,6 +7,13 @@ export const navigation = [ title: 'Hello Server', href: '/docs/quick-start/introduction' , desc: "Getting started with how to write a server using GoFR with basic examples and explanations. Boost your productivity with efficient coding practices and learn to build scalable applications quickly."}, + + { + title: 'CLI Applications', + href: '/docs/cli/cli', + desc: "Learn to build powerful command-line interface (CLI) applications using GoFr's app.NewCMD(), offering a robust framework for command-line tools." + }, + { title: 'Configuration', href: '/docs/quick-start/configuration', diff --git a/examples/cli-example/README.md b/examples/cli-example/README.md new file mode 100644 index 000000000..47420bc57 --- /dev/null +++ b/examples/cli-example/README.md @@ -0,0 +1,119 @@ +# GoFr CLI Examples + +This directory contains examples demonstrating how to build Command-Line Interface (CLI) applications using the GoFr framework. + +⚠️ **Version Note**: These examples are designed for the current stable GoFr version where automatic argument parsing (`ctx.Args()` / `ctx.Flags()`) is not yet available. Arguments and flags are therefore parsed manually using `os.Args` and helper functions. + +--- + +## Example 1: Multiple Subcommands with Manual Argument Parsing + +**File**: `example1-mutli-sub-command/main.go` + +This example demonstrates a robust GoFr CLI application supporting multiple subcommands (`hello` and `goodbye`). It showcases how to manually parse both flag-based and positional arguments using `os.Args` and a custom helper function. + +### Purpose +- Define multiple subcommands: `hello` and `goodbye`. +- Handle `--name` flag and positional arguments (e.g., `hello John`, `goodbye Jane`). +- Provide default values if no argument is supplied for either subcommand. +- Output results using `ctx.Out.Println`. + +### How to Run + +First, ensure you have the `example1-mutli-sub-command/main.go` file created in this directory (content provided below). + +Navigate to your GoFr project root and execute the following commands: + +1. **Run `hello` subcommand with default value (no arguments):** + ```bash + go run examples/cli-example/example1-mutli-sub-command/main.go hello + # Output: Hello, World! + ``` + +2. **Run `hello` subcommand with a positional argument:** + ```bash + go run examples/cli-example/example1-mutli-sub-command/main.go hello John + # Output: Hello, John! + ``` + +3. **Run `hello` subcommand with a flag argument:** + ```bash + go run examples/cli-example/example1-mutli-sub-command/main.go hello --name Jane + # Output: Hello, Jane! + ``` + +4. **Run `goodbye` subcommand with default value (no arguments):** + ```bash + go run examples/cli-example/example1-mutli-sub-command/main.go goodbye + # Output: Goodbye, Friend! + ``` + +5. **Run `goodbye` subcommand with a positional argument:** + ```bash + go run examples/cli-example/example1-mutli-sub-command/main.go goodbye Mark + # Output: Goodbye, Mark! + ``` + +6. **Run `goodbye` subcommand with a flag argument:** + ```bash + go run examples/cli-example/example1-mutli-sub-command/main.go goodbye --name Susan + # Output: Goodbye, Susan! + ``` + +### Key Concepts Demonstrated +- **`app.NewCMD()`**: Initializes a new GoFr CLI application. +- **`app.SubCommand("name", handler)`**: Registers multiple subcommands with their respective handler functions. +- **Manual Argument Parsing**: `os.Args` is used directly with a helper function (`getFlagValue`) to extract flag values (e.g., `--name`) and positional arguments within each subcommand's handler. +- **`ctx.Out.Println()`**: Prints output directly to the standard CLI output stream. +- **Subcommand Handler Signature:** + ```go + func(ctx *gofr.Context) (any, error) + ``` + This is the required signature for all subcommand handlers, returning an optional result and an error. + +--- + +## Example 2: Single Subcommand with Manual Argument Parsing + +**File**: `example2-single-sub-command/main.go` + +This example demonstrates a basic GoFr CLI application with a single subcommand (`hello`), focusing on manual argument parsing. + +### Purpose +- Define a single subcommand: `hello`. +- Handle `--name` flag and positional arguments (e.g., `hello John`). +- Provide default values if no argument is supplied. +- Output results using `ctx.Out.Println`. + +### How to Run + +Navigate to your GoFr project root and execute: + +1. **Run with default value (no arguments):** + ```bash + go run examples/cli-example/example2-single-sub-command/main.go hello + # Output: Hello, World! + ``` + +2. **Run with a positional argument:** + ```bash + go run examples/cli-example/example2-single-sub-command/main.go hello John + # Output: Hello, John! + ``` + +3. **Run with a flag argument:** + ```bash + go run examples/cli-example/example2-single-sub-command/main.go hello --name Jane + # Output: Hello, Jane! + ``` + +### Key Concepts Demonstrated +- **`app.NewCMD()`**: Initializes a new GoFr CLI application. +- **`app.SubCommand("name", handler)`**: Registers a subcommand with its handler function. +- **Manual Argument Parsing**: Use `os.Args` with a helper function (`getFlagValue`) to extract flag values (e.g., `--name`). +- **`ctx.Out.Println()`**: Prints output directly to the standard CLI output stream. +- **Subcommand Handler Signature:** + ```go + func(ctx *gofr.Context) (any, error) + ``` + This is required for all subcommand handlers, returning an optional result and an error. \ No newline at end of file diff --git a/examples/cli-example/example1-mutli-sub-command/main.go b/examples/cli-example/example1-mutli-sub-command/main.go new file mode 100644 index 000000000..3c2eed4f8 --- /dev/null +++ b/examples/cli-example/example1-mutli-sub-command/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "gofr.dev/pkg/gofr" +) + +// helper function to parse flags manually +func getFlagValue(args []string, flag string, defaultValue string) string { + for i := 0; i < len(args); i++ { + if args[i] == flag && i+1 < len(args) { + return args[i+1] + } + } + return defaultValue +} + +func main() { + app := gofr.NewCMD() + + // ----------------------- + // Subcommand: hello + // ----------------------- + app.SubCommand("hello", func(ctx *gofr.Context) (any, error) { + args := os.Args[2:] // arguments after "hello" + name := getFlagValue(args, "--name", "") + if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { + name = args[0] // fallback to positional arg + } + if name == "" { + name = "World" + } + ctx.Out.Println(fmt.Sprintf("Hello, %s!", name)) + return nil, nil + }) + + // ----------------------- + // Subcommand: goodbye + // ----------------------- + app.SubCommand("goodbye", func(ctx *gofr.Context) (any, error) { + args := os.Args[2:] // arguments after "goodbye" + name := getFlagValue(args, "--name", "") + if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { + name = args[0] + } + if name == "" { + name = "Friend" + } + ctx.Out.Println(fmt.Sprintf("Goodbye, %s!", name)) + return nil, nil + }) + + // Run the CLI app + app.Run() +} diff --git a/examples/cli-example/example2-single-sub-command/main.go b/examples/cli-example/example2-single-sub-command/main.go new file mode 100644 index 000000000..29f3fbcf6 --- /dev/null +++ b/examples/cli-example/example2-single-sub-command/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "gofr.dev/pkg/gofr" +) + +// Helper function to manually parse flags +func getFlagValue(args []string, flag string, defaultValue string) string { + for i := 0; i < len(args); i++ { + if args[i] == flag && i+1 < len(args) { + return args[i+1] + } + } + return defaultValue +} + +func main() { + app := gofr.NewCMD() + + // ----------------------- + // Subcommand: hello + // ----------------------- + app.SubCommand("hello", func(ctx *gofr.Context) (any, error) { + args := os.Args[2:] // arguments after "hello" + name := getFlagValue(args, "--name", "") + + // fallback to positional argument if --name not provided + if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { + name = args[0] + } + + if name == "" { + name = "World" + } + + ctx.Out.Println(fmt.Sprintf("Hello, %s!", name)) + return nil, nil + }) + + // Run the CLI application + app.Run() +} diff --git a/go.work.sum b/go.work.sum index b18de40ab..a1ada2122 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1156,6 +1156,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/testcontainers/testcontainers-go v0.28.0 h1:1HLm9qm+J5VikzFDYhOd+Zw12NtOl+8drH2E8nTY1r8= github.com/testcontainers/testcontainers-go v0.28.0/go.mod h1:COlDpUXbwW3owtpMkEB1zo9gwb1CoKVKlyrVPejF4AU= github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= @@ -1256,6 +1257,8 @@ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= @@ -1291,6 +1294,8 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -1324,6 +1329,8 @@ golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1349,6 +1356,7 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1389,6 +1397,7 @@ golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMe golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b h1:DU+gwOBXU+6bO0sEyO7o/NeMlxZxCZEvI7v+J4a1zRQ= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= +golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1449,6 +1458,8 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= From a7fdca74120d4d7d3d019875e58ef6f931d6780a Mon Sep 17 00:00:00 2001 From: Umang Mundhra Date: Mon, 1 Sep 2025 14:49:19 +0530 Subject: [PATCH 6/7] Release/v1.44.1 --- pkg/gofr/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index 71a9e6543..6df39812c 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.44.0" +const Framework = "v1.44.1" From 06537dfc896f82f81a3401342652cd6c637bcf95 Mon Sep 17 00:00:00 2001 From: Aryaman Vohra Date: Fri, 5 Sep 2025 09:50:36 +0530 Subject: [PATCH 7/7] Simplify CLI documentation and remove extra examples --- docs/cli/cli.md | 425 ++---------------- examples/cli-example/README.md | 119 ----- .../example1-mutli-sub-command/main.go | 58 --- .../example2-single-sub-command/main.go | 46 -- 4 files changed, 36 insertions(+), 612 deletions(-) delete mode 100644 examples/cli-example/README.md delete mode 100644 examples/cli-example/example1-mutli-sub-command/main.go delete mode 100644 examples/cli-example/example2-single-sub-command/main.go diff --git a/docs/cli/cli.md b/docs/cli/cli.md index f81a2583f..6574cc96d 100644 --- a/docs/cli/cli.md +++ b/docs/cli/cli.md @@ -1,427 +1,74 @@ -# Building CLI Applications with GoFr +# CLI Applications -GoFr provides a robust and straightforward way to build **command-line applications**. By leveraging `app.NewCMD()`, you gain access to a powerful framework that simplifies the process of defining custom commands, handling flags, managing configuration, and orchestrating execution flows for your CLI tools. +GoFr provides a simple way to build command-line applications using `app.NewCMD()`. This creates standalone CLI tools without starting an HTTP server. -This approach offers a clear separation from HTTP services, allowing you to focus purely on command-line interactions and logic. +## Getting Started -## Table of Contents - -- [Introduction](#introduction) -- [Getting Started: Your First GoFr CLI App](#getting-started-your-first-gofr-cli-app) - - [Understanding the `Handler` Function](#understanding-the-handler-function) - - [Using Command Options: `AddDescription` and `AddHelp`](#using-command-options-adddescription-and-addhelp) -- [Running the CLI Application](#running-the-cli-application) -- [Demo Run](#demo-run) -- [Error Handling with Exit Codes](#error-handling-with-exit-codes) -- [Logging in CLI Applications](#logging-in-cli-applications) - - [Log Levels](#log-levels) - - [Using the Logger in Handlers](#using-the-logger-in-handlers) - - [Controlling Log Level via Environment Variable](#controlling-log-level-via-environment-variable) -- [Testing CLI Commands](#testing-cli-commands) -- [Comparison Table: GoFr HTTP vs. CLI Applications](#comparison-table-gofr-http-vs-cli-applications) -- [Best Practices for GoFr CLI Applications](#best-practices-for-gofr-cli-applications) -- [Distinction: `gofr-cli` vs. `app.NewCMD()`](#distinction-gofr-cli-vs-appnewcmd) -- [Navigation Note](#navigation-note) - -## Introduction - -In GoFr, applications are typically initialized in two primary ways: - -- **`app.New()`**: This function is dedicated to creating **HTTP services**, such as REST APIs, web applications, and microservices. It automatically sets up an HTTP server, handles routing for web requests, integrates middleware, and provides a full suite of HTTP-specific functionalities for building networked applications. - -- **`app.NewCMD()`**: This function is specifically designed for building **standalone command-line interface (CLI) applications**. Unlike `app.New()`, it does not initialize or start an HTTP server. Instead, `app.NewCMD()` provides the necessary infrastructure to define distinct subcommands, associate them with handler functions, parse command-line arguments and flags, and execute logic directly from the terminal. It's ideal for automation, scripting, and developer tools. - -⚠️ **Version Note**: This example works with the current stable GoFr version in which automatic argument parsing (`ctx.Args()` / `ctx.Flags()`) is not yet available. Therefore, arguments and flags are parsed manually using `os.Args`. Future GoFr releases may provide built-in flag parsing, but this approach is fully functional for open-source use and ensures compatibility. - -## Getting Started: Your First GoFr CLI App - -Let's begin by creating a simple CLI application that performs basic arithmetic operations (addition and subtraction) using subcommands. - -First, ensure your Go module is initialized and GoFr is added as a dependency: - -```bash -go mod init github.com/example/mycli -go get gofr.dev -``` - -Next, create a `main.go` file (e.g., in your project's root) with the following content: +Create a basic CLI application with subcommands: ```go package main import ( "fmt" - "os" // Required for os.Args to manually parse arguments - "strings" // Required for string manipulation in argument parsing - "gofr.dev/pkg/gofr" ) -// helper function to parse flags manually -// This function is necessary because this GoFr version does not support automatic flag parsing -// via `ctx.Args()` or `ctx.Flags()`. -func getFlagValue(args []string, flag string, defaultValue string) string { - for i := 0; i < len(args); i++ { - if args[i] == flag && i+1 < len(args) { - return args[i+1] - } - } - return defaultValue -} - func main() { app := gofr.NewCMD() - // ----------------------- - // Subcommand: hello - // ----------------------- - app.SubCommand("hello", func(ctx *gofr.Context) (any, error) { - // In this GoFr version, argument parsing is done manually. - // os.Args[0] is the executable name, os.Args[1] is the subcommand ("hello"). - // So, actual arguments for the subcommand start from index 2. - args := os.Args[2:] - - // Attempt to get "name" from a flag like "--name John" - name := getFlagValue(args, "--name", "") - - // If no --name flag, fallback to a positional argument (e.g., "hello John") - if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { - name = args[0] - } - - if name == "" { - name = "World" // Default value if no argument is provided - } - - ctx.Out.Println(fmt.Sprintf("Hello, %s!", name)) - return nil, nil - }) + // Simple hello command + app.SubCommand("hello", func(c *gofr.Context) (any, error) { + return "Hello World!", nil + }, gofr.AddDescription("Print hello message")) - // ----------------------- - // Subcommand: goodbye - // ----------------------- - app.SubCommand("goodbye", func(ctx *gofr.Context) (any, error) { - args := os.Args[2:] // arguments after "goodbye" - name := getFlagValue(args, "--name", "") - if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { - name = args[0] - } + // Command with parameters + app.SubCommand("greet", func(c *gofr.Context) (any, error) { + name := c.Param("name") if name == "" { - name = "Friend" + name = "World" } - ctx.Out.Println(fmt.Sprintf("Goodbye, %s!", name)) - return nil, nil + return fmt.Sprintf("Hello, %s!", name), nil }) - // Run the CLI application. This will parse arguments and execute the matching subcommand. app.Run() } ``` -**Find this example and more in the `examples/cli-example` directory.** - -### Understanding the `Handler` Function -The handler function for `app.SubCommand` has the signature `func(ctx *gofr.Context) (any, error)`. +## Key GoFr CLI Methods -- **`ctx *gofr.Context`**: This is the GoFr context object, which provides access to: - - **Command-line parameters (manual parsing)**: In this GoFr version, `ctx.Args()` and `ctx.Flags()` are not available for automatic argument parsing. Instead, you will manually parse `os.Args` within your handler. You can use helper functions like `getFlagValue(args []string, flag string, defaultValue string)` (as shown in the example) to extract flag values. Positional arguments also need to be parsed from `os.Args`. The example `main.go` demonstrates handling both `--name` flags and positional arguments. - - **Logger**: `c.Logger` for structured logging. - - **Standard Output**: `ctx.Out.Println()` can be used to print directly to standard output. - - **Other dependencies**: Access to databases, services, and configuration registered with the GoFr application. -- **`any`**: The return value that will be printed to `stdout` upon successful execution of the command. -- **`error`**: If the handler returns a non-nil error, the application will print the error message to `stderr` and exit with a non-zero status code. This is crucial for scripting. +- **`app.NewCMD()`**: Initialize a CLI application +- **`app.SubCommand(name, handler, options...)`**: Add a subcommand +- **`gofr.AddDescription(desc)`**: Add help description +- **`gofr.AddHelp(help)`**: Add detailed help text +- **`ctx.Param(name)`**: Get command parameters +- **`ctx.Out.Println()`**: Print to stdout +- **`ctx.Logger`**: Access logging -### Using Command Options: `AddDescription` and `AddHelp` +## Running CLI Applications -When defining subcommands, you can provide additional `Options` to enhance the user experience and make your CLI tool self-documenting: - -- **`gofr.AddDescription(description string)`**: Provides a brief, one-line summary of what the subcommand does. This description is displayed when a user runs the main command without any arguments or with a global `--help` flag. -- **`gofr.AddHelp(helpString string)`**: Offers more detailed usage information specifically for that subcommand. This comprehensive help text is shown when a user runs the subcommand with the `--help` or `-h` flag (e.g., `./mycli add --help`). - -These options are crucial for creating user-friendly and discoverable CLI tools. - -## Running the CLI Application - -To make your CLI application executable, you first need to build it: +Build and run your CLI: ```bash go build -o mycli +./mycli hello +./mycli greet --name John +./mycli --help ``` -This command compiles your `main.go` file and creates an executable named `mycli` (or `mycli.exe` on Windows) in your current directory. - -## Demo Run -Here's a sample run demonstrating how the CLI operates, including how to access help and use different argument styles: +## Example Commands ```bash -# Run the 'hello' subcommand with no arguments (uses default) -$ ./mycli hello -Hello, World! - -# Run the 'hello' subcommand with a positional argument -$ ./mycli hello Jane -Hello, Jane! - -# Run the 'hello' subcommand with a --name flag -$ ./mycli hello --name John -Hello, John! - -# Run the 'goodbye' subcommand with no arguments (uses default) -$ ./mycli goodbye -Goodbye, Friend! - -# Run the 'goodbye' subcommand with a positional argument -$ ./mycli goodbye Mark -Goodbye, Mark! - -# Run the 'goodbye' subcommand with a --name flag -$ ./mycli goodbye --name Susan -Goodbye, Susan! - -# Access global help (shows descriptions of all subcommands) -$ ./mycli --help -Available commands: - - hello - Description: +# Basic command +./mycli hello +# Output: Hello World! - goodbye - Description: -``` -Note: Both positional arguments (e.g., "hello Jane") and flags (e.g., "hello --name John") are supported, but flags are parsed manually using the helper function `getFlagValue`. - -This demo illustrates how arguments are passed, how subcommands are executed, and how built-in help features work, reflecting the manual parsing approach. - -## Error Handling with Exit Codes +# Command with parameter +./mycli greet --name Alice +# Output: Hello, Alice! -For robust CLI applications, it's essential to communicate success or failure through standard exit codes. A `0` exit code indicates success, while any non-zero code signals an error. GoFr automatically handles returning a non-zero exit code if your handler returns an `error`. - -However, for specific error conditions like invalid arguments, you might want to explicitly exit with a particular code, such as `2` for incorrect usage. You can achieve this within your handler: - -```go -// Example of a handler explicitly exiting on validation error -app.SubCommand("mycommand", func(c *gofr.Context) (any, error) { - // Note: In this GoFr version, argument parsing is manual. - // You would use os.Args and your getFlagValue helper here. - // For demonstration, let's assume 'required_arg' is parsed. - requiredArg := getFlagValue(os.Args[2:], "--required_arg", "") - - if requiredArg == "" { - fmt.Fprintln(os.Stderr, "Error: 'required_arg' is missing.") - os.Exit(2) // Exit with code 2 for incorrect usage - } - // ... command logic ... - return "Command executed successfully", nil -}) +# Help +./mycli --help ``` -By default, if a handler returns an error, GoFr will log the error to `stderr` and exit with `1`. Using `os.Exit()` allows for more granular control over exit codes, which is vital for scripting. - -## Logging in CLI Applications - -In GoFr CLI applications, logging output is directed to `stdout` by default. This makes it easy to integrate your CLI output into scripting pipelines or redirect logs to files. You can control the verbosity of your application's logs using the `GOFR_LOG_LEVEL` environment variable. - -### Log Levels - -GoFr supports standard logging levels: -- `DEBUG`: Detailed information, typically of interest only when diagnosing problems. -- `INFO`: Informational messages that highlight the progress of the application at a coarse-grained level. -- `WARN`: Potentially harmful situations. -- `ERROR`: Error events that might still allow the application to continue running. -- `FATAL`: Severe error events that will presumably lead the application to abort. - -### Using the Logger in Handlers - -You can access the logger via the `gofr.Context` object within your subcommand handlers: - -```go -// Inside a subcommand handler -func(c *gofr.Context) (any, error) { - c.Logger.Debug("This is a debug message.") - // Note: c.Params() might not accurately reflect manually parsed arguments. - // Consider logging the manually parsed arguments directly. - c.Logger.Infof("Processing command. Manually parsed arguments would be used here.") - c.Logger.Warn("Potential issue detected.") - c.Logger.Errorf("Failed to complete task: %s", "reason for error") - // c.Logger.Fatal will terminate the application after logging - return "Task completed", nil -} -``` - -### Controlling Log Level via Environment Variable - -Set the `GOFR_LOG_LEVEL` environment variable before running your CLI application. Example output will vary based on the messages logged in your actual handler: - -```bash -# Set log level to DEBUG to see all messages -# For commands that use manual argument parsing, ensure your handler logic supports it. -$ GOFR_LOG_LEVEL=DEBUG ./mycli hello --name TestUser -# Expected output will depend on your handler's logging, e.g.: -{"level":"INFO","msg":"Hello, TestUser!"} - -# Set log level to INFO (default) -$ GOFR_LOG_LEVEL=INFO ./mycli goodbye AnotherUser -# Expected output: -{"level":"INFO","msg":"Goodbye, AnotherUser!"} - -# Set log level to ERROR to only see error and fatal messages -$ GOFR_LOG_LEVEL=ERROR ./mycli hello -# Expected output (assuming no error in default hello): -{"level":"INFO","msg":"Hello, World!"} -``` - -## Testing CLI Commands - -GoFr emphasizes testability, and CLI command handlers are no exception. You can easily write unit tests for your subcommand handlers by mocking the `gofr.Context` and asserting the output or returned errors. - -Here's an example of how you might test a subcommand handler using the manual argument parsing approach: - -```go -// In a file like 'main_test.go' alongside your main.go -package main - -import ( - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "gofr.dev/pkg/gofr" - "gofr.dev/pkg/gofr/cmd/cmdtest" // Utility for testing CLI commands -) - -// Define the helloHandler function separately for easier testing -func helloHandler(ctx *gofr.Context) (any, error) { - // For testing, cmdtest.NewContext can simulate arguments for ctx.Param. - // In a real application, you would manually parse os.Args. - name := ctx.Param("name") - if name == "" { - name = "World" - } - ctx.Out.Println(fmt.Sprintf("Hello, %s!", name)) - return nil, nil -} - -func TestHelloSubcommand(t *testing.T) { - tests := []struct { - name string - args []string // Arguments to simulate for the subcommand - expected string // Expected output to ctx.Out.Println - err string - }{ - { - name: "successful hello with name flag", - args: []string{"--name", "TestUser"}, - expected: "Hello, TestUser!", - err: "", - }, - { - name: "successful hello with positional argument", - args: []string{"TestPositional"}, // cmdtest.NewContext maps this to "name" param - expected: "Hello, TestPositional!", - err: "", - }, - { - name: "hello with no arguments (default)", - args: []string{}, - expected: "Hello, World!", - err: "", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // cmdtest.NewContext provides a mock context for testing CLI commands. - // For ctx.Param to work correctly in tests, you need to ensure cmdtest.NewContext - // is set up to mimic your manual parsing logic for os.Args. - // This often means providing mock parameters that `ctx.Param` can then retrieve. - c := cmdtest.NewContext(tc.args) - - // Example of how you might mock ctx.Param behavior within a test, - // though the exact implementation depends on cmdtest.NewContext capabilities. - // If cmdtest.NewContext directly parses string slices into parameters, - // then this manual mapping below might not be necessary. - - // Let's assume cmdtest.NewContext (or a custom test helper) - // correctly translates `tc.args` to what `ctx.Param` would return - // based on the `getFlagValue` logic in your main application. - // For instance, if `args: []string{"--name", "TestUser"}`, then `c.Param("name")` should return "TestUser". - // If `args: []string{"TestPositional"}`, then `c.Param("name")` should return "TestPositional". - // And if `args: []string{}`, `c.Param("name")` should return "". - - _, err := helloHandler(c) // We are primarily interested in the side-effect (Println) and error - - if tc.err != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.err) - } else { - assert.NoError(t, err) - // Asserting output can be done by capturing ctx.Out, but for simplicity - // we'll assume direct output from the handler is covered by integration tests. - // If you want to test output, you would need to mock ctx.Out to a buffer. - } - }) - } -} -``` -This test uses `cmdtest.NewContext` to create a mock context, allowing you to pass arguments programmatically and assert the output or errors from your handler. Remember to integrate `strconv` for argument parsing if your handler expects integer inputs from flags, and explicitly handle `os.Args` as demonstrated in the example `main.go` and `getFlagValue` helper. When using `cmdtest.NewContext`, ensure its setup accurately reflects how your `main.go` manually parses arguments into parameters accessible via `ctx.Param`. - -## Comparison Table: GoFr HTTP vs. CLI Applications - -| Feature | GoFr HTTP Applications (`app.New()`) | GoFr CLI Applications (`app.NewCMD()`) | -| :------------------- | :--------------------------------------------- | :-------------------------------------------------- | -| **Primary Purpose** | Building web services, APIs, and microservices | Creating command-line tools and scripts | -| **Server** | Starts an HTTP server (listens on ports) | Does NOT start an HTTP server | -| **Routing/Commands** | Handles HTTP routes (GET, POST, PUT, DELETE) | Handles subcommands and their respective handlers | -| **Request Handling** | Processes HTTP requests, generates responses | **Manually parses command-line arguments and flags via `os.Args`** | -| **Input/Output** | HTTP requests (JSON, XML), HTTP responses | Terminal arguments (`os.Args`), `stdout`/`stderr` | -| **Concurrency** | Manages concurrent HTTP requests | Typically executes one command at a time (sequential) | -| **Use Cases** | Web APIs, backend services for web/mobile UIs | Automation scripts, data processing, system utilities, developer tools | -| **Configuration** | Configures HTTP ports, database connections, etc. | Configures logging levels, file paths, etc. (often via environment variables) | - -## Best Practices for GoFr CLI Applications - -Adhering to best practices ensures your CLI tools are robust, user-friendly, and maintainable. - -* **Keep Commands Modular and Focused**: - * Design each subcommand to perform a single, well-defined task. Avoid creating "god" commands that try to do too many things. This improves clarity, makes the command easier to test, and reduces the likelihood of bugs. - * For complex workflows, consider chaining multiple simple commands rather than building one monolithic command. - -* **Provide Helpful Output and Exit Codes**: - * **Informative Output**: Ensure your CLI provides clear, concise, and actionable feedback to the user. Use `fmt.Println`, `ctx.Out.Println` or `c.Logger.Info` for general output. For structured data, consider JSON or table formats. - * **Meaningful Exit Codes**: Use standard Unix exit codes: - * `0`: Indicates successful execution. - * `1`: General error, uncategorized failure. - * `2`: Incorrect usage, e.g., missing arguments or invalid flags. - * Other non-zero codes: Can be used for specific error conditions (e.g., file not found, permission denied). This is crucial for scripting and automation, allowing other programs to react to your CLI's outcome. You can use `os.Exit(code)` to set the exit code explicitly. - * Direct user-facing error messages to `stderr` (`fmt.Fprintln(os.Stderr, "Error message")`) to separate them from successful command output. - -* **Add Tests for Command Handlers**: - * Just like HTTP handlers, your CLI subcommand handlers contain business logic that needs to be thoroughly tested. Write unit and integration tests for each handler to ensure its correctness and reliability. - * Leverage GoFr's `cmdtest` utilities to easily mock the `gofr.Context` for testing, keeping in mind the need for manual argument parsing in your current setup. - * Mock external dependencies (databases, APIs) to isolate your handler logic during unit tests. - -* **Robust Error Handling**: - * Always handle potential errors gracefully within your command handlers. Return a meaningful `error` from your handler function, which GoFr will then log or display. - * **Wrap errors with context**: Use `fmt.Errorf("descriptive message: %w", originalErr)` to add context to errors as they propagate up, making debugging much easier. This is demonstrated in the `Getting Started` example. - * For user-facing errors, provide clear messages that guide the user on how to resolve the issue. - -## Distinction: `gofr-cli` vs. `app.NewCMD()` - -It's important to clarify the relationship between the official `gofr-cli` tool and the custom CLI applications you build using `app.NewCMD()`: - -⚡ **Important Clarification** - -- The official **`gofr-cli`** tool ([https://gofr.dev/docs/references/gofrcli](https://gofr.dev/docs/references/gofrcli)) is a powerful, pre-built command-line utility distributed by the GoFr team. It helps GoFr developers with common tasks such as project initialization (`gofr-cli init`), database migrations (`gofr-cli migrate`), and more. -- **Crucially, the `gofr-cli` tool itself is implemented using `app.NewCMD()`**. This serves as a real-world example of how GoFr’s CLI system can be used to build production-ready command-line tools. When you execute commands like `gofr-cli migrate` or `gofr-cli init`, you are interacting with an application built *with* GoFr's CLI framework under the hood. - -👉 As a developer, you can leverage the exact same approach (`app.NewCMD()`) to build your **own custom CLI tools**. This allows you to create specialized utilities tailored to your project's unique automation, DevOps tasks, data processing needs, and more. - -### Example Use Cases for Your Own `app.NewCMD()` Tools - -* **Database migration tools**: Custom scripts to manage schema changes or data seeding. -* **Developer productivity scripts**: Automating repetitive development tasks specific to your codebase. -* **Infrastructure automation**: CLI tools to deploy, configure, or monitor your services. -* **Domain-specific CLI utilities**: Tools for managing specific business logic, data imports/exports, or system interactions. - -By integrating `app.NewCMD()`, you benefit from GoFr's consistent structure, flexible logging, and robust error handling, enabling you to build powerful and reliable command-line utilities with ease. \ No newline at end of file +For more details, see the [sample-cmd example](../../examples/sample-cmd). diff --git a/examples/cli-example/README.md b/examples/cli-example/README.md deleted file mode 100644 index 47420bc57..000000000 --- a/examples/cli-example/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# GoFr CLI Examples - -This directory contains examples demonstrating how to build Command-Line Interface (CLI) applications using the GoFr framework. - -⚠️ **Version Note**: These examples are designed for the current stable GoFr version where automatic argument parsing (`ctx.Args()` / `ctx.Flags()`) is not yet available. Arguments and flags are therefore parsed manually using `os.Args` and helper functions. - ---- - -## Example 1: Multiple Subcommands with Manual Argument Parsing - -**File**: `example1-mutli-sub-command/main.go` - -This example demonstrates a robust GoFr CLI application supporting multiple subcommands (`hello` and `goodbye`). It showcases how to manually parse both flag-based and positional arguments using `os.Args` and a custom helper function. - -### Purpose -- Define multiple subcommands: `hello` and `goodbye`. -- Handle `--name` flag and positional arguments (e.g., `hello John`, `goodbye Jane`). -- Provide default values if no argument is supplied for either subcommand. -- Output results using `ctx.Out.Println`. - -### How to Run - -First, ensure you have the `example1-mutli-sub-command/main.go` file created in this directory (content provided below). - -Navigate to your GoFr project root and execute the following commands: - -1. **Run `hello` subcommand with default value (no arguments):** - ```bash - go run examples/cli-example/example1-mutli-sub-command/main.go hello - # Output: Hello, World! - ``` - -2. **Run `hello` subcommand with a positional argument:** - ```bash - go run examples/cli-example/example1-mutli-sub-command/main.go hello John - # Output: Hello, John! - ``` - -3. **Run `hello` subcommand with a flag argument:** - ```bash - go run examples/cli-example/example1-mutli-sub-command/main.go hello --name Jane - # Output: Hello, Jane! - ``` - -4. **Run `goodbye` subcommand with default value (no arguments):** - ```bash - go run examples/cli-example/example1-mutli-sub-command/main.go goodbye - # Output: Goodbye, Friend! - ``` - -5. **Run `goodbye` subcommand with a positional argument:** - ```bash - go run examples/cli-example/example1-mutli-sub-command/main.go goodbye Mark - # Output: Goodbye, Mark! - ``` - -6. **Run `goodbye` subcommand with a flag argument:** - ```bash - go run examples/cli-example/example1-mutli-sub-command/main.go goodbye --name Susan - # Output: Goodbye, Susan! - ``` - -### Key Concepts Demonstrated -- **`app.NewCMD()`**: Initializes a new GoFr CLI application. -- **`app.SubCommand("name", handler)`**: Registers multiple subcommands with their respective handler functions. -- **Manual Argument Parsing**: `os.Args` is used directly with a helper function (`getFlagValue`) to extract flag values (e.g., `--name`) and positional arguments within each subcommand's handler. -- **`ctx.Out.Println()`**: Prints output directly to the standard CLI output stream. -- **Subcommand Handler Signature:** - ```go - func(ctx *gofr.Context) (any, error) - ``` - This is the required signature for all subcommand handlers, returning an optional result and an error. - ---- - -## Example 2: Single Subcommand with Manual Argument Parsing - -**File**: `example2-single-sub-command/main.go` - -This example demonstrates a basic GoFr CLI application with a single subcommand (`hello`), focusing on manual argument parsing. - -### Purpose -- Define a single subcommand: `hello`. -- Handle `--name` flag and positional arguments (e.g., `hello John`). -- Provide default values if no argument is supplied. -- Output results using `ctx.Out.Println`. - -### How to Run - -Navigate to your GoFr project root and execute: - -1. **Run with default value (no arguments):** - ```bash - go run examples/cli-example/example2-single-sub-command/main.go hello - # Output: Hello, World! - ``` - -2. **Run with a positional argument:** - ```bash - go run examples/cli-example/example2-single-sub-command/main.go hello John - # Output: Hello, John! - ``` - -3. **Run with a flag argument:** - ```bash - go run examples/cli-example/example2-single-sub-command/main.go hello --name Jane - # Output: Hello, Jane! - ``` - -### Key Concepts Demonstrated -- **`app.NewCMD()`**: Initializes a new GoFr CLI application. -- **`app.SubCommand("name", handler)`**: Registers a subcommand with its handler function. -- **Manual Argument Parsing**: Use `os.Args` with a helper function (`getFlagValue`) to extract flag values (e.g., `--name`). -- **`ctx.Out.Println()`**: Prints output directly to the standard CLI output stream. -- **Subcommand Handler Signature:** - ```go - func(ctx *gofr.Context) (any, error) - ``` - This is required for all subcommand handlers, returning an optional result and an error. \ No newline at end of file diff --git a/examples/cli-example/example1-mutli-sub-command/main.go b/examples/cli-example/example1-mutli-sub-command/main.go deleted file mode 100644 index 3c2eed4f8..000000000 --- a/examples/cli-example/example1-mutli-sub-command/main.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - - "gofr.dev/pkg/gofr" -) - -// helper function to parse flags manually -func getFlagValue(args []string, flag string, defaultValue string) string { - for i := 0; i < len(args); i++ { - if args[i] == flag && i+1 < len(args) { - return args[i+1] - } - } - return defaultValue -} - -func main() { - app := gofr.NewCMD() - - // ----------------------- - // Subcommand: hello - // ----------------------- - app.SubCommand("hello", func(ctx *gofr.Context) (any, error) { - args := os.Args[2:] // arguments after "hello" - name := getFlagValue(args, "--name", "") - if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { - name = args[0] // fallback to positional arg - } - if name == "" { - name = "World" - } - ctx.Out.Println(fmt.Sprintf("Hello, %s!", name)) - return nil, nil - }) - - // ----------------------- - // Subcommand: goodbye - // ----------------------- - app.SubCommand("goodbye", func(ctx *gofr.Context) (any, error) { - args := os.Args[2:] // arguments after "goodbye" - name := getFlagValue(args, "--name", "") - if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { - name = args[0] - } - if name == "" { - name = "Friend" - } - ctx.Out.Println(fmt.Sprintf("Goodbye, %s!", name)) - return nil, nil - }) - - // Run the CLI app - app.Run() -} diff --git a/examples/cli-example/example2-single-sub-command/main.go b/examples/cli-example/example2-single-sub-command/main.go deleted file mode 100644 index 29f3fbcf6..000000000 --- a/examples/cli-example/example2-single-sub-command/main.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - - "gofr.dev/pkg/gofr" -) - -// Helper function to manually parse flags -func getFlagValue(args []string, flag string, defaultValue string) string { - for i := 0; i < len(args); i++ { - if args[i] == flag && i+1 < len(args) { - return args[i+1] - } - } - return defaultValue -} - -func main() { - app := gofr.NewCMD() - - // ----------------------- - // Subcommand: hello - // ----------------------- - app.SubCommand("hello", func(ctx *gofr.Context) (any, error) { - args := os.Args[2:] // arguments after "hello" - name := getFlagValue(args, "--name", "") - - // fallback to positional argument if --name not provided - if name == "" && len(args) > 0 && !strings.HasPrefix(args[0], "--") { - name = args[0] - } - - if name == "" { - name = "World" - } - - ctx.Out.Println(fmt.Sprintf("Hello, %s!", name)) - return nil, nil - }) - - // Run the CLI application - app.Run() -}