babashka 2025-05-07

Can I capture output and also keep printing to *out* with babashka? I've got a CLI that prints to stderr something like a progress bar and then the output (dont ask why 😄 ) So I want to still be able to interactively keep printing the progress bar, but process the output as a string at the end.

do you mean you want to print the same output to stderr and stdout?

Here's an example I've got this cli

(ns cli)

(binding [*out* *err*]
  (println "0% Progress ..."))

(Thread/sleep 1000)

(binding [*out* *err*]
  (println "Final output"))
And I consume it like this
(ns test
  (:require
   [babashka.process :as bp]))

(->> (bp/shell {:out :string
                :err :out} "bb ./cli.clj")
     (:out)
     (println "Output"))
But, this means I'm waiting for the CLI to be finished. But I want to to keep printing the output from cli.clj and process the printed output after its finished

the issue here is buffering of the outputstream

what you could do is capture the output yourself in bb.cli and do whatever you want with it

let me fetch an example

Ok I think with that info about the outputstream buffer I've got it

(ns test
  (:require
   [babashka.process :as bp])
  (:import
   [java.io BufferedReader InputStreamReader]))

(let [content (atom [])]
  (with-open [err (:err (bp/process "bb ./cli.clj"))
              reader (BufferedReader. (InputStreamReader. err))]
    (loop [line (.readLine reader)]
      (when line
        (println "Error:" line)
        (swap! content conj line)
        (recur (.readLine reader)))))
  @content)

Is there an easier way for this?

so the proposed solution in the branch discussed in the comment is to add a :out-line-fn and :err-line-fn option. Perhaps we could also just pass a function to :out and :err

If you like that option, leave a comment in the issue

Nice will do thumbsup_all It's quite interesting, as I have to remove the clear screen instructions as well in the final result, otherwise the final print is offset weirdly

(ns test
  (:require
   [babashka.process :as bp]
   [clojure.string :as str])
  (:import
   [ BufferedReader InputStreamReader]))

(defn strip-ansi [s]
  (str/replace s #"\u001b\[[0-9;]*[JHm]" ""))

(let [content (atom [])]
  (with-open [err (:err (bp/process "bb ./cli.clj"))
              reader (BufferedReader. (InputStreamReader. err))]
    (loop [line (.readLine reader)]
      (when line
        (let [clean-line (strip-ansi line)]
          (println line)
          (when (not (str/blank? clean-line))
            (swap! content conj clean-line)))
        (recur (.readLine reader))))
    (println "FINAL CONTENT" @content)))
(ns cli)

(defn print-progress! [progress]
  (print "\u001b[2J") ; clear screen
  (print "\u001b[H")  ; move cursor to home position
  (println progress "% Progress ..."))


(binding [*out* *err*]
  (print-progress! 0)
  (Thread/sleep 1000)
  (print-progress! 50)
  (Thread/sleep 1000)
  (print-progress! 100))

(binding [*out* *err*]
  (println "Final output"))

interesting

What do you think about adding BreakIterator to babashka java classes? Useful to get "composed" character length of strings

(ns app
  (:import [java.text BreakIterator]))

(defn count-characters
  [^String text]
  (let [it (BreakIterator/getCharacterInstance)]
    (.setText it text)
    (loop [count 0]
      (if (= (.next it) BreakIterator/DONE)
        count
        (recur (inc count))))))

(comment
  (count-characters ":flag-ca:") ;; => 1
  (count ":flag-ca:") ;; => 4
  nil)
edit: Slacks keeps replacing the emojis 😬 I'm using "🇨🇦" in the comment block

Sweet! ❤️

you can install the new version with:

bash <(curl )  --dev-build --dir /tmp
once CI on master finishes (should be within 10 minutes or so)