简体   繁体   中英

Modify Clojure source code file in clojure

I was wondering if it's possible to load the code contained in a Clojure .clj source file as a list, without compiling it.

If I can load a .clj file as a list, I can modify that list and pretty print it back into the same file which can then be loaded again.

(Maybe this is a bad idea.) Does anyone know if this is possible?

It is not a bad idea, it is one of the major properties of lisp, code is data. you can read the clj file as a list using read-string modify it and write it back.


(ns tmp
  (:require [clojure.zip :as zip])
  (:use clojure.contrib.pprint))

(def some-var true)

;;stolen from http://nakkaya.com/2011/06/29/ferret-an-experimental-clojure-compiler/
(defn morph-form [tree pred f]
  (loop [loc (zip/seq-zip tree)]
    (if (zip/end? loc)
      (zip/root loc)
      (recur
       (zip/next
        (if (pred (zip/node loc))
          (zip/replace loc (f (zip/node loc)))
          loc))))))

(let [morphed (morph-form (read-string (str \( (slurp "test.clj")\)))
                          #(or (= 'true %)
                               (= 'false %))
                          (fn [v] (if (= 'true v)
                                   'false
                                   'true)))]
  (spit "test.clj"
        (with-out-str
          (doseq [f morphed]
            (pprint f)))))

This reads itself and toggles boolean values and writes it back.

A slightly simpler example:

user=> (def a '(println (+ 1 1))) ; "'" escapes the form to prevent immediate evaluation
#'user/a
user=> (spit "test.code" a) ; write it to a file
nil

user=> (def from-file (read-string (slurp "test.code"))) ; read it from a file
#'user/from-file
user=> (def modified (clojure.walk/postwalk-replace {1 2} from-file)) ; modify the code
#'user/modified
user=> (spit "new.code" modified) ; write it back
nil
user=> (load-string (slurp "new.code")) ; check it worked!
4
nil

Where slurp gives you a string, read-string gives you an un-evaluated form, and load-string gives you the result of evaluating the form.

There's a library called rewrite-clj which can this type of thing.

https://github.com/xsc/rewrite-clj

Without any libraries, what you'd like to do is

(with-open [reader (java.io.PushbackReader. 
                     (clojure.java.io/reader "src/my/file/ns_path.clj"))]
  (loop [forms []]
    (try 
      (recur (conj forms (read reader)))
      (catch Exception ex
        (println (.getMessage ex))
        forms))))

but Clojure doesn't allow recur across try , so instead you do

(with-open [reader (java.io.PushbackReader. 
                     (clojure.java.io/reader "src/my/file/ns_path.clj"))]
  (loop [[ forms done? ] 
         [ []    false ]]
    (if done?
      forms
      (recur (try
               [(conj forms (read reader)) false]
               (catch Exception ex
                 (println (.getMessage ex))
                 [forms true]))))))

You need to propagate the done? signal yourself once reading is complete upon the EOF (end-of-file) error. I tried to layout the loop bindings so they match up clearly top to bottom.

That will return a vector of all the forms in the file. From there, you can perform whatever transformations you want on the forms as data, and spit/print/write them back to the file again.


REQUISITE LEGAL DISCLAIMER REGARDING read

Don't run clojure.core/read on code/data you didn't write or check yourself first. See this ClojureDocs comment for examples of what can go wrong.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM