├── .gitignore ├── test ├── resources │ ├── bb-table-file-input.edn │ └── bin │ │ ├── bb_replace_test │ │ ├── standard-replacement.edn │ │ ├── .bb-replace.edn │ │ └── help-option.edn │ │ └── bb_table_test │ │ ├── simple-vec-errors.edn │ │ ├── vec-stdin-argument.edn │ │ ├── file-option.edn │ │ ├── map-stdin-argument.edn │ │ ├── reverse-sort-option.edn │ │ ├── sort-option.edn │ │ ├── print-headers-option.edn │ │ ├── columns-option-with-abbreviations.edn │ │ ├── columns-option-with-header-numbers.edn │ │ ├── number-rows-option.edn │ │ └── help-option.edn └── bin │ ├── bb_replace_test.clj │ └── bb_table_test.clj ├── doc ├── images │ ├── bar.png │ └── 2d-histogram-heatmap.png └── scripts.md ├── preloads.clj ├── deps.edn ├── src └── cldwalker │ └── bb_clis │ ├── tasks │ ├── specter.clj │ ├── rewrite_edn.clj │ ├── rewrite_clj.clj │ ├── github.clj │ ├── git.clj │ ├── rdf.clj │ ├── rdf_data.clj │ ├── util.clj │ ├── clj_kondo.clj │ └── logseq.clj │ ├── repl.clj │ ├── cli │ ├── logseq.clj │ └── misc.clj │ ├── util │ └── maven_artifact.clj │ ├── cli.clj │ └── tasks.clj ├── .bb-replace.edn ├── .github └── workflows │ ├── unused-vars-to-ignore.edn │ └── test.yml ├── .clj-kondo └── config.edn ├── dev-bb.edn ├── .bb-logseq-convert.edn ├── LICENSE.md ├── bin ├── bb-var-usages ├── bb-logseq-move-to-page ├── bb-missed-docstrings ├── bb-project-clj ├── bb-unused-vars ├── bb-github-pr-for-commit ├── bl ├── bb-github-repo ├── bb-try ├── bb-replace ├── bb-update-lein-dependency ├── bb-ns-dep-tree ├── bb-vis ├── bb-cli-test ├── bb-aws ├── bb-table └── bb-logseq-convert ├── README.md └── bb.edn /.gitignore: -------------------------------------------------------------------------------- 1 | /.cpcache 2 | /.clj-kondo/.cache 3 | /later 4 | -------------------------------------------------------------------------------- /test/resources/bb-table-file-input.edn: -------------------------------------------------------------------------------- 1 | {:a 1 :b 2} 2 | -------------------------------------------------------------------------------- /doc/images/bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cldwalker/bb-clis/HEAD/doc/images/bar.png -------------------------------------------------------------------------------- /test/resources/bin/bb_replace_test/standard-replacement.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, :out "", :err ""} 2 | -------------------------------------------------------------------------------- /preloads.clj: -------------------------------------------------------------------------------- 1 | ;; This file is loaded by $BABASHKA_PRELOADS 2 | (require '[clojure.pprint :refer [pprint]]) 3 | -------------------------------------------------------------------------------- /doc/images/2d-histogram-heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cldwalker/bb-clis/HEAD/doc/images/2d-histogram-heatmap.png -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/simple-vec-errors.edn: -------------------------------------------------------------------------------- 1 | {:exit 1, 2 | :out "Error: Input is not an EDN collection of maps\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:aliases 2 | {:clj-kondo 3 | {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.09.08"}} 4 | :main-opts ["-m" "clj-kondo.main"]}}} 5 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/vec-stdin-argument.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "+----+----+\n| 0 | 1 |\n+----+----+\n| :a | :b |\n| :d | :e |\n+----+----+\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/file-option.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "+-----+-------+\n| key | value |\n+-----+-------+\n| :a | 1 |\n| :b | 2 |\n+-----+-------+\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/map-stdin-argument.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "+-----+-------+\n| key | value |\n+-----+-------+\n| :a | 1 |\n| :b | 2 |\n+-----+-------+\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/reverse-sort-option.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "+-----+-------+\n| key | value |\n+-----+-------+\n| :b | 2 |\n| :a | 1 |\n+-----+-------+\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/sort-option.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "+-----+-------+\n| key | value |\n+-----+-------+\n| :a | 1 |\n| :b | 2 |\n| :c | 3 |\n+-----+-------+\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/print-headers-option.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "+--------+-------+\n| column | count |\n+--------+-------+\n| :a | 2 |\n| :b | 1 |\n+--------+-------+\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/columns-option-with-abbreviations.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "+-------+--------+\n| apple | orange |\n+-------+--------+\n| 1 | |\n| 2 | 1 |\n+-------+--------+\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/columns-option-with-header-numbers.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "+-------+--------+\n| apple | banana |\n+-------+--------+\n| 1 | 1 |\n| 2 | |\n+-------+--------+\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/specter.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.specter 2 | "Tasks using specter" 3 | (:require [com.rpl.specter :as s])) 4 | 5 | (defn example 6 | [] 7 | (s/transform [(s/walker number?) odd?] inc {:a 1 :b [1 2 3]})) 8 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/number-rows-option.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "+--------+-----+-------+\n| number | key | value |\n+--------+-----+-------+\n| 1 | :a | 1 |\n| 2 | :b | 2 |\n+--------+-----+-------+\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /.bb-replace.edn: -------------------------------------------------------------------------------- 1 | { 2 | :lein-version {:regex "(def.*?\\s+)\"[\\d.]+\"" 3 | :format-string "$1\"%s\"" 4 | :file "project.clj"} 5 | :json-version {:regex "(\"version\":\\s+)\"[\\d\\.]+\"" 6 | :format-string "$1\"%s\"" 7 | :file "package.json"} 8 | } 9 | -------------------------------------------------------------------------------- /test/resources/bin/bb_replace_test/.bb-replace.edn: -------------------------------------------------------------------------------- 1 | { 2 | :lein-version {:regex "(def.*?\\s+)\"[\\d.]+\"" 3 | :format-string "$1\"%s\"" 4 | :file "project.clj"} 5 | :json-version {:regex "(\"version\":\\s+)\"[\\d\\.]+\"" 6 | :format-string "$1\"%s\"" 7 | :file "package.json"} 8 | } 9 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/rewrite_edn.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.rewrite-edn 2 | (:require [borkdude.rewrite-edn :as r])) 3 | 4 | (defn update-gitlib 5 | [args] 6 | (let [[gitlib sha] args 7 | nodes (-> "deps.edn" slurp r/parse-string)] 8 | (spit "deps.edn" 9 | (str (r/assoc-in nodes [:deps (symbol gitlib) :sha] sha))))) 10 | -------------------------------------------------------------------------------- /test/resources/bin/bb_replace_test/help-option.edn: -------------------------------------------------------------------------------- 1 | {:exit 0, 2 | :out "Usage: bb-replace [OPTIONS] REPLACEMENT/REGEX [& ARGUMENTS]\nReplacements available: lein-version, json-version\nOptions:\n -h, --help\n -f, --file FILE Overrides default file for a replacement\n -F, --format-string FORMAT Overrides default format string for a replacement\n", 3 | :err ""} 4 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/repl.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.repl 2 | "Fns that are useful in a bb repl" 3 | (:require [babashka.deps :as deps])) 4 | 5 | (defn add-portal 6 | [] 7 | (deps/add-deps '{:deps {djblue/portal {:mvn/version "RELEASE"}}}) 8 | (require '[portal.api :as p]) 9 | (let [p ((requiring-resolve 'portal.api/open))] 10 | (add-tap (requiring-resolve 'portal.api/submit)) 11 | p)) 12 | -------------------------------------------------------------------------------- /.github/workflows/unused-vars-to-ignore.edn: -------------------------------------------------------------------------------- 1 | [:cldwalker.bb-clis.util.maven-artifact/artifact-files ; repl fn 2 | :cldwalker.bb-clis.repl/add-portal ; repl fn 3 | :bb-aws/result ; inline def 4 | ;; wip fn that may be used soon 5 | :cldwalker.bb-clis.cli.misc/stdin-active? 6 | ;; bb.edn fns 7 | :cldwalker.bb-clis.tasks.util/parse-options 8 | :cldwalker.bb-clis.tasks/repl 9 | :cldwalker.bb-clis.tasks/result] 10 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/rewrite_clj.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.rewrite-clj 2 | (:require [rewrite-clj.zip :as z])) 3 | 4 | (defn- find-symbol-first-right-sexpr 5 | [zloc sym] 6 | ;; Returns first symbol found 7 | (-> (z/find-value zloc z/next sym) 8 | z/right 9 | z/sexpr)) 10 | 11 | (defn var-sexp 12 | [[string-var file]] 13 | (let [zloc (z/of-string (slurp file))] 14 | (find-symbol-first-right-sexpr zloc (symbol string-var)))) 15 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/cli/logseq.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.cli.logseq 2 | "Cli fns for logseq" 3 | (:require [clojure.string :as str])) 4 | 5 | (defn block->properties [block] 6 | (->> block 7 | str/split-lines 8 | (map #(str/split % #"::\s*")) 9 | ;; TODO: warn when removing lines 10 | (remove #(< (count %) 2)) 11 | (into {}))) 12 | 13 | (defn properties->block [properties] 14 | (->> properties 15 | (map (fn [[k v]] (str (name k) ":: " v))) 16 | (str/join "\n"))) 17 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters 2 | ;; TODO: Re-enable when bin mismatches are fixed or removed 3 | {:namespace-name-mismatch {:level :off} 4 | :consistent-alias 5 | {:aliases 6 | {clojure.java.shell shell 7 | clojure.string str 8 | cheshire.core json 9 | clojure.edn edn 10 | clojure.java.io io 11 | cldwalker.bb-clis.cli cli 12 | clojure.pprint pprint 13 | babashka.pods pods}} 14 | :unresolved-symbol 15 | ;; linter doesn't know bb dynamic vars 16 | {:exclude [*input*]} 17 | ;; common bb namespaces that don't need to be required 18 | :unresolved-namespace {:exclude [shell str set]}}} 19 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/github.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.github 2 | (:require [babashka.process :refer [shell]])) 3 | 4 | (defn checkout-pr-branch 5 | "Check out a contributor's branch with write permissions. 6 | Copy this from a github PR by looking for the branch after `Add more commits by pushing`" 7 | [pr-branch] 8 | (let [[_ user repo branch :as matches] 9 | (re-find #"https://github.com/(\S+)/(\S+)/tree/(.*)$" pr-branch)] 10 | (assert matches "Branch url with correct format required") 11 | (shell "git remote add" user (format "git@github.com:%s/%s" user repo)) 12 | (shell "git fetch" user) 13 | (shell "git checkout -b" (str user "/" branch) "-t" (str user "/" branch)))) 14 | -------------------------------------------------------------------------------- /test/resources/bin/bb_table_test/help-option.edn: -------------------------------------------------------------------------------- 1 | {:out "Usage: bb-table [OPTIONS] 2 | Reads from stdin if no arguments given 3 | Options: 4 | -h, --help 5 | -f, --file FILE 6 | -c, --columns-filter FILTER Select columns by comma delimited substring matches or numbers from -H 7 | -H, --print-headers Print column headers and their counts across rows 8 | -n, --number-rows 9 | -s, --sort COLUMN Sort by column 10 | -r, --reverse-sort COLUMN Reverse sort by column 11 | -S, --style STYLE :plain Table style when using table clojar. Available styles are :plain, :org, :unicode and :github-markdown. Default is :plain. 12 | " 13 | :exit 0 14 | :err ""} 15 | -------------------------------------------------------------------------------- /dev-bb.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {logseq/bb-tasks 3 | #_{:local/root "../../work/bb-tasks"} 4 | {:git/url "https://github.com/logseq/bb-tasks" 5 | :git/sha "95e4fbdb7bbf1c720c6f8b58e3b3b96b3b487526"}} 6 | 7 | :pods 8 | {clj-kondo/clj-kondo {:version "2022.02.09"}} 9 | 10 | :tasks 11 | {lint:carve 12 | logseq.bb-tasks.lint.carve/-main 13 | 14 | lint:large-vars 15 | logseq.bb-tasks.lint.large-vars/-main 16 | 17 | lint:ns-docstrings 18 | logseq.bb-tasks.lint.ns-docstrings/-main 19 | 20 | lint:minimize-public-vars 21 | logseq.bb-tasks.lint.minimize-public-vars/-main} 22 | 23 | :tasks/config 24 | {:large-vars 25 | {:max-lines-count 25} 26 | 27 | :ns-docstrings 28 | {:ignore-regex "cldwalker.bb-clis.tasks.*"}}} 29 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/git.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.git 2 | (:require [clojure.tools.gitlibs :as gl])) 3 | 4 | (def clone-cli-options 5 | [["-b" "--branch BRANCH" :default "master"] 6 | ["-n" "--name NAME"]]) 7 | 8 | (defn clone 9 | [parsed-args] 10 | (let [{:keys [options arguments]} parsed-args 11 | url (first arguments) 12 | gitlib-name (or (some-> (:name options) symbol) 13 | ;; Try parsing a sensible name from a github.com/repo/name like url 14 | (let [[_ namespace name] (re-find (re-pattern "([^/]+)/([^/]+)$") url)] 15 | (when (nil? namespace) 16 | (throw (ex-info "name not detected from a url. Please specify --name" {}))) 17 | (symbol (str "gh." namespace) name))) 18 | dir (gl/procure url gitlib-name (:branch options))] 19 | (println "Cloned to" dir) 20 | dir)) 21 | -------------------------------------------------------------------------------- /.bb-logseq-convert.edn: -------------------------------------------------------------------------------- 1 | ;; Configures what properties a given host keeps to be converted to a block. 2 | ;; This is my config and should be moved to a personal global config 3 | {:host-properties 4 | {"github.com" 5 | #{"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" 6 | "http://ogp.me/ns#description" 7 | "http://schema.org/name"} 8 | 9 | "youtube.com" 10 | ;; consider duration and related u.author.{name,url} 11 | #{"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" 12 | "http://ogp.me/ns#description" 13 | "http://schema.org/name"} 14 | 15 | "imdb.com" 16 | ;; consider genre, rating, date published and actors 17 | ;; related anonymous _.name would be preferred 18 | #{"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" 19 | "http://opengraphprotocol.org/schema/title" 20 | "http://opengraphprotocol.org/schema/description"} 21 | 22 | "en.wikipedia.org" 23 | ;; type isn't consistently accurate here as there are usually multiple nodes with types 24 | #{"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" 25 | "http://schema.org/headline"}}} 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Gabriel Horner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/util/maven_artifact.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc cldwalker.bb-clis.util.maven-artifact 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as str] 4 | [clojure.java.shell :as shell])) 5 | 6 | (defn artifact-files 7 | "List clj/cljs/cljc files in a maven artifact. When orienting yourself 8 | in a new library, this fn and (dir) are helpful for quick api exploration." 9 | [artifact version] 10 | (let [artifact_ (if (.contains artifact "/") 11 | artifact 12 | (str artifact "/" artifact))] 13 | (if-let [jar-file (->> (format "%s/.m2/repository/%s/%s" 14 | (System/getenv "HOME") artifact_ version) 15 | io/file 16 | .listFiles 17 | seq 18 | (filter #(re-find #"\.jar$" (str %))) 19 | first)] 20 | (->> (str/split-lines (:out (shell/sh "jar" "-tf" (str jar-file)))) 21 | (filter #(re-find #"\.(clj|cljs|cljc)$" %)) 22 | (remove #(re-find #"project.clj$" %))) 23 | (println "No jar file found")))) 24 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/rdf.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.rdf 2 | "RDF related tasks" 3 | (:require [babashka.process :refer [process shell]] 4 | [clojure.string :as str])) 5 | 6 | (defn rdf-equal 7 | "Test if multiple turtle rdf files are equal" 8 | [& files] 9 | (let [write-nt 10 | (fn [in-file out-file] 11 | (println "Write" out-file) 12 | (spit out-file 13 | (-> (process "serdi -i turtle -o ntriples" in-file) 14 | (process {:out :string} "sort") 15 | deref 16 | :out))) 17 | file-pairs (map (fn [x] (vector x (str/replace-first x ".ttl" ".nt"))) 18 | files)] 19 | (doseq [file-pair file-pairs] 20 | (apply write-nt file-pair)) 21 | (apply shell "diff" (map second file-pairs)) 22 | (println "Success!"))) 23 | 24 | (defn triples-count 25 | "Count number of triples in a turtle file" 26 | [file] 27 | (println (format "%s triples!" 28 | (-> (shell {:out :string} "serdi -i turtle -o ntriples" file) 29 | :out 30 | str/split-lines 31 | count)))) 32 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/cli.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.cli 2 | "Common fns for babashka/clojure CLIs" 3 | (:require [clojure.string :as str] 4 | [clojure.java.io :as io] 5 | [clojure.tools.cli :as cli])) 6 | 7 | (defn error 8 | "Print error message(s) and exit" 9 | [& msgs] 10 | (apply println "Error:" msgs) 11 | (System/exit 1)) 12 | 13 | (defn print-summary 14 | "Print help summary given args and opts strings" 15 | [args-string options-summary] 16 | (println (format "Usage: %s [OPTIONS]%s\nOptions:\n%s" 17 | (.getName (io/file *file*)) 18 | args-string 19 | options-summary))) 20 | 21 | (defn run-command 22 | "Processes a command's functionality given a cli options definition, arguments 23 | and primary command fn. This handles option parsing, handles any errors with 24 | parsing and then passes parsed input to command fn" 25 | [command-fn args cli-opts & parse-opts-options] 26 | (let [{:keys [errors] :as parsed-input} 27 | (apply cli/parse-opts args cli-opts parse-opts-options)] 28 | (if (seq errors) 29 | (error (str/join "\n" (into ["Options failed to parse:"] errors))) 30 | (command-fn parsed-input)))) 31 | -------------------------------------------------------------------------------- /bin/bb-var-usages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vim: set filetype=clojure: 3 | ;; Print usages of a var for given source paths 4 | 5 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 6 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 7 | 8 | (ns bb-var-usages 9 | "Modified version of https://github.com/borkdude/clj-kondo/blob/master/analysis/src/clj_kondo/tools/find_var.clj" 10 | (:require [babashka.pods :as pods] 11 | [clojure.string :as str])) 12 | 13 | (pods/load-pod "clj-kondo") 14 | (require '[pod.borkdude.clj-kondo :as clj-kondo]) 15 | 16 | (defn -main [var & paths] 17 | (let [[var-ns var-name] (map symbol (str/split var #"/")) 18 | analysis (:analysis (clj-kondo/run! {:lint paths 19 | :config {:output {:analysis true}}})) 20 | {:keys [var-usages]} analysis 21 | usages (keep (fn [{:keys [to name] :as d}] 22 | (when (and (= var-ns to) 23 | (= var-name name)) 24 | d)) 25 | var-usages)] 26 | (doseq [{:keys [filename from from-var row col]} 27 | (sort-by (juxt :filename :row :col) usages)] 28 | (println (str var " is used in " from "/" from-var 29 | " at " filename ":" row ":" col))))) 30 | 31 | (apply -main *command-line-args*) 32 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/rdf_data.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.rdf-data 2 | (:require [babashka.process :as process] 3 | [clojure.string :as str] 4 | [cheshire.core :as json])) 5 | 6 | ;; TODO: Reuse with bb-logseq-convert 7 | (defn- process-by-timeout 8 | "Runs cmd which returns output as string. If timeout ms is reached, returns 9 | what has been written to stdout" 10 | [cmd timeout] 11 | (let [out-str (java.io.StringWriter.) 12 | ret (deref (:out (process/process cmd {:out out-str})) timeout :timeout)] 13 | (if (= ret :timeout) 14 | (str out-str) 15 | (str ret)))) 16 | 17 | (def rdf-data-cli-options 18 | [["-a" "--all" "Prints all data, not just schema.org predicates with graph and subject filtered out"]]) 19 | 20 | (defn rdf-data 21 | [args options] 22 | (let [rdf-data (process-by-timeout ["rdf-dereference" (first args)] 2500) 23 | filter-fn (if (:all options) 24 | (constantly true) 25 | (fn [{:keys [predicate]}] 26 | (or (str/includes? predicate "schema.org") 27 | ;; Next two are useful on github 28 | (str/includes? predicate "ogp.me") 29 | (= "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" predicate)))) 30 | map-fn (if (:all options) identity (fn [m] (dissoc m :subject :graph))) 31 | result (map map-fn 32 | (filter filter-fn (json/parse-string rdf-data true)))] 33 | result)) 34 | -------------------------------------------------------------------------------- /bin/bb-logseq-move-to-page: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ; vim: set filetype=clojure: 3 | ; Moves copied text to page with name extracted from block 4 | 5 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 6 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 7 | 8 | (ns bb-logseq-move-to-page 9 | (:require [cldwalker.bb-clis.cli :as cli] 10 | [cldwalker.bb-clis.cli.logseq :as logseq] 11 | [babashka.tasks :refer [shell]])) 12 | 13 | (defn- move-to-page [block options] 14 | (let [properties (logseq/block->properties block) 15 | page-name (properties "name")] 16 | (when-not page-name 17 | (println "Error: Name attribute required to move page") 18 | (System/exit 1)) 19 | (let [file (str (:directory options) "/pages/" page-name ".md") 20 | block (logseq/properties->block 21 | (into {} (remove #(= "name" (key %)) properties)))] 22 | (spit file block) 23 | (shell {:in (properties "name")} "pbcopy") 24 | (println "Page" page-name "written successfully.")))) 25 | 26 | (defn -main [{:keys [options summary]}] 27 | (if (:help options) 28 | (cli/print-summary "" summary) 29 | (move-to-page (:out (shell {:out :string} "pbpaste")) options))) 30 | 31 | (def options 32 | [["-d" "--directory LOGSEQ-DIR" 33 | :default-fn (fn [_] (str (System/getenv "HOME") "/code/priv/logseq-notes"))]]) 34 | 35 | (when (= *file* (System/getProperty "babashka.file")) 36 | (cli/run-command -main *command-line-args* options)) 37 | -------------------------------------------------------------------------------- /bin/bb-missed-docstrings: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vim: set filetype=clojure: 3 | 4 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 5 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 6 | 7 | (ns bb-missed-docstrings 8 | "A slightly modified version of https://github.com/borkdude/clj-kondo/blob/master/analysis/src/clj_kondo/tools/missing_docstrings.clj" 9 | (:require [cldwalker.bb-clis.cli :as cli] 10 | [babashka.pods :as pods])) 11 | 12 | (pods/load-pod "clj-kondo") 13 | (require '[pod.borkdude.clj-kondo :as clj-kondo]) 14 | 15 | (defn- missed-docstrings [paths {:keys [ignore-regex]}] 16 | (let [analysis (:analysis (clj-kondo/run! {:lint paths 17 | :config {:output {:analysis true}}})) 18 | {:keys [var-definitions]} analysis 19 | ignore-var? (if ignore-regex 20 | #(re-find (re-pattern ignore-regex) %) 21 | (constantly false))] 22 | (doseq [{:keys [ns name private doc]} var-definitions] 23 | (when (and (not private) 24 | (not doc) 25 | (not (ignore-var? (str name)))) 26 | (println (str ns "/" name ": missing docstring")))))) 27 | 28 | (defn -main [{:keys [arguments summary options]}] 29 | (if (:help options) 30 | (cli/print-summary " SOURCE-PATHS" summary) 31 | (missed-docstrings arguments options))) 32 | 33 | (def cli-options 34 | [["-h" "--help"] 35 | ["-i" "--ignore-regex REGEX"]]) 36 | 37 | (cli/run-command -main *command-line-args* cli-options) 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - name: Install babashka 9 | uses: DeLaGuardo/setup-clojure@10.2 10 | with: 11 | bb: 1.0.166 12 | - name: Run bb tests 13 | run: PATH=$PWD/bin:$PATH bb test 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Java 19 | uses: actions/setup-java@v3 20 | with: 21 | distribution: 'zulu' 22 | java-version: 17 23 | 24 | - name: Install clojure 25 | uses: DeLaGuardo/setup-clojure@master 26 | with: 27 | cli: 1.11.1.1165 28 | 29 | - run: clojure -M:clj-kondo --lint src bin/* test 30 | custom-lint: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v3 34 | - name: Install babashka 35 | uses: DeLaGuardo/setup-clojure@10.2 36 | with: 37 | bb: 1.0.166 38 | # TODO: Re-enable or replace with carve that has better support for bb.edn usage or ignoring a regex of ns 39 | # - name: Check for unused vars 40 | # run: bin/bb-unused-vars -i .github/workflows/unused-vars-to-ignore.edn bin/* src 41 | - run: bb --config dev-bb.edn lint:large-vars 42 | - run: bb --config dev-bb.edn lint:ns-docstrings 43 | - run: bb --config dev-bb.edn lint:minimize-public-vars 44 | 45 | # TODO: Re-enable or replace when there is more docstrings 46 | # - name: Check for missed docstrings 47 | # run: bin/bb-missed-docstrings -i 'cli-options|-main' bin/* src 48 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/util.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.util 2 | (:require [clojure.string :as str])) 3 | 4 | (defn parse-options 5 | "Provides :options for a task to define options for cli/parse-opts. Use 6 | :cli-options for additional arguments to cli/parse-opts. If no :options, 7 | return ::no-options." 8 | [task-map] 9 | ;; Unsupported task keys like :options don't return values with valid 10 | ;; fns. Eval translates symbols to fns. Could reach into bb internals 11 | ;; or ask for this to be supported upstream but this is least effort 12 | ;; and coupling. 13 | (if-let [task-opts (some-> (:options task-map) eval)] 14 | (let [parsed-args 15 | (apply (requiring-resolve 'clojure.tools.cli/parse-opts) *command-line-args* 16 | task-opts 17 | (:cli-options task-map))] 18 | parsed-args) 19 | ::no-options)) 20 | 21 | (defn check-for-required-arguments [parsed-args task-map] 22 | (let [required-args (->> (str (:usage task-map)) 23 | ((fn [s] (str/split s (re-pattern "\\s+")))) 24 | (take-while (fn [s] (re-find (re-pattern "^[A-Z]") s)))) 25 | args (if (= ::no-options parsed-args) 26 | *command-line-args* (:arguments parsed-args))] 27 | (when (< (count args) (count required-args)) 28 | (println "Wrong number of arguments given.") 29 | (println (format "Usage: bb %s %s%s" 30 | (:name task-map) 31 | (:usage task-map) 32 | (if-let [summary (:summary parsed-args)] 33 | (str "\nOptions:\n" summary) 34 | ""))) 35 | (System/exit 1)))) 36 | -------------------------------------------------------------------------------- /test/bin/bb_replace_test.clj: -------------------------------------------------------------------------------- 1 | (ns bin.bb-replace-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [clojure.edn :as edn] 4 | [clojure.java.io :as io] 5 | [clojure.string :as str] 6 | [clojure.java.shell :as shell])) 7 | 8 | (deftest help-option 9 | (let [;; call once to clone deps. This workaround is temporary if this ns 10 | ;; is no longer the first to be tested 11 | _ (shell/sh "bb-replace" "-h") 12 | cmd-results (shell/sh "bb-replace" "-h" 13 | :dir "test/resources/bin/bb_replace_test") 14 | expected-results (-> (io/resource "bin/bb_replace_test/help-option.edn") 15 | slurp 16 | edn/read-string)] 17 | (is (= (:exit expected-results) (:exit cmd-results))) 18 | (is (= (:out expected-results) (:out cmd-results))) 19 | (is (= (:err expected-results) (:err cmd-results))))) 20 | 21 | (deftest standard-replacement 22 | (spit "test/resources/bin/bb_replace_test/project.clj" "(defproject bar \"1.0.0\")") 23 | 24 | (let [cmd-results (shell/sh "bb-replace" "lein-version" "2.0.0" 25 | :dir "test/resources/bin/bb_replace_test") 26 | expected-results (-> (io/resource "bin/bb_replace_test/standard-replacement.edn") 27 | slurp 28 | edn/read-string)] 29 | (is (= (:exit expected-results) (:exit cmd-results))) 30 | (is (= (:out expected-results) (:out cmd-results))) 31 | (is (= (:err expected-results) (:err cmd-results))) 32 | (is (str/includes? (slurp (io/resource "bin/bb_replace_test/project.clj")) "2.0.0")) 33 | 34 | (io/delete-file "test/resources/bin/bb_replace_test/project.clj"))) 35 | -------------------------------------------------------------------------------- /bin/bb-project-clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ; Prints out a lein project.clj file as a map 3 | ; vim: set filetype=clojure: 4 | 5 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 6 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 7 | 8 | (ns bb-project-clj 9 | (:require [cldwalker.bb-clis.cli :as cli] 10 | [clojure.string :as str] 11 | [clojure.pprint :as pprint])) 12 | 13 | ;; While these are preventing breakages, these may be worth putting 14 | ;; behind an option if they have unintended consequences 15 | (defn- modify-project-clj [string] 16 | (-> string 17 | ;; Ignore eval forms that break read-string 18 | (str/replace "#=" "") 19 | ;; Turn regexs into strings as regexs aren't supported by read-string 20 | (str/replace "#\"" "\""))) 21 | 22 | (defn- extract-project-form 23 | "Extracts defproject form from given project.clj string. drop-forms is the 24 | number of forms to drop before getting to defproject" 25 | [drop-forms string] 26 | (->> (str "[" string "]") 27 | read-string 28 | (drop drop-forms) 29 | first)) 30 | 31 | ;; Modified from https://gist.github.com/swlkr/3f346c66410e5c60c59530c4413a248e#gistcomment-3232605 32 | (defn- project-clj-map [filename drop-forms] 33 | (->> (slurp filename) 34 | modify-project-clj 35 | (extract-project-form drop-forms) 36 | (drop 1) 37 | (partition 2) 38 | (map vec) 39 | (into {}))) 40 | 41 | (defn- -main [{:keys [options summary]}] 42 | (cond 43 | (:help options) (cli/print-summary "" summary) 44 | :else (let [project-clj (project-clj-map (:file options) (:drop-forms options))] 45 | (pprint/pprint project-clj)))) 46 | 47 | (def cli-options 48 | [["-f" "--file FILE" "Location of project.clj file" 49 | :default "project.clj"] 50 | ["-d" "--drop-forms FORMS" "Forms to drop before defproject form" 51 | :default 0 52 | :parse-fn #(Integer/parseInt %)] 53 | ["-h" "--help"]]) 54 | 55 | (cli/run-command -main *command-line-args* cli-options) 56 | -------------------------------------------------------------------------------- /bin/bb-unused-vars: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vim: set filetype=clojure: 3 | 4 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 5 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 6 | 7 | (ns bb-unused-vars 8 | "A slightly modified version of https://github.com/borkdude/clj-kondo/blob/master/analysis/src/clj_kondo/tools/unused_vars.clj" 9 | (:require [clojure.set :as set] 10 | [clojure.string :as str] 11 | [clojure.edn :as edn] 12 | [cldwalker.bb-clis.cli :as cli] 13 | [babashka.pods :as pods])) 14 | 15 | (pods/load-pod "clj-kondo") 16 | (require '[pod.borkdude.clj-kondo :as clj-kondo]) 17 | 18 | (defn check-unused-vars 19 | "Checks for unused vars. If found prints them and fails with exit 1." 20 | [paths {:keys [ignore-file]}] 21 | (let [analysis (:analysis (clj-kondo/run! {:lint paths 22 | :config {:output {:analysis true}}})) 23 | {:keys [:var-definitions :var-usages]} analysis 24 | defined-vars (set (map (juxt :ns :name) var-definitions)) 25 | used-vars (cond-> (set (map (juxt :to :name) var-usages)) 26 | ignore-file 27 | (set/union (set (map 28 | (juxt (comp symbol namespace) (comp symbol name)) 29 | (edn/read-string (slurp ignore-file)))))) 30 | unused-vars (map (fn [[ns v]] 31 | (symbol (str ns) (str v))) 32 | (set/difference defined-vars used-vars))] 33 | (if (seq unused-vars) 34 | (do (println "The following vars are unused:") 35 | (println (str/join "\n" unused-vars)) 36 | (System/exit 1)) 37 | (do (println "No unused vars found.") 38 | (System/exit 0))))) 39 | 40 | (defn -main [{:keys [summary arguments options]}] 41 | (if (or (:help options) (zero? (count arguments))) 42 | (cli/print-summary " SOURCE-PATHS" summary) 43 | (check-unused-vars arguments options))) 44 | 45 | (def cli-options 46 | [["-h" "--help"] 47 | ["-i" "--ignore-file FILE"]]) 48 | 49 | (cli/run-command -main *command-line-args* cli-options) 50 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/clj_kondo.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.clj-kondo 2 | (:require [babashka.pods :as pods])) 3 | 4 | ;; Need at least "2021.10.19" for var-meta 5 | (pods/load-pod "clj-kondo") 6 | (require '[pod.borkdude.clj-kondo :as clj-kondo]) 7 | 8 | (defn var-sizes 9 | [args] 10 | (let [paths (or args ["src"]) 11 | {{:keys [var-definitions var-usages]} :analysis} 12 | (clj-kondo/run! 13 | {:lint paths 14 | :config {:output {:analysis true}}}) 15 | comment-blocks (->> var-usages 16 | (filter (fn [m] (= (:name m) 'comment))) 17 | (map (fn [m] (select-keys m [:row :end-row :filename])))) 18 | in-comment-block? (fn [filename line] 19 | (some (fn [m] 20 | (and (= filename (:filename m)) (<= (:row m) line (:end-row m)))) 21 | comment-blocks)) 22 | vars (->> var-definitions 23 | (keep (fn [m] 24 | (when-not (in-comment-block? (:filename m) (:row m)) 25 | {:name (:name m) 26 | :loc-size (inc (- (:end-row m) (:row m))) 27 | :filename (:filename m)}))) 28 | (sort-by :loc-size (fn [x y] (compare y x))) 29 | (take 10))] 30 | vars)) 31 | 32 | (defn var-meta 33 | [args] 34 | (let [paths (or (seq args) ["src"]) 35 | {{:keys [var-definitions]} :analysis} 36 | (clj-kondo/run! 37 | {:lint paths 38 | :config {:output {:analysis {:var-definitions {:meta true}}}}}) 39 | matches (keep (fn [m] 40 | (when (:meta m) 41 | {:var (str (:ns m) "/" (:name m)) 42 | :meta (:meta m)})) 43 | var-definitions)] 44 | matches)) 45 | 46 | (defn ns-meta 47 | [args] 48 | (let [paths (or (seq args) ["src"]) 49 | {{:keys [namespace-definitions]} :analysis} 50 | (clj-kondo/run! 51 | {:lint paths 52 | :config {:output {:analysis {:namespace-definitions {:meta true}}}}}) 53 | matches (keep (fn [m] 54 | (when (:meta m) 55 | {:ns (:name m) 56 | :meta (:meta m)})) 57 | namespace-definitions)] 58 | matches)) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | An assortment of handy [Babashka](https://github.com/borkdude/babashka) CLIs: 4 | scripts and tasks. scripts strive to be compatible with Clojure. 5 | 6 | ## Prerequisites 7 | 8 | [Babashka](https://github.com/borkdude/babashka#installation) >= 1.0.166 is required. 9 | 10 | ## Tasks 11 | 12 | bb.edn contains global tasks i.e. tasks that are useful in any directory or 13 | project. To run these tasks from any directory, clone this repo and then use a 14 | shell function to reference the cloned directory: 15 | 16 | ```sh 17 | function bbg() { BABASHKA_EDN=/path/to/this-repo/bb.edn bb --config 18 | /path/to/this-repo/bb.edn $@ } 19 | ``` 20 | 21 | Run `bbg tasks` to see all the available tasks. 22 | 23 | ## Scripts 24 | 25 | ### Setup 26 | 27 | Scripts/commands/executables are located in `bin/`. To use an individual script, simply copy 28 | and use it: 29 | 30 | ```sh 31 | $ curl -o bb-github-repo https://raw.githubusercontent.com/cldwalker/bb-clis/master/bin/bb-github-repo 32 | $ chmod +x bb-github-repo 33 | $ ./bb-github-repo -h 34 | ``` 35 | 36 | If you want to use all scripts in this repo, then put `bin/` on `$PATH`: 37 | 38 | ```sh 39 | $ git clone https://github.com/cldwalker/bb-clis 40 | $ export PATH=$PATH:$HOME/path/to/bb-clis/bin 41 | ``` 42 | 43 | ### Usage 44 | 45 | See [scripts.md](doc/scripts.md) which provides useful examples of several scripts. 46 | 47 | ## Development 48 | 49 | Code is organized as follows: 50 | * `src/cldwalker/bb-clis/tasks/` - Namespaces that are mainly run within babashka tasks 51 | * `src/cldwalker/bb-clis/cli/` - Namespaces that useful to scripts and possibly tasks. 52 | * `src/cldwalker/bb-clis/util/` - Namespaces that are useful to any clojure or bb program, not just CLIs. 53 | 54 | ## Misc bb tips 55 | 56 | ### Preloads 57 | 58 | Babashka supports `$BABASHA_PRELOADS` which allows arbitrary clojure to be run at the start of each invocation. This is handy for loading one's preferred set of vars and namespaces, especially when paired with an alias. For example, `alias bbp="BABASHKA_PRELOADS='(load-file (str (System/getenv \"HOME\") \"/path/to/this-repo/preloads.clj\"))' bb"` 59 | 60 | Preloaded fns like `pprint` are then available on the commandline: 61 | 62 | ```sh 63 | bbp '(->> (System/getenv) (into {}) pprint)' 64 | ``` 65 | ## License 66 | See LICENSE.md 67 | 68 | ## Additional Links 69 | 70 | * For more bb setup and aliases, see [my dotfiles repo](https://github.com/cldwalker/dotfiles/search?q=bb&unscoped_q=bb) 71 | * See https://github.com/borkdude/babashka/blob/master/doc/examples.md for additional babashka cmd examples 72 | -------------------------------------------------------------------------------- /bin/bb-github-pr-for-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ; vim: set filetype=clojure: 3 | 4 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 5 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 6 | 7 | (ns bb-github-pr-for-commit 8 | (:require [babashka.curl :as curl] 9 | [cheshire.core :as json] 10 | [cldwalker.bb-clis.cli :as cli] 11 | [cldwalker.bb-clis.cli.misc :as misc])) 12 | 13 | (defn- fetch-response* [commit {:keys [repository token user]}] 14 | (try 15 | (curl/get (format "https://api.github.com/repos/%s/commits/%s/pulls" 16 | repository commit) 17 | ;; Check https://developer.github.com/v3/repos/commits/#list-pull-requests-associated-with-commit to see if this is still in preview 18 | (cond-> {:headers {"Accept" "application/vnd.github.groot-preview+json"}} 19 | (and user token) (assoc :basic-auth [user token]))) 20 | (catch clojure.lang.ExceptionInfo e 21 | (cli/error "Failed to fetch github information" (pr-str {:error (ex-message e)}))))) 22 | 23 | (defn- fetch-response [commit options] 24 | (let [{:keys [body]} (fetch-response* commit options) 25 | repos (json/parse-string body true)] 26 | (if-let [url (if (= 1 (count repos)) 27 | (-> repos first :html_url) 28 | (let [repos' (remove #(get-in % [:base :repo :fork]) repos)] 29 | (prn :URLS (map :html_url repos')) 30 | (->> repos' (filter :merged_at) first :html_url)))] 31 | (do (misc/open-url url) 32 | url) 33 | (cli/error "No github PR found for this commit")))) 34 | 35 | (defn -main [{:keys [options arguments summary]}] 36 | (when (:debug options) (println "Options:" options)) 37 | (if (or (:help options) (empty? arguments)) 38 | (cli/print-summary " COMMIT" summary) 39 | (fetch-response (first arguments) options))) 40 | 41 | (def cli-options 42 | ;; An option with a required argument 43 | [["-r" "--repository REPO" 44 | :default-fn misc/find-current-user-repo 45 | :default-desc "Current directory's repository" 46 | :validate [#(re-find #"\S+/\S+" %) "Must contain a '/'"]] 47 | ["-d" "--debug"] 48 | ["-u" "--user USER" 49 | :default-fn (fn [_x] (System/getenv "GITHUB_USER")) 50 | :default-desc "$GITHUB_USER"] 51 | ["-t" "--token TOKEN" 52 | :default-fn (fn [_x] (System/getenv "GITHUB_OAUTH_TOKEN")) 53 | :default-desc "$GITHUB_OAUTH_TOKEN"] 54 | ["-h" "--help"]]) 55 | 56 | (cli/run-command -main *command-line-args* cli-options) 57 | -------------------------------------------------------------------------------- /bin/bl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vim: set filetype=clojure: 3 | ;; (b)abashka (l)aunch - Launches a bb command quickly by matching on start 4 | ;; of command after `bb-`. 5 | ;; bb commands assumed to start with `bb-`. 6 | ;; Example: `bl t -h` -> bb-table -h 7 | 8 | (ns bl 9 | (:require [clojure.java.io :as io] 10 | [babashka.tasks :refer [shell]] 11 | [clojure.string :as str])) 12 | 13 | (defn- match-alias 14 | "Tries matching a query substring on a list of potential candidates. The following 15 | approaches are tried in order: 16 | * Match full word on first letter of each clojure cased word of a candidate e.g. 17 | gr -> github-repo but not grep-result-frequencies 18 | * Same as last match but match on partial word e.g. gp -> github-pr-for-commit 19 | * Match on start of candidate e.g. ta -> table" 20 | [query candidates] 21 | (let [full-clojure-case-abbrev 22 | (let [match-regex (re-pattern (str "^bb-" 23 | (->> query seq (map #(str % "[^-]+")) (str/join "-")) 24 | "$"))] 25 | #(re-find match-regex %)) 26 | matches (filterv full-clojure-case-abbrev candidates)] 27 | (if (= 1 (count matches)) 28 | matches 29 | (let [partial-clojure-case-abbrev 30 | (let [match-regex (re-pattern (str "^bb-" 31 | (->> query seq (map #(str % "[^-]+")) (str/join "-"))))] 32 | #(re-find match-regex %)) 33 | matches_ (filterv partial-clojure-case-abbrev candidates)] 34 | (if (= 1 (count matches_)) 35 | matches_ 36 | (filterv #(str/starts-with? % (str "bb-" query)) candidates)))))) 37 | 38 | (defn -main [args] 39 | (let [bb-commands (->> (str/split (System/getenv "PATH") #":") 40 | distinct 41 | (mapcat (fn [path] (seq (.list (io/file path))))) 42 | (filter #(str/starts-with? % "bb-"))) 43 | cmd-alias (first args) 44 | cmds (match-alias cmd-alias bb-commands) 45 | help-string (str "Available commands: " (str/join ", " bb-commands))] 46 | (cond 47 | (nil? cmd-alias) 48 | (println help-string) 49 | 50 | (zero? (count cmds)) 51 | (do 52 | (println (str "Error: No commands matched for " (pr-str cmd-alias) 53 | "\n" help-string)) 54 | (System/exit 1)) 55 | 56 | (> (count cmds) 1) 57 | (do 58 | (println (str "Error: More than one command matched for " (pr-str cmd-alias) 59 | "\n" help-string)) 60 | (System/exit 1)) 61 | 62 | :else (do (apply shell (into cmds (rest args))) 63 | nil)))) 64 | 65 | (-main *command-line-args*) 66 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/cli/misc.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.cli.misc 2 | "Misc fns that are useful to bb/clojure clis" 3 | (:require [cldwalker.bb-clis.cli :as cli] 4 | [clojure.edn :as edn] 5 | [clojure.string :as str] 6 | [clojure.java.shell :as shell])) 7 | 8 | ;; Misc 9 | ;; ==== 10 | (defn open-url 11 | "Osx specific way to open url in browser" 12 | [url] 13 | ;; -n needed to open in big sur. 14 | ;; See https://apple.stackexchange.com/questions/406556/macos-big-sur-terminal-command-open-behaviour-changed-and-i-dont-know-how-to 15 | (shell/sh "open" "-n" url)) 16 | 17 | 18 | (defn sh 19 | "Wrapper around a shell command which fails fast like bash's -e flag. 20 | Takes following options: 21 | * :dry-run: Prints command instead of executing it" 22 | [& args*] 23 | (let [default-opts {:is-error-fn #(-> % :exit (not= 0))} 24 | [options args] (if (map? (last args*)) 25 | [(merge default-opts (last args*)) 26 | (drop-last args*)] 27 | [default-opts args*])] 28 | (if (:dry-run options) 29 | (apply println "Command: " args) 30 | (let [{:keys [out err] :as res} 31 | (apply shell/sh 32 | (concat args [:dir (:dir options)]))] 33 | (if ((:is-error-fn options) res) 34 | (cli/error (format "Command '%s' failed with:\n%s" 35 | (str/join " " args) 36 | (str out "\n" err))) 37 | out))))) 38 | 39 | ;; Github 40 | ;; ====== 41 | (defn find-current-user-repo 42 | "Returns github user/repository of current directory" 43 | [opts] 44 | ;; Don't like this knowing about help but don't want error scenarios occurring 45 | ;; when running --help 46 | (if (:help opts) 47 | opts 48 | (let [{:keys [out exit err]} (shell/sh "git" "config" "remote.origin.url")] 49 | (if (zero? exit) 50 | ;; Can handle gh:atom/atom, https://github.com/atom/atom.git or git@github.com:atom/atom.git 51 | (if-let [user-repo (second (re-find #"(?:gh|github.com)(?::|/)([^/]+/[^/.\s]+)" out))] 52 | user-repo 53 | (cli/error "Failed to determine current directory's repository" (pr-str {:out out}))) 54 | (cli/error "Failed to determine current directory's repository" (pr-str {:error err :out out})))))) 55 | 56 | ;; I/O 57 | ;; === 58 | 59 | (defn read-stdin-edn 60 | "Read edn on *in* into a coll. Useful to convert a `bb -I --stream` cmd to 61 | a script that doesn't require that invocation." 62 | [] 63 | (take-while #(not (identical? ::EOF %)) 64 | (repeatedly #(edn/read {:eof ::EOF} *in*)))) 65 | 66 | (defn stdin-active? 67 | "Detect if stdin is active. Waits for a split second" 68 | [] 69 | ;; Sleep needed per https://github.com/babashka/babashka/issues/324#issuecomment-639810098 70 | (Thread/sleep 100) 71 | (pos? (.available System/in))) 72 | -------------------------------------------------------------------------------- /bin/bb-github-repo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ; Opens urls related to current or specified github repository 3 | ; vim: set filetype=clojure: 4 | 5 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 6 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 7 | 8 | (ns bb-github-repo 9 | (:require [cldwalker.bb-clis.cli :as cli] 10 | [cldwalker.bb-clis.cli.misc :as misc] 11 | [clojure.string :as str] 12 | [clojure.java.io :as io] 13 | [clojure.java.shell :as shell])) 14 | 15 | (defn- find-current-branch [] 16 | (if-let [branch (->> (shell/sh "git" "branch") 17 | :out 18 | str/split-lines 19 | (filter #(str/starts-with? % "*")) 20 | first 21 | (re-find #"\S+$"))] 22 | branch 23 | (cli/error "Unable to detect current tree"))) 24 | 25 | (defn- get-relative-git-root-path 26 | "Given a path, return its path relative to git root" 27 | [path] 28 | (let [top-level-dir (-> (shell/sh "git" "rev-parse" "--show-toplevel") 29 | :out 30 | str/trimr 31 | (str "/")) 32 | full-path (.getAbsolutePath (io/file path)) 33 | git-relative-path (str/replace-first full-path top-level-dir "")] 34 | git-relative-path)) 35 | 36 | (defn- open-github-url [{:keys [repository commit tree file]}] 37 | (let [url-base (str "https://github.com/" repository) 38 | url (cond 39 | commit (str url-base "/commit/" commit) 40 | tree (str url-base "/tree/" (find-current-branch)) 41 | file (str url-base "/blob/" (find-current-branch) 42 | "/" (get-relative-git-root-path file)) 43 | 44 | :else url-base)] 45 | (doto url misc/open-url))) 46 | 47 | (defn- open-circleci-url [{:keys [repository]}] 48 | (let [url (format "https://circleci.com/gh/%s/tree/%s" 49 | repository 50 | (find-current-branch))] 51 | (doto url misc/open-url))) 52 | 53 | (defn -main [{:keys [options _arguments summary]}] 54 | (cond 55 | (:help options) (cli/print-summary "" summary) 56 | (:circleci options) (open-circleci-url options) 57 | :else (open-github-url options))) 58 | 59 | (def cli-options 60 | [["-r" "--repository REPO" 61 | :default-fn misc/find-current-user-repo 62 | :default-desc "Current directory's repository" 63 | :validate [#(re-find #"\S+/\S+" %) "Must contain a '/'"]] 64 | ["-c" "--commit COMMIT" "Opens specified commit"] 65 | ["-C" "--circleci" "Opens current branch on circleci"] 66 | ["-t" "--tree" "Opens current branch on github"] 67 | ["-f" "--file FILE" "Opens file in current branch on github"] 68 | ["-h" "--help"]]) 69 | 70 | (cli/run-command -main *command-line-args* cli-options) 71 | -------------------------------------------------------------------------------- /bin/bb-try: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vim: set filetype=clojure: 3 | ;; Try a dependency with bb like lein-try 4 | 5 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 6 | ; (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:local/root "."}}}) 7 | 8 | (ns bb-try 9 | (:require [cldwalker.bb-clis.cli :as cli] 10 | [babashka.tasks :refer [shell]] 11 | [clojure.java.shell :as shell])) 12 | 13 | (def cli-options 14 | [["-h" "--help"] 15 | ["-c" "--command COMMAND"] 16 | ["-v" "--version DEPENDENCY_VERSION"] 17 | ;; print-command only works for commands without env var e.g. clj 18 | ["-p" "--print-command"]]) 19 | 20 | (defn- create-deps-string 21 | [dependency {:keys [version] :or {version "RELEASE"}}] 22 | (format "{:deps {%s {:mvn/version \"%s\"}}}" 23 | dependency version)) 24 | 25 | (defn- exec [args env print-command?] 26 | ;; print-command doesn't work for bb b/c it can't print env var in a way that's 27 | ;; reproducible for others 28 | (if print-command? 29 | (apply println (map #(if (.contains % " ") 30 | (str "'" % "'") 31 | %) 32 | args)) 33 | (do (apply shell {:extra-env env} args) 34 | nil))) 35 | 36 | (defn- bb-main 37 | [arguments {:keys [command print-command] :as options}] 38 | (let [[dependency & bb-args] arguments 39 | deps-string (create-deps-string dependency options) 40 | new-classpath (str (System/getenv "BABASHKA_CLASSPATH") 41 | ":" 42 | (:out (shell {:out :string} "clojure" "-Spath" "-Sdeps" deps-string))) 43 | bb-invocation (if (-> (shell/sh "which" "rlwrap") :exit zero?) 44 | ["rlwrap" command] 45 | [command])] 46 | (exec (concat bb-invocation bb-args) 47 | {"BABASHKA_CLASSPATH" new-classpath} 48 | print-command))) 49 | 50 | (defn- clj-main [arguments {:keys [print-command] :as options}] 51 | (let [[dependency & clj-args] arguments 52 | deps-string (create-deps-string dependency options) 53 | clj-options ["-Sdeps" deps-string] 54 | clj-invocation (if (-> (shell/sh "which" "rlwrap") :exit zero?) 55 | ["rlwrap" "clojure"] 56 | ["clojure"])] 57 | (exec (concat clj-invocation clj-options clj-args) 58 | {} 59 | print-command))) 60 | 61 | (defn- -main* 62 | [arguments {:keys [command] :or {command "bb"} :as options}] 63 | (if (#{"clj" "clojure"} command) 64 | (clj-main arguments options) 65 | (bb-main arguments (assoc options :command command)))) 66 | 67 | (defn -main 68 | [{:keys [summary arguments options]}] 69 | (cond 70 | (:help options) (cli/print-summary " DEPENDENCY [& ARGS]" summary) 71 | :else (-main* arguments options))) 72 | 73 | (when (= *file* (System/getProperty "babashka.file")) 74 | (cli/run-command -main *command-line-args* cli-options :in-order true)) 75 | -------------------------------------------------------------------------------- /bin/bb-replace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ; vim: set filetype=clojure: 3 | ; Light-weight alternative to sed or perl -i. Aims to provide replacements 4 | ; that are more readable by supporting named replacements. 5 | ; See get-replacements for config docs 6 | 7 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 8 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 9 | 10 | (ns bb-replace 11 | (:require [clojure.string :as str] 12 | [clojure.java.io :as io] 13 | [clojure.edn :as edn] 14 | [cldwalker.bb-clis.cli :as cli])) 15 | 16 | (defn- get-replacements 17 | "Reads replacements from ./bb-replace.edn and ~/.bb-replace.edn. 18 | A replacement consists of three keys: 19 | * :regex : re-pattern regex to match target substring using clojure.string/replace-first 20 | * :format-string - clojure.core/format generates the replacement string using this format and 21 | commandline arguments 22 | * :file - File on which to do replacement" 23 | [] 24 | (cond->> [] 25 | (.exists (io/file (System/getenv "HOME") ".bb-replace.edn")) 26 | (into 27 | [(edn/read-string (slurp (io/file (System/getenv "HOME") ".bb-replace.edn")))]) 28 | 29 | (.exists (io/file ".bb-replace.edn")) 30 | (into [(edn/read-string (slurp ".bb-replace.edn"))]) 31 | 32 | true (mapv (fn [m] (update-vals m #(update % :regex re-pattern)))) 33 | true (apply merge))) 34 | 35 | (defn- update-file-with-replacement 36 | [{:keys [regex file format-string]} arguments] 37 | (let [body (slurp file) 38 | new-body (str/replace-first body regex 39 | (apply format format-string arguments))] 40 | (if (= body new-body) 41 | (cli/error (str "Replacement did not change " file)) 42 | (spit file new-body)))) 43 | 44 | (defn- arg->replacement [arg options] 45 | (if-let [replacement (get (get-replacements) (keyword arg))] 46 | replacement 47 | (if-let [file (:file options)] 48 | {:regex (re-pattern arg) 49 | ;; Not sure if this is a sensible default 50 | :format-string "$1 %s" 51 | :file file} 52 | (cli/error "File option required if providing a regex as a replacement")))) 53 | 54 | (def cli-options 55 | [["-h" "--help"] 56 | ["-f" "--file FILE" "Overrides default file for a replacement"] 57 | ["-F" "--format-string FORMAT" "Overrides default format string for a replacement"]]) 58 | 59 | (defn -main [{:keys [options arguments summary]}] 60 | (if (or (:help options) (zero? (count arguments))) 61 | (cli/print-summary (str " REPLACEMENT/REGEX [& ARGUMENTS]\nReplacements available: " 62 | (str/join ", " (->> (get-replacements) keys (map name)))) 63 | summary) 64 | (let [replacement (arg->replacement (first arguments) options)] 65 | (update-file-with-replacement (merge replacement 66 | (select-keys options [:file :format-string])) 67 | (rest arguments))))) 68 | 69 | (cli/run-command -main *command-line-args* cli-options) 70 | -------------------------------------------------------------------------------- /bin/bb-update-lein-dependency: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ; Updates a lein dependency on specified git-able dir(s) 3 | ; vim: set filetype=clojure: 4 | 5 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 6 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 7 | 8 | (ns bb-update-lein-dependency 9 | (:require [cldwalker.bb-clis.cli :as cli] 10 | [clojure.java.io :as io] 11 | [clojure.string :as str] 12 | [clojure.java.shell :as shell])) 13 | 14 | (defn- sh 15 | "Wrapper around a shell command which fails fast like bash's -e flag. 16 | Takes following options: 17 | * :dry-run: Prints command instead of executing it" 18 | [& args*] 19 | (let [[options args] (if (map? (last args*)) 20 | [(last args*) (drop-last args*)] 21 | [{} args*])] 22 | (if (:dry-run options) 23 | (apply println "Command: " args) 24 | (let [{:keys [out err exit]} (apply shell/sh 25 | (concat args [:dir (:dir options)]))] 26 | (if (zero? exit) 27 | out 28 | (cli/error (format "Command '%s' failed with:\n%s" 29 | (str/join " " args) 30 | (str out "\n" err)))))))) 31 | 32 | (defn- update-project-clj 33 | "Updates project.clj and fails fast if unable to update" 34 | [file dependency version {:keys [dry-run]}] 35 | (if dry-run 36 | (println "Command: update-project-clj" file dependency version) 37 | (let [body (slurp file) 38 | new-body (str/replace-first body 39 | (re-pattern (format "(%s\\s+\")(\\w+)(\")" dependency)) 40 | (format "$1%s$3" version))] 41 | (if (= body new-body) 42 | (cli/error (str "Unable to find and update dependency in " file)) 43 | (spit file new-body))))) 44 | 45 | (defn- update-dir 46 | "Updates project.clj in dir and optionally git commits and pushes" 47 | [[dependency version] dir {:keys [commit-and-push] 48 | :as options*}] 49 | (println (str "Updating " dir " ...")) 50 | 51 | (let [options (assoc options* :dir dir)] 52 | (sh "git" "diff" "--exit-code" options) 53 | (update-project-clj (str (io/file dir "project.clj")) 54 | dependency 55 | version 56 | options) 57 | 58 | (when commit-and-push 59 | (sh "git" "commit" "-m" (format "Updated %s dependency" dependency) "." options) 60 | (sh "git" "push" options)))) 61 | 62 | (def cli-options 63 | [["-d" "--directories DIR" "Directories to update" 64 | :default-fn (fn [_x] [(System/getenv "PWD")]) 65 | :default-desc "Current directory" 66 | :multi true 67 | :update-fn conj 68 | :validate [#(.isDirectory (io/file % ".git")) 69 | "Must be a valid git directory"]] 70 | ["-c" "--commit-and-push" "Git commit and push to remote"] 71 | ["-n" "--dry-run" "Actions are printed and not executed"] 72 | ["-h" "--help"]]) 73 | 74 | (defn -main [{:keys [options arguments summary]}] 75 | (if (or (:help options) (< (count arguments) 2)) 76 | (cli/print-summary " DEPENDENCY VERSION" summary) 77 | (doseq [dir (:directories options)] 78 | (update-dir arguments dir options)))) 79 | 80 | (cli/run-command -main *command-line-args* cli-options) 81 | -------------------------------------------------------------------------------- /bin/bb-ns-dep-tree: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; Prints ascii tree of ns dependencies like tree command 3 | ;; vim: set filetype=clojure: 4 | 5 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 6 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 7 | 8 | (ns bb-ns-dep-tree 9 | (:require [cldwalker.bb-clis.cli :as cli] 10 | [clojure.java.io :as io] 11 | [babashka.pods :as pods])) 12 | 13 | (pods/load-pod "clj-kondo") 14 | (require '[pod.borkdude.clj-kondo :as clj-kondo]) 15 | 16 | ;; Ascii tree from https://github.com/babashka/babashka/blob/master/examples/tree.clj 17 | (def I-branch "│ ") 18 | (def T-branch "├── ") 19 | (def L-branch "└── ") 20 | (def SPACER " ") 21 | 22 | (def already-seen (atom #{})) 23 | 24 | (defn- build-tree 25 | [node children-map {:keys [expand-duplicate-branches] :as opts}] 26 | (let [can-expand? (or expand-duplicate-branches 27 | (not (contains? @already-seen node))) 28 | children (children-map node) 29 | parent? (seq children)] 30 | (swap! already-seen conj node) 31 | (cond-> {:name (str node (when (and parent? (not can-expand?)) " ...")) 32 | :type (if parent? "parent" "child")} 33 | (and parent? can-expand?) (assoc :children 34 | (map #(build-tree % children-map opts) 35 | children))))) 36 | 37 | (defn render-tree 38 | [{:keys [name children]}] 39 | (cons name 40 | (mapcat 41 | (fn [child index] 42 | (let [subtree (render-tree child) 43 | last? (= index (dec (count children))) 44 | prefix-first (if last? L-branch T-branch) 45 | prefix-rest (if last? SPACER I-branch)] 46 | (cons (str prefix-first (first subtree)) 47 | (map #(str prefix-rest %) (next subtree))))) 48 | children 49 | (range)))) 50 | 51 | (defn- clj-kondo-analysis 52 | [paths] 53 | (:analysis (clj-kondo/run! {:lint paths 54 | :config {:output {:analysis true}}}))) 55 | 56 | (defn- build-children-map [analysis lang-to-match] 57 | (reduce (fn [acc {:keys [from to lang]}] 58 | (if (or (nil? lang) (= lang-to-match lang)) 59 | (update acc from (fnil conj []) to) 60 | acc)) 61 | {} 62 | (:namespace-usages analysis))) 63 | 64 | (defn print-ns-tree [[ns-or-file] {:keys [source-paths lang] :as options}] 65 | (let [analysis (clj-kondo-analysis source-paths) 66 | ns-sym (if (.exists (io/file ns-or-file)) 67 | (some #(when (= ns-or-file (:filename %)) (:name %)) 68 | (:namespace-definitions analysis)) 69 | (symbol ns-or-file)) 70 | tree (build-tree ns-sym (build-children-map analysis lang) options)] 71 | (doseq [l (render-tree tree)] 72 | (println l)))) 73 | 74 | (def cli-options 75 | [["-h" "--help"] 76 | ["-e" "--expand-duplicate-branches" "Expands all ns dependencies including duplicate branches"] 77 | ["-l" "--lang LANG" "Language for .cljc analysis" 78 | :parse-fn keyword 79 | :default :clj] 80 | ["-s" "--source-paths DIR" "Source paths to analyze" 81 | :default ["src"] 82 | ;; Allows us to specify multiple values for this option 83 | :assoc-fn (fn [curr _key val] (update curr :source-paths conj val)) 84 | :validate [#(.isDirectory (io/file %)) 85 | "Must be a valid directory"]]]) 86 | 87 | (defn -main [{:keys [summary arguments options]}] 88 | (if (or (:help options) (zero? (count arguments))) 89 | (cli/print-summary " NS/FILE" summary) 90 | (print-ns-tree arguments options))) 91 | 92 | (when (= *file* (System/getProperty "babashka.file")) 93 | (cli/run-command -main *command-line-args* cli-options)) 94 | -------------------------------------------------------------------------------- /bin/bb-vis: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vim: set filetype=clojure: 3 | ;; Generates a png, pdf or svg visualization from a vega-lite spec. 4 | ;; Spec file can be json or edn. 5 | ;; See create-visualization for credits to oz 6 | 7 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 8 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 9 | 10 | (ns bb-vis 11 | (:require [cldwalker.bb-clis.cli :as cli] 12 | [cldwalker.bb-clis.cli.misc :as misc] 13 | [clojure.string :as str] 14 | [clojure.edn :as edn] 15 | [cheshire.core :as json]) 16 | (:import (java.io File) 17 | (java.util UUID))) 18 | 19 | (defn- tmp-filename 20 | [ext] 21 | (str (File/createTempFile (str (UUID/randomUUID)) (str "." (name ext))))) 22 | 23 | (defn- json-spec-file 24 | "Given a vega-lite spec json file, this automatically allows examples 25 | from https://vega.github.io/vega-lite/examples/ to just work. It does this 26 | by detecting if a data url starts with 'data/' and then fully expanding the url." 27 | [input-file format] 28 | (let [input-body (try 29 | (json/parse-string (slurp input-file)) 30 | (catch Exception e 31 | (cli/error "Unexpected failure while parsing json file:\n" (str e)))) 32 | url (get-in input-body ["data" "url"])] 33 | (if (and url (str/starts-with? url "data/")) 34 | (doto (tmp-filename format) 35 | (spit (json/generate-string 36 | (assoc-in input-body 37 | ["data" "url"] 38 | (str "https://raw.githubusercontent.com/vega/vega/master/docs/" 39 | url))))) 40 | input-file))) 41 | 42 | (defn- input-file-from-stdin 43 | [format] 44 | (let [body (slurp System/in)] 45 | (doto (tmp-filename format) 46 | (spit body)))) 47 | 48 | (defn- get-output-file [input-file {:keys [output-file format]}] 49 | (or output-file 50 | (str (or (some-> input-file 51 | (str/replace-first #"\.\w+$" "")) 52 | "output") 53 | "." format))) 54 | 55 | (defn- create-visualization 56 | "Parts of this were taken from oz's vega-cli - 57 | https://github.com/metasoarous/oz/blob/a7881779d11a9b3ed1893a023aed6aed28f65592/src/clj/oz/core.clj#L366" 58 | [[input-file] {:keys [open format] :as opts}] 59 | (let [output-file (get-output-file input-file opts) 60 | input-file_ (if (nil? input-file) (input-file-from-stdin format) input-file) 61 | spec-file (if (str/ends-with? input-file_ ".edn") 62 | (doto (tmp-filename format) 63 | (spit (json/generate-string (edn/read-string (slurp input-file_))))) 64 | (json-spec-file input-file_ format))] 65 | 66 | (misc/sh (str "vl2" format) spec-file output-file 67 | ;; Have to override this b/c commands like vl2png don't exit 68 | ;; properly when they fail 69 | {:is-error-fn #(or (not= 0 (:exit %)) 70 | (str/includes? (:err %) "Error"))}) 71 | (when open 72 | (misc/sh "open" output-file)) 73 | {:input-file spec-file :output-file output-file})) 74 | 75 | (defn -main [{:keys [summary arguments options]}] 76 | (cond 77 | (or (:help options) (> (count arguments) 1)) 78 | (cli/print-summary " FILE\nReads json from stdin if no file given." summary) 79 | 80 | :else (create-visualization arguments options))) 81 | 82 | (def cli-options 83 | [["-h" "--help"] 84 | ["-o" "--open"] 85 | ["-F" "--format FORMAT" 86 | :validate [#{"png" "pdf" "svg"} "Must be png, pdf or svg"] 87 | :default "png"] 88 | ["-f" "--output-file OUTPUT_FILE" 89 | :default-desc "Defaults to FILE.FORMAT"]]) 90 | 91 | (cli/run-command -main *command-line-args* cli-options) 92 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks 2 | (:require [babashka.tasks :refer [shell run]] 3 | [babashka.fs :as fs] 4 | [cheshire.core :as json] 5 | [clojure.edn :as edn] 6 | [clojure.data :as data] 7 | [clojure.string :as str])) 8 | 9 | (defn brew-search-info 10 | [args] 11 | (let [results (-> (shell {:out :string} 12 | (str/join " " (into ["brew" "search"] 13 | args))) 14 | :out 15 | str/split-lines) 16 | brew-packages (remove (fn [x] (re-find (re-pattern "(Casks|Formulae)$") x)) results)] 17 | (shell (str "brew info " (str/join " " brew-packages))))) 18 | 19 | (defn json= 20 | "Useful when diff fails you due to random sort of json files produced differently" 21 | [args] 22 | (apply = (map (fn [x] (-> x slurp json/parse-string)) args))) 23 | 24 | (defn edn= 25 | "Useful when diff fails you due to random sort of edn files produced differently" 26 | [& args] 27 | (apply = (map (fn [x] (-> x slurp edn/read-string)) args))) 28 | 29 | (defn data-diff 30 | "Runs data/diff on two edn files" 31 | [file1 file2] 32 | (prn (apply data/diff (map (fn [x] (-> x slurp edn/read-string)) [file1 file2])))) 33 | 34 | (def every-dir-shell-cli-options 35 | [["-d" "--directory DIR" "Directories" 36 | :id :directories 37 | :default-fn (fn [_x] [(System/getenv "PWD")]) 38 | :validate [fs/directory? "Must be a directory"] 39 | :multi true 40 | :update-fn conj]]) 41 | 42 | (defn every-dir-shell 43 | [parsed-args] 44 | (let [{:keys [options arguments]} parsed-args 45 | args (str/join " " arguments)] 46 | (doseq [dir (:directories options)] 47 | (println "=== Directory -" dir "===") 48 | (shell {:dir dir} args) 49 | (println "")))) 50 | 51 | (defn help 52 | [args] 53 | ;; Would rather not dip into explicit env to determine tasks 54 | (let [tasks (-> (or (System/getenv "BB_EDN") "bb.edn") 55 | slurp 56 | edn/read-string 57 | :tasks) 58 | task (first args)] 59 | (if-let [task-map (get tasks (symbol task))] 60 | (println (format "%s\n\nUsage: bb %s%s" 61 | (:doc task-map) 62 | task 63 | (if-let [usage (:usage task-map)] 64 | (str " " usage) 65 | ""))) 66 | (do 67 | (println "Error: No such task exists") 68 | (System/exit 1))))) 69 | 70 | (defn repl 71 | [args] 72 | (let [task (symbol (first args))] 73 | (binding [*command-line-args* (rest args)] 74 | ;; Assumes task stdout is edn 75 | #_:clj-kondo/ignore 76 | (def ^:private result (edn/read-string (with-out-str (run task))))) 77 | ;; Used to use clojure.main/repl but this allows for in-editor repl 78 | ((requiring-resolve 'clojure.core.server/start-server) 79 | {:port 5555 80 | :name "bb-task" 81 | :accept 'clojure.core.server/repl}) 82 | ((requiring-resolve 'clojure.core.server/repl)))) 83 | 84 | (defn do-sh 85 | "Runs shell command for each element on stdin seq" 86 | [& args] 87 | (run! #(apply shell (concat args [%])) (edn/read *in*))) 88 | 89 | (defn wc-l 90 | "Filter files by max loc" 91 | [max-loc & args] 92 | (let [max-loc_ (Integer/parseInt max-loc)] 93 | (->> (apply shell {:out :string} "wc -l" args) 94 | :out 95 | str/split-lines 96 | butlast 97 | (map #(let [[_ loc file] (str/split % #"\s+" 4)] 98 | {:loc (Integer/parseInt loc) :file file})) 99 | (sort-by :loc) 100 | (filter #(<= (:loc %) max-loc_)) 101 | (map :file) 102 | prn))) 103 | 104 | (defn grep-result-frequencies 105 | "Takes piped in grep output and prints out frequency by file counts" 106 | [] 107 | (let [results (->> *in* 108 | slurp 109 | str/split-lines 110 | (map #(second (re-find #"(\S+):" %))) 111 | frequencies 112 | (sort-by second >))] 113 | (doseq [r results] 114 | (apply println r)))) 115 | 116 | (comment 117 | (-> result) 118 | ) 119 | -------------------------------------------------------------------------------- /bin/bb-cli-test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vim: set filetype=clojure: 3 | 4 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 5 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 6 | 7 | (ns bb-cli-test 8 | (:require [clojure.string :as str] 9 | [clojure.java.shell :as shell] 10 | [clojure.pprint :as pprint] 11 | [clojure.java.io :as io] 12 | [cldwalker.bb-clis.cli :as cli])) 13 | 14 | (def default-test-format 15 | "(deftest %s 16 | (let [cmd-results (shell/sh %s) 17 | expected-results (-> (io/resource \"%s\") 18 | slurp 19 | edn/read-string)] 20 | (is (= (:exit expected-results) (:exit cmd-results))) 21 | (is (= (:out expected-results) (:out cmd-results))) 22 | (is (= (:err expected-results) (:err cmd-results))))) 23 | ") 24 | 25 | (defn- generate-test [test-name fixture-file cmd-vec] 26 | (format default-test-format 27 | test-name 28 | (str/join " " (mapv #(str "\"" % "\"") cmd-vec)) 29 | fixture-file)) 30 | 31 | (defn- record-test 32 | "Records test by running the given command and args and saving its output to a 33 | fixture file. If command and args is a single argument containing whitespace, 34 | the string is automatically wrapped in `bash -c`. This is useful for testing 35 | multiple commands e.g. `cat foo.edn | baz`." 36 | [cmd-and-args {:keys [test file verbose]}] 37 | (assert (and test file) "--test and --file are required options") 38 | (let [fixture-file (-> file 39 | (str/replace-first #"^test/" "test/resources/") 40 | (str/replace-first #"\.clj$" (str "/" test ".edn"))) 41 | ;; Wrap in bash if multiple commands 42 | cmd-and-args_ (if (and (= 1 (count cmd-and-args)) 43 | (str/includes? (first cmd-and-args) " ")) 44 | (concat ["bash" "-c"] cmd-and-args) 45 | cmd-and-args) 46 | cmd-output (apply shell/sh cmd-and-args_)] 47 | (when verbose 48 | (println "Ran command:" cmd-and-args "\nSTDOUT:") 49 | (println (:out cmd-output))) 50 | (when-not (-> fixture-file io/file .getParentFile .exists) 51 | (io/make-parents fixture-file)) 52 | (pprint/pprint cmd-output (io/writer fixture-file)) 53 | (println "Successfully recorded test!") 54 | 55 | {:fixture-file fixture-file 56 | :cmd-and-args cmd-and-args_})) 57 | 58 | (def default-ns-format 59 | "(ns %s 60 | (:require [clojure.test :refer [deftest is]] 61 | [clojure.edn :as edn] 62 | [clojure.java.io :as io] 63 | [clojure.java.shell :as shell])) 64 | ") 65 | 66 | (defn- record-and-add-test 67 | "Records output of cmd and args. Then adds a test to the given file and points it to 68 | the recorded fixture file." 69 | [args {:keys [test file] :as options}] 70 | (let [{:keys [fixture-file cmd-and-args]} (record-test args options) 71 | new-test-string (generate-test test 72 | (str/replace-first fixture-file "test/resources/" "") 73 | cmd-and-args)] 74 | (when-not (.exists (io/file file)) 75 | (spit file 76 | (format default-ns-format (-> file 77 | (str/replace #"(^test/|\.clj$)" "") 78 | (str/replace "/" ".") 79 | (str/replace "_" "-")))) 80 | (println "Created test file!")) 81 | (spit file (str "\n" new-test-string) :append true) 82 | (println "Successfully added test!"))) 83 | 84 | (def cli-options 85 | [["-h" "--help"] 86 | ["-f" "--file FILE"] 87 | ["-v" "--verbose"] 88 | ["-t" "--test TEST"]]) 89 | 90 | (defn -main 91 | [{:keys [summary arguments options]}] 92 | (cond 93 | (or (:help options) (zero? (count arguments))) 94 | (cli/print-summary " add|record &COMMAND-AND-ARGS" summary) 95 | 96 | (= "add" (first arguments)) (record-and-add-test (rest arguments) options) 97 | (= "record" (first arguments)) (do (record-test (rest arguments) options) nil) 98 | :else (cli/error "Unknown subcommand given"))) 99 | 100 | (when (= *file* (System/getProperty "babashka.file")) 101 | (cli/run-command -main *command-line-args* cli-options :in-order true :strict true)) 102 | -------------------------------------------------------------------------------- /bin/bb-aws: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vim: set filetype=clojure: 3 | ;; Usage 4 | ;; ===== 5 | ;; Run aws commands the same way the awscli does e.g. 6 | ;; - bb-aws s3api list-buckets 7 | ;; - bb-aws s3api get-bucket-acl --bucket X 8 | ;; Note: Does not support commands with non-option arguments e.g. 9 | ;; bb-aws lambda invoke --function-name FN OUT_FILE 10 | ;; Drop into a repl with command's result stored in #'result 11 | ;; - bb-aws --repl s3api list-buckets 12 | #_:clj-kondo/ignore 13 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 14 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 15 | 16 | (ns bb-aws 17 | {:clj-kondo/config 18 | '{:linters {:inline-def {:level :off}}}} 19 | (:require [babashka.pods :as pods] 20 | [cldwalker.bb-clis.cli :as cli] 21 | [clojure.string :as str] 22 | [clojure.main :as main] 23 | [clojure.pprint :as pprint])) 24 | 25 | (pods/load-pod 'org.babashka/aws "0.0.6") 26 | (require '[pod.babashka.aws :as aws]) 27 | 28 | (defn- clojure-case->camel-case 29 | [s] 30 | (->> (str/split s #"-") 31 | (map str/capitalize) 32 | str/join)) 33 | 34 | (def aws-command->aws-api-api 35 | {:s3api :s3}) 36 | 37 | (defn- parse-options-and-args [all-args] 38 | (loop [options {} 39 | args all-args] 40 | (if (some-> (first args) (str/starts-with? "--")) 41 | (recur (assoc options 42 | (str/replace-first (first args) #"^--" "") (second args)) 43 | (drop 2 args)) 44 | [options args]))) 45 | 46 | (defn invoke-aws-operation [[cmd subcmd & args] {:keys [region debug]}] 47 | (let [api (aws-command->aws-api-api (keyword cmd) (keyword cmd)) 48 | op (keyword (clojure-case->camel-case subcmd)) 49 | client (aws/client {:api api :region region}) 50 | [options _] (parse-options-and-args args) 51 | request (update-keys options 52 | #(-> % 53 | clojure-case->camel-case 54 | (str/replace-first #"[A-Z]" str/lower-case) 55 | keyword))] 56 | (when debug 57 | (println "API:" api) 58 | (println "OP:" op) 59 | (println "REQUEST:" request)) 60 | (try (aws/invoke client {:op op :request request}) 61 | ;; Noisy failure if the wrong number of args given 62 | ;; See https://github.com/babashka/pod-babashka-aws/blob/d7c4306bdd74ecde16ee3766825b71facb325588/src/pod/babashka/aws.clj#L120-L123 for noise. 63 | ;; TODO: Actually catch the noisy failure 64 | (catch Throwable e 65 | (println "\nError: Unexpected failure occurred. See error message above.") 66 | (when debug (println "Exception:" e)))))) 67 | 68 | (defn- auto-result [result subcmd] 69 | (cond 70 | ;; e.g. aws ecr describe-repositores 71 | (and (map? result) (= 1 (count result))) 72 | (-> result vals first) 73 | ;; e.g. aws s3 list-buckets 74 | (let [kw (some-> (str/split subcmd #"-") 75 | last 76 | str/capitalize 77 | keyword)] 78 | (contains? result kw)) 79 | (result (some-> (str/split subcmd #"-") 80 | last 81 | str/capitalize 82 | keyword)) 83 | :else 84 | result)) 85 | 86 | (def cli-options 87 | [["-h" "--help"] 88 | ["-d" "--debug"] 89 | ["-R" "--repl" "Drop into repl with operation result set to #'result"] 90 | ["-a" "--auto-result" "Drills into result one level if main entity to list is detected"] 91 | ["-r" "--region REGION" 92 | :default-desc "$AWS_REGION or us-east-2" 93 | :default-fn (fn [_] (or (System/getenv "AWS_REGION") "us-east-2"))]]) 94 | 95 | (defn -main [{:keys [summary arguments options]}] 96 | (if (or (:help options) (zero? (count arguments))) 97 | (cli/print-summary " COMMAND SUBCOMMAND [SUBCOMMAND-OPTIONS]" summary) 98 | (let [result (invoke-aws-operation arguments options) 99 | result_ (if (:auto-result options) 100 | (auto-result result (second arguments)) 101 | result)] 102 | (pprint/pprint result_) 103 | (when (:repl options) 104 | (def result result_) 105 | (main/repl))))) 106 | 107 | (when (= *file* (System/getProperty "babashka.file")) 108 | (cli/run-command -main *command-line-args* cli-options :in-order true)) 109 | 110 | (comment 111 | (def s3-client (aws/client {:api :s3 :region "us-east-1"})) 112 | (aws/doc s3-client :ListBuckets) 113 | (aws/invoke s3-client {:op :ListBuckets}) 114 | ) 115 | -------------------------------------------------------------------------------- /test/bin/bb_table_test.clj: -------------------------------------------------------------------------------- 1 | (ns bin.bb-table-test 2 | "All these tests were run in an environment with table clojar installed" 3 | (:require [clojure.test :refer [deftest is]] 4 | [clojure.edn :as edn] 5 | [clojure.java.io :as io] 6 | [clojure.java.shell :as shell])) 7 | 8 | (deftest help-option 9 | (let [cmd-results (shell/sh "bb-table" "-h") 10 | expected-results (-> (io/resource "bin/bb_table_test/help-option.edn") 11 | slurp 12 | edn/read-string)] 13 | (is (= (:exit expected-results) (:exit cmd-results))) 14 | (is (= (:out expected-results) (:out cmd-results))) 15 | (is (= (:err expected-results) (:err cmd-results))))) 16 | 17 | (deftest map-stdin-argument 18 | (let [cmd-results (shell/sh "bash" "-c" "echo {:a 1 :b 2} | bb-table") 19 | expected-results (-> (io/resource "bin/bb_table_test/map-stdin-argument.edn") 20 | slurp 21 | edn/read-string)] 22 | (is (= (:exit expected-results) (:exit cmd-results))) 23 | (is (= (:out expected-results) (:out cmd-results))) 24 | (is (= (:err expected-results) (:err cmd-results))))) 25 | 26 | (deftest vec-stdin-argument 27 | (let [cmd-results (shell/sh "bash" "-c" "echo '[[:a :b] [:d :e]]' | bb-table") 28 | expected-results (-> (io/resource "bin/bb_table_test/vec-stdin-argument.edn") 29 | slurp 30 | edn/read-string)] 31 | (is (= (:exit expected-results) (:exit cmd-results))) 32 | (is (= (:out expected-results) (:out cmd-results))) 33 | (is (= (:err expected-results) (:err cmd-results))))) 34 | 35 | (deftest reverse-sort-option 36 | (let [cmd-results (shell/sh "bash" "-c" "echo {:a 1 :b 2} | bb-table -r value") 37 | expected-results (-> (io/resource "bin/bb_table_test/reverse-sort-option.edn") 38 | slurp 39 | edn/read-string)] 40 | (is (= (:exit expected-results) (:exit cmd-results))) 41 | (is (= (:out expected-results) (:out cmd-results))) 42 | (is (= (:err expected-results) (:err cmd-results))))) 43 | 44 | (deftest sort-option 45 | (let [cmd-results (shell/sh "bash" "-c" "echo {:a 1 :c 3 :b 2} | bb-table -s value") 46 | expected-results (-> (io/resource "bin/bb_table_test/sort-option.edn") 47 | slurp 48 | edn/read-string)] 49 | (is (= (:exit expected-results) (:exit cmd-results))) 50 | (is (= (:out expected-results) (:out cmd-results))) 51 | (is (= (:err expected-results) (:err cmd-results))))) 52 | 53 | (deftest number-rows-option 54 | (let [cmd-results (shell/sh "bash" "-c" "echo {:a 1 :b 2} | bb-table -n") 55 | expected-results (-> (io/resource "bin/bb_table_test/number-rows-option.edn") 56 | slurp 57 | edn/read-string)] 58 | (is (= (:exit expected-results) (:exit cmd-results))) 59 | (is (= (:out expected-results) (:out cmd-results))) 60 | (is (= (:err expected-results) (:err cmd-results))))) 61 | 62 | (deftest print-headers-option 63 | (let [cmd-results (shell/sh "bash" "-c" "echo '[{:a 1 :b 1} {:a 2}]' | bb-table -H") 64 | expected-results (-> (io/resource "bin/bb_table_test/print-headers-option.edn") 65 | slurp 66 | edn/read-string)] 67 | (is (= (:exit expected-results) (:exit cmd-results))) 68 | (is (= (:out expected-results) (:out cmd-results))) 69 | ;; Locally this is fine but fails on CI as something tries to download clj 1.10.933 70 | #_(is (= (:err expected-results) (:err cmd-results))))) 71 | 72 | (deftest columns-option-with-abbreviations 73 | (let [cmd-results (shell/sh "bash" "-c" "echo '[{:apple 1 :banana 1} {:apple 2 :orange 1}]' | bb-table -c a,o") 74 | expected-results (-> (io/resource "bin/bb_table_test/columns-option-with-abbreviations.edn") 75 | slurp 76 | edn/read-string)] 77 | (is (= (:exit expected-results) (:exit cmd-results))) 78 | (is (= (:out expected-results) (:out cmd-results))) 79 | (is (= (:err expected-results) (:err cmd-results))))) 80 | 81 | (deftest columns-option-with-header-numbers 82 | (let [cmd-results (shell/sh "bash" "-c" "echo '[{:apple 1 :banana 1} {:apple 2 :orange 1}]' | bb-table -c 1,2") 83 | expected-results (-> (io/resource "bin/bb_table_test/columns-option-with-header-numbers.edn") 84 | slurp 85 | edn/read-string)] 86 | (is (= (:exit expected-results) (:exit cmd-results))) 87 | (is (= (:out expected-results) (:out cmd-results))) 88 | (is (= (:err expected-results) (:err cmd-results))))) 89 | 90 | (deftest file-option 91 | (let [cmd-results (shell/sh "bb-table" "-f" "test/resources/bb-table-file-input.edn") 92 | expected-results (-> (io/resource "bin/bb_table_test/file-option.edn") 93 | slurp 94 | edn/read-string)] 95 | (is (= (:exit expected-results) (:exit cmd-results))) 96 | (is (= (:out expected-results) (:out cmd-results))) 97 | (is (= (:err expected-results) (:err cmd-results))))) 98 | 99 | (deftest simple-vec-errors 100 | (let [cmd-results (shell/sh "bash" "-c" "echo '[:a :b]' |bb-table") 101 | expected-results (-> (io/resource "bin/bb_table_test/simple-vec-errors.edn") 102 | slurp 103 | edn/read-string)] 104 | (is (= (:exit expected-results) (:exit cmd-results))) 105 | (is (= (:out expected-results) (:out cmd-results))) 106 | (is (= (:err expected-results) (:err cmd-results))))) 107 | -------------------------------------------------------------------------------- /bin/bb-table: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vim: set filetype=clojure: 3 | ;; Prints an ascii table for streamed in edn or edn file 4 | ;; Edn must be a map or a collection of collections 5 | 6 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 7 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 8 | 9 | (ns bb-table 10 | (:require [clojure.edn :as edn] 11 | [clojure.string :as str] 12 | [babashka.deps :as deps] 13 | [cldwalker.bb-clis.cli :as cli] 14 | [cldwalker.bb-clis.cli.misc :as misc])) 15 | 16 | (defn- get-columns 17 | "Extract columns from rows. Defaults to searching across all rows" 18 | [rows] 19 | (distinct (mapcat identity (map keys rows)))) 20 | 21 | (defn- selected-columns-int 22 | [columns columns-filter] 23 | (let [selected (mapv #(Integer/parseInt %) 24 | (str/split columns-filter #"\s*,\s*"))] 25 | (mapv #(nth columns (dec %)) selected))) 26 | 27 | (defn- selected-columns-str 28 | [columns columns-filter] 29 | (->> (str/split columns-filter #"\s*,\s*") 30 | (map #(filter (fn [c] (str/starts-with? (name c) %)) columns)) 31 | (apply concat))) 32 | 33 | (defn- selected-columns 34 | [columns columns-filter] 35 | (if (re-find #"^\d" columns-filter) 36 | (selected-columns-int columns columns-filter) 37 | (selected-columns-str columns columns-filter))) 38 | 39 | (defn- print-table 40 | [columns rows style] 41 | (deps/add-deps '{:deps {table/table {:mvn/version "0.5.0"}}}) 42 | ((requiring-resolve 'table.core/table) rows :fields columns :style style)) 43 | 44 | (defn- print-rows* [rows {:keys [columns-filter style number-rows]}] 45 | (if (and (coll? rows) (-> rows first map?)) 46 | (let [columns (get-columns rows) 47 | columns_ (if number-rows 48 | (into [:number] (remove #(= % :number) columns)) 49 | columns) 50 | columns__ (if columns-filter 51 | (selected-columns columns_ columns-filter) 52 | columns_)] 53 | (print-table columns__ rows style)) 54 | (cli/error "Input is not an EDN collection of maps"))) 55 | 56 | (defn- get-sort-key [rows query] 57 | (let [columns (get-columns rows)] 58 | (or (first (selected-columns-str columns query)) 59 | ;; Since no matches, auto-try keyword 60 | (first (selected-columns-str columns (keyword query))) 61 | query))) 62 | 63 | (defn- possibly-sort-rows [rows sort reverse-sort] 64 | (cond 65 | sort 66 | (let [sort-key (get-sort-key rows sort)] 67 | (sort-by #(get % sort-key) rows)) 68 | 69 | reverse-sort 70 | (let [sort-key (get-sort-key rows reverse-sort)] 71 | (sort-by #(get % sort-key) 72 | ;; Handles int and string 73 | #(compare %2 %1) 74 | rows)) 75 | 76 | :else rows)) 77 | 78 | (defn- process-rows 79 | "Currently processes all row values even when we know certain columns are 80 | selected. Future improvement would be to only operate on selected column 81 | values" 82 | [rows {:keys [sort reverse-sort number-rows]}] 83 | (let [rows_ 84 | (if number-rows 85 | (mapv #(assoc %1 :number %2) 86 | rows 87 | (range 1 (inc (count rows)))) 88 | rows)] 89 | (possibly-sort-rows rows_ sort reverse-sort))) 90 | 91 | (defn- header-counts [rows] 92 | (let [columns (get-columns rows) 93 | column-counts (->> rows 94 | (map #(zipmap (keys %) (repeat 1))) 95 | (apply merge-with +))] 96 | (mapv (fn [col] 97 | {:column col :count (column-counts col 0)}) 98 | columns))) 99 | 100 | (defn print-rows 101 | "Prints rows in a table, handling a couple different data structures as input" 102 | [arg {:keys [print-headers] :as options}] 103 | (let [arg_ (if print-headers 104 | (header-counts arg) 105 | arg) 106 | rows (cond 107 | (map? arg_) (map (fn [[k v]] {"key" k "value" v}) arg_) 108 | 109 | ((some-fn vector? list? set?) (first arg_)) 110 | (map (fn [coll] (zipmap (range) coll)) arg_) 111 | 112 | :else arg_) 113 | processed-rows (process-rows rows options)] 114 | (print-rows* processed-rows options))) 115 | 116 | (defn -main [{:keys [summary _arguments options]}] 117 | (cond 118 | (:help options) (cli/print-summary "\nReads from stdin if no arguments given" summary) 119 | (:file options) (print-rows (-> (:file options) slurp edn/read-string) 120 | options) 121 | :else (print-rows (first (misc/read-stdin-edn)) 122 | options))) 123 | 124 | (def table-styles 125 | "Table styles that can be passed to table library via :style" 126 | #{:plain :org :unicode :github-markdown}) 127 | 128 | (defn- unalias-choice [choices query] 129 | (first 130 | (filter #(str/starts-with? (name %) query) 131 | choices))) 132 | 133 | (def cli-options 134 | [["-h" "--help"] 135 | ["-f" "--file FILE"] 136 | ["-c" "--columns-filter FILTER" "Select columns by comma delimited substring matches or numbers from -H"] 137 | ["-H" "--print-headers" "Print column headers and their counts across rows"] 138 | ["-n" "--number-rows"] 139 | ;; Sort and reverse sort don't handle keywords yet 140 | ["-s" "--sort COLUMN" "Sort by column"] 141 | ["-r" "--reverse-sort COLUMN" "Reverse sort by column"] 142 | ["-S" "--style STYLE" "Table style when using table clojar. Available styles are :plain, :org, :unicode and :github-markdown. Default is :plain." 143 | :default :plain 144 | :parse-fn #(keyword (unalias-choice table-styles %)) 145 | :validate [table-styles]]]) 146 | 147 | (cli/run-command -main *command-line-args* cli-options) 148 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :min-bb-version "1.0.166" 3 | :deps 4 | {com.github.cldwalker/bb-dialog 5 | {:git/sha "e10f2d8461b3de4c521c1fcf71552f38c47f33da"}} 6 | 7 | :tasks 8 | {:requires ([cldwalker.bb-clis.tasks.util :as tasks-util]) 9 | 10 | :enter (let [task-map (current-task)] 11 | (def ^:dynamic *parsed-args* 12 | (tasks-util/parse-options task-map)) 13 | (tasks-util/check-for-required-arguments *parsed-args* task-map)) 14 | 15 | help 16 | {:doc "Print a task's help" 17 | :usage "TASK" 18 | :requires ([cldwalker.bb-clis.tasks :as tasks]) 19 | :task (tasks/help *command-line-args*)} 20 | 21 | repl 22 | ;; Couldn't just reference whole task as a map as `bb tasks` failed 23 | {:doc "Pull up socket repl with #'tasks/result bound to result of given task and args" 24 | :usage "TASK [& ARGS]" 25 | :requires ([cldwalker.bb-clis.tasks :as tasks]) 26 | :task (tasks/repl *command-line-args*)} 27 | 28 | every-dir-shell 29 | {:doc "Run shell command on every dir" 30 | :requires ([cldwalker.bb-clis.tasks :as tasks]) 31 | :usage "CMD [&ARGS]" 32 | :options tasks/every-dir-shell-cli-options 33 | :cli-options [:in-order true] 34 | :task (tasks/every-dir-shell *parsed-args*)} 35 | 36 | json= 37 | {:doc "Check equality of given json files" 38 | :usage "[& FILES]" 39 | :requires ([cldwalker.bb-clis.tasks :as tasks]) 40 | :task (prn (tasks/json= *command-line-args*))} 41 | 42 | edn= 43 | {:doc "Check equality of given edn files" 44 | :usage "[& FILES]" 45 | :requires ([cldwalker.bb-clis.tasks :as tasks]) 46 | :task cldwalker.bb-clis.tasks/edn=} 47 | 48 | data-diff 49 | cldwalker.bb-clis.tasks/data-diff 50 | 51 | brew-search-info 52 | {:doc "Runs a brew info on all brew search results" 53 | :usage "SEARCH" 54 | :requires ([cldwalker.bb-clis.tasks :as tasks]) 55 | :task (tasks/brew-search-info *command-line-args*)} 56 | 57 | update-gitlib 58 | {:doc "Update git library sha in deps.edn" 59 | :extra-deps {borkdude/rewrite-edn {:mvn/version "0.1.0"}} 60 | :requires ([cldwalker.bb-clis.tasks.rewrite-edn :as rewrite-edn]) 61 | :usage "GITLIB SHA" 62 | :task (rewrite-edn/update-gitlib *command-line-args*)} 63 | 64 | rdf-data 65 | {:doc "Fetches rdf contents of a url using rdf-dereference" 66 | :usage "URL" 67 | :requires ([cldwalker.bb-clis.tasks.rdf-data :as rdf-data] 68 | [clojure.pprint :as pprint]) 69 | :options rdf-data/rdf-data-cli-options 70 | :task (pprint/pprint (rdf-data/rdf-data *command-line-args* (:options *parsed-args*)))} 71 | 72 | clone 73 | {:doc "Clone a git url" 74 | :usage "URL" 75 | :extra-deps {org.clojure/tools.gitlibs {:mvn/version "2.4.172"}} 76 | :requires ([cldwalker.bb-clis.tasks.git :as git]) 77 | :options git/clone-cli-options 78 | :task (git/clone *parsed-args*)} 79 | 80 | var-sizes 81 | {:doc "Print vars with largest LOCs" 82 | :usage "[& SOURCE-PATHS]" 83 | :requires ([cldwalker.bb-clis.tasks.clj-kondo :as clj-kondo]) 84 | :task (prn (clj-kondo/var-sizes *command-line-args*))} 85 | 86 | var-meta 87 | {:doc "Prints var metadata for source-paths. Defaults to src/ for source-paths" 88 | :usage "[& SOURCE-PATHS]" 89 | :requires ([cldwalker.bb-clis.tasks.clj-kondo :as clj-kondo]) 90 | :task (prn (clj-kondo/var-meta *command-line-args*))} 91 | 92 | ns-meta 93 | {:doc "Prints ns metadata for source-paths. Defaults to src/ for source-paths" 94 | :usage "[& SOURCE-PATHS]" 95 | :requires ([cldwalker.bb-clis.tasks.clj-kondo :as clj-kondo]) 96 | :task (prn (clj-kondo/ns-meta *command-line-args*))} 97 | 98 | ;; From @borkdude 99 | http-server 100 | {:extra-deps {babashka/http-server 101 | {:git/url "https://github.com/babashka/http-server" 102 | :git/sha "d1f01b47492dec245f3f6d297849ee038243ee86"}} 103 | :doc "Run a basic http server" 104 | :task babashka.http-server/-main} 105 | 106 | ;; From https://blog.michielborkent.nl/babashka-test-runner.html 107 | test 108 | {:extra-paths ["test" 109 | ;; This is specific to bb-clis but this task can still be 110 | ;; used for any bb repo 111 | "test/resources"] 112 | :extra-deps {io.github.cognitect-labs/test-runner 113 | {:git/tag "v0.5.1" :git/sha "dfb30dd"}} 114 | :task (exec 'cognitect.test-runner.api/test) 115 | :exec-args {:dirs ["test"]} 116 | :doc "Run tests with cognitect test runner" 117 | :org.babashka/cli {:coerce {:nses [:symbol] 118 | :vars [:symbol]}}} 119 | 120 | ns-in-dir 121 | {:extra-deps {org.clojure/tools.namespace {:git/url "https://github.com/babashka/tools.namespace" 122 | :git/sha "3625153ee66dfcec2ba600851b5b2cbdab8fae6c"}} 123 | :requires ([clojure.tools.namespace.find :as find] 124 | [babashka.fs :as fs]) 125 | :doc "Prints namespaces in dir" 126 | :usage "DIR" 127 | :task (prn (apply find/find-namespaces-in-dir (map fs/file *command-line-args*)))} 128 | 129 | var-sexp 130 | {:requires ([cldwalker.bb-clis.tasks.rewrite-clj :as rewrite-clj]) 131 | :doc "For given file, returns var sexp which is usually its value" 132 | :usage "VAR FILE" 133 | :task (prn (rewrite-clj/var-sexp *command-line-args*))} 134 | 135 | do-sh 136 | cldwalker.bb-clis.tasks/do-sh 137 | 138 | logseq:empty-files 139 | cldwalker.bb-clis.tasks.logseq/empty-files 140 | 141 | logseq:pages 142 | cldwalker.bb-clis.tasks.logseq/pages 143 | 144 | logseq:urls 145 | cldwalker.bb-clis.tasks.logseq/urls 146 | 147 | logseq:copy-entities 148 | cldwalker.bb-clis.tasks.logseq/copy-entities 149 | 150 | logseq:copy-files 151 | cldwalker.bb-clis.tasks.logseq/copy-files 152 | 153 | logseq:copy-common-pages 154 | cldwalker.bb-clis.tasks.logseq/copy-common-pages 155 | 156 | logseq:validate-common-pages 157 | cldwalker.bb-clis.tasks.logseq/validate-common-pages 158 | 159 | logseq:list-common-pages 160 | cldwalker.bb-clis.tasks.logseq/list-common-pages 161 | 162 | dev:rdf-equal 163 | cldwalker.bb-clis.tasks.rdf/rdf-equal 164 | 165 | dev:triples-count 166 | cldwalker.bb-clis.tasks.rdf/triples-count 167 | 168 | wc-l 169 | cldwalker.bb-clis.tasks/wc-l 170 | 171 | grep-result-frequencies 172 | cldwalker.bb-clis.tasks/grep-result-frequencies 173 | 174 | specter:example 175 | {:extra-deps {com.rpl/specter {:mvn/version "1.1.4"}} 176 | :task cldwalker.bb-clis.tasks.specter/example} 177 | 178 | gh:checkout-pr-branch 179 | cldwalker.bb-clis.tasks.github/checkout-pr-branch}} 180 | -------------------------------------------------------------------------------- /src/cldwalker/bb_clis/tasks/logseq.clj: -------------------------------------------------------------------------------- 1 | (ns cldwalker.bb-clis.tasks.logseq 2 | "Logseq related tasks" 3 | (:require [clojure.edn :as edn] 4 | [clojure.string :as str] 5 | [babashka.process :refer [process shell]] 6 | [babashka.fs :as fs] 7 | [bb-dialog.core :as bb-dialog] 8 | [clojure.pprint :as pprint] 9 | [cldwalker.bb-clis.cli.logseq :as logseq])) 10 | 11 | ;; Ast tasks 12 | ;; ========= 13 | (defn empty-files 14 | "Reads in stdin input from logseq-ast and prints files which have no content" 15 | ;; Empty is no nodes or one node with blank heading 16 | ;; Worked for all except one node with a property drawer that wasn't just timestamps 17 | [] 18 | (let [ast-files (edn/read *in*) 19 | empty-files (filter 20 | (fn [{:keys [ast]}] 21 | (let [h1 (filter (fn [x] (= "Heading" (ffirst x))) ast)] 22 | (or (zero? (count h1)) 23 | (and (= 1 (count h1)) (empty? (:title (second (ffirst h1)))))))) 24 | ast-files)] 25 | (prn (map :file empty-files)))) 26 | 27 | (defn- ast->pages [ast] 28 | (->> ast 29 | (mapcat (fn [node] 30 | (when (= "Heading" (ffirst node)) 31 | (filter #(and (= "Link" (first %)) 32 | (= "Page_ref" (-> % second :url first))) 33 | (-> node first second :title))))) 34 | (map #(-> % second :url second)))) 35 | 36 | (defn pages 37 | "Reads in stdin input from logseq-ast and prints pages" 38 | [] 39 | ;; ast-in can be a coll of asts by file or just the ast itself 40 | (let [ast-in (edn/read *in*) 41 | result-pages (if (and (coll? ast-in) (:file (first ast-in))) 42 | (keep #(when-let [pages (seq (ast->pages (:ast %)))] 43 | {:file (:file %) :pages pages}) 44 | ast-in) 45 | (ast->pages ast-in))] 46 | (prn result-pages))) 47 | 48 | (defn- ast->urls [ast] 49 | (->> ast 50 | (mapcat (fn [node] 51 | (when (= "Heading" (ffirst node)) 52 | (filter #(and (= "Link" (first %)) 53 | (not (contains? #{"Page_ref" "Block_ref"} (-> % second :url first)))) 54 | (-> node first second :title))))) 55 | (map (fn [x] 56 | ;; if a markdown label 57 | (if (str/starts-with? (-> x second :full_text) "[") 58 | (as-> (-> x second :url second) m 59 | (str (:protocol m) "://" (:link m))) 60 | (-> x second :full_text)))))) 61 | 62 | (defn urls 63 | "Reads in stdin input from logseq-ast and prints urls" 64 | [] 65 | ;; ast-in can be a coll of asts by file or just the ast itself 66 | (let [ast-in (edn/read *in*) 67 | result-urls (if (and (coll? ast-in) (:file (first ast-in))) 68 | (keep #(when-let [urls (seq (ast->urls (:ast %)))] 69 | {:file (:file %) :urls urls}) 70 | ast-in) 71 | (ast->urls ast-in))] 72 | (prn result-urls))) 73 | 74 | ;; Data migration 75 | ;; ============= 76 | (defn- search-graphs 77 | [dir & search-terms] 78 | (-> (process (str "find " dir " -name .git -prune -o -name '*' -print")) 79 | (process {:out :string} "grep -i -E" (str/join "|" search-terms)) 80 | deref 81 | :out 82 | str/split-lines)) 83 | 84 | (defn copy-entities 85 | "Copy mp entities to current graph" 86 | [& search-terms] 87 | (doseq [search-term search-terms] 88 | (if-let [ent (->> (shell {:out :string} "mp show -e" search-term "-r") 89 | :out 90 | edn/read-string 91 | :full-entity)] 92 | (let [camel-case #(str/replace % #"(?:^|-)(\S)" (fn [x] (str/capitalize (second x)))) 93 | [type & tags] (:tags ent) 94 | type' (let [tag (if (= "type" type) "class" type)] 95 | (format "[[%s]]" (camel-case tag)))] 96 | (spit (str "pages/" (camel-case search-term) ".md") 97 | (logseq/properties->block (cond-> {} 98 | (seq type') 99 | (assoc :type type') 100 | (and (some? (:url ent)) 101 | (not (str/includes? (:url ent) "https://notes.pinboard.in"))) 102 | (assoc :url (:url ent)) 103 | (seq (:description ent)) 104 | (assoc :desc (:description ent)) 105 | (seq tags) 106 | (merge (into {} 107 | (map #(str/split % #":") tags))))))) 108 | (println "Nothing found for" search-term)))) 109 | 110 | (defn copy-files 111 | "Copy files from one graph to another" 112 | [dir & search-terms] 113 | (let [files (apply search-graphs dir search-terms)] 114 | (if (= files [""]) 115 | (println "Error: No files found") 116 | (if-let [chosen-files 117 | (seq (bb-dialog/checklist "Copy files" 118 | "Choose files to copy between graphs" 119 | (map #(vector % "" false) files) {:out-fn str}))] 120 | (do 121 | (println (concat ["cp"] chosen-files ["pages"])) 122 | (apply shell (concat ["cp"] chosen-files ["pages"]))) 123 | (println "Error: No files copied since none chosen"))))) 124 | 125 | (defn copy-common-pages 126 | "Copies chosen common file to other destinations" 127 | [dir & search-terms] 128 | (let [files (apply search-graphs dir search-terms)] 129 | (if (= files [""]) 130 | (println "Error: No files found") 131 | (if-let [chosen-file (some-> (bb-dialog/menu "Copy common files" 132 | "Choose file to copy to other locations" 133 | (into {} (map #(vector % (-> % slurp str/split-lines count)) files)) 134 | :out-fn str) 135 | name)] 136 | (let [dest-files (remove #(= chosen-file %) files)] 137 | (println (concat ["cp -f" chosen-file] dest-files)) 138 | (doseq [dest-file dest-files] (fs/copy chosen-file dest-file {:replace-existing true}))) 139 | (println "Error: no files copied since none chosen"))))) 140 | 141 | (defn- get-graph-files 142 | "If one dir given, assume all children are graph dirs else assume each dir is 143 | a graph" 144 | [dirs] 145 | (if (= 1 (count dirs)) 146 | (fs/glob (first dirs) "*/pages/*") 147 | (mapcat #(fs/glob % "pages/*") dirs))) 148 | 149 | ;; TODO: Handle case-insensitive common pages like Clojure vs clojure 150 | (defn validate-common-pages 151 | "Find common pages across graphs and validate that they are equal" 152 | [& dirs] 153 | (let [not-equal (->> (get-graph-files dirs) 154 | (map str) 155 | (group-by fs/file-name) 156 | (filter #(> (count (val %)) 1)) 157 | (filter #(apply not= (map slurp (val %)))))] 158 | (if (empty? not-equal) 159 | (println "Success! All common pages are equal") 160 | (do 161 | (println "Doh! The following common pages are not equal:") 162 | (pprint/pprint not-equal))))) 163 | 164 | (defn list-common-pages 165 | "List common pages" 166 | [& dirs] 167 | (->> (get-graph-files dirs) 168 | (map str) 169 | (group-by fs/file-name) 170 | (filter #(> (count (val %)) 1)) 171 | (sort-by #(count (second %)) >) 172 | (map (fn [[k files]] 173 | (into {:file k} 174 | (map #(let [graph (or (keyword (second (re-find #"([^/]+)/pages/" %))) 175 | (throw (ex-info "Can't extract graph name" 176 | {:file %})))] 177 | [graph "x"]) 178 | files)))) 179 | pprint/print-table)) 180 | -------------------------------------------------------------------------------- /doc/scripts.md: -------------------------------------------------------------------------------- 1 | 2 | ## Scripts 3 | 4 | The following scripts are under bin/: 5 | 6 | * [bb-github-pr-for-commit](#bb-github-pr-for-commit) 7 | * [bb-github-repo](#bb-github-repo) 8 | * [bb-table](#bb-table) 9 | * [bb-project-clj](#bb-project-clj) 10 | * [bb-replace](#bb-replace) 11 | * [bb-vis](#bb-vis) 12 | * [bb-try](#bb-try) 13 | * [bb-ns-dep-tree](#bb-ns-dep-tree) 14 | * [bb-update-lein-dependency](#bb-update-lein-dependency) 15 | * [bb-logseq-convert](#bb-logseq-convert) 16 | * [bb-logseq-move-to-page](#bb-logseq-move-to-page) 17 | 18 | ### bb-github-pr-for-commit 19 | 20 | Prints github url of PR associated with a commit. It assumes a current directory's repository but any repository can be specified. See https://github.com/mislav/hub-api-utils/blob/master/bin/hub-pr-with-commit for an alternative implementation. 21 | 22 | #### Setup 23 | 24 | * Optional: To have this executable with private repositories, set `$GITHUB_USER` to your user and [create and set a $GITHUB_OAUTH_TOKEN](https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#non-web-application-flow) 25 | 26 | #### Usage 27 | 28 | ```sh 29 | # Prints url and opens url for osx 30 | $ ./bb-github-pr-for-commit -r atom/atom 0f521f1e8afbcaf73479ea93dd4c87d9187903cb 31 | "https://github.com/atom/atom/pull/20350" 32 | 33 | # Open url of current github repository 34 | $ ./bb-github-pr-for-commit SHA 35 | ``` 36 | 37 | ### bb-github-repo 38 | 39 | For the current github repo, open different repo urls e.g. commit or branch. Inspired by [this ruby version](https://github.com/cldwalker/irbfiles/blob/1fb97d84bcdf491325176d08e386468b12ece738/boson/commands/public/url/github.rb#L20-L50). 40 | 41 | To open a commit, `bb-github-repo -c SHA`. 42 | 43 | ### bb-table 44 | 45 | #### Usage 46 | Prints an ascii table given an EDN collection on stdin or as a file: 47 | 48 | ```sh 49 | $ echo '[{:a 4 :b 2} {:a 2 :c 3}]' | bb-table 50 | 51 | | :a | :b | :c | 52 | |----+----+----| 53 | | 4 | 2 | | 54 | | 2 | | 3 | 55 | 56 | $ bb-table -f something.edn 57 | ... 58 | ``` 59 | 60 | ### bb-project-clj 61 | Prints a project.clj defproject form as a map. Useful for manipulating this data on the commandline 62 | 63 | ```sh 64 | # Pretty prints a project's dependencies 65 | $ bb-project-clj -d 1 | bb -I '(-> *input* first :dependencies clojure.pprint/pprint)' 66 | ``` 67 | 68 | ### bb-replace 69 | Replaces a substring in a file using a regex to match it. Much less powerful 70 | than sed but more user friendly as it supports configuring and naming regexs. 71 | bb-replace reads configs found in ~/.bb-replace.edn and ./bb-replace.edn. See 72 | script for documentation on config format. 73 | 74 | ```sh 75 | # Use the default name replacements provided 76 | $ cp .bb-replace.edn ~/.bb-replace.edn 77 | 78 | # Navigate to a lein project and update project's version 79 | $ bb-replace lein-version 1.2.1 80 | 81 | # Navigate to a nodejs project and update project's version 82 | $ bb-replace json-version 2.1.1 83 | 84 | # A one-off regex can be used. This updates a map entry to false 85 | $ bb-replace -f project.clj -F '$1 %s' "(:pseudo-names)\s+\w+" false 86 | ``` 87 | 88 | ## bb-vis 89 | Generates [vega-lite](https://vega.github.io/vega-lite/) visualizations given vega-lite data as a file or on stdin. Data file can be json or edn. 90 | 91 | ### Setup 92 | Install vega-lite cmds with `yarn global add vega-lite --peer && yarn global add 93 | canvas`. This was last confirmed to work with vega-lite 5.1.0 and canvas 2.8.0. 94 | 95 | ### Usage 96 | 97 | Assume you have the following bar.edn: 98 | 99 | ```clj 100 | {:data 101 | {:values 102 | [{:a "A", :b 28} 103 | {:a "B", :b 55} 104 | {:a "C", :b 43} 105 | {:a "D", :b 91} 106 | {:a "E", :b 81} 107 | {:a "F", :b 53} 108 | {:a "G", :b 19} 109 | {:a "H", :b 87} 110 | {:a "I", :b 52}]}, 111 | :mark "bar", 112 | :encoding 113 | {:x {:field "a", :type "ordinal", :axis {:labelAngle 0}}, 114 | :y {:field "b", :type "quantitative"}}} 115 | ``` 116 | 117 | To generate this image and open it: `bb-vis bar.edn -o`. 118 | 119 | To generate this image as a pdf and open it: `bb-vis bar.edn -F pdf -o` 120 | 121 | Generated image: 122 | 123 | ![this](images/bar.png) 124 | 125 | Any of the [official examples](https://vega.github.io/vega-lite/examples/) can be generated by simply copying and pasting the data to stdin. For example, let's try the [2D histogram heatmap](https://vega.github.io/vega-lite/examples/rect_binned_heatmap.html): 126 | 127 | ```sh 128 | cat <SCREAMING_SNAKE_CASE :babashka-classpath)" 180 | :BABASHKA_CLASSPATH 181 | ``` 182 | 183 | Currently it only fetches the latest version of a library but I'm thinking of making a version optional. 184 | 185 | ## bb-ns-dep-tree 186 | 187 | Print the ns dependency tree for a given ns or file. For example, if we want to 188 | print the dependencies of 189 | [datascript.datafy](https://github.com/tonsky/datascript/blob/master/src/datascript/datafy.cljc): 190 | 191 | ```sh 192 | # On a local checkout of datascript 193 | $ bb-ns-dep-tree src/datascript/datafy.cljc 194 | datascript.datafy 195 | ├── clojure.core.protocols 196 | ├── datascript.pull-api 197 | │ ├── datascript.db ... 198 | │ └── datascript.pull-parser 199 | │ └── datascript.db ... 200 | ├── datascript.db 201 | │ ├── clojure.walk 202 | │ ├── clojure.data 203 | │ ├── me.tonsky.persistent-sorted-set 204 | │ └── me.tonsky.persistent-sorted-set.arrays 205 | └── datascript.impl.entity 206 | ├── clojure.core 207 | └── datascript.db ... 208 | 209 | # We can also print the cljs dependencies of the same ns 210 | $ bb-ns-dep-tree -l cljs datascript.datafy 211 | datascript.datafy 212 | ├── clojure.core.protocols 213 | ├── datascript.pull-api 214 | │ ├── datascript.db ... 215 | │ └── datascript.pull-parser 216 | │ └── datascript.db ... 217 | ├── datascript.db 218 | │ ├── goog.array 219 | │ ├── clojure.walk 220 | │ ├── clojure.data 221 | │ ├── me.tonsky.persistent-sorted-set 222 | │ ├── me.tonsky.persistent-sorted-set.arrays 223 | │ └── datascript.db ... 224 | └── datascript.impl.entity 225 | ├── cljs.core 226 | └── datascript.db ... 227 | ``` 228 | 229 | ### bb-update-lein-dependency 230 | 231 | Updates lein dependency of specified directories and optionally commits and pushes the change. For example, if I'm in the dependency's directory and I want to update two dependent projects to use its latest SHA, commit and git push: 232 | 233 | `bb-update-lein-dependency -c -d ../proj1 -d ../proj2 my-dep $(git rev-parse HEAD)`. 234 | 235 | ### bb-logseq-convert 236 | 237 | Given a url, returns auto populated properties as a logseq block. The properties 238 | are derived from the url's rdf data and what is converted to the logseq block is 239 | highly configurable. 240 | 241 | ### bb-logseq-move-to-page 242 | 243 | Given a logseq text block with a name property, moves that block to a logseq page. 244 | 245 | ## Logseq scripts 246 | 247 | Scripts starting with `bb-logseq-` are a group of scripts for use with 248 | [logseq](https://logseq.com/). My config of these scripts are in [this 249 | directory](https://github.com/cldwalker/dotfiles/tree/master/.bb-logseq). 250 | -------------------------------------------------------------------------------- /bin/bb-logseq-convert: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ; vim: set filetype=clojure: 3 | ; Converts a url into a set of markdown properties for use 4 | ; as a multi-line logseq block 5 | 6 | ;; Setup: npm install -g rdf-dereference 7 | 8 | (deps/add-deps '{:deps {io.github.cldwalker/bb-clis {:git/sha "c5da64153fb29e2f3fa807df4228b6e434f00fcd"}}}) 9 | ; (deps/add-deps {:deps {'io.github.cldwalker/bb-clis {:local/root (str (fs/parent (fs/parent *file*)))}}}) 10 | 11 | (ns bb-logseq-convert 12 | (:require [cldwalker.bb-clis.cli :as cli] 13 | [cldwalker.bb-clis.cli.logseq :as logseq] 14 | [clojure.string :as str] 15 | [clojure.set :as set] 16 | [clojure.edn :as edn] 17 | [clojure.java.io :as io] 18 | [cheshire.core :as json] 19 | [babashka.process :as process] 20 | [babashka.tasks :refer [shell]]) 21 | (:import (java.net URL) 22 | (java.io StringWriter))) 23 | 24 | ;; Config 25 | ;; ====== 26 | (def default-config 27 | ;; Configures what properties a given host keeps from rdf data to be converted 28 | ;; to a block. 29 | {:host-properties {} 30 | ;; Maps property urls that have text values to their names in logseq. 31 | ;; This also translates property values to normal strings by stripping them 32 | ;; of quotes and other suffix info e.g. "foo"@en -> foo 33 | :text-properties {} 34 | ;; Vec of url regexs to extract property info from. Extracts on first 35 | ;; matching regex 36 | :url-properties []}) 37 | 38 | ;; May need to move this to shared ns 39 | (defn- read-config [] 40 | (let [config-file (io/file (System/getenv "HOME") ".bb-logseq" "config.edn")] 41 | (if (.exists config-file) 42 | (edn/read-string (slurp (str config-file))) 43 | default-config))) 44 | 45 | (defn- read-entities-config 46 | "This is a map of urls to names. Entities are logseq pages or blocks that have 47 | url attributes. 48 | 49 | This is a separate file as it is expected to be autogenerated by a script e.g. a 50 | process that generates this map from your logseq data" 51 | [] 52 | (let [config-file (io/file (System/getenv "HOME") ".bb-logseq" "entities.edn")] 53 | (if (.exists config-file) 54 | (edn/read-string (slurp (str config-file))) 55 | {}))) 56 | 57 | ;; General util 58 | ;; ============ 59 | (defn- ->url-object [url] 60 | (try (URL. url) 61 | (catch Exception _ nil))) 62 | 63 | (defn- process-by-timeout 64 | "Runs cmd which returns output as string. If timeout ms is reached, returns 65 | what has been written to stdout" 66 | [cmd timeout] 67 | (let [out-str (StringWriter.) 68 | ret (deref (:out (process/process cmd {:out out-str})) timeout ::timeout)] 69 | (if (= ret ::timeout) 70 | (str out-str) 71 | (str ret)))) 72 | 73 | ;; Main CLI 74 | ;; ======== 75 | (defn- get-url-properties [url-obj {:keys [url-properties]} {:keys [debug]}] 76 | (let [url (str url-obj) 77 | props 78 | (some (fn [[regex-str {:keys [properties capture-names]}]] 79 | (when-let [matches (re-find (re-pattern regex-str) url)] 80 | (when debug (println "[DEBUG] Matched regex:" regex-str)) 81 | (merge properties 82 | (when (coll? matches) 83 | (zipmap capture-names (rest matches)))))) 84 | url-properties)] 85 | (when debug (println "[DEBUG] Url properties:" (pr-str props))) 86 | props)) 87 | 88 | (defn- translate-triple 89 | [{:keys [predicate object]} {:keys [text-property-urls url-to-name]}] 90 | (let [property-name (or (keyword (url-to-name predicate)) 91 | (throw (ex-info "Can't translate url to logseq property" {:url predicate}))) 92 | value (if (contains? text-property-urls predicate) 93 | (or (->> object (re-find #"^\"\s*(.*)\s*\"") second) 94 | object) 95 | object) 96 | value_ (if (->url-object value) 97 | (or (url-to-name value) 98 | ;; For cases like http://schema.org/Person with https://gist.github.com/borkdude 99 | (url-to-name (str/replace-first value "http://" "https://")) 100 | value) 101 | value)] 102 | [property-name value_])) 103 | 104 | (defn- build-dynamic-config 105 | [{:keys [text-properties entities]}] 106 | ;; Just add config for all https assuming all are described as http 107 | (let [new-text-properties 108 | (merge text-properties 109 | (into {} 110 | (map (fn [[k v]] 111 | ;; e.g. https://schema.description for SO 112 | [(str/replace-first k "http://" "https://") 113 | v]) 114 | text-properties)))] 115 | {:text-property-urls 116 | (set (keys new-text-properties)) 117 | :url-to-name 118 | (merge 119 | entities 120 | ;; override entities for abbreviated names like description -> desc 121 | new-text-properties 122 | ;; Not configurable for now as type is pretty critical to how all this 123 | ;; rdf data is converted 124 | {"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" "type"})})) 125 | 126 | (defn- get-rdf-properties* [triples url-obj config {:keys [debug]}] 127 | (let [dynamic-config (build-dynamic-config config) 128 | host (str/replace-first (.getHost url-obj) #"^www\." "") 129 | _ (when debug (println "[DEBUG] Host: " host)) 130 | properties-to-keep (get-in config [:host-properties host] 131 | #{"http://www.w3.org/1999/02/22-rdf-syntax-ns#type"}) 132 | _ (when debug (println "[DEBUG] Rdf Properties: " (pr-str properties-to-keep))) 133 | properties (->> triples 134 | (filter #(contains? properties-to-keep (:predicate %))) 135 | (map #(translate-triple % dynamic-config)) 136 | (into {}))] 137 | properties)) 138 | 139 | (defn get-rdf-properties [url-obj config options] 140 | (let [triples (try (-> ["rdf-dereference" (str url-obj)] 141 | ;; Need a timeout as rdf-dereference has been wierdly hanging 142 | ;; out for imdb and wikipedia 143 | (process-by-timeout 2500) 144 | (json/parse-string true) 145 | doall) 146 | ;; nytimes intermittently fails on partially streamed data 147 | (catch Exception e 148 | (println "Rdf properties unexpectedly failed with: " (str e))))] 149 | (when triples 150 | (get-rdf-properties* triples url-obj config options)))) 151 | 152 | (defn- url->properties [url-obj config options] 153 | (merge (get-rdf-properties url-obj config options) 154 | (get-url-properties url-obj config options))) 155 | 156 | (defn- create-block [arguments options] 157 | (let [entities (read-entities-config) 158 | config (assoc (read-config) 159 | :entities entities) 160 | url (if (seq arguments) 161 | (str/join " " arguments) 162 | (:out (shell {:out :string} "pbpaste"))) 163 | url-obj (or (->url-object url) 164 | (cli/error "The following is not a url -" (pr-str url))) 165 | properties (merge {:desc "" :url (str url-obj)} 166 | (url->properties url-obj config options)) 167 | ;; Keep first properties in order 168 | ordered-properties [:url :type :name :desc] 169 | properties-coll (concat (keep #(when-let [val (properties %)] [% val]) 170 | ordered-properties) 171 | (map (juxt identity properties) 172 | (set/difference (set (keys properties)) 173 | (set ordered-properties)))) 174 | block-text (logseq/properties->block properties-coll)] 175 | (shell {:in block-text} "pbcopy") 176 | (println block-text))) 177 | 178 | (defn -main [{:keys [options arguments summary]}] 179 | (if (:help options) 180 | (cli/print-summary " [& LOGSEQ_TEXT]" summary) 181 | (create-block arguments options))) 182 | 183 | (def cli-options 184 | [["-h" "--help"] 185 | ["-d" "--debug"]]) 186 | 187 | (when (= *file* (System/getProperty "babashka.file")) 188 | (cli/run-command -main *command-line-args* cli-options)) 189 | --------------------------------------------------------------------------------