├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── README.md ├── bb.edn ├── deps.edn ├── examples └── 1.bash ├── integration ├── echo.bash ├── envvar-get.bash ├── envvar-set-get.bash ├── envvar-set.bash ├── exit.bash ├── export-and-set.bash ├── group.bash ├── heredoc.bash ├── if-multiple.bash └── redir-both.bash ├── scripts └── watch ├── src └── bash2bb │ └── core.clj ├── test ├── bash2bb │ └── core_test.clj ├── integration.clj └── runner.clj └── todo.md /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | .clj-kondo/.cache 3 | .clj-kondo 4 | target 5 | .lsp 6 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | babashka 1.3.180 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | *0.1.119 - Initial Release* 2 | 3 | Many features work: 4 | 5 | * Variables (including environment variables) 6 | * Redirection 7 | * Conditional 8 | * Pipes 9 | * Heredoc 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bash2bb 2 | 3 | **Translates bash scripts into babashka scripts.** 4 | 5 | ``` 6 | % cat myscript.bash 7 | #!/bin/bash 8 | OF=myhome_directory_$(date +%Y%m%d).tar.gz 9 | tar -czf $OF /home/linuxconfig 10 | 11 | % bash2bb myscript.bash 12 | (require (quote [babashka.process :refer [shell pipeline pb]])) 13 | (def OF (System/getenv "OF")) 14 | (def OF (str "myhome_directory_" (:out (shell {:out :string} "date" "+%Y%m%d")) ".tar.gz")) 15 | (shell "tar" "-czf" OF "/home/linuxconfig") 16 | ``` 17 | 18 | 19 | ## About 20 | 21 | `bash2bb` generates a [babashka](https://babashka.org/) program that's roughly equivalent to an input bash script. This can help in a few ways: 22 | 23 | - _Learning_: If you already know how to perform a certain task in bash, you may wanto to learn how to do the same thing in babashka. With `bash2bb`, you can translate your bash knowledge into (the beginning of) a babashka program. 24 | 25 | - _Upgrading_: When you run up against limitations with bash as a scripting language, you may want to convert an existing bash script to babashka. `bash2bb` gives you a rough translation of the code. 26 | 27 | > **Note** 28 | > While `bash2bb` makes an effort to emulate various bash features, the result is likely to contain inaccuracies. Don't blindly trust the output – always review the generated script! 29 | 30 | *This is an early alpha release. Many bash language constructs aren't implemented yet.* 31 | 32 | See [CHANGELOG.md](CHANGELOG.md) for the release history. 33 | 34 | ## Installation 35 | 36 | bash2bb depends on the [shfmt](https://github.com/mvdan/sh) command-line tool. On macOS this dependency can be installed via homebrew: 37 | 38 | ``` 39 | brew install shfmt 40 | ``` 41 | 42 | You will also need to install [bbin](https://github.com/babashka/bbin): 43 | 44 | ``` 45 | brew install babashka/brew/bbin 46 | ``` 47 | 48 | and 49 | 50 | ``` 51 | echo 'export PATH="$PATH:$HOME/.babashka/bbin/bin"' >> ~/.$(basename $SHELL)rc && exec $SHELL 52 | ``` 53 | 54 | With that out of the way, you can now use bbin to install bash2bb: 55 | 56 | ``` 57 | bbin install io.github.pesterhazy/bash2bb 58 | ``` 59 | 60 | ## Usage 61 | 62 | Pass the script you'd like to translate as an argument: 63 | 64 | ``` 65 | bash2bb myscript.bash 66 | ``` 67 | 68 | If no argument is specified, bash2bb reads from stdin. 69 | 70 | ## Implementation status 71 | 72 | - [x] Redirection 73 | - [x] Command substitution 74 | - [x] If statements 75 | - [x] Quoting rules 76 | - [ ] Arrays, in particular `$@` 77 | - [ ] Functions 78 | - [ ] for loops 79 | - [ ] while loops 80 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {local/bash2bb {:local/root "."}} 3 | :tasks 4 | {test:bb {:extra-paths ["test"] 5 | :extra-deps {io.github.cognitect-labs/test-runner 6 | {:git/sha "7284cda41fb9edc0f3bc6b6185cfb7138fc8a023"}} 7 | :task runner/-main} 8 | integration:bb {:extra-paths ["test"] 9 | :task integration/-main}} 10 | :bbin/bin {bash2bb {:main-opts ["-m" "bash2bb.core"]}}} 11 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {backtick/backtick {:mvn/version "0.3.4"} 3 | zprint/zprint {:mvn/version "1.2.4"}}} 4 | -------------------------------------------------------------------------------- /examples/1.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | OF=myhome_directory_$(date +%Y%m%d).tar.gz 3 | tar -czf $OF /home/linuxconfig 4 | -------------------------------------------------------------------------------- /integration/echo.bash: -------------------------------------------------------------------------------- 1 | echo abc | rev 2 | -------------------------------------------------------------------------------- /integration/envvar-get.bash: -------------------------------------------------------------------------------- 1 | echo $WELLKNOWNVAR 2 | -------------------------------------------------------------------------------- /integration/envvar-set-get.bash: -------------------------------------------------------------------------------- 1 | echo $WELLKNOWNVAR 2 | WELLKNOWNVAR=def 3 | echo $WELLKNOWNVAR 4 | -------------------------------------------------------------------------------- /integration/envvar-set.bash: -------------------------------------------------------------------------------- 1 | ENVVAR=a bash -c 'echo $ENVVAR' 2 | -------------------------------------------------------------------------------- /integration/exit.bash: -------------------------------------------------------------------------------- 1 | echo a; exit 0; echo b -------------------------------------------------------------------------------- /integration/export-and-set.bash: -------------------------------------------------------------------------------- 1 | export VAR=a; python -c "import os; print(os.getenv('VAR'))" 2 | -------------------------------------------------------------------------------- /integration/group.bash: -------------------------------------------------------------------------------- 1 | { true; false; } || echo a 2 | -------------------------------------------------------------------------------- /integration/heredoc.bash: -------------------------------------------------------------------------------- 1 | cat <& /dev/null 2 | -------------------------------------------------------------------------------- /scripts/watch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (require '[babashka.process :refer [shell]]) 4 | 5 | (shell "watchexec" "-f" "*.clj" "--" "beep-boop" "bb" "test:bb") 6 | -------------------------------------------------------------------------------- /src/bash2bb/core.clj: -------------------------------------------------------------------------------- 1 | (ns bash2bb.core 2 | (:require 3 | [backtick :refer [template]] 4 | [clojure.walk] 5 | [clojure.pprint :refer [pprint]] 6 | [babashka.cli :as cli] 7 | [babashka.process :refer [shell]] 8 | [cheshire.core :as json])) 9 | 10 | (def ^:dynamic *!state* nil) 11 | 12 | (defn- swap-state! [& args] 13 | (apply swap! *!state* args)) 14 | 15 | (defn fixup 16 | [v] 17 | (clojure.walk/prewalk (fn [v] 18 | (if-not (map? v) 19 | v 20 | (-> v 21 | (dissoc "Position") 22 | (dissoc "Pos") 23 | (dissoc "OpPos") 24 | (dissoc "End") 25 | (dissoc "ValuePos") 26 | (dissoc "ValueEnd")))) 27 | v)) 28 | 29 | (defn pp 30 | [v] 31 | (pprint (fixup v))) 32 | 33 | (defn update-shell [cmd f & args] 34 | (assert (= 'shell (first cmd))) 35 | (let [opts (apply f (if (map? (second cmd)) (second cmd) {}) args) 36 | args (if (map? (second cmd)) 37 | (drop 2 cmd) 38 | (drop 1 cmd))] 39 | (if (empty? opts) 40 | (template (shell ~@args)) 41 | (template (shell ~opts ~@args))))) 42 | 43 | (defn only [xs] 44 | (assert (= 1 (count xs))) 45 | (first xs)) 46 | 47 | (declare stmt->forms) 48 | 49 | (defn concat-if-many [xs] 50 | (if (> (count xs) 1) 51 | (template (str ~@xs)) 52 | (first xs))) 53 | 54 | (defn do-if-many [xs] 55 | (if (> (count xs) 1) 56 | (template (do ~@xs)) 57 | (first xs))) 58 | 59 | (defn unwrap-arg [{parts "Parts"}] 60 | (concat-if-many (map (fn [part] 61 | (case (get part "Type") 62 | ("Lit" "SglQuoted") (get part "Value") 63 | "DblQuoted" (unwrap-arg part) 64 | "CmdSubst" 65 | (let [stmts (-> part (get "Stmts"))] 66 | (->> stmts 67 | (mapcat #(stmt->forms % {})) 68 | (map (fn [form] 69 | (template (:out ~(update-shell form assoc :out :string))))) 70 | concat-if-many)) 71 | "ParamExp" 72 | (let [var-name (-> part (get "Param") (get "Value"))] 73 | #_(when (get part "Exp") 74 | (pp (get part "Exp")) 75 | (assert (= 70 (-> part (get "Exp") (get "Op")))) 76 | (prn (-> part (get "Exp") (get "Word") (get "Parts") only (get "Value")))) 77 | (cond 78 | (re-matches #"\d+" var-name) 79 | (let [idx (Long/parseLong var-name)] 80 | (assert (pos? idx)) 81 | (template (nth *command-line-args* ~(dec idx)))) 82 | (= "#" var-name) 83 | '(dec (count *command-line-args*)) 84 | (= "@" var-name) 85 | (throw (Exception. "Not implemented: $@")) 86 | :else 87 | (do 88 | (swap-state! update :vars (fn [vars] (conj (or vars #{}) (symbol var-name)))) 89 | (symbol var-name)))) 90 | (do 91 | (pp part) 92 | (throw (Exception. (str "Part Type not implemented: " (get part "Type"))))))) parts))) 93 | 94 | (defn not-not-found 95 | "Like clojure.core/not-empty but uses ::not-found as sentinel" 96 | [coll] 97 | (if (= ::not-found coll) 98 | nil 99 | coll)) 100 | 101 | (defn builtin [[cmd & args]] 102 | (case cmd 103 | "exit" 104 | (do 105 | (assert (= 1 (count args))) 106 | [(template (System/exit ~(Long/parseLong (first args))))]) 107 | "set" 108 | [] 109 | ; "echo" 110 | ; (if (= "-n" (first args)) 111 | ; [(template (print ~@(rest args)))] 112 | ; [(template (println ~@args))]) 113 | ::not-found)) 114 | 115 | (defn- stmt->forms [{{type "Type", :as cmd} "Cmd", 116 | redirs "Redirs"} 117 | {:keys [context] :or {context :stmt}}] 118 | (assert (<= (count redirs) 2)) 119 | (let [finalize 120 | (fn [form] 121 | (if (and (= :binary context) (list? form) (= 'shell (first form))) 122 | (template (zero? (:exit ~(update-shell form assoc :continue true)))) 123 | form))] 124 | (case type 125 | "CallExpr" 126 | (let [{args "Args", assigns "Assigns"} cmd] 127 | (cond 128 | (and (empty? args) (seq assigns)) 129 | [(template (def 130 | ~(-> assigns only (get "Name") (get "Value") symbol) 131 | ~(-> assigns only (get "Value") unwrap-arg)))] 132 | (seq args) 133 | (or (not-not-found (builtin (mapv unwrap-arg args))) 134 | [(-> (let [opts 135 | (reduce 136 | (fn [opts redir] 137 | (case (get redir "Op") 138 | 54 139 | (assoc opts (case (-> redir (get "N") (get "Value")) 140 | (nil "1") :out 141 | "2" :err) 142 | (-> redir (get "Word") (get "Parts") only (get "Value"))) 143 | 56 144 | (assoc opts :in (template (slurp ~(-> redir (get "Word") (get "Parts") only (get "Value"))))) 145 | 59 ;; StdoutToFileDescriptor 146 | (let [target (-> redir (get "Word") unwrap-arg)] 147 | (cond 148 | (and (nil? (get redir "N")) 149 | (= "2" target)) 150 | (assoc opts :out 'System/err) 151 | (and (= "2" (-> redir (get "N") (get "Value"))) 152 | (= "1" target)) 153 | (assoc opts :err 'System/out) 154 | :else 155 | (assoc opts :out target :err :out))) 156 | 157 | 61 ;; here-doc 158 | (assoc opts :in (-> redir (get "Hdoc") (get "Parts") only (get "Value"))) 159 | 63 ;; here-string 160 | (assoc opts :in (-> redir (get "Word") (get "Parts") only (get "Value"))) 161 | ;; else 162 | (do 163 | (pp redir) 164 | (throw (Exception. (str "Redir Op not implemented: " (get redir "Op"))))))) 165 | {} 166 | redirs)] 167 | (template (shell 168 | ~@(into (if (empty? opts) [] [opts]) 169 | (mapv unwrap-arg args))))) 170 | (update-shell (fn [opts] 171 | (reduce (fn [opts assign] 172 | (update opts 173 | :extra-env 174 | (fn [env] 175 | (assoc env 176 | (-> assign (get "Name") (get "Value")) 177 | (-> assign (get "Value") unwrap-arg))))) 178 | opts 179 | assigns))) 180 | finalize)]) 181 | :else 182 | (throw (Exception. "Unknown CallExpr")))) 183 | "BinaryCmd" 184 | [(finalize (let [{op "Op", x "X", y "Y"} cmd] 185 | (case op 186 | 10 ;; && 187 | (template (and ~(do-if-many (stmt->forms x {:context :binary})) ~(do-if-many (stmt->forms y {})))) 188 | 11 ;; || 189 | (template (or ~(do-if-many (stmt->forms x {:context :binary})) ~(do-if-many (stmt->forms y {})))) 190 | 12 ;; | 191 | (update-shell (do-if-many (stmt->forms y {})) assoc :in (template (:out ~(update-shell (do-if-many (stmt->forms x {})) assoc :out :string)))) 192 | (do 193 | (pp cmd) 194 | (throw (Exception. (str "BinaryCmd Op not implemented: " op)))))))] 195 | "IfClause" 196 | [(finalize (if (get (get cmd "Else") "Then") 197 | (template 198 | (if ~(do-if-many (stmt->forms (only (get cmd "Cond")) {:context :binary})) 199 | ~(do-if-many (mapcat #(stmt->forms % {}) (get cmd "Then"))) 200 | ~(do-if-many (mapcat #(stmt->forms % {}) (get (get cmd "Else") "Then"))))) 201 | (template (when ~(do-if-many (stmt->forms (only (get cmd "Cond")) {:context :binary})) 202 | ~(do-if-many (mapcat #(stmt->forms % {}) (get cmd "Then")))))))] 203 | "TestClause" 204 | [(case context 205 | (:binary :stmt) 206 | (let [{{type "Type", op "Op", x "X", y "Y"} "X"} cmd] 207 | (case type 208 | "BinaryTest" 209 | (case op 210 | (40 74) ;; == 211 | (template (= ~(unwrap-arg x) ~(unwrap-arg y))) 212 | 41 ;; != 213 | (template (not= ~(unwrap-arg x) ~(unwrap-arg y))) 214 | (do 215 | (pp cmd) 216 | (throw (Exception. (str "BinaryTest Op not implemented: " op))))))))] 217 | "Block" 218 | [(let [[:as stmts] (-> cmd (get "Stmts"))] 219 | (assert (pos? (count stmts))) 220 | (let [forms (mapcat #(stmt->forms % {}) stmts)] 221 | (if (empty? forms) 222 | '(do) 223 | (template (do ~@(concat (butlast forms) [(finalize (last forms))]))))))] 224 | "DeclClause" 225 | [(let [arg (-> cmd (get "Args") only)] 226 | (assert (= "export" (-> cmd (get "Variant") (get "Value")))) 227 | (if (-> arg (get "Naked")) 228 | (template (alter-var-root #'babashka.process/*defaults* (fn [m] (update m :extra-env assoc ~(-> arg (get "Name") (get "Value")) ~(-> arg (get "Name") (get "Value") symbol))))) 229 | (template 230 | (do 231 | (def 232 | ~(-> arg (get "Name") (get "Value") symbol) 233 | ~(-> arg (get "Value") unwrap-arg)) 234 | (alter-var-root #'babashka.process/*defaults* (fn [m] (update m :extra-env assoc ~(-> arg (get "Name") (get "Value")) ~(-> arg (get "Name") (get "Value") symbol))))))))] 235 | ;; else 236 | [(do 237 | (pp cmd) 238 | (throw (ex-info (str "Cmd type not implemented: " type) {})))]))) 239 | 240 | (defn ast->forms+state 241 | [ast] 242 | (binding [*!state* (atom {})] 243 | [(vec (mapcat #(stmt->forms % {}) (get ast "Stmts"))) @*!state*])) 244 | 245 | (defn ast->forms 246 | [ast] 247 | (first (ast->forms+state ast))) 248 | 249 | (defn declarations [state] 250 | (->> (:vars state) 251 | (map (fn [sym] (template (def ~sym (System/getenv ~(name sym)))))) 252 | vec)) 253 | 254 | (defn bash->ast [bash] 255 | (json/parse-string (:out (shell {:in bash :out :string} "shfmt" "--to-json")))) 256 | 257 | (defn preamble [] 258 | '[(require '[babashka.process :refer [shell pipeline pb]])]) 259 | 260 | (def shebang "#!/usr/bin/env bb\n\n") 261 | 262 | (defn bash->bb [bash] 263 | (let [[forms state] (ast->forms+state (bash->ast bash))] 264 | (->> (concat (preamble) (declarations state) forms) 265 | (map prn-str) 266 | (apply str shebang)))) 267 | 268 | ;; ---------- 269 | 270 | (def cli-opts {:coerce {:ast :boolean, :zprint :boolean} :args->opts [:file]}) 271 | 272 | (defn -main [& args] 273 | (let [cli (cli/parse-opts args cli-opts)] 274 | (if (:ast cli) 275 | (pp (bash->ast (slurp (or (:file cli) *in*)))) 276 | (let [bb (bash->bb (slurp (or (:file cli) *in*)))] 277 | (if (:zprint cli) 278 | ;; requiring zprint doubles startup time, so load lazily 279 | (print ((requiring-resolve 'zprint.core/zprint-file-str) 280 | bb 281 | "script")) 282 | (print bb)))))) 283 | -------------------------------------------------------------------------------- /test/bash2bb/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns bash2bb.core-test 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [bash2bb.core :as x])) 5 | 6 | (deftest t-bash->ast 7 | (is (= {"Type" "File"} (x/bash->ast "")))) 8 | 9 | (deftest update-shell-no-change 10 | (is (= '(shell "cat") (x/update-shell '(shell "cat") identity)))) 11 | 12 | (deftest update-shell-add-out 13 | (is (= '(shell {:out :string} "cat") (x/update-shell '(shell "cat") assoc :out :string)))) 14 | 15 | (deftest update-shell-no-change-with-opt 16 | (is (= '(shell {:out :string} "cat") (x/update-shell '(shell {:out :string} "cat") identity)))) 17 | 18 | (deftest update-shell-remove-out 19 | (is (= '(shell "cat") (x/update-shell '(shell {:out :string} "cat") dissoc :out)))) 20 | 21 | (deftest empty-ast 22 | (is (= [] 23 | (x/ast->forms (x/bash->ast ""))))) 24 | 25 | (deftest cmd-one 26 | (is (= ['(shell "cmd" "one")] 27 | (x/ast->forms (x/bash->ast "cmd one"))))) 28 | 29 | (deftest cmd-two 30 | (is (= ['(shell "cmd" "one") 31 | '(shell "cmd" "two")] 32 | (x/ast->forms (x/bash->ast "cmd one\ncmd two"))))) 33 | 34 | (deftest cmd-double-quotes 35 | (is (= ['(shell "cmd" "a b")] 36 | (x/ast->forms (x/bash->ast "cmd \"a b\""))))) 37 | 38 | (deftest cmd-single-quotes 39 | (is (= ['(shell "cmd" "a b")] 40 | (x/ast->forms (x/bash->ast "cmd 'a b'"))))) 41 | 42 | (deftest cmd-pipe 43 | (is (= ['(shell {:in (:out (shell {:out :string} "cmd" "ab"))} "rev")] 44 | (x/ast->forms (x/bash->ast "cmd ab | rev"))))) 45 | 46 | (deftest cmd-redirect-stdout 47 | (is (= '[(shell {:out "stdout.txt"} "cmd" "a")] 48 | (x/ast->forms (x/bash->ast "cmd a > stdout.txt"))))) 49 | 50 | (deftest cmd-redirect-stderr 51 | (is (= '[(shell {:err "stderr.txt"} "cmd" "a")] 52 | (x/ast->forms (x/bash->ast "cmd a 2> stderr.txt"))))) 53 | 54 | (deftest cmd-redirect-stdin 55 | (is (= '[(shell {:in (slurp "stdin.txt")} "cat")] 56 | (x/ast->forms (x/bash->ast "cat < stdin.txt"))))) 57 | 58 | (deftest cmd-redirect-both 59 | (is (= '[(shell {:out "both.txt", :err :out} "cmd" "a")] 60 | (x/ast->forms (x/bash->ast "cmd a >& both.txt"))))) 61 | 62 | (deftest cmd-redirect-stdout-to-fd 63 | (is (= ['(shell {:out System/err} "cmd" "a")] 64 | (x/ast->forms (x/bash->ast "cmd a >&2"))))) 65 | 66 | (deftest cmd-redirect-stderr-to-stdout 67 | (is (= ['(shell {:err System/out} "cmd" "a")] 68 | (x/ast->forms (x/bash->ast "cmd a 2>&1"))))) 69 | 70 | (deftest cmd-pipe-3 71 | (is (= ['(shell {:in (:out (shell {:in (:out (shell {:out :string} "cmd" "ab")) :out :string} "cat"))} "rev")] 72 | (x/ast->forms (x/bash->ast "cmd ab | cat | rev"))))) 73 | 74 | (deftest cmd-cmd-subst 75 | (is (= '[(shell "cmd" (:out (shell {:out :string} "cmd" "a")))] 76 | (x/ast->forms (x/bash->ast "cmd $(cmd a)"))))) 77 | 78 | (deftest cmd-cmd-subst-2-stmts 79 | (is (= '[(shell "cmd" (str (:out (shell {:out :string} "cmd" "a")) 80 | (:out (shell {:out :string} "cmd" "b"))))] 81 | (x/ast->forms (x/bash->ast "cmd $(cmd a; cmd b)"))))) 82 | 83 | (deftest cmd-2-parts 84 | (is (= '[(shell "cmd" (str (:out (shell {:out :string} "cmd" "a")) "="))] 85 | (x/ast->forms (x/bash->ast "cmd \"$(cmd a)=\""))))) 86 | 87 | (deftest here-string 88 | (is (= '[(shell {:in "abc"} "cat")] 89 | (x/ast->forms (x/bash->ast "cat <<< abc"))))) 90 | 91 | (deftest here-doc 92 | (is (= '[(shell {:in "hello\nworld\n"} "cat")] 93 | (x/ast->forms (x/bash->ast "cat <forms (x/bash->ast "cmd $1"))))) 102 | 103 | (deftest param-dollar-2 104 | (is (= '[(shell "cmd" (nth *command-line-args* 1))] 105 | (x/ast->forms (x/bash->ast "cmd $2"))))) 106 | 107 | (deftest param-dollar-hash 108 | (is (= '[(shell "cmd" (dec (count *command-line-args*)))] 109 | (x/ast->forms (x/bash->ast "cmd $#"))))) 110 | 111 | #_(deftest param-dollar-at 112 | (is (= '[(apply shell "cmd" *command-line-args*)] 113 | (x/ast->forms (x/bash->ast "cmd $@"))))) 114 | 115 | (deftest binary-and 116 | (is (= '[(and (zero? (:exit (shell {:continue true} "true"))) (shell "cmd" "a"))] 117 | (x/ast->forms (x/bash->ast "true && cmd a"))))) 118 | 119 | (deftest binary-or 120 | (is (= '[(or (zero? (:exit (shell {:continue true} "true"))) (shell "cmd" "a"))] 121 | (x/ast->forms (x/bash->ast "true || cmd a"))))) 122 | 123 | (deftest binary-and-3 124 | (is (= '[(and (and (zero? (:exit (shell {:continue true} "a"))) (shell "b")) (shell "c"))] 125 | (x/ast->forms (x/bash->ast "a && b && c"))))) 126 | 127 | (deftest conditional 128 | (is (= '[(if (zero? (:exit (shell {:continue true} "true"))) 129 | (shell "cmd" "a") 130 | (shell "cmd" "b"))] 131 | (x/ast->forms (x/bash->ast "if true; then cmd a; else cmd b; fi"))))) 132 | 133 | (deftest conditional-multiple-stmts 134 | (is (= '[(if (zero? (:exit (shell {:continue true} "true"))) 135 | (do 136 | (shell "cmd" "a") 137 | (shell "cmd" "b")) 138 | (do 139 | (shell "cmd" "c") 140 | (shell "cmd" "d")))] 141 | (x/ast->forms (x/bash->ast "if true; then cmd a; cmd b; else cmd c; cmd d; fi"))))) 142 | 143 | (deftest conditional-no-else 144 | (is (= '[(when (zero? (:exit (shell {:continue true} "true"))) 145 | (shell "cmd" "a"))] 146 | (x/ast->forms (x/bash->ast "if true; then cmd a; fi"))))) 147 | 148 | (deftest conditional-no-else-multiple-stmts 149 | (is (= '[(when (zero? (:exit (shell {:continue true} "true"))) 150 | (do 151 | (shell "cmd" "a") 152 | (shell "cmd" "b")))] 153 | (x/ast->forms (x/bash->ast "if true; then cmd a; cmd b; fi"))))) 154 | 155 | (deftest conditional-expr-== 156 | (is (= ['(= "x" "x")] (x/ast->forms (x/bash->ast "[[ x == x ]]"))))) 157 | 158 | (deftest conditional-expr-= 159 | (is (= ['(= "x" "x")] (x/ast->forms (x/bash->ast "[[ x = x ]]"))))) 160 | 161 | (deftest conditional-expr-!= 162 | (is (= ['(not= "x" "x")] (x/ast->forms (x/bash->ast "[[ x != x ]]"))))) 163 | 164 | (deftest conditional-expr-and 165 | (is (= '[(and (= "x" "x") (shell "cmd" "a"))] (x/ast->forms (x/bash->ast "[[ x == x ]] && cmd a"))))) 166 | 167 | (deftest var-assignment 168 | (is (= '[(def var "a")] (x/ast->forms (x/bash->ast "var=a"))))) 169 | 170 | (deftest var-expansion 171 | (is (= '[(def var "a") (shell "cmd" var)] 172 | (x/ast->forms (x/bash->ast "var=a; cmd $var"))))) 173 | 174 | (deftest envvar 175 | (is (= '[(shell {:extra-env {"ENVVAR" "a"}} "bash" "-c" "cmd $ENVVAR")] 176 | (x/ast->forms (x/bash->ast "ENVVAR=a bash -c 'cmd $ENVVAR'"))))) 177 | 178 | (deftest export-var 179 | (is (= '[(def VAR "a") (alter-var-root (var babashka.process/*defaults*) (fn [m] (update m :extra-env assoc "VAR" VAR)))] (x/ast->forms (x/bash->ast "VAR=a; export VAR"))))) 180 | 181 | (deftest export-and-set-var 182 | (is (= '[(do (def VAR "a") (alter-var-root (var babashka.process/*defaults*) (fn [m] (update m :extra-env assoc "VAR" VAR))))] (x/ast->forms (x/bash->ast "export VAR=a"))))) 183 | 184 | (deftest exit 185 | (is (= '[(System/exit 0)] 186 | (x/ast->forms (x/bash->ast "exit 0"))))) 187 | 188 | (deftest block 189 | (is (= ['(do (shell "cmd" "one") (shell "cmd" "two"))] 190 | (x/ast->forms (x/bash->ast "{ cmd one; cmd two; }"))))) 191 | 192 | (deftest block-boolean 193 | (is (= ['(or (do (zero? (:exit (shell {:continue true} "false")))) (shell "cmd" "a"))] 194 | (x/ast->forms (x/bash->ast "{ false; } || cmd a"))))) 195 | 196 | (deftest set-builtin 197 | (is (= [] 198 | (x/ast->forms (x/bash->ast "set -e"))))) 199 | 200 | (deftest set-builtin-in-block 201 | (is (= '[(do)] 202 | (x/ast->forms (x/bash->ast "{ set -e; }"))))) 203 | 204 | #_(deftest echo 205 | (is (= '[(println "hi")] 206 | (x/ast->forms (x/bash->ast "echo hi"))))) 207 | 208 | #_(deftest echo-n 209 | (is (= '[(print "hi")] 210 | (x/ast->forms (x/bash->ast "echo -n hi"))))) 211 | 212 | #_(deftest var-default 213 | (is (= [:???] 214 | (x/ast->forms (x/bash->ast "cmd ${1-mydefault}"))))) 215 | 216 | ;; ---------------------- 217 | 218 | (deftest has-state 219 | (is (map? (second (x/ast->forms+state (x/bash->ast "")))))) 220 | 221 | (deftest var-remembered-in-state 222 | (is (= {:vars #{'VAR}} (second (x/ast->forms+state (x/bash->ast "cmd $VAR")))))) 223 | 224 | (deftest declarations-none 225 | (is (= [] (x/declarations {})))) 226 | 227 | (deftest declarations-var 228 | (is (= '[(def VAR (System/getenv "VAR"))] (x/declarations {:vars #{'VAR}})))) 229 | 230 | (deftest bash->bb 231 | (is (= (str x/shebang "(require (quote [babashka.process :refer [shell pipeline pb]]))\n(shell \"cmd\" \"a\")\n") 232 | (x/bash->bb "cmd a")))) 233 | 234 | (deftest bash->bb-var 235 | (is (= (str x/shebang "(require (quote [babashka.process :refer [shell pipeline pb]]))\n(def VAR (System/getenv \"VAR\"))\n(shell \"cmd\" VAR)\n") 236 | (x/bash->bb "cmd $VAR")))) 237 | 238 | ;; TODO: 239 | ;; 240 | ;; should we use trim-newline on shell output? 241 | ;; for loop 242 | ;; export VAR=a 243 | ;; ( cd xxx; echo $PWD ) 244 | -------------------------------------------------------------------------------- /test/integration.clj: -------------------------------------------------------------------------------- 1 | (ns integration 2 | (:require 3 | [babashka.fs :as fs] 4 | [babashka.process :refer [shell]] 5 | [bash2bb.core :as x] 6 | [clojure.test :refer [deftest is run-tests]])) 7 | 8 | (deftest integration 9 | (doseq [fname (fs/glob "integration" "*.bash")] 10 | (let [bash (slurp (str fname)) 11 | bb (str (x/bash->bb bash) "\n" "nil")] 12 | (println "•" (fs/file-name fname)) 13 | (is (= (:out (shell {:out :string :extra-env {"WELLKNOWNVAR" "abc"}} 14 | "bb" "-e" bb)) 15 | (:out (shell {:out :string :extra-env {"WELLKNOWNVAR" "abc"}} 16 | "bash" "-c" bash))))))) 17 | 18 | (defn -main [] 19 | (run-tests 'integration)) 20 | -------------------------------------------------------------------------------- /test/runner.clj: -------------------------------------------------------------------------------- 1 | (ns runner 2 | (:require [babashka.fs :as fs] 3 | [clojure.stacktrace :as stacktrace] 4 | [clojure.string :as str] 5 | [clojure.test :as test] 6 | [cognitect.test-runner] 7 | [sci.core :as sci])) 8 | 9 | (defmacro try-expr 10 | "Used by the 'is' macro to catch unexpected exceptions. 11 | You don't call this." 12 | {:added "1.1"} 13 | [msg form] 14 | `(try ~(clojure.test/assert-expr msg form) 15 | (catch ~(with-meta 'Exception {:sci/error true}) t# 16 | (clojure.test/do-report {:type :error, :message ~msg, 17 | :expected '~form, :actual t#})))) 18 | 19 | (alter-var-root #'clojure.test/try-expr (constantly @#'try-expr)) 20 | 21 | (defn right-pad [s n] 22 | (let [n (- n (count s))] 23 | (str s (str/join (repeat n " "))))) 24 | 25 | (defn format-stacktrace [st] 26 | (let [st (force st) 27 | data (keep (fn [{:keys [:file :ns :line :column :sci/built-in 28 | :local] 29 | nom :name}] 30 | (when (or line built-in) 31 | {:name (str (if nom 32 | (str ns "/" nom) 33 | ns) 34 | (when local 35 | (str "#" local))) 36 | :loc (str (or (some->> file (fs/relativize (fs/cwd))) 37 | (if built-in 38 | "" 39 | "")) 40 | (when line 41 | (str ":" line ":" column)))})) 42 | st) 43 | max-name (reduce max 0 (map (comp count :name) data))] 44 | (mapv (fn [{:keys [:name :loc]}] 45 | (str (right-pad name max-name) " - " loc)) 46 | data))) 47 | 48 | (defn testing-vars-str 49 | [m] 50 | (let [{:keys [file line]} m] 51 | (str 52 | (reverse (map #(:name (meta %)) clojure.test/*testing-vars*)) 53 | " (" (when (and file (not= "" file)) 54 | (fs/relativize (fs/cwd) file)) ":" line ")"))) 55 | 56 | (defn print-stack-trace [e] 57 | (stacktrace/print-throwable (.getCause e)) 58 | (newline) 59 | (->> e 60 | (sci/stacktrace) 61 | (format-stacktrace) 62 | (run! println))) 63 | 64 | (defn report-error [m] 65 | (test/inc-report-counter :error) 66 | (println "\nERROR in" (testing-vars-str m)) 67 | (when-let [message (:message m)] (println message)) 68 | (println "expected:" (pr-str (:expected m))) 69 | (print " actual: ") 70 | (let [actual (:actual m)] 71 | (if (instance? Throwable actual) 72 | (if (= :sci/error (-> actual ex-data :type)) 73 | (print-stack-trace actual) 74 | (clojure.stacktrace/print-cause-trace actual)) 75 | (prn actual)))) 76 | 77 | (defn with-error-reporting [f] 78 | (let [original-report clojure.test/report] 79 | (with-redefs [clojure.test/report 80 | (fn [event] 81 | (if (and (= :error (:type event)) 82 | (instance? Throwable (:actual event))) 83 | (report-error event) 84 | (original-report event)))] 85 | (f)))) 86 | 87 | (defn -main [& args] 88 | (with-error-reporting (fn [] (apply cognitect.test-runner/-main args)))) 89 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | - [ ] remove stmt->form (singular) 2 | --------------------------------------------------------------------------------