diff --git a/.carve_ignore b/.carve_ignore new file mode 100644 index 00000000..765c627a --- /dev/null +++ b/.carve_ignore @@ -0,0 +1,3 @@ +babashka.impl.clojure.stacktrace/root-cause +babashka.impl.classes/generate-reflection-file +babashka.main/-main diff --git a/.circleci/config.yml b/.circleci/config.yml index 16e2cbbb..c25a8ea2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ jobs: working_directory: ~/repo environment: LEIN_ROOT: "true" - # GRAALVM_HOME: /home/circleci/graalvm-ce-java8-19.3.0 + BABASHKA_PLATFORM: linux # could be used in jar name steps: - checkout - run: @@ -34,18 +34,6 @@ jobs: name: Install lsof command: | sudo apt-get install lsof - # - run: - # name: Download GraalVM - # command: | - # cd ~ - # if ! [ -d graalvm-ce-java8-19.3.0 ]; then - # curl -O -sL https://github.com/oracle/graal/releases/download/vm-19.2.0/graalvm-ce-linux-amd64-19.2.0.tar.gz - # tar xzf graalvm-ce-linux-amd64-19.2.0.tar.gz - # fi - # - run: - # name: Install GraalVM SSL libs - # command: | - # .circleci/script/graalvm_ssl - run: name: Run JVM tests command: | @@ -58,6 +46,16 @@ jobs: name: Run as lein command command: | .circleci/script/lein + - run: + name: Create uberjar + command: | + mkdir -p /tmp/release + lein do clean, uberjar + VERSION=$(cat resources/BABASHKA_VERSION) + cp target/babashka-$VERSION-standalone.jar /tmp/release/babashka-$VERSION-standalone.jar + - store_artifacts: + path: /tmp/release + destination: release - save_cache: paths: - ~/.m2 @@ -117,6 +115,7 @@ jobs: name: Run tests command: | script/test + script/run_lib_tests # - run: # name: Performance report # command: | @@ -133,6 +132,10 @@ jobs: - store_artifacts: path: /tmp/release destination: release + - run: + name: Publish artifact link to Slack + command: | + ./bb .circleci/script/publish_artifact.clj mac: macos: xcode: "9.0" @@ -180,6 +183,7 @@ jobs: name: Run tests command: | script/test + script/run_lib_tests # - run: # name: Performance report # command: | @@ -196,6 +200,10 @@ jobs: - store_artifacts: path: /tmp/release destination: release + - run: + name: Publish artifact link to Slack + command: | + ./bb .circleci/script/publish_artifact.clj deploy: docker: - image: circleci/clojure:lein-2.8.1 diff --git a/.circleci/script/publish_artifact.clj b/.circleci/script/publish_artifact.clj new file mode 100755 index 00000000..cb55f321 --- /dev/null +++ b/.circleci/script/publish_artifact.clj @@ -0,0 +1,19 @@ +(require '[clojure.java.shell :refer [sh]] + '[cheshire.core :refer [generate-string]]) + +(def channel "#babashka_circleci_builds") +#_(def channel "#_test") + +(def text (format "[%s - %s@%s]: https://%s-201467090-gh.circle-artifacts.com/0/release/babashka-0.0.61-SNAPSHOT-%s-amd64.zip" + (System/getenv "BABASHKA_PLATFORM") + (System/getenv "CIRCLE_BRANCH") + (System/getenv "CIRCLE_SHA1") + (System/getenv "CIRCLE_BUILD_NUM") + (System/getenv "BABASHKA_PLATFORM"))) + +(def slack-hook-url (System/getenv "SLACK_HOOK_URL")) +(when slack-hook-url + (let [json (generate-string {:username "borkdude" + :channel channel + :text text})] + (sh "curl" "-X" "POST" "-H" "Content-Type: application/json" "-d" json slack-hook-url))) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3a14c403..08efe995 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,7 +9,7 @@ assignees: '' **version** -[ Please specify which version of babashka you're using. You can find this with `babashka --version`. The documentation on the master branch may be ahead of the most released version. You can check the docs for your version by going to cljdoc. ] +[ Please specify which version of babashka you're using. You can find this with `babashka --version`. The documentation on the master branch may be ahead of the most released version. ] **platform** diff --git a/.gitignore b/.gitignore index a96a9d65..d8a7040d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ pom.xml.asc !java/src/babashka/impl/LockFix.class !test-resources/babashka/src_for_classpath_test/foo.jar .cpcache -deps.edn +reflection.json diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..e75cccb9 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,11 @@ +# Changes + +## Breaking changes + +## v0.0.44 - 0.0.45 +- #173: Rename `*in*` to `*input*` (in the `user` namespace). The reason for + this is that itt shadowed `clojure.core/*in*` when used unqualified. + +## v0.0.43 +- #160: Add support for `java.lang.ProcessBuilder`. See docs. This replaces the + `conch` namespace. diff --git a/README.md b/README.md index 65771518..ffb50eaa 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,24 @@ -# babashka + [![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. +
+

Life's too short to remember how to write Bash code. I feel liberated.

