Exporting environmental variables in Clojure / Babashka - clojure

I am trying to write an install script in babaska for my other babaska script (for some fun homogeneity).
Export doesn't seem to work using:
(shell "export NAME=" value)
Is there a canonical way to set environmental variables in Clojure / Babashka?

export is shell built-in; you can not call it outside a shell. So
what you want to do instead is run your command with another
environment. This can be done in sh with the :env
option. E.g.
(-> (shell/sh "sh" "-c" "echo $X" :env {"X" "Hello"}) :out)
edit: global state for environment
At least on the JVM, there is no easy way to change the
environment.
So you are better off, writing your own function to do the calls and
merge with your own global environment.
This example uses an atom to keep the environment around:
(def sh-env (atom {}))
(defn export!
[m]
(swap! sh-env merge m))
(defn sh
([cmd]
(sh cmd {}))
([cmd env]
(apply shell/sh (concat cmd [:env (merge #sh-env env)]))))
;;;
(def echo ["sh" "-c" "echo $X"])
(prn (sh echo))
(export! {"X" "Hello"})
(prn (sh echo))
(prn (sh echo {"X" "Goodbye"}))
(export! {"X" "Goodbye"})
(prn (sh echo))
; {:exit 0, :out "\n", :err ""}
; {:exit 0, :out "Hello\n", :err ""}
; {:exit 0, :out "Goodbye\n", :err ""}
; {:exit 0, :out "Goodbye\n", :err ""}

They way to set environment variables with shell is like this:
(shell {:extra-env {"NAME" "FOOBAR"}} command)
See docs here.

Related

Passing command line arguments in lein repl

I have a main function declaration which accepts command line arguments
(defn -main [& args]
(let [options (cli/parse-opts args [["-k", "--key KEY","Secret key"]
["-u", "--user USER", "User name"]])]
(println user key)))
I am trying to load this in lein repl for debugging, but unable to figure out how to pass arguments over REPL.
I have tried multiple methods to pass the args over repl including
(-main "key-val" "user-val") (prints nil nil)
(-main ("key-val" "user-val")) (prints nil nil)
I tried multiple failing attempts as well trying to pass a list or vector which throw cast errors.
you have defined params with keys "-k" and "-u", but haven't passed the keys to the function, so the parser won't be able to guess what do you expect it to get. The working version is like this:
user> (defn -main [& args]
(let [options (cli/parse-opts args [["-k", "--key KEY","Secret key"]
["-u", "--user USER", "User name"]])
{:keys [key user]} (:options options)]
[key user]))
#'user/-main
user> (-main "-k" "key-val" "-u" "user-val")
;;=> ["key-val" "user-val"]
user> (-main "--key" "key-val" "--user" "user-val")
;;=> ["key-val" "user-val"]
First, note that user and key are not bound to result of parse-opts.
parse-opts returns a map where these options are under the key :options.
Let's bind the result as follows:
(defn -main [& args]
(let [options (parse-opts args [["-k", "--key KEY","Secret key"]
["-u", "--user USER", "User name"]])
user (get-in options [:options :user])
key (get-in options [:options :key])]
(println user key)))
Your parse-opts call specifies that you expect named arguments, i.e., options to parse. A call like the following works from REPL:
(-main "-k a" "-u b") ; prints "b a"
Note that if you specify positional arguments, they are under the key :arguments. After changing the example to print the value bound to options ((println options)), your example call would work like so:
(-main "key-val" "user-val") ; {:arguments [key-val user-val] :options {} ...}
So the call was made without options, that is, without specifying named arguments.

clojure core.tools.cli: How to override boolean option?

I want a command that takes arguments which look like this:
--enable-boolean-flag --disable-boolean-flag --enable-boolean-flag
In the :options key returned by clojure.tools.cli/parse-opts, I want to have the :boolean-flag option set to true if the --enable-boolean-flag option came last on the command line and false if --disable-boolean-flag came last on the command line, if that makes any sense.
Any ideas?
EDIT: I'm using 0.3.6 of the core.tools.cli library.
You can achieve this by taking advantage of :id, :default, and :assoc-fn properties that tools-cli lets you specify for each command line option.
Use :id to set the same id for "--enable" and "--disable" options
Use :default on one of the options to specify what you want to happen if neither "--enable" or "--disable" are specified
Use :assoc-fn to specify what effect the option has on the options map. You want the value set to false every time "--disable" appears and to true every time --enable appears.
Putting it all together:
(ns clis.core
(:require [clojure.tools.cli :refer [parse-opts]])
(:gen-class))
(def cli-options
[["-e" "--enable" "Enable"
:default true
:id :boolean-flag
:assoc-fn (fn [m k _] (assoc m k true))]
["-d" "--disable" "Disable"
:id :boolean-flag
:assoc-fn (fn [m k _] (assoc m k false))]])
(defn -main [& args]
(parse-opts args cli-options))
Testing at the REPL:
(-main)
;; {:options {:boolean-flag true}, :arguments [], :summary " -e, --enable Enable\n -d, --disable Disable", :errors nil}
(-main "-e" "-d" "-e")
;; {:options {:boolean-flag true}, :arguments [], :summary " -e, --enable Enable\n -d, --disable Disable", :errors nil}
(-main "-e" "-d" "-e" "-d")
;; {:options {:boolean-flag false}, :arguments [], :summary " -e, --enable Enable\n -d, --disable Disable", :errors nil}

Light Table read input

I'm having some trouble to get started with Light Table.
Here's my code (Clojure)
(ns prova1-ed.core
(:gen-class))
(use 'clojure.java.io)
(defn -main [& args]
(println "Type the name of the file to read: ")
(let [fileName (read-line)]
(let [rdr (reader fileName)]
(doseq [line (line-seq rdr)]
(println line)
)))
)
I'm sure it works. I've tested with lein run. As you can see, the program should read a file which the name is given by the user.
I've tried CTRL+SPACE in Light Table, but this is what I receive:
ERROR: Unhandled REPL handler exception processing message {:data {:auto? false, :pos {:line 14, :ch 1}, :mime "text/x-clojure", :tags [:editor.clj :editor.clojure], :type-name "Clojure", :line-ending "\r\n", :ns user, :path "C:\\Users\\Tiago\\Documents\\Clojure\\prova1_ed\\src\\prova1_ed\\core.clj", :print-length nil, :name "core.clj", :local true, :code "(ns prova1-ed.core\n (:gen-class))\n\n(use 'clojure.java.io)\n\n(defn -main [& args]\n\n (println \"Type the name of the file to read: \")\n\n (let [fileName (read-line)]\n (let [rdr (reader fileName)]\n (doseq [line (line-seq rdr)]\n (println line)\n )))\n)\n"}, :id 90, :op editor.eval.clj.sonar, :session 65d1da68-a730-4ffe-9365-9527726384e3}
How can i run it in the Light Tables' enviroment, so that I can input the file name?
TLDR
I don't think you can run (read-line) in Light Table as it'd have to add explicit support for allowing input. There's no standard input basically.
An Alternative
I'd suggest you modify your -main function to accept an explicit file-name argument instead of trying to read it from a standard input that isn't available.
I've got a Clojure webapp that I work on in Light Table.
I've got a -main function in a namespace named my-app.web. It looks something like this:
(defn -main [& [port]]
(let [port (Integer. (or port (env :port) 5000))
store (cookie/cookie-store {:key (env :session-secret)})]
(jetty/run-jetty (-> #'secured-app
wrap-with-logging
wrap-current-user
wrap-current-auth
wrap-error-page
(site {:session {:store store}}))
{:port port :join? false})))
In a separate file I've named light-table-start.clj, I've got the following code to run my app inside Light Table:
(require '[my-app.web :as web])
(require '[ring.adapter.jetty :as jetty])
(defonce server (web/-main "5000"))
;; (.start server)
;; (.stop server)
I run the Eval: Eval editor contents command (Ctrl+Shift+Enter on Windows and Linux or ⌘+Shift+Enter on Mac OS) the first time I want to run my app (or later, if the connection is closed for some reason). When I want to start or stop the server I can just highlight the code on the respective commented lines and run the Eval: Eval a form in editor command (Ctrl+Enter on Windows and Linux or ⌘+Enter on Mac OS).

How to substitute path to home for "~"?

If I pass in a path from the command line, "~" expands to my home directory:
(defn -main
"I don't do a whole lot ... yet."
[& args]
(doseq [arg args]
(println arg)))
britannia:uberjar srseverance$ java -jar args-0.1.0-SNAPSHOT-standalone.jar ~/158.clj
/Volumes/Macintosh HD/Users/srseverance/158.clj
But if I try to use a path-file containing ~, I can't find the file.
user> (with-open [r (clojure.java.io/reader "~/158.clj")]
(doall (line-seq r)))
FileNotFoundException ~/158.clj (No such file or directory) java.io.FileInputStream.open0 (FileInputStream.java:-2)
How do I take a string like, "~/158.clj" and get back something clojure.java.io/reader can use, such as "/Volumes/Macintosh HD/Users/srseverance/158.clj"?
You can define
(defn expand-home [s]
(if (.startsWith s "~")
(clojure.string/replace-first s "~" (System/getProperty "user.home"))
s))
and use it to resolve home directory:
(clojure.java.io/reader (expand-home "~/158.clj"))]
You could also look into fs library definition of expand-home, which solves the ~foo problem outlined in bfontaine's comment below:
(let [homedir (io/file (System/getProperty "user.home"))
usersdir (.getParent homedir)]
(defn home
"With no arguments, returns the current value of the `user.home` system
property. If a `user` is passed, returns that user's home directory. It
is naively assumed to be a directory with the same name as the `user`
located relative to the parent of the current value of `user.home`."
([] homedir)
([user] (if (empty? user) homedir (io/file usersdir user)))))
(defn expand-home
"If `path` begins with a tilde (`~`), expand the tilde to the value
of the `user.home` system property. If the `path` begins with a
tilde immediately followed by some characters, they are assumed to
be a username. This is expanded to the path to that user's home
directory. This is (naively) assumed to be a directory with the same
name as the user relative to the parent of the current value of
`user.home`."
[path]
(let [path (str path)]
(if (.startsWith path "~")
(let [sep (.indexOf path File/separator)]
(if (neg? sep)
(home (subs path 1))
(io/file (home (subs path 1 sep)) (subs path (inc sep)))))
path)))
Addressing bfontaine's comment, we can get correct results for ~user and ~root by asking the system instead:
(require '[clojure.java.shell :refer [sh]])
(defn bash [command]
(sh "bash" "-c" command))
(defn expand [path]
(-> (str "echo -n " path)
bash
:out))
(expand "~")
;; => /home/teodorlu
(expand "~teodorlu")
;; => /home/teodorlu
(expand "~root")
;; => /root
Though, just use this for trusted code!
(expand "`cat ~/.passwords`")
;; => All my passwords!

How to display Clojure version in REPL?

Such as:
(println clojure-version)
?
Even shorter :
user> (clojure-version)
"1.2.0-beta1"
user>
Oops, I have to upgrade...
Very close.
user> (println *clojure-version*)
{:major 1, :minor 2, :incremental 0, :qualifier }
nil
Most builtin "global" variables like this have Common Lisp-style asterisk "earmuffs".
Just typing *clojure-version* will do the trick.
*clojure-version*
=> {:major 1, :minor 3, :incremental 0, :qualifier nil}
Extra repl examples
user=> (clojure-version)
"1.10.1"
user=> (println (clojure-version))
1.10.1
nil
user=> (print (clojure-version))
1.10.1nil
Use from clj
$ clj -M -e "(clojure-version)"
"1.10.1"
$ clj -M -e "(print (clojure-version))"
1.10.1
$ clj -M -e "*clojure-version*"
{:major 1, :minor 10, :incremental 1, :qualifier nil}
clojure -M -e '(println "Java" (System/getProperty "java.version") "Clojure" (clojure-version))'