Skip to content
AgileLogics
6 min read

Command-Line Scripts and Automation

Prerequisites: Files and I/O, Using Java libraries

Groovy is an excellent language for operations scripts: it has real data structures, JSON/XML/YAML parsing, and every Java library — without a build step.

Parsing options with CliBuilder

import groovy.cli.commons.CliBuilder

def cli = new CliBuilder(usage: 'backup [options] <dirs>')
cli.with {
    h(longOpt: 'help', 'Show usage')
    v(longOpt: 'verbose', 'Print every file')
    d(longOpt: 'dest', args: 1, argName: 'dir', 'Destination folder')
    k(longOpt: 'keep', args: 1, type: Integer, defaultValue: '7', 'Backups to keep')
}

def options = cli.parse(['--verbose', '-d', '/mnt/backup', 'docs', 'photos'])
println "verbose: ${options.v}"
println "dest: ${options.d}"
println "keep: ${options.k}"
println "dirs: ${options.arguments()}"
verbose: true
dest: /mnt/backup
keep: 7
dirs: [docs, photos]

In a real script, pass args instead of a literal list, and call cli.usage() — which prints every option with its description — when options.h is set or parsing fails. Groovy ships two implementations with the same API: groovy.cli.commons (Apache Commons CLI, used here) and groovy.cli.picocli (which needs the picocli jar on the classpath).

Running other programs

// illustrative — runs external commands
def proc = ['git', 'log', '--oneline', '-3'].execute()
def out = new StringBuilder(), err = new StringBuilder()
proc.consumeProcessOutput(out, err)
proc.waitFor()
if (proc.exitValue() != 0) {
    System.err.println "git failed: $err"
    System.exit(1)
}
out.eachLine { println "commit: $it" }

// run in a folder with extra environment variables (Groovy 6)
def build = ['./gradlew', 'test'].execute(dir: 'service', env: [CI: 'true'])
build.inputStream.eachLine { println it }
println "exit code: ${build.waitFor()}"
  • .execute() on a list starts a process — use a list rather than a single string so arguments with spaces are passed correctly.
  • Always consume the output (or the process can block when its buffer fills) and check the exit code.

Working with YAML, TOML, and CSV

import groovy.yaml.YamlSlurper
import groovy.yaml.YamlBuilder

def yaml = '''
services:
  - name: api
    replicas: 3
  - name: worker
    replicas: 1
'''
def conf = new YamlSlurper().parseText(yaml)
println conf.services.collect { "$it.name x$it.replicas" }
println conf.services.sum { it.replicas }

def out = new YamlBuilder()
out(environment: 'prod', replicas: conf.services*.replicas)
println out.toString().trim()
[api x3, worker x1]
4
---
environment: "prod"
replicas:
- 3
- 1

groovy-yaml and groovy-toml (and, in Groovy 6, groovy-csv) follow the same slurper/builder pattern as JSON. Validate YAML with the YAML Validator.

Calling an HTTP API

// illustrative — needs network access
import groovy.json.JsonSlurper
import java.net.http.*

def client = HttpClient.newHttpClient()
def request = HttpRequest.newBuilder(URI.create('https://api.github.com/repos/apache/groovy'))
    .header('Accept', 'application/json')
    .build()
def response = client.send(request, HttpResponse.BodyHandlers.ofString())
def repo = new JsonSlurper().parseText(response.body())
println "${repo.full_name}: ${repo.stargazers_count} stars"

Java's HttpClient works well from Groovy. Groovy 6 also has an incubating groovy-http-builder module with a small request DSL.

Script tips

  • Start with #!/usr/bin/env groovy and chmod +x on Unix.
  • Fail loudly: exit with a non-zero code on errors, so CI notices.
  • Pin @Grab versions, and cache grapes in CI.
  • Start-up takes a second or so; for scripts that run thousands of times a day, compile them (groovyc) or use a long-running service instead.