+ — + @laheadle on Clojurians Slack +
+ ## Quickstart ``` shellsession $ bash <(curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install) -$ ls | bb --time -i '(filter #(-> % io/file .isDirectory) *in*)' +$ ls | bb --time -i '(filter #(-> % io/file .isDirectory) *input*)' ("doc" "resources" "sci" "script" "src" "target" "test") bb took 4ms. ``` @@ -41,45 +47,42 @@ Non-goals: * Provide a mixed Clojure/bash DSL (see portability). * Replace existing shells. Babashka is a tool you can use inside existing shells like bash and it is designed to play well with them. It does not aim to replace them. -Reasons why babashka may not be the right fit for your use case: - -- It uses [sci](https://github.com/borkdude/sci) for interpreting Clojure. Sci -implements only a subset of Clojure and is not as performant as compiled code. -- External libraries are not available (although you may use `load-file` for - loading external scripts). +Babashka uses [sci](https://github.com/borkdude/sci) for interpreting Clojure. Sci +implements a subset of Clojure and is not as performant as compiled code. If your script is taking more than a few seconds, Clojure on the JVM may be a better fit. Read more about the differences with Clojure [here](#differences-with-clojure). ## Status -Experimental. Breaking changes are expected to happen at this phase. +Experimental. Breaking changes are expected to happen at this phase. Keep an eye +on [CHANGES.md](CHANGES.md) for a list of breaking changes. ## Examples ``` shellsession -$ ls | bb -i '*in*' -["LICENSE" "README.md" "bb" "doc" "pom.xml" "project.clj" "reflection.json" "resources" "script" "src" "target" "test"] +$ ls | bb -i '*input*' +["LICENSE" "README.md" "bb" "doc" "pom.xml" "project.clj" "resources" "script" "src" "target" "test"] -$ ls | bb -i '(count *in*)' +$ ls | bb -i '(count *input*)' 12 -$ bb '(vec (dedupe *in*))' <<< '[1 1 1 1 2]' +$ bb '(vec (dedupe *input*))' <<< '[1 1 1 1 2]' [1 2] -$ bb '(filterv :foo *in*)' <<< '[{:foo 1} {:bar 2}]' +$ bb '(filterv :foo *input*)' <<< '[{:foo 1} {:bar 2}]' [{:foo 1}] -$ bb '(#(+ %1 %2 %3) 1 2 *in*)' <<< 3 +$ bb '(#(+ %1 %2 %3) 1 2 *input*)' <<< 3 6 -$ ls | bb -i '(filterv #(re-find #"reflection" %) *in*)' -["reflection.json"] +$ ls | bb -i '(filterv #(re-find #"README" %) *input*)' +["README.md"] $ bb '(run! #(shell/sh "touch" (str "/tmp/test/" %)) (range 100))' -$ ls /tmp/test | bb -i '*in*' +$ ls /tmp/test | bb -i '*input*' ["0" "1" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "2" "20" "21" ...] -$ bb -O '(repeat "dude")' | bb --stream '(str *in* "rino")' | bb -I '(take 3 *in*)' +$ bb -O '(repeat "dude")' | bb --stream '(str *input* "rino")' | bb -I '(take 3 *input*)' ("duderino" "duderino" "duderino") ``` @@ -128,27 +131,31 @@ You may also download a binary from [Github](https://github.com/borkdude/babashk ``` shellsession Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose] - [ ( --classpath | -cp ) ] [ ( --main | -m ) ] - ( -e | -f | --repl | --socket-repl [:] ) + [ ( --classpath | -cp ) ] [ --uberscript ] + [ ( --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. - --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. + --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. + --uberscript Collect preloads, -e, -f and -m and all required namespaces from the classpath into a single executable file. + + -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*. @@ -165,40 +172,22 @@ enumerated explicitly. - `clojure.set` aliased as `set` - `clojure.edn` aliased as `edn`: - `read-string` -- `clojure.java.shell` aliases as `shell`: - - `sh` +- `clojure.java.shell` aliases as `shell` - `clojure.java.io` aliased as `io`: - - `as-relative-path`, `copy`, `delete-file`, `file` + - `as-relative-path`, `as-url`, `copy`, `delete-file`, `file`, `input-stream`, + `make-parents`, `output-stream`, `reader`, `resource`, `writer` +- `clojure.main`: `repl` - [`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` +- `clojure.stacktrace` - [`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: +A selection of java classes are available, see `babashka/impl/classes.clj`. -- `ArithmeticException` -- `AssertionError` -- `Boolean` -- `Class` -- `Double` -- `Exception` -- `clojure.lang.ExceptionInfo` -- `Integer` -- `java.io.File` -- `java.nio.Files` -- `java.util.regex.Pattern` -- `String` -- `System` -- `Thread` - -More classes can be added by request. See `reflection.json` and the `:classes` -option in `main.clj`. - -Babashka supports `import` : `(import clojure.lang.ExceptionInfo)`. +Babashka supports `import`: `(import clojure.lang.ExceptionInfo)`. Babashka supports a subset of the `ns` form where you may use `:require` and `:import`: @@ -211,11 +200,48 @@ Babashka supports a subset of the `ns` form where you may use `:require` and `:i For the unsupported parts of the ns form, you may use [reader conditionals](#reader-conditionals) to maintain compatibility with JVM Clojure. -Special vars: +### Input and output flags -- `*in*`: contains the input read from stdin. EDN by default, multiple lines of -text with the `-i` option, or multiple EDN values with the `-I` option. -- `*command-line-args*`: contain the command line args +In one-liners the `*input*` value may come in handy. It contains the input read from stdin as EDN by default. If you want to read in text, use the `-i` flag, which binds `*input*` to a lazy seq of lines of text. If you want to read multiple EDN values, use the `-I` flag. The `-o` option prints the result as lines of text. The `-O` option prints the result as lines of EDN values. + +The following table illustrates the combination of options for commands of the form + + echo "{{Input}}" | bb {{Input flags}} {{Output flags}} "*input*" + +| Input | Input flags | Output flag | `*input*` | Output | +|----------------|-------------|-------------|---------------|----------| +| `{:a 1}`
`{:a 2}` | | | `{:a 1}` | `{:a 1}` | +| hello
bye | `-i` | | `("hello" "bye")` | `("hello" "bye")` | +| hello
bye | `-i` | `-o` | `("hello" "bye")` | hello
bye | +| `{:a 1}`
`{:a 2}` | `-I` | | `({:a 1} {:a 2})` | `({:a 1} {:a 2})` | +| `{:a 1}`
`{:a 2}` | `-I` | `-O` | `({:a 1} {:a 2})` | `{:a 1}`
`{:a 2}` | + +When combined with the `--stream` option, the expression is executed for each value in the input: + +``` clojure +$ echo '{:a 1} {:a 2}' | bb --stream '*input*' +{:a 1} +{:a 2} +``` + +### Current file path + +The var `*file*` contains the full path of the file that is currently being +executed: + +``` shellsession +$ cat example.clj +(prn *file*) + +$ bb example.clj +"/Users/borkdude/example.clj" +``` + +### Command-line arguments + +Command-line arguments can be retrieved using `*command-line-args*`. + +### Additional functions Additionally, babashka adds the following functions: @@ -329,7 +355,7 @@ export BABASHKA_PRELOADS Note that you can concatenate multiple expressions. Now you can use these functions in babashka: ``` shellsession -$ bb '(-> (foo *in*) bar)' <<< 1 +$ bb '(-> (foo *input*) bar)' <<< 1 6 ``` @@ -339,7 +365,7 @@ You can also preload an entire file using `load-file`: export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")' ``` -Note that `*in*` is not available in preloads. +Note that `*input*` is not available in preloads. ## Classpath @@ -361,10 +387,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 @@ -381,6 +407,57 @@ $ bb "(my-gist-script/-main)" Hello from gist script! ``` +### Deps.clj + +The [`deps.clj`](https://github.com/borkdude/deps.clj/) script can be used to work with `deps.edn`-based projects: + +``` shell +$ deps.clj -A:my-script -Scommand "bb -cp {{classpath}} {{main-opts}}" +Hello from gist script! +``` + +Create these aliases for brevity: + +``` shell +$ alias bbk='deps.clj -Scommand "bb -cp {{classpath}} {{main-opts}}"' +$ alias babashka='rlwrap deps.clj -Scommand "bb -cp {{classpath}} {{main-opts}}"' +$ bbk -A:my-script +Hello from gist script! +$ babashka +Babashka v0.0.58 REPL. +Use :repl/quit or :repl/exit to quit the REPL. +Clojure rocks, Bash reaches. + +user=> (require '[my-gist-script :as mgs]) +nil +user=> (mgs/-main) +Hello from gist script! +nil +``` + +## 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 and deployment. + +Given the `deps.edn` from above: + +``` clojure +$ deps.clj -A:my-script -Scommand "bb -cp {{classpath}} {{main-opts}} --uberscript my-script.clj" + +$ cat my-script.clj +(ns my-gist-script) +(defn -main [& args] + (println "Hello from 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`: @@ -446,22 +523,27 @@ A socket REPL client for Emacs is ## Spawning and killing a process -You may use the `conch` namespace for this. It maps to -[`me.raynes.conch.low-level`](https://github.com/clj-commons/conch#low-level-usage). +Use the `java.lang.ProcessBuilder` class. Example: ``` clojure -$ bb ' -(def ws (conch/proc "python" "-m" "SimpleHTTPServer" "1777")) -(net/wait-for-it "localhost" 1777) (conch/destroy ws)' +user=> (def ws (-> (ProcessBuilder. ["python" "-m" "SimpleHTTPServer" "1777"]) (.start))) +#'user/ws +user=> (wait/wait-for-port "localhost" 1777) +{:host "localhost", :port 1777, :took 2} +user=> (.destroy ws) +nil ``` +Also see this [example](examples/process_builder.clj). + ## Async -Apart from `future` for creating threads and the `conch` namespace for creating -processes, you may use the `async` namespace, which maps to `clojure.core.async`, for asynchronous scripting. The following -example shows how to get first available value from two different processes: +Apart from `future` and `pmap` for creating threads, you may use the `async` +namespace, which maps to `clojure.core.async`, for asynchronous scripting. The +following example shows how to get first available value from two different +processes: ``` clojure bb ' @@ -487,9 +569,6 @@ same. Multi-threading is supported (`pmap`, `future`). Differences with Clojure: -- No first class vars. Note that you can define and redefine global values with -`def` / `defn`, but there is no `var` indirection. - - A subset of Java classes are supported. - Only the `clojure.core`, `clojure.set`, `clojure.string` and `clojure.walk` @@ -500,6 +579,62 @@ Differences with Clojure: - No support for unboxed types. +## External resources + +### Tools and libraries + +The following libraries are known to work with Babashka: + +#### [deps.clj](https://github.com/borkdude/deps.clj) + +A port of the [clojure](https://github.com/clojure/brew-install/) bash script to +Clojure / babashka. + +#### [spartan.test](https://github.com/borkdude/spartan.test/) + +A minimal test framework compatible with babashka. + +#### [medley](https://github.com/borkdude/medley/) + +A fork of [medley](https://github.com/weavejester/medley) made compatible with +babashka. Requires `bb` >= v0.0.58. + +#### [clj-http-lite](https://github.com/borkdude/clj-http-lite) + +This fork does not depend on any other libraries. Example: + +``` shell +$ export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {clj-http-lite {:git/url "https://github.com/borkdude/clj-http-lite" :sha "f44ebe45446f0f44f2b73761d102af3da6d0a13e"}}}' -Spath)" + +$ bb "(require '[clj-http.lite.client :as client]) (:status (client/get \"https://www.clojure.org\"))" +200 +``` + +#### [limit-break](https://github.com/technomancy/limit-break) + +A debug REPL library. Example: + +``` shell +$ export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {limit-break {:git/url "https://github.com/technomancy/limit-break" :sha "050fcfa0ea29fe3340927533a6fa6fffe23bfc2f" :deps/manifest :deps}}}' -Spath)" + +$ bb "(require '[limit.break :as lb]) (let [x 1] (lb/break))" +Babashka v0.0.49 REPL. +Use :repl/quit or :repl/exit to quit the REPL. +Clojure rocks, Bash reaches. + +break> x +1 +``` + +### Blogs + +- [Clojure Start Time in 2019](https://stuartsierra.com/2019/12/21/clojure-start-time-in-2019) by Stuart Sierra +- [Advent of Random + Hacks](https://lambdaisland.com/blog/2019-12-19-advent-of-parens-19-advent-of-random-hacks) + by Arne Brasseur +- [Clojure in the Shell](https://lambdaisland.com/blog/2019-12-05-advent-of-parens-5-clojure-in-the-shell) by Arne Brasseur +- [Clojure Tool](https://purelyfunctional.tv/issues/purelyfunctional-tv-newsletter-351-clojure-tool-babashka/) by Eric Normand + ## Developing Babashka To work on Babashka itself make sure Git submodules are checked out. @@ -520,6 +655,19 @@ You need [Leiningen](https://leiningen.org/), and for building binaries you need `lein repl` will get you a standard REPL/nREPL connection. To work on tests use `lein with-profiles +test repl`. +### Adding classes + +Add necessary classes to `babashka/impl/classes.clj`. For every addition, write +a unit test, so it's clear why it is added and removing it will break the +tests. Try to reduce the size of the binary by only adding the necessary parts +of a class in `:instance-check`, `:constructors`, `:methods`, `:fields` or +`:custom`. + +The `reflection.json` file that is needed for GraalVM compilation is generated +with: + + lein with-profiles +reflection run + ### Test Test on the JVM (for development): @@ -553,7 +701,7 @@ welcome! ### Delete a list of files returned by a Unix command ``` -find . | grep conflict | bb -i '(doseq [f *in*] (.delete (io/file f)))' +find . | grep conflict | bb -i '(doseq [f *input*] (.delete (io/file f)))' ``` ### Calculate aggregate size of directory @@ -587,7 +735,7 @@ $ cat /tmp/test.txt 3 Babashka 4 Goodbye -$ < /tmp/test.txt bb -io '(shuffle *in*)' +$ < /tmp/test.txt bb -io '(shuffle *input*)' 3 Babashka 2 Clojure 4 Goodbye @@ -601,7 +749,7 @@ For converting JSON to EDN, see [jet](https://github.com/borkdude/jet). ``` shellsession $ curl -s https://api.github.com/repos/borkdude/babashka/tags | jet --from json --keywordize --to edn | -bb '(-> *in* first :name (subs 1))' +bb '(-> *input* first :name (subs 1))' "0.0.4" ``` @@ -610,16 +758,18 @@ bb '(-> *in* first :name (subs 1))' ``` shellsession $ curl -s https://api.github.com/repos/borkdude/babashka/releases | jet --from json --keywordize | -bb '(-> *in* first :assets)' | -bb '(some #(re-find #".*linux.*" (:browser_download_url %)) *in*)' +bb '(-> *input* first :assets)' | +bb '(some #(re-find #".*linux.*" (:browser_download_url %)) *input*)' "https://github.com/borkdude/babashka/releases/download/v0.0.4/babashka-0.0.4-linux-amd64.zip" ``` ### View download statistics from Clojars +Contributed by [@plexus](https://github.com/plexus). + ``` shellsession $ curl https://clojars.org/stats/all.edn | -bb -o '(for [[[group art] counts] *in*] (str (reduce + (vals counts)) " " group "/" art))' | +bb -o '(for [[[group art] counts] *input*] (str (reduce + (vals counts)) " " group "/" art))' | sort -rn | less 14113842 clojure-complete/clojure-complete @@ -665,6 +815,29 @@ 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 ``` +### Convert project.clj to deps.edn + +Contributed by [@plexus](https://github.com/plexus). + +``` shellsession +$ cat project.clj | +sed -e 's/#=//g' -e 's/~@//g' -e 's/~//g' | +bb '(let [{:keys [dependencies source-paths resource-paths]} (apply hash-map (drop 3 *input*))] + {:paths (into source-paths resource-paths) + :deps (into {} (for [[d v] dependencies] [d {:mvn/version v}]))}) ' | +jet --pretty > deps.edn +``` + +### Print current time in California + +See [examples/pst.clj](https://github.com/borkdude/babashka/blob/master/examples/pst.clj) + +### Tiny http server + +See [examples/http_server.clj](https://github.com/borkdude/babashka/blob/master/examples/http_server.clj) + +Original by [@souenzzo](https://gist.github.com/souenzzo/a959a4c5b8c0c90df76fe33bb7dfe201) + ## Thanks - [adgoji](https://www.adgoji.com/) for financial support @@ -677,5 +850,3 @@ Distributed under the EPL License. See LICENSE. This project contains code from: - Clojure, which is licensed under the same EPL License. -- [conch](https://github.com/clj-commons/conch), which is licensed under the -same EPL License. diff --git a/deps.edn b/deps.edn new file mode 100644 index 00000000..e3051185 --- /dev/null +++ b/deps.edn @@ -0,0 +1,12 @@ +{:paths ["src" "sci/src" "resources" "sci/resources"], + :deps {org.clojure/clojure {:mvn/version "1.10.1"}, + org.clojure/tools.reader {:mvn/version "1.3.2"}, + borkdude/edamame {:mvn/version "0.0.10-alpha.4"}, + borkdude/graal.locking {:mvn/version "0.0.2"}, + borkdude/sci.impl.reflector {:mvn/version "0.0.1"} + org.clojure/core.async {:mvn/version "0.4.500"}, + org.clojure/tools.cli {:mvn/version "0.4.2"}, + org.clojure/data.csv {:mvn/version "0.1.4"}, + cheshire {:mvn/version "5.9.0"}} + :aliases {:main + {:main-opts ["-m" "babashka.main"]}}} diff --git a/examples/http_server.clj b/examples/http_server.clj new file mode 100755 index 00000000..0c045993 --- /dev/null +++ b/examples/http_server.clj @@ -0,0 +1,51 @@ +#!/usr/bin/env bb + +(import (java.net ServerSocket)) +(require '[clojure.string :as string] + '[clojure.java.io :as io]) + +(with-open [server-socket (new ServerSocket 8080) + client-socket (.accept server-socket)] + (loop [] + (let [out (io/writer (.getOutputStream client-socket)) + in (io/reader (.getInputStream client-socket)) + [req-line & headers] (loop [headers []] + (let [line (.readLine in)] + (if (string/blank? line) + headers + (recur (conj headers line))))) + [_ _ path _] (re-find #"([^\s]+)\s([^\s]+)\s([^\s]+)" req-line) + f (io/file (format "./%s" path)) + status (if (.exists f) + 200 + 404) + html (fn html-fn [tag & body] + (let [attrs? (map? (first body)) + attrs-str (str (when attrs? + (format " %s" (string/join " " (for [[k v] (first body)] + (format "%s=%s" (name k) (name v)))))))] + (format "<%s%s>%s" + (name tag) + attrs-str + (string/join (if attrs? (rest body) body)) + (name tag)))) + body (cond + (not (.exists f)) "" + (.isFile f) (slurp f) + (.isDirectory f) (format "\n%s" + (html :html + (html :head + (html :title path)) + (html :body + (html :h1 path) + (html :tt + (apply html :pre + (for [i (.list f)] + (html :div (html :a {:href (format "\"%s\"" i)} i)))))))))] + (prn path) + (.write out (format "HTTP/1.1 %s OK\r\nContent-Length: %s\r\n\r\n%s" + status + (count body) + body)) + (.flush out)) + (recur))) diff --git a/examples/process_builder.clj b/examples/process_builder.clj new file mode 100755 index 00000000..0b064516 --- /dev/null +++ b/examples/process_builder.clj @@ -0,0 +1,19 @@ +#!/usr/bin/env bb + +(require '[clojure.java.io :as io]) +(import '[java.lang ProcessBuilder$Redirect]) + +(defn grep [input pattern] + (let [proc (-> (ProcessBuilder. ["grep" pattern]) + (.redirectOutput ProcessBuilder$Redirect/INHERIT) + (.redirectError ProcessBuilder$Redirect/INHERIT) + (.start)) + proc-input (.getOutputStream proc)] + (with-open [w (io/writer proc-input)] + (binding [*out* w] + (print input) + (flush))) + (.waitFor proc) + nil)) + +(grep "hello\nbye\n" "bye") diff --git a/examples/pst.clj b/examples/pst.clj new file mode 100755 index 00000000..2a919f5f --- /dev/null +++ b/examples/pst.clj @@ -0,0 +1,7 @@ +#!/usr/bin/env bb + +(def now (java.time.ZonedDateTime/now)) +(def LA-timezone (java.time.ZoneId/of "America/Los_Angeles")) +(def LA-time (.withZoneSameInstant now LA-timezone)) +(def pattern (java.time.format.DateTimeFormatter/ofPattern "HH:mm")) +(println (.format LA-time pattern)) diff --git a/logo/babashka.png b/logo/babashka.png new file mode 100644 index 00000000..f042bf5e Binary files /dev/null and b/logo/babashka.png differ diff --git a/logo/babashka.svg b/logo/babashka.svg new file mode 100644 index 00000000..f3f63ee5 --- /dev/null +++ b/logo/babashka.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/logo/icon.png b/logo/icon.png new file mode 100644 index 00000000..b1dd077c Binary files /dev/null and b/logo/icon.png differ diff --git a/logo/icon.svg b/logo/icon.svg new file mode 100644 index 00000000..ef89d121 --- /dev/null +++ b/logo/icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/project.clj b/project.clj index 055a9d2a..70a59c9d 100644 --- a/project.clj +++ b/project.clj @@ -11,7 +11,9 @@ :resource-paths ["resources" "sci/resources"] :dependencies [[org.clojure/clojure "1.10.1"] [org.clojure/tools.reader "1.3.2"] - [borkdude/edamame "0.0.10-alpha.2"] + [borkdude/edamame "0.0.10-alpha.4"] + [borkdude/graal.locking "0.0.2"] + [borkdude/sci.impl.reflector "0.0.1"] [org.clojure/core.async "0.4.500"] [org.clojure/tools.cli "0.4.2"] [org.clojure/data.csv "0.1.4"] @@ -22,7 +24,8 @@ :jvm-opts ["-Dclojure.compiler.direct-linking=true" "-Dclojure.spec.skip-macros=true"] :main babashka.main - :aot :all}} + :aot :all} + :reflection {:main babashka.impl.classes/generate-reflection-file}} :aliases {"bb" ["run" "-m" "babashka.main"]} :deploy-repositories [["clojars" {:url "https://clojars.org/repo" :username :env/clojars_user diff --git a/reflection.json b/reflection.json deleted file mode 100644 index 676d7b83..00000000 --- a/reflection.json +++ /dev/null @@ -1,189 +0,0 @@ -[ - { - "name":"java.lang.ArithmeticException", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.lang.AssertionError", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.lang.Boolean", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.io.BufferedWriter", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.io.BufferedReader", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name": "java.lang.Class", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allPublicMethods": true - }, - { - "name":"java.lang.Double", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.lang.Exception", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name": "clojure.lang.ExceptionInfo", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.lang.Integer", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.io.File", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"clojure.lang.LineNumberingPushbackReader", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.util.concurrent.LinkedBlockingQueue", - "allPublicMethods":true - }, - { - "name":"java.util.regex.Pattern", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.lang.Process", - "allPublicMethods":true - }, - { - "name":"java.lang.ProcessBuilder", - "allPublicConstructors":true - }, - { - "name":"java.lang.String", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.io.StringReader", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.io.StringWriter", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.lang.System", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.lang.Thread", - "methods": [{"name":"run"},{"name":"toString"},{"name":"isInterrupted"},{"name":"currentThread"},{"name":"getName"},{"name":"join"},{"name":"getThreadGroup"},{"name":"getStackTrace"},{"name":"holdsLock"},{"name":"checkAccess"},{"name":"dumpStack"},{"name":"yield"},{"name":"setPriority"},{"name":"setDaemon"},{"name":"start"},{"name":"sleep"},{"name":"interrupt"},{"name":"interrupted"},{"name":"isAlive"},{"name":"getPriority"},{"name":"setName"},{"name":"activeCount"},{"name":"enumerate"},{"name":"isDaemon"},{"name":"getContextClassLoader"},{"name":"setContextClassLoader"},{"name":"getAllStackTraces"},{"name":"getId"},{"name":"getState"},{"name":"setDefaultUncaughtExceptionHandler"},{"name":"getDefaultUncaughtExceptionHandler"},{"name":"getUncaughtExceptionHandler"},{"name":"setUncaughtExceptionHandler"}], - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "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 - }, - { - "name":"java.nio.file.CopyOption", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.nio.file.FileAlreadyExistsException", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.nio.file.Files", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.nio.file.NoSuchFileException", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"java.nio.file.StandardCopyOption", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - { - "name":"sun.nio.fs.UnixPath", - "allPublicMethods":true, - "allPublicFields": true, - "allPublicConstructors": true - }, - {"name":"com.sun.xml.internal.stream.XMLInputFactoryImpl", - "methods":[{"name":"","parameterTypes":[] }]}, - {"name":"com.sun.xml.internal.stream.XMLOutputFactoryImpl", - "methods":[{"name":"","parameterTypes":[] }]} -] diff --git a/resources/BABASHKA_RELEASED_VERSION b/resources/BABASHKA_RELEASED_VERSION index 54a00221..67ccd586 100644 --- a/resources/BABASHKA_RELEASED_VERSION +++ b/resources/BABASHKA_RELEASED_VERSION @@ -1 +1 @@ -0.0.42 +0.0.60 \ No newline at end of file diff --git a/resources/BABASHKA_VERSION b/resources/BABASHKA_VERSION index 382bf853..e2e3d510 100644 --- a/resources/BABASHKA_VERSION +++ b/resources/BABASHKA_VERSION @@ -1 +1 @@ -0.0.43-SNAPSHOT +0.0.61-SNAPSHOT \ No newline at end of file diff --git a/sci b/sci index 07d28ee5..57b584ba 160000 --- a/sci +++ b/sci @@ -1 +1 @@ -Subproject commit 07d28ee572e90a629e01b10aa5b98cb33ccdc1e5 +Subproject commit 57b584ba0a6a1f74a887d350463a700976dd37d8 diff --git a/script/bump_version.clj b/script/bump_version.clj new file mode 100755 index 00000000..6a3591bd --- /dev/null +++ b/script/bump_version.clj @@ -0,0 +1,60 @@ +#!/usr/bin/env bb + +(ns bump-version + (:require [clojure.java.io :as io] + [clojure.string :as str])) + +(import '[java.lang ProcessBuilder$Redirect]) + +(defn shell-command + "Executes shell command. Exits script when the shell-command has a non-zero exit code, propagating it. + Accepts the following options: + `:input`: instead of reading from stdin, read from this string. + `:to-string?`: instead of writing to stdoud, write to a string and + return it." + ([args] (shell-command args nil)) + ([args {:keys [:input :to-string?]}] + (let [args (mapv str args) + pb (cond-> (-> (ProcessBuilder. ^java.util.List args) + (.redirectError ProcessBuilder$Redirect/INHERIT)) + (not to-string?) (.redirectOutput ProcessBuilder$Redirect/INHERIT) + (not input) (.redirectInput ProcessBuilder$Redirect/INHERIT)) + proc (.start pb)] + (when input + (with-open [w (io/writer (.getOutputStream proc))] + (binding [*out* w] + (print input) + (flush)))) + (let [string-out + (when to-string? + (let [sw (java.io.StringWriter.)] + (with-open [w (io/reader (.getInputStream proc))] + (io/copy w sw)) + (str sw))) + exit-code (.waitFor proc)] + (when-not (zero? exit-code) + (System/exit exit-code)) + string-out)))) + +(def version-file (io/file "resources" "BABASHKA_VERSION")) +(def released-version-file (io/file "resources" "BABASHKA_RELEASED_VERSION")) + +(case (first *command-line-args*) + "release" (let [version-string (str/trim (slurp version-file)) + [major minor patch] (str/split version-string #"\.") + patch (str/replace patch "-SNAPSHOT" "") + new-version (str/join "." [major minor patch])] + (spit version-file new-version) + (shell-command ["git" "commit" "-a" "-m" (str "v" new-version)]) + (shell-command ["git" "diff" "HEAD^" "HEAD"])) + "post-release" (do + (io/copy version-file released-version-file) + (let [version-string (str/trim (slurp version-file)) + [major minor patch] (str/split version-string #"\.") + patch (Integer. patch) + patch (str (inc patch) "-SNAPSHOT") + new-version (str/join "." [major minor patch])] + (spit version-file new-version) + (shell-command ["git" "commit" "-a" "-m" "Version bump"]) + (shell-command ["git" "diff" "HEAD^" "HEAD"]))) + (println "Expected: release | post-release.")) diff --git a/script/compile b/script/compile index 94cb69fa..ac925a5a 100755 --- a/script/compile +++ b/script/compile @@ -23,7 +23,9 @@ BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION) # mkdir -p src/sci # cp -R /tmp/sci/src/* src +lein with-profiles +reflection do run lein do clean, uberjar + $NATIVE_IMAGE \ -jar target/babashka-$BABASHKA_VERSION-standalone.jar \ -H:Name=bb \ diff --git a/script/lib_tests/clj_http_lite_test b/script/lib_tests/clj_http_lite_test new file mode 100755 index 00000000..d9c0cf43 --- /dev/null +++ b/script/lib_tests/clj_http_lite_test @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -eo pipefail + +export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {clj-http-lite {:git/url "https://github.com/borkdude/clj-http-lite" :sha "f44ebe45446f0f44f2b73761d102af3da6d0a13e"}}}' -Spath) + +./bb -e " +(require '[clj-http.lite.client :as client]) + +(prn (:status (client/get \"https://www.clojure.org\"))) + +(prn (:status (client/get \"https://postman-echo.com/get?foo1=bar1&foo2=bar2\"))) + +(prn (:status (client/post \"https://postman-echo.com/post\"))) + +(prn (:status (client/put \"https://postman-echo.com/put\"))) +" diff --git a/script/lib_tests/deps_clj_test b/script/lib_tests/deps_clj_test new file mode 100755 index 00000000..29d78dc3 --- /dev/null +++ b/script/lib_tests/deps_clj_test @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -eo pipefail + +curl -sL https://raw.githubusercontent.com/borkdude/deps.clj/master/deps.clj -o deps_test.clj +chmod +x deps_test.clj +./bb deps_test.clj -Sdescribe +rm deps_test.clj + diff --git a/script/lib_tests/spartan_spec_test b/script/lib_tests/spartan_spec_test new file mode 100755 index 00000000..5afe02ef --- /dev/null +++ b/script/lib_tests/spartan_spec_test @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -eo pipefail + +export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {spartan.spec {:git/url "https://github.com/borkdude/spartan.spec" :sha "16f7eec4b6589c77c96c9fcf989f78fffcee7c4c"}}}' -Spath) + +./bb -e " +(require '[spartan.spec :as s]) +(s/explain (s/cat :i int? :s string?) [1 :foo]) +(s/conform (s/cat :i int? :s string?) [1 \"foo\"]) +" diff --git a/script/run_lib_tests b/script/run_lib_tests new file mode 100755 index 00000000..9e107365 --- /dev/null +++ b/script/run_lib_tests @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -eo pipefail + +script/lib_tests/clj_http_lite_test +script/lib_tests/deps_clj_test +script/lib_tests/spartan_spec_test diff --git a/src-bash/bbk b/src-bash/bbk deleted file mode 100755 index 38b6c36e..00000000 --- a/src-bash/bbk +++ /dev/null @@ -1,219 +0,0 @@ -#!/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/Boolean.clj b/src/babashka/impl/Boolean.clj deleted file mode 100644 index ee2e1853..00000000 --- a/src/babashka/impl/Boolean.clj +++ /dev/null @@ -1,15 +0,0 @@ -(ns babashka.impl.Boolean - {:no-doc true} - (:refer-clojure :exclude [list])) - -(set! *warn-on-reflection* true) - -(defn parseBoolean [^String x] - (Boolean/parseBoolean x)) - -(def boolean-bindings - {'Boolean/parseBoolean parseBoolean}) - -(comment - - ) diff --git a/src/babashka/impl/Double.clj b/src/babashka/impl/Double.clj deleted file mode 100644 index 50882b12..00000000 --- a/src/babashka/impl/Double.clj +++ /dev/null @@ -1,15 +0,0 @@ -(ns babashka.impl.Double - {:no-doc true} - (:refer-clojure :exclude [list])) - -(set! *warn-on-reflection* true) - -(defn parseDouble [^String x] - (Double/parseDouble x)) - -(def double-bindings - {'Double/parseDouble parseDouble}) - -(comment - - ) diff --git a/src/babashka/impl/cheshire.clj b/src/babashka/impl/cheshire.clj index 21faa7ba..76cc4180 100644 --- a/src/babashka/impl/cheshire.clj +++ b/src/babashka/impl/cheshire.clj @@ -11,4 +11,8 @@ 'generate-smile json/generate-smile 'decode json/decode 'parse-string json/parse-string + 'parse-smile json/parse-smile + 'parse-stream json/parse-stream + 'parsed-seq json/parsed-seq + 'parsed-smile-seq json/parsed-smile-seq 'decode-smile json/decode-smile}) diff --git a/src/babashka/impl/classes.clj b/src/babashka/impl/classes.clj new file mode 100644 index 00000000..415c2e41 --- /dev/null +++ b/src/babashka/impl/classes.clj @@ -0,0 +1,232 @@ +(ns babashka.impl.classes + {:no-doc true} + (:require + [cheshire.core :as json] + #_[clojure.string :as str])) + +;; (def os-name (str/lower-case (System/getProperty "os.name"))) +;; (def os (cond (str/includes? os-name "mac") :mac +;; (or (str/includes? os-name "nix") +;; (str/includes? os-name "nux")) :linux +;; (str/includes? os-name "win") :windows)) +;; (def unix-like? (or (identical? os :linux) +;; (identical? os :mac))) + +(def classes + '{:all [java.io.BufferedReader + java.io.BufferedWriter + java.io.ByteArrayInputStream + java.io.ByteArrayOutputStream + java.io.File + java.io.InputStream + java.io.IOException + java.io.OutputStream + java.io.StringReader + java.io.StringWriter + java.lang.ArithmeticException + java.lang.AssertionError + java.lang.Boolean + java.lang.Class + java.lang.Double + java.lang.Exception + java.lang.Integer + java.lang.Math + java.util.concurrent.LinkedBlockingQueue + java.lang.Object + java.lang.String + java.lang.System + java.lang.Throwable + java.lang.Process + java.lang.ProcessBuilder + java.lang.ProcessBuilder$Redirect + java.net.URI + java.net.HttpURLConnection + java.net.ServerSocket + java.net.Socket + java.net.UnknownHostException + java.net.URLEncoder + java.net.URLDecoder + java.nio.file.CopyOption + java.nio.file.FileAlreadyExistsException + java.nio.file.Files + java.nio.file.LinkOption + java.nio.file.NoSuchFileException + java.nio.file.Path + java.nio.file.Paths + java.nio.file.StandardCopyOption + java.nio.file.attribute.FileAttribute + java.nio.file.attribute.FileTime + java.nio.file.attribute.PosixFilePermission + java.nio.file.attribute.PosixFilePermissions + java.time.format.DateTimeFormatter + java.time.Clock + java.time.DateTimeException + java.time.DayOfWeek + java.time.Duration + java.time.Instant + java.time.LocalDate + java.time.LocalDateTime + java.time.LocalTime + java.time.Month + java.time.MonthDay + java.time.OffsetDateTime + java.time.OffsetTime + java.time.Period + java.time.Year + java.time.YearMonth + java.time.ZonedDateTime + java.time.ZoneId + java.time.ZoneOffset + java.time.temporal.TemporalAccessor + java.util.regex.Pattern + java.util.Base64 + java.util.Base64$Decoder + java.util.Base64$Encoder + java.util.Date + java.util.UUID + java.util.concurrent.TimeUnit + java.util.zip.InflaterInputStream + java.util.zip.DeflaterInputStream + java.util.zip.GZIPInputStream + java.util.zip.GZIPOutputStream] + :constructors [clojure.lang.Delay + clojure.lang.MapEntry + clojure.lang.LineNumberingPushbackReader] + :methods [borkdude.graal.LockFix ;; support for locking + ] + :fields [clojure.lang.PersistentQueue] + :instance-checks [clojure.lang.ExceptionInfo + clojure.lang.IObj + clojure.lang.IEditableCollection] + :custom {clojure.lang.LineNumberingPushbackReader {:allPublicConstructors true + :allPublicMethods true} + java.lang.Thread + {:allPublicConstructors true + ;; generated with `public-declared-method-names`, see in + ;; `comment` below + :methods [{:name "activeCount"} + {:name "checkAccess"} + {:name "currentThread"} + {:name "dumpStack"} + {:name "enumerate"} + {:name "getAllStackTraces"} + {:name "getContextClassLoader"} + {:name "getDefaultUncaughtExceptionHandler"} + {:name "getId"} + {:name "getName"} + {:name "getPriority"} + {:name "getStackTrace"} + {:name "getState"} + {:name "getThreadGroup"} + {:name "getUncaughtExceptionHandler"} + {:name "holdsLock"} + {:name "interrupt"} + {:name "interrupted"} + {:name "isAlive"} + {:name "isDaemon"} + {:name "isInterrupted"} + {:name "join"} + {:name "run"} + {:name "setContextClassLoader"} + {:name "setDaemon"} + {:name "setDefaultUncaughtExceptionHandler"} + {:name "setName"} + {:name "setPriority"} + {:name "setUncaughtExceptionHandler"} + {:name "sleep"} + {:name "start"} + {:name "toString"} + {:name "yield"}]} + java.net.URL + {:allPublicConstructors true + :allPublicFields true + ;; generated with `public-declared-method-names`, see in + ;; `comment` below + :methods [{:name "equals"} + {:name "getAuthority"} + {:name "getContent"} + {:name "getDefaultPort"} + {:name "getFile"} + {:name "getHost"} + {:name "getPath"} + {:name "getPort"} + {:name "getProtocol"} + {:name "getQuery"} + {:name "getRef"} + {:name "getUserInfo"} + {:name "hashCode"} + {:name "openConnection"} + {:name "openStream"} + {:name "sameFile"} + ;; not supported: {:name "setURLStreamHandlerFactory"} + {:name "toExternalForm"} + {:name "toString"} + {:name "toURI"}]}}}) + +(defmacro gen-class-map [] + (let [classes (concat (:all classes) + (keys (:custom classes)) + (:constructors classes) + (:methods classes) + (:fields classes) + (:instance-checks classes)) + m (apply hash-map + (for [c classes + c [(list 'quote c) c]] + c))] + (assoc m :public-class + (fn [v] + (cond (instance? java.nio.file.Path v) + java.nio.file.Path + (instance? java.lang.Process v) + java.lang.Process))))) + +(def class-map (gen-class-map)) + +(defn generate-reflection-file + "Generate reflection.json file" + [& args] + (let [entries (vec (for [c (sort (:all classes)) + :let [class-name (str c)]] + {:name class-name + :allPublicMethods true + :allPublicFields true + :allPublicConstructors true})) + constructors (vec (for [c (sort (:constructors classes)) + :let [class-name (str c)]] + {:name class-name + :allPublicConstructors true})) + methods (vec (for [c (sort (:methods classes)) + :let [class-name (str c)]] + {:name class-name + :allPublicMethods true})) + fields (vec (for [c (sort (:fields classes)) + :let [class-name (str c)]] + {:name class-name + :allPublicFields true})) + custom-entries (for [[c v] (:custom classes) + :let [class-name (str c)]] + (assoc v :name class-name)) + all-entries (concat entries constructors methods fields custom-entries)] + (spit (or + (first args) + "reflection.json") (json/generate-string all-entries {:pretty true})))) + +(comment + + (defn public-declared-method? [c m] + (and (= c (.getDeclaringClass m)) + (not (.getAnnotation m Deprecated)))) + + (defn public-declared-method-names [c] + (->> (.getMethods c) + (keep (fn [m] + (when (public-declared-method? c m) + {:name (.getName m)})) ) + (distinct) + (sort-by :name) + (vec))) + + (public-declared-method-names java.lang.UNIXProcess) + (public-declared-method-names java.net.URL) + ) diff --git a/src/babashka/impl/classpath.clj b/src/babashka/impl/classpath.clj index 1b16611f..a90e5126 100644 --- a/src/babashka/impl/classpath.clj +++ b/src/babashka/impl/classpath.clj @@ -7,29 +7,37 @@ (set! *warn-on-reflection* true) (defprotocol IResourceResolver - (getResource [this path])) + (getResource [this path opts])) (deftype DirectoryResolver [path] IResourceResolver - (getResource [this resource-path] + (getResource [this resource-path {:keys [:url?]}] (let [f (io/file path resource-path)] (when (.exists f) - (slurp f))))) + (if url? + (java.net.URL. (str "file:" + (.getCanonicalPath f))) + {:file (.getCanonicalPath f) + :source (slurp f)}))))) (defn path-from-jar - [^java.io.File jar-file path] + [^java.io.File jar-file path {:keys [:url?]}] (with-open [jar (JarFile. jar-file)] (let [entries (enumeration-seq (.entries jar)) entry (some (fn [^JarFile$JarFileEntry x] (let [nm (.getName x)] (when (and (not (.isDirectory x)) (= path nm)) - (slurp (.getInputStream jar x))))) entries)] + (if url? + (java.net.URL. + (str "jar:file:" (.getCanonicalPath jar-file) "!/" path)) + {:file path + :source (slurp (.getInputStream jar x))})))) entries)] entry))) (deftype JarFileResolver [path] IResourceResolver - (getResource [this resource-path] - (path-from-jar path resource-path))) + (getResource [this resource-path opts] + (path-from-jar path resource-path opts))) (defn part->entry [part] (if (str/ends-with? part ".jar") @@ -38,25 +46,25 @@ (deftype Loader [entries] IResourceResolver - (getResource [this resource-path] - (some #(getResource % resource-path) entries))) + (getResource [this resource-path opts] + (some #(getResource % resource-path opts) 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] +(defn source-for-namespace [loader namespace opts] (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))) + (some #(getResource loader % opts) 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) + (source-for-namespace l 'babashka.impl.cheshire nil) + (:file (source-for-namespace l 'cheshire.core nil)) ) diff --git a/src/babashka/impl/clojure/core.clj b/src/babashka/impl/clojure/core.clj index 896d06a2..d3a69ee4 100644 --- a/src/babashka/impl/clojure/core.clj +++ b/src/babashka/impl/clojure/core.clj @@ -1,14 +1,22 @@ (ns babashka.impl.clojure.core {:no-doc true} - (:refer-clojure :exclude [future])) + (:refer-clojure :exclude [future]) + (:require [borkdude.graal.locking :as locking])) + +(defn locking* [form bindings v f & args] + (apply @#'locking/locking form bindings v f args)) (def core-extras {'file-seq file-seq 'agent agent + 'instance? instance? ;; TODO: move to sci 'send send 'send-off send-off 'promise promise 'deliver deliver + 'locking (with-meta locking* {:sci/macro true}) 'shutdown-agents shutdown-agents 'slurp slurp - 'spit spit}) + 'spit spit + 'Throwable->map Throwable->map + 'compare-and-set! compare-and-set!}) diff --git a/src/babashka/impl/clojure/core/server.clj b/src/babashka/impl/clojure/core/server.clj index ea6f14c8..2b1c821f 100644 --- a/src/babashka/impl/clojure/core/server.clj +++ b/src/babashka/impl/clojure/core/server.clj @@ -14,10 +14,11 @@ :no-doc true} babashka.impl.clojure.core.server (:refer-clojure :exclude [locking]) + (:require [sci.core :as sci]) (:import [clojure.lang LineNumberingPushbackReader] [java.net InetAddress Socket ServerSocket SocketException] - [java.io Reader Writer PrintWriter BufferedWriter BufferedReader InputStreamReader OutputStreamWriter])) + [java.io BufferedWriter InputStreamReader OutputStreamWriter])) (set! *warn-on-reflection* true) @@ -41,9 +42,9 @@ args - to pass to accept-fn" [^Socket conn client-id in out err accept args] (try - (binding [*in* in - *out* out - *err* err] + (sci/with-bindings {sci/in in + sci/out out + sci/err err} (swap! server assoc-in [:sessions client-id] {}) (apply accept args)) (catch SocketException _disconnect) diff --git a/src/babashka/impl/clojure/java/io.clj b/src/babashka/impl/clojure/java/io.clj index 59e9b1f2..5c26164b 100644 --- a/src/babashka/impl/clojure/java/io.clj +++ b/src/babashka/impl/clojure/java/io.clj @@ -1,8 +1,10 @@ (ns babashka.impl.clojure.java.io + {:no-doc true} (:require [clojure.java.io :as io])) (def io-namespace {'as-relative-path io/as-relative-path + 'as-url io/as-url 'copy io/copy 'delete-file io/delete-file 'file io/file diff --git a/src/babashka/impl/clojure/java/shell.clj b/src/babashka/impl/clojure/java/shell.clj new file mode 100644 index 00000000..1d3aa429 --- /dev/null +++ b/src/babashka/impl/clojure/java/shell.clj @@ -0,0 +1,42 @@ +(ns babashka.impl.clojure.java.shell + {:no-doc true} + (:require [clojure.java.shell :as shell] + [sci.core :as sci])) + +(def sh-dir (sci/new-dynamic-var '*sh-dir* nil)) +(def sh-env (sci/new-dynamic-var '*sh-env* nil)) + +(defn with-sh-dir* + [_ _ dir & forms] + `(binding [clojure.java.shell/*sh-dir* ~dir] + ~@forms)) + +(defn with-sh-env* + [_ _ env & forms] + `(binding [clojure.java.shell/*sh-env* ~env] + ~@forms)) + +(defn parse-args + [args] + (let [default-encoding "UTF-8" ;; see sh doc string + default-opts {:out-enc default-encoding + :in-enc default-encoding + :dir @sh-dir + :env @sh-env} + [cmd opts] (split-with string? args) + opts (merge default-opts (apply hash-map opts)) + opts-seq (apply concat opts)] + (concat cmd opts-seq))) + +(defn sh [& args] + (let [args (parse-args args)] + (apply shell/sh args))) + +(def shell-namespace + {'*sh-dir* sh-dir + '*sh-env* sh-env + 'with-sh-dir (with-meta with-sh-dir* + {:sci/macro true}) + 'with-sh-env (with-meta with-sh-env* + {:sci/macro true}) + 'sh sh}) diff --git a/src/babashka/impl/clojure/main.clj b/src/babashka/impl/clojure/main.clj index b7c9010a..3d8a9f2f 100644 --- a/src/babashka/impl/clojure/main.clj +++ b/src/babashka/impl/clojure/main.clj @@ -15,9 +15,13 @@ :author "Stephen C. Gilardi and Rich Hickey" :no-doc true} babashka.impl.clojure.main - (:refer-clojure :exclude [with-bindings]) - (:import (java.io StringReader) - (clojure.lang LineNumberingPushbackReader LispReader$ReaderException))) + (:refer-clojure :exclude [with-bindings])) + +(defn demunge + "Given a string representation of a fn class, + as in a stack trace element, returns a readable version." + [fn-name] + (clojure.lang.Compiler/demunge fn-name)) (defmacro with-bindings "Executes body in the context of thread-local bindings for several vars @@ -44,18 +48,6 @@ *e nil] ~@body)) -(defn repl-prompt - "Default :prompt hook for repl" - [] - (print "bb=> " )) - -(defn repl-caught - "Default :caught hook for repl" - [e] - (binding [*out* *err*] - (println (.getMessage ^Exception e)) - (flush))) - (defn repl "Generic, reusable, read-eval-print loop. By default, reads from *in*, writes to *out*, and prints exception summaries to *err*. If you use the @@ -92,14 +84,7 @@ read, eval, or print throws an exception or error default: repl-caught" [& options] - (let [{:keys [init need-prompt prompt flush read eval print caught] - :or {need-prompt (if (instance? LineNumberingPushbackReader *in*) - #(.atLineStart ^LineNumberingPushbackReader *in*) - #(identity true)) - prompt repl-prompt - flush flush - print prn - caught repl-caught}} + (let [{:keys [init need-prompt prompt flush read eval print caught]} (apply hash-map options) request-prompt (Object.) request-exit (Object.) diff --git a/src/babashka/impl/clojure/stacktrace.clj b/src/babashka/impl/clojure/stacktrace.clj index a220fd88..96fd9f32 100644 --- a/src/babashka/impl/clojure/stacktrace.clj +++ b/src/babashka/impl/clojure/stacktrace.clj @@ -80,9 +80,9 @@ (print "Caused by: " ) (recur cause n)))) -(defn e - "REPL utility. Prints a brief stack trace for the root cause of the - most recent exception." - {:added "1.1"} - [] - (print-stack-trace (root-cause *e) 8)) +(def stacktrace-namespace + {'root-cause root-cause + 'print-trace-element print-trace-element + 'print-throwable print-throwable + 'print-stack-trace print-stack-trace + 'print-cause-trace print-cause-trace}) diff --git a/src/babashka/impl/conch.clj b/src/babashka/impl/conch.clj deleted file mode 100644 index e53e8a99..00000000 --- a/src/babashka/impl/conch.clj +++ /dev/null @@ -1,18 +0,0 @@ -(ns babashka.impl.conch - {:no-doc true} - (:require - [babashka.impl.me.raynes.conch.low-level :as ll])) - -(def conch-namespace - {;; low level API - 'proc ll/proc - 'destroy ll/destroy - 'exit-code ll/exit-code - 'flush ll/flush - 'done ll/done - 'stream-to ll/stream-to - 'feed-from ll/feed-from - 'stream-to-string ll/stream-to-string - 'stream-to-out ll/stream-to-out - 'feed-from-string ll/feed-from-string - 'read-line ll/read-line}) diff --git a/src/babashka/impl/exceptions.clj b/src/babashka/impl/exceptions.clj index 2d0dc9fd..b0c882ca 100644 --- a/src/babashka/impl/exceptions.clj +++ b/src/babashka/impl/exceptions.clj @@ -1,7 +1 @@ -(ns babashka.impl.exceptions) - -(def exception-bindings - {'ArithmeticException ArithmeticException - 'java.lang.ArithmeticException ArithmeticException - 'java.lang.AssertionError AssertionError - 'AssertionError AssertionError}) +(ns babashka.impl.exceptions) \ No newline at end of file diff --git a/src/babashka/impl/me/raynes/conch/low_level.clj b/src/babashka/impl/me/raynes/conch/low_level.clj deleted file mode 100644 index 0de31bdc..00000000 --- a/src/babashka/impl/me/raynes/conch/low_level.clj +++ /dev/null @@ -1,126 +0,0 @@ -;; From https://github.com/clj-commons/conch - -(ns babashka.impl.me.raynes.conch.low-level - "A simple but flexible library for shelling out from Clojure." - {:no-doc true} - (:refer-clojure :exclude [flush read-line]) - (:require [clojure.java.io :as io]) - (:import [java.util.concurrent TimeUnit TimeoutException] - [java.io InputStream OutputStream])) - -(set! *warn-on-reflection* true) - -(defn proc - "Spin off another process. Returns the process's input stream, - output stream, and err stream as a map of :in, :out, and :err keys - If passed the optional :dir and/or :env keyword options, the dir - and enviroment will be set to what you specify. If you pass - :verbose and it is true, commands will be printed. If it is set to - :very, environment variables passed, dir, and the command will be - printed. If passed the :clear-env keyword option, then the process - will not inherit its environment from its parent process." - [& args] - (let [[cmd args] (split-with (complement keyword?) args) - args (apply hash-map args) - builder (ProcessBuilder. ^"[Ljava.lang.String;" (into-array String cmd)) - env (.environment builder)] - (when (:clear-env args) - (.clear env)) - (doseq [[k v] (:env args)] - (.put env k v)) - (when-let [dir (:dir args)] - (.directory builder (io/file dir))) - (when (:verbose args) (apply println cmd)) - (when (= :very (:verbose args)) - (when-let [env (:env args)] (prn env)) - (when-let [dir (:dir args)] (prn dir))) - (when (:redirect-err args) - (.redirectErrorStream builder true)) - (let [process (.start builder)] - {:out (.getInputStream process) - :in (.getOutputStream process) - :err (.getErrorStream process) - :process process}))) - -(defn destroy - "Destroy a process." - [process] - (.destroy ^Process (:process process))) - -;; .waitFor returns the exit code. This makes this function useful for -;; both getting an exit code and stopping the thread until a process -;; terminates. -(defn exit-code - "Waits for the process to terminate (blocking the thread) and returns - the exit code. If timeout is passed, it is assumed to be milliseconds - to wait for the process to exit. If it does not exit in time, it is - killed (with or without fire)." - ([process] (.waitFor ^Process (:process process))) - ([process timeout] - (try - (let [^java.util.concurrent.Future fut - (future (.waitFor ^Process (:process process)))] - (.get fut timeout TimeUnit/MILLISECONDS)) - (catch Exception e - (if (or (instance? TimeoutException e) - (instance? TimeoutException (.getCause e))) - (do (destroy process) - :timeout) - (throw e)))))) - -(defn flush - "Flush the output stream of a process." - [process] - (let [^OutputStream in (:in process)] - (.flush in))) - -(defn done - "Close the process's output stream (sending EOF)." - [proc] - (let [^OutputStream in (:in proc)] - (.close in))) - -(defn stream-to - "Stream :out or :err from a process to an ouput stream. - Options passed are fed to clojure.java.io/copy. They are :encoding to - set the encoding and :buffer-size to set the size of the buffer. - :encoding defaults to UTF-8 and :buffer-size to 1024." - [process from to & args] - (apply io/copy (process from) to args)) - -(defn feed-from - "Feed to a process's input stream with optional. Options passed are - fed to clojure.java.io/copy. They are :encoding to set the encoding - and :buffer-size to set the size of the buffer. :encoding defaults to - UTF-8 and :buffer-size to 1024. If :flush is specified and is false, - the process will be flushed after writing." - [process from & {flush? :flush :or {flush? true} :as all}] - (apply io/copy from (:in process) all) - (when flush? (flush process))) - -(defn stream-to-string - "Streams the output of the process to a string and returns it." - [process from & args] - (with-open [writer (java.io.StringWriter.)] - (apply stream-to process from writer args) - (str writer))) - -;; The writer that Clojure wraps System/out in for *out* seems to buffer -;; things instead of writing them immediately. This wont work if you -;; really want to stream stuff, so we'll just skip it and throw our data -;; directly at System/out. -(defn stream-to-out - "Streams the output of the process to System/out" - [process from & args] - (apply stream-to process from (System/out) args)) - -(defn feed-from-string - "Feed the process some data from a string." - [process s & args] - (apply feed-from process (java.io.StringReader. s) args)) - -(defn read-line - "Read a line from a process' :out or :err." - [process from] - (binding [*in* (io/reader (from process))] - (clojure.core/read-line))) diff --git a/src/babashka/impl/repl.clj b/src/babashka/impl/repl.clj index d97cec3a..3460aaeb 100644 --- a/src/babashka/impl/repl.clj +++ b/src/babashka/impl/repl.clj @@ -5,50 +5,72 @@ [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])) + [sci.impl.parser :as parser] + [sci.core :as sci] + [sci.impl.io :as sio])) + +(defn repl-caught + "Default :caught hook for repl" + [e] + (sci/with-bindings {sci/out @sci/err} + (sio/println (.getMessage ^Exception e)) + (sio/flush))) (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))))) + ([sci-ctx] (repl sci-ctx nil)) + ([sci-ctx {:keys [:init :read :eval :need-prompt :prompt :flush :print :caught]}] + (let [in (r/indexing-push-back-reader (r/push-back-reader @sci/in))] + (m/repl + :init (or init + #(do (sio/println "Babashka" + (str "v" (str/trim (slurp (io/resource "BABASHKA_VERSION")))) + "REPL.") + (sio/println "Use :repl/quit or :repl/exit to quit the REPL.") + (sio/println "Clojure rocks, Bash reaches.") + (sio/println))) + :read (or read + (fn [_request-prompt request-exit] + ;; (prn "PEEK" @sci/in (r/peek-char @sci/in)) + ;; (prn "PEEK" @sci/in (r/peek-char @sci/in)) this works fine + (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 (or 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 (or need-prompt (fn [] true)) + :prompt (or prompt #(sio/printf "%s=> " (-> sci-ctx :env deref :current-ns))) + :flush (or flush sio/flush) + :print (or print sio/prn) + :caught (or caught repl-caught))))) -(defn start-repl! [ctx] - (let [sci-ctx (init ctx)] - (repl sci-ctx))) +(defn start-repl! + ([sci-ctx] (start-repl! sci-ctx nil)) + ([sci-ctx opts] + (repl sci-ctx opts))) + +;;;; Scratch + +(comment + (def in (-> (java.io.StringReader. "(+ 1 2 3)") clojure.lang.LineNumberingPushbackReader.)) + (r/peek-char in) + (r/read-char in) + ) diff --git a/src/babashka/impl/socket_repl.clj b/src/babashka/impl/socket_repl.clj index 13195287..c18458d3 100644 --- a/src/babashka/impl/socket_repl.clj +++ b/src/babashka/impl/socket_repl.clj @@ -3,19 +3,17 @@ (:require [babashka.impl.clojure.core.server :as server] [babashka.impl.repl :as repl] - [clojure.string :as str] - [sci.impl.opts :refer [init]])) + [clojure.string :as str])) (set! *warn-on-reflection* true) -(defn start-repl! [host+port sci-opts] +(defn start-repl! [host+port sci-ctx] (let [parts (str/split host+port #":") [host port] (if (= 1 (count parts)) [nil (Integer. ^String (first parts))] [(first parts) (Integer. ^String (second parts))]) host+port (if-not host (str "localhost:" port) host+port) - sci-ctx (init sci-opts) socket (server/start-server {:address host :port port @@ -29,7 +27,6 @@ (server/stop-server)) (comment - (def sock (start-repl! "0.0.0.0:1666" {:env (atom {})})) @#'server/servers (stop-repl!) ) diff --git a/src/babashka/impl/utils.clj b/src/babashka/impl/utils.clj deleted file mode 100644 index 55387bc3..00000000 --- a/src/babashka/impl/utils.clj +++ /dev/null @@ -1,9 +0,0 @@ -(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 b1bd3679..c9f5c86b 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -3,26 +3,34 @@ (:require [babashka.impl.async :refer [async-namespace]] [babashka.impl.cheshire :refer [cheshire-core-namespace]] + [babashka.impl.classes :as classes] + [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.clojure.java.shell :refer [shell-namespace]] + [babashka.impl.clojure.main :refer [demunge]] + [babashka.impl.clojure.stacktrace :refer [stacktrace-namespace print-stack-trace]] [babashka.impl.csv :as csv] - [babashka.impl.xml :as xml] [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]] - [babashka.impl.utils :refer [eval-string]] - [babashka.impl.classpath :as cp] + [babashka.impl.xml :as xml] [babashka.wait :as wait] [clojure.edn :as edn] [clojure.java.io :as io] - [clojure.java.shell :as shell] [clojure.string :as str] - [sci.addons :as addons]) + [sci.addons :as addons] + [sci.core :as sci] + [sci.impl.interpreter :refer [eval-string*]] + [sci.impl.opts :as sci-opts] + [sci.impl.vars :as vars]) (:gen-class)) +(sci/alter-var-root sci/in (constantly *in*)) +(sci/alter-var-root sci/out (constantly *out*)) +(sci/alter-var-root sci/err (constantly *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 '...' @@ -31,77 +39,83 @@ (defn parse-opts [options] (let [opts (loop [options options opts-map {}] - (if-let [opt (first options)] - (case opt - ("--version") {:version true} - ("--help" "-h" "-?") {:help? true} - ("--verbose")(recur (rest options) + (if options + (let [opt (first options)] + (case opt + ("--version") {:version true} + ("--help" "-h" "-?") {:help? true} + ("--verbose")(recur (next options) + (assoc opts-map + :verbose? true)) + ("--stream") (recur (next options) + (assoc opts-map + :stream? true)) + ("--time") (recur (next options) (assoc opts-map - :verbose? true)) - ("--stream") (recur (rest options) - (assoc opts-map - :stream? true)) - ("--time") (recur (rest options) - (assoc opts-map - :time? true)) - ("-i") (recur (rest options) - (assoc opts-map - :shell-in true)) - ("-I") (recur (rest options) - (assoc opts-map - :edn-in true)) - ("-o") (recur (rest options) - (assoc opts-map - :shell-out true)) - ("-O") (recur (rest options) - (assoc opts-map - :edn-out true)) - ("-io") (recur (rest options) - (assoc opts-map - :shell-in true - :shell-out true)) - ("-IO") (recur (rest options) - (assoc opts-map - :edn-in true - :edn-out true)) - ("-f" "--file") - (let [options (rest options)] - (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) - (assoc opts-map - :socket-repl (first options)))) - ("--eval", "-e") - (let [options (rest options)] - (recur (rest options) - (assoc opts-map :expression (first options)))) - ("--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))) - (.exists (io/file opt))) + :time? true)) + ("-i") (recur (next options) + (assoc opts-map + :shell-in true)) + ("-I") (recur (next options) + (assoc opts-map + :edn-in true)) + ("-o") (recur (next options) + (assoc opts-map + :shell-out true)) + ("-O") (recur (next options) + (assoc opts-map + :edn-out true)) + ("-io") (recur (next options) + (assoc opts-map + :shell-in true + :shell-out true)) + ("-IO") (recur (next options) + (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) + (assoc opts-map + :file (first options)))) + ("--repl") + (let [options (next options)] + (recur (next options) + (assoc opts-map + :repl true))) + ("--socket-repl") + (let [options (next options)] + (recur (next options) + (assoc opts-map + :socket-repl (first options)))) + ("--eval", "-e") + (let [options (next options)] + (recur (next options) + (assoc opts-map :expression (first options)))) + ("--main", "-m") + (let [options (next options)] + (recur (next options) + (assoc opts-map :main (first options)))) + (if (some opts-map [:file :socket-repl :expression :main]) (assoc opts-map - :file opt - :command-line-args (rest options)) - (assoc opts-map - :expression opt - :command-line-args (rest options))))) + :command-line-args options) + (if (and (not= \( (first (str/trim opt))) + (.exists (io/file opt))) + (assoc opts-map + :file opt + :command-line-args (next options)) + (assoc opts-map + :expression opt + :command-line-args (next options)))))) opts-map))] opts)) @@ -122,8 +136,9 @@ (println (str "babashka v"(str/trim (slurp (io/resource "BABASHKA_VERSION")))))) (def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose] - [ ( --classpath | -cp ) ] [ ( --main | -m ) ] - ( -e | -f | --repl | --socket-repl [:] ) + [ ( --classpath | -cp ) ] [ --uberscript ] + [ ( --main | -m ) | -e | -f | + --repl | --socket-repl [:] ] [ arg* ]") (defn print-usage [] (println usage-string)) @@ -136,21 +151,24 @@ (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. - -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. + --uberscript Collect preloads, -e, -f and -m and all required namespaces from the classpath into a single executable file. + + -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*.")) @@ -167,28 +185,21 @@ Everything after that is bound to *command-line-args*.")) (edn/read {;;:readers *data-readers* :eof ::EOF} *in*)) -(defn load-file* [ctx file] - (let [s (slurp file)] - (eval-string s ctx))) +(def reflection-var (sci/new-dynamic-var '*warn-on-reflection* false)) -(defn eval* [ctx form] - (eval-string (pr-str form) ctx)) +(defn load-file* [sci-ctx f] + (let [f (io/file f) + s (slurp f)] + (sci/with-bindings {vars/file-var (.getCanonicalPath f)} + (eval-string* sci-ctx s)))) -(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 eval* [sci-ctx form] + (eval-string* sci-ctx (pr-str form))) -(defn start-socket-repl! [address ctx read-next] - (let [ctx (update ctx :bindings assoc - (with-meta '*in* - {:sci/deref! true}) - (read-next))] - (socket-repl/start-repl! address ctx) - ;; hang until SIGINT - @(promise))) +(defn start-socket-repl! [address ctx] + (socket-repl/start-repl! address ctx) + ;; hang until SIGINT + @(promise)) (defn exit [n] (throw (ex-info "" {:bb/exit-code n}))) @@ -197,6 +208,49 @@ 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 shell-namespace + '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 + 'clojure.stacktrace stacktrace-namespace + 'clojure.main {'demunge demunge} + 'clojure.repl {'demunge demunge} + 'clojure.data.xml xml/xml-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!) @@ -208,7 +262,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?) @@ -222,6 +276,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")) @@ -229,133 +284,122 @@ 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))) - ctx {:aliases '{tools.cli 'clojure.tools.cli - edn clojure.edn - wait babashka.wait - sig babashka.signal - shell clojure.java.shell - io clojure.java.io - conch me.raynes.conch.low-level - async clojure.core.async - 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 - 'clojure.edn {'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 - 'me.raynes.conch.low-level conch-namespace - 'clojure.core.async async-namespace - 'clojure.data.csv csv/csv-namespace - 'cheshire.core cheshire-core-namespace - 'clojure.data.xml xml/xml-namespace} - :bindings {'java.lang.System/exit exit ;; override exit, so we have more control - 'System/exit exit} + (let [res (cp/source-for-namespace loader namespace nil)] + (when uberscript (swap! uberscript-sources conj (:source res))) + res))) + _ (when file (vars/bindRoot vars/file-var (.getCanonicalPath (io/file file)))) + ctx {:aliases aliases + :namespaces (-> namespaces + (assoc '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)) + (assoc-in ['clojure.java.io 'resource] + #(when classpath (cp/getResource loader % {:url? true})))) + :bindings bindings :env env :features #{:bb} - :classes {'java.lang.ArithmeticException ArithmeticException - '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 - 'clojure.lang.ExceptionInfo clojure.lang.ExceptionInfo - 'java.lang.Integer Integer - 'java.io.File java.io.File - 'clojure.lang.LineNumberingPushbackReader clojure.lang.LineNumberingPushbackReader - 'java.util.regex.Pattern java.util.regex.Pattern - 'java.lang.String String - 'java.io.StringReader java.io.StringReader - 'java.io.StringWriter java.io.StringWriter - '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 - 'java.nio.file.NoSuchFileException java.nio.file.NoSuchFileException - 'java.nio.file.StandardCopyOption java.nio.file.StandardCopyOption - } + :classes classes/class-map :imports '{ArithmeticException java.lang.ArithmeticException AssertionError java.lang.AssertionError Boolean java.lang.Boolean Class java.lang.Class Double java.lang.Double Exception java.lang.Exception + IllegalArgumentException java.lang.IllegalArgumentException Integer java.lang.Integer File java.io.File + Math java.lang.Math + Object java.lang.Object + ProcessBuilder java.lang.ProcessBuilder String java.lang.String System java.lang.System Thread java.lang.Thread} - :load-fn load-fn} - ctx (update ctx :bindings assoc 'eval #(eval* ctx %) - 'load-file #(load-file* ctx %)) + :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?)))) exit-code - (or - #_(binding [*out* *err*] - (prn ">>" _opts)) - (second - (cond version - [(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 - (let [expr (if file (read-file file) expression)] - (if expr - (loop [in (read-next *in*)] - (let [ctx (update-in ctx [:namespaces 'user] assoc (with-meta '*in* - (when-not stream? - {:sci/deref! true})) in)] - (if (identical? ::EOF in) - [nil 0] ;; done streaming - (let [res [(let [res (eval-string expr ctx)] - (when (some? res) - (if-let [pr-f (cond shell-out println - edn-out prn)] - (if (coll? res) - (doseq [l res - :while (not (pipe-signal-received?))] - (pr-f l)) - (pr-f res)) - (prn res)))) 0]] - (if stream? - (recur (read-next *in*)) - res))))) - [(start-repl! ctx #(read-next *in*)) 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) + ;; handle preloads + (if exit-code exit-code + (do (when preloads (try (eval-string* sci-ctx preloads) + (catch Throwable e + (error-handler* e verbose?)))) + nil)) + exit-code + (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] + (not (str/blank? expression)) + (try + (loop [in (read-next *in*)] + (let [_ (swap! env update-in [:namespaces 'user] + assoc (with-meta '*input* + (when-not stream? + {:sci.impl/deref! true})) + (sci/new-dynamic-var '*input* in))] + (if (identical? ::EOF in) + [nil 0] ;; done streaming + (let [res [(let [res (eval-string* sci-ctx expression)] + (when (some? res) + (if-let [pr-f (cond shell-out println + edn-out prn)] + (if (coll? res) + (doseq [l res + :while (not (pipe-signal-received?))] + (pr-f l)) + (pr-f res)) + (prn res)))) 0]] + (if stream? + (recur (read-next *in*)) + res))))) + (catch Throwable e + (error-handler* e verbose?))) + uberscript [nil 0] + :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 preloads :append true) + (spit uberscript-out expression :append true))) (when time? (binding [*out* *err*] (println "bb took" (str (- t1 t0) "ms.")))) - (flush) exit-code)) (defn -main diff --git a/src/babashka/wait.clj b/src/babashka/wait.clj index dbf54d2b..af2cc9d4 100644 --- a/src/babashka/wait.clj +++ b/src/babashka/wait.clj @@ -17,8 +17,8 @@ opts) t0 (System/currentTimeMillis)] (loop [] - (let [v (try (Socket. host port) - (- (System/currentTimeMillis) t0) + (let [v (try (with-open [_ (Socket. host port)] + (- (System/currentTimeMillis) t0)) (catch ConnectException _e (let [took (- (System/currentTimeMillis) t0)] (if (and timeout (>= took timeout)) diff --git a/test-resources/babashka/src_for_classpath_test/ns_with_error.clj b/test-resources/babashka/src_for_classpath_test/ns_with_error.clj new file mode 100644 index 00000000..d1f909ba --- /dev/null +++ b/test-resources/babashka/src_for_classpath_test/ns_with_error.clj @@ -0,0 +1,4 @@ +(ns ns-with-error) + +(def x 0) +(def y (/ 1 0)) diff --git a/test/babashka/classpath_test.clj b/test/babashka/classpath_test.clj index b3b876d3..dd20b197 100644 --- a/test/babashka/classpath_test.clj +++ b/test/babashka/classpath_test.clj @@ -2,7 +2,8 @@ (:require [babashka.test-utils :as tu] [clojure.edn :as edn] - [clojure.test :as t :refer [deftest is]])) + [clojure.test :as t :refer [deftest is]] + [clojure.java.io :as io])) (defn bb [input & args] (edn/read-string (apply tu/bb (when (some? input) (str input)) (map str args)))) @@ -25,3 +26,26 @@ (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")))) + +(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" + " +(try + (require '[ns-with-error]) + (catch Exception nil)) +(nil? (resolve 'ns-with-error/x))")))) + +(deftest resource-test + (let [tmp-file (java.io.File/createTempFile "icon" ".png")] + (.deleteOnExit tmp-file) + (bb nil "--classpath" "logo" "-e" (format "(io/copy (io/input-stream (io/resource \"icon.png\")) (io/file \"%s\"))" (.getPath tmp-file))) + (is (= (.length (io/file "logo" "icon.png")) + (.length tmp-file))))) diff --git a/test/babashka/file_var_test.clj b/test/babashka/file_var_test.clj new file mode 100644 index 00000000..909373bb --- /dev/null +++ b/test/babashka/file_var_test.clj @@ -0,0 +1,17 @@ +(ns babashka.file-var-test + (:require + [babashka.test-utils :as tu] + [clojure.test :as t :refer [deftest is]] + [clojure.string :as str])) + +(defn bb [input & args] + (apply tu/bb (when (some? input) (str input)) (map str args))) + +(deftest file-var-test + (let [[f1 f2 f3] + (str/split (bb nil "--classpath" "test/babashka/scripts" + "test/babashka/scripts/file_var.bb") + #"\n")] + (is (str/ends-with? f1 "file_var_classpath.bb")) + (is (str/ends-with? f2 "loaded_by_file_var.bb")) + (is (str/ends-with? f3 "file_var.bb")))) diff --git a/test/babashka/http_connection_test.clj b/test/babashka/http_connection_test.clj new file mode 100644 index 00000000..ec78fa12 --- /dev/null +++ b/test/babashka/http_connection_test.clj @@ -0,0 +1,21 @@ +(ns babashka.http-connection-test + (:require + [babashka.test-utils :as tu] + [clojure.test :as t :refer [deftest is]] + [clojure.string :as str])) + +(defn bb [& args] + (apply tu/bb nil (map str args))) + +(deftest open-connection-test + (is (= "\"1\"" (str/trim (bb "-e" " +(require '[cheshire.core :as json]) +(let [conn ^java.net.HttpURLConnection (.openConnection (java.net.URL. \"https://postman-echo.com/get?foo=1\"))] + (.setConnectTimeout conn 1000) + (.setRequestProperty conn \"Content-Type\" \"application/json\") ;; nonsensical, but to test if this method exists + (.connect conn) + (let [is (.getInputStream conn) + err (.getErrorStream conn) + response (json/decode (slurp is) true)] + (-> response :args :foo))) +"))))) diff --git a/test/babashka/impl/clojure/java/shell_test.clj b/test/babashka/impl/clojure/java/shell_test.clj new file mode 100644 index 00000000..0f1cd8db --- /dev/null +++ b/test/babashka/impl/clojure/java/shell_test.clj @@ -0,0 +1,17 @@ +(ns babashka.impl.clojure.java.shell-test + (:require [clojure.test :as t :refer [deftest is testing]] + [babashka.test-utils :as test-utils] + [clojure.string :as str])) + +(deftest with-sh-env-test + (is (= "\"BAR\"" + (str/trim (test-utils/bb nil " +(-> (shell/with-sh-env {:FOO \"BAR\"} + (shell/sh \"bash\" \"-c\" \"echo $FOO\")) + :out + str/trim)")))) + (is (str/includes? (str/trim (test-utils/bb nil " +(-> (shell/with-sh-dir \"logo\" + (shell/sh \"ls\")) + :out)")) + "icon.svg"))) diff --git a/test/babashka/impl/repl_test.clj b/test/babashka/impl/repl_test.clj index 8adbde77..d0f1bc5b 100644 --- a/test/babashka/impl/repl_test.clj +++ b/test/babashka/impl/repl_test.clj @@ -2,32 +2,38 @@ (:require [babashka.impl.repl :refer [start-repl!]] [clojure.string :as str] - [clojure.test :as t :refer [deftest is]])) + [clojure.test :as t :refer [deftest is]] + [sci.impl.opts :refer [init]] + [sci.core :as sci] + [sci.impl.vars :as vars])) (set! *warn-on-reflection* true) +;; (vars/bindRoot sci/in *in*) +;; (vars/bindRoot sci/out *out*) +(vars/bindRoot sci/err *err*) + (defn repl! [] - (start-repl! {:bindings {(with-meta '*in* - {:sci/deref! true}) - (delay [1 2 3]) - '*command-line-args* - ["a" "b" "c"]} - :env (atom {})})) + (start-repl! (init {:bindings {'*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") + (is (str/includes? (sci/with-out-str + (sci/with-in-str (str expr "\n:repl/quit") (repl!))) expected))) (deftest repl-test + (assert-repl "1" "1") + (assert-repl "[1 2 3]" "[1 2 3]") + (assert-repl "()" "()") (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]")) + (assert-repl "*command-line-args*" "[\"a\" \"b\" \"c\"]")) ;;;; Scratch diff --git a/test/babashka/impl/socket_repl_test.clj b/test/babashka/impl/socket_repl_test.clj index 085561dd..83038f0e 100644 --- a/test/babashka/impl/socket_repl_test.clj +++ b/test/babashka/impl/socket_repl_test.clj @@ -5,7 +5,8 @@ [clojure.java.shell :refer [sh]] [clojure.string :as str] [clojure.test :as t :refer [deftest is testing]] - [clojure.java.io :as io])) + [clojure.java.io :as io] + [sci.impl.opts :refer [init]])) (set! *warn-on-reflection* true) @@ -31,13 +32,10 @@ (deftest socket-repl-test (try (if tu/jvm? - (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 {}) - :features #{:bb}}) + (start-repl! "0.0.0.0:1666" (init {:bindings {'*command-line-args* + ["a" "b" "c"]} + :env (atom {}) + :features #{:bb}})) (future (sh "bash" "-c" "echo '[1 2 3]' | ./bb --socket-repl 0.0.0.0:1666 a b c"))) @@ -47,8 +45,6 @@ (sh "bash" "-c" "lsof -t -i:1666")))))) (is (socket-command "(+ 1 2 3)" "user=> 6")) - (testing "*in*" - (is (socket-command "*in*" "[1 2 3]"))) (testing "*command-line-args*" (is (socket-command '*command-line-args* "\"a\" \"b\" \"c\""))) (testing "&env" @@ -72,7 +68,7 @@ (dotimes [_ 1000] (t/run-tests)) (stop-repl!) - (start-repl! "0.0.0.0:1666" {:bindings {(with-meta '*in* + (start-repl! "0.0.0.0:1666" {:bindings {(with-meta '*input* {:sci/deref! true}) (delay [1 2 3]) '*command-line-args* diff --git a/test/babashka/java_time_test.clj b/test/babashka/java_time_test.clj new file mode 100644 index 00000000..65b22709 --- /dev/null +++ b/test/babashka/java_time_test.clj @@ -0,0 +1,19 @@ +(ns babashka.java-time-test + (:require + [babashka.test-utils :as test-utils] + [clojure.edn :as edn] + [clojure.test :as test :refer [deftest is]])) + +(defn bb [expr] + (edn/read-string (apply test-utils/bb nil [(str expr)]))) + +(deftest java-time-test + (is (= "2019-12-18" (bb '(str (java.time.LocalDate/of 2019 12 18))))) + (is (= "2019-12-01" (bb '(str + (-> (java.time.LocalDate/of 2019 12 18) + (.minusDays 17)))))) + (is (= "MONDAY" (bb '(str java.time.DayOfWeek/MONDAY)))) + (is (= "18-12-2019 16:01:41" + (bb '(.format + (java.time.LocalDateTime/parse "2019-12-18T16:01:41.485") + (java.time.format.DateTimeFormatter/ofPattern "dd-MM-yyyy HH:mm:ss")))))) diff --git a/test/babashka/main_test.clj b/test/babashka/main_test.clj index e1f2a224..0e90881b 100644 --- a/test/babashka/main_test.clj +++ b/test/babashka/main_test.clj @@ -3,10 +3,10 @@ [babashka.main :as main] [babashka.test-utils :as test-utils] [clojure.edn :as edn] + [clojure.java.io :as io] [clojure.java.shell :refer [sh]] [clojure.string :as str] [clojure.test :as test :refer [deftest is testing]] - [clojure.java.io :as io] [sci.core :as sci])) (defn bb [input & args] @@ -21,58 +21,58 @@ (testing "distinguish automatically between expression or file name" (is (= {:expression "(println 123)" - :command-line-args []} + :command-line-args nil} (main/parse-opts ["(println 123)"]))) (is (= {:file "src/babashka/main.clj" - :command-line-args []} + :command-line-args nil} (main/parse-opts ["src/babashka/main.clj"]))) (is (= {:expression "does-not-exist" - :command-line-args []} + :command-line-args nil} (main/parse-opts ["does-not-exist"]))))) (deftest main-test (testing "-io behaves as identity" - (= "foo\nbar\n" (test-utils/bb "foo\nbar\n" "-io" "*in*"))) + (= "foo\nbar\n" (test-utils/bb "foo\nbar\n" "-io" "*input*"))) (testing "if and when" - (is (= 1 (bb 0 '(if (zero? *in*) 1 2)))) - (is (= 2 (bb 1 '(if (zero? *in*) 1 2)))) - (is (= 1 (bb 0 '(when (zero? *in*) 1)))) - (is (nil? (bb 1 '(when (zero? *in*) 1))))) + (is (= 1 (bb 0 '(if (zero? *input*) 1 2)))) + (is (= 2 (bb 1 '(if (zero? *input*) 1 2)))) + (is (= 1 (bb 0 '(when (zero? *input*) 1)))) + (is (nil? (bb 1 '(when (zero? *input*) 1))))) (testing "and and or" - (is (= false (bb 0 '(and false true *in*)))) - (is (= 0 (bb 0 '(and true true *in*)))) - (is (= 1 (bb 1 '(or false false *in*)))) - (is (= false (bb false '(or false false *in*)))) - (is (= 3 (bb false '(or false false *in* 3))))) + (is (= false (bb 0 '(and false true *input*)))) + (is (= 0 (bb 0 '(and true true *input*)))) + (is (= 1 (bb 1 '(or false false *input*)))) + (is (= false (bb false '(or false false *input*)))) + (is (= 3 (bb false '(or false false *input* 3))))) (testing "fn" - (is (= 2 (bb 1 "(#(+ 1 %) *in*)"))) + (is (= 2 (bb 1 "(#(+ 1 %) *input*)"))) (is (= [1 2 3] (bb 1 "(map #(+ 1 %) [0 1 2])"))) - (is (= 1 (bb 1 "(#(when (odd? *in*) *in*))")))) + (is (= 1 (bb 1 "(#(when (odd? *input*) *input*))")))) (testing "map" (is (= [1 2 3] (bb 1 '(map inc [0 1 2]))))) (testing "keep" (is (= [false true false] (bb 1 '(keep odd? [0 1 2]))))) (testing "->" - (is (= 4 (bb 1 '(-> *in* inc inc (inc)))))) + (is (= 4 (bb 1 '(-> *input* inc inc (inc)))))) (testing "->>" - (is (= 10 (edn/read-string (test-utils/bb "foo\n\baar\baaaaz" "-i" "(->> *in* (map count) (apply max))"))))) + (is (= 10 (edn/read-string (test-utils/bb "foo\n\baar\baaaaz" "-i" "(->> *input* (map count) (apply max))"))))) (testing "literals" (is (= {:a 4 :b {:a 2} :c [1 1] :d #{1 2}} - (bb 1 '{:a (+ 1 2 *in*) - :b {:a (inc *in*)} - :c [*in* *in*] - :d #{*in* (inc *in*)}})))) + (bb 1 '{:a (+ 1 2 *input*) + :b {:a (inc *input*)} + :c [*input* *input*] + :d #{*input* (inc *input*)}})))) (testing "shuffle the contents of a file" (let [in "foo\n Clojure is nice. \nbar\n If you're nice to clojure. " in-lines (set (str/split in #"\n")) out (test-utils/bb in "-io" - (str '(shuffle *in*))) + (str '(shuffle *input*))) out-lines (set (str/split out #"\n"))] (is (= in-lines out-lines)))) (testing "find occurrences in file by line number" @@ -80,14 +80,14 @@ (-> (bb "foo\n Clojure is nice. \nbar\n If you're nice to clojure. " "-i" - "(map-indexed #(-> [%1 %2]) *in*)") - (bb "(keep #(when (re-find #\"(?i)clojure\" (second %)) (first %)) *in*)")))))) + "(map-indexed #(-> [%1 %2]) *input*)") + (bb "(keep #(when (re-find #\"(?i)clojure\" (second %)) (first %)) *input*)")))))) (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" + (testing "bb doesn't wait for input if *input* isn't used" (is (= "2\n" (with-out-str (main/main "(inc 1)")))))) (deftest System-test @@ -114,12 +114,12 @@ (is (re-find #"doctype html" resp)))) (deftest stream-test - (is (= "2\n3\n4\n" (test-utils/bb "1 2 3" "--stream" "(inc *in*)"))) - (is (= "2\n3\n4\n" (test-utils/bb "{:x 2} {:x 3} {:x 4}" "--stream" "(:x *in*)"))) + (is (= "2\n3\n4\n" (test-utils/bb "1 2 3" "--stream" "(inc *input*)"))) + (is (= "2\n3\n4\n" (test-utils/bb "{:x 2} {:x 3} {:x 4}" "--stream" "(:x *input*)"))) (let [x "foo\n\bar\n"] - (is (= x (test-utils/bb x "--stream" "-io" "*in*")))) + (is (= x (test-utils/bb x "--stream" "-io" "*input*")))) (let [x "f\n\b\n"] - (is (= x (test-utils/bb x "--stream" "-io" "(subs *in* 0 1)"))))) + (is (= x (test-utils/bb x "--stream" "-io" "(subs *input* 0 1)"))))) (deftest load-file-test (let [tmp (java.io.File/createTempFile "script" ".clj")] @@ -145,29 +145,35 @@ (deftest pipe-test (when test-utils/native? (let [out (:out (sh "bash" "-c" "./bb -o '(range)' | - ./bb --stream '(* *in* *in*)' | + ./bb --stream '(* *input* *input*)' | head -n10")) out (str/split-lines out) out (map edn/read-string out)] (is (= (take 10 (map #(* % %) (range))) out)))) (when test-utils/native? (let [out (:out (sh "bash" "-c" "./bb -O '(repeat \"dude\")' | - ./bb --stream '(str *in* \"rino\")' | - ./bb -I '(take 3 *in*)'")) + ./bb --stream '(str *input* \"rino\")' | + ./bb -I '(take 3 *input*)'")) out (edn/read-string out)] (is (= '("duderino" "duderino" "duderino") out))))) (deftest lazy-text-in-test (when test-utils/native? - (let [out (:out (sh "bash" "-c" "yes | ./bb -i '(take 2 *in*)'")) + (let [out (:out (sh "bash" "-c" "yes | ./bb -i '(take 2 *input*)'")) out (edn/read-string out)] (is (= '("y" "y") out))))) (deftest future-test (is (= 6 (bb nil "@(future (+ 1 2 3))")))) -(deftest conch-test - (is (str/includes? (bb nil "(->> (conch/proc \"ls\") (conch/stream-to-string :out))") +(deftest process-builder-test + (is (str/includes? (bb nil " +(def ls (-> (ProcessBuilder. [\"ls\"]) (.start))) +(def input (.getOutputStream ls)) +(.write (io/writer input) \"hello\") ;; dummy test just to see if this works +(def output (.getInputStream ls)) +(assert (int? (.waitFor ls))) +(slurp output)") "LICENSE"))) (deftest create-temp-file-test @@ -181,11 +187,14 @@ temp-dir-path)))))) (deftest wait-for-port-test - (is (= :timed-out - (bb nil "(def web-server (conch/proc \"python\" \"-m\" \"SimpleHTTPServer\" \"7171\")) - (wait/wait-for-port \"127.0.0.1\" 7171) - (conch/destroy web-server) - (wait/wait-for-port \"localhost\" 7172 {:default :timed-out :timeout 50})")))) + (let [server (test-utils/start-server! 1777)] + (is (= 1777 (:port (bb nil "(wait/wait-for-port \"127.0.0.1\" 1777)")))) + (test-utils/stop-server! server) + (is (= :timed-out (bb nil "(wait/wait-for-port \"127.0.0.1\" 1777 {:default :timed-out :timeout 50})")))) + (let [edn (bb nil (io/file "test" "babashka" "scripts" "socket_server.bb"))] + (is (= "127.0.0.1" (:host edn))) + (is (= 1777 (:port edn))) + (is (number? (:took edn))))) (deftest wait-for-path-test (let [temp-dir-path (System/getProperty "java.io.tmpdir")] @@ -274,8 +283,8 @@ f2 (.toFile p')] (bb nil (format "(let [f (io/file \"%s\") - p (.toPath (io/file f)) - p' (.resolveSibling p \"f2\")] + p (.toPath (io/file f)) + p' (.resolveSibling p \"f2\")] (.delete (.toFile p')) (dotimes [_ 2] (try @@ -287,6 +296,48 @@ (deftest future-print-test (testing "the root binding of sci/*out*" - (is (= "hello" (bb nil "@(future (prn \"hello\"))")))) + (is (= "hello" (bb nil "@(future (prn \"hello\"))"))))) +(deftest Math-test + (is (== 8.0 (bb nil "(Math/pow 2 3)")))) + +(deftest Base64-test + (is (= "babashka" + (bb nil "(String. (.decode (java.util.Base64/getDecoder) (.encode (java.util.Base64/getEncoder) (.getBytes \"babashka\"))))")))) + +(deftest Thread-test + (is (= "hello" (bb nil "(doto (java.lang.Thread. (fn [] (prn \"hello\"))) (.start) (.join)) nil")))) + +(deftest dynvar-test + (is (= 1 (bb nil "(binding [*command-line-args* 1] *command-line-args*)"))) + (is (= 1 (bb nil "(binding [*input* 1] *input*)")))) + +(deftest file-in-error-msg-test + (is (thrown-with-msg? Exception #"error.bb" + (bb nil (.getPath (io/file "test" "babashka" "scripts" "error.bb")))))) + +(deftest compatibility-test + (is (true? (bb nil "(set! *warn-on-reflection* true)")))) + +(deftest clojure-main-repl-test + (is (= "\"> foo!\\nnil\\n> \"\n" (test-utils/bb nil " +(defn foo [] (println \"foo!\")) +(with-out-str + (with-in-str \"(foo)\" + (clojure.main/repl :init (fn []) :prompt (fn [] (print \"> \")))))")))) + +(deftest command-line-args-test + (is (true? (bb nil "(nil? *command-line-args*)"))) + (is (= ["1" "2" "3"] (bb nil "*command-line-args*" "1" "2" "3")))) + +(deftest need-constructors-test + (testing "the clojure.lang.Delay constructor works" + (is (= 1 (bb nil "@(delay 1)")))) + (testing "the clojure.lang.MapEntry constructor works" + (is (true? (bb nil "(= (first {1 2}) (clojure.lang.MapEntry. 1 2))"))))) + +;;;; Scratch + +(comment + (dotimes [_ 10] (wait-for-port-test)) ) diff --git a/test/babashka/scripts/error.bb b/test/babashka/scripts/error.bb new file mode 100644 index 00000000..f4333f8b --- /dev/null +++ b/test/babashka/scripts/error.bb @@ -0,0 +1 @@ +(/ 1 0) diff --git a/test/babashka/scripts/file_var.bb b/test/babashka/scripts/file_var.bb new file mode 100644 index 00000000..a35bf126 --- /dev/null +++ b/test/babashka/scripts/file_var.bb @@ -0,0 +1,6 @@ +(ns file-var + (:require [clojure.java.io :as io])) + +(require '[file-var-classpath]) +(load-file (io/file "test" "babashka" "scripts" "loaded_by_file_var.bb")) +(println *file*) diff --git a/test/babashka/scripts/file_var_classpath.bb b/test/babashka/scripts/file_var_classpath.bb new file mode 100644 index 00000000..9c67bd2f --- /dev/null +++ b/test/babashka/scripts/file_var_classpath.bb @@ -0,0 +1 @@ +(println *file*) diff --git a/test/babashka/scripts/loaded_by_file_var.bb b/test/babashka/scripts/loaded_by_file_var.bb new file mode 100644 index 00000000..c34fdfa2 --- /dev/null +++ b/test/babashka/scripts/loaded_by_file_var.bb @@ -0,0 +1,3 @@ +(ns loaded-by-file-var) + +(println *file*) diff --git a/test/babashka/scripts/socket_server.bb b/test/babashka/scripts/socket_server.bb new file mode 100644 index 00000000..76467255 --- /dev/null +++ b/test/babashka/scripts/socket_server.bb @@ -0,0 +1,22 @@ +(require '[babashka.wait :as wait]) + +(defn socket-loop [^java.net.ServerSocket server] + (with-open [listener server] + (loop [] + (with-open [socket (.accept listener)] + (let [input-stream (.getInputStream socket)] + (print (slurp input-stream)) + (flush))) + (recur)))) + +(defn start-server! [port] + (let [server (java.net.ServerSocket. port)] + (future (socket-loop server)) + server)) + +(defn stop-server! [^java.net.ServerSocket server] + (.close server)) + +(let [server (start-server! 1777)] + (prn (wait/wait-for-port "127.0.0.1" 1777)) + (stop-server! server)) diff --git a/test/babashka/test_utils.clj b/test/babashka/test_utils.clj index 5a42415d..071e263c 100644 --- a/test/babashka/test_utils.clj +++ b/test/babashka/test_utils.clj @@ -2,7 +2,8 @@ (:require [babashka.main :as main] [me.raynes.conch :refer [let-programs] :as sh] - [sci.core :as sci])) + [sci.core :as sci] + [sci.impl.vars :as vars])) (set! *warn-on-reflection* true) @@ -14,17 +15,25 @@ 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) - {:stdout (str os) - :stderr (str es)}))))))) + (try + (when input (vars/bindRoot sci/in is)) + (vars/bindRoot sci/out os) + (vars/bindRoot sci/err es) + (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) + {:stdout (str os) + :stderr (str es)}))))) + (finally + (when input (vars/bindRoot sci/in *in*)) + (vars/bindRoot sci/out *out*) + (vars/bindRoot sci/err *err*))))) (defn bb-native [input & args] (let-programs [bb "./bb"] @@ -49,3 +58,20 @@ (if jvm? (println "==== Testing JVM version") (println "==== Testing native version")) + +(defn socket-loop [^java.net.ServerSocket server] + (with-open [listener server] + (loop [] + (with-open [socket (.accept listener)] + (let [input-stream (.getInputStream socket)] + (print (slurp input-stream)) + (flush))) + (recur)))) + +(defn start-server! [port] + (let [server (java.net.ServerSocket. port)] + (future (socket-loop server)) + server)) + +(defn stop-server! [^java.net.ServerSocket server] + (.close server))