Create babashka scripts to automate command-line tasks, file operations, and HTTP requests
When using p/process and p/shell a variable list of strings is expected at the end. When creating the command using a vector or similar, be sure to use apply so that the vector is unwrapped
(apply p/process {} ["echo" "123"])When using fs/glob to find files for a pattern, do it like this:
(def example-pattern "some-dir/**/*.clj")
(->> (fs/glob "." example-pattern)
(map fs/file))
pattern is a regular string
Some useful flags for file processing scripts
--dry-run only print actions, don’t execute--verbose log additional inputWhen creating namespaces and functions using the babashka.cli APIs, it is useful to alias them into your bb.edn file so that they can used as a shorter command
e.g. {:tasks {prune some.ns/prune}}
Turn Clojure functions into CLIs!
Add to your deps.edn or bb.edn :deps entry:
org.babashka/cli {:mvn/version "<latest-version>"}
babashka.cli provides a simple way to parse command line arguments in Clojure and babashka CLIs:
$ cli command :opt1 v1 :opt2 v2
# or
$ cli command --long-opt1 v1 -o v2
Key features:
Here's a simple example:
#!/usr/bin/env bb
(require '[babashka.cli :as cli]
'[babashka.fs :as fs])
(def cli-spec
{:spec
{:num {:coerce :long
:desc "Number of items"
:alias :n
:validate pos?
:require true}
:dir {:desc "Directory name"
:alias :d
:validate fs/directory?}
:flag {:coerce :boolean
:desc "Flag option"}}})
(defn -main [args]
(let [opts (cli/parse-opts args cli-spec)]
(if (:help opts)
(println (cli/format-opts cli-spec))
(println "CLI args:" opts))))
(-main *command-line-args*)
Parse options using parse-opts or parse-args:
(cli/parse-opts ["--port" "1339"] {:coerce {:port :long}})
;;=> {:port 1339}
;; With alias
(cli/parse-opts ["-p" "1339"] {:alias {:p :port} :coerce {:port :long}})
;;=> {:port 1339}
;; Collection values
(cli/parse-opts ["--paths" "src" "test"] {:coerce {:paths []}})
;;=> {:paths ["src" "test"]}
Handle subcommands using dispatch:
(def table
[{:cmds ["copy"] :fn copy :args->opts [:file]}
{:cmds ["delete"] :fn delete :args->opts [:file]}
{:cmds [] :fn help}])
(defn -main [& args]
(cli/dispatch table args {:coerce {:depth :long}}))
For better CLI documentation, use the spec format:
(def spec
{:port {:ref "<port>"
:desc "Port to listen on"
:coerce :long
:default 8080
:alias :p}})
Print help with:
(println (cli/format-opts {:spec spec}))
(require [babashka.fs :as fs])
absolute? - Returns true if path is absoluteabsolutize - Converts path to absolute path canonicalize - Returns canonical pathcomponents - Returns seq of path components split by file separatorcopy - Copies file to destination. Options: :replace-existing, :copy-attributes, :nofollow-linkscopy-tree - Copies entire file tree recursivelycreate-dir - Creates single directorycreate-dirs - Creates directory and parents (like mkdir -p)create-file - Creates empty file with optional permissionscreate-link - Creates hard linkcreate-sym-link - Creates symbolic linkdelete - Deletes file/directorydelete-if-exists - Deletes if exists, returns booleandelete-tree - Recursively deletes directory tree (like rm -rf)move - Moves/renames file or directorydirectory? - Checks if path is directoryexists? - Checks if path existsexecutable? - Checks if file is executablehidden? - Checks if file is hiddenreadable? - Checks if file is readableregular-file? - Checks if is regular filerelative? - Checks if path is relativesize - Gets file size in bytessym-link? - Checks if is symbolic linkwritable? - Checks if file is writableexpand-home - Expands ~ to user home directoryfile - Converts to java.io.Filefile-name - Gets name from pathnormalize - Normalizes pathparent - Gets parent directorypath - Converts to java.nio.file.Pathrelativize - Gets relative path between pathsunixify - Converts path separators to Unix styleread-all-bytes - Reads file as byte arrayread-all-lines - Reads file as lineswrite-bytes - Writes bytes to filewrite-lines - Writes lines to fileupdate-file - Updates text file contentscreation-time - Gets file creation timelast-modified-time - Gets last modified timeset-creation-time - Sets creation timeset-last-modified-time - Sets last modified timeglob - Finds files matching glob patternlist-dir - Lists directory contentslist-dirs - Lists contents of multiple directoriesmatch - Finds files matching patternmodified-since - Finds files modified after referencewalk-file-tree - Traverses directory tree with visitor functionsgunzip - Extracts gzip filegzip - Compresses file with gzipunzip - Extracts zip archivezip - Creates zip archivecreate-temp-dir - Creates temporary directorycreate-temp-file - Creates temporary filedelete-on-exit - Marks for deletion on JVM exittemp-dir - Gets system temp directorywith-temp-dir - Executes with temporary directoryget-attribute - Gets file attributeowner - Gets file ownerposix-file-permissions - Gets POSIX permissionsread-attributes - Reads file attributesset-attribute - Sets file attributeset-posix-file-permissions - Sets POSIX permissionsstr->posix - Converts string to POSIX permissionsposix->str - Converts POSIX permissions to stringexec-paths - Gets executable search pathswhich - Finds executable in PATHwhich-all - Finds all matching executableswindows? - Checks if running on Windowsxdg-cache-home - Gets XDG cache directoryxdg-config-home - Gets XDG config directoryxdg-data-home - Gets XDG data directoryxdg-state-home - Gets XDG state directoryfile-separator - System file separatorpath-separator - System path separatorEach function preserves its original docstring and source link from the original API documentation.
request [opts] - Core function for making HTTP requests:uri:headers, :method, :body, :query-params, :form-params:basic-auth, :oauth-token:async, :timeout, :throw:client, :interceptors, :version(get uri [opts]) ; GET request
(post uri [opts]) ; POST request
(put uri [opts]) ; PUT request
(delete uri [opts]) ; DELETE request
(patch uri [opts]) ; PATCH request
(head uri [opts]) ; HEAD request
client [opts] - Create custom HTTP client
:follow-redirects - :never, :always, :normal:connect-timeout - Connection timeout in ms:executor, :ssl-context, :ssl-parameters:proxy, :authenticator, :cookie-handler:version - :http1.1 or :http2default-client-opts - Default options used by implicit client
(->Authenticator {:user "user" :pass "pass"})
(->CookieHandler {:store cookie-store :policy :accept-all})
(->Executor {:threads 4})
(->ProxySelector {:host "proxy.com" :port 8080})
(->SSLContext {:key-store "cert.p12" :key-store-pass "pass"})
(->SSLParameters {:ciphers ["TLS_AES_128_GCM_SHA256"]})
Interceptors modify requests/responses in the processing chain.
accept-header - Add Accept headerbasic-auth - Add Basic Auth headeroauth-token - Add Bearer token headerquery-params - Encode and append query paramsform-params - Encode form parametersconstruct-uri - Build URI from map componentsmultipart - Handle multipart requestsdecode-body - Decode response as :string/:stream/:bytesdecompress-body - Handle gzip/deflate encodingthrow-on-exceptional-status-code - Error on bad statusdefault-interceptors - Default processing chain(websocket {
:uri "ws://example.com"
:headers {"Authorization" "Bearer token"}
:on-open (fn [ws] ...)
:on-message (fn [ws data last] ...)
:on-close (fn [ws status reason] ...)
})
(send! ws data [opts]) ; Send message
(ping! ws data) ; Send ping
(pong! ws data) ; Send pong
(close! ws [code reason]) ; Graceful close
(abort! ws) ; Immediate close
(get "https://api.example.com/data"
{:headers {"Accept" "application/json"}})
(post "https://api.example.com/create"
{:body "{\"name\":\"test\"}"
:headers {"Content-Type" "application/json"}})
(def custom-client
(client {:authenticator {:user "user" :pass "pass"}
:follow-redirects :always
:connect-timeout 5000}))
(request {:client custom-client
:uri "https://api.example.com/secure"})
(def ws (websocket
{:uri "wss://echo.example.com"
:on-message (fn [ws data last]
(println "Received:" data))
:on-error (fn [ws err]
(println "Error:" err))}))
(send! ws "Hello WebSocket!")
:interceptors option:async true with callbacksHere’s a useful snippet for using Simon Willison’s llm command line tool from Babashka:
(require '[babashka.process :as p])
(defn- build-command
"Builds the llm command with options"
[{:keys [model system template continue conversation-id
no-stream extract extract-last options attachments
attachment-types out]
:as _opts}]
(cond-> ["llm"]
model (conj "-m" model)
system (conj "-s" system)
template (conj "-t" template)
continue (conj "-c")
conversation-id (conj "--cid" conversation-id)
no-stream (conj "--no-stream")
extract (conj "-x")
extract-last (conj "--xl")
out (conj (str "--out=" out))
options (concat (mapcat (fn [[k v]] ["-o" (name k) (str v)]) options))
attachments (concat (mapcat (fn [a] ["-a" a]) attachments))
attachment-types (concat (mapcat (fn [[path type]] ["--at" path type]) attachment-types))))
(defn prompt
"Execute an LLM prompt. Returns the response as a string.
LLM Options:
:model - Model to use (e.g. \"gpt-4o\")
:system - System prompt
:template - Template name to use
:continue - Continue previous conversation
:conversation-id - Specific conversation ID to continue
:no-stream - Disable streaming output
:extract - Extract first code block
:extract-last - Extract last code block
:options - Map of model options (e.g. {:temperature 0.7})
:attachments - Vector of attachment paths/URLs
:attachment-types - Map of {path content-type} for attachments
:out - Output file path"
([prompt] (prompt {} prompt))
([opts prompt]
(-> (apply p/process
{:out :string
:in prompt}
(build-command opts))
deref
:out)))
;; Example usage:
#_(comment
;; Basic prompt
(prompt "Write a haiku about coding")
;; Prompt with options
(prompt {:model "gpt-4o"
:system "You are a helpful assistant"
:options {:temperature 0.7}}
"Explain monads simply"))
Updating front matter in markdown files is best done by using
(require '[clj-yaml.core :as yaml])
(defn extract-frontmatter
"Extracts YAML frontmatter from markdown content.
Returns [frontmatter remaining-content] or nil if no frontmatter found."
[content]
(when (str/starts-with? content "---\n")
(when-let [end-idx (str/index-of content "\n---\n" 4)]
(let [frontmatter (subs content 4 end-idx)
remaining (subs content (+ end-idx 5))]
[frontmatter remaining]))))
(defn update-frontmatter
"Updates the frontmatter by adding type: post if not present"
[markdown-str update-fn]
(let [[frontmatter content] (extract-frontmatter markdown-str)
data (yaml/parse-string frontmatter)
new-frontmatter (yaml/generate-string (update-fn data) :dumper-options {:flow-style :block})]
(str "---\n" new-frontmatter "---\n" content)))
Add this context to your project via the
ctxs command line integration: