- 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
```clojure
;; 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 | `shell` | `process` |
|--------|---------|-----------|
| Blocking | Yes | No (returns immediately) |
| Exit check | Throws on non-zero | No checking |
| I/O default | `:inherit` (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` - `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.
```clojure
(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.
```clojure
(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.
```clojure
(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`.
```clojure
(require '[babashka.process :refer [sh]])
(sh "ls" "-la")
;; => {:exit 0 :out "..." :err ""}
```
### $
Macro for shell-like syntax with interpolation.
```clojure
(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.
```clojure
(require '[babashka.process :refer [tokenize]])
(tokenize "ls -la")
;; => ["ls" "-la"]
(tokenize "echo 'hello world'")
;; => ["echo" "hello world"]
```
### alive?
Check if process is running.
```clojure
(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+).
```clojure
(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).
```clojure
(require '[babashka.process :refer [exec]])
;; Replaces bb process with ls
(exec "ls" "-la")
```
### pb / pipeline
Create process builders for pipelines.
```clojure
(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 |
|--------|--------|-------------|
| `:in` | stream, string, `:inherit` | Stdin source |
| `:out` | `:string`, `:bytes`, `:inherit`, `:write`, `:append`, file | Stdout destination |
| `:err` | Same as `:out`, plus `:out` to merge | Stderr destination |
| `:in-enc` | charset | Input encoding |
| `:out-enc` | charset | Output encoding |
| `:err-enc` | charset | Error encoding |
### Process Options
| Option | Description |
|--------|-------------|
| `:dir` | Working directory |
| `:env` | Replace environment (map) |
| `:extra-env` | Add to environment (map) |
| `:inherit` | If true, inherit all streams |
| `:cmd` | Command vector (overrides args) |
| `:prev` | Previous process for piping |
### Hooks
| Option | Description |
|--------|-------------|
| `:pre-start-fn` | Called before start with process info |
| `:shutdown` | Called when child process ends |
| `:exit-fn` | Called on exit (JDK11+) |
## Common Patterns
### Capture Output
```clojure
;; 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
```clojure
(shell {:dir "/tmp"} "ls")
```
### Environment Variables
```clojure
;; Add to environment
(shell {:extra-env {"DEBUG" "1"}} "./script.sh")
;; Replace environment
(shell {:env {"PATH" "/usr/bin"}} "ls")
```
### Piping Processes
```clojure
;; 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
```clojure
;; 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:
```clojure
;; 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
```clojure
(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
```clojure
(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
```clojure
;; Overwrite
(shell {:out :write :out-file "log.txt"} "ls")
;; Append
(shell {:out :append :out-file "log.txt"} "date")
```
### Discard Output
```clojure
(require '[babashka.process :refer [shell null-file]])
(shell {:out null-file :err null-file} "noisy-command")
```
### Timeout
```clojure
(let [p (process "sleep" "100")]
(when-not (deref p 1000 nil)
(destroy-tree p)
(println "Timed out")))
```
### Pre-start Hook
```clojure
(shell {:pre-start-fn (fn [{:keys [cmd]}]
(println "Running:" cmd))}
"ls")
```
## Error Handling
### Check Exit Code
```clojure
;; 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
```clojure
;; 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
```clojure
عرض على GitHub