hugoduncan

babashka.process

@hugoduncan/babashka.process
hugoduncan
3
0 forks
Updated 1/18/2026
View on GitHub

Clojure library for spawning sub-processes and shell operations

Installation

$skills install @hugoduncan/babashka.process
Claude Code
Cursor
Copilot
Codex
Antigravity

Details

Pathplugins/clojure-libraries/skills/babashka-process/SKILL.md
Branchmaster
Scoped Name@hugoduncan/babashka.process

Usage

After installing, this skill will be available to your AI coding assistant.

Verify installation:

skills list

Skill Instructions


name: babashka.process description: Clojure library for spawning sub-processes and shell operations

babashka.process

A Clojure library for shelling out and spawning sub-processes. Wraps java.lang.ProcessBuilder with an ergonomic API supporting pipelines, streaming I/O, and process control.

Overview

babashka.process provides two main entry points:

  • shell - High-level convenience function with sensible defaults
  • process - Low-level function for fine-grained control

Included in Babashka since v0.2.3. Also usable as a JVM library.

Repository: https://github.com/babashka/process

Installation

;; deps.edn
{:deps {babashka/process {:mvn/version "0.6.25"}}}

Built into Babashka - no installation needed for bb scripts.

Core Concepts

shell vs process

Aspectshellprocess
BlockingYesNo (returns immediately)
Exit checkThrows on non-zeroNo checking
I/O default:inherit (console)Streams
TokenizationAuto-tokenizes first argManual

Use shell for simple commands. Use process for pipelines, streaming, or async operations.

Process Records

Both functions return a record containing:

  • :proc - java.lang.Process instance
  • :in - Input stream (stdin)
  • :out - Output stream (stdout)
  • :err - Error stream (stderr)
  • :cmd - Command vector
  • :prev - Previous process (pipelines)

Dereferencing (@ or deref) waits for completion and adds :exit.

API Reference

shell

High-level function for running external programs.

