Skip to content

Instantly share code, notes, and snippets.

@rplevy
Last active May 2, 2019 00:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rplevy/90ecd756a9e9f588d0672dd99cd01337 to your computer and use it in GitHub Desktop.
Save rplevy/90ecd756a9e9f588d0672dd99cd01337 to your computer and use it in GitHub Desktop.
cmd-line-scripting-with-plk-and-clj.org

command-line scripting with plk & clj

what to write scripts in?

bash?

perl?

python?
why not clojure?

why not?

bash/perl etc pre-installed

unorthodox installation requirements sometimes not worth it

this is actually a pretty good reason not to
but often it does make sense, pretty often actually

for example it’s great for clj/s dev tooling

jvm+clojure start time

planck cljs (by mfikes) starts fast

it runs on *nix now, not just mac os
planck is great

hassle to invoke the code

standalone shebang scripting in clojure is a recent development

previously no automatic dep management for standalone scripts too
now we have tools.cli, clj, and planck has plk
automatic dependency management in a standalone script

why?

expressive/maintainable

great for development tool yak shaving

stay in your favorite language (more productive)

how to

#!/usr/bin/env bash
"exec" "plk" \
       "-Sdeps" "{:deps {funcool/tubax {:mvn/version \"0.2.0\"}}}" \
       "-Ksf" "$0" "$@"

(ns scripts.html2clj
  (:require [clojure.string :as string]
            [clojure.walk :as walk]
            [cljs.pprint :refer [pprint]]
            [planck.core :refer [slurp *command-line-args*
                                 *in* line-seq]]
            [tubax.core :as tubax]))
...

tips

easily replace plk with clj

#!/usr/bin/env bash
"exec" "clj" \
       "-Sdeps" "{:deps {zprint {:mvn/version \"0.4.9\"}}}" \
       "$0" "$@"

(ns scripts.zprint
  (:require [zprint.core :as zp]))

(zp/zprint (slurp *in*) 80 {:parse-string-all? true
                            :parse {:interpose "\n\n"}
                            :map {:comma? false}})

(System/exit 0)

planck.core provides vars in clojurescript that clj users know and love

  • slurp
  • in
  • command-line-args

    etc.

more info:

http://planck-repl.org/scripts.html

plk install depends on clj being installed

more info

reduce reliance on emacs-lisp

same goes for non-emacs editors as well

