Library-skills babashka.process
Clojure library for spawning sub-processes and shell operations
git clone https://github.com/hugoduncan/library-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/hugoduncan/library-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/clojure-libraries/skills/babashka-process" ~/.claude/skills/hugoduncan-library-skills-babashka-process && rm -rf "$T"
plugins/clojure-libraries/skills/babashka-process/SKILL.mdbabashka.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:
- High-level convenience function with sensible defaultsshell
- Low-level function for fine-grained controlprocess
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
| Aspect | | |
|---|---|---|
| Blocking | Yes | No (returns immediately) |
| Exit check | Throws on non-zero | No checking |
| I/O default | (console) | Streams |
| Tokenization | Auto-tokenizes first arg | Manual |
Use
shell for simple commands. Use process for pipelines, streaming, or async operations.
Process Records
Both functions return a record containing:
-:proc
instancejava.lang.Process
- Input stream (stdin):in
- Output stream (stdout):out
- Error stream (stderr):err
- Command vector:cmd
- Previous process (pipelines):prev
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:
- Don't throw on non-zero exit:continue- All
options supportedprocess
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
| Option | Values | Description |
|---|---|---|
| stream, string, | Stdin source |
| , , , , , file | Stdout destination |
| Same as , plus to merge | Stderr destination |
| charset | Input encoding |
| charset | Output encoding |
| charset | Error encoding |
Process Options
| Option | Description |
|---|---|
| Working directory |
| Replace environment (map) |
| Add to environment (map) |
| If true, inherit all streams |
| Command vector (overrides args) |
| Previous process for piping |
Hooks
| Option | Description |
|---|---|
| Called before start with process info |
| Called when child process ends |
| Called 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
- Avoid shell for simple operations - Use Clojure/Java directly when possible
- Stream large outputs - Don't use
for large data; stream instead:string - Reuse process builders - For repeated commands, create
oncepb - Use
- Prevent zombie processes when killingdestroy-tree
Platform Notes
Windows
- Environment variable names are case-sensitive for
:extra-env - Cannot launch
scripts directly; invoke through PowerShell:.ps1(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
explicitly when neededtokenize
Comparison with clojure.java.shell
| Feature | | |
|---|---|---|
| Blocking | Always | Explicit via deref |
| Piping | No | Yes |
| Streaming | No | Yes |
| Process control | Limited | Full access |
| Exit checking | Manual | / |
See Also
- babashka.fs - File system operations
- babashka-cli - CLI argument parsing