I'm an absolute Clojure beginner and I'm trying to build a CLI app using the clojure.tools.cli library.
My problem is that I can't show any error when an option is not provided with required parameter.
What I want:
$ java -jar test.jar -m SAMPLE
Given file: SAMPLE
$ java -jar test.jar -m
ERROR: Please provide a file
What happens:
$ java -jar test.jar -m SAMPLE
Given file: SAMPLE
$ java -jar test.jar -m
$
It doesn't show anything.
Here is my code:
(ns obmed-clj.core
(:require [clojure.tools.cli :refer [parse-opts]])
(:gen-class))
(def cli-options
[["-m" "--menu FILE" "Provide menu file path"
:parse-fn #(if (nil? %)
(println "ERROR: Please provide a file")
%)
:validate-fn #(println "Given file:" %)]])
(defn -main [& args]
(parse-opts args cli-options))
You are abusing the -fn arguments here a little. Their use is to convert the "string" (in your case, since you have "--menu FILE") and then do additional validation on that (but rather use :validate [fn msg] instead). So e.g.:
user=> (def cli-opts [["-m" "--menu FILE" "menu file"
:parse-fn #(java.io.File. %)
:validate [#(.exists %) "file must exist"]]])
#'user/cli-opts
Missing argument:
user=> (parse-opts ["-m"] cli-opts)
{:arguments [],
:errors ["Missing required argument for \"-m FILE\""],
:options {},
:summary " -m, --menu FILE menu file"}
File not existing:
user=> (parse-opts ["-m" "XXX"] cli-opts)
{:arguments [],
:errors ["Failed to validate \"-m XXX\": file must exist"],
:options {},
:summary " -m, --menu FILE menu file"}
All is well:
user=> (parse-opts ["-m" "/etc/hosts"] cli-opts)
{:arguments [],
:errors nil,
:options {:menu #<java.io.File#34d63c80 /etc/hosts>},
:summary " -m, --menu FILE menu file"}
Related
The https://github.com/yogthos/config approach let's you lay out per-profile env variables in separate files, like the below , in a project.clj .
Per the below, one can use lein with-profile prod uberjar or lein with-profile dev repl and the like.
But my issue is I have been unable to figure out how to place some common values into a shared area, accessible by dev, stage, prod profiles.
Basic example
(defproject edn-config-test "0.1.0-SNAPSHOT"
...
:profiles {:shared {:resource-paths ["config/shared"]}
:dev {:resource-paths ["config/dev"]}
:stage {:resource-paths ["config/stage"]}
:prod {:resource-paths ["config/prod"]}}
...
(with files)
config/shared/config.edn
config/dev/config.edn
config/stage/config.edn
config/prod/config.edn
I tried this without luck
lein with-profile shared,prod lein , borrowing from the composite approach in
https://github.com/technomancy/leiningen/blob/stable/doc/PROFILES.md#composite-profiles
When I do that, I only get variables in prod profile, for example.
I think it is a limitation of config. I tried this (more explicit):
:profiles {:dev {:resource-paths ["config/shared" "config/dev"]}
:prod {:resource-paths [ "config/prod" "config/shared"]}}
However, the last file wins and the first is ignored. So for :dev the shared stuff is ignored, and for :prod the prod stuff is ignored (like it doesn't exist):
config/dev/config.edn => {:special-val :dev-val}
config/prod/config.edn => {:special-val :prod-val}
cat config/shared/config.edn => {:shared-val 42}
and results:
> lein with-profile prod run
(:shared-val env) => 42
(:special-val env) => nil
> lein with-profile dev run
(:shared-val env) => nil
(:special-val env) => :dev-val
Perhaps you'd like to submit an enhancement PR to the project?
Here is the problem. It uses io/resource to read config.edn, which implicitly expects there to be only one file config.edn anywhere on the classpath:
(defn- read-config-file [f]
(try
(when-let [url (io/resource f)]
(with-open [r (-> url io/reader PushbackReader.)]
(edn/read r))) ...
(read-config-file "config.edn")
So you'd have to get away from the hard-coded filename config.edn, and make something like config-dev.edn, config-prod.edn, and config-shared.edn. At least then they could all live in a single ./resources dir.
I'm getting debconf: DbDriver \"passwords\" warning: could not open /var/cache/debconf/passwords.dat: Permission denied\ndebconf: DbDriver \"config\": could not write /var/cache/debconf/config.dat-new: Permission denied\n#> [packages]: Packages : FAIL\n" when I run this. Looks like it isn't sudoing for whatever reason.
(ns localhost.idk
(:require (pallet [compute :as compute]
[api :as api :refer [lift]]
[actions :as actions])))
;; Running this on my local machine
(def my-data-center
(compute/instantiate-provider
"node-list"
:node-list [["localhost" "ed" "127.0.0.1" :ubuntu
:is-64bit nil]]))
(def user-deadghost
(api/make-user "deadghost"
:password "my-pw"
:sudo-password nil)) ; pwless sudo set up
(defn install-ed []
(pallet.api/lift
(pallet.api/group-spec
"ed"
:phases {:configure (api/plan-fn
;; This works:
;; (pallet.actions/exec-script
;; ("sudo aptitude install ed"))
;; This is trying to run without sudo:
(actions/packages :aptitude ["ed"]))})
;; log shows: p.script-builder prefix kw :no-sudo
:compute my-data-center
:user user-deadghost))
Script it's running:
#!/usr/bin/env bash
mkdir -p /home/deadghost || exit 1
cd /home/deadghost
set -h
echo '[packages]: Packages...';
{
{ debconf-set-selections <<EOF
debconf debconf/frontend select noninteractive
debconf debconf/frontend seen false
EOF
} && enableStart() {
rm /usr/sbin/policy-rc.d
} && apt-get -q -y install ed+ && dpkg --get-selections
} || { echo '#> [packages]: Packages : FAIL'; exit 1;} >&2
echo '#> [packages]: Packages : SUCCESS'
exit $?
The error I'm receiving is consistent with the debconf portion not having sudo.
Answer provided by hugod, author of pallet.
As of pallet 0.8.0-RC.11 when running on localhost the default script prefix is :no-sudo when the default would otherwise be :sudo. This is for historical reasons I don't know the details about.
To change the script prefix back to sudo, wrap your action with (pallet.action/with-action-options {:script-prefix :sudo} YOUR-ACTION-HERE). So in my case it will look like this:
(defn install-ed []
(pallet.api/lift
(pallet.api/group-spec
"ed"
:phases {:configure (api/plan-fn
(pallet.action/with-action-options
{:script-prefix :sudo}
(actions/packages :aptitude ["ed"])))})
:compute my-data-center
:user user-deadghost))
Is there a simple text encyptor for Clojure which just needs one password to decrypt? I just want something like to encrypt:
(encrypt "Some secret message" "Some secret key")
:and to decrypt:
(decrypt (encrypt "Some secret message" "Some secret key") "Some secret key")
:will return:
"Some secret message"
I decided this functionality would be helpful for a project I am working on.
Aside from the standard clojure, it also requires commons-codec.
project.clj:
(defproject <project> "VERSION"
...
:dependencies [[org.clojure/clojure "1.5.1"]
[commons-codec "1.8"]])
<project>/src/crypt.clj:
(ns <whatever>.crypt
(:import (javax.crypto KeyGenerator SecretKey Cipher SecretKeyFactory)
(javax.crypto.spec SecretKeySpec PBEKeySpec)
(org.apache.commons.codec.binary Base64)))
(def ^:dynamic *salt* "BIND SALT IN APP")
(defn cipher- [] (. Cipher getInstance "AES"))
(defn aes-keyspec [rawkey] (new SecretKeySpec rawkey "AES"))
(defn encrypt-
[rawkey plaintext]
(let [cipher (cipher-)
mode (. Cipher ENCRYPT_MODE)]
(. cipher init mode (aes-keyspec rawkey))
(. cipher doFinal (. plaintext getBytes))))
(defn decrypt-
[rawkey ciphertext]
(let [cipher (cipher-)
mode (. Cipher DECRYPT_MODE)]
(. cipher init mode (aes-keyspec rawkey))
(new String(. cipher doFinal ciphertext))))
(defn passkey
[password & [iterations size]]
(let [keymaker (SecretKeyFactory/getInstance "PBKDF2WithHmacSHA1")
pass (.toCharArray password)
salt (.getBytes *salt*)
iterations (or iterations 1000)
size (or size 128)
keyspec (PBEKeySpec. pass salt iterations size)]
(-> keymaker (.generateSecret keyspec) .getEncoded)))
(defn encrypt
[password plaintext]
(encrypt- (passkey password) plaintext))
(defn decrypt
[password cyphertext]
(decrypt- (passkey password) cyphertext))
usage:
(binding [crypt/*salt* "THE SALT WE ARE USING"]
(crypt/encrypt "password" "message")
(crypt/decrypt "password" *1)))
if you can use generated keys, that is going to be more secure:
(defn aes-keygen [] (. KeyGenerator getInstance "AES"))
(defn genkey
[keygen]
(. keygen init 128)
(. (. keygen generateKey ) getEncoded))
(def generated (keygen aes-keygen))
(encrypt- generated plaintext)
(decrypt- generated *1)
This is only using vanilla security features provided with the jvm (commons-codec is just used for base-64 encoding/decoding so that we can operate on arbitrary input, it is not part of the security setup).
Try simple-crypto:
[org.clojars.tnoda/simple-crypto "0.1.0"]
It do exactly what you want:
user=> (use 'org.clojars.tnoda.simple-crypto)
user=> (decrypt (encrypt "Some secret message" "Some secret key!") "Some secret key!")
"Some secret message"
I'm not aware of any Clojure library that is doing that mostly -I would say- because it is too easy to do using javax.crypto, javax.crypto.spec and java.security packages.
Could be done within 30 lines.
Newbie Clojure and leiningen question:
Given the code snippet in my project below, this works from the lein repl :
==> (-main "something")
produces the expected "Command: something ... running ... done"
but doesn't work from the command line:
me pallet1]lein run "something"
produces "Command: something ... error: not resolved as a command"
Why? / how do I fix it?
To reproduce:
lein new eg
Then edit the generated project file, adding :main eg.core to define the main function, and edit the generated src/eg/core.clj file, and paste this in:
core.clj
(ns eg.core)
(defn something [] (println "Something!"))
(defn run-command-if-any [^String commandname]
(printf "Command: %s ..." commandname)
(if-let [cmd (ns-resolve *ns* (symbol commandname))]
(
(println "running ...") (cmd) (println "done.")
)
(println "error: not resolved as a command.")
))
(defn -main [ commandname ] (run-command-if-any commandname))
Then
lein repl
eg.core=> (-main "something")
works (ie prints "Something!) , but
lein run something
doesn't (ie prints the "error: not resolved" message)
The problem is that when you run it from lein your default namespace is "user" namespace:
(defn -main [ commandname ] (println *ns*))
Prints #<Namespace user>. So it doesn't contain something function because it is from another namespace. You have several choices:
Pass fully qualified function name: your-namespace/something instead of something.
Use your-namespace instead of *ns*: (ns-resolve 'your-namespace (symbol commandname))
Change namespace to your-namespace in -main.
Example of method 3:
(defn -main [ commandname ]
(in-ns 'your-namespace)
(run-command-if-any commandname))
Also you if you want to call several functions one by one you should use do:
(do (println "Hello")
(println "World"))
Not just braces like ( (println "hello") (println "World"))
the lein exec plugin is very useful for scripting such things in the context of a project. I have used this extensively for writing Jenkins jobs in clojure and other scripting situations
lein exec -pe '(something ...) (something-else) (save-results)'
I am moving from [org.clojure/tools.cli "0.1.0"] to 0.2.2, but am getting
Exception in thread "main" clojure.lang.ArityException:
Wrong number of args (2) passed to: PersistentVector
at the line beginning with (cli args
(defn parse-opts
"Using the newer cli library, parses command line args."
[args]
(cli args
["--ifn1" ".csv input file" :default "benetrak_roster.csv"]
["--ifn2" ".csv input file" :default "billing_roster.csv"]
["--rpt" ".csv pipe delimited output file" :default "bene_gic_rpt.csv"]
["--dump1" "text file report for debug output" :default "dumpfile1.txt"]
["--dump2" "text file report for debug output" :default "dumpfile2.txt"]
["--debug" "Debug flag for logging." :default 0 :parse-fn #(Integer. %)]))
tools.cli is included like this (:use clojure.tools.cli).
I can't see what I'm doing wrong, and would appreciate any pointers or help. Thanks.
By the way, I've tried the following from looking at examples, and it does not work:
(defn -main
[& args]
(let [[opts args banner]
(cli args
["--ifn1" ".csv input file" :default "benetrak_roster.csv"]
["--ifn2" ".csv input file" :default "billing_roster.csv"]
["--rpt" ".csv pipe delimited output file" :default "bene_gic_rpt.csv"]
["--dump1" "text file report for debug output" :default "dumpfile1.txt"]
["--dump2" "text file report for debug output" :default "dumpfile2.txt"]
["--debug" "Debug flag for logging." :default 0 :parse-fn #(Integer. %)])
start-time (str (Date.))]
.
.
.
This seems to work:
(ns test.core
(:use clojure.tools.cli))
(defn parse-opts
"Using the newer cli library, parses command line args."
[args]
(cli args
["--ifn1" ".csv input file" :default "benetrak_roster.csv"]
["--ifn2" ".csv input file" :default "billing_roster.csv"]
["--rpt" ".csv pipe delimited output file" :default "bene_gic_rpt.csv"]
["--dump1" "text file report for debug output" :default "dumpfile1.txt"]
["--dump2" "text file report for debug output" :default "dumpfile2.txt"]
["--debug" "Debug flag for logging." :default 0 :parse-fn #(Integer. %)]))
test.core> (parse-opts [])
[{:debug 0, :dump2 "dumpfile2.txt", :dump1 "dumpfile1.txt", :rpt "bene_gic_rpt.csv", :ifn2 "billing_roster.csv", :ifn1 "benetrak_roster.csv"} [] "Usage:\n\n Switches Default Desc \n -------- ------- ---- \n --ifn1 benetrak_roster.csv .csv input file \n --ifn2 billing_roster.csv .csv input file \n --rpt bene_gic_rpt.csv .csv pipe delimited output file \n --dump1 dumpfile1.txt text file report for debug output \n --dump2 dumpfile2.txt text file report for debug output \n --debug 0 Debug flag for logging. \n"]
test.core>
Are you sure the error isn't in whatever you're passing to parse-opts?
Also: are you sure you've got the right version (and ONLY the right version) of tools.cli in your project.clj?