Skip to content

Commit

Permalink
chore: refactor cli/cmdx package
Browse files Browse the repository at this point in the history
  • Loading branch information
ravisuhag committed Nov 27, 2024
1 parent 71afe66 commit 2b73a7c
Show file tree
Hide file tree
Showing 33 changed files with 1,088 additions and 509 deletions.
193 changes: 193 additions & 0 deletions cli/cmdx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@

# cmdx

`cmdx` is a utility package designed to enhance the functionality of [Cobra](https://github.com/spf13/cobra), a popular Go library for creating command-line interfaces. It provides various helper functions and features to streamline CLI development, such as custom help topics, shell completion, command annotations, and client-specific configurations.

## Features

- **Help Topics**: Add custom help topics with descriptions and examples.
- **Shell Completions**: Generate completion scripts for Bash, Zsh, Fish, and PowerShell.
- **Command Reference**: Generate markdown documentation for all commands.
- **Client Hooks**: Apply custom configurations to commands annotated with `client:true`.


## Installation

To install the `cmdx` package, add it to your project using `go get`:

```bash
go get github.com/raystack/salt/cli/cmdx
```

## Usages

### SetHelpTopicCmd

Provides a way to define custom help topics that appear in the `help` command.

#### Example Usage
```go
topic := map[string]string{
"short": "Environment variables help",
"long": "Detailed information about environment variables used by the CLI.",
"example": "$ mycli help env",
}

rootCmd.AddCommand(cmdx.SetHelpTopicCmd("env", topic))
```

#### Output
```bash
$ mycli help env
Detailed information about environment variables used by the CLI.

EXAMPLES

$ mycli help env
```

---

### SetCompletionCmd

Adds a `completion` command to generate shell completion scripts for Bash, Zsh, Fish, and PowerShell.

#### Example Usage
```go
completionCmd := cmdx.SetCompletionCmd("mycli")
rootCmd.AddCommand(completionCmd)
```

#### Command Output
```bash
# Generate Bash completion script
$ mycli completion bash

# Generate Zsh completion script
$ mycli completion zsh

# Generate Fish completion script
$ mycli completion fish
```

#### Supported Shells
- **Bash**: Use `mycli completion bash`.
- **Zsh**: Use `mycli completion zsh`.
- **Fish**: Use `mycli completion fish`.
- **PowerShell**: Use `mycli completion powershell`.

---

### SetRefCmd

Adds a `reference` command to generate markdown documentation for all commands in the CLI hierarchy.

#### Example Usage
```go
refCmd := cmdx.SetRefCmd(rootCmd)
rootCmd.AddCommand(refCmd)
```

#### Command Output
```bash
$ mycli reference
# mycli reference

## `example`

A sample subcommand for the CLI.

## `another`

Another example subcommand.
```

---

### SetClientHook

Applies a custom function to commands annotated with `client:true`. Useful for client-specific configurations.

#### Example Usage
```go
cmdx.SetClientHook(rootCmd, func(cmd *cobra.Command) {
cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
fmt.Println("Executing client-specific setup for", cmd.Name())
}
})
```

#### Command Example
```go
clientCmd := &cobra.Command{
Use: "client-action",
Short: "A client-specific action",
Annotations: map[string]string{
"client": "true",
},
}
rootCmd.AddCommand(clientCmd)
```

#### Output
```bash
$ mycli client-action
Executing client-specific setup for client-action
```

---

## Examples

Adding all features to a CLI

```go
package main

import (
"fmt"
"github.com/spf13/cobra"
"github.com/raystack/salt/cli/cmdx"
)

func main() {
rootCmd := &cobra.Command{
Use: "mycli",
Short: "A custom CLI tool",
}

// Add Help Topic
topic := map[string]string{
"short": "Environment variables help",
"long": "Details about environment variables used by the CLI.",
"example": "$ mycli help env",
}
rootCmd.AddCommand(cmdx.SetHelpTopicCmd("env", topic))

// Add Completion Command
rootCmd.AddCommand(cmdx.SetCompletionCmd("mycli"))

// Add Reference Command
rootCmd.AddCommand(cmdx.SetRefCmd(rootCmd))

// Add Client Hook
clientCmd := &cobra.Command{
Use: "client-action",
Short: "A client-specific action",
Annotations: map[string]string{
"client": "true",
},
}
rootCmd.AddCommand(clientCmd)

cmdx.SetClientHook(rootCmd, func(cmd *cobra.Command) {
cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
fmt.Println("Executing client-specific setup for", cmd.Name())
}
})

if err := rootCmd.Execute(); err != nil {
fmt.Println("Error:", err)
}
}
```
70 changes: 40 additions & 30 deletions cli/cmdx/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,64 +13,72 @@ import (
"github.com/raystack/salt/config"
)

// Environment variables for configuration paths
const (
RAYSTACK_CONFIG_DIR = "RAYSTACK_CONFIG_DIR"
XDG_CONFIG_HOME = "XDG_CONFIG_HOME"
APP_DATA = "AppData"
LOCAL_APP_DATA = "LocalAppData"
RaystackConfigDirEnv = "RAYSTACK_CONFIG_DIR"
XDGConfigHomeEnv = "XDG_CONFIG_HOME"
AppDataEnv = "AppData"
)

// ConfigLoaderOpt defines a functional option for configuring the Config object.
type ConfigLoaderOpt func(c *Config)

// WithFlags binds command-line flags to configuration values.
func WithFlags(pfs *pflag.FlagSet) ConfigLoaderOpt {
return func(c *Config) {
c.boundedPFlags = pfs
c.boundFlags = pfs
}
}

// WithLoaderOptions adds custom loader options for configuration loading.
func WithLoaderOptions(opts ...config.LoaderOption) ConfigLoaderOpt {
return func(c *Config) {
c.loaderOpts = append(c.loaderOpts, opts...)
}
}

// SetConfig allows to set a client config file. It is used to
// load and save a config file for command line clients.
// SetConfig initializes a new Config object for the specified application.
func SetConfig(app string) *Config {
return &Config{
filename: configFile(app),
}
}

// Config manages the application's configuration file and related operations.
type Config struct {
filename string
boundedPFlags *pflag.FlagSet
loaderOpts []config.LoaderOption
filename string
boundFlags *pflag.FlagSet
loaderOpts []config.LoaderOption
}

// File returns the path to the configuration file.
func (c *Config) File() string {
return c.filename
}

// Defaults populates the given configuration struct with default values.
func (c *Config) Defaults(cfg interface{}) {
defaults.SetDefaults(cfg)
}

// Init initializes the configuration file with default values.
func (c *Config) Init(cfg interface{}) error {
defaults.SetDefaults(cfg)

if fileExist(c.filename) {
return errors.New("config file already exists")
if fileExists(c.filename) {
return errors.New("configuration file already exists")
}

return c.Write(cfg)
}

// Read reads the content of the configuration file as a string.
func (c *Config) Read() (string, error) {
cfg, err := os.ReadFile(c.filename)
return string(cfg), err
data, err := os.ReadFile(c.filename)
return string(data), err
}

// Write writes the given configuration struct to the configuration file in YAML format.
func (c *Config) Write(cfg interface{}) error {
data, err := yaml.Marshal(cfg)
if err != nil {
Expand All @@ -87,42 +95,44 @@ func (c *Config) Write(cfg interface{}) error {
return nil
}

// Load loads the configuration from the file and applies the provided loader options.
func (c *Config) Load(cfg interface{}, opts ...ConfigLoaderOpt) error {
for _, opt := range opts {
opt(c)
}

loaderOpts := []config.LoaderOption{config.WithFile(c.filename)}

if c.boundedPFlags != nil {
loaderOpts = append(loaderOpts, config.WithBindPFlags(c.boundedPFlags, cfg))
if c.boundFlags != nil {
loaderOpts = append(loaderOpts, config.WithBindPFlags(c.boundFlags, cfg))
}
loaderOpts = append(loaderOpts, c.loaderOpts...)

loader := config.NewLoader(loaderOpts...)

if err := loader.Load(cfg); err != nil {
return err
}
return nil
return loader.Load(cfg)
}

// configFile determines the full path to the configuration file for the application.
func configFile(app string) string {
file := app + ".yml"
return filepath.Join(configDir("raystack"), file)
filename := app + ".yml"
return filepath.Join(configDir("raystack"), filename)
}

// configDir determines the appropriate directory for storing configuration files.
func configDir(root string) string {
var path string
if a := os.Getenv(RAYSTACK_CONFIG_DIR); a != "" {
path = a
} else if b := os.Getenv(XDG_CONFIG_HOME); b != "" {
path = filepath.Join(b, root)
} else if c := os.Getenv(APP_DATA); runtime.GOOS == "windows" && c != "" {
path = filepath.Join(c, root)
if env := os.Getenv(RaystackConfigDirEnv); env != "" {
path = env
} else if env := os.Getenv(XDGConfigHomeEnv); env != "" {
path = filepath.Join(env, root)
} else if runtime.GOOS == "windows" {
if env := os.Getenv(AppDataEnv); env != "" {
path = filepath.Join(env, root)
}
} else {
d, _ := os.UserHomeDir()
path = filepath.Join(d, ".config", root)
home, _ := os.UserHomeDir()
path = filepath.Join(home, ".config", root)
}

if !dirExists(path) {
Expand Down
Loading

0 comments on commit 2b73a7c

Please sign in to comment.