|
1 | 1 | (ns eca.features.tools.filesystem |
2 | 2 | (:require |
3 | 3 | [babashka.fs :as fs] |
| 4 | + [clojure.java.io :as io] |
| 5 | + [clojure.java.shell :as shell] |
4 | 6 | [clojure.string :as string] |
5 | 7 | [eca.features.tools.util :as tools.util] |
6 | 8 | [eca.shared :as shared])) |
|
56 | 58 |
|
57 | 59 | (defn ^:private search-files [arguments db] |
58 | 60 | (or (invalid-arguments arguments (concat (path-validations db) |
59 | | - [["pattern" #(and % (not (string/blank? %))) "Invalid glob pattern '$pattern'"]])) |
| 61 | + [["pattern" #(not (string/blank? %)) "Invalid glob pattern '$pattern'"]])) |
60 | 62 | (let [pattern (get arguments "pattern") |
61 | 63 | pattern (if (string/includes? pattern "*") |
62 | 64 | pattern |
|
71 | 73 | (string/join "\n" paths) |
72 | 74 | "No matches found"))))) |
73 | 75 |
|
| 76 | +(defn ^:private run-ripgrep [path pattern include] |
| 77 | + (let [cmd (cond-> ["rg" "--files-with-matches" "--no-heading"] |
| 78 | + include (concat ["--glob" include]) |
| 79 | + :always (concat ["-e" pattern path]))] |
| 80 | + (->> (apply shell/sh cmd) |
| 81 | + :out |
| 82 | + (string/split-lines) |
| 83 | + (filterv #(not (string/blank? %)))))) |
| 84 | + |
| 85 | +(defn ^:private run-grep [path pattern ^String include] |
| 86 | + (let [include-patterns (if (and include (.contains include "{")) |
| 87 | + (let [pattern-match (re-find #"\*\.\{(.+)\}" include)] |
| 88 | + (when pattern-match |
| 89 | + (map #(str "*." %) (clojure.string/split (second pattern-match) #",")))) |
| 90 | + [include]) |
| 91 | + cmd (cond-> ["grep" "-E" "-l" "-r" "--exclude-dir=.*"] |
| 92 | + (and include (> (count include-patterns) 1)) (concat (mapv #(str "--include=" %) include-patterns)) |
| 93 | + include (concat [(str "--include=" include)]) |
| 94 | + :always (concat [pattern path]))] |
| 95 | + (->> (apply shell/sh cmd) |
| 96 | + :out |
| 97 | + (string/split-lines) |
| 98 | + (filterv #(not (string/blank? %)))))) |
| 99 | + |
| 100 | +(defn ^:private run-java-grep [path pattern include] |
| 101 | + (let [include-pattern (when include |
| 102 | + (re-pattern (str ".*\\.(" |
| 103 | + (-> include |
| 104 | + (string/replace #"^\*\." "") |
| 105 | + (string/replace #"\*\.\{(.+)\}" "$1") |
| 106 | + (string/replace #"," "|")) |
| 107 | + ")$"))) |
| 108 | + pattern-regex (re-pattern pattern)] |
| 109 | + (letfn [(search [dir] |
| 110 | + (keep |
| 111 | + (fn [file] |
| 112 | + (cond |
| 113 | + (and (fs/directory? file) (not (fs/hidden? file))) |
| 114 | + (search file) |
| 115 | + |
| 116 | + (and (not (fs/directory? file)) |
| 117 | + (or (nil? include-pattern) |
| 118 | + (re-matches include-pattern (fs/file-name file)))) |
| 119 | + (try |
| 120 | + (with-open [rdr (io/reader (fs/file file))] |
| 121 | + (loop [lines (line-seq rdr)] |
| 122 | + (when (seq lines) |
| 123 | + (if (re-find pattern-regex (first lines)) |
| 124 | + (str (fs/canonicalize file)) |
| 125 | + (recur (rest lines)))))) |
| 126 | + (catch Exception _ nil)))) |
| 127 | + (fs/list-dir dir)))] |
| 128 | + (when (fs/exists? path) |
| 129 | + (flatten (search path)))))) |
| 130 | + |
| 131 | +(defn ^:private grep [arguments db] |
| 132 | + (or (invalid-arguments arguments (concat (path-validations db) |
| 133 | + [["path" fs/readable? "File $path is not readable"] |
| 134 | + ["pattern" #(and % (not (string/blank? %))) "Invalid content regex pattern '$pattern'"] |
| 135 | + ["include" #(or (nil? %) (not (string/blank? %))) "Invalid file pattern '$include'"] |
| 136 | + ["max_results" #(or (nil? %) number?) "Invalid number '$max_results'"]])) |
| 137 | + (let [path (get arguments "path") |
| 138 | + pattern (get arguments "pattern") |
| 139 | + include (get arguments "include") |
| 140 | + max-results (or (get arguments "max_results") 1000) |
| 141 | + paths |
| 142 | + (->> (cond |
| 143 | + (tools.util/command-available? "rg" "--version") |
| 144 | + (run-ripgrep path pattern include) |
| 145 | + |
| 146 | + (tools.util/command-available? "grep" "--version") |
| 147 | + (run-grep path pattern include) |
| 148 | + |
| 149 | + :else |
| 150 | + (run-java-grep path pattern include)) |
| 151 | + (take max-results))] |
| 152 | + (single-text-content (if (seq paths) |
| 153 | + (string/join "\n" paths) |
| 154 | + "No files found for given pattern"))))) |
| 155 | + |
74 | 156 | (def definitions |
75 | 157 | {"list_directory" |
76 | 158 | {:description (str "Get a detailed listing of all files and directories in a specified path. " |
|
94 | 176 | :parameters {:type "object" |
95 | 177 | :properties {"path" {:type "string" |
96 | 178 | :description "The absolute path to the file to read."} |
97 | | - "head" {:type "number" |
| 179 | + "head" {:type "integer" |
98 | 180 | :description "If provided, returns only the first N lines of the file"} |
99 | | - "tail" {:type "number" |
| 181 | + "tail" {:type "integer" |
100 | 182 | :description "If provided, returns only the last N lines of the file"}} |
101 | 183 | :required ["path"]} |
102 | 184 | :handler #'read-file} |
|
113 | 195 | :description (str "Glob pattern following java FileSystem#getPathMatcher matching files or directory names." |
114 | 196 | "Use '**/*' to match search in multiple levels like '**/*.txt'")}} |
115 | 197 | :required ["path" "pattern"]} |
116 | | - :handler #'search-files}}) |
| 198 | + :handler #'search-files} |
| 199 | + "grep" |
| 200 | + {:description (str "Fast content search tool that works with any codebase size. " |
| 201 | + "Finds the paths to files that have matching contents using regular expressions. " |
| 202 | + "Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.). " |
| 203 | + "Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\"). " |
| 204 | + "Returns matching file paths sorted by modification time. " |
| 205 | + "Use this tool when you need to find files containing specific patterns.") |
| 206 | + :parameters {:type "object" |
| 207 | + :properties {"path" {:type "string" |
| 208 | + :description "The absolute path to search in."} |
| 209 | + "pattern" {:type "string" |
| 210 | + :description "The regular expression pattern to search for in file contents"} |
| 211 | + "include" {:type "string" |
| 212 | + :description "File pattern to include in the search (e.g. \"*.clj\", \"*.{clj,cljs}\")"} |
| 213 | + "max_results" {:type "integer" |
| 214 | + :description "Maximum number of results to return (default: 1000)"}} |
| 215 | + :required ["path" "pattern"]} |
| 216 | + :handler #'grep}}) |
0 commit comments