How-to: Read from STDIN or a File in Clojure

Bryan Lott published on
3 min, 447 words

or... how to avoid wasting 2 days!

First, the completed code:

(ns char-check.core
  (:require
    [clojure.tools.cli :refer [parse-opts]])
  (:gen-class))

(defn exit
  "Exits the program with a status code and message."
  [status msg]
  (println msg)
  (System/exit status))

(defn main
  "Main function.  Calling without a file object results in reading from STDIN (*in*).
  Otherwise, opens the in-file as a stream."
  ([]
   (main (java.io.BufferedReader. *in*)))
  ([in-file]
   (with-open [r in-file]
     (line-seq r))))

(def cli-options
  [["-h" "--help"]])

(defn -main
  "Entrypoint, parses arguments, exits with any errors, provides args to main."
  [& args]
  (let [{:keys [options arguments errors summary]} (parse-opts args cli-options)]
    (cond
      (:help options) (exit 0 summary)
      errors (exit 1 (join "\n" errors))
      (empty? arguments) (main)
      :else (main (clojure.java.io/reader (first arguments))))))

So, here's what we're doing... first, take a look at:

(cond
  (:help options) (exit 0 summary)  ;; if we get the help option, spit out some help!
  errors (exit 1 (join "\n" errors))  ;; if we get errors, print them out and exit
  (empty? arguments) (main)  ;; if we don't get a file argument, call main with no args and let it take care of things
  :else (main (clojure.java.io/reader (first arguments))) ;; if we got an argument, treat the first argument (yeah, it's naive...) as a filename and wrap it in a file reader
 )))

So, this is pretty basic flow control for the command line arguments and the presence or lack of a filename to run through. Arguments come in through the & args and get parsed via parse-opts. This results in any filenames ending up in arguments and if we don't get any, pass that along to main. If, however, we get at least one, grab the first, wrap it in a file reader, and pass it along to main (arity 1).

The next bit is in the main function:

([]  ;; arity 0... if we don't get any arguments, call main again, but this time pass it a BufferedReader wrapped *in*
 (main (java.io.BufferedReader. *in*)))
([in-file]  ;; this will either get called with the arity 0 function and be passed *in* OR it will have an actual filename that can then be opened up and operated on.
 (with-open [r in-file]
   (line-seq r))))

So here we're either getting 0 or 1 argument. If we get 1 argument, great, treat it as a file and run with it. Otherwise, we got 0 arguments which means we need to wrap *in* in a buffered file and then pass it along to the arity 1 "version" of main.

The final bit is (line-seq r). That's a lazy function to return lines from the file/stdin. Use that like you would any other lazy sequence.