Skip to content

Instantly share code, notes, and snippets.

@davidandrzej
Created May 14, 2011 23:14
Show Gist options
  • Save davidandrzej/972741 to your computer and use it in GitHub Desktop.
Save davidandrzej/972741 to your computer and use it in GitHub Desktop.
Count project lines of code
; Calculate lines of code contained in a given directory
; -ignores empty lines
; -ignores comment-only lines
; -does *not* ignore block comments
;
; David Andrzejewski ([email protected])
;
; 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 cpp (struct src-type :cpp "cpp" "//"))
(def cheader (struct src-type :cheader "h" "//"))
(def srctypes (list clojure python csrc cheader cpp))
; Pre-defined 'ignore' patterns
(def svn (list ".*\\.svn$"))
(def hg (list ".*\\.hg$"))
(def build (list ".*build$"))
(def ignores (concat svn hg 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)]
(do
(println basepath)
(print-counts (process-dir rootdir
(partial get-relative-path basepath)
(partial count-file srctypes)
(partial not-ignore ignores)))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment