diff --git a/.circleci/script/release b/.circleci/script/release index fa085248..2d32f514 100755 --- a/.circleci/script/release +++ b/.circleci/script/release @@ -3,14 +3,16 @@ rm -rf /tmp/release mkdir -p /tmp/release cp bb /tmp/release +# cp src-bash/bbk /tmp/release + VERSION=$(cat resources/BABASHKA_VERSION) cd /tmp/release ## release binary as zip archive -zip "babashka-$VERSION-$BABASHKA_PLATFORM-amd64.zip" bb +zip "babashka-$VERSION-$BABASHKA_PLATFORM-amd64.zip" bb # bbk ## cleanup -rm bb +rm bb # bbk diff --git a/.gitignore b/.gitignore index 2a556140..a96a9d65 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ pom.xml.asc /bb .clj-kondo/.cache !java/src/babashka/impl/LockFix.class +!test-resources/babashka/src_for_classpath_test/foo.jar +.cpcache +deps.edn diff --git a/README.md b/README.md index 7f52bcaf..65771518 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![CircleCI](https://circleci.com/gh/borkdude/babashka/tree/master.svg?style=shield)](https://circleci.com/gh/borkdude/babashka/tree/master) [![Clojars Project](https://img.shields.io/clojars/v/borkdude/babashka.svg)](https://clojars.org/borkdude/babashka) [![cljdoc badge](https://cljdoc.org/badge/borkdude/babashka)](https://cljdoc.org/d/borkdude/babashka/CURRENT) +[![project chat](https://img.shields.io/badge/slack-join_chat-brightgreen.svg)](https://app.slack.com/client/T03RZGPFR/CLX41ASCS) + A Clojure [babushka](https://en.wikipedia.org/wiki/Headscarf) for the grey areas of Bash. @@ -125,22 +127,28 @@ You may also download a binary from [Github](https://github.com/borkdude/babashk ## Usage ``` shellsession -Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] ( -e | -f | --socket-repl [:] ) +Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose] + [ ( --classpath | -cp ) ] [ ( --main | -m ) ] + ( -e | -f | --repl | --socket-repl [:] ) + [ arg* ] Options: - --help, -h or -?: print this help text. - --version: print the current version of babashka. - - -i: bind *in* to a lazy seq of lines from stdin. - -I: bind *in* to a lazy seq of EDN values from stdin. - -o: write lines to stdout. - -O: write EDN values to stdout. - --stream: stream over lines or EDN values from stdin. Combined with -i or -I *in* becomes a single value per iteration. - -e, --eval : evaluate an expression - -f, --file : evaluate a file - --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 *in* to a lazy seq of lines from stdin. + -I Bind *in* 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 *in* 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. 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*. @@ -149,8 +157,9 @@ Everything after that is bound to *command-line-args*. The `clojure.core` functions are accessible without a namespace alias. The following namespaces are required by default and available through the -pre-defined aliases. You may use `require` + `:as` and/or `:refer` on these -namespaces. If not all vars are available, they are enumerated explicitly. +pre-defined aliases in the `user` namespace. You may use `require` + `:as` +and/or `:refer` on these namespaces. If not all vars are available, they are +enumerated explicitly. - `clojure.string` aliased as `str` - `clojure.set` aliased as `set` @@ -167,6 +176,7 @@ namespaces. If not all vars are available, they are enumerated explicitly. aliased as `conch` - [`clojure.tools.cli`](https://github.com/clojure/tools.cli) aliased as `tools.cli` - [`clojure.data.csv`](https://github.com/clojure/data.csv) aliased as `csv` +- [`cheshire.core`](https://github.com/dakrone/cheshire) aliased as `json` The following Java classes are available: @@ -305,6 +315,72 @@ $ cat script.clj ("hello" "1" "2" "3") ``` +## Preloads + +The environment variable `BABASHKA_PRELOADS` allows to define code that will be +available in all subsequent usages of babashka. + +``` shellsession +BABASHKA_PRELOADS='(defn foo [x] (+ x 2))' +BABASHKA_PRELOADS=$BABASHKA_PRELOADS' (defn bar [x] (* x 2))' +export BABASHKA_PRELOADS +``` + +Note that you can concatenate multiple expressions. Now you can use these functions in babashka: + +``` shellsession +$ bb '(-> (foo *in*) bar)' <<< 1 +6 +``` + +You can also preload an entire file using `load-file`: + +``` shellsession +export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")' +``` + +Note that `*in*` is not available in preloads. + +## Classpath + +Babashka accepts a `--classpath` option that will be used to search for +namespaces and load them: + +``` clojure +$ cat src/my/namespace.clj +(ns my.namespace) +(defn -main [& _args] + (println "Hello from my namespace!")) + +$ bb --classpath src --main my.namespace +Hello from my namespace! +``` + +Note that you can use the `clojure` tool to produce classpaths and download dependencies: + +``` shellsession +$ cat deps.edn +{:deps + {my_gist_script + {:git/url "https://gist.github.com/borkdude/263b150607f3ce03630e114611a4ef42" + :sha "cfc761d06dfb30bb77166b45d439fe8fe54a31b8"}}} + + +$ CLASSPATH=$(clojure -Spath) +$ bb --classpath "$CLASSPATH" --main my-gist-script +Hello from gist script! +``` + +If there is no `--classpath` argument, the `BABASHKA_CLASSPATH` environment +variable will be used: + +``` shellsession +$ export BABASHKA_CLASSPATH=$(clojure -Spath) +$ export BABASHKA_PRELOADS="(require '[my-gist-script])" +$ bb "(my-gist-script/-main)" +Hello from gist script! +``` + ## Parsing command line arguments Babashka ships with `clojure.tools.cli`: @@ -342,32 +418,6 @@ $ ./bb example.clj babashka doesn't support in-ns yet! ``` -## Preloads - -The environment variable `BABASHKA_PRELOADS` allows to define code that will be -available in all subsequent usages of babashka. - -``` shellsession -BABASHKA_PRELOADS='(defn foo [x] (+ x 2))' -BABASHKA_PRELOADS=$BABASHKA_PRELOADS' (defn bar [x] (* x 2))' -export BABASHKA_PRELOADS -``` - -Note that you can concatenate multiple expressions. Now you can use these functions in babashka: - -``` shellsession -$ bb '(-> (foo *in*) bar)' <<< 1 -6 -``` - -You can also preload an entire file using `load-file`: - -``` shellsession -export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")' -``` - -Note that `*in*` is not available in preloads. - ## Socket REPL Start the socket REPL like this: @@ -442,16 +492,8 @@ Differences with Clojure: - A subset of Java classes are supported. -- Only the `clojure.core`, `clojure.set` and `clojure.string` namespaces are - available from Clojure. - -- There is no classpath and no support for loading code from Maven/Clojars - dependencies. However, you can use `load-file` to load external code from - disk. - -- `require` does not load files; it only provides a way to create different - aliases for included namespaces, which makes it easier to make scripts - portable between the JVM and babashka. +- Only the `clojure.core`, `clojure.set`, `clojure.string` and `clojure.walk` + namespaces are available from Clojure. - Interpretation comes with overhead. Therefore tight loops are likely slower than in Clojure on the JVM. @@ -588,7 +630,7 @@ less ### Portable tree command -See [examples/tree.clj](https://github.com/borkdude/babashka/blob/8afb87142e0e4da8b6f912cfd7daf9c30b805ab3/examples/tree.clj). +See [examples/tree.clj](https://github.com/borkdude/babashka/blob/master/examples/tree.clj). ``` shellsession $ clojure -Sdeps '{:deps {org.clojure/tools.cli {:mvn/version "0.4.2"}}}' examples/tree.clj src @@ -605,6 +647,22 @@ src ├── impl │ ├── tools │ │ └── cli.clj +... +``` + +### List outdated maven dependencies + +See [examples/outdated.clj](https://github.com/borkdude/babashka/blob/master/examples/outdated.clj). +Inspired by an idea from [@seancorfield](https://github.com/seancorfield). + +``` shellsession +$ cat /tmp/deps.edn +{:deps {cheshire {:mvn/version "5.8.1"} + clj-http {:mvn/version "3.4.0"}}} + +$ examples/outdated.clj /tmp/deps.edn +clj-http/clj-http can be upgraded from 3.4.0 to 3.10.0 +cheshire/cheshire can be upgraded from 5.8.1 to 5.9.0 ``` ## Thanks diff --git a/examples/outdated.clj b/examples/outdated.clj new file mode 100755 index 00000000..1dfa7eef --- /dev/null +++ b/examples/outdated.clj @@ -0,0 +1,34 @@ +#!/usr/bin/env bb + +(require '[clojure.edn :as edn]) +(require '[clojure.java.shell :refer [sh]]) +(require '[clojure.string :as str]) + +(def deps (-> (slurp (or (first *command-line-args*) + "deps.edn")) + edn/read-string + :deps)) +(def with-release (zipmap (keys deps) + (map #(assoc % :mvn/version "RELEASE") + (vals deps)))) + +(defn deps->versions [deps] + (let [res (sh "clojure" "-Sdeps" (str {:deps deps}) "-Stree") + tree (:out res) + lines (str/split tree #"\n") + top-level (remove #(str/starts-with? % " ") lines) + deps (map #(str/split % #" ") top-level)] + (reduce (fn [acc [dep version]] + (assoc acc dep version)) + {} + deps))) + +(def version-map (deps->versions deps)) +(def new-version-map (deps->versions with-release)) + +(doseq [[dep version] version-map + :let [new-version (get new-version-map dep)] + :when (not= version new-version)] + (println dep "can be upgraded from" version "to" new-version)) + +;; Inspired by an idea from @seancorfield on Clojurians Slack diff --git a/project.clj b/project.clj index 331dcd97..d12e4652 100644 --- a/project.clj +++ b/project.clj @@ -11,10 +11,11 @@ :resource-paths ["resources" "sci/resources"] :dependencies [[org.clojure/clojure "1.10.1"] [org.clojure/tools.reader "1.3.2"] - [borkdude/edamame "0.0.9-alpha.2"] + [borkdude/edamame "0.0.10-alpha.2"] [org.clojure/core.async "0.4.500"] [org.clojure/tools.cli "0.4.2"] [org.clojure/data.csv "0.1.4"] + [cheshire "5.9.0"] [io.aviso/pretty "0.1.37"]] :profiles {:test {:dependencies [[clj-commons/conch "0.9.2"]]} :uberjar {:global-vars {*assert* false} diff --git a/reflection.json b/reflection.json index f7253b1e..a41c3fcd 100644 --- a/reflection.json +++ b/reflection.json @@ -25,7 +25,9 @@ }, { "name":"java.io.BufferedReader", - "allPublicMethods":true + "allPublicMethods":true, + "allPublicFields": true, + "allPublicConstructors": true }, { "name": "java.lang.Class", @@ -122,6 +124,24 @@ "name":"java.lang.UNIXProcess", "allPublicMethods":true }, + { + "name":"java.nio.file.attribute.FileAttribute", + "allPublicMethods":true, + "allPublicFields": true, + "allPublicConstructors": true + }, + { + "name":"java.nio.file.attribute.PosixFilePermission", + "allPublicMethods":true, + "allPublicFields": true, + "allPublicConstructors": true + }, + { + "name":"java.nio.file.attribute.PosixFilePermissions", + "allPublicMethods":true, + "allPublicFields": true, + "allPublicConstructors": true + }, { "name":"java.nio.file.Path", "allPublicMethods":true diff --git a/resources/BABASHKA_RELEASED_VERSION b/resources/BABASHKA_RELEASED_VERSION index 1435d6cf..54a00221 100644 --- a/resources/BABASHKA_RELEASED_VERSION +++ b/resources/BABASHKA_RELEASED_VERSION @@ -1 +1 @@ -0.0.37 +0.0.42 diff --git a/resources/BABASHKA_VERSION b/resources/BABASHKA_VERSION index 0d2182d0..382bf853 100644 --- a/resources/BABASHKA_VERSION +++ b/resources/BABASHKA_VERSION @@ -1 +1 @@ -0.0.38-SNAPSHOT +0.0.43-SNAPSHOT diff --git a/sci b/sci index 9ab0b1d1..07d28ee5 160000 --- a/sci +++ b/sci @@ -1 +1 @@ -Subproject commit 9ab0b1d181605e4c78eb8a90a75982df5c822514 +Subproject commit 07d28ee572e90a629e01b10aa5b98cb33ccdc1e5 diff --git a/script/test b/script/test index e631756a..58f31d5f 100755 --- a/script/test +++ b/script/test @@ -1,6 +1,15 @@ #!/usr/bin/env bash set -eo pipefail -export BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")' +BABASHKA_PRELOADS="" +BABASHKA_CLASSPATH="" +lein test "$@" -lein test +BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")' +BABASHKA_PRELOADS_TEST=true +lein test :only babashka.main-test/preloads-test + +BABASHKA_PRELOADS="(require '[env-ns])" +BABASHKA_CLASSPATH_TEST=true +BABASHKA_CLASSPATH="test-resources/babashka/src_for_classpath_test/env" +lein test :only babashka.classpath-test/classpath-env-test diff --git a/src-bash/bbk b/src-bash/bbk new file mode 100755 index 00000000..38b6c36e --- /dev/null +++ b/src-bash/bbk @@ -0,0 +1,219 @@ +#!/usr/bin/env bash + +set -e + +function join { local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; } + +# Extract opts +print_classpath=false +describe=false +verbose=false +force=false +repro=false +tree=false +pom=false +resolve_tags=false +help=false +resolve_aliases=() +classpath_aliases=() +main_aliases=() +all_aliases=() +while [ $# -gt 0 ] +do + case "$1" in + -J*) + shift + ;; + -R*) + resolve_aliases+=("${1:2}") + shift + ;; + -C*) + classpath_aliases+=("${1:2}") + shift + ;; + -O*) + shift + ;; + -M*) + main_aliases+=("${1:2}") + shift + ;; + -A*) + all_aliases+=("${1:2}") + shift + ;; + -Sdeps) + shift + deps_data="${1}" + shift + ;; + -Scp) + shift + force_cp="${1}" + shift + ;; + -Spath) + print_classpath=true + shift + ;; + -Sverbose) + verbose=true + shift + ;; + -Sdescribe) + describe=true + shift + ;; + -Sforce) + force=true + shift + ;; + -Srepro) + repro=true + shift + ;; + -Stree) + tree=true + shift + ;; + -Spom) + pom=true + shift + ;; + -Sresolve-tags) + resolve_tags=true + shift + ;; + -S*) + echo "Invalid option: $1" + exit 1 + ;; + -h|--help|"-?") + if [[ ${#main_aliases[@]} -gt 0 ]] || [[ ${#all_aliases[@]} -gt 0 ]]; then + break + else + help=true + shift + fi + ;; + *) + break + ;; + esac +done + +# Find clojure executable +set +e +CLOJURE_CMD=$(type -p clojure) +set -e +if [[ ! -n "$CLOJURE_CMD" ]]; then + >&2 echo "Couldn't find 'clojure'." + >&2 echo "You can launch Babashka directly using 'bb'." + >&2 echo "To use 'bbk', please ensure 'clojure' is installed and on" + >&2 echo "your path. See https://clojure.org/guides/getting_started" + exit 1 +fi + +if "$help"; then + cat <<-END +Usage: bbk [dep-opt*] [bb-opt*] [arg*] + +The bbk script is a runner for Babashka which ultimately constructs and +invokes a command-line of the form: + +bb --classpath classpath [bb-opt*] [*args] + + The dep-opts are used to build the classpath using the clojure tool: + -Ralias... Concatenated resolve-deps aliases, ex: -R:bench:1.9 + -Calias... Concatenated make-classpath aliases, ex: -C:dev + -Malias... Concatenated main option aliases, ex: -M:test + -Aalias... Concatenated aliases of any kind, ex: -A:dev:mem + -Sdeps EDN Deps data to use as the final deps file + -Spath Compute classpath and echo to stdout only + -Scp CP Do NOT compute or cache classpath, use this one instead + -Srepro Ignore the ~/.clojure/deps.edn config file + -Sforce Force recomputation of the classpath (don't use the cache) + -Spom Generate (or update existing) pom.xml with deps and paths + -Stree Print dependency tree + -Sresolve-tags Resolve git coordinate tags to shas and update deps.edn + -Sverbose Print important path info to console + -Sdescribe Print environment and command parsing info as data + + Additionally, for compatibility with clojure, -Jopt and -Oalias... dep-opts + are accepted but ignored. + +Babashka options: +END + bb -h | tail -n +9 + exit 0 +fi + +# Execute resolve-tags command +if "$resolve_tags"; then + "$CLOJURE_CMD" -Sresolve-tags + exit +fi + +clojure_args=() +if [[ -n "$deps_data" ]]; then + clojure_args+=("-Sdeps" "$deps_data") +fi +if [[ ${#resolve_aliases[@]} -gt 0 ]]; then + clojure_args+=("-R$(join '' ${resolve_aliases[@]})") +fi +if [[ ${#classpath_aliases[@]} -gt 0 ]]; then + clojure_args+=("-C$(join '' ${classpath_aliases[@]})") +fi +if [[ ${#main_aliases[@]} -gt 0 ]]; then + clojure_args+=("-M$(join '' ${main_aliases[@]})") +fi +if [[ ${#all_aliases[@]} -gt 0 ]]; then + clojure_args+=("-A$(join '' ${all_aliases[@]})") +fi +if "$repro"; then + clojure_args+=("-Srepro") +fi +if "$force"; then + clojure_args+=("-Sforce") +fi + +if "$pom"; then + if "$verbose"; then + clojure_args+=("-Sverbose") + fi + "$CLOJURE_CMD" "${clojure_args[@]}" -Spom +elif "$describe"; then + if "$verbose"; then + clojure_args+=("-Sverbose") + fi + "$CLOJURE_CMD" "${clojure_args[@]}" -Sdescribe +elif "$tree"; then + if "$verbose"; then + clojure_args+=("-Sverbose") + fi + "$CLOJURE_CMD" "${clojure_args[@]}" -Stree +else + set -f + if [[ -n "$force_cp" ]]; then + cp="$force_cp" + else + if "$verbose"; then + "$CLOJURE_CMD" "${clojure_args[@]}" -Sverbose -e nil + fi + cp=`"$CLOJURE_CMD" "${clojure_args[@]}" -Spath` + fi + if "$print_classpath"; then + echo $cp + else + if [[ ${#main_aliases[@]} -gt 0 ]] || [[ ${#all_aliases[@]} -gt 0 ]]; then + # Attempt to extract the main cache filename by parsing the output of -Sverbose + cp_file=`"$CLOJURE_CMD" "${clojure_args[@]}" -Sverbose -Spath | grep cp_file | cut -d = -f 2 | sed 's/^ *//g'` + main_file="${cp_file%.cp}.main" + fi + if [[ -e "$main_file" ]]; then + main_cache_opts=($(cat "$main_file")) + fi + exec bb --classpath "$cp" "${main_cache_opts[@]}" "$@" + fi +fi diff --git a/src/babashka/impl/async.clj b/src/babashka/impl/async.clj index afb04b06..61022448 100644 --- a/src/babashka/impl/async.clj +++ b/src/babashka/impl/async.clj @@ -4,7 +4,7 @@ (defn thread [_ _ & body] - `(~'async/thread-call (fn [] ~@body))) + `(~'clojure.core.async/thread-call (fn [] ~@body))) (def async-namespace {'entry [part] + (if (str/ends-with? part ".jar") + (JarFileResolver. (io/file part)) + (DirectoryResolver. (io/file part)))) + +(deftype Loader [entries] + IResourceResolver + (getResource [this resource-path] + (some #(getResource % resource-path) entries))) + +(defn loader [^String classpath] + (let [parts (.split classpath (System/getProperty "path.separator")) + entries (map part->entry parts)] + (Loader. entries))) + +(defn source-for-namespace [loader namespace] + (let [ns-str (name namespace) + ^String ns-str (munge ns-str) + path (.replace ns-str "." (System/getProperty "file.separator")) + paths (map #(str path %) [".bb" ".clj" ".cljc"])] + (some #(getResource loader %) paths))) + +;;;; Scratch + +(comment + (def l (loader "src:/Users/borkdude/.m2/repository/cheshire/cheshire/5.9.0/cheshire-5.9.0.jar")) + (source-for-namespace l 'babashka.impl.cheshire) + (source-for-namespace l 'cheshire.core) + ) diff --git a/src/babashka/impl/clojure/core.clj b/src/babashka/impl/clojure/core.clj index b998ca02..896d06a2 100644 --- a/src/babashka/impl/clojure/core.clj +++ b/src/babashka/impl/clojure/core.clj @@ -2,65 +2,8 @@ {:no-doc true} (:refer-clojure :exclude [future])) -(defn future - [_ _ & body] - `(~'future-call (fn [] ~@body))) - -(defn __close!__ [^java.io.Closeable x] - (.close x)) - -(defn with-open* - [_ _ bindings & body] - (cond - (= (count bindings) 0) `(do ~@body) - (symbol? (bindings 0)) `(let ~(subvec bindings 0 2) - (try - (with-open ~(subvec bindings 2) ~@body) - (finally - (~'__close!__ ~(bindings 0))))) - :else (throw (IllegalArgumentException. - "with-open only allows Symbols in bindings")))) - -(defn binding* - "This macro only works with symbols that evaluate to vars themselves. See `*in*` and `*out*` below." - [_ _ bindings & body] - `(do - (let [] - (push-thread-bindings (hash-map ~@bindings)) - (try - ~@body - (finally - (pop-thread-bindings)))))) - -;; this works now! -"(def w (java.io.StringWriter.)) (push-thread-bindings {clojure.core/*out* w}) (try (println \"hello\") (finally (pop-thread-bindings))) (prn \">\" (str w))" - -;; this also works now! "(def w (java.io.StringWriter.)) (binding [clojure.core/*out* w] (println \"hello\")) (str w)" - -(defn with-out-str* - [_ _ & body] - `(let [s# (java.io.StringWriter.)] - (binding [*out* s#] - ~@body - (str s#)))) - -(defn with-in-str* - [_ _ s & body] - `(with-open [s# (-> (java.io.StringReader. ~s) clojure.lang.LineNumberingPushbackReader.)] - (binding [*in* s#] - ~@body))) - (def core-extras - {'*in* #'*in* - '*out* #'*out* - 'binding (with-meta binding* {:sci/macro true}) - 'file-seq file-seq - '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? + {'file-seq file-seq 'agent agent 'send send 'send-off send-off @@ -68,18 +11,4 @@ 'deliver deliver 'shutdown-agents shutdown-agents 'slurp slurp - 'spit spit - 'pmap pmap - 'pr pr - 'prn prn - 'print print - 'println println - 'println-str println-str - 'pop-thread-bindings pop-thread-bindings - 'push-thread-bindings push-thread-bindings - 'flush flush - 'read-line read-line - '__close!__ __close!__ - 'with-open (with-meta with-open* {:sci/macro true}) - 'with-out-str (with-meta with-out-str* {:sci/macro true}) - 'with-in-str (with-meta with-in-str* {:sci/macro true})}) + 'spit spit}) diff --git a/src/babashka/impl/repl.clj b/src/babashka/impl/repl.clj new file mode 100644 index 00000000..d97cec3a --- /dev/null +++ b/src/babashka/impl/repl.clj @@ -0,0 +1,54 @@ +(ns babashka.impl.repl + {:no-doc true} + (:require + [babashka.impl.clojure.main :as m] + [clojure.java.io :as io] + [clojure.string :as str] + [clojure.tools.reader.reader-types :as r] + [sci.core :as sci] + [sci.impl.interpreter :refer [eval-form]] + [sci.impl.opts :refer [init]] + [sci.impl.parser :as parser])) + +(defn repl + "REPL with predefined hooks for attachable socket server." + [sci-ctx] + (let [in (r/indexing-push-back-reader (r/push-back-reader *in*))] + (m/repl + :init #(do (println "Babashka" + (str "v" (str/trim (slurp (io/resource "BABASHKA_VERSION")))) + "REPL.") + (println "Use :repl/quit or :repl/exit to quit the REPL.") + (println "Clojure rocks, Bash reaches.") + (println)) + :read (fn [_request-prompt request-exit] + (if (r/peek-char in) ;; if this is nil, we reached EOF + (let [v (parser/parse-next sci-ctx in)] + (if (or (identical? :repl/quit v) + (identical? :repl/exit v) + (identical? :edamame.impl.parser/eof v)) + request-exit + v)) + request-exit)) + :eval (fn [expr] + (let [ret (sci/with-bindings {sci/in *in* + sci/out *out* + sci/err *err*} + (eval-form (update sci-ctx + :env + (fn [env] + (swap! env update-in [:namespaces 'clojure.core] + assoc + '*1 *1 + '*2 *2 + '*3 *3 + '*e *e) + env)) + expr))] + ret)) + :need-prompt (fn [] true) + :prompt #(printf "%s=> " (-> sci-ctx :env deref :current-ns))))) + +(defn start-repl! [ctx] + (let [sci-ctx (init ctx)] + (repl sci-ctx))) diff --git a/src/babashka/impl/socket_repl.clj b/src/babashka/impl/socket_repl.clj index 53cd465a..13195287 100644 --- a/src/babashka/impl/socket_repl.clj +++ b/src/babashka/impl/socket_repl.clj @@ -2,51 +2,12 @@ {:no-doc true} (:require [babashka.impl.clojure.core.server :as server] - [babashka.impl.clojure.main :as m] - [clojure.java.io :as io] + [babashka.impl.repl :as repl] [clojure.string :as str] - [clojure.tools.reader.reader-types :as r] - [sci.impl.interpreter :refer [opts->ctx eval-form]] - [sci.impl.parser :as parser])) + [sci.impl.opts :refer [init]])) (set! *warn-on-reflection* true) -(defn repl - "REPL with predefined hooks for attachable socket server." - [sci-ctx] - (let [in (r/indexing-push-back-reader (r/push-back-reader *in*))] - (m/repl - :init #(do (println "Babashka" - (str "v" (str/trim (slurp (io/resource "BABASHKA_VERSION")))) - "REPL.") - (println "Use :repl/quit or :repl/exit to quit the REPL.") - (println "Clojure rocks, Bash reaches.") - (println)) - :read (fn [_request-prompt request-exit] - (if (r/peek-char in) ;; if this is nil, we reached EOF - (let [v (parser/parse-next in #{:bb} {:current (-> sci-ctx :env deref :current-ns)})] - (if (or (identical? :repl/quit v) - (identical? :repl/exit v) - (identical? :edamame.impl.parser/eof v)) - request-exit - v)) - request-exit)) - :eval (fn [expr] - (let [ret (eval-form (update sci-ctx - :env - (fn [env] - (swap! env update-in [:namespaces 'clojure.core] - assoc - '*1 *1 - '*2 *2 - '*3 *3 - '*e *e) - env)) - expr)] - ret)) - :need-prompt (fn [] true) - :prompt #(printf "%s=> " (-> sci-ctx :env deref :current-ns))))) - (defn start-repl! [host+port sci-opts] (let [parts (str/split host+port #":") [host port] (if (= 1 (count parts)) @@ -54,12 +15,12 @@ [(first parts) (Integer. ^String (second parts))]) host+port (if-not host (str "localhost:" port) host+port) - sci-ctx (opts->ctx sci-opts) + sci-ctx (init sci-opts) socket (server/start-server {:address host :port port :name "bb" - :accept babashka.impl.socket-repl/repl + :accept babashka.impl.repl/repl :args [sci-ctx]})] (println "Babashka socket REPL started at" host+port) socket)) diff --git a/src/babashka/impl/utils.clj b/src/babashka/impl/utils.clj new file mode 100644 index 00000000..55387bc3 --- /dev/null +++ b/src/babashka/impl/utils.clj @@ -0,0 +1,9 @@ +(ns babashka.impl.utils + {:no-doc true} + (:require [sci.core :as sci])) + +(defn eval-string [expr ctx] + (sci/with-bindings {sci/out *out* + sci/in *in* + sci/err *err*} + (sci/eval-string expr ctx))) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index b0c42242..94e55aa1 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -2,21 +2,25 @@ {:no-doc true} (:require [babashka.impl.async :refer [async-namespace]] + [babashka.impl.cheshire :refer [cheshire-core-namespace]] + [babashka.impl.classpath :as cp] [babashka.impl.clojure.core :refer [core-extras]] [babashka.impl.clojure.java.io :refer [io-namespace]] [babashka.impl.clojure.stacktrace :refer [print-stack-trace]] [babashka.impl.conch :refer [conch-namespace]] [babashka.impl.csv :as csv] [babashka.impl.pipe-signal-handler :refer [handle-pipe! pipe-signal-received?]] + [babashka.impl.repl :as repl] [babashka.impl.socket-repl :as socket-repl] [babashka.impl.tools.cli :refer [tools-cli-namespace]] - [io.aviso.ansi :as ansi] + [babashka.impl.utils :refer [eval-string]] [babashka.wait :as wait] [clojure.edn :as edn] [clojure.java.io :as io] [clojure.java.shell :as shell] [clojure.string :as str] - [sci.core :as sci]) + [io.aviso.ansi :as ansi] + [sci.addons :as addons]) (:gen-class)) (set! *warn-on-reflection* true) @@ -65,6 +69,11 @@ (recur (rest options) (assoc opts-map :file (first options)))) + ("--repl") + (let [options (rest options)] + (recur (rest options) + (assoc opts-map + :repl true))) ("--socket-repl") (let [options (rest options)] (recur (rest options) @@ -74,7 +83,15 @@ (let [options (rest options)] (recur (rest options) (assoc opts-map :expression (first options)))) - (if (some opts-map [:file :socket-repl :expression]) + ("--classpath", "-cp") + (let [options (rest options)] + (recur (rest options) + (assoc opts-map :classpath (first options)))) + ("--main", "-m") + (let [options (rest options)] + (recur (rest options) + (assoc opts-map :main (first options)))) + (if (some opts-map [:file :socket-repl :expression :main]) (assoc opts-map :command-line-args options) (if (and (not= \( (first (str/trim opt))) @@ -104,31 +121,36 @@ (defn print-version [] (println (str "babashka v"(str/trim (slurp (io/resource "BABASHKA_VERSION")))))) -(def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [--verbose] [ --stream ] ( -e | -f | --socket-repl [:] )") +(def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose] + [ ( --classpath | -cp ) ] [ ( --main | -m ) ] + ( -e | -f | --repl | --socket-repl [:] ) + [ arg* ]") (defn print-usage [] (println usage-string)) (defn print-help [] - (println (str "babashka v" (str/trim (slurp (io/resource "BABASHKA_VERSION"))))) + (println (str "Babashka v" (str/trim (slurp (io/resource "BABASHKA_VERSION"))))) ;; (println (str "sci v" (str/trim (slurp (io/resource "SCI_VERSION"))))) (println) (print-usage) (println) (println "Options:") (println " - --help, -h or -?: print this help text. - --version: print the current version of babashka. - - -i: bind *in* to a lazy seq of lines from stdin. - -I: bind *in* 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 *in* becomes a single value per iteration. - -e, --eval : evaluate an expression - -f, --file : evaluate a file - --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 *in* to a lazy seq of lines from stdin. + -I Bind *in* 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 *in* 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. 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*.")) @@ -147,10 +169,17 @@ Everything after that is bound to *command-line-args*.")) (defn load-file* [ctx file] (let [s (slurp file)] - (sci/eval-string s ctx))) + (eval-string s ctx))) (defn eval* [ctx form] - (sci/eval-string (pr-str form) ctx)) + (eval-string (pr-str form) ctx)) + +(defn start-repl! [ctx read-next] + (let [ctx (update ctx :bindings assoc + (with-meta '*in* + {:sci/deref! true}) + (read-next))] + (repl/start-repl! ctx))) (defn start-socket-repl! [address ctx read-next] (let [ctx (update ctx :bindings assoc @@ -164,6 +193,10 @@ Everything after that is bound to *command-line-args*.")) (defn exit [n] (throw (ex-info "" {:bb/exit-code n}))) +;; (sci/set-var-root! sci/*in* *in*) +;; (sci/set-var-root! sci/*out* *out*) +;; (sci/set-var-root! sci/*err* *err*) + (defn main [& args] (handle-pipe!) @@ -172,7 +205,10 @@ Everything after that is bound to *command-line-args*.")) (let [t0 (System/currentTimeMillis) {:keys [:version :shell-in :edn-in :shell-out :edn-out :help? :file :command-line-args - :expression :stream? :time? :socket-repl :verbose?] :as _opts} + :expression :stream? :time? + :repl :socket-repl + :verbose? :classpath + :main] :as _opts} (parse-opts args) read-next (fn [*in*] (if (pipe-signal-received?) @@ -187,6 +223,13 @@ Everything after that is bound to *command-line-args*.")) :else (edn/read *in*)))))) env (atom {}) + classpath (or classpath + (System/getenv "BABASHKA_CLASSPATH")) + loader (when classpath + (cp/loader classpath)) + load-fn (when classpath + (fn [{:keys [:namespace]}] + (cp/source-for-namespace loader namespace))) ctx {:aliases '{tools.cli 'clojure.tools.cli edn clojure.edn wait babashka.wait @@ -195,7 +238,8 @@ Everything after that is bound to *command-line-args*.")) io clojure.java.io conch me.raynes.conch.low-level async clojure.core.async - csv clojure.data.csv} + csv clojure.data.csv + json cheshire.core} :namespaces {'clojure.core (assoc core-extras '*command-line-args* command-line-args) 'clojure.tools.cli tools-cli-namespace @@ -208,6 +252,7 @@ Everything after that is bound to *command-line-args*.")) 'me.raynes.conch.low-level conch-namespace 'clojure.core.async async-namespace 'clojure.data.csv csv/csv-namespace + 'cheshire.core cheshire-core-namespace 'io.aviso.ansi {'blue ansi/blue 'red ansi/red}} :bindings {'java.lang.System/exit exit ;; override exit, so we have more control @@ -218,6 +263,7 @@ Everything after that is bound to *command-line-args*.")) 'java.lang.AssertionError AssertionError 'java.lang.Boolean Boolean 'java.io.BufferedWriter java.io.BufferedWriter + 'java.io.BufferedReader java.io.BufferedReader 'java.lang.Class Class 'java.lang.Double Double 'java.lang.Exception Exception @@ -232,6 +278,9 @@ Everything after that is bound to *command-line-args*.")) 'java.lang.System System 'java.lang.Thread Thread 'sun.nio.fs.UnixPath sun.nio.fs.UnixPath + 'java.nio.file.attribute.FileAttribute java.nio.file.attribute.FileAttribute + 'java.nio.file.attribute.PosixFilePermission java.nio.file.attribute.PosixFilePermission + 'java.nio.file.attribute.PosixFilePermissions java.nio.file.attribute.PosixFilePermissions 'java.nio.file.CopyOption java.nio.file.CopyOption 'java.nio.file.FileAlreadyExistsException java.nio.file.FileAlreadyExistsException 'java.nio.file.Files java.nio.file.Files @@ -248,10 +297,16 @@ Everything after that is bound to *command-line-args*.")) File java.io.File String java.lang.String System java.lang.System - Thread java.lang.Thread}} + Thread java.lang.Thread} + :load-fn load-fn} ctx (update ctx :bindings assoc 'eval #(eval* ctx %) - 'load-file #(load-file* ctx %)) - _preloads (some-> (System/getenv "BABASHKA_PRELOADS") (str/trim) (sci/eval-string ctx)) + 'load-file #(load-file* ctx %)) + 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) exit-code (or #_(binding [*out* *err*] @@ -261,6 +316,7 @@ Everything after that is bound to *command-line-args*.")) [(print-version) 0] help? [(print-help) 0] + repl [(start-repl! ctx #(read-next *in*)) 0] socket-repl [(start-socket-repl! socket-repl ctx #(read-next *in*)) 0] :else (try @@ -272,7 +328,7 @@ Everything after that is bound to *command-line-args*.")) {:sci/deref! true})) in)] (if (identical? ::EOF in) [nil 0] ;; done streaming - (let [res [(let [res (sci/eval-string expr ctx)] + (let [res [(let [res (eval-string expr ctx)] (when (some? res) (if-let [pr-f (cond shell-out println edn-out prn)] @@ -285,7 +341,7 @@ Everything after that is bound to *command-line-args*.")) (if stream? (recur (read-next *in*)) res))))) - [(print-help) 1])) + [(start-repl! ctx #(read-next *in*)) 0])) (catch Throwable e (binding [*out* *err*] (let [d (ex-data e) diff --git a/test-resources/babashka/src_for_classpath_test/env/env_ns.clj b/test-resources/babashka/src_for_classpath_test/env/env_ns.clj new file mode 100644 index 00000000..bc600eec --- /dev/null +++ b/test-resources/babashka/src_for_classpath_test/env/env_ns.clj @@ -0,0 +1,4 @@ +(ns env-ns) + +(defn foo [] + "env!") diff --git a/test-resources/babashka/src_for_classpath_test/foo.jar b/test-resources/babashka/src_for_classpath_test/foo.jar new file mode 100644 index 00000000..870949e7 Binary files /dev/null and b/test-resources/babashka/src_for_classpath_test/foo.jar differ diff --git a/test-resources/babashka/src_for_classpath_test/my/impl.cljc b/test-resources/babashka/src_for_classpath_test/my/impl.cljc new file mode 100644 index 00000000..6b5c97f3 --- /dev/null +++ b/test-resources/babashka/src_for_classpath_test/my/impl.cljc @@ -0,0 +1,5 @@ +(ns my.impl) + +(defn impl-fn + "identity" + [x] x) diff --git a/test-resources/babashka/src_for_classpath_test/my/main.clj b/test-resources/babashka/src_for_classpath_test/my/main.clj new file mode 100644 index 00000000..9ca77065 --- /dev/null +++ b/test-resources/babashka/src_for_classpath_test/my/main.clj @@ -0,0 +1,5 @@ +(ns my.main + (:require [my.impl :as impl])) + +(defn -main [& args] + (impl/impl-fn args)) diff --git a/test-resources/babashka/src_for_classpath_test/my_script.bb b/test-resources/babashka/src_for_classpath_test/my_script.bb new file mode 100644 index 00000000..99e31941 --- /dev/null +++ b/test-resources/babashka/src_for_classpath_test/my_script.bb @@ -0,0 +1,4 @@ +(ns my-script) + +(defn foo [] + ::bb) diff --git a/test/babashka/classpath_test.clj b/test/babashka/classpath_test.clj new file mode 100644 index 00000000..b3b876d3 --- /dev/null +++ b/test/babashka/classpath_test.clj @@ -0,0 +1,27 @@ +(ns babashka.classpath-test + (:require + [babashka.test-utils :as tu] + [clojure.edn :as edn] + [clojure.test :as t :refer [deftest is]])) + +(defn bb [input & args] + (edn/read-string (apply tu/bb (when (some? input) (str input)) (map str args)))) + +(deftest classpath-test + (is (= :my-script/bb + (bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" + "(require '[my-script :as ms]) (ms/foo)"))) + (is (= "hello from foo\n" + (tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test/foo.jar" + "(require '[foo :as f]) (f/foo)")))) + +(deftest classpath-env-test + ;; for this test you have to set `BABASHKA_CLASSPATH` to test-resources/babashka/src_for_classpath_test/env + ;; and `BABASHKA_PRELOADS` to "(require '[env-ns])" + (when (System/getenv "BABASHKA_CLASSPATH_TEST") + (println (System/getenv "BABASHKA_CLASSPATH")) + (is (= "env!" (bb nil "(env-ns/foo)"))))) + +(deftest main-test + (is (= "(\"1\" \"2\" \"3\" \"4\")\n" + (tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" "-m" "my.main" "1" "2" "3" "4")))) diff --git a/test/babashka/impl/repl_test.clj b/test/babashka/impl/repl_test.clj new file mode 100644 index 00000000..8adbde77 --- /dev/null +++ b/test/babashka/impl/repl_test.clj @@ -0,0 +1,35 @@ +(ns babashka.impl.repl-test + (:require + [babashka.impl.repl :refer [start-repl!]] + [clojure.string :as str] + [clojure.test :as t :refer [deftest is]])) + +(set! *warn-on-reflection* true) + +(defn repl! [] + (start-repl! {:bindings {(with-meta '*in* + {:sci/deref! true}) + (delay [1 2 3]) + '*command-line-args* + ["a" "b" "c"]} + :env (atom {})})) + +(defn assert-repl [expr expected] + (is (str/includes? (with-out-str + (with-in-str (str expr "\n:repl/quit") + (repl!))) expected))) + +(deftest repl-test + (assert-repl "(+ 1 2 3)" "6") + (assert-repl "(defn foo [] (+ 1 2 3)) (foo)" "6") + (assert-repl "(defn foo [] (+ 1 2 3)) (foo)" "6") + (assert-repl "1\n(inc *1)" "2") + (assert-repl "1\n(dec *1)(+ *2 *2)" "2") + (assert-repl "1\n(dec *1)(+ *2 *2)" "2") + (assert-repl "*command-line-args*" "[\"a\" \"b\" \"c\"]") + (assert-repl "*in*" "[1 2 3]")) + +;;;; Scratch + +(comment + ) diff --git a/test/babashka/impl/socket_repl_test.clj b/test/babashka/impl/socket_repl_test.clj index 7b7f41ac..085561dd 100644 --- a/test/babashka/impl/socket_repl_test.clj +++ b/test/babashka/impl/socket_repl_test.clj @@ -4,19 +4,29 @@ [babashka.test-utils :as tu] [clojure.java.shell :refer [sh]] [clojure.string :as str] - [clojure.test :as t :refer [deftest is testing]])) + [clojure.test :as t :refer [deftest is testing]] + [clojure.java.io :as io])) -(def mac? - (str/includes? - (str/lower-case (System/getProperty "os.name")) - "mac")) +(set! *warn-on-reflection* true) -(defn socket-command [expr] - (let [expr (format "echo \"%s\n:repl/exit\" | nc 127.0.0.1 1666" - (pr-str expr)) - ret (sh "bash" "-c" - expr)] - (:out ret))) +(defn socket-command [expr expected] + (with-open [socket (java.net.Socket. "127.0.0.1" 1666) + reader (io/reader socket) + sw (java.io.StringWriter.) + writer (io/writer socket)] + (binding [*out* writer] + (println (str expr)) + (println ":repl/exit\n")) + (loop [] + (when-let [l (.readLine ^java.io.BufferedReader reader)] + (binding [*out* sw] + (println l)) + (recur))) + (let [s (str sw)] + (is (str/includes? s expected) + (format "\"%s\" does not contain \"%s\"" + s expected)) + s))) (deftest socket-repl-test (try @@ -26,7 +36,8 @@ (delay [1 2 3]) '*command-line-args* ["a" "b" "c"]} - :env (atom {})}) + :env (atom {}) + :features #{:bb}}) (future (sh "bash" "-c" "echo '[1 2 3]' | ./bb --socket-repl 0.0.0.0:1666 a b c"))) @@ -35,36 +46,19 @@ (while (not (zero? (:exit (sh "bash" "-c" "lsof -t -i:1666")))))) - (is (str/includes? (socket-command '(+ 1 2 3)) - "user=> 6")) - (testing "ctrl-d exits normally, doesn't print nil" - (is (str/ends-with? (:out (sh "bash" "-c" - (if mac? ;; mac doesn't support -q - "echo \"(inc 1336)\" | nc 127.0.0.1 1666" - "echo \"(inc 1336)\" | nc -q 1 127.0.0.1 1666"))) - "1337\nuser=> "))) + (is (socket-command "(+ 1 2 3)" "user=> 6")) (testing "*in*" - (is (str/includes? (socket-command "*in*") - "[1 2 3]"))) + (is (socket-command "*in*" "[1 2 3]"))) (testing "*command-line-args*" - (is (str/includes? (socket-command '*command-line-args*) - "\"a\" \"b\" \"c\""))) + (is (socket-command '*command-line-args* "\"a\" \"b\" \"c\""))) (testing "&env" - (socket-command '(defmacro bindings [] (mapv #(list 'quote %) (keys &env)))) - (socket-command '(defn bar [x y z] (bindings))) - (is (str/includes? (socket-command '(bar 1 2 3)) - "[x y z]"))) + (socket-command "(defmacro bindings [] (mapv #(list 'quote %) (keys &env)))" "bindings") + (socket-command "(defn bar [x y z] (bindings))" "bar") + (is (socket-command "(bar 1 2 3)" "[x y z]"))) (testing "reader conditionals" - (is (str/includes? (let [ret (sh "bash" "-c" - (format "echo \"%s\n:repl/exit\" | nc 127.0.0.1 1666" - "#?(:bb 1337 :clj 8888)"))] - (:out ret)) - "1337"))) + (is (socket-command "#?(:bb 1337 :clj 8888)" "1337"))) (testing "*1, *2, *3, *e" - (is (= 2 (count (re-seq #"1\n" (let [ret (sh "bash" "-c" - (format "echo \"%s\n*1\n:repl/exit\" | nc 127.0.0.1 1666" - "1"))] - (:out ret))))))) + (is (socket-command "1\n*1" "1"))) (finally (if tu/jvm? (stop-repl!) @@ -75,4 +69,14 @@ (comment (socket-repl-test) + (dotimes [_ 1000] + (t/run-tests)) + (stop-repl!) + (start-repl! "0.0.0.0:1666" {:bindings {(with-meta '*in* + {:sci/deref! true}) + (delay [1 2 3]) + '*command-line-args* + ["a" "b" "c"]} + :env (atom {})}) + (socket-command "(+ 1 2 3)" "6") ) diff --git a/test/babashka/main_test.clj b/test/babashka/main_test.clj index af5c342d..e1f2a224 100644 --- a/test/babashka/main_test.clj +++ b/test/babashka/main_test.clj @@ -6,10 +6,11 @@ [clojure.java.shell :refer [sh]] [clojure.string :as str] [clojure.test :as test :refer [deftest is testing]] - [clojure.java.io :as io])) + [clojure.java.io :as io] + [sci.core :as sci])) (defn bb [input & args] - (edn/read-string (apply test-utils/bb (str input) (map str args)))) + (edn/read-string (apply test-utils/bb (when (some? input) (str input)) (map str args)))) (deftest parse-opts-test (is (= {:expression "(println 123)"} @@ -82,6 +83,9 @@ "(map-indexed #(-> [%1 %2]) *in*)") (bb "(keep #(when (re-find #\"(?i)clojure\" (second %)) (first %)) *in*)")))))) +(deftest println-test + (is (= "hello\n" (test-utils/bb nil "(println \"hello\")")))) + (deftest input-test (testing "bb doesn't wait for input if *in* isn't used" (is (= "2\n" (with-out-str (main/main "(inc 1)")))))) @@ -93,29 +97,20 @@ (is (not-empty s)))) (let [out (java.io.StringWriter.) err (java.io.StringWriter.) - exit-code (binding [*out* out *err* err] - (main/main "--time" "(println \"Hello world!\") (System/exit 42)"))] + exit-code (sci/with-bindings {sci/out out + sci/err err} + (binding [*out* out *err* err] + (main/main "--time" "(println \"Hello world!\") (System/exit 42)")))] (is (= (str out) "Hello world!\n")) (is (re-find #"took" (str err))) (is (= 42 exit-code)))) (deftest malformed-command-line-args-test (is (thrown-with-msg? Exception #"File does not exist: non-existing\n" - (bb nil "-f" "non-existing"))) - (testing "no arguments prints help" - (is (str/includes? - (try (test-utils/bb nil) - (catch clojure.lang.ExceptionInfo e - (:stdout (ex-data e)))) - "Usage:")))) + (bb nil "-f" "non-existing")))) (deftest ssl-test - (let [graalvm-home (System/getenv "GRAALVM_HOME") - lib-path (format "%1$s/jre/lib:%1$s/jre/lib/amd64" graalvm-home) - ;; _ (prn "lib-path" lib-path) - resp (bb nil (format "(System/setProperty \"java.library.path\" \"%s\") - (slurp \"https://www.google.com\")" - lib-path))] + (let [resp (bb nil "(slurp \"https://www.google.com\")")] (is (re-find #"doctype html" resp)))) (deftest stream-test @@ -140,7 +135,8 @@ (deftest preloads-test ;; THIS TEST REQUIRES: ;; export BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")' - (is (= "foobar" (bb nil "(str (__bb__foo) (__bb__bar))")))) + (when (System/getenv "BABASHKA_PRELOADS_TEST") + (is (= "foobar" (bb nil "(str (__bb__foo) (__bb__bar))"))))) (deftest io-test (is (true? (bb nil "(.exists (io/file \"README.md\"))"))) @@ -288,3 +284,9 @@ (java.nio.file.Files/copy p p' (into-array [java.nio.file.StandardCopyOption/REPLACE_EXISTING]))))))" temp-path)) (is (.exists f2)))) + +(deftest future-print-test + (testing "the root binding of sci/*out*" + (is (= "hello" (bb nil "@(future (prn \"hello\"))")))) + + ) diff --git a/test/babashka/test_utils.clj b/test/babashka/test_utils.clj index b5996386..5a42415d 100644 --- a/test/babashka/test_utils.clj +++ b/test/babashka/test_utils.clj @@ -1,19 +1,25 @@ (ns babashka.test-utils (:require [babashka.main :as main] - [me.raynes.conch :refer [let-programs] :as sh])) + [me.raynes.conch :refer [let-programs] :as sh] + [sci.core :as sci])) (set! *warn-on-reflection* true) (defn bb-jvm [input & args] - (let [es (java.io.StringWriter.) - os (java.io.StringWriter.)] - (binding [*err* es - *out* os] - (let [res (if input - (with-in-str input - (apply main/main args)) - (apply main/main args))] + (let [os (java.io.StringWriter.) + es (java.io.StringWriter.) + is (when input + (java.io.StringReader. input)) + bindings-map (cond-> {sci/out os + sci/err es} + is (assoc sci/in is))] + (sci/with-bindings bindings-map + (let [res (binding [*out* os + *err* es] + (if input + (with-in-str input (apply main/main args)) + (apply main/main args)))] (if (zero? res) (str os) (throw (ex-info (str es)