(require '[babashka.process :refer [shell]])

;; Basic usage - tokenizes automatically
(shell "ls -la")

;; Multiple arguments
(shell "git" "commit" "-m" "message")

;; With options
(shell {:dir "src"} "ls")

;; Capture output
(-> (shell {:out :string} "echo hello") :out)
;; => "hello\n"

;; Continue on error (don't throw)
(shell {:continue true} "ls nonexistent")

Options:

  • :continue - Don't throw on non-zero exit
  • All process options supported

process

Low-level function with no opinionated defaults.

(require '[babashka.process :refer [process]])

;; Returns immediately
(def p (process "sleep" "5"))

;; Deref to wait and get exit code
(:exit @p)

;; Capture output
(->> (process {:out :string} "ls") deref :out)

check

Wait for process and throw on non-zero exit.

(require '[babashka.process :refer [process check]])

;; Throws if ls fails
(->> (process {:out :string} "ls") check :out)

;; Chain with process
(-> (process "make") check)

sh

Convenience wrapper defaulting :out and :err to :string.

(require '[babashka.process :refer [sh]])

(sh "ls" "-la")
;; => {:exit 0 :out "..." :err ""}

$

Macro for shell-like syntax with interpolation.

(require '[babashka.process :refer [$]])

(def file "README.md")
($ ls -la ~file)

;; With options via metadata
(^{:out :string} $ echo hello)

tokenize

Split string into argument vector.

(require '[babashka.process :refer [tokenize]])

(tokenize "ls -la")
;; => ["ls" "-la"]

(tokenize "echo 'hello world'")
;; => ["echo" "hello world"]

alive?

Check if process is running.

(require '[babashka.process :refer [process alive?]])

(def p (process "sleep" "10"))
(alive? p) ;; => true

destroy / destroy-tree

Terminate process. destroy-tree also kills descendants (JDK9+).

(require '[babashka.process :refer [process destroy destroy-tree]])

(def p (process "sleep" "100"))
(destroy p)

;; Kill process and all children
(destroy-tree p)

exec

Replace current process image (GraalVM/Babashka only).

(require '[babashka.process :refer [exec]])

;; Replaces bb process with ls
(exec "ls" "-la")

pb / pipeline

Create process builders for pipelines.

(require '[babashka.process :refer [pb pipeline]])

;; JDK9+ pipeline
(-> (pipeline (pb "cat" "file.txt")
              (pb "grep" "pattern")
              (pb "wc" "-l"))
    last
    deref
    :out
    slurp)

Options Reference

I/O Options

OptionValuesDescription
:instream, string, :inheritStdin source
:out:string, :bytes, :inherit, :write, :append, fileStdout destination
:errSame as :out, plus :out to mergeStderr destination
:in-enccharsetInput encoding
:out-enccharsetOutput encoding
:err-enccharsetError encoding

Process Options

OptionDescription
:dirWorking directory
:envReplace environment (map)
:extra-envAdd to environment (map)
:inheritIf true, inherit all streams
:cmdCommand vector (overrides args)
:prevPrevious process for piping

Hooks

OptionDescription
:pre-start-fnCalled before start with process info
:shutdownCalled when child process ends
:exit-fnCalled on exit (JDK11+)

Common Patterns

Capture Output

;; As string
(-> (shell {:out :string} "date") :out str/trim)

;; As bytes
(-> (shell {:out :bytes} "cat" "image.png") :out)

;; Merge stderr into stdout
(shell {:err :out :out :string} "cmd")

Working Directory

(shell {:dir "/tmp"} "ls")

Environment Variables

;; Add to environment
(shell {:extra-env {"DEBUG" "1"}} "./script.sh")

;; Replace environment
(shell {:env {"PATH" "/usr/bin"}} "ls")

Piping Processes

;; Using threading
(->> (process "cat" "file.txt")
     (process {:out :string} "grep" "pattern")
     deref
     :out)

;; Using pipeline (JDK9+)
(-> (pipeline (pb "ls") (pb "grep" "clj"))
    last deref :out slurp)

Input to Process

;; String input
(-> (process {:in "hello\nworld" :out :string} "cat")
    deref :out)

;; File input
(-> (process {:in (io/file "data.txt") :out :string} "wc")
    deref :out)

Close Stdin

For processes that read stdin until EOF, close it immediately:

;; Empty string - simplest approach
(process {:in ""} "cmd")

;; Null device
(process {:in null-file} "cmd")

;; Explicit close after start
(let [p (process "cmd")]
  (.close (:in p))
  p)

Streaming Output

(require '[clojure.java.io :as io])

(def p (process {:err :inherit} "bb" "-e" "(doseq [i (range)] (println i) (Thread/sleep 100))"))

(with-open [rdr (io/reader (:out p))]
  (doseq [line (line-seq rdr)]
    (println "Got:" line)))

Interactive Process

(def p (process "cat"))
(def w (io/writer (:in p)))

(binding [*out* w]
  (println "hello")
  (println "world"))
(.close w)

(slurp (:out p))
;; => "hello\nworld\n"

Write to File

;; Overwrite
(shell {:out :write :out-file "log.txt"} "ls")

;; Append
(shell {:out :append :out-file "log.txt"} "date")

Discard Output

(require '[babashka.process :refer [shell null-file]])

(shell {:out null-file :err null-file} "noisy-command")

Timeout

(let [p (process "sleep" "100")]
  (when-not (deref p 1000 nil)
    (destroy-tree p)
    (println "Timed out")))

Pre-start Hook

(shell {:pre-start-fn (fn [{:keys [cmd]}]
                        (println "Running:" cmd))}
       "ls")

Error Handling

Check Exit Code

;; shell throws by default
(try
  (shell "ls" "nonexistent")
  (catch Exception e
    (println "Failed:" (ex-message e))))

;; Suppress with :continue
(let [{:keys [exit]} (shell {:continue true} "ls" "nonexistent")]
  (when-not (zero? exit)
    (println "Command failed")))

Process Errors

;; Manual checking with process
(let [{:keys [exit out err]} @(process {:out :string :err :string} "cmd")]
  (if (zero? exit)
    (println "Success:" out)
    (println "Error:" err)))

Capture Stderr

(let [{:keys [err]} (shell {:err :string :continue true} "ls" "nonexistent")]
  (println "Error output:" err))

Performance Tips

  1. Avoid shell for simple operations - Use Clojure/Java directly when possible
  2. Stream large outputs - Don't use :string for large data; stream instead
  3. Reuse process builders - For repeated commands, create pb once
  4. Use destroy-tree - Prevent zombie processes when killing

Platform Notes

Windows

  • Environment variable names are case-sensitive for :extra-env
  • Cannot launch .ps1 scripts directly; invoke through PowerShell:
    (shell "powershell" "-File" "script.ps1")
    
  • Globbing doesn't work; expand patterns in Clojure

macOS/Linux

  • First argument not tokenized if it contains spaces without quotes
  • Use tokenize explicitly when needed

Comparison with clojure.java.shell

Featureclojure.java.shell/shbabashka.process
BlockingAlwaysExplicit via deref
PipingNoYes
StreamingNoYes
Process controlLimitedFull access
Exit checkingManualcheck / shell

See Also