How To Execute A Split String As Arguments To Clojure.shell/sh - clojure

I have a command like:
(clojure.java.shell/sh "curl" "https://somesite.com" "-F" "file=#somefile.txt")
and I want to create a function that will run standard shell commands I supply... something like this:
(defn run-shell-command [command]
(clojure.java.shell/sh (clojure.string/split command #" ")))
so that I could call it like this:
(run-shell-command "curl https://somesite.com -F file=#somefile.txt")
but this throws:
Unhandled java.lang.IllegalArgumentException
No value supplied for key: ["curl" "https://somesite.com" "-F" "file=#somefile.txt"]
How do I make this work?
(of course, open to better ideas for how to do this)

You can also use the underlying command processor to process the string. E.g. using bash:
(clojure.java.shell/sh "bash" "-c" command)
...wrap it in a function and hide the command processor if you wish:
(defn run-shell-command [command]
(clojure.java.shell/sh "bash" "-c" command))
then you can use any bash shell commands. E.g.
(run-shell-command "for i in $(ls); do echo $i; done")

Your question is a common one amongst beginner lispers: If we have a function accepting arguments, how can we apply the function on a list of these arguments?
(defn f [a b c] (+ a b c))
(def args [1 2 3])
The answer is, as hinted above, the apply method. The method applies a function to a list of arguments. So in the example above:
(= (f 1 2 3)
(apply f args))
i.e
(defn run-shell-command [command]
(apply clojure.java.shell/sh (clojure.string/split command #" ")))

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 clojure.string/split hit java.lang.ClassCastException: clojure.lang.LazySeq cannot be cast to java.lang.CharSequence

I tried to write a function to process a line of string by calling str\split, the function works fine if i call it directly in LEIN REPL window, but will hit the above call error while trying to run the program from LEIN RUN.
Any suggestion?
(let [num-letters (count (apply str line))
num-spaces-needed (- column-length num-letters)
num-words (count (clojure.string/split line #"\s"))
num-space-in-group (if (= 1 num-words) num-spaces-needed (/ num-spaces-needed (- num-words 1)))
group-of-spaces (repeat num-space-in-group " ")
padding (create-list-spaces num-spaces-needed (dec (count line)))]
( clojure.string/join "" (if (empty? padding) (cons line group-of-spaces)
(cons (first line) (interleave (rest line) padding)))))
I suppose you pass line as a parameter to your function, although it was omitted from your code snippet.
You should check for differences in the line parameter, when calling the function from these two different entry points. First, let's name your function as tokenize for convenience. Now, the vanilla app template in Leiningen creates a -main that looks similar to this, after I add the tokenize call:
(defn -main
[& args]
(tokenize args))
The arguments are destructured with the rest operator &, which builds a Seq of the arguments (args). So, when running this with lein run I want this to work!, you end up invoking the tokenize function with a sequence. clojure.string/split cannot be applied to a sequence, and you get a stack trace.
However, when you call your function from lein repl, a natural way to do it is with a spell like (tokenize "Iä! Iä! Cthulhu fhtang!"). This will work, as your call parameter is now just a string, not a sequence.
In the end, it comes down to how you call your function. A more confident answer would require details on that, as #sam-estep commented.

evaluate an arg when passing to a macro

I have a small macro which takes a series of cmds as clojure s expressions and returns their string representations. I am having trouble passing an argument to the macro -
(defmacro cmd [& cmds]
(->> (for [v cmds]
(join " " v))
(join "; ")))
(defn install-jive [version]
(cmd
(yum remove jive-add-ons)
(yum install jive-add-ons ~version)))
(install-jive 1.2)
I want the fn to return the following -
(install-jive 1.2) => "yum remove jive-add-ons; yum install jive-add-ons 1.2"
However currently it returns -
"yum remove jive-add-ons; yum install jive-add-ons
(clojure.core/unquote version)"
You need to think about what forms you need your macro to generate. For example in your case you might want the macro to generate something like this after expansion:
(defn install-jive [version]
(clojure.string/join
";"
[(clojure.string/join " " (list "yum" "remove" "jive-add-ons"))
(clojure.string/join " " (list "yum" "install" "jive-add-ons" version))]))
The macro transformed the first 3 elements on the list in a String and didn't touch version. Here is a macro that do that for one form:
(defmacro single-cmd [cmd-form]
`(clojure.string/join " " (list ~#(map str (take 3 cmd-form))
~#(drop 3 cmd-form))))
Expanding this macro generates this:
(macroexpand '(single-cmd (yum remove jive-add-ons version)))
=> (clojure.string/join " " (clojure.core/list "yum" "remove" "jive-add-ons" version))
(macroexpand '(single-cmd (yum remove)))
=> (clojure.string/join " " (clojure.core/list "yum" "remove"))
Which is what we needed for each command to expand into. Now we need to create a macro that expands this:
(cmd
(yum remove jive-add-ons)
(yum install jive-add-ons version))
Into the form that we want. This is a simple version, just to give you the idea:
(defmacro cmd [& cmds]
`(clojure.string/join
";"
(list (single-cmd ~(first cmds))
(single-cmd ~(second cmds)))))
Expanding it generates:
(macroexpand '(cmd
(yum remove jive-add-ons)
(yum install jive-add-ons version)))
=> (clojure.string/join
";"
(clojure.core/list (user/single-cmd (yum remove jive-add-ons))
(user/single-cmd (yum install jive-add-ons version))))
And now:
(install-jive 1.2)
=> "yum remove jive-add-ons;yum install jive-add-ons 1.2"
My version makes two big assumptions: the first three elements should always be converted to String and you always call cmd with two, and only two, commands. You will probably need to improve that to solve your problem.
But I think this gives you a nudge in the right direction. There is a great book Mastering Clojure Macros that can help you develop your macros skills.
(defmacro cmd [& cmds]
`(join "; "
[~#(for [cmd cmds]
`(join " "
[~#(for [arg cmd]
(if (and (seq? arg) (= (first arg) 'clojure.core/unquote))
(second arg)
(list 'quote arg)))]))]))
If you compare to the original code, you'll see that this one defers calls to join until runtime and turns unquote forms as expressions in the resulting command template.

How to write a nested macro that prints its own suffix?

I am trying to better understand listing 13.3 in The Joy of Clojure. It is a macro that generates other macros (much like how primitive array functions are implemented in Clojure 1.4).
I want to write a macro that, when run, simply prints the suffix of the generated macro. i.e.
user=> (nested-macro joe)
user=> (nested-macro-named-joe)
hello from joe
nil
I am having trouble making this work.
Here is what I've tried:
Attempt 1
(defmacro nested-macro [name]
`(defmacro ~(symbol (str "nested-macro-named-" name))
[]
`(println "hello from " ~name)))
Output:
hello from #<core$name clojure.core$name#502c06b2>
Attempt 2
(defmacro nested-macro [name]
(let [local-name name]
`(defmacro ~(symbol (str "my-custom-macro-named-" ~local-name))
[]
`(println "hello from " ~local-name))))
Error
IllegalStateException Attempting to call unbound fn: #'clojure.core/unquote clojure.lang.Var$Unbound.throwArity (Var.java:43)
Attempt 3:
(defmacro nested-macro [name]
(let [local-name name]
`(defmacro ~(symbol (str "nested-macro-named-" name))
[]
`(println "hello from " ~(symbol local-name)))))
Compiler Error:
CompilerException java.lang.RuntimeException: No such var: joy.dsl/local-name
Just for the heck of it, I've also tried adding # to the local variables, with similar results as above but with "auto" names, such as local-name__1127__auto__ I don't see that being as part of the solution, however.
How can I make this work?
To know what is going wrong with macros I always use macroexpand-1.
From your first example:
(macroexpand-1 '(nested-macro joe))
Results in:
(clojure.core/defmacro nested-macro-named-joe []
(clojure.core/seq
(clojure.core/concat
(clojure.core/list (quote clojure.core/println))
(clojure.core/list "hello from ")
(clojure.core/list clojure.core/name))))
If you look at the last param, it shows that you are using clojure.core/name, which probably is not what you want, as you actually want the parameter named "name".
To fix it, just add another unquote to the name param, but as the name param is actually a symbol, what you really want is to get the name of it as in:
(defmacro nested-macro [the-name]
`(defmacro ~(symbol (str "nested-macro-named-" the-name))
[]
`(println "hello from " ~~(name the-name))))

How do I mix non-optional cli arguments with optional ones?

Using tools.cli, how can I can I create a non-optional argument with optional ones?
I have a function
(defn parse-opts
[args]
(cli args
["-f" "--ifn" "input file"]
(optional ["-o" "--outp" ".csv pipe delimited output file"
:default "assess_pro_out.csv"] identity)
(optional ["-d" "--debug" "Debug flag for logging." :default 0
:parse-fn #(Integer. %)])))
that compiles but produces
Exception in thread "main" clojure.lang.ArityException:
Wrong number of args (2) passed to: PersistentVector
when I run my main program without arguments.
If this option is made like the rest
(optional ["-f" "--ifn" "input file"] identity)
everything works fine.
I just want one parameter to be non-optional. What am I doing wrong?
I do have a workaround for this, but I'd still like to know if it is okay to mix optional and non-optional arguments to cli.
(defn -main
[& args]
(let [opts (parse-opts args)
start-time (str (Date.))
parsed-csv-data (if-not (:ifn opts)
(do
(println "Usage: assess-chk [-f -ifn] input-file-name")
(System/exit -2))
(utl/fetch-csv-data (:ifn opts)))
Thanks.
You're using an old version of tools.cli (probably v0.1.0). For that version it appears that you should use (required ...) for required options. See the docs at https://github.com/clojure/tools.cli/tree/a741b23f230123179fc518af772f1c057058f7d2
In the current version of tools.cli, options are always optional and the optional and required functions are removed.