diff --git a/README.md b/README.md index 604328fc..4fab947e 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,7 @@ If most of your shell script evolves into Clojure, you might want to turn to: ## Status -Experimental. Breaking changes are expected to happen at this phase. Not all -Clojure core functions are supported yet, but can be easily -[added](https://github.com/borkdude/babashka/blob/master/src/babashka/interpreter.clj#L10). PRs -welcome. +Experimental. Breaking changes are expected to happen at this phase. ## Installation @@ -51,40 +48,43 @@ You may also download a binary from [Github](https://github.com/borkdude/babashk ... | bb [--raw] [--println] '' ``` -There is one special variable, `*in*`, which is the input read from stdin. The -input is read as EDN by default, unless the `--raw` flag is provided. When using -the `--println` flag, the output is printed using `println` instead of `prn`. +There is one special variable, `*in*`, which is the input read from stdin. The +input is read as EDN by default, unless the `-i` flag is provided, then the +input is read as a string split by newlines into a vector.. The output is +printed as EDN by default, unless the `-o` flag is provided, then the output is +printed using `println`. To combine `-i` and `-o` you can use `-io`. The current version can be printed with `bb --version`. +Currently only the macros `if`, `when`, `and`, `or`, `->` and `->>` are +supported. + Examples: ``` shellsession -$ ls | bb --raw '*in*' +$ ls | bb -i '*in*' ["LICENSE" "README.md" "bb" "doc" "pom.xml" "project.clj" "reflection.json" "resources" "script" "src" "target" "test"] -$ ls | bb --raw '(count *in*)' -11 +$ ls | bb -i '(count *in*)' +12 $ bb '(vec (dedupe *in*))' <<< '[1 1 1 1 2]' [1 2] -$ bb '(filter :foo *in*)' <<< '[{:foo 1} {:bar 2}]' -({:foo 1}) +$ bb '(filterv :foo *in*)' <<< '[{:foo 1} {:bar 2}]' +[{:foo 1}] ``` -Functions are written using the reader tag `#f`. Currently up to three -arguments are supported. +Anonymous functions literals are allowed with currently up to three positional +arguments. ``` shellsession -$ bb '(#f(+ %1 %2 %3) 1 2 *in*)' <<< 3 +$ bb '(#(+ %1 %2 %3) 1 2 *in*)' <<< 3 6 ``` -Regexes are written using the reader tag `#r`. - ``` shellsession -$ ls | bb --raw '(filterv #f(re-find #r "reflection" %) *in*)' +$ ls | bb -i '(filterv #(re-find #"reflection" %) *in*)' ["reflection.json"] ``` @@ -97,7 +97,7 @@ $ cat /tmp/test.txt 3 Babashka 4 Goodbye -$ < /tmp/test.txt bb --raw '(shuffle *in*)' | bb --println '(str/join "\n" *in*)' +$ < /tmp/test.txt bb -io '(shuffle *in*)' 3 Babashka 2 Clojure 4 Goodbye @@ -113,8 +113,8 @@ Clojure is nice bar when you're nice to clojure -$ < /tmp/test.txt bb --raw '(map-indexed #f[%1 %2] *in*))' | \ -bb '(keep #f(when (re-find #r"(?i)clojure" (second %)) (first %)) *in*)' +$ < /tmp/test.txt bb -i '(map-indexed #(vector %1 %2) *in*))' | \ +bb '(keep #f(when (re-find #"(?i)clojure" (second %)) (first %)) *in*)' (1 3) ``` @@ -137,6 +137,22 @@ You will need leiningen and GraalVM. script/compile +## Gallery + +Here's a gallery of more useful examples. Do you have a useful example? PR +welcome! + +### Fetch latest Github release tag + +For converting JSON to EDN, see [jet](https://github.com/borkdude/jet). + +``` shellsession +$ curl -s https://api.github.com/repos/borkdude/clj-kondo/tags \ +| jet --from json --keywordize --to edn \ +| bb '(-> *in* first :name (subs 1))' +"2019.07.31-alpha" +``` + ## License Copyright © 2019 Michiel Borkent diff --git a/src/babashka/interpreter.clj b/src/babashka/interpreter.clj index bd5f35cc..deed0eed 100644 --- a/src/babashka/interpreter.clj +++ b/src/babashka/interpreter.clj @@ -1,43 +1,152 @@ (ns babashka.interpreter {:no-doc true} - (:refer-clojure :exclude [comparator]) (:require [clojure.walk :refer [postwalk]] [clojure.string :as str] [clojure.set :as set])) -(def syms '(= < <= >= + +' - * / - aget alength apply assoc assoc-in - bit-set bit-shift-left bit-shift-right bit-xor boolean boolean? booleans boolean-array butlast - char char? conj cons contains? count - dec dec' decimal? dedupe dissoc distinct disj drop - eduction even? every? - get - first float? floats fnil - identity inc int-array iterate +(defn expand-> + "The -> macro from clojure.core." + [[x & forms]] + (loop [x x, forms forms] + (if forms + (let [form (first forms) + threaded (if (seq? form) + (with-meta (concat (list (first form) x) + (next form)) + (meta form)) + (list form x))] + (recur threaded (next forms))) + x))) + +(defn expand->> + "The ->> macro from clojure.core." + [[x & forms]] + (loop [x x, forms forms] + (if forms + (let [form (first forms) + threaded (if (seq? form) + (with-meta (concat (cons (first form) (next form)) + (list x)) + (meta form)) + (list form x))] + (recur threaded (next forms))) + x))) + +(declare interpret) + +(defn eval-and + "The and macro from clojure.core." + [in args] + (if (empty? args) true + (let [[x & xs] args + v (interpret x in)] + (if v + (if (empty? xs) v + (eval-and in xs)) + v)))) + +(defn eval-or + "The or macro from clojure.core." + [in args] + (if (empty? args) nil + (let [[x & xs] args + v (interpret x in)] + (if v v + (if (empty? xs) v + (eval-or in xs)))))) + +(def syms '(= < <= >= + +' - -' * *' / == aget alength apply assoc assoc-in + associative? array-map + + bit-and-not bit-set bit-shift-left bit-shift-right bit-xor boolean + boolean? booleans boolean-array bound? butlast byte-array bytes + bigint bit-test bit-and bounded-count bytes? bit-or bit-flip + biginteger bigdec bit-not byte + + cat char char? conj cons contains? count cycle comp concat + comparator coll? compare complement char-array constantly + char-escape-string chars completing counted? chunk-rest + char-name-string class chunk-next + + dec dec' decimal? dedupe dissoc distinct distinct? disj double + double? drop drop-last drop-while denominator doubles + + eduction empty empty? even? every? every-pred ensure-reduced + + first float? floats fnil fnext ffirst flatten false? filter + filterv find format frequencies float float-array + + get get-in group-by gensym + + hash hash-map hash-set hash-unordered-coll + + ident? identical? identity inc inc' int-array interleave into + iterate int int? interpose indexed? integer? ints into-array + juxt - filter filterv find frequencies - last line-seq - keep keep-indexed keys - map mapv map-indexed mapcat merge merge-with munge - name newline not= num - neg? nth nthrest - odd? - peek pos? - re-seq re-find re-pattern rest reverse - set? sequential? some? str - take take-last take-nth tree-seq type - unchecked-inc-int unchecked-long unchecked-negate unchecked-remainder-int - unchecked-subtract-int unsigned-bit-shift-right unchecked-float - vals vec vector? - rand-int rand-nth range reduce reduced? remove - second set seq seq? shuffle simple-symbol? sort sort-by subs - set/difference set/join - str/join str/starts-with? str/ends-with? str/split - zero?)) + + keep keep-indexed key keys keyword keyword? + + last line-seq long list list? longs list* long-array + + map map? map-indexed map-entry? mapv mapcat max max-key meta merge + merge-with min min-key munge mod make-array + + name newline nfirst not not= not-every? num neg? neg-int? nth + nthnext nthrest nil? nat-int? number? not-empty not-any? next + nnext namespace-munge numerator + + odd? object-array + + peek pop pos? pos-int? partial partition partition-all + partition-by + + qualified-ident? qualified-symbol? qualified-keyword? quot + + re-seq re-find re-pattern re-matches rem remove rest repeatedly + reverse rand-int rand-nth range reduce reduce-kv reduced reduced? + reversible? replicate rsubseq reductions rational? rand replace + rseq ratio? rationalize random-sample repeat + + set? sequential? select-keys simple-keyword? simple-symbol? some? + string? str + + set/difference set/index set/intersection set/join set/map-invert + set/project set/rename set/rename-keys set/select set/subset? + set/superset? set/union + + str/blank? str/capitalize str/ends-with? str/escape str/includes? + str/index-of str/join str/last-index-of str/lower-case + str/re-quote-replacement str/replace str/replace-first str/reverse + str/split str/split-lines str/starts-with? str/trim + str/trim-newline str/triml str/trimr str/upper-case + + second set seq seq? seque short shuffle + sort sort-by subs symbol symbol? special-symbol? subvec some-fn + some split-at split-with sorted-set subseq sorted-set-by + sorted-map-by sorted-map sorted? simple-ident? sequence seqable? + shorts + + take take-last take-nth take-while transduce tree-seq type true? + to-array + + update update-in uri? uuid? unchecked-inc-int unchecked-long + unchecked-negate unchecked-remainder-int unchecked-subtract-int + unsigned-bit-shift-right unchecked-float unchecked-add-int + unchecked-double unchecked-multiply-int unchecked-int + unchecked-multiply unchecked-dec-int unchecked-add unreduced + unchecked-divide-int unchecked-subtract unchecked-negate-int + unchecked-inc unchecked-char unchecked-byte unchecked-short + + val vals vary-meta vec vector vector? + + xml-seq + + zipmap zero?)) ;; TODO: #_(def all-syms - '#{when-first while if-not when-let inc' cat StackTraceElement->vec flush take-while vary-meta <= alter -' if-some conj! repeatedly zipmap reset-vals! alter-var-root biginteger remove * re-pattern min pop! chunk-append prn-str with-precision format reversible? shutdown-agents conj bound? transduce lazy-seq *print-length* *file* compare-and-set! *use-context-classloader* await1 let ref-set pop-thread-bindings interleave printf map? -> defstruct *err* assert-same-protocol get doto identity into areduce long double volatile? definline nfirst meta find-protocol-impl bit-and-not *default-data-reader-fn* var? method-sig unchecked-add-int unquote-splicing hash-ordered-coll future reset-meta! Vec cycle fn seque empty? short definterface add-tap filterv hash quot ns-aliases read unchecked-double key longs not= string? uri? aset-double unchecked-multiply-int chunk-rest pcalls *allow-unresolved-vars* remove-all-methods ns-resolve as-> aset-boolean trampoline double? when-not *1 vec *print-meta* when int map-entry? ns-refers rand second vector-of hash-combine > replace int? associative? unchecked-int set-error-handler! inst-ms* keyword? force bound-fn* namespace-munge group-by prn extend unchecked-multiply some->> default-data-readers ->VecSeq even? unchecked-dec Inst tagged-literal? double-array in-ns create-ns re-matcher defn ref bigint extends? promise aset-char rseq ex-cause construct-proxy agent-errors *compile-files* ex-message *math-context* float pr-str concat aset-short set-agent-send-off-executor! ns symbol to-array-2d mod amap pop use VecNode unquote declare dissoc! reductions aset-byte indexed? ref-history-count - assoc! hash-set reduce-kv or cast reset! name ffirst sorted-set counted? byte-array IVecImpl tagged-literal println extend-type macroexpand-1 assoc-in char-name-string bit-test defmethod requiring-resolve EMPTY-NODE time memoize alter-meta! future? zero? simple-keyword? require unchecked-dec-int persistent! nnext add-watch not-every? class? rem agent-error some future-cancelled? memfn neg-int? struct-map drop *data-readers* nth sorted? nil? extend-protocol split-at *e load-reader random-sample cond-> dotimes select-keys bit-and bounded-count update list* reify update-in prefer-method aset-int *clojure-version* ensure-reduced *' instance? with-open mix-collection-hash re-find run! val defonce unchecked-add loaded-libs ->Vec bytes? not with-meta unreduced the-ns record? type identical? unchecked-divide-int ns-name max-key *unchecked-math* defn- *out* file-seq agent ns-map set-validator! ident? defprotocol swap! vals unchecked-subtract tap> *warn-on-reflection* sorted-set-by sync qualified-ident? assert *compile-path* true? release-pending-sends print empty remove-method *in* print-ctor letfn volatile! / read-line reader-conditional? bit-or clear-agent-errors vector proxy-super >= drop-last not-empty distinct partition loop add-classpath bit-flip long-array descendants merge accessor integer? mapv partition-all partition-by numerator object-array with-out-str condp derive load-string special-symbol? ancestors subseq error-handler gensym cond ratio? delay? intern print-simple flatten doubles halt-when with-in-str remove-watch ex-info ifn? some-> nat-int? proxy-name ns-interns all-ns find-protocol-method subvec for binding partial chunked-seq? find-keyword replicate min-key reduced char-escape-string re-matches array-map unchecked-byte with-local-vars ns-imports send-off defmacro every-pred keys rationalize load-file distinct? pos-int? extenders unchecked-short methods odd? ->ArrayChunk float-array *3 alias frequencies read-string proxy rsubseq inc get-method with-redefs uuid? bit-clear filter locking list + split-with aset ->VecNode keyword *ns* destructure *assert* defmulti chars str next hash-map if-let underive ref-max-history Throwable->map false? *print-readably* ints class some-fn case *flush-on-newline* to-array bigdec list? simple-ident? bit-not io! xml-seq VecSeq byte max == *agent* lazy-cat comment parents count supers *fn-loader* ArrayChunk sorted-map-by apply interpose deref assoc rational? transient clojure-version chunk-cons comparator sorted-map send drop-while proxy-call-with-super realized? char-array resolve compare complement *compiler-options* *print-dup* defrecord with-redefs-fn sequence constantly get-proxy-class make-array shorts completing update-proxy unchecked-negate-int hash-unordered-coll repeat unchecked-inc nthnext and create-struct get-validator number? await-for chunk-next print-str not-any? into-array qualified-symbol? init-proxy chunk-buffer seqable? symbol? when-some unchecked-char ->> future-cancel var-get commute coll? get-in fnext denominator bytes gen-and-load-class refer-clojure}) + '#{when-first while if-not when-let if-some let as-> when-not some->> or cond-> loop condp cond some-> if-let case and when-some }) (declare var-lookup apply-fn) @@ -51,35 +160,59 @@ (define-lookup) +(defn resolve-symbol [expr] + (let [n (name expr)] + (if (str/starts-with? n "'") + (symbol (subs n 1)) + (or (var-lookup expr) + (throw (Exception. (format "Could not resolve symbol: %s." n))))))) + (defn interpret [expr in] - (cond - (= '*in* expr) in - (symbol? expr) (var-lookup expr) - (list? expr) - (if-let [f (first expr)] - (if-let [v (var-lookup f)] - (apply-fn v in (rest expr)) - (cond - (or (= 'if f) (= 'when f)) - (let [[_if cond then else] expr] - (if (interpret cond in) - (interpret then in) - (interpret else in))) - ;; bb/fn passed as higher order fn, still needs input - (-> f meta :bb/fn) - (apply-fn (f in) in (rest expr)) - (ifn? f) - (apply-fn f in (rest expr)) - :else nil)) - expr) - ;; bb/fn passed as higher order fn, still needs input - (-> expr meta :bb/fn) - (expr in) - :else expr)) + (let [i #(interpret % in)] + (cond + (= '*in* expr) in + (symbol? expr) (resolve-symbol expr) + (map? expr) + (zipmap (map i (keys expr)) + (map i (vals expr))) + (or (vector? expr) (set? expr)) + (into (empty expr) (map i expr)) + (seq? expr) + (if-let [f (first expr)] + (if-let [v (var-lookup f)] + (apply-fn v i (rest expr)) + (case f + (if when) + (let [[_if cond then else] expr] + (if (interpret cond in) + (interpret then in) + (interpret else in))) + -> + (interpret (expand-> (rest expr)) in) + ->> + (interpret (expand->> (rest expr)) in) + and + (eval-and in (rest expr)) + or + (eval-or in (rest expr)) + ;; fallback + ;; read fn passed as higher order fn, still needs input + (cond (-> f meta ::fn) + (apply-fn (f in) i (rest expr)) + (symbol? f) + (apply-fn (resolve-symbol f) i (rest expr)) + (ifn? f) + (apply-fn f i (rest expr)) + :else nil))) + expr) + ;; read fn passed as higher order fn, still needs input + (-> expr meta ::fn) + (expr in) + :else expr))) (defn read-fn [form] - ^:bb/fn + ^::fn (fn [in] (fn [& [x y z]] (interpret (postwalk (fn [elt] @@ -93,11 +226,15 @@ (defn read-regex [form] (re-pattern form)) -(defn apply-fn [f in args] - (let [args (mapv #(interpret % in) args)] +(defn apply-fn [f i args] + (let [args (mapv i args)] (apply f args))) ;;;; Scratch (comment + (interpret '(and *in* 3) 1) + (interpret '(and *in* 3 false) 1) + (interpret '(or *in* 3) nil) + (ifn? 'foo) ) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 21716c90..2c781329 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -19,7 +19,7 @@ opts-map {} current-opt nil] (if-let [opt (first options)] - (if (starts-with? opt "--") + (if (starts-with? opt "-") (recur (rest options) (assoc opts-map opt []) opt) @@ -28,28 +28,46 @@ current-opt)) opts-map)) version (boolean (get opts "--version")) - raw (boolean (get opts "--raw")) + 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"))] {:version version - :raw raw + :raw-in raw-in + :raw-out raw-out :println? println?})) (defn -main [& args] - (let [{:keys [:version :raw :println?]} (parse-opts args)] + (let [{:keys [:version :raw-in :raw-out :println?]} (parse-opts args)] (cond version (println (str/trim (slurp (io/resource "BABASHKA_VERSION")))) :else (let [expr (last args) - expr (read-edn expr) + expr (read-edn (-> expr + (str/replace "#(" "#f(") + (str/replace "#\"" "#r\""))) in (slurp *in*) - in (if raw + ;; _ (prn in) + in (if raw-in (str/split in #"\n") - (read-edn (format "[%s]" in))) - in (if (= 1 (count in)) (first in) in)] - ((if println? println prn) - (i/interpret expr in)))))) + (read-edn in)) + ;; _ (prn in) + res (try (i/interpret expr in) + (catch Exception e + (binding [*out* *err*] + (println (.getMessage e))) + (System/exit 1)))] + (if raw-out + (if (coll? res) + (doseq [l res] + (println l)) + (println res)) + ((if println? println? prn) res)))))) ;;;; Scratch -(comment) +(comment + ) diff --git a/test/babashka/main_test.clj b/test/babashka/main_test.clj index 06d1328a..4a4daa8b 100644 --- a/test/babashka/main_test.clj +++ b/test/babashka/main_test.clj @@ -2,29 +2,59 @@ (:require [clojure.test :as test :refer [deftest is testing]] [babashka.test-utils :as test-utils] - [clojure.edn :as edn])) + [clojure.edn :as edn] + [clojure.string :as str])) (defn bb [input & args] (edn/read-string (apply test-utils/bb (str input) (map str args)))) (deftest main-test + (testing "-io behaves as identity" + (= "foo\nbar\n" (test-utils/bb "foo\nbar\n" "-io" "*in*"))) (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))))) + (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))))) (testing "fn" - (is (= 2 (bb 1 "(#f(+ 1 %) *in*)"))) - (is (= [1 2 3] (bb nil "(map #f(+ 1 %) [0 1 2])"))) - (is (bb 1 "(#f (when (odd? *in*) *in*) 1)"))) + (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)"))) (testing "map" - (is (= [1 2 3] (bb nil '(map inc [0 1 2]))))) + (is (= [1 2 3] (bb 1 '(map inc [0 1 2]))))) (testing "keep" - (is (= [false true false] (bb nil '(keep odd? [0 1 2]))))) - (testing "..." + (is (= [false true false] (bb 1 '(keep odd? [0 1 2]))))) + (testing "->" + (is (= 4 (bb 1 '(-> *in* inc inc (inc)))))) + (testing "->>" + (is (= 10 (edn/read-string (test-utils/bb "foo\n\baar\baaaaz" "-i" "(->> *in* (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*)}})))) + (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*))) + out-lines (set (str/split out #"\n"))] + (= in-lines out-lines))) + (testing "find occurrences in file by line number" (is (= '(1 3) (-> (bb "foo\n Clojure is nice. \nbar\n If you're nice to clojure. " - "--raw" - "(map-indexed #f[%1 %2] *in*)") - (bb "(keep #f(when (re-find #r\"(?i)clojure\" (second %)) (first %)) *in*)")))))) + "-i" + "(map-indexed #(-> [%1 %2]) *in*)") + (bb "(keep #f(when (re-find #\"(?i)clojure\" (second %)) (first %)) *in*)"))))))