{"id":13903170,"url":"https://github.com/borkdude/grasp","last_synced_at":"2025-04-13T02:20:33.421Z","repository":{"id":42589148,"uuid":"308011504","full_name":"borkdude/grasp","owner":"borkdude","description":"Grep Clojure code using clojure.spec regexes","archived":false,"fork":false,"pushed_at":"2023-10-04T10:21:19.000Z","size":106,"stargazers_count":244,"open_issues_count":5,"forks_count":7,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-04T04:12:57.090Z","etag":null,"topics":["clojure","clojure-spec","code-search"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/borkdude.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null},"funding":{"github":"borkdude"}},"created_at":"2020-10-28T12:37:45.000Z","updated_at":"2025-03-20T12:44:51.000Z","dependencies_parsed_at":"2024-01-30T01:59:31.207Z","dependency_job_id":"59d60718-c9a6-41dd-b331-6612e4499e71","html_url":"https://github.com/borkdude/grasp","commit_stats":{"total_commits":99,"total_committers":6,"mean_commits":16.5,"dds":0.08080808080808077,"last_synced_commit":"1e4a5673d680d9de20ba10781a09f2478afd7dee"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borkdude%2Fgrasp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borkdude%2Fgrasp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borkdude%2Fgrasp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borkdude%2Fgrasp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/borkdude","download_url":"https://codeload.github.com/borkdude/grasp/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248654645,"owners_count":21140335,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojure-spec","code-search"],"created_at":"2024-08-06T22:01:41.891Z","updated_at":"2025-04-13T02:20:33.400Z","avatar_url":"https://github.com/borkdude.png","language":"Clojure","funding_links":["https://github.com/sponsors/borkdude"],"categories":["clojure"],"sub_categories":[],"readme":"# grasp\n\nGrep Clojure code using clojure.spec regexes. Inspired by [grape](https://github.com/bfontaine/grape).\n\n## Why\n\nThis tool allows you to find patterns in Clojure code. I use it as a research\ntool for [sci](https://github.com/borkdude/sci/issues/485),\n[clj-kondo](https://github.com/borkdude/clj-kondo) or Clojure\n[tickets](https://clojure.atlassian.net/browse/CLJ-1656).\n\n## Dependency\n\n### deps.edn\n\n``` clojure\nio.github.borkdude/grasp {:mvn/version \"0.1.4\"}\n```\n\n## API\n\nThe `grasp.api` namespace currently exposes:\n\n- `(grasp path-or-paths spec)`: returns matched sexprs in path or paths for\n  spec. Accept source file, directory, jar file or classpath as string as well\n  as a collection of strings for passing multiple paths. In case of a directory,\n  it will be scanned recursively for source files ending with `.clj`, `.cljs` or\n  `.cljc`.\n- `(grasp-string string spec)`: returns matched sexprs in string for spec.\n- `resolve-symbol`: returns the resolved symbol for a symbol, taking into\n  account aliases and refers. You can also use `rsym` to create a spec that\n  matches a fully-qualified, resolved symbol.\n- `unwrap`: see [Finding keywords](#finding-keywords).\n- `cat`, `or`, `seq`, `vec`: see [Convenience macros](#convenience-macros).\n- `*`, `?`, `+`: aliases for `(s/* any?)`, etc.\n\n## Status\n\nVery alpha. API will almost certainly change.\n\n## Example usage\n\nAssuming you have the following requires:\n\n``` clojure\n(require '[clojure.java.io :as io]\n         '[clojure.pprint :as pprint]\n         '[clojure.string :as str]\n         '[clojure.spec.alpha :as s]\n         '[grasp.api :as g])\n```\n\n### Find reify usages\n\nFind `reify` usage with more than one interface:\n\n``` clojure\n(def clojure-core (slurp (io/resource \"clojure/core.clj\")))\n\n(s/def ::clause (s/cat :sym symbol? :lists (s/+ list?)))\n\n(s/def ::reify\n  (s/cat :reify #{'reify}\n         :clauses (s/cat :clause ::clause :clauses (s/+ ::clause))))\n\n(def matches (g/grasp-string clojure-core ::reify))\n\n(doseq [m matches]\n  (prn (meta m))\n  (pprint/pprint m)\n  (println))\n```\n\nThis outputs:\n\n``` clojure\n{:line 6974, :column 5, :end-line 6988, :end-column 56}\n(reify\n clojure.lang.IDeref\n (deref [_] (deref-future fut))\n clojure.lang.IBlockingDeref\n (deref\n  [_ timeout-ms timeout-val]\n  (deref-future fut timeout-ms timeout-val))\n ...)\n\n{:line 7107, :column 5, :end-line 7125, :end-column 16}\n(reify\n clojure.lang.IDeref\n ...)\n```\n(output abbreviated for readability)\n\n### Find usages based on resolved symbol\n\nFind all usages of `clojure.set/difference`:\n\n``` clojure\n(defn table-row [sexpr]\n  (-\u003e (meta sexpr)\n      (select-keys [:uri :line :column])\n      (assoc :sexpr sexpr)))\n\n(-\u003e\u003e\n   (g/grasp \"/Users/borkdude/git/clojure/src\"\n            ;; Alt 1: using rsym:\n            (g/rsym 'clojure.set/difference)\n            ;; Alt 2: do it manually:\n            #_(fn [sym]\n              (when (symbol? sym)\n                (= 'clojure.set/difference (g/resolve-symbol sym)))))\n   (map table-row)\n   pprint/print-table)\n```\n\nThis outputs:\n\n``` clojure\n|                                                         :uri | :line | :column |         :sexpr |\n|--------------------------------------------------------------+-------+---------+----------------|\n|     file:/Users/borkdude/git/clojure/src/clj/clojure/set.clj |    49 |       7 |     difference |\n|     file:/Users/borkdude/git/clojure/src/clj/clojure/set.clj |    62 |      14 |     difference |\n|     file:/Users/borkdude/git/clojure/src/clj/clojure/set.clj |   172 |       2 |     difference |\n|    file:/Users/borkdude/git/clojure/src/clj/clojure/data.clj |   112 |      19 | set/difference |\n|    file:/Users/borkdude/git/clojure/src/clj/clojure/data.clj |   113 |      19 | set/difference |\n| file:/Users/borkdude/git/clojure/src/clj/clojure/reflect.clj |   107 |      37 | set/difference |\n```\n\n### Find a function call\n\nFind all calls to `clojure.core/map` that take 1 argument:\n\n```clojure\n(g/grasp-string \"(comment (map identity))\" (g/seq (g/rsym 'clojure.core/map) any?))\n; =\u003e [(map identity)]\n```\n\n### Grasp a classpath\n\nGrasp the entire classpath for usage of `frequencies`:\n\n``` clojure\n(-\u003e\u003e (g/grasp (System/getProperty \"java.class.path\") #{'frequencies})\n     (take 2)\n     (map (comp #(select-keys % [:uri :line]) meta)))\n```\n\nOutput:\n\n``` clojure\n({:uri \"file:/Users/borkdude/.gitlibs/libs/borkdude/sci/cb96d7fb2a37a7c21c78fc145948d6867c30936a/src/sci/impl/namespaces.cljc\", :line 815}\n {:uri \"file:/Users/borkdude/.gitlibs/libs/borkdude/sci/cb96d7fb2a37a7c21c78fc145948d6867c30936a/src/sci/impl/namespaces.cljc\", :line 815})\n```\n\n### Finding keywords\n\nWhen searching for keywords you will run into the problem that they do not have\nlocation information because they can't carry metadata. To solve this problem,\ngrasp lets you wrap non-metadata supporting forms in a container. Grasp exposes\nthe `unwrap` function to get hold of the form, while you can access the location\nof that form using the container's metadata. Say we would like to find all\noccurrences of `:my.cljs.app.subs/my-data` in this example:\n\n`/tmp/code.clj`:\n``` clojure\n(ns my.cljs.app.views\n  (:require [my.cljs.app.subs :as subs]\n            [re-frame.core :refer [subscribe]]))\n\n(subscribe [::subs/my-data])\n(subscribe [:my.cljs.app.subs/my-data])\n```\n\nWe can find them like this:\n\n``` clojure\n(s/def ::subscription (fn [x] (= :my.cljs.app.subs/my-data (unwrap x))))\n\n(def matches\n  (grasp \"/tmp/code.clj\" ::subscription {:wrap true}))\n\n(run! prn (map meta matches))\n```\n\nNote that you explicitly have to provide `:wrap true` to make grasp wrap\nkeywords.\n\nThe output:\n\n``` clojure\n{:line 5, :column 13, :end-line 5, :end-column 27, :uri \"file:/tmp/code.clj\"}\n{:line 6, :column 13, :end-line 6, :end-column 38, :uri \"file:/tmp/code.clj\"}\n```\n\n### Keep-fn\n\nGrasp supports a custom `:keep-fn`, the function which decides whether to\ncollect a matched result. The default `:keep-fn` is:\n\n\n``` clojure\n(defn default-keep-fn\n  [{:keys [spec expr uri]}]\n  (when (s/valid? spec expr)\n    (impl/with-uri expr uri)))\n```\n\nWhen a spec result is valid, then the URI is attached to the result's metadata and kept.\n\nIn a custom `:keep-fn` you are able to call `s/conform` and keep that result around:\n\n``` clojure\n(defn keep-fn [{:keys [spec expr uri]}]\n  (let [conformed (s/conform spec expr)]\n    (when-not (s/invalid? conformed)\n      {:var-name (grasp/resolve-symbol (second expr))\n       :expr expr\n       :uri uri})))\n```\n\nNow the result of `g/grasp` will be a seq of maps instead of expressions and you\ncan do whatever you want with it.\n\n### Matching on source string\n\nUsing the option `:source true`, grasp will attach the source string as metadata\non parsed s-expressions. This can be used to match on things like function\nliterals like `#(foo %)` or keywords like `::foo`. For example: we can grasp for\nfunction literals that have more than one argument:\n\n``` clojure\n(s/def ::fn-literal\n  (fn [x] (and (seq? x) (= 'fn* (first x)) (\u003e (count (second x)) 1)\n               (some-\u003e x meta :source (str/starts-with? \"#(\"))))))\n\n(def match (first (g/grasp-string \"#(+ % %2)\" ::fn-literal {:source true})))\n\n(prn [match (meta match)])\n```\n\nOutput:\n\n``` clojure\n[(fn* [%1 %2] (+ %1 %2)) {:source \"#(+ % %2)\", :line 1, :column 1, :end-line 1, :end-column 10}]\n```\n\n### More examples\n\nMore examples in [examples](examples).\n\n## Convenience macros\n\nGrasp exposes the `cat`, `seq`, `vec` and `or` convenience macros.\n\nAll of these macros support passing in a single quoted value for matching a\nliteral thing `'foo` for matching that symbol instead of\n`#{'foo}`. Additionally, they let you write specs without names for each parsed\nitem: `(g/cat 'foo int?)` instead of `(s/cat :s #{'foo} :i int?)`. The `seq`\nand `vec` macros are like the `cat` macro but additionally check for `seq?` and\n`vector?` respectively.\n\n## Binary\n\nA CLI binary can be obtained from Github releases.\n\nIt can be invoked like this:\n\n``` shell\n$ ./grasp ~/git/spec.alpha/src -e \"(set-opts! {:wrap true}) (fn [k] (= :clojure.spec.alpha/invalid (unwrap k)))\" | grep file | wc -l\n      68\n```\n\nThe binary supports the following options:\n\n``` clojure\n-p, --path: path\n-e, --expr: spec from expr\n-f, --file: spec from file\n```\n\nThe path and spec may also be provided without flags, like `grasp \u003cpath\u003e\n\u003cspec\u003e`. Use `-` for grasping from stdin.\n\nThe evaluated code from `-e` or `-f` may return a spec (or spec keyword) or call\n    `set-opts!` with a map that contains `:spec` and other options. E.g.:\n\n``` clojure\n(require '[clojure.spec.alpha :as s])\n(require '[grasp.api :as g])\n\n(s/def ::spec (fn [x] (= :clojure.spec.alpha/invalid (g/unwrap x))))\n\n(g/set-opts! {:spec ::spec :wrap true})\n```\n\nIf `nil` is returned from the evaluated code and `set-opts!` wasn't called, the\nCLI assumes that code will handle the results and no printing will be\ndone. These programs may call `g/grasp` and pass `g/*path*` which contains the\npath that was passed to the CLI.\n\nFull example:\n\n`fn_literal.clj`:\n``` clojure\n(require '[clojure.pprint :as pprint]\n         '[clojure.spec.alpha :as s]\n         '[clojure.string :as str]\n         '[grasp.api :as g])\n\n(s/def ::fn-literal\n  (fn [x] (and (seq? x) (= 'fn* (first x)) (\u003e (count (second x)) 1)\n               (some-\u003e x meta :source (str/starts-with? \"#(\")))))\n\n(let [matches (g/grasp g/*path* ::fn-literal {:source true})\n      rows (map (fn [match]\n                  (let [m (meta match)]\n                    {:source (:source m)\n                     :match match}))\n                matches)]\n  (pprint/print-table rows))\n```\n\n``` clojure\n$ grasp - fn_literal.clj \u003c\u003c\u003c \"#(foo %1 %2)\"\n\n|  :uri | :line |      :source |                    :match |\n|-------+-------+--------------+---------------------------|\n| stdin |     1 | #(foo %1 %2) | (fn* [%1 %2] (foo %1 %2)) |\n```\n\n### Pattern matching\n\nThe matched s-expressions can be conformed and then pattern-matched using\nlibraries like [meander](https://github.com/noprompt/meander).\n\nRevisiting the `::reify` spec which finds reify usage with more than one\ninterface:\n\n``` clojure\n(s/def ::clause (s/cat :sym symbol? :lists (s/+ list?)))\n\n(s/def ::reify\n  (s/cat :reify #{'reify}\n         :clauses (s/cat :clause ::clause :clauses (s/+ ::clause))))\n\n(def clojure-core (slurp (io/resource \"clojure/core.clj\")))\n\n(def matches (g/grasp-string clojure-core ::reify))\n\n(def conformed (map #(s/conform ::reify %) matches))\n```\n\n#### [Matchete](https://github.com/xapix-io/matchete)\n\n``` clojure\n(require '[matchete.core :as mc])\n\n(def pattern\n  {:clauses\n   {:clause {:sym '!interface}\n    :clauses (mc/each {:sym '!interface})}})\n\n(first (mc/matches pattern (first conformed)))\n```\n\nReturns:\n\n``` clojure\n{!interface [clojure.lang.IDeref clojure.lang.IBlockingDeref clojure.lang.IPending java.util.concurrent.Future]}\n```\n\n#### [Meander](https://github.com/noprompt/meander)\n\n```\n(require '[meander.epsilon :as m])\n\n(m/find\n  (first conformed)\n  {:clauses {:clause {:sym !interface} :clauses [{:sym !interface} ...]}}\n  !interface)\n```\n\nReturns:\n\n``` clojure\n[clojure.lang.IDeref clojure.lang.IBlockingDeref clojure.lang.IPending java.util.concurrent.Future]\n```\n\n### Build\n\nRun `script/compile` to compile the `grasp` binary using\n[GraalVM](https://www.graalvm.org/downloads)\n## License\n\nCopyright © 2020 Michiel Borkent\n\nDistributed under the EPL License. See LICENSE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborkdude%2Fgrasp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fborkdude%2Fgrasp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborkdude%2Fgrasp/lists"}