├── src ├── bb.edn ├── args.clj ├── hello.clj └── journal ├── .gitignore ├── babooka.pdf ├── babooka.epub ├── project ├── bb.edn ├── src │ └── journal │ │ ├── list.clj │ │ ├── utils.clj │ │ └── add.clj └── journal ├── README.adoc └── README.org /src/bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["."]} 2 | -------------------------------------------------------------------------------- /src/args.clj: -------------------------------------------------------------------------------- 1 | *command-line-args* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | entries.edn 3 | -------------------------------------------------------------------------------- /babooka.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braveclojure/babooka/HEAD/babooka.pdf -------------------------------------------------------------------------------- /babooka.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braveclojure/babooka/HEAD/babooka.epub -------------------------------------------------------------------------------- /src/hello.clj: -------------------------------------------------------------------------------- 1 | (require '[clojure.string :as str]) 2 | (prn (str/join " " ["Hello" "inner" "world!"])) 3 | -------------------------------------------------------------------------------- /project/bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :tasks {:requires ([babashka.fs :as fs] 3 | [journal.list :as list]) 4 | clear (fs/delete-if-exists "entries.edn") 5 | list (list/list-entries nil) 6 | add (exec 'journal.add/add-entry)}} 7 | -------------------------------------------------------------------------------- /project/src/journal/list.clj: -------------------------------------------------------------------------------- 1 | (ns journal.list 2 | (:require 3 | [journal.utils :as utils])) 4 | 5 | (defn list-entries 6 | [_] 7 | (let [entries (utils/read-entries)] 8 | (doseq [{:keys [timestamp entry]} (reverse entries)] 9 | (println timestamp) 10 | (println entry "\n")))) 11 | -------------------------------------------------------------------------------- /project/src/journal/utils.clj: -------------------------------------------------------------------------------- 1 | (ns journal.utils 2 | (:require 3 | [babashka.fs :as fs] 4 | [clojure.edn :as edn])) 5 | 6 | (def ENTRIES-LOCATION "entries.edn") 7 | 8 | (defn read-entries 9 | [] 10 | (if (fs/exists? ENTRIES-LOCATION) 11 | (edn/read-string (slurp ENTRIES-LOCATION)) 12 | [])) 13 | -------------------------------------------------------------------------------- /project/journal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (require '[babashka.cli :as cli]) 4 | (require '[journal.add :as add]) 5 | (require '[journal.list :as list]) 6 | 7 | (def cli-opts 8 | {:entry {:alias :e 9 | :desc "Your dreams." 10 | :require true} 11 | :timestamp {:alias :t 12 | :desc "A unix timestamp, when you recorded this." 13 | :coerce {:timestamp :long}}}) 14 | 15 | (def table 16 | [{:cmds ["add"] :fn #(add/add-entry (:opts %)) :spec cli-opts} 17 | {:cmds ["list"] :fn #(list/list-entries nil)}]) 18 | 19 | (cli/dispatch table *command-line-args*) 20 | -------------------------------------------------------------------------------- /project/src/journal/add.clj: -------------------------------------------------------------------------------- 1 | (ns journal.add 2 | (:require 3 | [journal.utils :as utils])) 4 | 5 | (defn add-entry 6 | [opts] 7 | (let [entries (utils/read-entries)] 8 | (spit utils/ENTRIES-LOCATION 9 | (conj entries 10 | (merge {:timestamp (System/currentTimeMillis)} ;; default timestamp 11 | opts))))) 12 | 13 | 14 | 15 | (ns journal.add 16 | (:require 17 | [journal.utils :as utils])) 18 | 19 | (defn -main 20 | [entry-text] 21 | (let [entries (utils/read-entries)] 22 | (spit utils/ENTRIES-LOCATION 23 | (conj entries 24 | {:timestamp (System/currentTimeMillis) 25 | :entry entry-text})))) 26 | -------------------------------------------------------------------------------- /src/journal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (require '[babashka.cli :as cli]) 4 | (require '[babashka.fs :as fs]) 5 | (require '[clojure.edn :as edn]) 6 | 7 | (def ENTRIES-LOCATION "entries.edn") 8 | 9 | (defn read-entries 10 | [] 11 | (if (fs/exists? ENTRIES-LOCATION) 12 | (edn/read-string (slurp ENTRIES-LOCATION)) 13 | [])) 14 | 15 | (defn add-entry 16 | [{:keys [opts]}] 17 | (let [entries (read-entries)] 18 | (spit ENTRIES-LOCATION 19 | (conj entries 20 | (merge {:timestamp (System/currentTimeMillis)} ;; default timestamp 21 | opts))))) 22 | 23 | (def cli-opts 24 | {:entry {:alias :e 25 | :desc "Your dreams." 26 | :require true} 27 | :timestamp {:alias :t 28 | :desc "A unix timestamp, when you recorded this." 29 | :coerce {:timestamp :long}}}) 30 | 31 | (defn help 32 | [_] 33 | (println 34 | (str "add\n" 35 | (cli/format-opts {:spec cli-opts})))) 36 | 37 | (def table 38 | [{:cmds ["add"] :fn add-entry :spec cli-opts} 39 | {:cmds [] :fn help}]) 40 | 41 | (cli/dispatch table *command-line-args*) 42 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Babashka Babooka: Write Command-Line Clojure = 2 | Daniel Higginbotham 3 | 4 | 5 | 6 | = Introduction = 7 | * https://raw.githubusercontent.com/braveclojure/babooka/main/babooka.pdf[Download the free PDF] 8 | * https://raw.githubusercontent.com/braveclojure/babooka/main/babooka.epub[Download the free epub] 9 | 10 | There are two types of programmers in the world: the practical, sensible, 11 | shell-resigned people who need to google the correct argument order for `ln -s`; 12 | and those twisted, Stockholmed souls who will gleefully run their company's 13 | entire infrastructure on 57 commands stitched together into a single-line 14 | bash script. 15 | 16 | This guide is for the former. For the latter: sorry, but I can't help you. 17 | 18 | https://babashka.org[Babashka] is a Clojure scripting runtime that is a powerful, delightful 19 | alternative to the shell scripts you're used to. This comprehensive tutorial 20 | will teach you: 21 | 22 | * What babashka is, what it does, how it works, and how it can fit into your 23 | workflow 24 | * How to write babashka scripts 25 | * How to organize your babashka projects 26 | * What pods are, and how they provide a native Clojure interface for external 27 | programs 28 | * How to use tasks to create interfaces similar to `make` or `npm` 29 | 30 | If you'd like to stop doing something that hurts (writing incomprehensible shell 31 | scripts) and start doing something that feels great (writing Babashka scripts), 32 | then read on! 33 | 34 | NOTE: If you're unfamiliar with Clojure, Babashka is actually a great tool for 35 | learning! https://www.braveclojure.com/do-things/[This crash course] and https://www.braveclojure.com/organization/[this chapter on namespaces] cover what you need 36 | to understand the Clojure used here. There are many good editor extensions for 37 | working with Clojure code, including https://calva.io/getting-started/[Calva for VS Code] and https://docs.cider.mx/cider/index.html[CIDER for emacs]. If 38 | you're new to the command line, check out https://www.learnenough.com/command-line-tutorial[Learn Enough Command Line to be 39 | Dangerous]. 40 | 41 | 42 | == Sponsor == 43 | If you enjoy this tutorial, https://github.com/sponsors/flyingmachine[consider sponsoring me, Daniel Higginbotham, through 44 | GitHub sponsors]. As of April 2022 I am spending two days a week working on 45 | free Clojure educational materials and open source libraries to make Clojure 46 | more beginner-friendly, and appreciate any support! 47 | 48 | Please also https://github.com/sponsors/borkdude[consider sponsoring Michiel Borkent, aka borkdude, who created 49 | babashka]. Michiel is doing truly incredible work to transform the Clojure 50 | landscape, extending its usefulness and reach in ways that benefit us all. He 51 | has a proven track record of delivering useful tools and engaging with the 52 | commuity. 53 | 54 | 55 | == What is Babashka? == 56 | From a user perspective, babashka is a scripting runtime for the Clojure 57 | programming language. It lets you execute Clojure programs in contexts where 58 | you'd typically use bash, ruby, python, and the like. Use cases include build 59 | scripts, command line utilities, small web applications, git hooks, AWS Lambda 60 | functions, and everywhere you want to use Clojure where fast startup and/or low 61 | resource usage matters. 62 | 63 | You can run something like the following in a terminal to immediately execute 64 | your Clojure program: 65 | 66 | [source,bash] 67 | ---- 68 | bb my-clojure-program.clj 69 | ---- 70 | 71 | If you're familiar with Clojure, you'll find this significant because it 72 | eliminates the startup time you'd otherwise have to contend with for a 73 | JVM-compiled Clojure program, not to mention you don't have to compile the file. 74 | It also uses much less memory than running a jar. Babashka makes it feasible to 75 | use Clojure even more than you already do. 76 | 77 | If you're unfamiliar with Clojure, using Babashka is a great way to try out the 78 | language. Clojure is a _hosted_ language, meaning that the language is defined 79 | independently of the underlying runtime environment. Most Clojure programs are 80 | compiled to run on the Java Virtual Machine (JVM) so that they can be run 81 | anywhere Java runs. The other main target is JavaScript, allowing Clojure to run 82 | in a browser. With Babashka, you can now run Clojure programs where you'd 83 | normally run bash scripts. The time you spend investing in Clojure pays 84 | dividends as your knowledge transfers to these varied environments. 85 | 86 | From an implementation perspective, Babashka is a standalone, natively-compiled 87 | binary, meaning that the operating system executes it directly, rather than 88 | running in a JVM. When the babashka binary gets compiled, it includes many 89 | Clojure namespaces and libraries so that they are usable with native 90 | performance. You can https://book.babashka.org/#libraries[check out the full list of built-in namespaces]. Babashka 91 | can also include other libraries, just like if you're using deps.edn or 92 | Leiningen. 93 | 94 | The binary also includes the https://github.com/babashka/SCI[Small Clojure Interpreter (SCI)] to interpret the 95 | Clojure you write and additional libraries you include on the fly. Its 96 | implementation of Clojure is nearly at parity with JVM Clojure, and it improves 97 | daily thanks to https://github.com/borkdude[Michiel Borkent]'s ceaseless work. It's built with GraalVM. This 98 | guide is focused on becoming productive with Babashka and doesn't cover the 99 | implementation in depth, but you can learn more about it by reading https://medium.com/graalvm/babashka-how-graalvm-helped-create-a-fast-starting-scripting-environment-for-clojure-b0fcc38b0746[this article 100 | on the GraalVM blog]. 101 | 102 | 103 | == Why should you use it? == 104 | I won't go into the benefits of Clojure itself because there are plenty of 105 | materials on that https://jobs-blog.braveclojure.com/2022/03/24/long-term-clojure-benefits.html[elsewhere]. 106 | 107 | Beyond the fact that it's Clojure, Babashka brings a few features that make it 108 | stand apart from contenders: 109 | 110 | *First-class support for multi-threaded programming.* Clojure makes 111 | multi-threaded programming simple and easy to write and reason about. With 112 | Babashka, you can write straightforward scripts that e.g. fetch and process data 113 | from multiple databases in parallel. 114 | 115 | *Real testing.* You can unit test your Babashka code just as you would any other 116 | Clojure project. How do you even test bash? 117 | 118 | *Real project organization.* Clojure namespaces are a sane way to organize your 119 | project's functions and build reusable libraries. 120 | 121 | *Cross-platform compatibility.* It's nice not having to worry that an OS 122 | X-developed script is broken in your continuous integration pipeline. 123 | 124 | *Interactive Development.* Following the lisp tradition, Babashka provides a 125 | read-eval-print loop (REPL) that gives you that good good bottom-up 126 | fast-feedback feeling. Script development is inherently a fast; Babashka makes 127 | it a faster. 128 | 129 | *Built-in tools for defining your script's interface.* One reason to write a 130 | shell script is to provide a concise, understandable interface for a complicated 131 | process. For example, you might write a build script that includes `build` and 132 | `deploy` commands that you call like 133 | 134 | [source,bash] 135 | ---- 136 | ./my-script build 137 | ./my-script deploy 138 | ---- 139 | 140 | Babashka comes with tools that gives you a consistent way of defining such 141 | commands, and for parsing command-line arguments into Clojure data structures. 142 | Take that, bash! 143 | 144 | *A rich set of libraries.* Babashka comes with helper utilities for doing 145 | typical shell script grunt work like interacting with processes or mucking about 146 | with the filesystem. It also has support for the following without needing extra 147 | dependencies: 148 | 149 | * JSON parsing 150 | * YAML parsing 151 | * Starting an HTTP server 152 | * Writing generative tests 153 | 154 | And of course, you can add Clojure libraries as dependencies to accomplish even 155 | more. Clojure is a gateway drug to other programming paradigms, so if you ever 156 | wanted to do e.g. logic programming from the command line, now's your chance! 157 | 158 | *Good error messages.* Babashka's error handling is the friendliest of all 159 | Clojure implementations, directing you precisely to where an error occurred. 160 | 161 | 162 | == Installation == 163 | Installing with brew is `brew install borkdude/brew/babashka`. 164 | 165 | https://github.com/babashka/babashka#installation[For other systems, see Babashka's complete installation instructions.] 166 | 167 | 168 | = Your first script = 169 | Throughout this tutorial we're going to play with building a little CLI-based 170 | dream journal. Why? Because the idea of you nerds recording your weird little 171 | subconscious hallucinations is deeply amusing to me. 172 | 173 | In this section, you're going to learn: 174 | 175 | * How to write and run your first Babashka script 176 | * How default output is handled 177 | * A little about how Babashka treats namespaces 178 | 179 | Create a file named `hello.clj` and put this in it: 180 | 181 | [source,clojure] 182 | ---- 183 | (require '[clojure.string :as str]) 184 | (prn (str/join " " ["Hello" "inner" "world!"])) 185 | ---- 186 | 187 | Now run it with `bb`, the babashka executable: 188 | 189 | [source,clojure] 190 | ---- 191 | bb hello.clj 192 | ---- 193 | 194 | You should see it print the text `"Hello inner world!"`. 195 | 196 | There are a few things here to point out for experienced Clojurians: 197 | 198 | * You didn't need a deps.edn file or project.clj 199 | * There's no namespace declaration; we use `(require ...)` 200 | * It's just Clojure 201 | 202 | I very much recommend that you actually try this example before proceeding 203 | because it _feels_ different from what you're used to. It's unlikely that you're 204 | used to throwing a few Clojure expressions into a file and being able to run 205 | them immediately. 206 | 207 | When I first started using Babashka, it felt so different that it was 208 | disorienting. It was like the first time I tried driving an electric car and my 209 | body freaked out a little because I wasn't getting the typical sensory cues like 210 | hearing and feeling the engine starting. 211 | 212 | Babashka's like that: the experience is so quiet and smooth it's jarring. No 213 | deps.edn, no namespace declaration, write only the code you need and it runs! 214 | 215 | That's why I included the "It's just Clojure" bullet point. It might feel 216 | different, but this is still Clojure. Let's explore the other points in more 217 | detail. 218 | 219 | 220 | == Babashka's output == 221 | Here's what's going on: `bb` interprets the Clojure code you've written, 222 | executing it on the fly. `prn` prints to `stdout`, which is why `"Hello, 223 | inner world!"` is returned in your terminal. 224 | 225 | NOTE: When you print text to `stdout`, it gets printed to your terminal. This 226 | tutorial doesn't get into what `stdout` actually is, but you can think of it as 227 | the channel between the internal world of your program and the external world of 228 | the environment calling your program. When your program sends stuff to `stdout`, 229 | your terminal receives it and prints it. 230 | 231 | Notice that the quotes are maintained when the value is printed. `bb` will 232 | print the _stringified representation of your data structure_. If you updated 233 | `hello.clj` to read 234 | 235 | [source,clojure] 236 | ---- 237 | "Hello, inner world!" 238 | (prn ["It's" "me," "your" "wacky" "subconscious!"]) 239 | ---- 240 | 241 | Then `["It's" "me," "your" "wacky" "subconscious!"]` would get printed, and 242 | `"Hello, inner world!"` would not. You must use a printing function on a form 243 | for it to be sent to `stdout` 244 | 245 | If you want to print a string without the surrounding quotes, you can use 246 | 247 | [source,clojure] 248 | ---- 249 | (println "Hello, inner world!") 250 | ---- 251 | 252 | 253 | == Namespace is optional == 254 | As for the lack of namespace: this is part of what makes Babashka useful as a 255 | scripting tool. When you're in a scripting state of mind, you want to start 256 | hacking on ideas immediately; you don't want to have to deal with boilerplate 257 | just to get started. Babashka has your babacka. 258 | 259 | You _can_ define a namespace (we'll look at that more when we get into project 260 | organization), but if you don't then Babashka uses the `user` namespace by 261 | default. Try updating your file to read: 262 | 263 | [source,clojure] 264 | ---- 265 | (str "Hello from " *ns* ", inner world!") 266 | ---- 267 | 268 | Running it will print `"Hello from user, inner world!"`. This might be 269 | surprising because there's a mismatch between filename (`hello.clj`) and 270 | namespace name. In other Clojure implementations, the current namespace strictly 271 | corresponds to the source file's filename, but Babashka relaxes that a little 272 | bit in this specific context. It provides a scripting experience that's more in 273 | line with what you'd expect from using other scripting languages. 274 | 275 | 276 | == What about requiring other namespaces? == 277 | You might want to include a namespace declaration because you want to require 278 | some namespaces. With JVM Clojure and Clojurescript, you typically require 279 | namespaces like this: 280 | 281 | [source,clojure] 282 | ---- 283 | (ns user 284 | (:require 285 | [clojure.string :as str])) 286 | ---- 287 | 288 | It's considered bad form to require namespaces by putting `(require 289 | '[clojure.string :as str])` in your source code. 290 | 291 | That's not the case with Babashka. You'll see `(require ...)` used liberally in 292 | other examples, and it's OK for you to do that too. 293 | 294 | 295 | == Make your script executable == 296 | What if you want to execute your script by typing something like `./hello` 297 | instead of `bb hello.clj`? You just need to rename your file, add a shebang, and 298 | `chmod +x` that bad boy. Update `hello.clj` to read: 299 | 300 | [source,clojure] 301 | ---- 302 | #!/usr/bin/env bb 303 | 304 | (str "Hello from " *ns* ", inner world!") 305 | ---- 306 | 307 | NOTE: The first line, `#!/usr/bin/env bb` is the "shebang", and I'm not going to 308 | explain it. 309 | 310 | Then run this in your terminal: 311 | 312 | [source,bash] 313 | ---- 314 | mv hello{.clj,} 315 | chmod +x hello 316 | ./hello 317 | ---- 318 | 319 | First you rename the file, then you call `chmod +x` on it to make it executable. 320 | Then you actually execute it, saying hi to your own inner world which is kind of 321 | adorable. 322 | 323 | 324 | == Summary == 325 | Here's what you learned in this section: 326 | 327 | * You can run scripts with `bb script-name.clj` 328 | * You can make scripts directly executable by adding `#!/usr/bin/env bb` on the 329 | top line and adding the `execute` permission with `chmod +x script-name.clj` 330 | * You don't have to include an `(ns ...)` declaration in your script. But it 331 | still runs and it's still Clojure! 332 | * It's acceptable and even encouraged to require namespaces with `(require 333 | ...)`. 334 | * Babashka writes the last value it encounters to `stdout`, except if that value 335 | is `nil` 336 | 337 | 338 | = Working with files = 339 | Shell scripts often need to read input from the command line and produce output 340 | somewhere, and our dream journal utility is no exception. It's going to store 341 | entries in the file `entries.edn`. The journal will be a vector, and each entry 342 | will be a map with the keys `:timestamp` and `:entry` (the entry has linebreaks 343 | for readability): 344 | 345 | [source,clojure] 346 | ---- 347 | [{:timestamp 0 348 | :entry "Dreamt the drain was clogged again, except when I went to unclog 349 | it it kept growing and getting more clogged and eventually it 350 | swallowed up my little unclogger thing"} 351 | {:timestamp 1 352 | :entry "Bought a house in my dream, was giving a tour of the backyard and 353 | all the... topiary? came alive and I had to fight it with a sword. 354 | I understood that this happens every night was very annoyed that 355 | this was not disclosed in the listing."}] 356 | ---- 357 | 358 | To write to the journal, we want to run the command `./journal add --entry 359 | "Hamsters. Hamsters everywhere. Again."`. The result should be that a map gets 360 | appended to the vector. 361 | 362 | Let's get ourselves part of the way there. Create the file `journal` and make it 363 | executable with `chmod +x journal`, then make it look like this: 364 | 365 | [source,clojure] 366 | ---- 367 | #!/usr/bin/env bb 368 | 369 | (require '[babashka.fs :as fs]) 370 | (require '[clojure.edn :as edn]) 371 | 372 | (def ENTRIES-LOCATION "entries.edn") 373 | 374 | (defn read-entries 375 | [] 376 | (if (fs/exists? ENTRIES-LOCATION) 377 | (edn/read-string (slurp ENTRIES-LOCATION)) 378 | [])) 379 | 380 | (defn add-entry 381 | [text] 382 | (let [entries (read-entries)] 383 | (spit ENTRIES-LOCATION 384 | (conj entries {:timestamp (System/currentTimeMillis) 385 | :entry text})))) 386 | 387 | (add-entry (first *command-line-args*)) 388 | ---- 389 | 390 | We require a couple namespaces: `babashka.fs` and `clojure.edn`. `babashka.fs` is 391 | a collection of functions for working with the filesystem; check out its https://github.com/babashka/fs[API 392 | docs]. When you're writing shell scripts, you're very likely to work with the 393 | filesystem, so this namespace is going to be your friend. 394 | 395 | Here, we're using the `fs/exists?` function to check that `entries.edn` exists 396 | before attempting to read it because `slurp` will throw an exception if it can't 397 | find the file for the path you passed it. 398 | 399 | The `add-entry` function uses `read-entries` to get a vector of entries, uses 400 | `conj` to add an entry, and then uses `spit` to write back to `entries.edn`. By 401 | default, `spit` will overwrite a file; if you want to append to it, you would 402 | call it like 403 | 404 | [source,clojure] 405 | ---- 406 | (spit "entries.edn" {:timestap 0 :entry ""} :append true) 407 | ---- 408 | 409 | Maybe overwriting the whole file is a little dirty, but that's the scripting 410 | life babyyyyy! 411 | 412 | 413 | = Creating an interface for your script = 414 | OK so in the last line we call `(add-entry (first \*command-line-args*))`. 415 | `\*command-line-args*` is a sequence containing, well, all the command line 416 | arguments that were passed to the script. If you were to create the file 417 | `args.clj` with the contents `\*command-line-args*`, then ran `bb args.clj 1 2 418 | 3`, it would print `("1" "2" "3")`. 419 | 420 | Our `journal` file is at the point where we can add an entry by calling 421 | `./journal "Flying\!\! But to Home Depot??"`. This is almost what we want; we 422 | actually want to call `./journal add --entry "Flying\!\! But to Home Depot??"`. 423 | The assumption here is that we'll want to have other commands like `./journal 424 | list` or `./journal delete`. (You have to escape the exclamation marks otherwise 425 | bash interprets them as history commands.) 426 | 427 | To accomplish this, we'll need to handle the command line arguments in a more 428 | sophisticated way. The most obvious and least-effort way to do this would be to 429 | dispatch on the first argument to `\*command-line-args*`, something like this: 430 | 431 | [source,clojure] 432 | ---- 433 | (let [[command _ entry] *command-line-args*] 434 | (case command 435 | "add" (add-entry entry))) 436 | ---- 437 | 438 | This might be totally fine for your use case, but sometimes you want something 439 | more robust. You might want your script to: 440 | 441 | * List valid commands 442 | * Give an intelligent error message when a user calls a command that doesn't 443 | exist (e.g. if the user calls `./journal add-dream` instead of `./journal 444 | add`) 445 | * Parse arguments, recognizing option flags and converting values to keywords, 446 | numbers, vectors, maps, etc 447 | 448 | Generally speaking, *you want a clear and consistent way to define an interface 449 | for your script*. This interface is responsible for taking the data provided at 450 | the command line -- arguments passed to the script, as well as data piped in 451 | through `stdin` -- and using that data to handle these three responsibilities: 452 | 453 | * Dispatching to a Clojure function 454 | * Parsing command-line arguments into Clojure data, and passing that to the 455 | dispatched function 456 | * Providing feedback in cases where there's a problem performing the above 457 | responsibilities. 458 | 459 | The broader Clojure ecosystem provides at least two libraries for handling 460 | argument parsing: 461 | 462 | * https://github.com/clojure/tools.cli[clojure.tools.cli] 463 | * https://github.com/nubank/docopt.clj[nubank/docopt.clj] 464 | 465 | Babashka provides the https://github.com/babashka/cli[babashka.cli library] for both parsing options and 466 | dispatches subcommands. We're going to focus just on babashka.cli. 467 | 468 | 469 | == parsing options with babashka.cli == 470 | The https://github.com/babashka/cli[babashka.cli docs] do a good job of explaining how to use the library to meet 471 | all your command line parsing needs. Rather than going over every option, I'll 472 | just focus on what we need to build our dream journal. To parse options, we 473 | require the `babashka.cli` namespace and we define a _CLI spec_: 474 | 475 | [source,clojure] 476 | ---- 477 | (require '[babashka.cli :as cli]) 478 | (def cli-opts 479 | {:entry {:alias :e 480 | :desc "Your dreams." 481 | :require true} 482 | :timestamp {:alias :t 483 | :desc "A unix timestamp, when you recorded this." 484 | :coerce {:timestamp :long}}}) 485 | ---- 486 | 487 | A CLI spec is a map where each key is a keyword, and each value is an _option 488 | spec_. This key is the _long name_ of your option; `:entry` corresponds to the 489 | flag `--entry` on the command line. 490 | 491 | The option spec is a map you can use to further config the option. `:alias` lets 492 | you specify a _short name_ for you options, so that you can use e.g. `-e` 493 | instead of `--entry` at the command line. `:desc` is used to create a summary 494 | for your interface, and `:require` is used to enforce the presence of an option. 495 | `:coerce` is used to transform the option's value into some other data type. 496 | 497 | We can experiment with this CLI spec in a REPL. There are many options for 498 | starting a Babashka REPL, and the most straightforward is simply typing `bb 499 | repl` at the command line. If you want to use CIDER, first add the file `bb.edn` 500 | and put an empty map, `{}`, in it. Then you can use `cider-jack-in`. After that, 501 | you can paste in the code from the snippet above, then paste in this snippet: 502 | 503 | [source,clojure] 504 | ---- 505 | (cli/parse-opts ["-e" "The more I mowed, the higher the grass got :("] {:spec cli-opts}) 506 | ;; => 507 | {:entry "The more I mowed, the higher the grass got :("} 508 | ---- 509 | 510 | Note that `cli/parse-opts` returns a map with the parsed options, which will 511 | make it easy to use the options later. 512 | 513 | Leaving out a required flag throws an exception: 514 | 515 | [source,clojure] 516 | ---- 517 | (cli/parse-opts [] {:spec cli-opts}) 518 | ;; exception gets thrown, this gets printed: 519 | : Required option: :entry user 520 | ---- 521 | 522 | `cli/parse-opts` is a great tool for building an interface for simple scripts! 523 | You can communicate that interface to the outside world with `cli/format-opts`. 524 | This function will take an option spec and return a string that you can print to 525 | aid people in using your program. Behold: 526 | 527 | [source,clojure] 528 | ---- 529 | (println (cli/format-opts {:spec cli-opts})) 530 | ;; => 531 | -e, --entry Your dreams. 532 | -t, --timestamp A unix timestamp, when you recorded this. 533 | ---- 534 | 535 | 536 | == dispatching subcommands with babashka.cli == 537 | babashka.cli goes beyond option parsing to also giving you a way to dispatch 538 | subcommands, which is exactly what we want to get `./journal add --entry "..."` 539 | working. Here's the final version of `journal`: 540 | 541 | [source,clojure] 542 | ---- 543 | #!/usr/bin/env bb 544 | 545 | (require '[babashka.cli :as cli]) 546 | (require '[babashka.fs :as fs]) 547 | (require '[clojure.edn :as edn]) 548 | 549 | (def ENTRIES-LOCATION "entries.edn") 550 | 551 | (defn read-entries 552 | [] 553 | (if (fs/exists? ENTRIES-LOCATION) 554 | (edn/read-string (slurp ENTRIES-LOCATION)) 555 | [])) 556 | 557 | (defn add-entry 558 | [{:keys [opts]}] 559 | (let [entries (read-entries)] 560 | (spit ENTRIES-LOCATION 561 | (conj entries 562 | (merge {:timestamp (System/currentTimeMillis)} ;; default timestamp 563 | opts))))) 564 | 565 | (def cli-opts 566 | {:entry {:alias :e 567 | :desc "Your dreams." 568 | :require true} 569 | :timestamp {:alias :t 570 | :desc "A unix timestamp, when you recorded this." 571 | :coerce {:timestamp :long}}}) 572 | 573 | (defn help 574 | [_] 575 | (println 576 | (str "add\n" 577 | (cli/format-opts {:spec cli-opts})))) 578 | 579 | (def table 580 | [{:cmds ["add"] :fn add-entry :spec cli-opts} 581 | {:cmds [] :fn help}]) 582 | 583 | (cli/dispatch table *command-line-args*) 584 | ---- 585 | 586 | Try it out with the following at your terminal: 587 | 588 | [source,bash] 589 | ---- 590 | ./journal 591 | ./journal add -e "dreamt they did one more episode of Firefly, and I was in it" 592 | ---- 593 | 594 | The function `cli/dispatch` at the bottom takes a dispatch table as its first 595 | argument. `cli/dispatch` figures out which of the arguments you passed in at the 596 | command line correspond to commands, and then calls the corresponding `:fn`. If 597 | you type `./journal add ...`, it will dispatch the `add-entry` function. If you 598 | just type `./journal` with no arguments, then the `help` function gets 599 | dispatched. 600 | 601 | The dispatched function receives a map as its argument, and that map contains 602 | the `:opts` key. This is a map of parsed command line options, and we use it to 603 | build our dream journal entry in the `add-entry` function. 604 | 605 | And that, my friends, is how you build an interface for your script! 606 | 607 | == Summary == 608 | * For scripts of any complexity, you generally need to _parse_ the command line 609 | options into Clojure data structures 610 | * The libraries `clojure.tools.cli` and `nubank/docopts` will parse command line 611 | arguments into options for you 612 | * I prefer using `babashka.cli` because it also handles subcommand dispatch, but 613 | really this decision is a matter of taste 614 | * `cli/parse-opts` takes an _options spec_ and returns a map 615 | * `cli/format-opts` is useful for creating help text 616 | * Your script might provide _subcommands_, e.g. `add` in `journal add`, and you 617 | will need to map the command line arguments to the appropriate function in 618 | your script with `cli/dispatch` 619 | 620 | 621 | = Organizing your project = 622 | You can now record your subconscious's nightly improv routine. That's great! 623 | High on this accomplishment, you decide to kick things up a notch and add the 624 | ability to list your entries. You want to run `./journal list` and have your 625 | script return something like this: 626 | 627 | [source,] 628 | ---- 629 | 2022-12-07 08:03am 630 | There were two versions of me, and one version baked the other into a pie and ate it. 631 | Feeling both proud and disturbed. 632 | 633 | 2022-12-06 07:43am 634 | Was on a boat, but the boat was powered by cucumber sandwiches, and I had to keep 635 | making those sandwiches so I wouldn't get stranded at sea. 636 | ---- 637 | 638 | You read somewhere that source files should be AT MOST 25 lines long, so you 639 | decide that you want to split up your codebase and put this list functionality 640 | in its own file. How do you do that? 641 | 642 | You can organize your Babashka projects just like your other Clojure projects, 643 | splitting your codebase into separate files, with each file defining a namespace 644 | and with namespaces corresponding to file names. Let's reorganize our current 645 | codebase a bit, making sure everything still works, and then add a namespace for 646 | listing entries. 647 | 648 | 649 | == File system structure == 650 | One way to organize our dream journal project would be to create the following 651 | file structure: 652 | 653 | [source,] 654 | ---- 655 | ./journal 656 | ./src/journal/add.clj 657 | ./src/journal/utils.clj 658 | ---- 659 | 660 | Already, you can see that this looks both similar to typical Clojure project 661 | file structures, and a bit different. We're placing our namespaces in the 662 | `src/journal` directory, which lines up with what you'd see in JVM or 663 | ClojureScript projects. What's different in our Babashka project is that we're 664 | still using `./journal` to serve as the executable entry point for our program, 665 | rather than the convention of using `./src/journal/core.clj` or something like 666 | that. This might feel a little weird but it's valid and it's still Clojure. 667 | 668 | And like other Clojure environments, you need to tell Babashka to look in the 669 | `src` directory when you require namespaces. You do that by creating the file 670 | `bb.edn` in the same directory as `journal` and putting this in it: 671 | 672 | [source,clojure] 673 | ---- 674 | {:paths ["src"]} 675 | ---- 676 | 677 | `bb.edn` is similar to a `deps.edn` file in that one of its responsibilities is 678 | telling Babashka how to construct your classpath. The classpath is the set of 679 | the directories that Babashka should look in when you require namespaces, and by 680 | adding `"src"` to it you can use `(require '[journal.add])` in your project. 681 | Babashka will be able to find the corresponding file. 682 | 683 | Note that there is nothing special about the `"src"` directory. You could use 684 | `"my-code"` or even `"."` if you wanted, and you can add more than one path. 685 | `"src"` is just the convention preferred by discerning Clojurians the world 686 | over. 687 | 688 | With this in place, we'll now update `journal` so that it looks like this: 689 | 690 | [source,clojure] 691 | ---- 692 | #!/usr/bin/env bb 693 | 694 | (require '[babashka.cli :as cli]) 695 | (require '[journal.add :as add]) 696 | 697 | (def cli-opts 698 | {:entry {:alias :e 699 | :desc "Your dreams." 700 | :require true} 701 | :timestamp {:alias :t 702 | :desc "A unix timestamp, when you recorded this." 703 | :coerce {:timestamp :long}}}) 704 | 705 | (def table 706 | [{:cmds ["add"] :fn add/add-entry :spec cli-opts}]) 707 | 708 | (cli/dispatch table *command-line-args*) 709 | ---- 710 | 711 | Now the file is only responsible for parsing command line arguments and 712 | dispatching to the correct function. The add functionality has been moved to 713 | another namespace. 714 | 715 | 716 | == Namespaces == 717 | You can see on line 4 that we're requiring a new namespace, `journal.add`. The 718 | file corresponding to this namespace is `./src/journal/add.clj`. Here's what 719 | that looks like: 720 | 721 | [source,clojure] 722 | ---- 723 | (ns journal.add 724 | (:require 725 | [journal.utils :as utils])) 726 | 727 | (defn add-entry 728 | [opts] 729 | (let [entries (utils/read-entries)] 730 | (spit utils/ENTRIES-LOCATION 731 | (conj entries 732 | (merge {:timestamp (System/currentTimeMillis)} ;; default timestamp 733 | opts))))) 734 | ---- 735 | 736 | Look, it's a namespace declaration! And that namespace declaration has a 737 | `(:require ...)` form. We know that when you write Babashka scripts, you can 738 | forego declaring a namespace if all your code is in one file, like in the 739 | original version of `journal`. However, once you start splitting your code into 740 | multiple files, the normal rules of Clojure project organization apply: 741 | 742 | * Namespace names must correspond to filesystem paths. If you want to name a 743 | namespace `journal.add`, Babashka must be able to find it at 744 | `journal/add.clj`. 745 | * You must tell Babashka where to look to find the files that correspond to 746 | namespaces. You do this by creating a `bb.edn` file and putting `{:paths 747 | ["src"]}` in it. 748 | 749 | To finish our tour of our new project organization, here's 750 | `./src/journal/utils.clj`: 751 | 752 | [source,clojure] 753 | ---- 754 | (ns journal.utils 755 | (:require 756 | [babashka.fs :as fs] 757 | [clojure.edn :as edn])) 758 | 759 | (def ENTRIES-LOCATION "entries.edn") 760 | 761 | (defn read-entries 762 | [] 763 | (if (fs/exists? ENTRIES-LOCATION) 764 | (edn/read-string (slurp ENTRIES-LOCATION)) 765 | [])) 766 | ---- 767 | 768 | If you call `./journal add -e "visited by the tooth fairy, except he was a 769 | balding 45-year-old man with a potbelly from Brooklyn"`, it should still work. 770 | 771 | Now lets create a the `journal.list` namespace. Open the file 772 | `src/journal/list.clj` and put this in it: 773 | 774 | [source,clojure] 775 | ---- 776 | (ns journal.list 777 | (:require 778 | [journal.utils :as utils])) 779 | 780 | (defn list-entries 781 | [_] 782 | (let [entries (utils/read-entries)] 783 | (doseq [{:keys [timestamp entry]} (reverse entries)] 784 | (println timestamp) 785 | (println entry "\n")))) 786 | ---- 787 | 788 | This doesn't format the timestamp, but other than that it lists our entries in 789 | reverse-chronologial order, just like we want. Yay! 790 | 791 | To finish up, we need to add `journal.list/list-entries` to our dispatch table 792 | in the `journal` file. That file should now look like this: 793 | 794 | [source,clojure] 795 | ---- 796 | #!/usr/bin/env bb 797 | 798 | (require '[babashka.cli :as cli]) 799 | (require '[journal.add :as add]) 800 | (require '[journal.list :as list]) 801 | 802 | (def cli-opts 803 | {:entry {:alias :e 804 | :desc "Your dreams." 805 | :require true} 806 | :timestamp {:alias :t 807 | :desc "A unix timestamp, when you recorded this." 808 | :coerce {:timestamp :long}}}) 809 | 810 | (def table 811 | [{:cmds ["add"] :fn #(add/add-entry (:opts %)) :spec cli-opts} 812 | {:cmds ["list"] :fn #(list/list-entries %)}]) 813 | 814 | (cli/dispatch table *command-line-args*) 815 | ---- 816 | 817 | 818 | == Summary == 819 | * Namespaces work like they do in JVM Clojure and Clojurescript: namespace names 820 | must correspond to file system structure 821 | * Put the map `{:paths ["src"]}` in `bb.edn` to tell Babashka where to find the 822 | files for namespaces 823 | 824 | 825 | = Adding dependencies = 826 | You can add dependencies to your projects by adding a `:deps` key to your 827 | `bb.edn` file, resulting in something like this: 828 | 829 | [source,clojure] 830 | ---- 831 | {:paths ["src"] 832 | :deps {medley/medley {:mvn/version "1.3.0"}}} 833 | ---- 834 | 835 | What's cool about Babashka though is that you can also add deps directly in your 836 | script, or even in the repl, like so: 837 | 838 | [source,clojure] 839 | ---- 840 | (require '[babashka.deps :as deps]) 841 | (deps/add-deps '{:deps {medley/medley {:mvn/version "1.3.0"}}}) 842 | ---- 843 | 844 | This is in keeping with the nature of a scripting language, which should enable 845 | quick, low-ceremony development. 846 | 847 | At this point you should be fully equipped to start writing your own Clojure 848 | shell scripts with Babashka. Woohoo! 849 | 850 | In the sections that follow, I'll cover aspects of Babashka that you might not 851 | need immediately but that will be useful to you as your love of Clojure 852 | scripting grows until it becomes all-consuming. 853 | 854 | 855 | = Pods = 856 | Babashka _pods_ introduce a way to interact with external processes by calling 857 | Clojure functions, so that you can write code that looks and feels like Clojure 858 | (because it is) even when working with a process that's running outside your 859 | Clojure application, and even when that process is written in another language. 860 | 861 | 862 | == Pod usage == 863 | Let's look at what that means in more concrete terms. Suppose you want to 864 | encrypt your dream journal. You find out about https://github.com/rorokimdim/stash[stash], "a command line program 865 | for storing text data in encrypted form." This is exactly what you need! Except 866 | it's written in Haskell, and furthermore it has a _terminal user interface_ 867 | (TUI) rather than a command-line interface. 868 | 869 | That is, when you run `stash` from the command line it "draws" an ascii 870 | interface in your terminal, and you must provide additional input to store text. 871 | You can't store text directly from the command line with something like 872 | 873 | [source,bash] 874 | ---- 875 | stash store dreams.stash \ 876 | --key 20221210092035 \ 877 | --value "was worried that something was wrong with the house's foundation, 878 | then the whole thing fell into a sinkhole that kept growing until 879 | it swallowed the whole neighborhood" 880 | ---- 881 | 882 | 883 | If that were possible, then you could use `stash` from within your Bashka 884 | project by using the `babashka.process/shell` function, like this: 885 | 886 | [source,clojure] 887 | ---- 888 | (require '[babashka.process :as bp]) 889 | (bp/shell "stash store dreams.stash --key 20221210092035 --value \"...\"") 890 | ---- 891 | 892 | `bp/shell` is lets you take advantage of a program's command-line interface; but 893 | again, `stash` doesn't provide that. 894 | 895 | However, `stash` provides a _pod interface_, so we can use it like this in a 896 | Clojure file: 897 | 898 | [source,clojure] 899 | ---- 900 | (require '[babashka.pods :as pods]) 901 | (pods/load-pod 'rorokimdim/stash "0.3.1") 902 | (require '[pod.rorokimdim.stash :as stash]) 903 | 904 | (stash/init {"encryption-key" "foo" 905 | "stash-path" "foo.stash" 906 | "create-stash-if-missing" true}) 907 | 908 | (stash/set 20221210092035 "dream entry") 909 | ---- 910 | 911 | Let's start at the last line, `(stash/set 20221210092035 "dream entry")`. This 912 | is the point of pods: they expose an external process's commands as Clojure 913 | functions. They allow these processes to have a _Clojure interface_ so that you 914 | can interact with them by writing Clojure code, as opposed to having to shell 915 | out or make HTTP calls or something like that. 916 | 917 | In the next section I'll explain the rest of the snippet above. 918 | 919 | 920 | == Pod implementation == 921 | Where does the `stash/set` function come from? Both the namespace 922 | `pod.rorokimdim.stash` and the functions in it are dynamically generated by the 923 | call `(pods/load-pod 'rorokimdim/stash "0.3.1")`. 924 | 925 | For this to be possible, the external program has to be written to support the 926 | _pod protocol_. "Protocol" here does not refer to a Clojure protocol, it refers 927 | to a standard for exchanging information. Your Clojure application and the 928 | external application need to have some way to communicate with each other given 929 | that they don't live in the same process and they could even be written in 930 | different languages. 931 | 932 | By implementing the pod protocol, a program becomes a pod. In doing so, it gains 933 | the ability to tell the _client_ Clojure application what namespaces and 934 | functions it has available. When the client application calls those functions, 935 | it encodes data and sends it to the pod as a message. The pod will be written 936 | such that it can listen to those messages, decode them, execute the desired 937 | command internally, and send a response message to the client. 938 | 939 | The pod protocol is documented in https://github.com/babashka/pods[the pod GitHub repo]. 940 | 941 | 942 | == Summary == 943 | * Babashka's pod system lets you interact with external processes using Clojure 944 | functions, as opposed to shelling out with `babashka.process/shell` or making 945 | HTTP requests, or something like that 946 | * Those external processes are called _pods_ and must implement the _pod 947 | protocol_ to tell client programs how to interact with them 948 | 949 | 950 | = Other ways of executing code = 951 | This tutorial has focused on helping you build a standalone script that you 952 | interact with like would a typical bash script script: you make it executable 953 | with `chmod +x` and you call it from the command line like `./journal add -e 954 | "dream entry"`. 955 | 956 | There are other flavors (for lack of a better word) of shell scripting that bash 957 | supports: 958 | 959 | * Direct expression evaluation 960 | * Invoking a Clojure function 961 | * Naming tasks 962 | 963 | 964 | == Direct Expression Evaluation == 965 | You can give Babashka a Clojure expression and it will evaluate it and print the 966 | result: 967 | 968 | [source,bash] 969 | ---- 970 | $ bb -e '(+ 1 2 3)' 971 | 9 972 | 973 | $ bb -e '(map inc [1 2 3])' 974 | (2 3 4) 975 | ---- 976 | 977 | Personally I haven't used this much myself, but it's there if you need it! 978 | 979 | 980 | == Invoking a Clojure function == 981 | If we wanted to call our `journal.add/add-entry` function directly, we could do 982 | this: 983 | 984 | [source,bash] 985 | ---- 986 | bb -x journal.add/add-entry --entry "dreamt of foo" 987 | ---- 988 | 989 | When you use `bb -x`, you can specify the fully-qualified name of a function and 990 | Babashka will call it. It will parse command-line arguments using `babashka.cli` 991 | into a Clojure value and pass that to the specified function. See https://book.babashka.org/#_x[the -x section 992 | of the Babashka docs] for more information. 993 | 994 | You can also use `bb -m some-namespace/some-function` to call a function. The 995 | difference between this and `bb -x` is that with `bb -m`, each command line 996 | argument is passed unparsed to the Clojure function. For example: 997 | 998 | [source,bash] 999 | ---- 1000 | $ bb -m clojure.core/identity 99 1001 | "99" 1002 | 1003 | $ bb -m clojure.core/identity "[99 100]" 1004 | "[99 100]" 1005 | 1006 | $ bb -m clojure.core/identity 99 100 1007 | ----- Error -------------------------------------------------------------------- 1008 | Type: clojure.lang.ArityException 1009 | Message: Wrong number of args (2) passed to: clojure.core/identity 1010 | Location: :1:37 1011 | ---- 1012 | 1013 | When using `bb -m`, you can just pass in a namespace and Babashka will call the 1014 | `-main` function for that namespace. Like, if we wanted our `journal.add` 1015 | namespace to work with this flavor of invocation, we would write it like this: 1016 | 1017 | [source,clojure] 1018 | ---- 1019 | (ns journal.add 1020 | (:require 1021 | [journal.utils :as utils])) 1022 | 1023 | (defn -main 1024 | [entry-text] 1025 | (let [entries (utils/read-entries)] 1026 | (spit utils/ENTRIES-LOCATION 1027 | (conj entries 1028 | {:timestamp (System/currentTimeMillis) 1029 | :entry entry-text})))) 1030 | ---- 1031 | 1032 | And we could do this: 1033 | 1034 | [source,bash] 1035 | ---- 1036 | $ bb -m journal.add "recurring foo dream" 1037 | ---- 1038 | 1039 | Note that for `bb -x` or `bb -m` to work, you must set up your `bb.edn` file so 1040 | that the namespace you're invoking is reachable on the classpath. 1041 | 1042 | 1043 | = Tasks = 1044 | Another flavor of running command line programs is to call them similarly to 1045 | `make` and `npm`. In your travels as a programmer, you might have run these at 1046 | the command line: 1047 | 1048 | [source,bash] 1049 | ---- 1050 | make install 1051 | npm build 1052 | npm run build 1053 | npm run dev 1054 | ---- 1055 | 1056 | Babashka allows you to write commands similarly. For our dream journal, we might 1057 | want to be able to execute the following in a terminal: 1058 | 1059 | [source,bash] 1060 | ---- 1061 | bb add -e "A monk told me the meaning of life. Woke up, for got it." 1062 | bb list 1063 | ---- 1064 | 1065 | We're going to build up to that in small steps. 1066 | 1067 | 1068 | == A basic task == 1069 | First, let's look at a very basic task definition. Tasks are defined in your 1070 | `bb.edn` file. Update yours to look like this: 1071 | 1072 | [source,clojure] 1073 | ---- 1074 | {:tasks {welcome (println "welcome to your dream journal")}} 1075 | ---- 1076 | 1077 | Tasks are defined using a map under the `:tasks` keyword. Each key of the map 1078 | names a task, and it should be a symbol. Each value should be a Clojure 1079 | expression. In this example, the `welcome` names a task and the associated 1080 | expression is `(println "welcome to your dream journal")`. 1081 | 1082 | When you call `bb welcome`, it looks up the `welcome` key under `:tasks` and 1083 | evaluates the associated expression. Note that you must explicitly print values 1084 | if you want them to be sent to `stdout`; this wouldn't print anything: 1085 | 1086 | [source,clojure] 1087 | ---- 1088 | {:tasks {welcome "welcome to your dream journal"}} 1089 | ---- 1090 | 1091 | 1092 | == How to require namespaces for tasks == 1093 | Let's say you wanted to create a task to delete your journal entries. Here's 1094 | what that would looke like: 1095 | 1096 | [source,clojure] 1097 | ---- 1098 | {:tasks {welcome (println "welcome to your dream journal") 1099 | clear (shell "rm -rf entries.edn")}} 1100 | ---- 1101 | 1102 | If you run `bb clear` it will delete your `entries.edn` file. This works because 1103 | `shell` is automatically referred in namespaces, just `clojure.core` functions 1104 | are. 1105 | 1106 | If you wanted to delete your file in a cross-platform-friendly way, you could 1107 | use the `babashka.fs/delete-if-exists` function. To do that, you must require 1108 | the `babashka.fs` namespace. You might assume that you could update your 1109 | `bb.edn` to look like this and it would work, but it wouldn't: 1110 | 1111 | [source,clojure] 1112 | ---- 1113 | {:tasks {clear (do (require '[babashka.fs :as fs]) 1114 | (fs/delete-if-exists "entries.edn"))}} 1115 | ---- 1116 | 1117 | Instead, to require namespaces you must do so like this: 1118 | 1119 | [source,clojure] 1120 | ---- 1121 | {:tasks {:requires ([babashka.fs :as fs]) 1122 | clear (fs/delete-if-exists "entries.edn")}} 1123 | ---- 1124 | 1125 | 1126 | == Use `exec` to parse arguments and call a function == 1127 | We still want to be able to call `bb add` and `bb list`. We have what we need to 1128 | implement `bb list`; we can just update `bb.edn` to look like this: 1129 | 1130 | [source,clojure] 1131 | ---- 1132 | {:paths ["src"] 1133 | :tasks {:requires ([babashka.fs :as fs] 1134 | [journal.list :as list]) 1135 | clear (fs/delete-if-exists "entries.edn") 1136 | list (list/list-entries nil)}} 1137 | ---- 1138 | 1139 | In the previous task examples I excluded the `:paths` key because it wasn't 1140 | needed, but we need to bring it back so that Babashka can find `journal.list` on 1141 | the classpath. `journal.list/list-entries` takes one argument that gets ignored, 1142 | so we can just pass in `nil` and it works. 1143 | 1144 | `journal.add/add-entries`, however, takes a Clojure map with an `:entries` key. 1145 | Thus we need some way of parsing the command line arguments into that map and then 1146 | passing that to `journal.add/add-entries`. Babashka provides the `exec` function 1147 | for this. Update your `bb.edn` like so, and everything should work: 1148 | 1149 | [source,clojure] 1150 | ---- 1151 | {:paths ["src"] 1152 | :tasks {:requires ([babashka.fs :as fs] 1153 | [journal.list :as list]) 1154 | clear (fs/delete-if-exists "entries.edn") 1155 | list (list/list-entries nil) 1156 | add (exec 'journal.add/add-entry)}} 1157 | ---- 1158 | 1159 | Now we can call this, and it should work: 1160 | 1161 | [source,bash] 1162 | ---- 1163 | $ bb add --entry "dreamt I was done writing a tutorial. bliss" 1164 | 1165 | $ bb list 1166 | 1670718856173 1167 | dreamt I was done writing a tutorial. bliss 1168 | ---- 1169 | 1170 | The key here is the `exec` function. With `(exec 'journal.add/add-entry)`, it's 1171 | as if you called this on the command line: 1172 | 1173 | [source,bash] 1174 | ---- 1175 | $ bb -x journal.add/add-entry --entry "dreamt I was done writing a tutorial. bliss" 1176 | ---- 1177 | 1178 | `exec` will parse command line arguments in the same way as `bb -x` does and 1179 | pass the result to the designated function, which is `journal.add/add-entry` in 1180 | this example. 1181 | 1182 | 1183 | == Task dependencies, parallel tasks, and more == 1184 | Babashka's task system has even more capabilities, which I'm not going to cover 1185 | in detail but which you can read about in the https://book.babashka.org/#tasks[Task runner section of the 1186 | Babashka docs]. 1187 | 1188 | I do want to highlight two very useful features: _task dependencies_ and 1189 | _parallel task execution_. 1190 | 1191 | Babashka let's you define task dependencies, meaning that you can define 1192 | `task-a` to depend on `task-b` such that if you run `bb task-a`, internally 1193 | `task-b` will be executed if needed. This is useful for creating compilation 1194 | scripts. If you were building a web app, for example, you might have separate 1195 | tasks for compiling a backend jar file and frontend javascript file. You could 1196 | have the tasks `build-backend`, `build-frontend`, and then have a `build` task 1197 | that depended on the other two. If you were to call `bb build`, Babashka would 1198 | be able to determine which of the other two tasks needed to be run and only 1199 | run them when necessary. 1200 | 1201 | Parallel task execution will have Babashka running multiple tasks at the same 1202 | time. In our build example, `bb build` could run `build-backend` and 1203 | `build-frontend` at the same time, which could be a real time saver. 1204 | 1205 | 1206 | == Summary == 1207 | * You define tasks in `bb.edn` under the `:tasks` key 1208 | * Task definitions are key-value pairs where the key is a symbol naming the 1209 | task, and the value is a Clojure expression 1210 | * Add a `:requires` key under the `:tasks` key to require namespaces 1211 | * `exec` executes functions as if invoked with `bb -x journal.add/add-entry`; it 1212 | parses command line args before passing to the function 1213 | * You can declare task dependencies 1214 | * You can run tasks in parallel 1215 | 1216 | 1217 | = Additional Resources = 1218 | * https://github.com/babashka/babashka/wiki/Bash-and-Babashka-equivalents[Bash and Babashka equivalents] is indispensable for transferring your Bash 1219 | knowledge to Babashka 1220 | 1221 | 1222 | = Acknowledgments = 1223 | The following people read drafts of this and gave feedback. Thank you! 1224 | 1225 | * Michiel Borkent @borkdude 1226 | * Marcela Poffalo 1227 | * Gabriel Horner @cldwalker 1228 | * @geraldodev 1229 | * Andrew Patrick @Ajpatri 1230 | * Alex Gravem @kartesus 1231 | * Inge Solvoll @ingesol 1232 | * @focaskater 1233 | * @monkey1@fosstodon.org 1234 | * Kira McLean 1235 | 1236 | 1237 | = Feedback = 1238 | If you have feedback, please open an issue at 1239 | https://github.com/braveclojure/babooka[https://github.com/braveclojure/babooka]. I can't promise I'll respond in a 1240 | timely manner, or even at all, so I apologize in advice! I'm just not great at 1241 | responding, it's one of my character flaws, but I appreciate the feedback! 1242 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+title: Babashka Babooka: Write Command-Line Clojure 2 | 3 | * Introduction 4 | 5 | - [[https://raw.githubusercontent.com/braveclojure/babooka/main/babooka.pdf][Download the free PDF]] 6 | - [[https://raw.githubusercontent.com/braveclojure/babooka/main/babooka.epub][Download the free epub]] 7 | 8 | There are two types of programmers in the world: the practical, sensible, 9 | shell-resigned people who need to google the correct argument order for ~ln -s~; 10 | and those twisted, Stockholmed souls who will gleefully run their company's 11 | entire infrastructure on 57 commands stitched together into a single-line 12 | bash script. 13 | 14 | This guide is for the former. For the latter: sorry, but I can't help you. 15 | 16 | [[https://babashka.org][Babashka]] is a Clojure scripting runtime that is a powerful, delightful 17 | alternative to the shell scripts you're used to. This comprehensive tutorial 18 | will teach you: 19 | 20 | - What babashka is, what it does, how it works, and how it can fit into your 21 | workflow 22 | - How to write babashka scripts 23 | - How to organize your babashka projects 24 | - What pods are, and how they provide a native Clojure interface for external 25 | programs 26 | - How to use tasks to create interfaces similar to ~make~ or ~npm~ 27 | 28 | If you'd like to stop doing something that hurts (writing incomprehensible shell 29 | scripts) and start doing something that feels great (writing Babashka scripts), 30 | then read on! 31 | 32 | NOTE: If you're unfamiliar with Clojure, Babashka is actually a great tool for 33 | learning! [[https://www.braveclojure.com/do-things/][This crash course]] and [[https://www.braveclojure.com/organization/][this chapter on namespaces]] cover what you need 34 | to understand the Clojure used here. There are many good editor extensions for 35 | working with Clojure code, including [[https://calva.io/getting-started/][Calva for VS Code]] and [[https://docs.cider.mx/cider/index.html][CIDER for emacs]]. If 36 | you're new to the command line, check out [[https://www.learnenough.com/command-line-tutorial][Learn Enough Command Line to be 37 | Dangerous]]. 38 | 39 | ** Sponsor 40 | 41 | If you enjoy this tutorial, [[https://github.com/sponsors/flyingmachine][consider sponsoring me, Daniel Higginbotham, through 42 | GitHub sponsors]]. As of April 2022 I am spending two days a week working on 43 | free Clojure educational materials and open source libraries to make Clojure 44 | more beginner-friendly, and appreciate any support! 45 | 46 | Please also [[https://github.com/sponsors/borkdude][consider sponsoring Michiel Borkent, aka borkdude, who created 47 | babashka]]. Michiel is doing truly incredible work to transform the Clojure 48 | landscape, extending its usefulness and reach in ways that benefit us all. He 49 | has a proven track record of delivering useful tools and engaging with the 50 | commuity. 51 | 52 | ** What is Babashka? 53 | 54 | From a user perspective, babashka is a scripting runtime for the Clojure 55 | programming language. It lets you execute Clojure programs in contexts where 56 | you'd typically use bash, ruby, python, and the like. Use cases include build 57 | scripts, command line utilities, small web applications, git hooks, AWS Lambda 58 | functions, and everywhere you want to use Clojure where fast startup and/or low 59 | resource usage matters. 60 | 61 | You can run something like the following in a terminal to immediately execute 62 | your Clojure program: 63 | 64 | #+begin_src bash 65 | bb my-clojure-program.clj 66 | #+end_src 67 | 68 | If you're familiar with Clojure, you'll find this significant because it 69 | eliminates the startup time you'd otherwise have to contend with for a 70 | JVM-compiled Clojure program, not to mention you don't have to compile the file. 71 | It also uses much less memory than running a jar. Babashka makes it feasible to 72 | use Clojure even more than you already do. 73 | 74 | If you're unfamiliar with Clojure, using Babashka is a great way to try out the 75 | language. Clojure is a /hosted/ language, meaning that the language is defined 76 | independently of the underlying runtime environment. Most Clojure programs are 77 | compiled to run on the Java Virtual Machine (JVM) so that they can be run 78 | anywhere Java runs. The other main target is JavaScript, allowing Clojure to run 79 | in a browser. With Babashka, you can now run Clojure programs where you'd 80 | normally run bash scripts. The time you spend investing in Clojure pays 81 | dividends as your knowledge transfers to these varied environments. 82 | 83 | From an implementation perspective, Babashka is a standalone, natively-compiled 84 | binary, meaning that the operating system executes it directly, rather than 85 | running in a JVM. When the babashka binary gets compiled, it includes many 86 | Clojure namespaces and libraries so that they are usable with native 87 | performance. You can [[https://book.babashka.org/#libraries][check out the full list of built-in namespaces]]. Babashka 88 | can also include other libraries, just like if you're using deps.edn or 89 | Leiningen. 90 | 91 | The binary also includes the [[https://github.com/babashka/SCI][Small Clojure Interpreter (SCI)]] to interpret the 92 | Clojure you write and additional libraries you include on the fly. Its 93 | implementation of Clojure is nearly at parity with JVM Clojure, and it improves 94 | daily thanks to [[https://github.com/borkdude][Michiel Borkent]]'s ceaseless work. It's built with GraalVM. This 95 | guide is focused on becoming productive with Babashka and doesn't cover the 96 | implementation in depth, but you can learn more about it by reading [[https://medium.com/graalvm/babashka-how-graalvm-helped-create-a-fast-starting-scripting-environment-for-clojure-b0fcc38b0746][this article 97 | on the GraalVM blog]]. 98 | 99 | ** Why should you use it? 100 | 101 | I won't go into the benefits of Clojure itself because there are plenty of 102 | materials on that [[https://jobs-blog.braveclojure.com/2022/03/24/long-term-clojure-benefits.html][elsewhere]]. 103 | 104 | Beyond the fact that it's Clojure, Babashka brings a few features that make it 105 | stand apart from contenders: 106 | 107 | *First-class support for multi-threaded programming.* Clojure makes 108 | multi-threaded programming simple and easy to write and reason about. With 109 | Babashka, you can write straightforward scripts that e.g. fetch and process data 110 | from multiple databases in parallel. 111 | 112 | *Real testing.* You can unit test your Babashka code just as you would any other 113 | Clojure project. How do you even test bash? 114 | 115 | *Real project organization.* Clojure namespaces are a sane way to organize your 116 | project's functions and build reusable libraries. 117 | 118 | *Cross-platform compatibility.* It's nice not having to worry that an OS 119 | X-developed script is broken in your continuous integration pipeline. 120 | 121 | *Interactive Development.* Following the lisp tradition, Babashka provides a 122 | read-eval-print loop (REPL) that gives you that good good bottom-up 123 | fast-feedback feeling. Script development is inherently a fast; Babashka makes 124 | it a faster. 125 | 126 | *Built-in tools for defining your script's interface.* One reason to write a 127 | shell script is to provide a concise, understandable interface for a complicated 128 | process. For example, you might write a build script that includes ~build~ and 129 | ~deploy~ commands that you call like 130 | 131 | #+begin_src bash 132 | ./my-script build 133 | ./my-script deploy 134 | #+end_src 135 | 136 | Babashka comes with tools that gives you a consistent way of defining such 137 | commands, and for parsing command-line arguments into Clojure data structures. 138 | Take that, bash! 139 | 140 | *A rich set of libraries.* Babashka comes with helper utilities for doing 141 | typical shell script grunt work like interacting with processes or mucking about 142 | with the filesystem. It also has support for the following without needing extra 143 | dependencies: 144 | 145 | - JSON parsing 146 | - YAML parsing 147 | - Starting an HTTP server 148 | - Writing generative tests 149 | 150 | And of course, you can add Clojure libraries as dependencies to accomplish even 151 | more. Clojure is a gateway drug to other programming paradigms, so if you ever 152 | wanted to do e.g. logic programming from the command line, now's your chance! 153 | 154 | *Good error messages.* Babashka's error handling is the friendliest of all 155 | Clojure implementations, directing you precisely to where an error occurred. 156 | 157 | ** Installation 158 | 159 | Installing with brew is ~brew install borkdude/brew/babashka~. 160 | 161 | [[https://github.com/babashka/babashka#installation][For other systems, see Babashka's complete installation instructions.]] 162 | 163 | * Your first script 164 | 165 | Throughout this tutorial we're going to play with building a little CLI-based 166 | dream journal. Why? Because the idea of you nerds recording your weird little 167 | subconscious hallucinations is deeply amusing to me. 168 | 169 | In this section, you're going to learn: 170 | 171 | - How to write and run your first Babashka script 172 | - How default output is handled 173 | - A little about how Babashka treats namespaces 174 | 175 | Create a file named ~hello.clj~ and put this in it: 176 | 177 | #+begin_src clojure 178 | (require '[clojure.string :as str]) 179 | (prn (str/join " " ["Hello" "inner" "world!"])) 180 | #+end_src 181 | 182 | Now run it with ~bb~, the babashka executable: 183 | 184 | #+begin_src clojure 185 | bb hello.clj 186 | #+end_src 187 | 188 | You should see it print the text ~"Hello inner world!"~. 189 | 190 | There are a few things here to point out for experienced Clojurians: 191 | 192 | - You didn't need a deps.edn file or project.clj 193 | - There's no namespace declaration; we use ~(require ...)~ 194 | - It's just Clojure 195 | 196 | I very much recommend that you actually try this example before proceeding 197 | because it /feels/ different from what you're used to. It's unlikely that you're 198 | used to throwing a few Clojure expressions into a file and being able to run 199 | them immediately. 200 | 201 | When I first started using Babashka, it felt so different that it was 202 | disorienting. It was like the first time I tried driving an electric car and my 203 | body freaked out a little because I wasn't getting the typical sensory cues like 204 | hearing and feeling the engine starting. 205 | 206 | Babashka's like that: the experience is so quiet and smooth it's jarring. No 207 | deps.edn, no namespace declaration, write only the code you need and it runs! 208 | 209 | That's why I included the "It's just Clojure" bullet point. It might feel 210 | different, but this is still Clojure. Let's explore the other points in more 211 | detail. 212 | 213 | ** Babashka's output 214 | 215 | Here's what's going on: ~bb~ interprets the Clojure code you've written, 216 | executing it on the fly. ~prn~ prints to ~stdout~, which is why ~"Hello, 217 | inner world!"~ is returned in your terminal. 218 | 219 | NOTE: When you print text to ~stdout~, it gets printed to your terminal. This 220 | tutorial doesn't get into what ~stdout~ actually is, but you can think of it as 221 | the channel between the internal world of your program and the external world of 222 | the environment calling your program. When your program sends stuff to ~stdout~, 223 | your terminal receives it and prints it. 224 | 225 | Notice that the quotes are maintained when the value is printed. ~bb~ will 226 | print the /stringified representation of your data structure/. If you updated 227 | ~hello.clj~ to read 228 | 229 | #+begin_src clojure 230 | "Hello, inner world!" 231 | (prn ["It's" "me," "your" "wacky" "subconscious!"]) 232 | #+end_src 233 | 234 | Then ~["It's" "me," "your" "wacky" "subconscious!"]~ would get printed, and 235 | ~"Hello, inner world!"~ would not. You must use a printing function on a form 236 | for it to be sent to ~stdout~ 237 | 238 | If you want to print a string without the surrounding quotes, you can use 239 | 240 | #+begin_src clojure 241 | (println "Hello, inner world!") 242 | #+end_src 243 | 244 | ** Namespace is optional 245 | 246 | As for the lack of namespace: this is part of what makes Babashka useful as a 247 | scripting tool. When you're in a scripting state of mind, you want to start 248 | hacking on ideas immediately; you don't want to have to deal with boilerplate 249 | just to get started. Babashka has your babacka. 250 | 251 | You /can/ define a namespace (we'll look at that more when we get into project 252 | organization), but if you don't then Babashka uses the ~user~ namespace by 253 | default. Try updating your file to read: 254 | 255 | #+BEGIN_SRC clojure 256 | (str "Hello from " *ns* ", inner world!") 257 | #+END_SRC 258 | 259 | Running it will print ~"Hello from user, inner world!"~. This might be 260 | surprising because there's a mismatch between filename (~hello.clj~) and 261 | namespace name. In other Clojure implementations, the current namespace strictly 262 | corresponds to the source file's filename, but Babashka relaxes that a little 263 | bit in this specific context. It provides a scripting experience that's more in 264 | line with what you'd expect from using other scripting languages. 265 | 266 | ** What about requiring other namespaces? 267 | 268 | You might want to include a namespace declaration because you want to require 269 | some namespaces. With JVM Clojure and Clojurescript, you typically require 270 | namespaces like this: 271 | 272 | #+begin_src clojure 273 | (ns user 274 | (:require 275 | [clojure.string :as str])) 276 | #+end_src 277 | 278 | It's considered bad form to require namespaces by putting ~(require 279 | '[clojure.string :as str])~ in your source code. 280 | 281 | That's not the case with Babashka. You'll see ~(require ...)~ used liberally in 282 | other examples, and it's OK for you to do that too. 283 | 284 | ** Make your script executable 285 | 286 | What if you want to execute your script by typing something like ~./hello~ 287 | instead of ~bb hello.clj~? You just need to rename your file, add a shebang, and 288 | ~chmod +x~ that bad boy. Update ~hello.clj~ to read: 289 | 290 | #+begin_src clojure 291 | #!/usr/bin/env bb 292 | 293 | (str "Hello from " *ns* ", inner world!") 294 | #+end_src 295 | 296 | NOTE: The first line, ~#!/usr/bin/env bb~ is the "shebang", and I'm not going to 297 | explain it. 298 | 299 | Then run this in your terminal: 300 | 301 | #+begin_src bash 302 | mv hello{.clj,} 303 | chmod +x hello 304 | ./hello 305 | #+end_src 306 | 307 | First you rename the file, then you call ~chmod +x~ on it to make it executable. 308 | Then you actually execute it, saying hi to your own inner world which is kind of 309 | adorable. 310 | 311 | ** Summary 312 | 313 | Here's what you learned in this section: 314 | 315 | - You can run scripts with ~bb script-name.clj~ 316 | - You can make scripts directly executable by adding ~#!/usr/bin/env bb~ on the 317 | top line and adding the ~execute~ permission with ~chmod +x script-name.clj~ 318 | - You don't have to include an ~(ns ...)~ declaration in your script. But it 319 | still runs and it's still Clojure! 320 | - It's acceptable and even encouraged to require namespaces with ~(require 321 | ...)~. 322 | - Babashka writes the last value it encounters to ~stdout~, except if that value 323 | is ~nil~ 324 | 325 | * Working with files 326 | 327 | Shell scripts often need to read input from the command line and produce output 328 | somewhere, and our dream journal utility is no exception. It's going to store 329 | entries in the file ~entries.edn~. The journal will be a vector, and each entry 330 | will be a map with the keys ~:timestamp~ and ~:entry~ (the entry has linebreaks 331 | for readability): 332 | 333 | #+BEGIN_SRC clojure 334 | [{:timestamp 0 335 | :entry "Dreamt the drain was clogged again, except when I went to unclog 336 | it it kept growing and getting more clogged and eventually it 337 | swallowed up my little unclogger thing"} 338 | {:timestamp 1 339 | :entry "Bought a house in my dream, was giving a tour of the backyard and 340 | all the... topiary? came alive and I had to fight it with a sword. 341 | I understood that this happens every night was very annoyed that 342 | this was not disclosed in the listing."}] 343 | #+END_SRC 344 | 345 | To write to the journal, we want to run the command ~./journal add --entry 346 | "Hamsters. Hamsters everywhere. Again."~. The result should be that a map gets 347 | appended to the vector. 348 | 349 | Let's get ourselves part of the way there. Create the file ~journal~ and make it 350 | executable with ~chmod +x journal~, then make it look like this: 351 | 352 | #+begin_src clojure 353 | #!/usr/bin/env bb 354 | 355 | (require '[babashka.fs :as fs]) 356 | (require '[clojure.edn :as edn]) 357 | 358 | (def ENTRIES-LOCATION "entries.edn") 359 | 360 | (defn read-entries 361 | [] 362 | (if (fs/exists? ENTRIES-LOCATION) 363 | (edn/read-string (slurp ENTRIES-LOCATION)) 364 | [])) 365 | 366 | (defn add-entry 367 | [text] 368 | (let [entries (read-entries)] 369 | (spit ENTRIES-LOCATION 370 | (conj entries {:timestamp (System/currentTimeMillis) 371 | :entry text})))) 372 | 373 | (add-entry (first *command-line-args*)) 374 | #+end_src 375 | 376 | We require a couple namespaces: ~babashka.fs~ and ~clojure.edn~. ~babashka.fs~ is 377 | a collection of functions for working with the filesystem; check out its [[https://github.com/babashka/fs][API 378 | docs]]. When you're writing shell scripts, you're very likely to work with the 379 | filesystem, so this namespace is going to be your friend. 380 | 381 | Here, we're using the ~fs/exists?~ function to check that ~entries.edn~ exists 382 | before attempting to read it because ~slurp~ will throw an exception if it can't 383 | find the file for the path you passed it. 384 | 385 | The ~add-entry~ function uses ~read-entries~ to get a vector of entries, uses 386 | ~conj~ to add an entry, and then uses ~spit~ to write back to ~entries.edn~. By 387 | default, ~spit~ will overwrite a file; if you want to append to it, you would 388 | call it like 389 | 390 | #+begin_src clojure 391 | (spit "entries.edn" {:timestap 0 :entry ""} :append true) 392 | #+end_src 393 | 394 | Maybe overwriting the whole file is a little dirty, but that's the scripting 395 | life babyyyyy! 396 | 397 | * Creating an interface for your script 398 | 399 | OK so in the last line we call ~(add-entry (first \*command-line-args*))~. 400 | ~\*command-line-args*~ is a sequence containing, well, all the command line 401 | arguments that were passed to the script. If you were to create the file 402 | ~args.clj~ with the contents ~\*command-line-args*~, then ran ~bb args.clj 1 2 403 | 3~, it would print ~("1" "2" "3")~. 404 | 405 | Our ~journal~ file is at the point where we can add an entry by calling 406 | ~./journal "Flying\!\! But to Home Depot??"~. This is almost what we want; we 407 | actually want to call ~./journal add --entry "Flying\!\! But to Home Depot??"~. 408 | The assumption here is that we'll want to have other commands like ~./journal 409 | list~ or ~./journal delete~. (You have to escape the exclamation marks otherwise 410 | bash interprets them as history commands.) 411 | 412 | To accomplish this, we'll need to handle the command line arguments in a more 413 | sophisticated way. The most obvious and least-effort way to do this would be to 414 | dispatch on the first argument to ~\*command-line-args*~, something like this: 415 | 416 | #+BEGIN_SRC clojure 417 | (let [[command _ entry] *command-line-args*] 418 | (case command 419 | "add" (add-entry entry))) 420 | #+END_SRC 421 | 422 | This might be totally fine for your use case, but sometimes you want something 423 | more robust. You might want your script to: 424 | 425 | - List valid commands 426 | - Give an intelligent error message when a user calls a command that doesn't 427 | exist (e.g. if the user calls ~./journal add-dream~ instead of ~./journal 428 | add~) 429 | - Parse arguments, recognizing option flags and converting values to keywords, 430 | numbers, vectors, maps, etc 431 | 432 | Generally speaking, *you want a clear and consistent way to define an interface 433 | for your script*. This interface is responsible for taking the data provided at 434 | the command line -- arguments passed to the script, as well as data piped in 435 | through ~stdin~ -- and using that data to handle these three responsibilities: 436 | 437 | - Dispatching to a Clojure function 438 | - Parsing command-line arguments into Clojure data, and passing that to the 439 | dispatched function 440 | - Providing feedback in cases where there's a problem performing the above 441 | responsibilities. 442 | 443 | The broader Clojure ecosystem provides at least two libraries for handling 444 | argument parsing: 445 | 446 | - [[https://github.com/clojure/tools.cli][clojure.tools.cli]] 447 | - [[https://github.com/nubank/docopt.clj][nubank/docopt.clj]] 448 | 449 | Babashka provides the [[https://github.com/babashka/cli][babashka.cli library]] for both parsing options and 450 | dispatches subcommands. We're going to focus just on babashka.cli. 451 | 452 | ** parsing options with babashka.cli 453 | 454 | The [[https://github.com/babashka/cli][babashka.cli docs]] do a good job of explaining how to use the library to meet 455 | all your command line parsing needs. Rather than going over every option, I'll 456 | just focus on what we need to build our dream journal. To parse options, we 457 | require the ~babashka.cli~ namespace and we define a /CLI spec/: 458 | 459 | #+BEGIN_SRC clojure 460 | (require '[babashka.cli :as cli]) 461 | (def cli-opts 462 | {:entry {:alias :e 463 | :desc "Your dreams." 464 | :require true} 465 | :timestamp {:alias :t 466 | :desc "A unix timestamp, when you recorded this." 467 | :coerce {:timestamp :long}}}) 468 | #+END_SRC 469 | 470 | A CLI spec is a map where each key is a keyword, and each value is an /option 471 | spec/. This key is the /long name/ of your option; ~:entry~ corresponds to the 472 | flag ~--entry~ on the command line. 473 | 474 | The option spec is a map you can use to further config the option. ~:alias~ lets 475 | you specify a /short name/ for you options, so that you can use e.g. ~-e~ 476 | instead of ~--entry~ at the command line. ~:desc~ is used to create a summary 477 | for your interface, and ~:require~ is used to enforce the presence of an option. 478 | ~:coerce~ is used to transform the option's value into some other data type. 479 | 480 | We can experiment with this CLI spec in a REPL. There are many options for 481 | starting a Babashka REPL, and the most straightforward is simply typing ~bb 482 | repl~ at the command line. If you want to use CIDER, first add the file ~bb.edn~ 483 | and put an empty map, ~{}~, in it. Then you can use ~cider-jack-in~. After that, 484 | you can paste in the code from the snippet above, then paste in this snippet: 485 | 486 | #+begin_src clojure 487 | (cli/parse-opts ["-e" "The more I mowed, the higher the grass got :("] {:spec cli-opts}) 488 | ;; => 489 | {:entry "The more I mowed, the higher the grass got :("} 490 | #+end_src 491 | 492 | Note that ~cli/parse-opts~ returns a map with the parsed options, which will 493 | make it easy to use the options later. 494 | 495 | Leaving out a required flag throws an exception: 496 | 497 | #+begin_src clojure 498 | (cli/parse-opts [] {:spec cli-opts}) 499 | ;; exception gets thrown, this gets printed: 500 | : Required option: :entry user 501 | #+end_src 502 | 503 | ~cli/parse-opts~ is a great tool for building an interface for simple scripts! 504 | You can communicate that interface to the outside world with ~cli/format-opts~. 505 | This function will take an option spec and return a string that you can print to 506 | aid people in using your program. Behold: 507 | 508 | #+begin_src clojure 509 | (println (cli/format-opts {:spec cli-opts})) 510 | ;; => 511 | -e, --entry Your dreams. 512 | -t, --timestamp A unix timestamp, when you recorded this. 513 | #+end_src 514 | 515 | ** dispatching subcommands with babashka.cli 516 | 517 | babashka.cli goes beyond option parsing to also giving you a way to dispatch 518 | subcommands, which is exactly what we want to get ~./journal add --entry "..."~ 519 | working. Here's the final version of ~journal~: 520 | 521 | #+begin_src clojure 522 | #!/usr/bin/env bb 523 | 524 | (require '[babashka.cli :as cli]) 525 | (require '[babashka.fs :as fs]) 526 | (require '[clojure.edn :as edn]) 527 | 528 | (def ENTRIES-LOCATION "entries.edn") 529 | 530 | (defn read-entries 531 | [] 532 | (if (fs/exists? ENTRIES-LOCATION) 533 | (edn/read-string (slurp ENTRIES-LOCATION)) 534 | [])) 535 | 536 | (defn add-entry 537 | [{:keys [opts]}] 538 | (let [entries (read-entries)] 539 | (spit ENTRIES-LOCATION 540 | (conj entries 541 | (merge {:timestamp (System/currentTimeMillis)} ;; default timestamp 542 | opts))))) 543 | 544 | (def cli-opts 545 | {:entry {:alias :e 546 | :desc "Your dreams." 547 | :require true} 548 | :timestamp {:alias :t 549 | :desc "A unix timestamp, when you recorded this." 550 | :coerce {:timestamp :long}}}) 551 | 552 | (defn help 553 | [_] 554 | (println 555 | (str "add\n" 556 | (cli/format-opts {:spec cli-opts})))) 557 | 558 | (def table 559 | [{:cmds ["add"] :fn add-entry :spec cli-opts} 560 | {:cmds [] :fn help}]) 561 | 562 | (cli/dispatch table *command-line-args*) 563 | #+end_src 564 | 565 | Try it out with the following at your terminal: 566 | 567 | #+begin_src bash 568 | ./journal 569 | ./journal add -e "dreamt they did one more episode of Firefly, and I was in it" 570 | #+end_src 571 | 572 | The function ~cli/dispatch~ at the bottom takes a dispatch table as its first 573 | argument. ~cli/dispatch~ figures out which of the arguments you passed in at the 574 | command line correspond to commands, and then calls the corresponding ~:fn~. If 575 | you type ~./journal add ...~, it will dispatch the ~add-entry~ function. If you 576 | just type ~./journal~ with no arguments, then the ~help~ function gets 577 | dispatched. 578 | 579 | The dispatched function receives a map as its argument, and that map contains 580 | the ~:opts~ key. This is a map of parsed command line options, and we use it to 581 | build our dream journal entry in the ~add-entry~ function. 582 | 583 | And that, my friends, is how you build an interface for your script! 584 | ** Summary 585 | 586 | - For scripts of any complexity, you generally need to /parse/ the command line 587 | options into Clojure data structures 588 | - The libraries ~clojure.tools.cli~ and ~nubank/docopts~ will parse command line 589 | arguments into options for you 590 | - I prefer using ~babashka.cli~ because it also handles subcommand dispatch, but 591 | really this decision is a matter of taste 592 | - ~cli/parse-opts~ takes an /options spec/ and returns a map 593 | - ~cli/format-opts~ is useful for creating help text 594 | - Your script might provide /subcommands/, e.g. ~add~ in ~journal add~, and you 595 | will need to map the command line arguments to the appropriate function in 596 | your script with ~cli/dispatch~ 597 | 598 | * Organizing your project 599 | 600 | You can now record your subconscious's nightly improv routine. That's great! 601 | High on this accomplishment, you decide to kick things up a notch and add the 602 | ability to list your entries. You want to run ~./journal list~ and have your 603 | script return something like this: 604 | 605 | #+begin_src 606 | 2022-12-07 08:03am 607 | There were two versions of me, and one version baked the other into a pie and ate it. 608 | Feeling both proud and disturbed. 609 | 610 | 2022-12-06 07:43am 611 | Was on a boat, but the boat was powered by cucumber sandwiches, and I had to keep 612 | making those sandwiches so I wouldn't get stranded at sea. 613 | #+end_src 614 | 615 | You read somewhere that source files should be AT MOST 25 lines long, so you 616 | decide that you want to split up your codebase and put this list functionality 617 | in its own file. How do you do that? 618 | 619 | You can organize your Babashka projects just like your other Clojure projects, 620 | splitting your codebase into separate files, with each file defining a namespace 621 | and with namespaces corresponding to file names. Let's reorganize our current 622 | codebase a bit, making sure everything still works, and then add a namespace for 623 | listing entries. 624 | 625 | ** File system structure 626 | 627 | One way to organize our dream journal project would be to create the following 628 | file structure: 629 | 630 | #+begin_src 631 | ./journal 632 | ./src/journal/add.clj 633 | ./src/journal/utils.clj 634 | #+end_src 635 | 636 | Already, you can see that this looks both similar to typical Clojure project 637 | file structures, and a bit different. We're placing our namespaces in the 638 | ~src/journal~ directory, which lines up with what you'd see in JVM or 639 | ClojureScript projects. What's different in our Babashka project is that we're 640 | still using ~./journal~ to serve as the executable entry point for our program, 641 | rather than the convention of using ~./src/journal/core.clj~ or something like 642 | that. This might feel a little weird but it's valid and it's still Clojure. 643 | 644 | And like other Clojure environments, you need to tell Babashka to look in the 645 | ~src~ directory when you require namespaces. You do that by creating the file 646 | ~bb.edn~ in the same directory as ~journal~ and putting this in it: 647 | 648 | #+begin_src clojure 649 | {:paths ["src"]} 650 | #+end_src 651 | 652 | ~bb.edn~ is similar to a ~deps.edn~ file in that one of its responsibilities is 653 | telling Babashka how to construct your classpath. The classpath is the set of 654 | the directories that Babashka should look in when you require namespaces, and by 655 | adding ~"src"~ to it you can use ~(require '[journal.add])~ in your project. 656 | Babashka will be able to find the corresponding file. 657 | 658 | Note that there is nothing special about the ~"src"~ directory. You could use 659 | ~"my-code"~ or even ~"."~ if you wanted, and you can add more than one path. 660 | ~"src"~ is just the convention preferred by discerning Clojurians the world 661 | over. 662 | 663 | With this in place, we'll now update ~journal~ so that it looks like this: 664 | 665 | #+begin_src clojure 666 | #!/usr/bin/env bb 667 | 668 | (require '[babashka.cli :as cli]) 669 | (require '[journal.add :as add]) 670 | 671 | (def cli-opts 672 | {:entry {:alias :e 673 | :desc "Your dreams." 674 | :require true} 675 | :timestamp {:alias :t 676 | :desc "A unix timestamp, when you recorded this." 677 | :coerce {:timestamp :long}}}) 678 | 679 | (def table 680 | [{:cmds ["add"] :fn add/add-entry :spec cli-opts}]) 681 | 682 | (cli/dispatch table *command-line-args*) 683 | #+end_src 684 | 685 | Now the file is only responsible for parsing command line arguments and 686 | dispatching to the correct function. The add functionality has been moved to 687 | another namespace. 688 | 689 | ** Namespaces 690 | 691 | You can see on line 4 that we're requiring a new namespace, ~journal.add~. The 692 | file corresponding to this namespace is ~./src/journal/add.clj~. Here's what 693 | that looks like: 694 | 695 | #+caption: 696 | #+begin_src clojure 697 | (ns journal.add 698 | (:require 699 | [journal.utils :as utils])) 700 | 701 | (defn add-entry 702 | [opts] 703 | (let [entries (utils/read-entries)] 704 | (spit utils/ENTRIES-LOCATION 705 | (conj entries 706 | (merge {:timestamp (System/currentTimeMillis)} ;; default timestamp 707 | opts))))) 708 | #+end_src 709 | 710 | Look, it's a namespace declaration! And that namespace declaration has a 711 | ~(:require ...)~ form. We know that when you write Babashka scripts, you can 712 | forego declaring a namespace if all your code is in one file, like in the 713 | original version of ~journal~. However, once you start splitting your code into 714 | multiple files, the normal rules of Clojure project organization apply: 715 | 716 | - Namespace names must correspond to filesystem paths. If you want to name a 717 | namespace ~journal.add~, Babashka must be able to find it at 718 | ~journal/add.clj~. 719 | - You must tell Babashka where to look to find the files that correspond to 720 | namespaces. You do this by creating a ~bb.edn~ file and putting ~{:paths 721 | ["src"]}~ in it. 722 | 723 | To finish our tour of our new project organization, here's 724 | ~./src/journal/utils.clj~: 725 | 726 | #+begin_src clojure 727 | (ns journal.utils 728 | (:require 729 | [babashka.fs :as fs] 730 | [clojure.edn :as edn])) 731 | 732 | (def ENTRIES-LOCATION "entries.edn") 733 | 734 | (defn read-entries 735 | [] 736 | (if (fs/exists? ENTRIES-LOCATION) 737 | (edn/read-string (slurp ENTRIES-LOCATION)) 738 | [])) 739 | #+end_src 740 | 741 | If you call ~./journal add -e "visited by the tooth fairy, except he was a 742 | balding 45-year-old man with a potbelly from Brooklyn"~, it should still work. 743 | 744 | Now lets create a the ~journal.list~ namespace. Open the file 745 | ~src/journal/list.clj~ and put this in it: 746 | 747 | #+begin_src clojure 748 | (ns journal.list 749 | (:require 750 | [journal.utils :as utils])) 751 | 752 | (defn list-entries 753 | [_] 754 | (let [entries (utils/read-entries)] 755 | (doseq [{:keys [timestamp entry]} (reverse entries)] 756 | (println timestamp) 757 | (println entry "\n")))) 758 | #+end_src 759 | 760 | This doesn't format the timestamp, but other than that it lists our entries in 761 | reverse-chronologial order, just like we want. Yay! 762 | 763 | To finish up, we need to add ~journal.list/list-entries~ to our dispatch table 764 | in the ~journal~ file. That file should now look like this: 765 | 766 | #+begin_src clojure 767 | #!/usr/bin/env bb 768 | 769 | (require '[babashka.cli :as cli]) 770 | (require '[journal.add :as add]) 771 | (require '[journal.list :as list]) 772 | 773 | (def cli-opts 774 | {:entry {:alias :e 775 | :desc "Your dreams." 776 | :require true} 777 | :timestamp {:alias :t 778 | :desc "A unix timestamp, when you recorded this." 779 | :coerce {:timestamp :long}}}) 780 | 781 | (def table 782 | [{:cmds ["add"] :fn #(add/add-entry (:opts %)) :spec cli-opts} 783 | {:cmds ["list"] :fn #(list/list-entries %)}]) 784 | 785 | (cli/dispatch table *command-line-args*) 786 | #+end_src 787 | 788 | ** Summary 789 | 790 | - Namespaces work like they do in JVM Clojure and Clojurescript: namespace names 791 | must correspond to file system structure 792 | - Put the map ~{:paths ["src"]}~ in ~bb.edn~ to tell Babashka where to find the 793 | files for namespaces 794 | 795 | * Adding dependencies 796 | 797 | You can add dependencies to your projects by adding a ~:deps~ key to your 798 | ~bb.edn~ file, resulting in something like this: 799 | 800 | #+begin_src clojure 801 | {:paths ["src"] 802 | :deps {medley/medley {:mvn/version "1.3.0"}}} 803 | #+end_src 804 | 805 | What's cool about Babashka though is that you can also add deps directly in your 806 | script, or even in the repl, like so: 807 | 808 | #+begin_src clojure 809 | (require '[babashka.deps :as deps]) 810 | (deps/add-deps '{:deps {medley/medley {:mvn/version "1.3.0"}}}) 811 | #+end_src 812 | 813 | This is in keeping with the nature of a scripting language, which should enable 814 | quick, low-ceremony development. 815 | 816 | At this point you should be fully equipped to start writing your own Clojure 817 | shell scripts with Babashka. Woohoo! 818 | 819 | In the sections that follow, I'll cover aspects of Babashka that you might not 820 | need immediately but that will be useful to you as your love of Clojure 821 | scripting grows until it becomes all-consuming. 822 | 823 | * Pods 824 | 825 | Babashka /pods/ introduce a way to interact with external processes by calling 826 | Clojure functions, so that you can write code that looks and feels like Clojure 827 | (because it is) even when working with a process that's running outside your 828 | Clojure application, and even when that process is written in another language. 829 | 830 | ** Pod usage 831 | 832 | Let's look at what that means in more concrete terms. Suppose you want to 833 | encrypt your dream journal. You find out about [[https://github.com/rorokimdim/stash][stash]], "a command line program 834 | for storing text data in encrypted form." This is exactly what you need! Except 835 | it's written in Haskell, and furthermore it has a /terminal user interface/ 836 | (TUI) rather than a command-line interface. 837 | 838 | That is, when you run ~stash~ from the command line it "draws" an ascii 839 | interface in your terminal, and you must provide additional input to store text. 840 | You can't store text directly from the command line with something like 841 | 842 | #+begin_src bash 843 | stash store dreams.stash \ 844 | --key 20221210092035 \ 845 | --value "was worried that something was wrong with the house's foundation, 846 | then the whole thing fell into a sinkhole that kept growing until 847 | it swallowed the whole neighborhood" 848 | #+end_src 849 | 850 | 851 | If that were possible, then you could use ~stash~ from within your Bashka 852 | project by using the ~babashka.process/shell~ function, like this: 853 | 854 | #+begin_src clojure 855 | (require '[babashka.process :as bp]) 856 | (bp/shell "stash store dreams.stash --key 20221210092035 --value \"...\"") 857 | #+end_src 858 | 859 | ~bp/shell~ is lets you take advantage of a program's command-line interface; but 860 | again, ~stash~ doesn't provide that. 861 | 862 | However, ~stash~ provides a /pod interface/, so we can use it like this in a 863 | Clojure file: 864 | 865 | #+begin_src clojure 866 | (require '[babashka.pods :as pods]) 867 | (pods/load-pod 'rorokimdim/stash "0.3.1") 868 | (require '[pod.rorokimdim.stash :as stash]) 869 | 870 | (stash/init {"encryption-key" "foo" 871 | "stash-path" "foo.stash" 872 | "create-stash-if-missing" true}) 873 | 874 | (stash/set 20221210092035 "dream entry") 875 | #+end_src 876 | 877 | Let's start at the last line, ~(stash/set 20221210092035 "dream entry")~. This 878 | is the point of pods: they expose an external process's commands as Clojure 879 | functions. They allow these processes to have a /Clojure interface/ so that you 880 | can interact with them by writing Clojure code, as opposed to having to shell 881 | out or make HTTP calls or something like that. 882 | 883 | In the next section I'll explain the rest of the snippet above. 884 | 885 | ** Pod implementation 886 | 887 | Where does the ~stash/set~ function come from? Both the namespace 888 | ~pod.rorokimdim.stash~ and the functions in it are dynamically generated by the 889 | call ~(pods/load-pod 'rorokimdim/stash "0.3.1")~. 890 | 891 | For this to be possible, the external program has to be written to support the 892 | /pod protocol/. "Protocol" here does not refer to a Clojure protocol, it refers 893 | to a standard for exchanging information. Your Clojure application and the 894 | external application need to have some way to communicate with each other given 895 | that they don't live in the same process and they could even be written in 896 | different languages. 897 | 898 | By implementing the pod protocol, a program becomes a pod. In doing so, it gains 899 | the ability to tell the /client/ Clojure application what namespaces and 900 | functions it has available. When the client application calls those functions, 901 | it encodes data and sends it to the pod as a message. The pod will be written 902 | such that it can listen to those messages, decode them, execute the desired 903 | command internally, and send a response message to the client. 904 | 905 | The pod protocol is documented in [[https://github.com/babashka/pods][the pod GitHub repo]]. 906 | 907 | ** Summary 908 | 909 | - Babashka's pod system lets you interact with external processes using Clojure 910 | functions, as opposed to shelling out with ~babashka.process/shell~ or making 911 | HTTP requests, or something like that 912 | - Those external processes are called /pods/ and must implement the /pod 913 | protocol/ to tell client programs how to interact with them 914 | 915 | * Other ways of executing code 916 | 917 | This tutorial has focused on helping you build a standalone script that you 918 | interact with like would a typical bash script script: you make it executable 919 | with ~chmod +x~ and you call it from the command line like ~./journal add -e 920 | "dream entry"~. 921 | 922 | There are other flavors (for lack of a better word) of shell scripting that bash 923 | supports: 924 | 925 | - Direct expression evaluation 926 | - Invoking a Clojure function 927 | - Naming tasks 928 | 929 | ** Direct Expression Evaluation 930 | 931 | You can give Babashka a Clojure expression and it will evaluate it and print the 932 | result: 933 | 934 | #+begin_src bash 935 | $ bb -e '(+ 1 2 3)' 936 | 9 937 | 938 | $ bb -e '(map inc [1 2 3])' 939 | (2 3 4) 940 | #+end_src 941 | 942 | Personally I haven't used this much myself, but it's there if you need it! 943 | 944 | ** Invoking a Clojure function 945 | 946 | If we wanted to call our ~journal.add/add-entry~ function directly, we could do 947 | this: 948 | 949 | #+begin_src bash 950 | bb -x journal.add/add-entry --entry "dreamt of foo" 951 | #+end_src 952 | 953 | When you use ~bb -x~, you can specify the fully-qualified name of a function and 954 | Babashka will call it. It will parse command-line arguments using ~babashka.cli~ 955 | into a Clojure value and pass that to the specified function. See [[https://book.babashka.org/#_x][the -x section 956 | of the Babashka docs]] for more information. 957 | 958 | You can also use ~bb -m some-namespace/some-function~ to call a function. The 959 | difference between this and ~bb -x~ is that with ~bb -m~, each command line 960 | argument is passed unparsed to the Clojure function. For example: 961 | 962 | #+begin_src bash 963 | $ bb -m clojure.core/identity 99 964 | "99" 965 | 966 | $ bb -m clojure.core/identity "[99 100]" 967 | "[99 100]" 968 | 969 | $ bb -m clojure.core/identity 99 100 970 | ----- Error -------------------------------------------------------------------- 971 | Type: clojure.lang.ArityException 972 | Message: Wrong number of args (2) passed to: clojure.core/identity 973 | Location: :1:37 974 | #+end_src 975 | 976 | When using ~bb -m~, you can just pass in a namespace and Babashka will call the 977 | ~-main~ function for that namespace. Like, if we wanted our ~journal.add~ 978 | namespace to work with this flavor of invocation, we would write it like this: 979 | 980 | #+begin_src clojure 981 | (ns journal.add 982 | (:require 983 | [journal.utils :as utils])) 984 | 985 | (defn -main 986 | [entry-text] 987 | (let [entries (utils/read-entries)] 988 | (spit utils/ENTRIES-LOCATION 989 | (conj entries 990 | {:timestamp (System/currentTimeMillis) 991 | :entry entry-text})))) 992 | #+end_src 993 | 994 | And we could do this: 995 | 996 | #+begin_src bash 997 | $ bb -m journal.add "recurring foo dream" 998 | #+end_src 999 | 1000 | Note that for ~bb -x~ or ~bb -m~ to work, you must set up your ~bb.edn~ file so 1001 | that the namespace you're invoking is reachable on the classpath. 1002 | 1003 | * Tasks 1004 | 1005 | Another flavor of running command line programs is to call them similarly to 1006 | ~make~ and ~npm~. In your travels as a programmer, you might have run these at 1007 | the command line: 1008 | 1009 | #+begin_src bash 1010 | make install 1011 | npm build 1012 | npm run build 1013 | npm run dev 1014 | #+end_src 1015 | 1016 | Babashka allows you to write commands similarly. For our dream journal, we might 1017 | want to be able to execute the following in a terminal: 1018 | 1019 | #+begin_src bash 1020 | bb add -e "A monk told me the meaning of life. Woke up, for got it." 1021 | bb list 1022 | #+end_src 1023 | 1024 | We're going to build up to that in small steps. 1025 | 1026 | ** A basic task 1027 | 1028 | First, let's look at a very basic task definition. Tasks are defined in your 1029 | ~bb.edn~ file. Update yours to look like this: 1030 | 1031 | #+begin_src clojure 1032 | {:tasks {welcome (println "welcome to your dream journal")}} 1033 | #+end_src 1034 | 1035 | Tasks are defined using a map under the ~:tasks~ keyword. Each key of the map 1036 | names a task, and it should be a symbol. Each value should be a Clojure 1037 | expression. In this example, the ~welcome~ names a task and the associated 1038 | expression is ~(println "welcome to your dream journal")~. 1039 | 1040 | When you call ~bb welcome~, it looks up the ~welcome~ key under ~:tasks~ and 1041 | evaluates the associated expression. Note that you must explicitly print values 1042 | if you want them to be sent to ~stdout~; this wouldn't print anything: 1043 | 1044 | #+begin_src clojure 1045 | {:tasks {welcome "welcome to your dream journal"}} 1046 | #+end_src 1047 | 1048 | ** How to require namespaces for tasks 1049 | 1050 | Let's say you wanted to create a task to delete your journal entries. Here's 1051 | what that would looke like: 1052 | 1053 | #+begin_src clojure 1054 | {:tasks {welcome (println "welcome to your dream journal") 1055 | clear (shell "rm -rf entries.edn")}} 1056 | #+end_src 1057 | 1058 | If you run ~bb clear~ it will delete your ~entries.edn~ file. This works because 1059 | ~shell~ is automatically referred in namespaces, just ~clojure.core~ functions 1060 | are. 1061 | 1062 | If you wanted to delete your file in a cross-platform-friendly way, you could 1063 | use the ~babashka.fs/delete-if-exists~ function. To do that, you must require 1064 | the ~babashka.fs~ namespace. You might assume that you could update your 1065 | ~bb.edn~ to look like this and it would work, but it wouldn't: 1066 | 1067 | #+begin_src clojure 1068 | {:tasks {clear (do (require '[babashka.fs :as fs]) 1069 | (fs/delete-if-exists "entries.edn"))}} 1070 | #+end_src 1071 | 1072 | Instead, to require namespaces you must do so like this: 1073 | 1074 | #+begin_src clojure 1075 | {:tasks {:requires ([babashka.fs :as fs]) 1076 | clear (fs/delete-if-exists "entries.edn")}} 1077 | #+end_src 1078 | 1079 | ** Use ~exec~ to parse arguments and call a function 1080 | 1081 | We still want to be able to call ~bb add~ and ~bb list~. We have what we need to 1082 | implement ~bb list~; we can just update ~bb.edn~ to look like this: 1083 | 1084 | #+begin_src clojure 1085 | {:paths ["src"] 1086 | :tasks {:requires ([babashka.fs :as fs] 1087 | [journal.list :as list]) 1088 | clear (fs/delete-if-exists "entries.edn") 1089 | list (list/list-entries nil)}} 1090 | #+end_src 1091 | 1092 | In the previous task examples I excluded the ~:paths~ key because it wasn't 1093 | needed, but we need to bring it back so that Babashka can find ~journal.list~ on 1094 | the classpath. ~journal.list/list-entries~ takes one argument that gets ignored, 1095 | so we can just pass in ~nil~ and it works. 1096 | 1097 | ~journal.add/add-entries~, however, takes a Clojure map with an ~:entries~ key. 1098 | Thus we need some way of parsing the command line arguments into that map and then 1099 | passing that to ~journal.add/add-entries~. Babashka provides the ~exec~ function 1100 | for this. Update your ~bb.edn~ like so, and everything should work: 1101 | 1102 | #+begin_src clojure 1103 | {:paths ["src"] 1104 | :tasks {:requires ([babashka.fs :as fs] 1105 | [journal.list :as list]) 1106 | clear (fs/delete-if-exists "entries.edn") 1107 | list (list/list-entries nil) 1108 | add (exec 'journal.add/add-entry)}} 1109 | #+end_src 1110 | 1111 | Now we can call this, and it should work: 1112 | 1113 | #+begin_src bash 1114 | $ bb add --entry "dreamt I was done writing a tutorial. bliss" 1115 | 1116 | $ bb list 1117 | 1670718856173 1118 | dreamt I was done writing a tutorial. bliss 1119 | #+end_src 1120 | 1121 | The key here is the ~exec~ function. With ~(exec 'journal.add/add-entry)~, it's 1122 | as if you called this on the command line: 1123 | 1124 | #+begin_src bash 1125 | $ bb -x journal.add/add-entry --entry "dreamt I was done writing a tutorial. bliss" 1126 | #+end_src 1127 | 1128 | ~exec~ will parse command line arguments in the same way as ~bb -x~ does and 1129 | pass the result to the designated function, which is ~journal.add/add-entry~ in 1130 | this example. 1131 | 1132 | ** Task dependencies, parallel tasks, and more 1133 | 1134 | Babashka's task system has even more capabilities, which I'm not going to cover 1135 | in detail but which you can read about in the [[https://book.babashka.org/#tasks][Task runner section of the 1136 | Babashka docs]]. 1137 | 1138 | I do want to highlight two very useful features: /task dependencies/ and 1139 | /parallel task execution/. 1140 | 1141 | Babashka let's you define task dependencies, meaning that you can define 1142 | ~task-a~ to depend on ~task-b~ such that if you run ~bb task-a~, internally 1143 | ~task-b~ will be executed if needed. This is useful for creating compilation 1144 | scripts. If you were building a web app, for example, you might have separate 1145 | tasks for compiling a backend jar file and frontend javascript file. You could 1146 | have the tasks ~build-backend~, ~build-frontend~, and then have a ~build~ task 1147 | that depended on the other two. If you were to call ~bb build~, Babashka would 1148 | be able to determine which of the other two tasks needed to be run and only 1149 | run them when necessary. 1150 | 1151 | Parallel task execution will have Babashka running multiple tasks at the same 1152 | time. In our build example, ~bb build~ could run ~build-backend~ and 1153 | ~build-frontend~ at the same time, which could be a real time saver. 1154 | 1155 | ** Summary 1156 | 1157 | - You define tasks in ~bb.edn~ under the ~:tasks~ key 1158 | - Task definitions are key-value pairs where the key is a symbol naming the 1159 | task, and the value is a Clojure expression 1160 | - Add a ~:requires~ key under the ~:tasks~ key to require namespaces 1161 | - ~exec~ executes functions as if invoked with ~bb -x journal.add/add-entry~; it 1162 | parses command line args before passing to the function 1163 | - You can declare task dependencies 1164 | - You can run tasks in parallel 1165 | 1166 | * Additional Resources 1167 | 1168 | - [[https://github.com/babashka/babashka/wiki/Bash-and-Babashka-equivalents][Bash and Babashka equivalents]] is indispensable for transferring your Bash 1169 | knowledge to Babashka 1170 | 1171 | * Acknowledgments 1172 | The following people read drafts of this and gave feedback. Thank you! 1173 | 1174 | - Michiel Borkent @borkdude 1175 | - Marcela Poffalo 1176 | - Gabriel Horner @cldwalker 1177 | - @geraldodev 1178 | - Andrew Patrick @Ajpatri 1179 | - Alex Gravem @kartesus 1180 | - Inge Solvoll @ingesol 1181 | - @focaskater 1182 | - @monkey1@fosstodon.org 1183 | - Kira McLean 1184 | 1185 | * Feedback 1186 | 1187 | If you have feedback, please open an issue at 1188 | https://github.com/braveclojure/babooka. I can't promise I'll respond in a 1189 | timely manner, or even at all, so I apologize in advice! I'm just not great at 1190 | responding, it's one of my character flaws, but I appreciate the feedback! 1191 | 1192 | * COMMENT outline 1193 | ** What is babashka? 1194 | *** how it's meant to be used 1195 | *** implementation 1196 | ** Who should use it? 1197 | *** learning clojure 1198 | *** experienced clojure developers 1199 | *** people who work on the command line 1200 | ** Why should you use it? 1201 | *** fast learning tool 1202 | *** powerful of a real programming language 1203 | *** seamless multithreading 1204 | *** self-contained environment 1205 | *** task management 1206 | ** Installation 1207 | ** Your first script 1208 | *** writing your first script 1209 | *** invoking it 1210 | *** output 1211 | ** built-in facilities 1212 | ** IO 1213 | ** project organization 1214 | *** the library ecosystem 1215 | *** bb.edn 1216 | ** pods 1217 | ** tasks 1218 | ** feedback 1219 | *** DONE problem with ./journal list -e / :require true 1220 | -e is required for adding, but not for list 1221 | *** DONE add entry "Flying!!" messes up because of bash !! 1222 | *** DONE add a "who this is for" section at beginning 1223 | *** DONE becase 1224 | *** more consistently call out instructions 1225 | e.g. not "Create the file journal" 1226 | *** getting an entry ad ./journal add --entry "..." step 1227 | *** update acknowledgements 1228 | * COMMENT todo 1229 | ** DONE link to borkdude sponsors 1230 | ** DONE link to my sponsorship 1231 | ** DONE link to PDF 1232 | ** DONE instructions on getting feedback 1233 | ** DONE fix ~*command-line-args*~ 1234 | ** DONE remove gumroad link from sidebar 1235 | --------------------------------------------------------------------------------