[#146] support --classpath / -cp and --main / -m (#150)

This commit is contained in:
Michiel Borkent 2019-12-12 23:07:35 +01:00 committed by GitHub
parent c5d6768158
commit a74be0ad1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 470 additions and 60 deletions

View file

@ -3,14 +3,16 @@
rm -rf /tmp/release
mkdir -p /tmp/release
cp bb /tmp/release
cp src-bash/bbk /tmp/release
VERSION=$(cat resources/BABASHKA_VERSION)
cd /tmp/release
## release binary as zip archive
zip "babashka-$VERSION-$BABASHKA_PLATFORM-amd64.zip" bb
zip "babashka-$VERSION-$BABASHKA_PLATFORM-amd64.zip" bb bbk
## cleanup
rm bb
rm bb bbk

3
.gitignore vendored
View file

@ -13,3 +13,6 @@ pom.xml.asc
/bb
.clj-kondo/.cache
!java/src/babashka/impl/LockFix.class
!test-resources/babashka/src_for_classpath_test/foo.jar
.cpcache
deps.edn

115
README.md
View file

@ -311,6 +311,83 @@ $ cat script.clj
("hello" "1" "2" "3")
```
## Preloads
The environment variable `BABASHKA_PRELOADS` allows to define code that will be
available in all subsequent usages of babashka.
``` shellsession
BABASHKA_PRELOADS='(defn foo [x] (+ x 2))'
BABASHKA_PRELOADS=$BABASHKA_PRELOADS' (defn bar [x] (* x 2))'
export BABASHKA_PRELOADS
```
Note that you can concatenate multiple expressions. Now you can use these functions in babashka:
``` shellsession
$ bb '(-> (foo *in*) bar)' <<< 1
6
```
You can also preload an entire file using `load-file`:
``` shellsession
export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")'
```
Note that `*in*` is not available in preloads.
## Classpath
Babashka accepts a `--classpath` option that will be used to search for
namespaces and load them:
``` clojure
$ cat src/my/namespace.clj
(ns my.namespace)
(defn -main [& _args]
(println "Hello from my namespace!"))
$ bb --classpath src --main my.namespace
Hello from my namespace!
```
Note that you can use the `clojure` tool to produce classpaths and download dependencies:
``` shellsession
$ cat deps.edn
{:deps
{my_gist_script
{:git/url "https://gist.github.com/borkdude/263b150607f3ce03630e114611a4ef42"
:sha "cfc761d06dfb30bb77166b45d439fe8fe54a31b8"}}}
$ CLASSPATH=$(clojure -Spath)
$ bb --classpath "$CLASSPATH" --main my-gist-script
Hello from gist script!
```
The `bbk` shell script is a thin wrapper around the `clojure` tool, so you can
use Babashka projects in a similar way:
``` shellsession
$ bbk -m my-gist-script
Hello from gist script!
```
The script will call `bb` with the `--classpath` argument as a result of calling
`clojure`.
If there is no `--classpath` argument, the `BABASHKA_CLASSPATH` environment
variable will be used if set:
``` shellsession
$ export BABASHKA_CLASSPATH=$(clojure -Spath)
$ export BABASHKA_PRELOADS="(require '[my-gist-script])"
$ bb "(my-gist-script/-main)"
Hello from gist script!
```
## Parsing command line arguments
Babashka ships with `clojure.tools.cli`:
@ -348,32 +425,6 @@ $ ./bb example.clj
babashka doesn't support in-ns yet!
```
## Preloads
The environment variable `BABASHKA_PRELOADS` allows to define code that will be
available in all subsequent usages of babashka.
``` shellsession
BABASHKA_PRELOADS='(defn foo [x] (+ x 2))'
BABASHKA_PRELOADS=$BABASHKA_PRELOADS' (defn bar [x] (* x 2))'
export BABASHKA_PRELOADS
```
Note that you can concatenate multiple expressions. Now you can use these functions in babashka:
``` shellsession
$ bb '(-> (foo *in*) bar)' <<< 1
6
```
You can also preload an entire file using `load-file`:
``` shellsession
export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")'
```
Note that `*in*` is not available in preloads.
## Socket REPL
Start the socket REPL like this:
@ -448,16 +499,8 @@ Differences with Clojure:
- A subset of Java classes are supported.
- Only the `clojure.core`, `clojure.set` and `clojure.string` namespaces are
available from Clojure.
- There is no classpath and no support for loading code from Maven/Clojars
dependencies. However, you can use `load-file` to load external code from
disk.
- `require` does not load files; it only provides a way to create different
aliases for included namespaces, which makes it easier to make scripts
portable between the JVM and babashka.
- Only the `clojure.core`, `clojure.set`, `clojure.string` and `clojure.walk`
namespaces are available from Clojure.
- Interpretation comes with overhead. Therefore tight loops are likely slower
than in Clojure on the JVM.

2
sci

@ -1 +1 @@
Subproject commit 60778bfaabf4fbed635d9490bac88b86fdfb0e4d
Subproject commit 968734ac60a2410f0b716df3829a5287f263e8f0

View file

@ -1,6 +1,15 @@
#!/usr/bin/env bash
set -eo pipefail
export BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")'
BABASHKA_PRELOADS=""
BABASHKA_CLASSPATH=""
lein test "$@"
lein test
BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")'
BABASHKA_PRELOADS_TEST=true
lein test :only babashka.main-test/preloads-test
BABASHKA_PRELOADS="(require '[env-ns])"
BABASHKA_CLASSPATH_TEST=true
BABASHKA_CLASSPATH="test-resources/babashka/src_for_classpath_test/env"
lein test :only babashka.classpath-test/classpath-env-test

219
src-bash/bbk Executable file
View file

@ -0,0 +1,219 @@
#!/usr/bin/env bash
set -e
function join { local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; }
# Extract opts
print_classpath=false
describe=false
verbose=false
force=false
repro=false
tree=false
pom=false
resolve_tags=false
help=false
resolve_aliases=()
classpath_aliases=()
main_aliases=()
all_aliases=()
while [ $# -gt 0 ]
do
case "$1" in
-J*)
shift
;;
-R*)
resolve_aliases+=("${1:2}")
shift
;;
-C*)
classpath_aliases+=("${1:2}")
shift
;;
-O*)
shift
;;
-M*)
main_aliases+=("${1:2}")
shift
;;
-A*)
all_aliases+=("${1:2}")
shift
;;
-Sdeps)
shift
deps_data="${1}"
shift
;;
-Scp)
shift
force_cp="${1}"
shift
;;
-Spath)
print_classpath=true
shift
;;
-Sverbose)
verbose=true
shift
;;
-Sdescribe)
describe=true
shift
;;
-Sforce)
force=true
shift
;;
-Srepro)
repro=true
shift
;;
-Stree)
tree=true
shift
;;
-Spom)
pom=true
shift
;;
-Sresolve-tags)
resolve_tags=true
shift
;;
-S*)
echo "Invalid option: $1"
exit 1
;;
-h|--help|"-?")
if [[ ${#main_aliases[@]} -gt 0 ]] || [[ ${#all_aliases[@]} -gt 0 ]]; then
break
else
help=true
shift
fi
;;
*)
break
;;
esac
done
# Find clojure executable
set +e
CLOJURE_CMD=$(type -p clojure)
set -e
if [[ ! -n "$CLOJURE_CMD" ]]; then
>&2 echo "Couldn't find 'clojure'."
>&2 echo "You can launch Babashka directly using 'bb'."
>&2 echo "To use 'bbk', please ensure 'clojure' is installed and on"
>&2 echo "your path. See https://clojure.org/guides/getting_started"
exit 1
fi
if "$help"; then
cat <<-END
Usage: bbk [dep-opt*] [bb-opt*] [arg*]
The bbk script is a runner for Babashka which ultimately constructs and
invokes a command-line of the form:
bb --classpath classpath [bb-opt*] [*args]
The dep-opts are used to build the classpath using the clojure tool:
-Ralias... Concatenated resolve-deps aliases, ex: -R:bench:1.9
-Calias... Concatenated make-classpath aliases, ex: -C:dev
-Malias... Concatenated main option aliases, ex: -M:test
-Aalias... Concatenated aliases of any kind, ex: -A:dev:mem
-Sdeps EDN Deps data to use as the final deps file
-Spath Compute classpath and echo to stdout only
-Scp CP Do NOT compute or cache classpath, use this one instead
-Srepro Ignore the ~/.clojure/deps.edn config file
-Sforce Force recomputation of the classpath (don't use the cache)
-Spom Generate (or update existing) pom.xml with deps and paths
-Stree Print dependency tree
-Sresolve-tags Resolve git coordinate tags to shas and update deps.edn
-Sverbose Print important path info to console
-Sdescribe Print environment and command parsing info as data
Additionally, for compatibility with clojure, -Jopt and -Oalias... dep-opts
are accepted but ignored.
Babashka options:
END
bb -h | tail -n +9
exit 0
fi
# Execute resolve-tags command
if "$resolve_tags"; then
"$CLOJURE_CMD" -Sresolve-tags
exit
fi
clojure_args=()
if [[ -n "$deps_data" ]]; then
clojure_args+=("-Sdeps" "$deps_data")
fi
if [[ ${#resolve_aliases[@]} -gt 0 ]]; then
clojure_args+=("-R$(join '' ${resolve_aliases[@]})")
fi
if [[ ${#classpath_aliases[@]} -gt 0 ]]; then
clojure_args+=("-C$(join '' ${classpath_aliases[@]})")
fi
if [[ ${#main_aliases[@]} -gt 0 ]]; then
clojure_args+=("-M$(join '' ${main_aliases[@]})")
fi
if [[ ${#all_aliases[@]} -gt 0 ]]; then
clojure_args+=("-A$(join '' ${all_aliases[@]})")
fi
if "$repro"; then
clojure_args+=("-Srepro")
fi
if "$force"; then
clojure_args+=("-Sforce")
fi
if "$pom"; then
if "$verbose"; then
clojure_args+=("-Sverbose")
fi
"$CLOJURE_CMD" "${clojure_args[@]}" -Spom
elif "$describe"; then
if "$verbose"; then
clojure_args+=("-Sverbose")
fi
"$CLOJURE_CMD" "${clojure_args[@]}" -Sdescribe
elif "$tree"; then
if "$verbose"; then
clojure_args+=("-Sverbose")
fi
"$CLOJURE_CMD" "${clojure_args[@]}" -Stree
else
set -f
if [[ -n "$force_cp" ]]; then
cp="$force_cp"
else
if "$verbose"; then
"$CLOJURE_CMD" "${clojure_args[@]}" -Sverbose -e nil
fi
cp=`"$CLOJURE_CMD" "${clojure_args[@]}" -Spath`
fi
if "$print_classpath"; then
echo $cp
else
if [[ ${#main_aliases[@]} -gt 0 ]] || [[ ${#all_aliases[@]} -gt 0 ]]; then
# Attempt to extract the main cache filename by parsing the output of -Sverbose
cp_file=`"$CLOJURE_CMD" "${clojure_args[@]}" -Sverbose -Spath | grep cp_file | cut -d = -f 2 | sed 's/^ *//g'`
main_file="${cp_file%.cp}.main"
fi
if [[ -e "$main_file" ]]; then
main_cache_opts=($(cat "$main_file"))
fi
exec bb --classpath "$cp" "${main_cache_opts[@]}" "$@"
fi
fi

View file

@ -0,0 +1,62 @@
(ns babashka.impl.classpath
{:no-doc true}
(:require [clojure.java.io :as io]
[clojure.string :as str])
(:import [java.util.jar JarFile JarFile$JarFileEntry]))
(set! *warn-on-reflection* true)
(defprotocol IResourceResolver
(getResource [this path]))
(deftype DirectoryResolver [path]
IResourceResolver
(getResource [this resource-path]
(let [f (io/file path resource-path)]
(when (.exists f)
(slurp f)))))
(defn path-from-jar
[^java.io.File jar-file path]
(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)]
entry)))
(deftype JarFileResolver [path]
IResourceResolver
(getResource [this resource-path]
(path-from-jar path resource-path)))
(defn part->entry [part]
(if (str/ends-with? part ".jar")
(JarFileResolver. (io/file part))
(DirectoryResolver. (io/file part))))
(deftype Loader [entries]
IResourceResolver
(getResource [this resource-path]
(some #(getResource % resource-path) entries)))
(defn loader [^String classpath]
(let [parts (.split classpath (System/getProperty "path.separator"))
entries (map part->entry parts)]
(Loader. entries)))
(defn source-for-namespace [loader namespace]
(let [ns-str (name namespace)
^String ns-str (munge ns-str)
path (.replace ns-str "." (System/getProperty "file.separator"))
paths (map #(str path %) [".bb" ".clj" ".cljc"])]
(some #(getResource loader %) paths)))
;;;; Scratch
(comment
(def l (loader "src:/Users/borkdude/.m2/repository/cheshire/cheshire/5.9.0/cheshire-5.9.0.jar"))
(source-for-namespace l 'babashka.impl.cheshire)
(source-for-namespace l 'cheshire.core)
)

View file

@ -13,6 +13,7 @@
[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.wait :as wait]
[clojure.edn :as edn]
[clojure.java.io :as io]
@ -81,7 +82,15 @@
(let [options (rest options)]
(recur (rest options)
(assoc opts-map :expression (first options))))
(if (some opts-map [:file :socket-repl :expression])
("--classpath", "-cp")
(let [options (rest options)]
(recur (rest options)
(assoc opts-map :classpath (first options))))
("--main", "-m")
(let [options (rest options)]
(recur (rest options)
(assoc opts-map :main (first options))))
(if (some opts-map [:file :socket-repl :expression :main])
(assoc opts-map
:command-line-args options)
(if (and (not= \( (first (str/trim opt)))
@ -111,7 +120,10 @@
(defn print-version []
(println (str "babashka v"(str/trim (slurp (io/resource "BABASHKA_VERSION"))))))
(def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [--verbose] [ --stream ] ( -e <expression> | -f <file> | --repl | --socket-repl [<host>:]<port> )")
(def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose]
[ ( --classpath | -cp ) <cp> ] [ ( --main | -m ) <main-namespace> ]
( -e <expression> | -f <file> | --repl | --socket-repl [<host>:]<port> )
[ arg* ]")
(defn print-usage []
(println usage-string))
@ -123,20 +135,21 @@
(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 <expression>: evaluate an expression
-f, --file <path>: evaluate a file
--repl: start REPL
--socket-repl: start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
--time: print execution time before exiting.
--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 <expr> Evaluate an expression.
-f, --file <path> Evaluate a file.
-cp, --classpath Classpath to use.
-m, --main <ns> 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*."))
@ -193,7 +206,8 @@ Everything after that is bound to *command-line-args*."))
:help? :file :command-line-args
:expression :stream? :time?
:repl :socket-repl
:verbose?] :as _opts}
:verbose? :classpath
:main] :as _opts}
(parse-opts args)
read-next (fn [*in*]
(if (pipe-signal-received?)
@ -208,6 +222,13 @@ Everything after that is bound to *command-line-args*."))
:else
(edn/read *in*))))))
env (atom {})
classpath (or classpath
(System/getenv "BABASHKA_CLASSPATH"))
loader (when classpath
(cp/loader classpath))
load-fn (when classpath
(fn [{:keys [:namespace]}]
(cp/source-for-namespace loader namespace)))
ctx {:aliases '{tools.cli 'clojure.tools.cli
edn clojure.edn
wait babashka.wait
@ -273,11 +294,16 @@ Everything after that is bound to *command-line-args*."))
File java.io.File
String java.lang.String
System java.lang.System
Thread java.lang.Thread}}
Thread java.lang.Thread}
:load-fn load-fn}
ctx (update ctx :bindings assoc 'eval #(eval* ctx %)
'load-file #(load-file* ctx %))
ctx (addons/future ctx)
_preloads (some-> (System/getenv "BABASHKA_PRELOADS") (str/trim) (eval-string ctx))
expression (if main
(format "(ns user (:require [%1$s])) (apply %1$s/-main *command-line-args*)"
main)
expression)
exit-code
(or
#_(binding [*out* *err*]

View file

@ -0,0 +1,4 @@
(ns env-ns)
(defn foo []
"env!")

Binary file not shown.

View file

@ -0,0 +1,5 @@
(ns my.impl)
(defn impl-fn
"identity"
[x] x)

View file

@ -0,0 +1,5 @@
(ns my.main
(:require [my.impl :as impl]))
(defn -main [& args]
(impl/impl-fn args))

View file

@ -0,0 +1,4 @@
(ns my-script)
(defn foo []
::bb)

View file

@ -0,0 +1,27 @@
(ns babashka.classpath-test
(:require
[babashka.test-utils :as tu]
[clojure.edn :as edn]
[clojure.test :as t :refer [deftest is]]))
(defn bb [input & args]
(edn/read-string (apply tu/bb (when (some? input) (str input)) (map str args))))
(deftest classpath-test
(is (= :my-script/bb
(bb nil "--classpath" "test-resources/babashka/src_for_classpath_test"
"(require '[my-script :as ms]) (ms/foo)")))
(is (= "hello from foo\n"
(tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test/foo.jar"
"(require '[foo :as f]) (f/foo)"))))
(deftest classpath-env-test
;; for this test you have to set `BABASHKA_CLASSPATH` to test-resources/babashka/src_for_classpath_test/env
;; and `BABASHKA_PRELOADS` to "(require '[env-ns])"
(when (System/getenv "BABASHKA_CLASSPATH_TEST")
(println (System/getenv "BABASHKA_CLASSPATH"))
(is (= "env!" (bb nil "(env-ns/foo)")))))
(deftest main-test
(is (= "(\"1\" \"2\" \"3\" \"4\")\n"
(tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" "-m" "my.main" "1" "2" "3" "4"))))

View file

@ -135,7 +135,8 @@
(deftest preloads-test
;; THIS TEST REQUIRES:
;; export BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")'
(is (= "foobar" (bb nil "(str (__bb__foo) (__bb__bar))"))))
(when (System/getenv "BABASHKA_PRELOADS_TEST")
(is (= "foobar" (bb nil "(str (__bb__foo) (__bb__bar))")))))
(deftest io-test
(is (true? (bb nil "(.exists (io/file \"README.md\"))")))