; Calculate lines of code contained in a given directory ; -ignores empty lines ; -ignores comment-only lines ; -does *not* ignore block comments ; ; David Andrzejewski (andrzeje@cs.wisc.edu) ; ; Command-line arguments ; 0 Code root directory ; ; EX: clj loc.clj ~/private/myProject ; (use '[clojure.contrib.duck-streams :only (read-lines)]) (import java.io.File) ; Recursively calculated line-of-code counts at a given file/directory (defstruct code-count :path :counts :children) ; A type of source file (defstruct src-type :name :extension :commentchar) ; Pre-defined source file types (def clojure (struct src-type :clojure "clj" ";")) (def python (struct src-type :python "py" "#")) (def csrc (struct src-type :csrc "c" "//")) (def cheader (struct src-type :cheader "h" "//")) ; Pre-defined 'ignore' patterns (def svn (list ".*\\.svn$")) (def hg (list ".*\\.hg$")) (def build (list ".*build$")) (defn not-nil? "Could not find this in core API...?!" [val] (not (nil? val))) (defn has-extension "Does this filename have this extension?" [extension fname] (not-nil? (re-matches (re-pattern (str ".+\\." extension "$")) fname))) (defn valid-line "Is this line 1) non-whitespace? and 2) not a comment?" [commentchar line] (and (not (zero? (.. line trim length))) (nil? (re-matches (re-pattern (str "^\\s*" commentchar ".*")) line)))) (defn lines-of-code "Count non-commented, non-empty lines of code in this file" [commentchar fname] (count (filter (partial valid-line commentchar) (read-lines fname)))) (defn process-dir "Takes a File object" [dir getrelpath fcounter ignorer] (struct code-count (getrelpath dir) nil (for [file (filter ignorer (. dir listFiles))] (if (. file isDirectory) (process-dir file getrelpath fcounter ignorer) (struct code-count (getrelpath file) (fcounter file) nil))))) (defn get-relative-path "Get path relative to some base path" [basepath file] (. (. file getCanonicalPath) substring (. basepath length))) (defn path-depth "How deep is this path? (for indenting)" [path] (dec (count (filter #(. (new Character java.io.File/separatorChar) equals %1) (. path toCharArray))))) (defn get-counts "Code counts at this entry (include subdirectories)" [ccount] (apply merge-with + (cons (:counts ccount) (map get-counts (:children ccount))))) (defn indent-depth "Add tabs for path depth" [pdepth] (apply str (for [i (range pdepth)] "\t"))) (defn nonempty-count? "Don't print non-matching files or empty directories" [ccount] (or (not-nil? (get-counts ccount)) (not-nil? (:children ccount)))) (defn print-indented-counts "Print source type LOC counts, appropriately indented" [indent cts] (doseq [key (keys cts)] (println (format " %s%s %d" indent key (key cts))))) (defn print-counts "Recursively print LOC counts for all entries" [ccount] (let [cts (get-counts ccount) indent (indent-depth (path-depth (:path ccount)))] (println (str indent (:path ccount))) (print-indented-counts indent cts) (doseq [child (filter nonempty-count? (:children ccount))] (print-counts child)))) (defn count-loc "Count lines of code for a particular type of src" [srctype fpath] (if (has-extension (:extension srctype) fpath) (hash-map (:name srctype) (lines-of-code (:commentchar srctype) fpath)))) (defn count-file "Try to count srctypes code for this file" [srctypes file] (let [fpath (. file getCanonicalPath)] (apply merge-with + (map #(count-loc %1 fpath) srctypes)))) (defn not-ignore "True if we should *not* ignore" [ignores file] (every? nil? (map #(re-matches (re-pattern %1) (. file getCanonicalPath)) ignores))) (let [rootdirname (nth *command-line-args* 0) rootdir (new java.io.File rootdirname) basepath (. rootdir getCanonicalPath) srctypes (list clojure python csrc cheader) ignores (concat svn hg build)] (do (println basepath) (print-counts (process-dir rootdir (partial get-relative-path basepath) (partial count-file srctypes) (partial not-ignore ignores)))))