1 | (ns com.github.kyleburton.sandbox.utils |
2 | (:require [clojure.contrib.duck-streams :as ds])) |
3 |
|
4 | (defn raise |
5 | "Simple wrapper around throw." |
6 | [args] |
7 | (throw (RuntimeException. (apply format args)))) |
8 |
|
9 | (defmacro assert-throws [& body] |
10 | `(let [did-throw# (atom false)] |
11 | (try |
12 | ~body |
13 | (catch Exception ~'_ (reset! did-throw# true))) |
14 | (~'clojure.contrib.test-is/is did-throw# "Form did not throw?"))) |
15 |
|
16 | (defn log [& args] |
17 | (prn (apply format args))) |
18 |
|
19 | (defn #^String get-user-home |
20 | "Get the user's home dir as a string." |
21 | [] |
22 | (System/getProperty "user.home")) |
23 |
|
24 | (defn #^java.io.File ->file |
25 | "Coerce into a File." |
26 | [thing] |
27 | (java.io.File. (str thing))) |
28 |
|
29 | (defmacro with-tmp-file [[var & [prefix suffix]] & body] |
30 | `(let [prefix# ~prefix |
31 | suffix# ~suffix |
32 | ~var (java.io.File/createTempFile (or prefix# "pfx") (or suffix# "sfx"))] |
33 | ~@body)) |
34 |
|
35 | (defmacro with-tmp-dir [[var & [prefix suffix]] & body] |
36 | `(let [prefix# ~prefix |
37 | suffix# ~suffix |
38 | ~var (java.io.File/createTempFile (or prefix# "pfx") (or suffix# "sfx"))] |
39 | (try |
40 | (do |
41 | (.delete ~var) |
42 | ~@body) |
43 | (finally |
44 | ;; TODO: this will fail if dir is not empty!, should this recrusively remove all the files? |
45 | (.delete ~var))))) |
46 |
|
47 | (defn basename [fname] |
48 | (cond (isa? fname java.io.File) |
49 | (.getParent fname) |
50 | true |
51 | (.getParent (java.io.File. (str fname))))) |
52 |
|
53 |
|
54 | (defn #^java.io.File $HOME |
55 | "Construct a path relative to the user's home directory." |
56 | [& paths] |
57 | (->file (apply str |
58 | (cons (str (System/getProperty "user.home") "/") |
59 | (apply str (interpose "/" paths)))))) |
60 |
|
61 | (defmulti expand-file-name |
62 | "Perform bash style expansion on the given path. Eg: ~/file.txt." |
63 | class) |
64 |
|
65 | (defmethod expand-file-name String [#^String path] |
66 | (cond (.startsWith path "~/") |
67 | (.replaceFirst path "^~(/|$)" (str (get-user-home) "/")) |
68 | (.startsWith path "file://~/") |
69 | (.replaceFirst path "^file://~/" (str "file://" (get-user-home) "/")) |
70 | true |
71 | path)) |
72 |
|
73 |
|
74 | (defn mkdir |
75 | "Create the given directory path, fall back gracefuly if the path |
76 | exists, warning if it's not a directory." |
77 | [path] |
78 | (let [f (->file path)] |
79 | (if (not (.exists f)) |
80 | (do |
81 | ;(log "[INFO] mkdir: creating %s" path) |
82 | (.mkdirs f) |
83 | true) |
84 | (if (not (.isDirectory f)) |
85 | (do |
86 | ;(log "[WARN] mkdir: %s exists and is not a directory!" path) |
87 | false) |
88 | (do |
89 | ;(log "[DEBUG] mkdir: exists: %s" path) |
90 | true))))) |
91 |
|
92 | (defn drain-line-reader |
93 | "Drain a buffered reader into a sequence." |
94 | [#^java.io.BufferedReader rdr] |
95 | (loop [res [] |
96 | line (.readLine rdr)] |
97 | (if line |
98 | (recur (conj res line) |
99 | (.readLine rdr)) |
100 | res))) |
101 |
|
102 | (defn exec |
103 | "Simple wrapper around Runtime.exec - not intended to compete with clojure.contrib.shell-out" |
104 | [cmd] |
105 | (let [proc (.exec (Runtime/getRuntime) cmd) |
106 | rv (.waitFor proc)] |
107 | {:error (drain-line-reader (java.io.BufferedReader. (java.io.InputStreamReader. (.getErrorStream proc)))) |
108 | :output (drain-line-reader (java.io.BufferedReader. (java.io.InputStreamReader. (.getInputStream proc)))) |
109 | :exit rv})) |
110 |
|
111 | (defn symlink |
112 | "Create a symlink." |
113 | [src dst] |
114 | (let [src (->file src) |
115 | dst (->file dst)] |
116 | (if (not (.exists src)) |
117 | (raise "symlink: src does not exist: %s" src)) |
118 | (if (.exists dst) |
119 | (log "[INFO] symlink: dst exists %s => %s" src dst) |
120 | (let [cmd (format "ln -s %s %s" src dst) |
121 | res (exec cmd)] |
122 | (log "[INFO] symlink: %s=>%s : %s" src dst cmd) |
123 | (if (not (= 0 (:exit res))) |
124 | (log "[ERROR] %s" (:error res))))))) |
125 |
|
126 | (defn delete |
127 | "Remove a file if it exists." |
128 | [path] |
129 | (let [path (->file path)] |
130 | (if (.exists path) |
131 | (.delete path)))) |
132 |
|
133 | (defn url-get |
134 | "Very simplistic retreival of a url target." |
135 | [url] |
136 | (with-open [is (.openStream (java.net.URL. url))] |
137 | (loop [sb (StringBuffer.) |
138 | chr (.read is)] |
139 | (if (= -1 chr) |
140 | sb |
141 | (do |
142 | (.append sb (char chr)) |
143 | (recur sb |
144 | (.read is))))))) |
145 |
|
146 | (defn url-download |
147 | "Shell's out to wget to pull the file into the target directory." |
148 | [url #^String target-dir] |
149 | (let [cmd (format "wget -P %s -c %s" target-dir url) |
150 | res (exec cmd)] |
151 | (log "[INFO] wget: %s" cmd) |
152 | (if (not (= 0 (:exit res))) |
153 | (log "[ERROR] %s" (:error res))))) |
154 |
|
155 |
|
156 | (defn all-groups |
157 | "Extracts all the groups from a java.util.regex.Matcher into a seq." |
158 | [#^java.util.regex.Matcher m] |
159 | (for [grp (range 1 (+ 1 (.groupCount m)))] |
160 | (.group m grp))) |
161 |
|
162 |
|
163 | (defn re-find-all |
164 | "Retreive all of the matches for a regex in a given string." |
165 | [re str] |
166 | (doall |
167 | (loop [m (re-matcher (if (isa? (class re) String) (re-pattern re) re) str) |
168 | res []] |
169 | (if (.find m) |
170 | (recur m (conj res (vec (all-groups m)))) |
171 | res)))) |
172 |
|
173 | (defn re-find-first |
174 | "Retreive the first set of match groups for a regex in a given string." |
175 | [re str] |
176 | (first |
177 | (doall |
178 | (loop [m (re-matcher (if (isa? (class re) String) (re-pattern re) re) str) |
179 | res []] |
180 | (if (.find m) |
181 | (recur m (conj res (vec (all-groups m)))) |
182 | res))))) |
183 |
|
184 |
|
185 | ;; There is no pattern matching in clojure atm, and Java6 doesn't yet |
186 | ;; support named capture groups (sure wish it did), what about a |
187 | ;; re-bind macro that created bindings for the parts? Here is an |
188 | ;; example of what it might look like: |
189 |
|
190 | ;; (re-bind "[Nov. 18th, 2008|<b>10:03 pm</b>]" |
191 | ;; ["\s*" |
192 | ;; [month-name "(\S+)"] |
193 | ;; "\s+" |
194 | ;; [day-of-month "(\S+)"] ",\s+" |
195 | ;; [year "(\S+)"] ] |
196 | ;; {:month month-name |
197 | ;; :day day-of-month |
198 | ;; :year year}) |
199 |
|
200 |
|
201 |
|
202 |
|
203 | (defn chmod |
204 | "Change a file or directory's permissions. Shells out to perform the chmod." |
205 | [perms file] |
206 | (let [cmd (format "chmod %s %s" perms file) |
207 | res (exec cmd)] |
208 | (log "[INFO] chmod: %s" cmd) |
209 | (if (not (= 0 (:exit res))) |
210 | (log "[ERROR] %s" (:error res))))) |
211 |
|
212 |
|
213 | ;; reflection class and doc utils |
214 | (defn methods-seq |
215 | ([thing] |
216 | (if (= Class (class thing)) |
217 | (seq (.getDeclaredMethods thing)) |
218 | (seq (.getDeclaredMethods (class thing)))))) |
219 |
|
220 | (defn fields-seq |
221 | ([thing] |
222 | (if (= Class (class thing)) |
223 | (seq (.getDeclaredFields thing)) |
224 | (seq (.getDeclaredFields (class thing)))))) |
225 |
|
226 | (defn fields-value-seq |
227 | ([thing] |
228 | (map #(.get % thing) (fields-seq thing)))) |
229 |
|
230 | (defn fields-and-values-seq |
231 | ([thing] |
232 | (for [field (fields-seq thing)] |
233 | [(.getName field) |
234 | (.get field thing)]))) |
235 |
|
236 | ;; this is not the same as 'bean' - bean doesn't grab fields, this |
237 | ;; grabs only fields... |
238 | (defn fields-and-values-map |
239 | ([thing] |
240 | (reduce |
241 | (fn [m [k v]] |
242 | (assoc m (keyword k) v)) |
243 | {} |
244 | (fields-and-values-seq thing)))) |
245 |
|
246 | (defn constructors-seq |
247 | ([thing] |
248 | (if (= Class (class thing)) |
249 | (seq (.getConstructors thing)) |
250 | (seq (.getConstructors (class thing)))))) |
251 |
|
252 | (defn method-modifiers-as-strings [#^java.lang.reflect.Method method] |
253 | (str (.getModifiers method))) |
254 |
|
255 | (defn method-return-type [#^java.lang.reflect.Method method] |
256 | (.getReturnType method)) |
257 |
|
258 | (defn method-name-short [#^java.lang.reflect.Method method] |
259 | (.getReturnType method)) |
260 |
|
261 | (defn method-argument-list [#^java.lang.reflect.Method method] |
262 | (.getParameterTypes method)) |
263 |
|
264 | (defn short-method-sig [#^java.lang.reflect.Method method] |
265 | (str (method-modifiers-as-strings method) |
266 | " " |
267 | (method-return-type method) |
268 | " " |
269 | (method-name-short method) |
270 | "(" |
271 | (method-argument-list method) |
272 | ")")) |
273 |
|
274 | (defn make-subst-fn [pattern replacement] |
275 | (fn [str] |
276 | (.replaceAll (.toString str) |
277 | pattern replacement))) |
278 |
|
279 |
|
280 | ;; TODO: add in abstract/interface/class info, then list all parent |
281 | ;; classes and interfaces |
282 | (defn doc-class [thing] |
283 | "Prints (to *out*) a summary of the class, it's members and its |
284 | methods." |
285 | (let [tclass (if (= Class (class thing)) |
286 | (identity thing) |
287 | (class thing)) |
288 | trimmer (make-subst-fn "java.lang." "")] |
289 | (println (format "Class %s (%s)" (.getName tclass) (.getSuperclass tclass))) |
290 | (println (format " Interfaces:")) |
291 | (doseq [interf (seq (.getInterfaces tclass))] |
292 | (println (str " " (trimmer interf)))) |
293 | (println (str " Constructors:")) |
294 | (doseq [constructor (constructors-seq tclass)] |
295 | (println (str " " (trimmer constructor)))) |
296 | (println (str " Members:")) |
297 | (doseq [field (fields-seq tclass)] |
298 | (println (str " " (trimmer field)))) |
299 | (println (str " Methods:")) |
300 | (doseq [method (methods-seq tclass)] |
301 | (println (str " " (trimmer method)))))) |
302 |
|
303 |
|
304 | (defn object->file [obj file] |
305 | (with-open [outp (java.io.ObjectOutputStream. (java.io.FileOutputStream. file))] |
306 | (.writeObject outp obj))) |
307 |
|
308 |
|
309 | (defn file->object [file] |
310 | (with-open [inp (java.io.ObjectInputStream. (java.io.FileInputStream. file))] |
311 | (.readObject inp))) |
312 |
|
313 | (defn freeze |
314 | ([obj] |
315 | (with-open [baos (java.io.ByteArrayOutputStream. 1024) |
316 | oos (java.io.ObjectOutputStream. baos)] |
317 | (.writeObject oos obj) |
318 | (.toByteArray baos))) |
319 | ([obj & objs] |
320 | (freeze (vec (cons obj objs))))) |
321 |
|
322 | ;; (freeze "foo") |
323 | ;; (freeze "foo" "bar" "qux") |
324 |
|
325 |
|
326 | (defn thaw [bytes] |
327 | (with-open [bais (java.io.ByteArrayInputStream. bytes) |
328 | ois (java.io.ObjectInputStream. bais)] |
329 | (.readObject ois))) |
330 |
|
331 | ;; (thaw (freeze "foo")) |
332 |
|
333 | ;; (object->file "foo" ($HOME "/foo.bin")) |
334 | ;; (file->object ($HOME "/foo.bin")) |
335 |
|
336 | (defmacro with-stdout-to-file [file & body] |
337 | `(with-open [out# (ds/writer ~file)] |
338 | (binding [*out* out#] |
339 | ~@body))) |
340 |
|
341 | (defmacro with-stderr-to-file [file & body] |
342 | `(with-open [out# (ds/writer ~file)] |
343 | (binding [*err* out#] |
344 | ~@body))) |
345 |
|
346 | (defn pairs->map [pairs] |
347 | (if (not (even? (count pairs))) |
348 | (throw (RuntimeException. (format "Error, pairs->map on odd # of fields? %d:(%s)" (count pairs) pairs))) |
349 | (reduce (fn [m [k v]] (assoc m k v)) |
350 | {} |
351 | (partition 2 pairs)))) |
352 |
|
353 |
|
354 | (defn md5->string [bytes] |
355 | (let [digester (java.security.MessageDigest/getInstance "MD5")] |
356 | (.update digester bytes) |
357 | (apply str (map (fn [byte] |
358 | (Integer/toHexString (bit-and 0xFF byte))) |
359 | (.digest digester))))) |
360 |
|
361 | (defn sha1->string [bytes] |
362 | (let [digester (java.security.MessageDigest/getInstance "SHA1")] |
363 | (.update digester bytes) |
364 | (apply str (map (fn [byte] |
365 | (Integer/toHexString (bit-and 0xFF byte))) |
366 | (.digest digester))))) |
367 |
|
368 | ;; (md5->string (.getBytes "foo bar\n")) |
369 | ;; (sha1->string (.getBytes "foo bar\n")) |
370 |
|
371 |
|
372 |
|
373 |
|
374 |
|
375 |
|
376 | ;; console is not available through slime :(, but X is - try using swing/get-password-dialog instead |
377 | ;; (String. (.readPassword console "[%s]", (into-array ["password"]))) |
378 |
|
379 |
|
380 | (defn parse-paired-arglist |
381 | "Ensures the given set of key/value pairs is a map, extracting non |
382 | 'pair' arguments into an additional list. Eg: |
383 |
|
384 | (parse-paired-arglist [:foo 1 :bar 2]) => [[] {:bar 2, :foo 1}] |
385 | (parse-paired-arglist [:foo 1 :bar 2]) => [[] {:bar 2, :foo 1}] |
386 | (parse-paired-arglist [:foo 1 3 4 :bar 2]) => [[3 4] {:bar 2, :foo 1}] |
387 |
|
388 | This is most useful in functions where you want to be able to take |
389 | sets of optional parameters: |
390 |
|
391 | (defn my-func [arg1 & params] |
392 | (let [[additional args] (parse-paired-arglist params) |
393 | args (merge {:foo \"default\" :bar \"default\"} args)] |
394 | (prn (format \"foo=%s; bar=%s; qux=%s\" |
395 | (:foo args) |
396 | (:bar args) |
397 | (:qux args))))) |
398 | (my-func 1) => \"foo=default; bar=default; qux=null\" |
399 | (my-func 1 :foo 2) => \"foo=2; bar=default; qux=null\" |
400 | (my-func 1 :bar 3 :qux 4) => \"foo=default; bar=3; qux=4\" |
401 | (my-func 1 :foo 2 :bar 3 :qux 4) => \"foo=2; bar=3; qux=4\" |
402 | " |
403 | [args] |
404 | (if (map? args) |
405 | [[] args] |
406 | (loop [res {} |
407 | unnamed [] |
408 | [arg & args] args] |
409 | (if (not arg) |
410 | [unnamed res] |
411 | (if (keyword? arg) |
412 | (recur (assoc res arg (first args)) |
413 | unnamed |
414 | (rest args)) |
415 | (recur res |
416 | (conj unnamed arg) |
417 | args)))))) |
418 |
|
419 | ;; (parse-paired-arglist '[:foo bar this that :other thing]) |
420 | ;; (parse-paired-arglist {:foo 'bar :other 'thing}) |
421 |
|