diff --git a/README.md b/README.md index 191a0565..5dcc97de 100644 --- a/README.md +++ b/README.md @@ -71,18 +71,16 @@ bb [ --help ] [ -i ] [ -o ] [ -io ] [ --version ] [ -f ] [ expression ] Type `bb --help` to see a full explanation of the options. -There is one special variable, `*in*`, which is the input read from stdin. The -input is read as EDN by default. If the `-i` flag is provided, then the input is -read as a string which is then split on newlines. The output is printed as EDN -by default, unless the `-o` flag is provided, then the output is turned into -shell-scripting friendly output. To combine `-i` and `-o` you can use `-io`. +The input is read as EDN by default. If the `-i` flag is provided, then the +input is read as a string which is then split on newlines. The output is printed +as EDN by default, unless the `-o` flag is provided, then the output is turned +into shell-scripting friendly output. To combine `-i` and `-o` you can use +`-io`. The current version can be printed with `bb --version`. -Currently only the following special forms/macros are supported: anonymous -function literals like `#(%1 %2)`, `quote`, `do`,`if`, `when`, `let`, `and`, -`or`, `->`, `->>`, `as->`. Anonymous functions literals are allowed with -currently up to three positional arguments. +Babashka supports a subset of Clojure which is interpreted by +[sci](https://github.com/borkdude/sci). The `clojure.core` functions are accessible without a namespace alias. @@ -96,7 +94,12 @@ through the aliases: From Java the following is available: -- `System`: `getProperty`, `getProperties`, `getenv` +- `System`: `exit`, `getProperty`, `getProperties`, `getenv` + +Special vars: + +- `*in*`: contains the input read from stdin +- `*command-line-args*`: contain the command line args Examples: @@ -143,13 +146,26 @@ bb -f script.clj Using `bb` with a shebang also works: ``` shellsession -$ cat script.clj -#!/usr/bin/env bb -f +#!/usr/bin/env bb -io -f -(+ 1 2 3) +(defn get-url [url] + (println "Fetching url:" url) + (let [{:keys [:exit :err :out]} (shell/sh "curl" "-sS" url)] + (if (zero? exit) out + (do (println "ERROR:" err) + (System/exit 1))))) -$ ./script.clj -6 +(defn write-html [file html] + (println "Writing html to" file) + (spit file html)) + +(let [[url file] *command-line-args*] + (when (or (empty? url) (empty? file)) + (println "Usage: ") + (System/exit 1)) + (write-html file (get-url url))) + +(System/exit 0) ``` ## Test diff --git a/project.clj b/project.clj index 9c32dd35..650113a0 100644 --- a/project.clj +++ b/project.clj @@ -9,7 +9,7 @@ :url "http://opensource.org/licenses/eclipse-1.0.php"} :source-paths ["src"] :dependencies [[org.clojure/clojure "1.9.0"] - [borkdude/sci "0.0.4"]] + [borkdude/sci "0.0.7-SNAPSHOT"]] :profiles {:clojure-1.9.0 {:dependencies [[org.clojure/clojure "1.9.0"]]} :clojure-1.10.1 {:dependencies [[org.clojure/clojure "1.10.1"]]} :test {:dependencies [[clj-commons/conch "0.9.2"]]} diff --git a/src/babashka/main.clj b/src/babashka/main.clj index f02a0f19..e62e3411 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -4,7 +4,7 @@ [clojure.edn :as edn] [clojure.java.io :as io] [clojure.java.shell :as shell] - [clojure.string :as str :refer [starts-with?]] + [clojure.string :as str] [sci.core :as sci]) (:gen-class)) @@ -20,33 +20,34 @@ (defn- parse-opts [options] (let [opts (loop [options options - opts-map {} - current-opt nil] + opts-map {}] (if-let [opt (first options)] - (if (starts-with? opt "-") - (recur (rest options) - (assoc opts-map opt []) - opt) - (recur (rest options) - (update opts-map current-opt conj opt) - current-opt)) - opts-map)) - version (boolean (get opts "--version")) - raw-in (boolean (or (get opts "--raw") - (get opts "-i") - (get opts "-io"))) - raw-out (boolean (or (get opts "-o") - (get opts "-io"))) - println? (boolean (get opts "--println")) - help? (boolean (get opts "--help")) - file (first (or (get opts "-f") - (get opts "--file")))] - {:version version - :raw-in raw-in - :raw-out raw-out - :println? println? - :help? help? - :file file})) + (case opt + ("--version") {:version true} + ("--help") {:help? true} + ("-i") (recur (rest options) + (assoc opts-map + :raw-in true)) + ("-o") (recur (rest options) + (assoc opts-map + :raw-out true)) + ("-io") (recur (rest options) + (assoc opts-map + :raw-in true + :raw-out true)) + ("-f" "--file") + (let [options (rest options)] + (recur (rest options) + (assoc opts-map + :file (first options)))) + (if (not (:file opts-map)) + (assoc opts-map + :expression opt + :command-line-args (rest options)) + (assoc opts-map + :command-line-args options))) + opts-map))] + opts)) (defn parse-shell-string [s] (str/split s #"\n")) @@ -54,7 +55,7 @@ (defn print-version [] (println (str "babashka v"(str/trim (slurp (io/resource "BABASHKA_VERSION")))))) -(def usage-string "Usage: [ --help ] [ -i ] [ -o ] [ -io ] [ --version ] [ expression ]") +(def usage-string "Usage: [ --help ] | [ --version ] | [ -i ] [ -o ] [ -io ] ( expression | -f )") (defn print-usage [] (println usage-string)) @@ -76,10 +77,13 @@ ")) (defn read-file [file] - (as-> (slurp file) x - ;; remove hashbang - (str/replace x #"^#!.*" "") - (format "(do %s)" x))) + (let [f (io/file file)] + (if (.exists f) + (as-> (slurp file) x + ;; remove hashbang + (str/replace x #"^#!.*" "") + (format "(do %s)" x)) + (throw (Exception. (str "File does not exist: " file)))))) (defn get-env ([] (System/getenv)) @@ -94,10 +98,15 @@ (defn get-properties [] (System/getProperties)) +(defn exit [n] + (System/exit n)) + (def bindings {'run! run! 'shell/sh shell/sh 'csh shell/sh ;; backwards compatibility, deprecated + 'slurp slurp + 'spit spit 'pmap pmap 'print print 'pr-str pr-str @@ -106,13 +115,19 @@ 'edn/read-string edn/read-string 'System/getenv get-env 'System/getProperty get-property - 'System/getProperties get-properties}) + 'System/getProperties get-properties + 'System/exit exit}) (defn main [& args] + #_(binding [*out* *err*] + (prn ">> args" args)) (or (let [{:keys [:version :raw-in :raw-out :println? - :help? :file]} (parse-opts args)] + :help? :file :command-line-args + :expression] :as _opts} (parse-opts args)] + #_(binding [*out* *err*] + (prn ">>" _opts)) (second (cond version [(print-version) 0] @@ -120,30 +135,30 @@ [(print-help) 0] :else (try - [(let [exprs (drop-while #(str/starts-with? % "-") args) - _ (when-not (or (= 1 (count exprs)) file) - (throw (Exception. ^String usage-string))) - expr (if file (read-file file) (last args)) - in (delay (let [in (slurp *in*)] - (if raw-in - (parse-shell-string in) - (read-edn in)))) - res (sci/eval-string - expr - {:bindings (assoc bindings - (with-meta '*in* - {:sci/deref! true}) in)})] - (if raw-out - (if (coll? res) - (doseq [l res] - (println l)) - (println res)) - ((if println? println? prn) res))) 0] + [(do (when-not (or expression file) + (throw (Exception. "Missing expression."))) + (let [expr (if file (read-file file) expression) + in (delay (let [in (slurp *in*)] + (if raw-in + (parse-shell-string in) + (read-edn in)))) + res (sci/eval-string + expr + {:bindings (assoc bindings + (with-meta '*in* + {:sci/deref! true}) in + '*command-line-args* command-line-args)})] + (if raw-out + (if (coll? res) + (doseq [l res] + (println l)) + (println res)) + ((if println? println? prn) res)))) 0] (catch Exception e (binding [*out* *err*] - (println (str/trim - (or (:stderr (ex-data e)) - (.getMessage e))) )) + (when-let [msg (or (:stderr (ex-data e)) + (.getMessage e))] + (println (str/trim msg) ))) [nil 1]))))) 1)) diff --git a/test/babashka/main_test.clj b/test/babashka/main_test.clj index e787dd13..6ff5fcdb 100644 --- a/test/babashka/main_test.clj +++ b/test/babashka/main_test.clj @@ -26,7 +26,7 @@ (testing "fn" (is (= 2 (bb 1 "(#(+ 1 %) *in*)"))) (is (= [1 2 3] (bb 1 "(map #(+ 1 %) [0 1 2])"))) - (is (bb 1 "(#(when (odd? *in*) *in*) 1)"))) + (is (= 1 (bb 1 "(#(when (odd? *in*) *in*))")))) (testing "map" (is (= [1 2 3] (bb 1 '(map inc [0 1 2]))))) (testing "keep" @@ -69,3 +69,9 @@ (is (= "bar" (second res))) (doseq [s res] (is (not-empty s))))) + +(deftest malformed-command-line-args + (is (thrown-with-msg? Exception #"File does not exist: non-existing\n" + (bb nil "-f" "non-existing"))) + (is (thrown-with-msg? Exception #"Missing expression.\n" + (bb nil)))) diff --git a/test/babashka/test_utils.clj b/test/babashka/test_utils.clj index 4f73ffef..2caba366 100644 --- a/test/babashka/test_utils.clj +++ b/test/babashka/test_utils.clj @@ -6,19 +6,25 @@ (set! *warn-on-reflection* true) (defn bb-jvm [input & args] - (with-out-str - (if input - (with-in-str input - (apply main/main args)) - (apply main/main input args)))) + (let [sw (java.io.StringWriter.) + res (binding [*err* sw] + (with-out-str + (if input + (with-in-str input + (apply main/main args)) + (apply main/main input args))))] + (if-let [err ^String (not-empty (str sw))] + (throw (Exception. err)) res))) (defn bb-native [input & args] (let-programs [bb "./bb"] - (binding [sh/*throw* false] - (if input - (apply bb (conj (vec args) - {:in input})) - (apply bb input args))))) + (try (if input + (apply bb (conj (vec args) + {:in input})) + (apply bb input args)) + (catch Exception e + (let [err-msg (or (:stderr (ex-data e)) "")] + (throw (Exception. ^String err-msg))))))) (def bb (case (System/getenv "BABASHKA_TEST_ENV")