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

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}

Related

Exporting environmental variables in Clojure / Babashka

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.

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.

Parse Clojure Command Line Args

I am trying to separate my cli options into a stand-alone namespace for starting up an HTTP server, and I am getting this error-
clojure.lang.ArraySeq cannot be cast to java.lang.CharSequence
In main.clj, this code works fine-
(ns served.main
(:require [org.httpkit.server :refer [run-server]]
[served.app.core :refer [handler]]
[served.server.cli-options :refer [set-options]]
[clojure.tools.cli :refer [parse-opts]])
(:gen-class))
(def cli-options
[
["-p" "--port PORT" "Port number"
:default 5000
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
])
(defn -main [& args]
(println "Server starting")
(let [options (get (parse-opts args cli-options) :options)]
;;(let [options (set-options args)]
(println (str options))
(run-server handler options)))
It will work with the default options in (def cli-options) and it compiles correctly if I pass in arguments, such as -p 7000.
When I call the main function with the external namespace served.server.cli-options instead of clojure.tools.cli directly (i.e. switch the comment in main), I get the error only when passing in args.
That is, starting the server without arguments, e.g. lein run compiles fine and will print out the defaults. The error comes with lein run -p 7000.
After deleting (def cli-options) in main to avoid any global conflict, here is served.server.cli-options
(ns served.server.cli-options
(:require [clojure.tools.cli :refer [parse-opts]]))
(def cli-options
[
["-p" "--port PORT" "Port number"
:default 5000
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
])
(defn set-options [& args]
(let [options (get (parse-opts args cli-options) :options)]
(println (str options))
options))
So far as I can tell, I copied the contents to the new namespace correctly. Here are the docs for parse-opts, here is the example that I am drawing from, and a similar but different SO issue here.
My question - how are the CLI args being transformed to throw casting error, and how do I fix it?
Any help would be greatly appreciated.
Delete the & in:
(defn set-options [& args]
& wraps up any additional arguments in a seq. Since you’ve already wrapped the program arguments once in main, you mustn’t do it again in the call to set-options.

Why can't clojure.repl/print-doc binding be changed in Clojure REPL?

This works as expected:
java -jar clojure-1.4.0.jar -e "(do (require 'clojure.repl) (.setDynamic #'clojure.repl/print-doc) (with-bindings {#'clojure.repl/print-doc str} (eval '(clojure.repl/doc println))))"
Output:
"{:ns #<Namespace clojure.core>, :name println, :arglists ([& more]), :added \"1.0\", :static true, :doc \"Same as print followed by (newline)\", :line 3325, :file \"clojure/core.clj\"}"
But the same does not work in the REPL:
java -jar clojure-1.4.0.jar -e "(do (require 'clojure.repl) (.setDynamic #'clojure.repl/print-doc) (clojure.main/repl :init (fn [] {#'clojure.repl/print-doc str}))))"
Output of (doc println):
user=> (doc println)
-------------------------
clojure.core/println
([& more])
Same as print followed by (newline)
nil
user=>
I don't know what I'm doing wrong.
Found the answer after diving into the counterclockwise and nrepl code:
java -jar clojure-1.4.0.jar -e "(do (require 'clojure.repl) (.setDynamic #'clojure.repl/print-doc) (with-bindings {#'clojure.repl/print-doc str} (clojure.main/repl)))))"
The output is the same as above:
"{:ns #<Namespace clojure.core>, :name println, :arglists ([& more]), :added \"1.0\", :static true, :doc \"Same as print followed by (newline)\", :line 3325, :file \"clojure/core.clj\"}"
The trick is to use with-bindings before calling repl:
(with-bindings {#'clojure.repl/print-doc str}
(repl))

Number format exception in compojure

As total clojure noob, I am trying to start one small tutorial app, in order to get familiar with compojure. It's a small application which lets user add two numbers, and after clicking on button displays their sum on the other page. I followed instruction from Mark McGranaghan blog. Everything seems ok, until I try to get sum of two numbers I have entered, instead of getting result, I am redirected to the same page (so basically I am stuck on first step of this tutorial). After checking the code, it seems that NumberFormatException is triggered when input parsing takes place (for some reason). In all my tests, I have tried to input all kinds of number format , but with no success. Here is the simplest code version , for which author said should work (I have tried the latest version from github site- same scenario: NFE):
(ns adder.core
(:use compojure.core)
(:use hiccup.core)
(:use hiccup.page-helpers))
(defn view-layout [& content]
(html
(doctype :xhtml-strict)
(xhtml-tag "en"
[:head
[:meta {:http-equiv "Content-type"
:content "text/html; charset=utf-8"}]
[:title "adder"]]
[:body content])))
(defn view-input []
(view-layout
[:h2 "add two numbers"]
[:form {:method "post" :action "/"}
[:input.math {:type "text" :name "a"}] [:span.math " + "]
[:input.math {:type "text" :name "b"}] [:br]
[:input.action {:type "submit" :value "add"}]]))
(defn view-output [a b sum]
(view-layout
[:h2 "two numbers added"]
[:p.math a " + " b " = " sum]
[:a.action {:href "/"} "add more numbers"]))
(defn parse-input [a b] ;; this is the place where problem occures
[(Integer/parseInt a) (Integer/parseInt b)])
(defroutes app
(GET "/" []
(view-input))
(POST "/" [a b]
(let [[a b] (parse-input a b)
sum (+ a b)]
(view-output a b sum)))
Can anyone tell me better way to pars the input values, in order to avoid this exception?I have tried couple of techniques , but nothing worked for me. I am using Leningen v1.7.1 with clojure 1.3 on win 7 machine.
Here is content of my project.clj file:
(defproject adder "0.0.1"
:description "Add two numbers."
:dependencies
[[org.clojure/clojure "1.3.0"]
[org.clojure/clojure-contrib "1.1.0"]
[ring/ring-core "1.0.2"]
[ring/ring-devel "1.0.2"]
[ring/ring-jetty-adapter "1.0.2"]
[compojure "1.0.1"]
[hiccup "0.3.8"]]
:dev-dependencies
[[lein-run "1.0.0"]])
and run.clj script:
(use 'ring.adapter.jetty)
(require 'adder.core)
(let [port (Integer/parseInt (get (System/getenv) "PORT" "8080"))]
(run-jetty #'adder.core/app {:port port}))
Thanks.
You are using compojure 1.0.1, the example in the blog you are following is using compojure 0.4.0.
As of version 0.6.0, Compojure no longer adds default middleware to routes. This means you must explicitly add the wrap-params and wrap-cookies middleware to your routes.
Source: https://github.com/weavejester/compojure
So you need to explicitly add the wrap-params middleware. So the following changes are required...
(ns adder.core
(:use ; change to idiomatic usage of :use
[compojure.core]
[hiccup.core]
[hiccup.page-helpers]
[ring.middleware.params :only [wrap-params]])) ; add middleware for params
(defn view-layout [& content]
(html
(doctype :xhtml-strict)
(xhtml-tag "en"
[:head
[:meta {:http-equiv "Content-type"
:content "text/html; charset=utf-8"}]
[:title "adder"]]
[:body content])))
(defn view-input []
(view-layout
[:h2 "add two numbers"]
[:form {:method "post" :action "/"}
[:input.math {:type "text" :name "a" :id "a"}] [:span.math " + "]
[:input.math {:type "text" :name "b" :id "a"}] [:br]
[:input.action {:type "submit" :value "add"}]]))
(defn view-output [a b sum]
(view-layout
[:h2 "two numbers added"]
[:p.math a " + " b " = " sum]
[:a.action {:href "/"} "add more numbers"]))
(defn parse-input [a b]
[(Integer/parseInt a) (Integer/parseInt b)])
(defroutes main-routes ; needs to be renamed
(GET "/" []
(view-input))
(POST "/" [a b]
(let [[a b] (parse-input a b)
sum (+ a b)]
(view-output a b sum))))
(def app (wrap-params main-routes)) ; wrap the params to allow destructuring to work