Installation

# Add this to your shard.yml
dependencies:
  clim:
    github: at-grandpa/clim
    version: ~> 0.17.1

Then run:

shards install

shard.yml

Crystal
>= 1.0.0, < 2.0.0
License
MIT
Author
at-grandpa <@at_grandpa>
Target
  • clim from src/clim.cr

Dependencies

This version declares no dependencies.

README

# clim

"clim" is slim command line interface builder for Crystal.

_"clim" = "cli" + "slim"_

![spec1](https://github.com/at-grandpa/clim/workflows/spec1/badge.svg)
![spec2](https://github.com/at-grandpa/clim/workflows/spec2/badge.svg)
![spec3](https://github.com/at-grandpa/clim/workflows/spec3/badge.svg)
![spec4](https://github.com/at-grandpa/clim/workflows/spec4/badge.svg)
![spec5](https://github.com/at-grandpa/clim/workflows/spec5/badge.svg)
![format-check](https://github.com/at-grandpa/clim/workflows/check-formatting/badge.svg)
![completion](https://github.com/at-grandpa/clim/workflows/completion/badge.svg)

## Goals

* Slim implementation.
* Intuitive code.

## Support

- [x] Option types
  - [x] `Int8`
  - [x] `Int16`
  - [x] `Int32`
  - [x] `Int64`
  - [x] `UInt8`
  - [x] `UInt16`
  - [x] `UInt32`
  - [x] `UInt64`
  - [x] `Float32`
  - [x] `Float64`
  - [x] `String`
  - [x] `Bool`
  - [x] `Array(Int8)`
  - [x] `Array(Int16)`
  - [x] `Array(Int32)`
  - [x] `Array(Int64)`
  - [x] `Array(UInt8)`
  - [x] `Array(UInt16)`
  - [x] `Array(UInt32)`
  - [x] `Array(UInt64)`
  - [x] `Array(Float32)`
  - [x] `Array(Float64)`
  - [x] `Array(String)`
- [x] Argument types
  - [x] `Int8`
  - [x] `Int16`
  - [x] `Int32`
  - [x] `Int64`
  - [x] `UInt8`
  - [x] `UInt16`
  - [x] `UInt32`
  - [x] `UInt64`
  - [x] `Float32`
  - [x] `Float64`
  - [x] `String`
  - [x] `Bool`
- [x] Default values for option & argument
- [x] Required flag for option & argument
- [x] Nested sub commands
- [x] `--help` option
- [x] Customizable help message
- [x] `version` macro
- [x] Command name alias
- [x] Bash completion


## Installation

Add this to your application's `shard.yml`:

```yaml
dependencies:
  clim:
    github: at-grandpa/clim
    version: 0.17.1
```

## Samples

### Minimum sample

*src/minimum.cr*

```crystal
require "clim"

class MyCli < Clim
  main do
    run do |opts, args|
      puts "#{args.all_args.join(", ")}!"
    end
  end
end

MyCli.start(ARGV)
```

```console
$ crystal build -o ./minimum src/minimum.cr
$ ./minimum foo bar baz
foo, bar, baz!
```

### Command information sample

*src/hello.cr*

```crystal
require "clim"

module Hello
  class Cli < Clim
    main do
      desc "Hello CLI tool."
      usage "hello [options] [arguments] ..."
      version "Version 0.1.0"
      option "-g WORDS", "--greeting=WORDS", type: String, desc: "Words of greetings.", default: "Hello"
      argument "first_member", type: String, desc: "first member name.", default: "member1"
      argument "second_member", type: String, desc: "second member name.", default: "member2"
      run do |opts, args|
        print "#{opts.greeting}, "
        print "#{args.first_member} & #{args.second_member} !\n"
        print "And #{args.unknown_args.join(", ")} !"
        print "\n"
      end
    end
  end
end

Hello::Cli.start(ARGV)
```

```console
$ crystal build src/hello.cr
$ ./hello --help

  Hello CLI tool.

  Usage:

    hello [options] [arguments] ...

  Options:

    -g WORDS, --greeting=WORDS       Words of greetings. [type:String] [default:"Hello"]
    --help                           Show this help.
    --version                        Show version.

  Arguments:

    01. first_member       first member name. [type:String] [default:"member1"]
    02. second_member      second member name. [type:String] [default:"member2"]

$ ./hello -g 'Good night' Ichiro Miko Takashi Taro
Good night, Ichiro & Miko !
And Takashi, Taro !
```

### Sub commands sample

*src/fake-crystal-command.cr*

```crystal
require "clim"

module FakeCrystalCommand
  class Cli < Clim
    main do
      desc "Fake Crystal command."
      usage "fcrystal [sub_command] [arguments]"
      run do |opts, args|
        puts opts.help_string # => help string.
      end
      sub "tool" do
        desc "run a tool"
        usage "fcrystal tool [tool] [arguments]"
        run do |opts, args|
          puts "Fake Crystal tool!!"
        end
        sub "format" do
          desc "format project, directories and/or files"
          usage "fcrystal tool format [options] [file or directory]"
          run do |opts, args|
            puts "Fake Crystal tool format!!"
          end
        end
      end
      sub "spec" do
        desc "build and run specs"
        usage "fcrystal spec [options] [files]"
        run do |opts, args|
          puts "Fake Crystal spec!!"
        end
      end
    end
  end
end

FakeCrystalCommand::Cli.start(ARGV)
```

Build and run.

```console
$ crystal build -o ./fcrystal src/fake-crystal-command.cr
$ ./fcrystal

  Fake Crystal command.

  Usage:

    fcrystal [sub_command] [arguments]

  Options:

    --help                           Show this help.

  Sub Commands:

    tool   run a tool
    spec   build and run specs

```

Show sub command help.

```console
$ ./fcrystal tool --help

  run a tool

  Usage:

    fcrystal tool [tool] [arguments]

  Options:

    --help                           Show this help.

  Sub Commands:

    format   format project, directories and/or files

```

Run sub sub command.

```console
$ ./fcrystal tool format
Fake Crystal tool format!!
```

## How to use

### require & inherit

```crystal
require "clim"

class MyCli < Clim

  # ...

end
```

### Command Informations

#### desc

Description of the command. It is displayed in Help.

```crystal
class MyCli < Clim
  main do
    desc "My Command Line Interface."
    run do |opts, args|
      # ...
    end
  end
end
```

#### usage

Usage of the command. It is displayed in Help.

```crystal
class MyCli < Clim
  main do
    usage  "mycli [sub-command] [options] ..."
    run do |opts, args|
      # ...
    end
  end
end
```

#### alias_name

An alias for the command. It can be specified only for subcommand.

```crystal
require "clim"

class MyCli < Clim
  main do
    run do |opts, args|
      # ...
    end
    sub "sub" do
      alias_name  "alias1", "alias2"
      run do |opts, args|
        puts "sub_command run!!"
      end
    end
  end
end

MyCli.start(ARGV)
```

```console
$ ./mycli sub
sub_command run!!
$ ./mycli alias1
sub_command run!!
$ ./mycli alias2
sub_command run!!
```

#### version

You can specify the string to be displayed with `--version`.

```crystal
require "clim"

class MyCli < Clim
  main do
    version "mycli version: 1.0.1"
    run do |opts, args|
      # ...
    end
  end
end

MyCli.start(ARGV)
```

```console
$ ./mycli --version
mycli version: 1.0.1
```

If you want to display it even with `-v`, add ` short: "-v" `.

```crystal
require "clim"

class MyCli < Clim
  main do
    version "mycli version: 1.0.1", short: "-v"
    run do |opts, args|
      # ...
    end
  end
end

MyCli.start(ARGV)
```

```console
$ ./mycli --version
mycli version: 1.0.1
$ ./mycli -v
mycli version: 1.0.1
```

#### Short option for help

The short help option is not set by default. If you want help to appear by specifying `-h` , specify `help short: "-h"` .

(However, it should not conflict with other options.)

```crystal
require "clim"

class MyCli < Clim
  main do
    desc "help directive test."
    usage "mycli [options] [arguments]"
    help short: "-h"
    run do |opts, args|
      # ...
    end
  end
end

MyCli.start(ARGV)
```

```console
$ ./mycli -h

  help directive test.

  Usage:

    mycli [options] [arguments]

  Options:

    -h, --help                       Show this help.

$ ./mycli --help

  help directive test.

  Usage:

    mycli [options] [arguments]

  Options:

    -h, --help                       Show this help.

```

In addition to `-h`, you can specify any single character. For example, `help short: "-a"` .

#### option

You can specify multiple options for the command.

 | Argument        | Description        | Example                       | Required | Default                 |
 | --------------- | ------------------ | ----------------------------- | -------- | ----------------------- |
 | First argument  | short or long name | `-t TIMES`, `--times TIMES`   | true     | -                       |
 | Second argument | long name          | `--times TIMES`               | false    | -                       |
 | `type`          | option type        | `type: Array(Float32)`        | false    | `String`                |
 | `desc`          | option description | `desc: "option description."` | false    | `"Option description."` |
 | `default`       | default value      | `default: [1.1_f32, 2.2_f32]` | false    | `nil`                   |
 | `required`      | required flag      | `required: true`              | false    | `false`                 |

```crystal
class MyCli < Clim
  main do
    option "--greeting=WORDS", desc: "Words of greetings.", default: "Hello"
    option "-n NAME", "--name=NAME", type: Array(String), desc: "Target name.", default: ["Taro"]
    run do |opts, args|
      puts typeof(opts.greeting) # => String
      puts typeof(opts.name)     # => Array(String)
    end
  end
end
```

The type of the option is determined by the `default` and `required` patterns.

*Number*

For example `Int8`.

 | `default` | `required` | Type                                    |
 | --------- | ---------- | --------------------------------------- |
 | exist     | `true`     | `Int8` (default: Your specified value.) |
 | exist     | `false`    | `Int8` (default: Your specified value.) |
 | not exist | `true`     | `Int8`                                  |
 | not exist | `false`    | `Int8 \| Nil`                           |

*String*

 | `default` | `required` | Type                                      |
 | --------- | ---------- | ----------------------------------------- |
 | exist     | `true`     | `String` (default: Your specified value.) |
 | exist     | `false`    | `String` (default: Your specified value.) |
 | not exist | `true`     | `String`                                  |
 | not exist | `false`    | `String \| Nil`                           |

*Bool*

 | `default` | `required` | Type                                    |
 | --------- | ---------- | --------------------------------------- |
 | exist     | `true`     | `Bool` (default: Your specified value.) |
 | exist     | `false`    | `Bool` (default: Your specified value.) |
 | not exist | `true`     | `Bool`                                  |
 | not exist | `false`    | `Bool` (default: `false`)               |

*Array*

 | `default` | `required` | Type                                        |
 | --------- | ---------- | ------------------------------------------- |
 | exist     | `true`     | `Array(T)` (default: Your specified value.) |
 | exist     | `false`    | `Array(T)` (default: Your specified value.) |
 | not exist | `true`     | `Array(T)`                                  |
 | not exist | `false`    | `Array(T)` (default: `[] of T`)             |

For Bool, you do not need to specify arguments for short or long.

```crystal
class MyCli < Clim
  main do
    option "-v", "--verbose", type: Bool, desc: "Verbose."
    run do |opts, args|
      puts typeof(opts.verbose) # => Bool
    end
  end
end
```

Option method names are long name if there is a long, and short name if there is only a short. Also, hyphens are replaced by underscores.

```crystal
class MyCli < Clim
  main do
    option "-n", type: String, desc: "name."  # => short name only.
    option "--my-age", type: Int32, desc: "age." # => long name only.
    run do |opts, args|
      puts typeof(opts.n)      # => (String | Nil)
      puts typeof(opts.my_age) # => (Int32 | Nil)
    end
  end
end
```

#### argument

You can specify multiple arguments for the command.

 | Argument       | Description          | Example                         | Required | Default                   |
 | -------------- | -------------------- | ------------------------------- | -------- | ------------------------- |
 | First argument | name                 | `my_argument`                   | true     | -                         |
 | `type`         | argument type        | `type: String`                  | false    | `String`                  |
 | `desc`         | argument description | `desc: "argument description."` | false    | `"Argument description."` |
 | `default`      | default value        | `default: "default value"`      | false    | `nil`                     |
 | `required`     | required flag        | `required: true`                | false    | `false`                   |

The order of the arguments is related to the order in which they are defined. Also, when calling a method, hyphens in the method name of the argument are converted to underscores. There are also `all_args`, `unknown_args` and `argv` methods.

```crystal
require "clim"

class MyCli < Clim
  main do
    desc "argument sample"
    usage "command [options] [arguments]"

    option "--dummy=WORDS",
      desc: "dummy option"

    argument "first-arg",
      desc: "first argument!",
      type: String,
      default: "default value"

    argument "second-arg",
      desc: "second argument!",
      type: Int32,
      default: 999

    run do |opts, args|
      puts "typeof(args.first_arg)    => #{typeof(args.first_arg)}"
      puts "       args.first_arg     => #{args.first_arg}"
      puts "typeof(args.second_arg)   => #{typeof(args.second_arg)}"
      puts "       args.second_arg    => #{args.second_arg}"
      puts "typeof(args.all_args)     => #{typeof(args.all_args)}"
      puts "       args.all_args      => #{args.all_args}"
      puts "typeof(args.unknown_args) => #{typeof(args.unknown_args)}"
      puts "       args.unknown_args  => #{args.unknown_args}"
      puts "typeof(args.argv)         => #{typeof(args.argv)}"
      puts "       args.argv          => #{args.argv}"
    end
  end
end

```

```console
$ crystal run src/argument.cr -- --help

  argument sample

  Usage:

    command [options] [arguments]

  Options:

    --dummy=WORDS                    dummy option [type:String]
    --help                           Show this help.

  Arguments:

    01. first-arg       first argument! [type:String] [default:"default value"]
    02. second-arg      second argument! [type:Int32] [default:999]

$ crystal run src/argument.cr -- 000 111 --dummy dummy_words 222 333
typeof(args.first_arg)    => String
       args.first_arg     => 000
typeof(args.second_arg)   => Int32
       args.second_arg    => 111
typeof(args.all_args)     => Array(String)
       args.all_args      => ["000", "111", "222", "333"]
typeof(args.unknown_args) => Array(String)
       args.unknown_args  => ["222", "333"]
typeof(args.argv)         => Array(String)
       args.argv          => ["000", "111", "--dummy", "dummy_words", "222", "333"]

```

The type of the arguments is determined by the `default` and `required` patterns.

*Number*

For example `Int8`.

 | `default` | `required` | Type                                    |
 | --------- | ---------- | --------------------------------------- |
 | exist     | `true`     | `Int8` (default: Your specified value.) |
 | exist     | `false`    | `Int8` (default: Your specified value.) |
 | not exist | `true`     | `Int8`                                  |
 | not exist | `false`    | `Int8 \| Nil`                           |

*String*

 | `default` | `required` | Type                                      |
 | --------- | ---------- | ----------------------------------------- |
 | exist     | `true`     | `String` (default: Your specified value.) |
 | exist     | `false`    | `String` (default: Your specified value.) |
 | not exist | `true`     | `String`                                  |
 | not exist | `false`    | `String \| Nil`                           |

*Bool*

 | `default` | `required` | Type                                    |
 | --------- | ---------- | --------------------------------------- |
 | exist     | `true`     | `Bool` (default: Your specified value.) |
 | exist     | `false`    | `Bool` (default: Your specified value.) |
 | not exist | `true`     | `Bool`                                  |
 | not exist | `false`    | `Bool \| Nil`                           |

### help_template

You can customize the help message by `help_template` block. It must be placed in main block. Also it needs to return `String`. Block arguments are `desc : String`, `usage : String`, `options : HelpOptionsType`, `argments : HelpArgumentsType` and `sub_commands : HelpSubCommandsType`.

*help_template_test.cr*

```crystal
require "clim"

class MyCli < Clim
  main do
    help_template do |desc, usage, options, arguments, sub_commands|
      options_help_lines = options.map do |option|
        option[:names].join(", ") + "\n" + "    #{option[:desc]}"
      end
      arguments_help_lines = arguments.map do |argument|
        ("%02d: " % [argument[:sequence_number]]) +
          argument[:display_name] +
          "\n" +
          "      #{argument[:desc]}"
      end

      base = <<-BASE_HELP
      #{usage}

      #{desc}

      options:
      #{options_help_lines.join("\n")}

      arguments:
      #{arguments_help_lines.join("\n")}

      BASE_HELP

      sub = <<-SUB_COMMAND_HELP

      sub commands:
      #{sub_commands.map(&.[](:help_line)).join("\n")}
      SUB_COMMAND_HELP

      sub_commands.empty? ? base : base + sub
    end
    desc "Your original command line interface tool."
    usage <<-USAGE
    usage: my_cli [--version] [--help] [-P PORT|--port=PORT]
                  [-h HOST|--host=HOST] [-p PASSWORD|--password=PASSWORD] [arguments]
    USAGE
    version "version 1.0.0"
    option "-P PORT", "--port=PORT", type: Int32, desc: "Port number.", default: 3306
    option "-h HOST", "--host=HOST", type: String, desc: "Host name.", default: "localhost"
    option "-p PASSWORD", "--password=PASSWORD", type: String, desc: "Password."
    argument "image_name", type: String, desc: "The name of your favorite docker image."
    argument "container_id", type: String, desc: "The ID of the running container."
    run do |opts, args|
    end
    sub "sub_command" do
      desc "my_cli's sub_comand."
      run do |opts, args|
      end
    end
  end
end

MyCli.start(ARGV)
```

```console
$ crystal run src/help_template_test.cr -- --help
usage: my_cli [--version] [--help] [-P PORT|--port=PORT]
              [-h HOST|--host=HOST] [-p PASSWORD|--password=PASSWORD] [arguments]

Your original command line interface tool.

options:
-P PORT, --port=PORT
    Port number.
-h HOST, --host=HOST
    Host name.
-p PASSWORD, --password=PASSWORD
    Password.
--help
    Show this help.
--version
    Show version.

arguments:
01: image_name
      The name of your favorite docker image.
02: container_id
      The ID of the running container.

sub commands:
    sub_command   my_cli's sub_comand.

```

options:

```crystal
# `options` type
alias HelpOptionsType = Array(NamedTuple(
    names:     Array(String),
    type:      Int8.class | Int32.class | ... | String.class | Bool.class, # => Support Types
    desc:      String,
    default:   Int8 | Int32 | ... | String | Bool, # => Support Types,
    required:  Bool,
    help_line: String
))

# `options` example
[
  {
    names:     ["-g WORDS", "--greeting=WORDS"],
    type:      String,
    desc:      "Words of greetings.",
    default:   "Hello",
    required:  false,
    help_line: "    -g WORDS, --greeting=WORDS       Words of greetings. [type:String] [default:\"Hello\"]",
  },
  {
    names:     ["-n NAME"],
    type:      Array(String),
    desc:      "Target name.",
    default:   ["Taro"],
    required:  true,
    help_line: "    -n NAME                          Target name. [type:Array(String)] [default:[\"Taro\"]] [required]",
  },
  {
    names:     ["--help"],
    type:      Bool,
    desc:      "Show this help.",
    default:   false,
    required:  false,
    help_line: "    --help                           Show this help.",
  },
]
```
arguments:

```crystal
# `arguments` type
alias HelpArgumentsType = Array(NamedTuple(
    method_name:     String,
    display_name:    String,
    type:            Int8.class | Int32.class | ... | String.class | Bool.class, # => Support Types
    desc:            String,
    default:         Int8 | Int32 | ... | String | Bool, # => Support Types,
    required:        Bool,
    sequence_number: Int32,
    help_line:       String
))

# `arguments` example
[
  {
    method_name:     "argument1",
    display_name:    "argument1",
    type:            String,
    desc:            "first argument.",
    default:         "default value",
    required:        true,
    sequence_number: 1,
    help_line:       "    01. argument1            first argument. [type:String] [default:\"default value\"] [required]",
  },
  {
    method_name:     "argument2foo",
    display_name:    "argument2foo",
    type:            Int32,
    desc:            "second argument.",
    default:         1,
    required:        false,
    sequence_number: 2,
    help_line:       "    02. argument2foo         second argument. [type:Int32] [default:1]",
  },
]
```

sub_commands:

```crystal
# `sub_commands` type
alias HelpSubCommandsType = Array(NamedTuple(
    names:     Array(String),
    desc:      String,
    help_line: String
))

# `sub_commands` example
[
  {
    names:     ["abc", "def", "ghi"],
    desc:      "abc command.",
    help_line: "    abc, def, ghi            abc command.",
  },
  {
    names:     ["abcdef", "ghijkl", "mnopqr"],
    desc:      "abcdef command.",
    help_line: "    abcdef, ghijkl, mnopqr   abcdef command.",
  },
]
```

### help string

```crystal
class MyCli < Clim
  main do
    run do |opts, args|
      opts.help_string # => help string
    end
  end
end
```

### `io` in run block

You can receive `io` in a run block by passing it as the second argument to the start method.

```crystal
require "clim"

class IoCommand < Clim
  main do
    run do |opts, args, io|
      io.puts "in main"
    end
  end
end

io = IO::Memory.new
IoCommand.start([] of String, io: io)
puts io.to_s # => "in main\n"
```

### Bash completion

You can use bash completion.

*src/sample.cr*

```crystal
require "clim"

class Cli < Clim
  main do
    version "Version 1.0"
    option "-p PORT", "--port=PORT", type: Int32, desc: "Port number."
    option "-h HOST", "--host=HOST", type: String, desc: "Host name."
    run do |opts, args|
      # ...
    end

    sub "tool" do
      option "-v", "--verbose", type: Bool
      run do |opts, args|
        # ...
      end
    end

    sub "run" do
      help short: "-h"
      run do |opts, args|
        # ...
      end
    end
  end
end

Cli.start(ARGV)
```

- Step1: Build your program.
- Step2: Add ```eval "`{your_program} --bash-completion`"``` to `~/.bashrc`.
- Step3: Reload `~/.bashrc`.
- Step4: You can use bash completion for options and subcommands.

```console
$ crystal build src/sample.cr -o /usr/local/bin/sample
$ echo 'eval "`sample --bash-completion`"' >> ~/.bashrc
$ . ~/.bashrc
$ sample [TAB][TAB]
--help     --host     --port     --version  -h         -p         run        tool
$ sample tool -[TAB][TAB]
--help     --verbose  -v
$ sample tool --[TAB][TAB]
--help     --verbose
$ sample tool --help

  Command Line Interface Tool.

  Usage:

    tool [options] [arguments]

  Options:

    -v, --verbose                    Option description. [type:Bool]
    --help                           Show this help.

$
```


## Development

```
$ make spec
```

## Contributing

1. Fork it ( https://github.com/at-grandpa/clim/fork )
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

- [at-grandpa](https://github.com/at-grandpa) - creator, maintainer