caml

Ruby gem

Build CLI apps from a declarative caml.yaml file.

caml turns well-documented YAML tasks into a runnable CLI. make and just inspired it, but the tasks live in YAML. Give a task a name, a desc, and an execute shell command, then run caml to list every task with its description. caml does not track file timestamps, so it never skips work that is already done. Treat it as a task runner, not a build system.

version
1.0.0
ruby
3.0 or newer
license
MIT
install
gem install caml

install

caml ships as a Ruby gem and needs Ruby 3.0 or newer.

gem install caml

To pin caml to one project instead, add it to that project's Gemfile.

gem 'caml'

quickstart

Run caml init first, because it writes the starter caml.yaml that the next two commands read.

gem install caml
caml init       # scaffold a starter caml.yaml
caml            # list tasks
caml hello      # run the starter task

If a caml.yaml already exists, caml refuses to overwrite it and exits 1. Rename the old file, then retry.

The scaffold holds one task, hello. Open the file, replace that task with a real one, then read the reference for every key a task accepts.

features

You declare commands. caml builds the CLI around them on every run.

declarative
Define your commands in YAML. Every top-level key in the file is a task.
three keys
A task needs a name, a desc, and an execute shell command. It needs nothing else.
read on every run
caml reads caml.yaml and registers the commands each time you invoke it. No build step. No cache to clear.

reference

Each topic below shows the YAML first and then explains it, so you can skim the blocks and stop where the shape matches your problem. Copy any example as it stands.

Tasks

test:
  desc: Run the test suite
  execute: bundle exec rspec

Each top-level key is a task. A task has a desc and an execute shell command. Run caml or caml help to list every task with its description.

Multi-line execute

setup:
  desc: Install and migrate
  execute:
    - bundle install
    - bin/rails db:migrate

Pass a list to run several steps. Steps run with fail-fast semantics, so the first non-zero exit aborts the task.

Arguments

greet:
  desc: Say hello to someone
  args:
    name:
      desc: Person to greet
      type: string
  execute: echo Hello, {{name}}!
caml greet world
# Hello, world!

Positional arguments substitute into the execute template through {{name}}. caml shell-escapes every value it substitutes.

Options

build:
  desc: Build the project
  opts:
    target:
      type: string
      default: dist
      aliases:
        - t
      desc: Output directory
    verbose:
      type: boolean
      aliases:
        - v
      desc: Print verbose output
  execute: make build TARGET={{target}}
caml build --target release
caml build -t release -v

An option is a flag with a type. It also takes optional aliases, a default value, and an override execute.

Option-driven dispatch

start:
  desc: Start the app
  opts:
    background:
      type: boolean
      aliases:
        - b
      desc: Run as a daemon
      execute: app start --daemon
  execute: app start
caml start                # app start
caml start --background   # app start --daemon

When a boolean option carries its own execute, that command runs instead of the task's default.

Aliases

test:
  desc: Run the test suite
  aliases:
    - t
  execute: bundle exec rspec
caml t   # same as caml test

Add shortcut names for a task with aliases.

Dependencies

test:
  desc: Run tests
  execute: bundle exec rspec

lint:
  desc: Check style
  execute: bundle exec rubocop

ci:
  desc: Lint and test
  needs:
    - lint
    - test
caml ci   # runs lint, then test

A task can declare prerequisites with needs. Dependencies run in declared order, each at most once per invocation, and a failure aborts the run.

A task with only needs: and no execute is a pure orchestrator, which is useful for grouping.

Discovery

caml walks up from the current directory to find a caml.yaml, just like git. So you can run it from any subdirectory of your project.

If no caml.yaml sits in the current directory or any parent, caml says so and exits 1. Run caml init to scaffold one.

Built-in commands

CommandDescription
caml initScaffold a starter caml.yaml
caml --versionPrint the installed version
caml help [cmd]Show help for a specific task (its args and opts)