Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/devnote-dev/cling

A modular, non-macro-based command line interface library
https://github.com/devnote-dev/cling

cli cling command-line crystal crystal-lang

Last synced: about 1 month ago
JSON representation

A modular, non-macro-based command line interface library

Awesome Lists containing this project

README

        

# Cling

Based on [spf13/cobra](https://github.com/spf13/cobra), Cling is built to be almost entirely modular, giving you absolute control over almost everything without the need for embedded macros - there isn't even a default help command or flag!

## Contents

- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [Commands](#commands)
- [Arguments and Options](#arguments-and-options)
- [Customising](#customising)
- [Extensions](#extensions)
- [Motivation](#motivation)
- [Projects using Cling](#projects-using-cling)
- [Contributing](#contributing)
- [Contributors](#contributors)

## Installation

1. Add the dependency to your `shard.yml`:

```yaml
dependencies:
cling:
github: devnote-dev/cling
version: ">= 3.0.0"
```

2. Run `shards install`

## Basic Usage

```crystal
require "cling"

class MainCommand < Cling::Command
def setup : Nil
@name = "greet"
@description = "Greets a person"
add_argument "name", description: "the name of the person to greet", required: true
add_option 'c', "caps", description: "greet with capitals"
add_option 'h', "help", description: "sends help information"
end

def pre_run(arguments : Cling::Arguments, options : Cling::Options) : Bool
if options.has? "help"
puts help_template # generated using Cling::Formatter

false
else
true
end
end

def run(arguments : Cling::Arguments, options : Cling::Options) : Nil
message = "Hello, #{arguments.get("name")}!"

if options.has? "caps"
puts message.upcase
else
puts message
end
end
end

main = MainCommand.new
main.execute ARGV
```

```
$ crystal greet.cr -h
Usage:
greet [options]

Arguments:
name the name of the person to greet (required)

Options:
-c, --caps greet with capitals
-h, --help sends help information

$ crystal greet.cr Dev
Hello, Dev!

$ crystal greet.cr -c Dev
HELLO, DEV!
```

## Commands

By default, the `Command` class is initialized with almost no values. All information about the command must be defined in the `setup` method.

```crystal
class MainCommand < Cling::Command
def setup : Nil
@name = "greet"
@description = "Greets a person"
# defines an argument
add_argument "name", description: "the name of the person to greet", required: true
# defines a flag option
add_option 'c', "caps", description: "greet with capitals"
add_option 'h', "help", description: "sends help information"
end
end
```

> [!NOTE]
> See [command.cr](/src/cling/command.cr) for the full list of options.

Commands can also contain children, or subcommands:

```crystal
require "cling"
# import our subcommand here
require "./welcome_command"

# using the `MainCommand` created earlier
main = MainCommand.new
main.add_command WelcomeCommand.new
# there is also the `add_commands` method for adding multiple
# subcommands at one time

# run the command
main.execute ARGV
```

```
$ crystal greet.cr -h
Usage:
greet [options]

Commands:
welcome sends a friendly welcome message

Arguments:
name the name of person to greet (required)

Options:
-c, --caps greet with capitals
-h, --help sends help information

$ crystal greet.cr welcome Dev
Welcome to the CLI world, Dev!
```

As well as being able to have subcommands, they can also inherit certain properties from the parent command:

```crystal
# in welcome_command.cr ...
class WelcomeCommand < Cling::Command
def setup : Nil
# ...

# this will inherit the header and footer properties
@inherit_borders = true
# this will NOT inherit the parent flag options
@inherit_options = false
# this will inherit the input, output and error IO streams
@inherit_streams = true
end
end
```

## Arguments and Options

Arguments and flag options can be defined in the `setup` method of a command using the `add_argument` and `add_option` methods respectively.

```crystal
class MainCommand < Cling::Command
def setup : Nil
add_argument "name",
# sets a description for it
description: "the name of the person to greet",
# set it as a required or optional argument
required: true,
# allow multiple values for the argument
multiple: false,
# make it hidden from the help template
hidden: false

# define an option with a short flag using chars
add_option 'c', "caps",
# sets a description for it
description: "greet with capitals",
# set it as a required or optional flag
required: false,
# the type of option it is, can be:
# :none to take no arguments
# :single to take one argument
# or :multiple to take multiple arguments
type: :none,
# optionally set a default value
default: nil,
# make it hidden from the help template
hidden: false
end
end
```

> [!WARNING]
> You can only have **one** argument with the `multiple` option which will include all the remaining input values (or unknown arguments). See the [example command](/examples/cat/cat.cr) for usage.

These arguments and options can then be accessed at execution time via the `arguments` and `options` parameters in the `pre_run`, `run` and `post_run` methods of a command:

```crystal
class MainCommand < Cling::Command
# ...

def pre_run(arguments : Cling::Arguments, options : Cling::Options) : Bool # can also be `Nil`
if arguments.get("name").as_s.blank?
stderr.puts "Your name can't be blank!"

false
else
true
end
end
end
```

The `pre_run` method is slightly different to the other run methods: it allows returning a boolean to the command executor, which will determine whether the command should continue running – `false` will stop the command, `true` will continue. Explicitly returning `nil` or not specifying a return type is the same as returning `true`; the command will continue to run.

If you try to access the value of an argument or option that isn't set, it will raise a `ValueNotFound` exception. To avoid this, use the `get?` method and check accordingly:

```crystal
# ...

def run(arguments : Cling::Arguments, options : Cling::Options) : Nil
caps = options.get?("caps").try(&.as_bool) || false
stdout.puts caps # => false
end
```

> [!NOTE]
> See [argument.cr](/src/cling/argument.cr#L34) and [option.cr](/src/cling/option.cr#L51) for more information on parameter methods, and [value.cr](/src/cling/value.cr) for value methods.

## Customising

The help template is divided into the following sections:

```
[HEADER]

[DESCRIPTION]

[USAGE]
]" "[]">

[COMMANDS]
[ALIASES]

[ARGUMENTS]
["(required)"]

[OPTIONS]
[SHORT] ["(required)"] ["(default: ...)"]

[FOOTER]
```

Sections in `<>` will always be present, and ones in `[]` are optional depending on whether they are defined. Because of Cling's modularity, this means that you could essentially have a blank help template (wouldn't recommend it though).

You can customise the following options for the help template formatter:

```crystal
class Cling::Formatter::Options
# The character to use for flag option delimiters (default is `-`).
property option_delim : Char

# Whether to show the `default` tag for options with default values (default is `true`).
property show_defaults : Bool

# Whether to show the `required` tag for required arguments/options (default is `true`).
property show_required : Bool
end
```

And pass it to the command like so:

```crystal
require "cling"

options = Cling::Formatter::Options.new option_delim: '+', show_defaults: false
# we can re-use this in multiple commands
formatter = Cling::Formatter.new options

class MainCommand < Cling::Command
# ...

def help_template : String
formatter.generate self
end
end
```

Alternatively, if you want a completely custom design, you can pass a string directly:

```crystal
def help_template : String
<<-TEXT
My custom command help text!

Use:
greet [-c | --caps] [-h | --help]
TEXT
end
```

## Extensions

Cling comes with a few useful extension methods for handling argument and option values:

```crystal
require "cling"
require "cling/ext"

class StatCommand < Cling::MainCommand
def setup : Nil
super

@name = "stat"
@description = "Gets the stat information of a file"

add_argument "path", description: "the path of the file to stat", required: true
end

def run(arguments : Cling::Arguments, options : Cling::Options) : Nil
path = arguments.get("path").as_path

if File.exists? path
info = File.info path
stdout.puts <<-INFO
name: #{path.basename}
size: #{info.size}
directory: #{info.directory?}
symlink: #{info.symlink?}
permissions: #{info.permissions}
INFO
else
stderr.puts "No file found at that path"
end
end
end

StatCommand.new.execute ARGV
```

```
$ crystal stat.cr ./shard.yml
name: shard.yml
size: 272
directory: false
symlink: false
permissions: rwxrwxrwx (0o777)
```

> [!NOTE]
> See [ext.cr](/src/cling/ext.cr) for the full list of extension methods.

Additionally, you can define your own extension methods on the `Value` struct like so:

```crystal
require "cling"

module Cling
struct Value
def as_chars : Array(Char)
@raw.to_s.chars
end
end
end

class StatCommand
# ...

def pre_run(arguments : Cling::Arguments, options : Cling::Options) : Nil
puts arguments.get("path").as_chars
# => ['.', '/', 's', 'h', 'a', 'r', 'd', '.', 'y', 'm', 'l']
end
end
```

## Motivation

Most Crystal CLI builders/DSLs are opinionated with limited customisation available. Cling aims to be entirely modular so that you have the freedom to change whatever you want without having to write tons of boilerplate or monkey-patch code. Macro-based CLI shards can also be quite restrictive as they are not scalable, meaning that you may eventually have to refactor your application to another CLI shard. This is not meant to discourage you from using macro-based CLI shards, they are still useful for short and simple applications with a general template, but if you are looking for something to handle larger applications with guaranteed stability and scalability, Cling is the library for you.

## Projects using Cling

Information made available thanks to [shards.info](https://shards.info/github/devnote-dev/cling/).

- [Docr](https://github.com/devnote-dev/docr) - A CLI tool for searching Crystal documentation
- [Fossil](https://github.com/PteroPackages/Fossil) - 📦 Pterodactyl Archive Manager
- [Geode](https://github.com/devnote-dev/geode) - An alternative Crystal package manager
- [Crimson](https://github.com/crimson-crystal/crimson) - A Crystal Version Manager
- [tanda_cli](https://github.com/DanielGilchrist/tanda_cli) - A CLI application for people using Tanda/Workforce.com

## Contributing

1. Fork it ()
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

## Contributors

- [Devonte W](https://github.com/devnote-dev) - creator and maintainer

This repository is managed under the Mozilla Public License v2.

© 2022-present devnote-dev