you people have something like elisp in your editors too I think, right?
anyway, most editors allow you to shell out like this
(defmacro defshellonreg (name cmd &optional replace)
  `(defun ,name (beg end)
     (interactive "r")
     (shell-command-on-region beg end ,cmd
                              (if ,replace (current-buffer) nil)
                              ,replace)))
example usage
(defshellonreg htm2clj-region "html2clj" t)
another example
(defshellonreg zprint-region "zprint" t)
another one
(defun sort-sexp ()
  (interactive)
  (mark-sexp)
  (sort-sexp-region (region-beginning) (region-end))
  (zprint-region (region-beginning) (region-end))
  (indent-region (region-beginning) (region-end)))

demo

html2clj

<div>
  <h1>hello world</h1>
  <br/>
  <img src="smile.gif"/>
</div>

sort-sexp

(ns clojure.test-clojure.spec
  (:require [clojure.spec-alpha2 :as s]
            [clojure.spec-alpha2.protocols :as prot]
            [clojure.spec-alpha2.gen :as gen]
            [clojure.spec-alpha2.test :as stest]
            [clojure.test :refer :all]))
zprint-sexp
(deftest conform-explain
  (let [a (s/and #(> % 5) #(< % 10))
        o (s/or :s string? :k keyword?)
        c (s/cat :a string? :b keyword?)
        either (s/alt :a string? :b keyword?)
        star (s/* keyword?)
        plus (s/+ keyword?)
        opt (s/? keyword?)
        andre (s/& (s/* keyword?) even-count?)
        andre2 (s/& (s/* keyword?) #{[:a]})
        m (s/map-of keyword? string?)
        mkeys (s/map-of (s/and keyword? (s/conformer name)) any?)
        mkeys2 (s/map-of (s/and keyword? (s/conformer name)) any? :conform-keys true)
        s (s/coll-of (s/cat :tag keyword? :val any?) :kind list?)
        v (s/coll-of keyword? :kind vector?)
        coll (s/coll-of keyword?)
        lrange (s/int-in 7 42)
        drange (s/double-in :infinite? false :NaN? false :min 3.1 :max 3.2)
        irange (s/inst-in #inst "1939" #inst "1946")
        schema1 (s/schema [::k1 ::k2])
        schema2 (s/schema {:a int? :b keyword?})
        union (s/union [::k1 ::k2] {:a int? :b keyword?})
        select1 (s/select [] [::k1 ::k2])
        select2 (s/select [] [::k1 {::sch [::mk1]}])
        select* (s/select [::k1 ::k2] [*])
        ]
    (are [spec x conformed ed]
      (let [co (result-or-ex (s/conform spec x))
            e (result-or-ex (::sa/problems (s/explain-data spec x)))]
        (when (not= conformed co) (println "conform fail\n\texpect=" conformed "\n\tactual=" co))
        (when (not (every? true? (map submap? ed e)))
          (println "explain failures\n\texpect=" ed "\n\tactual failures=" e "\n\tsubmap?=" (map submap? ed e)))
        (and (= conformed co) (every? true? (map submap? ed e))))

      lrange 7 7 nil
      lrange 8 8 nil
      lrange 42 ::s/invalid [{:pred '(fn [%] (clojure.spec-alpha2/int-in-range? 7 42 %)), :val 42}]

      irange #inst "1938" ::s/invalid [{:pred '(fn [%] (clojure.spec-alpha2/inst-in-range? #inst "1939-01-01T00:00:00.000-00:00" #inst "1946-01-01T00:00:00.000-00:00" %)), :val #inst "1938"}]
      irange #inst "1942" #inst "1942" nil
      irange #inst "1946" ::s/invalid [{:pred '(fn [%] (clojure.spec-alpha2/inst-in-range? #inst "1939-01-01T00:00:00.000-00:00" #inst "1946-01-01T00:00:00.000-00:00" %)), :val #inst "1946"}]

      drange 3.0 ::s/invalid [{:pred '(fn [%] (if 3.1 (clojure.core/<= 3.1 %) true)), :val 3.0}]
      drange 3.1 3.1 nil
      drange 3.2 3.2 nil
      drange Double/POSITIVE_INFINITY ::s/invalid [{:pred '(fn [%] (clojure.core/if-not false (clojure.core/not (Double/isInfinite %)))), :val Double/POSITIVE_INFINITY}]
      ;; can't use equality-based test for Double/NaN
      ;; drange Double/NaN ::s/invalid {[] {:pred '(fn [%] (clojure.core/not (Double/isNaN %))), :val Double/NaN}}

      (s/spec keyword?) :k :k nil
      (s/spec keyword?) nil ::s/invalid [{:pred `keyword? :val nil}]
      (s/spec keyword?) "abc" ::s/invalid [{:pred `keyword? :val "abc"}]

      a 6 6 nil
      a 3 ::s/invalid '[{:pred (fn [%] (clojure.core/> % 5)), :val 3}]
      a 20 ::s/invalid '[{:pred (fn [%] (clojure.core/< % 10)), :val 20}]
      a nil "java.lang.NullPointerException" "java.lang.NullPointerException"
      a :k "java.lang.ClassCastException" "java.lang.ClassCastException"

      o "a" [:s "a"] nil
      o :a [:k :a] nil
      o 'a ::s/invalid '[{:pred clojure.core/string?, :val a, :path [:s]} {:pred clojure.core/keyword?, :val a :path [:k]}]

      c nil ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/string?, :val (), :path [:a]}]
      c [] ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/string?, :val (), :path [:a]}]
      c [:a] ::s/invalid '[{:pred clojure.core/string?, :val :a, :path [:a], :in [0]}]
      c ["a"] ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/keyword?, :val (), :path [:b]}]
      c ["s" :k] '{:a "s" :b :k} nil
      c ["s" :k 5] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec-alpha2/cat :a clojure.core/string? :b clojure.core/keyword?), :val (5)}]
      (s/cat) nil {} nil
      (s/cat) [5] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec-alpha2/cat), :val (5), :in [0]}]

      either nil ::s/invalid '[{:reason "Insufficient input", :pred (clojure.spec-alpha2/alt :a clojure.core/string? :b clojure.core/keyword?), :val () :via []}]
      either [] ::s/invalid '[{:reason "Insufficient input", :pred (clojure.spec-alpha2/alt :a clojure.core/string? :b clojure.core/keyword?), :val () :via []}]
      either [:k] [:b :k] nil
      either ["s"] [:a "s"] nil
      either [:b "s"] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec-alpha2/alt :a clojure.core/string? :b clojure.core/keyword?), :val ("s") :via []}]

      star nil [] nil
      star [] [] nil
      star [:k] [:k] nil
      star [:k1 :k2] [:k1 :k2] nil
      star [:k1 :k2 "x"] ::s/invalid '[{:pred clojure.core/keyword?, :val "x" :via []}]
      star ["a"] ::s/invalid '[{:pred clojure.core/keyword?, :val "a" :via []}]

      plus nil ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/keyword?, :val () :via []}]
      plus [] ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/keyword?, :val () :via []}]
      plus [:k] [:k] nil
      plus [:k1 :k2] [:k1 :k2] nil
      plus [:k1 :k2 "x"] ::s/invalid '[{:pred clojure.core/keyword?, :val "x", :in [2]}]
      plus ["a"] ::s/invalid '[{:pred clojure.core/keyword?, :val "a" :via []}]

      opt nil nil nil
      opt [] nil nil
      opt :k ::s/invalid '[{:pred (fn [%] (clojure.core/or (clojure.core/nil? %) (clojure.core/sequential? %))), :val :k}]
      opt [:k] :k nil
      opt [:k1 :k2] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec-alpha2/? clojure.core/keyword?), :val (:k2)}]
      opt [:k1 :k2 "x"] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec-alpha2/? clojure.core/keyword?), :val (:k2 "x")}]
      opt ["a"] ::s/invalid '[{:pred clojure.core/keyword?, :val "a"}]

      andre nil nil nil
      andre [] nil nil
      andre :k ::s/invalid '[{:pred (fn [%] (clojure.core/or (clojure.core/nil? %) (clojure.core/sequential? %))), :val :k}]
      andre [:k] ::s/invalid '[{:pred clojure.test-clojure.spec/even-count?, :val [:k]}]
      andre [:j :k] [:j :k] nil

      andre2 nil ::s/invalid [{:pred #{[:a]}, :val []}]
      andre2 [] ::s/invalid [{:pred #{[:a]}, :val []}]
      andre2 [:a] [:a] nil

      m nil ::s/invalid '[{:pred clojure.core/map?, :val nil}]
      m {} {} nil
      m {:a "b"} {:a "b"} nil

      mkeys nil ::s/invalid '[{:pred clojure.core/map?, :val nil}]
      mkeys {} {} nil
      mkeys {:a 1 :b 2} {:a 1 :b 2} nil

      mkeys2 nil ::s/invalid '[{:pred clojure.core/map?, :val nil}]
      mkeys2 {} {} nil
      mkeys2 {:a 1 :b 2} {"a" 1 "b" 2} nil

      s '([:a 1] [:b "2"]) '({:tag :a :val 1} {:tag :b :val "2"}) nil

      v [:a :b] [:a :b] nil
      v '(:a :b) ::s/invalid '[{:pred clojure.core/vector? :val (:a :b)}]

      coll nil ::s/invalid '[{:path [], :pred clojure.core/coll?, :val nil, :via [], :in []}]
      coll [] [] nil
      coll [:a] [:a] nil
      coll [:a :b] [:a :b] nil
      coll (map identity [:a :b]) '(:a :b) nil
      ;;coll [:a "b"] ::s/invalid '[{:pred (coll-checker keyword?), :val [:a b]}]

      schema1 {} {} nil
      schema1 {::k1 1 ::k2 :a} {::k1 1 ::k2 :a} nil
      schema1 "oops" ::s/invalid [{:pred 'clojure.core/map? :val "oops"}]
      schema1 {::k1 :a ::k2 1} ::s/invalid [{:pred 'clojure.core/int? :val :a} {:pred 'clojure.core/keyword? :val 1}]

      schema2 {} {} nil
      schema2 {:a 10 :b :x} {:a 10 :b :x} nil
      schema2 {:a :x :b 10} ::s/invalid [{:pred 'clojure.core/int? :val :x} {:pred 'clojure.core/keyword? :val 10}]

      union {} {} nil
      union {::k1 1 ::k2 :a :a 1 :b :x} {::k1 1 ::k2 :a :a 1 :b :x} nil
      union "oops" ::s/invalid [{:pred 'clojure.core/map? :val "oops"}]
      union {::k1 :a ::k2 1} ::s/invalid [{:pred 'clojure.core/int? :val :a} {:pred 'clojure.core/keyword? :val 1}]

      select1 {::k1 1 ::k2 :a} {::k1 1 ::k2 :a} nil
      select1 "oops" ::s/invalid [{:pred 'clojure.core/map? :val "oops"}]
      select1 {::k1 1} ::s/invalid [{:pred '(clojure.core/fn [m] (clojure.core/contains? m ::k2)) :val {::k1 1}}]
      select1 {::k1 1 ::k2 5} ::s/invalid [{:pred 'clojure.core/keyword? :val 5}]

      select2 {::k1 1} {::k1 1} nil
      select2 {::k1 1 ::m {::mk1 10}} {::k1 1 ::m {::mk1 10}} nil
      ;; problems here from both the registered key in the outer map and from missing selection
      select2 {::k1 1 ::m {}} ::s/invalid [{:pred '(clojure.core/fn [%] (clojure.core/contains? % ::mk1)) :val {}}
                                           {:pred '(clojure.core/fn [m] (clojure.core/contains? m ::mk1)) :val {}}]

      select* {::k1 1 ::k2 :a} {::k1 1 ::k2 :a} nil
      select* "oops" ::s/invalid [{:pred 'clojure.core/map? :val "oops"}]
      select* {::k1 1} ::s/invalid [{:pred '(clojure.core/fn [m] (clojure.core/contains? m ::k2)) :val {::k1 1}}]
      select* {::k1 1 ::k2 5} ::s/invalid [{:pred 'clojure.core/keyword? :val 5}]

      )))

thank you

code examples are from https://github.com/rplevy/dotfiles

I am @rplevy on github and twitter and other places
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment