diff --git a/README.md b/README.md index e6611cd9..4663a60e 100644 --- a/README.md +++ b/README.md @@ -137,25 +137,26 @@ You may also download a binary from [Github](https://github.com/borkdude/babashk Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose] [ ( --classpath | -cp ) ] [ ( --main | -m ) ] ( -e | -f | --repl | --socket-repl [:] ) - [ arg* ] + [ --uberscript ] [ arg* ] Options: - --help, -h or -? Print this help text. - --version Print the current version of babashka. - -i Bind *input* to a lazy seq of lines from stdin. - -I Bind *input* to a lazy seq of EDN values from stdin. - -o Write lines to stdout. - -O Write EDN values to stdout. - --verbose Print entire stacktrace in case of exception. - --stream Stream over lines or EDN values from stdin. Combined with -i or -I *input* becomes a single value per iteration. - -e, --eval Evaluate an expression. - -f, --file Evaluate a file. - -cp, --classpath Classpath to use. - -m, --main Call the -main function from namespace with args. - --repl Start REPL - --socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). - --time Print execution time before exiting. + --help, -h or -? Print this help text. + --version Print the current version of babashka. + -i Bind *input* to a lazy seq of lines from stdin. + -I Bind *input* to a lazy seq of EDN values from stdin. + -o Write lines to stdout. + -O Write EDN values to stdout. + --verbose Print entire stacktrace in case of exception. + --stream Stream over lines or EDN values from stdin. Combined with -i or -I *input* becomes a single value per iteration. + -e, --eval Evaluate an expression. + -f, --file Evaluate a file. + -cp, --classpath Classpath to use. + -m, --main Call the -main function from namespace with args. + --uberscript Collect preloads, -e, -f and -m and all required namespaces from the classpath into a single executable file. + --repl Start REPL + --socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). + --time Print execution time before exiting. If neither -e, -f, or --socket-repl are specified, then the first argument that is not parsed as a option is treated as a file if it exists, or as an expression otherwise. Everything after that is bound to *command-line-args*. @@ -408,10 +409,10 @@ Note that you can use the `clojure` tool to produce classpaths and download depe ``` shellsession $ cat deps.edn {:deps - {my_gist_script - {:git/url "https://gist.github.com/borkdude/263b150607f3ce03630e114611a4ef42" - :sha "cfc761d06dfb30bb77166b45d439fe8fe54a31b8"}}} - + {my_gist_script + {:git/url "https://gist.github.com/borkdude/263b150607f3ce03630e114611a4ef42" + :sha "cfc761d06dfb30bb77166b45d439fe8fe54a31b8"}} + :aliases {:my-script {:main-opts ["-m" "my-gist-script"]}}} $ CLASSPATH=$(clojure -Spath) $ bb --classpath "$CLASSPATH" --main my-gist-script @@ -428,6 +429,36 @@ $ bb "(my-gist-script/-main)" Hello from gist script! ``` +Using the [deps.clj](https://github.com/borkdude/deps.clj/) script, you can also +pass the classpath and main opts to `bb`: + +``` shell +$ deps.clj -A:my-script -Scommand "bb my_script.clj {{main-opts}}" +Hello from gist script! +``` + +## Uberscript + +The `--uberscript ` option collects the expressions in +`BABASHKA_PRELOADS`, the command line expression or file, the main entrypoint +and all required namespaces from the classpath into a single file. This can be +convenient for debugging purposes and deployment. + +Given the `deps.edn` from above: + +``` shell +$ deps.clj -A:my-script -Scommand "bb {{main-opts}} --uberscript my-script.clj" + +$ cat my-script.clj +(ns my-gist-script) +(defn -main [& args] + (println "Hello from gist script!"))(require '[my-gist-script]) +(ns user (:require [my-gist-script])) (apply my-gist-script/-main *command-line-args*)% + +$ bb my-script.clj +Hello from gist script! +``` + ## Parsing command line arguments Babashka ships with `clojure.tools.cli`: diff --git a/resources/BABASHKA_VERSION b/resources/BABASHKA_VERSION index 38c0191e..ba4921ce 100644 --- a/resources/BABASHKA_VERSION +++ b/resources/BABASHKA_VERSION @@ -1 +1 @@ -0.0.51-SNAPSHOT \ No newline at end of file +0.0.51 \ No newline at end of file diff --git a/sci b/sci index 79563a0c..92f2251f 160000 --- a/sci +++ b/sci @@ -1 +1 @@ -Subproject commit 79563a0c94daa11e56e0969c963a52af9f7ee00f +Subproject commit 92f2251fcc2ca795db05e78605e1c78bb8462884 diff --git a/src/babashka/impl/utils.clj b/src/babashka/impl/utils.clj deleted file mode 100644 index 8000e5ac..00000000 --- a/src/babashka/impl/utils.clj +++ /dev/null @@ -1,12 +0,0 @@ -(ns babashka.impl.utils - {:no-doc true} - (:require - [sci.impl.vars :as vars] - [sci.core :as sci])) - -(sci.impl.vars/bindRoot sci/in *in*) -(sci.impl.vars/bindRoot sci/out *out*) -(sci.impl.vars/bindRoot sci/err *err*) - -(defn eval-string [expr ctx] - (sci/eval-string expr ctx)) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 51e9b983..7f34a985 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -13,7 +13,6 @@ [babashka.impl.repl :as repl] [babashka.impl.socket-repl :as socket-repl] [babashka.impl.tools.cli :refer [tools-cli-namespace]] - [babashka.impl.utils :refer [eval-string]] [babashka.wait :as wait] [clojure.edn :as edn] [clojure.java.io :as io] @@ -26,6 +25,10 @@ [sci.impl.interpreter :refer [eval-string*]]) (:gen-class)) +(sci.impl.vars/bindRoot sci/in *in*) +(sci.impl.vars/bindRoot sci/out *out*) +(sci.impl.vars/bindRoot sci/err *err*) + (set! *warn-on-reflection* true) ;; To detect problems when generating the image, run: ;; echo '1' | java -agentlib:native-image-agent=config-output-dir=/tmp -jar target/babashka-xxx-standalone.jar '...' @@ -68,6 +71,15 @@ (assoc opts-map :edn-in true :edn-out true)) + ("--classpath", "-cp") + (let [options (next options)] + (recur (next options) + (assoc opts-map :classpath (first options)))) + ("--uberscript") + (let [options (next options)] + (recur (next options) + (assoc opts-map + :uberscript (first options)))) ("-f" "--file") (let [options (next options)] (recur (next options) @@ -87,10 +99,6 @@ (let [options (next options)] (recur (next options) (assoc opts-map :expression (first options)))) - ("--classpath", "-cp") - (let [options (next options)] - (recur (next options) - (assoc opts-map :classpath (first options)))) ("--main", "-m") (let [options (next options)] (recur (next options) @@ -128,7 +136,7 @@ (def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose] [ ( --classpath | -cp ) ] [ ( --main | -m ) ] ( -e | -f | --repl | --socket-repl [:] ) - [ arg* ]") + [ --uberscript ] [ arg* ]") (defn print-usage [] (println usage-string)) @@ -140,21 +148,22 @@ (println) (println "Options:") (println " - --help, -h or -? Print this help text. - --version Print the current version of babashka. - -i Bind *input* to a lazy seq of lines from stdin. - -I Bind *input* to a lazy seq of EDN values from stdin. - -o Write lines to stdout. - -O Write EDN values to stdout. - --verbose Print entire stacktrace in case of exception. - --stream Stream over lines or EDN values from stdin. Combined with -i or -I *input* becomes a single value per iteration. - -e, --eval Evaluate an expression. - -f, --file Evaluate a file. - -cp, --classpath Classpath to use. - -m, --main Call the -main function from namespace with args. - --repl Start REPL - --socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). - --time Print execution time before exiting. + --help, -h or -? Print this help text. + --version Print the current version of babashka. + -i Bind *input* to a lazy seq of lines from stdin. + -I Bind *input* to a lazy seq of EDN values from stdin. + -o Write lines to stdout. + -O Write EDN values to stdout. + --verbose Print entire stacktrace in case of exception. + --stream Stream over lines or EDN values from stdin. Combined with -i or -I *input* becomes a single value per iteration. + -e, --eval Evaluate an expression. + -f, --file Evaluate a file. + -cp, --classpath Classpath to use. + -m, --main Call the -main function from namespace with args. + --uberscript Collect preloads, -e, -f and -m and all required namespaces from the classpath into a single executable file. + --repl Start REPL + --socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). + --time Print execution time before exiting. If neither -e, -f, or --socket-repl are specified, then the first argument that is not parsed as a option is treated as a file if it exists, or as an expression otherwise. Everything after that is bound to *command-line-args*.")) @@ -173,14 +182,14 @@ Everything after that is bound to *command-line-args*.")) (def reflection-var (sci/new-dynamic-var '*warn-on-reflection* false)) -(defn load-file* [ctx f] +(defn load-file* [sci-ctx f] (let [f (io/file f) s (slurp f)] (sci/with-bindings {vars/file-var (.getCanonicalPath f)} - (eval-string s ctx)))) + (eval-string* sci-ctx s)))) -(defn eval* [ctx form] - (eval-string (pr-str form) ctx)) +(defn eval* [sci-ctx form] + (eval-string* sci-ctx (pr-str form))) (defn start-socket-repl! [address ctx] (socket-repl/start-repl! address ctx) @@ -194,6 +203,45 @@ Everything after that is bound to *command-line-args*.")) ;; (sci/set-var-root! sci/*out* *out*) ;; (sci/set-var-root! sci/*err* *err*) +(def aliases + '{tools.cli 'clojure.tools.cli + edn clojure.edn + wait babashka.wait + sig babashka.signal + shell clojure.java.shell + io clojure.java.io + async clojure.core.async + csv clojure.data.csv + json cheshire.core}) + +(def namespaces + {'clojure.tools.cli tools-cli-namespace + 'clojure.edn {'read edn/read + 'read-string edn/read-string} + 'clojure.java.shell {'sh shell/sh} + 'babashka.wait {'wait-for-port wait/wait-for-port + 'wait-for-path wait/wait-for-path} + 'babashka.signal {'pipe-signal-received? pipe-signal-received?} + 'clojure.java.io io-namespace + 'clojure.core.async async-namespace + 'clojure.data.csv csv/csv-namespace + 'cheshire.core cheshire-core-namespace}) + +(def bindings + {'java.lang.System/exit exit ;; override exit, so we have more control + 'System/exit exit}) + +(defn error-handler* [^Exception e verbose?] + (binding [*out* *err*] + (let [d (ex-data e) + exit-code (:bb/exit-code d)] + (if exit-code [nil exit-code] + (do (if verbose? + (print-stack-trace e) + (println (.getMessage e))) + (flush) + [nil 1]))))) + (defn main [& args] (handle-pipe!) @@ -205,7 +253,7 @@ Everything after that is bound to *command-line-args*.")) :expression :stream? :time? :repl :socket-repl :verbose? :classpath - :main] :as _opts} + :main :uberscript] :as _opts} (parse-opts args) read-next (fn [*in*] (if (pipe-signal-received?) @@ -219,6 +267,7 @@ Everything after that is bound to *command-line-args*.")) (edn-seq *in*) :else (edn/read *in*)))))) + uberscript-sources (atom ()) env (atom {}) classpath (or classpath (System/getenv "BABASHKA_CLASSPATH")) @@ -226,35 +275,18 @@ Everything after that is bound to *command-line-args*.")) (cp/loader classpath)) load-fn (when classpath (fn [{:keys [:namespace]}] - (cp/source-for-namespace loader namespace))) + (let [res (cp/source-for-namespace loader namespace)] + (when uberscript (swap! uberscript-sources conj (:source res))) + res))) _ (when file (vars/bindRoot vars/file-var (.getCanonicalPath (io/file file)))) - ctx {:aliases '{tools.cli 'clojure.tools.cli - edn clojure.edn - wait babashka.wait - sig babashka.signal - shell clojure.java.shell - io clojure.java.io - async clojure.core.async - csv clojure.data.csv - json cheshire.core} - :namespaces {'clojure.core (assoc core-extras - '*command-line-args* - (sci/new-dynamic-var '*command-line-args* command-line-args) - '*file* vars/file-var - '*warn-on-reflection* reflection-var) - 'clojure.tools.cli tools-cli-namespace - 'clojure.edn {'read edn/read - 'read-string edn/read-string} - 'clojure.java.shell {'sh shell/sh} - 'babashka.wait {'wait-for-port wait/wait-for-port - 'wait-for-path wait/wait-for-path} - 'babashka.signal {'pipe-signal-received? pipe-signal-received?} - 'clojure.java.io io-namespace - 'clojure.core.async async-namespace - 'clojure.data.csv csv/csv-namespace - 'cheshire.core cheshire-core-namespace} - :bindings {'java.lang.System/exit exit ;; override exit, so we have more control - 'System/exit exit} + ctx {:aliases aliases + :namespaces (assoc namespaces 'clojure.core + (assoc core-extras + '*command-line-args* + (sci/new-dynamic-var '*command-line-args* command-line-args) + '*file* vars/file-var + '*warn-on-reflection* reflection-var)) + :bindings bindings :env env :features #{:bb} :classes classes/class-map @@ -271,37 +303,47 @@ Everything after that is bound to *command-line-args*.")) String java.lang.String System java.lang.System Thread java.lang.Thread} - :load-fn load-fn} - ctx (update-in ctx [:namespaces 'clojure.core] assoc - 'eval #(eval* ctx %) - 'load-file #(load-file* ctx %)) - ctx (assoc-in ctx [:namespaces 'clojure.main 'repl] - (fn [& opts] - (let [opts (apply hash-map opts)] - (repl/start-repl! ctx opts)))) + :load-fn load-fn + :dry-run uberscript} ctx (addons/future ctx) - _preloads (some-> (System/getenv "BABASHKA_PRELOADS") (str/trim) (eval-string ctx)) - expression (if main - (format "(ns user (:require [%1$s])) (apply %1$s/-main *command-line-args*)" - main) - expression) sci-ctx (sci-opts/init ctx) + _ (swap! (:env sci-ctx) + (fn [env] + (update-in env [:namespaces 'clojure.core] assoc + 'eval #(eval* sci-ctx %) + 'load-file #(load-file* sci-ctx %)))) + _ (swap! (:env sci-ctx) + (fn [env] + (assoc-in env [:namespaces 'clojure.main 'repl] + (fn [& opts] + (let [opts (apply hash-map opts)] + (repl/start-repl! sci-ctx opts)))))) + preloads (some-> (System/getenv "BABASHKA_PRELOADS") (str/trim)) + [expression exit-code] + (cond expression [expression nil] + main [(format "(ns user (:require [%1$s])) (apply %1$s/-main *command-line-args*)" + main) nil] + file (try [(read-file file) nil] + (catch Exception e + (error-handler* e verbose?)))) + expression (str (when preloads + (str preloads "\n")) + expression) exit-code - (sci/with-bindings {reflection-var false} - (or - #_(binding [*out* *err*] - (prn ">>" _opts)) - (second - (cond version - [(print-version) 0] - help? - [(print-help) 0] - repl [(repl/start-repl! sci-ctx) 0] - socket-repl [(start-socket-repl! socket-repl sci-ctx) 0] - :else - (try - (let [ expr (if file (read-file file) expression)] - (if expr + (or exit-code + (sci/with-bindings {reflection-var false} + (or + #_(binding [*out* *err*] + (prn ">>" _opts)) + (second + (cond version + [(print-version) 0] + help? + [(print-help) 0] + repl [(repl/start-repl! sci-ctx) 0] + socket-repl [(start-socket-repl! socket-repl sci-ctx) 0] + expression + (try (loop [in (read-next *in*)] (let [_ (swap! env update-in [:namespaces 'user] assoc (with-meta '*input* @@ -310,7 +352,7 @@ Everything after that is bound to *command-line-args*.")) (sci/new-dynamic-var '*input* in))] (if (identical? ::EOF in) [nil 0] ;; done streaming - (let [res [(let [res (eval-string* sci-ctx expr)] + (let [res [(let [res (eval-string* sci-ctx expression)] (when (some? res) (if-let [pr-f (cond shell-out println edn-out prn)] @@ -323,22 +365,22 @@ Everything after that is bound to *command-line-args*.")) (if stream? (recur (read-next *in*)) res))))) - [(repl/start-repl! sci-ctx) 0])) - (catch Throwable e - (binding [*out* *err*] - (let [d (ex-data e) - exit-code (:bb/exit-code d)] - (if exit-code [nil exit-code] - (do (if verbose? - (print-stack-trace e) - (println (.getMessage e))) - (flush) - [nil 1])))))))) - 1)) + (catch Throwable e + (error-handler* e verbose?))) + :else [(repl/start-repl! sci-ctx) 0])) + 1))) t1 (System/currentTimeMillis)] + (flush) + (when uberscript + uberscript + (let [uberscript-out uberscript] + (spit uberscript-out "") ;; reset file + (doseq [s @uberscript-sources] + (spit uberscript-out s :append true)) + (spit uberscript-out expression :append true) + (spit uberscript-out file :append true))) (when time? (binding [*out* *err*] (println "bb took" (str (- t1 t0) "ms.")))) - (flush) exit-code)) (defn -main diff --git a/test/babashka/classpath_test.clj b/test/babashka/classpath_test.clj index 06117332..24048ade 100644 --- a/test/babashka/classpath_test.clj +++ b/test/babashka/classpath_test.clj @@ -26,6 +26,13 @@ (is (= "(\"1\" \"2\" \"3\" \"4\")\n" (tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" "-m" "my.main" "1" "2" "3" "4")))) +(deftest uberscript-test + (let [tmp-file (java.io.File/createTempFile "uberscript" ".clj")] + (.deleteOnExit tmp-file) + (tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" "-m" "my.main" "--uberscript" (.getPath tmp-file)) + (is (= "(\"1\" \"2\" \"3\" \"4\")\n" + (tu/bb nil "--file" (.getPath tmp-file) "1" "2" "3" "4"))))) + (deftest error-while-loading-test (is (true? (bb nil "--classpath" "test-resources/babashka/src_for_classpath_test"