diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11ecb79 --- /dev/null +++ b/LICENSE @@ -0,0 +1,198 @@ +Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation + distributed under this Agreement, and +b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + + where such changes and/or additions to the Program originate from and are + distributed by that particular Contributor. A Contribution 'originates' from + a Contributor if it was added to the Program by such Contributor itself or + anyone acting on such Contributor's behalf. Contributions do not include + additions to the Program which: (i) are separate modules of software + distributed in conjunction with the Program under their own license + agreement, and (ii) are not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + a) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free copyright license to + reproduce, prepare derivative works of, publicly display, publicly perform, + distribute and sublicense the Contribution of such Contributor, if any, and + such derivative works, in source code and object code form. + b) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free patent license under + Licensed Patents to make, use, sell, offer to sell, import and otherwise + transfer the Contribution of such Contributor, if any, in source code and + object code form. This patent license shall apply to the combination of the + Contribution and the Program if, at the time the Contribution is added by + the Contributor, such addition of the Contribution causes such combination + to be covered by the Licensed Patents. The patent license shall not apply + to any other combinations which include the Contribution. No hardware per + se is licensed hereunder. + c) Recipient understands that although each Contributor grants the licenses to + its Contributions set forth herein, no assurances are provided by any + Contributor that the Program does not infringe the patent or other + intellectual property rights of any other entity. Each Contributor + disclaims any liability to Recipient for claims brought by any other entity + based on infringement of intellectual property rights or otherwise. As a + condition to exercising the rights and licenses granted hereunder, each + Recipient hereby assumes sole responsibility to secure any other + intellectual property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to distribute the Program, it + is Recipient's responsibility to acquire that license before distributing + the Program. + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under its +own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + b) its license agreement: + i) effectively disclaims on behalf of all Contributors all warranties and + conditions, express and implied, including warranties or conditions of + title and non-infringement, and implied warranties or conditions of + merchantability and fitness for a particular purpose; + ii) effectively excludes on behalf of all Contributors all liability for + damages, including direct, indirect, special, incidental and + consequential damages, such as lost profits; + iii) states that any provisions which differ from this Agreement are offered + by that Contributor alone and not by any other party; and + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a reasonable + manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + b) a copy of this Agreement must be included with each copy of the Program. + Contributors may not remove or alter any copyright notices contained within + the Program. + +Each Contributor must identify itself as the originator of its Contribution, if +any, in a manner that reasonably allows subsequent Recipients to identify the +originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with +respect to end users, business partners and the like. While this license is +intended to facilitate the commercial use of the Program, the Contributor who +includes the Program in a commercial product offering should do so in a manner +which does not create potential liability for other Contributors. Therefore, if +a Contributor includes the Program in a commercial product offering, such +Contributor ("Commercial Contributor") hereby agrees to defend and indemnify +every other Contributor ("Indemnified Contributor") against any losses, damages +and costs (collectively "Losses") arising from claims, lawsuits and other legal +actions brought by a third party against the Indemnified Contributor to the +extent caused by the acts or omissions of such Commercial Contributor in +connection with its distribution of the Program in a commercial product +offering. The obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property infringement. In order +to qualify, an Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial Contributor to +control, and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may participate in +any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product +offering, Product X. That Contributor is then a Commercial Contributor. If that +Commercial Contributor then makes performance claims, or offers warranties +related to Product X, those performance claims and warranties are such +Commercial Contributor's responsibility alone. Under this section, the +Commercial Contributor would have to defend claims against the other +Contributors related to those performance claims and warranties, and if a court +requires any other Contributor to pay any damages as a result, the Commercial +Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each +Recipient is solely responsible for determining the appropriateness of using and +distributing the Program and assumes all risks associated with its exercise of +rights under this Agreement , including but not limited to the risks and costs +of program errors, compliance with applicable laws, damage to or loss of data, +programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY +CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS +GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable +law, it shall not affect the validity or enforceability of the remainder of the +terms of this Agreement, and without further action by the parties hereto, such +provision shall be reformed to the minimum extent necessary to make such +provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Program itself +(excluding combinations of the Program with other software or hardware) +infringes such Recipient's patent(s), then such Recipient's rights granted under +Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to +comply with any of the material terms or conditions of this Agreement and does +not cure such failure in a reasonable period of time after becoming aware of +such noncompliance. If all Recipient's rights under this Agreement terminate, +Recipient agrees to cease use and distribution of the Program as soon as +reasonably practicable. However, Recipient's obligations under this Agreement +and any licenses granted by Recipient relating to the Program shall continue and +survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in +order to avoid inconsistency the Agreement is copyrighted and may only be +modified in the following manner. The Agreement Steward reserves the right to +publish new versions (including revisions) of this Agreement from time to time. +No one other than the Agreement Steward has the right to modify this Agreement. +The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation +may assign the responsibility to serve as the Agreement Steward to a suitable +separate entity. Each new version of the Agreement will be given a +distinguishing version number. The Program (including Contributions) may always +be distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to distribute the Program (including its Contributions) +under the new version. Except as expressly stated in Sections 2(a) and 2(b) +above, Recipient receives no rights or licenses to the intellectual property of +any Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted under +this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial in +any resulting litigation. diff --git a/README.md b/README.md index 04704f0..4032bd0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ # deps.clj -A rewrite of the clojure bash script in clojure. Can be run with babashka. + +A port of the [clojure](https://github.com/clojure/brew-install/) bash script in +clojure. Can be run with [babashka](https://github.com/borkdude/babashka/). + +## Status + +Extremely experimental, breaking changes will happen. Feedback is welcome. + +## Non-standard options + +The `deps.clj` script adds the following non-standard options: + +``` + -Sdeps-file Use this file instead of deps.edn + -Scommand A custom command that will be invoked. Substitutions: {{classpath}}, `{{main-opts}}`. +``` + +## Example usage + +Gives this `script-deps.edn` file: + +``` +{:paths ["scripts"] + :aliases + {:main + {:main-opts ["-m" "scripts.main"]}}} +``` + +and `scripts/main.clj`: + +``` +(ns scripts.main) + +(defn -main [& _args] + (println "Hello from script!")) +``` + +you can invoke `deps.clj` as follows: + +``` shell +$ deps.clj -Sdeps-file script-deps.edn -A:main -Scommand "bb -cp {{classpath}} {{main-opts}}" +Hello from script! +``` + +## License + +Copyright © 2019 Michiel Borkent + +Distributed under the EPL License. See LICENSE. + +This project is based on code from +[clojure/brew-install](https://github.com/clojure/brew-install/) which is +licensed under the same EPL License. diff --git a/deps.clj b/deps.clj new file mode 100755 index 0000000..89058e2 --- /dev/null +++ b/deps.clj @@ -0,0 +1,383 @@ +#!/usr/bin/env bb --verbose + +(require '[clojure.string :as str] + '[clojure.java.io :as io]) + +(import '[java.lang ProcessBuilder$Redirect]) + +(set! *warn-on-reflection* true) + +(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 project-version "0.0.1-SNAPSHOT") + +(def help-text (str/trim " +Usage: clojure [dep-opt*] [init-opt*] [main-opt] [arg*] + clj [dep-opt*] [init-opt*] [main-opt] [arg*] + +The clojure script is a runner for Clojure. clj is a wrapper +for interactive repl use. These scripts ultimately construct and +invoke a command-line of the form: + +java [java-opt*] -cp classpath clojure.main [init-opt*] [main-opt] [arg*] + +The dep-opts are used to build the java-opts and classpath: + -Jopt Pass opt through in java_opts, ex: -J-Xmx512m + -Oalias... Concatenated jvm option aliases, ex: -O:mem + -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 last deps file to be merged + -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 + -Strace Write a trace.edn file that traces deps expansion + +The following non-standard options are added: + + -Sdeps-file Use this file instead of deps.edn + -Scommand A custom command that will be invoked. Substitutions: {{classpath}}, `{{main-opts}}`. + +init-opt: + -i, --init path Load a file or resource + -e, --eval string Eval exprs in string; print non-nil values + --report target Report uncaught exception to \"file\" (default), \"stderr\", or \"none\", + overrides System property clojure.main.report + +main-opt: + -m, --main ns-name Call the -main function from namespace w/args + -r, --repl Run a repl + path Run a script from a file or resource + - Run a script from standard input + -h, -?, --help Print this help message and exit + +For more info, see: + https://clojure.org/guides/deps_and_cli + https://clojure.org/reference/repl_and_main +")) + +(def parse-opts->keyword + {"-J" :jvm-opts + "-R" :resolve-aliases + "-C" :classpath-aliases + "-O" :jvm-aliases + "-M" :main-aliases + "-A" :all-aliases}) + +(def bool-opts->keyword + {"-Spath" :print-classpath + "-Sverbose" :verbose + "-Strace" :trace + "-Sdescribe" :describe + "-Sforce" :force + "-Srepro" :repro + "-Stree" :tree + "-Spom" :pom + "-Sresolve-tags" :resolve-tags}) + +(def string-opts->keyword + {"-Sdeps" :deps-data + "-Scp" :force-cp + "-Sdeps-file" :deps-file + "-Scommand" :command}) + +(def args "the parsed arguments" + (loop [command-line-args (seq *command-line-args*) + acc {}] + (if command-line-args + (let [arg (first command-line-args) + bool-opt-keyword (get bool-opts->keyword arg) + string-opt-keyword (get string-opts->keyword arg)] + (cond (some #(str/starts-with? arg %) ["-J" "-R" "-C" "-O" "-M" "-A"]) + (recur (next command-line-args) + (update acc (get parse-opts->keyword (subs arg 0 2)) + str (subs arg 2))) + bool-opt-keyword (recur + (next command-line-args) + (assoc acc bool-opt-keyword true)) + string-opt-keyword (recur + (nnext command-line-args) + (assoc acc string-opt-keyword + (second command-line-args))) + (str/starts-with? arg "-S") (binding [*out* *err*] + (println "Invalid option:" arg) + (System/exit 1)) + (and + (not (some acc [:main-aliases :all-aliases])) + (or (= "-h" arg) + (= "--help" arg))) (assoc acc :help true) + :else (assoc acc :args command-line-args))) + acc))) + +(when (:help args) + (println help-text) + (System/exit 0)) + +(def java-cmd "the java executable" + (let [java-cmd (str/trim (shell-command ["type" "-p" "java"] {:to-string? true}))] + (if (str/blank? java-cmd) + (let [java-home (System/getenv "JAVA_HOME")] + (if-not (str/blank? java-home) + (let [f (io/file java-home "bin" "java")] + (if (and (.exists f) + (.canExecute f)) + (.getCanonicalPath f) + (throw (Exception. "Couldn't find 'java'. Please set JAVA_HOME.")))) + (throw (Exception. "Couldn't find 'java'. Please set JAVA_HOME.")))) + java-cmd))) + +(def install-dir + (let [clojure-on-path (str/trim (shell-command ["type" "-p" "clojure"] {:to-string? true})) + f (io/file clojure-on-path) + f (io/file (.getCanonicalPath f)) + parent (.getParent f) + parent (.getParent (io/file parent))] + parent)) + +(def tools-cp + (let [files (.listFiles (io/file install-dir "libexec")) + jar (some #(let [name (.getName %)] + (when (and (str/starts-with? name "clojure-tools") + (str/ends-with? name ".jar")) + %)) + files)] + (.getCanonicalPath jar))) + +(def deps-edn (or (:deps-file args) + "deps.edn")) + +(when (:resolve-tags args) + (let [f (io/file deps-edn)] + (if (.exists f) + (do (shell-command [java-cmd "-Xms256m" "-classpath" tools-cp + "clojure.main" "-m" "clojure.tools.deps.alpha.script.resolve-tags" + "--deps-file=deps.edn"]) + (System/exit 0)) + (binding [*out* *err*] + (println "deps.edn does not exist") + (System/exit 1))))) + +(def config-dir + (or (System/getenv "CLJ_CONFIG") + (when-let [xdg-config-home (System/getenv "XDG_CONFIG_HOME")] + (.getPath (io/file xdg-config-home "clojure"))) + (.getPath (io/file (System/getProperty "user.home") ".clojure")))) + +;; If user config directory does not exist, create it +(when-not (.exists (io/file config-dir)) + (.mkdirs config-dir)) + +(let [config-deps-edn (io/file config-dir "deps.edn")] + (when-not (.exists config-deps-edn) + (io/copy (io/file install-dir "example-deps.edn") + config-deps-edn))) + +;; Determine user cache directory +(def user-cache-dir + (or (System/getenv "CLJ_CACHE") + (when-let [xdg-config-home (System/getenv "XDG_CACHE_HOME")] + (.getPath (io/file xdg-config-home "clojure"))) + (.getPath (io/file config-dir ".cpcache")))) + +;; Chain deps.edn in config paths. repro=skip config dir +(def config-user + (when-not (:repro args) + (.getPath (io/file config-dir "deps.edn")))) + +(def config-project deps-edn) +(def config-paths + (if (:repro args) + [(.getPath (io/file install-dir "deps.edn")) deps-edn] + [(.getPath (io/file install-dir "deps.edn")) + (.getPath (io/file config-dir "deps.edn")) + deps-edn])) + +(def config-str (str/join "," config-paths)) + +;; Determine whether to use user or project cache +(def cache-dir + (if (.exists (io/file "deps.edn")) + ".cpcache" + user-cache-dir)) + +;; Construct location of cached classpath file +(def val* + (str/join "|" + (concat [(:resolve-aliases args) + (:classpath-aliases args) + (:all-aliases args) + (:jvm-aliases args) + (:main-aliases args) + (:deps-data args)] + (map (fn [config-path] + (if (.exists (io/file config-path)) + config-path + "NIL")) + config-paths)))) + +(def ck (-> (shell-command ["cksum"] {:input val* + :to-string? true}) + (str/split #" ") + first)) + +(def libs-file (.getPath (io/file cache-dir (str ck ".libs")))) +(def cp-file (.getPath (io/file cache-dir (str ck ".cp")))) +(def jvm-file (.getPath (io/file cache-dir (str ck ".jvm")))) +(def main-file (.getPath (io/file cache-dir (str ck ".main")))) + +(when (:verbose args) + (println "version =" project-version) + (println "install_dir =" install-dir) + (println "config_dir =" config-dir) + (println "config_paths =" (str/join " " config-paths)) + (println "cache_dir =" cache-dir) + (println "cp_file =" cp-file) + (println)) + +(def stale "true if classpath file is stale" + (or (:force args) + (:trace args) + (not (.exists (io/file cp-file))) + (let [cp-file (io/file cp-file)] + (some (fn [config-path] + (let [f (io/file config-path)] + (or (not (.exists f)) + (> (.lastModified f) + (.lastModified cp-file))))) config-paths)))) + +(def tools-args + (when (or stale (:pom args)) + (cond-> [] + (not (str/blank? (:deps-data args))) + (conj "--config-data" (:deps-data args)) + (:resolve-aliases args) + (conj (str "-R" (:resolve-aliases args))) + (:classpath-aliases args) + (conj (str "-C" (:classpath-aliases args))) + (:jvm-aliases args) + (conj (str "-J" (:jvm-aliases args))) + (:main-aliases args) + (conj (str "-M" (:main-aliases args))) + (:all-aliases args) + (conj (str "-A" (:all-aliases args))) + (:force-cp args) + (conj "--skip-cp") + (:trace args) + (conj "--trace")))) + +;; If stale, run make-classpath to refresh cached classpath +(when (and stale (not (:describe args))) + (when (:verbose args) + (println "Refreshing classpath")) + (shell-command (into [java-cmd "-Xms256m" + "-classpath" tools-cp + "clojure.main" "-m" "clojure.tools.deps.alpha.script.make-classpath2" + "--config-user" config-user + "--config-project" config-project + "--libs-file" libs-file + "--cp-file" cp-file + "--jvm-file" jvm-file + "--main-file" main-file] + tools-args))) + +(def cp + (cond (:describe args) nil + (not (str/blank? (:force-cp args))) (:force-cp args) + :else (slurp cp-file))) + +(defn describe-line [[kw val]] + (pr kw val )) + +(defn describe [lines] + (let [[first-line & lines] lines] + (print "{") (describe-line first-line) + (doseq [line lines] + (print "\n ") (describe-line line)) + (println "}"))) + +(cond (:pom args) + (shell-command [java-cmd "-Xms256m" + "-classpath" tools-cp + "clojure.main" "-m" "clojure.tools.deps.alpha.script.generate-manifest2" + "--config-user" config-user + "--config-project" config-project + "--gen=pom" (str/join " " tools-args)]) + (:print-classpath args) + (println cp) + (:describe args) + (describe [[:version project-version] + [:config-files (filterv #(.exists (io/file %)) config-paths)] + [:config-user config-user] + [:config-project config-project] + [:install-dir install-dir] + [:cache-dir cache-dir] + [:force (str (:force args))] + [:repro (str (:repro args))] + [:resolve-aliases (str (:resolve-aliases args))] + [:classpath-aliases (str (:claspath-aliases args))] + [:jvm-aliases (str (:jvm-aliases args))] + [:main-aliases (str (:main-aliases args))] + [:all-aliases (str (:all-aliases args))]]) + (:tree args) + (println (str/trim (shell-command [java-cmd "-Xms256m" + "-classpath" tools-cp + "clojure.main" "-m" "clojure.tools.deps.alpha.script.print-tree" + "--libs-file" libs-file] + {:to-string? true}))) + (:trace args) + (println "Writing trace.edn") + (:command args) + (let [command (str/replace (:command args) "{{classpath}}" (str cp)) + main-cache-opts (when (.exists (io/file main-file)) + (slurp main-file)) + command (str/replace command "{{main-opts}}" (str main-cache-opts)) + command (str/split command #"\s+")] + (shell-command command)) + :else + (let [jvm-cache-opts (when (.exists (io/file jvm-file)) + (slurp jvm-file)) + main-cache-opts (when (.exists (io/file main-file)) + (slurp main-file)) + main-cache-opts (when main-cache-opts (str/split main-cache-opts #"\s")) + main-args (into (vector java-cmd jvm-cache-opts (:jvm-opts args) + (str "-Dclojure.libfile=" libs-file) "-classpath" cp "clojure.main") main-cache-opts) + main-args (filterv some? main-args) + main-args (into main-args (:args args))] + (shell-command main-args)))