diff --git a/README.md b/README.md index 0916e3a1..22a20201 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,9 @@ explicitly. - `sh` - `clojure.java.io` aliased as `io`: - `as-relative-path`, `copy`, `delete-file`, `file` +- [`clojure.core.async`](https://clojure.github.io/core.async/) aliased as + `async`. The `alt` and `go` macros are not available but `alts!!` does work as + it is a function. - [`me.raynes.conch.low-level`](https://github.com/clj-commons/conch#low-level-usage) aliased as `conch` @@ -164,6 +167,20 @@ Waits for TCP connection to be available on host and port. Options map supports `:timeout` and `:pause`. If `:timeout` is provided and reached, exception will be thrown. The `:pause` option determines the time waited between retries. +- `sig/pipe-signal-received?`. Usage: + +``` clojure +(sig/pipe-signal-received?) +``` + +Returns true if `PIPE` signal was received. Example: + +``` shellsession +$ bb '((fn [x] (println x) (when (not (sig/pipe-signal-received?)) (recur (inc x)))) 0)' | head -n2 +1 +2 +``` + ## Examples ``` shellsession @@ -308,6 +325,23 @@ $ bb ' (net/wait-for-it "localhost" 1777) (conch/destroy ws)' ``` +## Async + +Apart from `future` for creating threads and the `conch` namespace for creating +processes, you may use `core.async` to script asynchronously. The following +example shows how to get first available value from two different processes: + +``` clojure +bb ' +(defn async-command [& args] + (async/thread (apply shell/sh "bash" "-c" args))) + +(-> (async/alts!! [(async-command "sleep 2 && echo process 1") + (async-command "sleep 1 && echo process 2")]) + first :out str/trim println)' +process 2 +``` + ## Enabling SSL If you want to be able to use SSL to e.g. run `(slurp diff --git a/project.clj b/project.clj index a457e952..505fae96 100644 --- a/project.clj +++ b/project.clj @@ -9,7 +9,8 @@ :url "http://opensource.org/licenses/eclipse-1.0.php"} :source-paths ["src" "sci/src" "sci/inlined" "conch/src"] :resource-paths ["resources" "sci/resources"] - :dependencies [[org.clojure/clojure "1.10.1"]] + :dependencies [[org.clojure/clojure "1.10.1"] + [org.clojure/core.async "0.4.500"]] :profiles {:test {:dependencies [[clj-commons/conch "0.9.2"]]} :uberjar {:global-vars {*assert* false} :jvm-opts ["-Dclojure.compiler.direct-linking=true" diff --git a/src/babashka/impl/async.clj b/src/babashka/impl/async.clj new file mode 100644 index 00000000..5cdaab5f --- /dev/null +++ b/src/babashka/impl/async.clj @@ -0,0 +1,68 @@ +(ns babashka.impl.async + {:no-doc true} + (:require [clojure.core.async :as async])) + +(defn thread + [& body] + `(~'async/thread-call (fn [] ~@body))) + +(def async-bindings + {'async/!! async/>!! + 'async/admix async/admix + 'async/alts! async/alts! + 'async/alts!! async/alts!! + 'async/buffer async/buffer + 'async/chan async/chan + 'async/close! async/close! + 'async/do-alt async/do-alt + 'async/do-alts async/do-alts + 'async/dropping-buffer async/dropping-buffer + 'async/filter< async/filter< + 'async/filter> async/filter> + 'async/into async/into + 'async/map async/map + 'async/map< async/map< + 'async/map> async/map> + 'async/mapcat< async/mapcat< + 'async/mapcat> async/mapcat> + 'async/merge async/merge + 'async/mix async/mix + 'async/mult async/mult + 'async/offer! async/offer! + 'async/onto-chan async/onto-chan + 'async/partition async/partition + 'async/partition-by async/partition-by + 'async/pipe async/pipe + 'async/pipeline async/pipeline + 'async/pipeline-async async/pipeline-async + 'async/pipeline-blocking async/pipeline-blocking + 'async/poll! async/poll! + 'async/promise-chan async/promise-chan + 'async/pub async/pub + 'async/put! async/put! + 'async/reduce async/reduce + 'async/remove< async/remove< + 'async/remove> async/remove> + 'async/sliding-buffer async/sliding-buffer + 'async/solo-mode async/solo-mode + 'async/split async/split + 'async/sub async/sub + 'async/take async/take + 'async/take! async/take! + 'async/tap async/tap + 'async/thread (with-meta thread {:sci/macro true}) + 'async/thread-call async/thread-call + 'async/timeout async/timeout + 'async/to-chan async/to-chan + 'async/toggle async/toggle + 'async/transduce async/transduce + 'async/unblocking-buffer? async/unblocking-buffer? + 'async/unique async/unique + 'async/unmix async/unmix + 'async/unmix-all async/unmix-all + 'async/unsub async/unsub + 'async/unsub-all async/unsub-all + 'async/untap async/untap + 'async/untap-all async/untap-all}) + diff --git a/src/babashka/impl/clojure/core.clj b/src/babashka/impl/clojure/core.clj index eb41f32c..d9c5eea1 100644 --- a/src/babashka/impl/clojure/core.clj +++ b/src/babashka/impl/clojure/core.clj @@ -10,17 +10,35 @@ {;; atoms 'atom atom 'swap! swap! + 'swap-vals! swap-vals! 'reset! reset! 'add-watch add-watch - + 'future-call future-call + 'future (with-meta future {:sci/macro true}) + 'future-cancel future-cancel + 'future-cancelled? future-cancelled? + 'future-done? future-done? + 'future? future? + 'deref deref + 'agent agent + 'send send + 'send-off send-off + 'promise promise + 'deliver deliver + 'shutdown-agents shutdown-agents 'run! run! 'slurp slurp 'spit spit 'pmap pmap - 'print print + 'pr pr 'pr-str pr-str 'prn prn + 'prn-str prn-str + 'print-str print-str + 'print print 'println println - 'future-call future-call - 'future (with-meta future {:sci/macro true}) - 'deref deref}) + 'println-str println-str + 'flush flush + 'ex-info ex-info + 'ex-data ex-data + }) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 6067dedc..00063cb5 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -4,6 +4,7 @@ [babashka.impl.File :refer [file-bindings]] [babashka.impl.System :refer [system-bindings]] [babashka.impl.Thread :refer [thread-bindings]] + [babashka.impl.async :refer [async-bindings]] [babashka.impl.clojure.core :refer [core-bindings]] [babashka.impl.clojure.stacktrace :refer [print-stack-trace]] [babashka.impl.conch :refer [conch-bindings]] @@ -147,12 +148,14 @@ Everything after that is bound to *command-line-args*.")) 'io/file io/file 'io/reader io/reader 'edn/read-string edn/read-string - 'net/wait-for-it net/wait-for-it} + 'net/wait-for-it net/wait-for-it + 'sig/pipe-signal-received? pipe-signal-received?} core-bindings system-bindings file-bindings thread-bindings - conch-bindings)) + conch-bindings + async-bindings)) (defn read-edn [] (edn/read {;;:readers *data-readers* @@ -244,6 +247,7 @@ Everything after that is bound to *command-line-args*.")) t1 (System/currentTimeMillis)] (when time? (binding [*out* *err*] (println "bb took" (str (- t1 t0) "ms.")))) + (flush) exit-code)) (defn -main diff --git a/test/babashka/main_test.clj b/test/babashka/main_test.clj index a4398840..7dd24392 100644 --- a/test/babashka/main_test.clj +++ b/test/babashka/main_test.clj @@ -129,7 +129,7 @@ (let [tmp (java.io.File/createTempFile "script" ".clj")] (spit tmp "(defn foo [x y] (+ x y)) (defn bar [x y] (* x y))") (is (= "120\n" (test-utils/bb nil (format "(load-file \"%s\") (bar (foo 10 30) 3)" - (.getPath tmp))))))) + (.getPath tmp))))))) (deftest preloads-test ;; THIS TEST REQUIRES: @@ -176,3 +176,12 @@ (net/wait-for-it \"127.0.0.1\" 7171) (conch/destroy web-server) (net/wait-for-it \"localhost\" 7172 {:timeout 50})")))) + +(deftest async-test + (is (= "process 2\n" (test-utils/bb nil " + (defn async-command [& args] + (async/thread (apply shell/sh \"bash\" \"-c\" args))) + + (-> (async/alts!! [(async-command \"sleep 2 && echo process 1\") + (async-command \"sleep 1 && echo process 2\")]) + first :out str/trim println)"))))