rlm@10: ;;; duck_streams.clj -- duck-typed I/O streams for Clojure rlm@10: rlm@10: ;; by Stuart Sierra, http://stuartsierra.com/ rlm@10: ;; May 13, 2009 rlm@10: rlm@10: ;; Copyright (c) Stuart Sierra, 2009. All rights reserved. The use rlm@10: ;; and distribution terms for this software are covered by the Eclipse rlm@10: ;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) rlm@10: ;; which can be found in the file epl-v10.html at the root of this rlm@10: ;; distribution. By using this software in any fashion, you are rlm@10: ;; agreeing to be bound by the terms of this license. You must not rlm@10: ;; remove this notice, or any other, from this software. rlm@10: rlm@10: rlm@10: ;; This file defines "duck-typed" I/O utility functions for Clojure. rlm@10: ;; The 'reader' and 'writer' functions will open and return an rlm@10: ;; instance of java.io.BufferedReader and java.io.PrintWriter, rlm@10: ;; respectively, for a variety of argument types -- filenames as rlm@10: ;; strings, URLs, java.io.File's, etc. 'reader' even works on http rlm@10: ;; URLs. rlm@10: ;; rlm@10: ;; Note: this is not really "duck typing" as implemented in languages rlm@10: ;; like Ruby. A better name would have been "do-what-I-mean-streams" rlm@10: ;; or "just-give-me-a-stream", but ducks are funnier. rlm@10: rlm@10: rlm@10: ;; CHANGE LOG rlm@10: ;; rlm@10: ;; July 23, 2010: DEPRECATED in 1.2. Use clojure.java.io instead. rlm@10: ;; rlm@10: ;; May 13, 2009: added functions to open writers for appending rlm@10: ;; rlm@10: ;; May 3, 2009: renamed file to file-str, for compatibility with rlm@10: ;; clojure.contrib.java-utils. reader/writer no longer use this rlm@10: ;; function. rlm@10: ;; rlm@10: ;; February 16, 2009: (lazy branch) fixed read-lines to work with lazy rlm@10: ;; Clojure. rlm@10: ;; rlm@10: ;; January 10, 2009: added *default-encoding*, so streams are always rlm@10: ;; opened as UTF-8. rlm@10: ;; rlm@10: ;; December 19, 2008: rewrote reader and writer as multimethods; added rlm@10: ;; slurp*, file, and read-lines rlm@10: ;; rlm@10: ;; April 8, 2008: first version rlm@10: rlm@10: (ns rlm@10: ^{:author "Stuart Sierra", rlm@10: :deprecated "1.2" rlm@10: :doc "This file defines \"duck-typed\" I/O utility functions for Clojure. rlm@10: The 'reader' and 'writer' functions will open and return an rlm@10: instance of java.io.BufferedReader and java.io.PrintWriter, rlm@10: respectively, for a variety of argument types -- filenames as rlm@10: strings, URLs, java.io.File's, etc. 'reader' even works on http rlm@10: URLs. rlm@10: rlm@10: Note: this is not really \"duck typing\" as implemented in languages rlm@10: like Ruby. A better name would have been \"do-what-I-mean-streams\" rlm@10: or \"just-give-me-a-stream\", but ducks are funnier."} rlm@10: clojure.contrib.duck-streams rlm@10: (:refer-clojure :exclude (spit)) rlm@10: (:import rlm@10: (java.io Reader InputStream InputStreamReader PushbackReader rlm@10: BufferedReader File PrintWriter OutputStream rlm@10: OutputStreamWriter BufferedWriter Writer rlm@10: FileInputStream FileOutputStream ByteArrayOutputStream rlm@10: StringReader ByteArrayInputStream) rlm@10: (java.net URI URL MalformedURLException Socket))) rlm@10: rlm@10: rlm@10: (def rlm@10: ^{:doc "Name of the default encoding to use when reading & writing. rlm@10: Default is UTF-8." rlm@10: :tag "java.lang.String"} rlm@10: *default-encoding* "UTF-8") rlm@10: rlm@10: (def rlm@10: ^{:doc "Size, in bytes or characters, of the buffer used when rlm@10: copying streams."} rlm@10: *buffer-size* 1024) rlm@10: rlm@10: (def rlm@10: ^{:doc "Type object for a Java primitive byte array."} rlm@10: *byte-array-type* (class (make-array Byte/TYPE 0))) rlm@10: rlm@10: rlm@10: (defn ^File file-str rlm@10: "Concatenates args as strings and returns a java.io.File. Replaces rlm@10: all / and \\ with File/separatorChar. Replaces ~ at the start of rlm@10: the path with the user.home system property." rlm@10: [& args] rlm@10: (let [^String s (apply str args) rlm@10: s (.replaceAll (re-matcher #"[/\\]" s) File/separator) rlm@10: s (if (.startsWith s "~") rlm@10: (str (System/getProperty "user.home") rlm@10: File/separator (subs s 1)) rlm@10: s)] rlm@10: (File. s))) rlm@10: rlm@10: rlm@10: (defmulti ^{:tag BufferedReader rlm@10: :doc "Attempts to coerce its argument into an open rlm@10: java.io.BufferedReader. Argument may be an instance of Reader, rlm@10: BufferedReader, InputStream, File, URI, URL, Socket, or String. rlm@10: rlm@10: If argument is a String, it tries to resolve it first as a URI, then rlm@10: as a local file name. URIs with a 'file' protocol are converted to rlm@10: local file names. Uses *default-encoding* as the text encoding. rlm@10: rlm@10: Should be used inside with-open to ensure the Reader is properly rlm@10: closed." rlm@10: :arglists '([x])} rlm@10: reader class) rlm@10: rlm@10: (defmethod reader Reader [x] rlm@10: (BufferedReader. x)) rlm@10: rlm@10: (defmethod reader InputStream [^InputStream x] rlm@10: (BufferedReader. (InputStreamReader. x *default-encoding*))) rlm@10: rlm@10: (defmethod reader File [^File x] rlm@10: (reader (FileInputStream. x))) rlm@10: rlm@10: (defmethod reader URL [^URL x] rlm@10: (reader (if (= "file" (.getProtocol x)) rlm@10: (FileInputStream. (.getPath x)) rlm@10: (.openStream x)))) rlm@10: rlm@10: (defmethod reader URI [^URI x] rlm@10: (reader (.toURL x))) rlm@10: rlm@10: (defmethod reader String [^String x] rlm@10: (try (let [url (URL. x)] rlm@10: (reader url)) rlm@10: (catch MalformedURLException e rlm@10: (reader (File. x))))) rlm@10: rlm@10: (defmethod reader Socket [^Socket x] rlm@10: (reader (.getInputStream x))) rlm@10: rlm@10: (defmethod reader :default [x] rlm@10: (throw (Exception. (str "Cannot open " (pr-str x) " as a reader.")))) rlm@10: rlm@10: rlm@10: (def rlm@10: ^{:doc "If true, writer and spit will open files in append mode. rlm@10: Defaults to false. Use append-writer or append-spit." rlm@10: :tag "java.lang.Boolean"} rlm@10: *append-to-writer* false) rlm@10: rlm@10: rlm@10: (defmulti ^{:tag PrintWriter rlm@10: :doc "Attempts to coerce its argument into an open java.io.PrintWriter rlm@10: wrapped around a java.io.BufferedWriter. Argument may be an rlm@10: instance of Writer, PrintWriter, BufferedWriter, OutputStream, File, rlm@10: URI, URL, Socket, or String. rlm@10: rlm@10: If argument is a String, it tries to resolve it first as a URI, then rlm@10: as a local file name. URIs with a 'file' protocol are converted to rlm@10: local file names. rlm@10: rlm@10: Should be used inside with-open to ensure the Writer is properly rlm@10: closed." rlm@10: :arglists '([x])} rlm@10: writer class) rlm@10: rlm@10: (defn- assert-not-appending [] rlm@10: (when *append-to-writer* rlm@10: (throw (Exception. "Cannot change an open stream to append mode.")))) rlm@10: rlm@10: (defmethod writer PrintWriter [x] rlm@10: (assert-not-appending) rlm@10: x) rlm@10: rlm@10: (defmethod writer BufferedWriter [^BufferedWriter x] rlm@10: (assert-not-appending) rlm@10: (PrintWriter. x)) rlm@10: rlm@10: (defmethod writer Writer [x] rlm@10: (assert-not-appending) rlm@10: ;; Writer includes sub-classes such as FileWriter rlm@10: (PrintWriter. (BufferedWriter. x))) rlm@10: rlm@10: (defmethod writer OutputStream [^OutputStream x] rlm@10: (assert-not-appending) rlm@10: (PrintWriter. rlm@10: (BufferedWriter. rlm@10: (OutputStreamWriter. x *default-encoding*)))) rlm@10: rlm@10: (defmethod writer File [^File x] rlm@10: (let [stream (FileOutputStream. x *append-to-writer*)] rlm@10: (binding [*append-to-writer* false] rlm@10: (writer stream)))) rlm@10: rlm@10: (defmethod writer URL [^URL x] rlm@10: (if (= "file" (.getProtocol x)) rlm@10: (writer (File. (.getPath x))) rlm@10: (throw (Exception. (str "Cannot write to non-file URL <" x ">"))))) rlm@10: rlm@10: (defmethod writer URI [^URI x] rlm@10: (writer (.toURL x))) rlm@10: rlm@10: (defmethod writer String [^String x] rlm@10: (try (let [url (URL. x)] rlm@10: (writer url)) rlm@10: (catch MalformedURLException err rlm@10: (writer (File. x))))) rlm@10: rlm@10: (defmethod writer Socket [^Socket x] rlm@10: (writer (.getOutputStream x))) rlm@10: rlm@10: (defmethod writer :default [x] rlm@10: (throw (Exception. (str "Cannot open <" (pr-str x) "> as a writer.")))) rlm@10: rlm@10: rlm@10: (defn append-writer rlm@10: "Like writer but opens file for appending. Does not work on streams rlm@10: that are already open." rlm@10: [x] rlm@10: (binding [*append-to-writer* true] rlm@10: (writer x))) rlm@10: rlm@10: rlm@10: (defn write-lines rlm@10: "Writes lines (a seq) to f, separated by newlines. f is opened with rlm@10: writer, and automatically closed at the end of the sequence." rlm@10: [f lines] rlm@10: (with-open [^PrintWriter writer (writer f)] rlm@10: (loop [lines lines] rlm@10: (when-let [line (first lines)] rlm@10: (.write writer (str line)) rlm@10: (.println writer) rlm@10: (recur (rest lines)))))) rlm@10: rlm@10: (defn read-lines rlm@10: "Like clojure.core/line-seq but opens f with reader. Automatically rlm@10: closes the reader AFTER YOU CONSUME THE ENTIRE SEQUENCE." rlm@10: [f] rlm@10: (let [read-line (fn this [^BufferedReader rdr] rlm@10: (lazy-seq rlm@10: (if-let [line (.readLine rdr)] rlm@10: (cons line (this rdr)) rlm@10: (.close rdr))))] rlm@10: (read-line (reader f)))) rlm@10: rlm@10: (defn ^String slurp* rlm@10: "Like clojure.core/slurp but opens f with reader." rlm@10: [f] rlm@10: (with-open [^BufferedReader r (reader f)] rlm@10: (let [sb (StringBuilder.)] rlm@10: (loop [c (.read r)] rlm@10: (if (neg? c) rlm@10: (str sb) rlm@10: (do (.append sb (char c)) rlm@10: (recur (.read r)))))))) rlm@10: rlm@10: (defn spit rlm@10: "Opposite of slurp. Opens f with writer, writes content, then rlm@10: closes f." rlm@10: [f content] rlm@10: (with-open [^PrintWriter w (writer f)] rlm@10: (.print w content))) rlm@10: rlm@10: (defn append-spit rlm@10: "Like spit but appends to file." rlm@10: [f content] rlm@10: (with-open [^PrintWriter w (append-writer f)] rlm@10: (.print w content))) rlm@10: rlm@10: (defn pwd rlm@10: "Returns current working directory as a String. (Like UNIX 'pwd'.) rlm@10: Note: In Java, you cannot change the current working directory." rlm@10: [] rlm@10: (System/getProperty "user.dir")) rlm@10: rlm@10: rlm@10: rlm@10: (defmacro with-out-writer rlm@10: "Opens a writer on f, binds it to *out*, and evalutes body. rlm@10: Anything printed within body will be written to f." rlm@10: [f & body] rlm@10: `(with-open [stream# (writer ~f)] rlm@10: (binding [*out* stream#] rlm@10: ~@body))) rlm@10: rlm@10: (defmacro with-out-append-writer rlm@10: "Like with-out-writer but appends to file." rlm@10: [f & body] rlm@10: `(with-open [stream# (append-writer ~f)] rlm@10: (binding [*out* stream#] rlm@10: ~@body))) rlm@10: rlm@10: (defmacro with-in-reader rlm@10: "Opens a PushbackReader on f, binds it to *in*, and evaluates body." rlm@10: [f & body] rlm@10: `(with-open [stream# (PushbackReader. (reader ~f))] rlm@10: (binding [*in* stream#] rlm@10: ~@body))) rlm@10: rlm@10: (defmulti rlm@10: ^{:doc "Copies input to output. Returns nil. rlm@10: Input may be an InputStream, Reader, File, byte[], or String. rlm@10: Output may be an OutputStream, Writer, or File. rlm@10: rlm@10: Does not close any streams except those it opens itself rlm@10: (on a File). rlm@10: rlm@10: Writing a File fails if the parent directory does not exist." rlm@10: :arglists '([input output])} rlm@10: copy rlm@10: (fn [input output] [(type input) (type output)])) rlm@10: rlm@10: (defmethod copy [InputStream OutputStream] [^InputStream input ^OutputStream output] rlm@10: (let [buffer (make-array Byte/TYPE *buffer-size*)] rlm@10: (loop [] rlm@10: (let [size (.read input buffer)] rlm@10: (when (pos? size) rlm@10: (do (.write output buffer 0 size) rlm@10: (recur))))))) rlm@10: rlm@10: (defmethod copy [InputStream Writer] [^InputStream input ^Writer output] rlm@10: (let [^"[B" buffer (make-array Byte/TYPE *buffer-size*)] rlm@10: (loop [] rlm@10: (let [size (.read input buffer)] rlm@10: (when (pos? size) rlm@10: (let [chars (.toCharArray (String. buffer 0 size *default-encoding*))] rlm@10: (do (.write output chars) rlm@10: (recur)))))))) rlm@10: rlm@10: (defmethod copy [InputStream File] [^InputStream input ^File output] rlm@10: (with-open [out (FileOutputStream. output)] rlm@10: (copy input out))) rlm@10: rlm@10: (defmethod copy [Reader OutputStream] [^Reader input ^OutputStream output] rlm@10: (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)] rlm@10: (loop [] rlm@10: (let [size (.read input buffer)] rlm@10: (when (pos? size) rlm@10: (let [bytes (.getBytes (String. buffer 0 size) *default-encoding*)] rlm@10: (do (.write output bytes) rlm@10: (recur)))))))) rlm@10: rlm@10: (defmethod copy [Reader Writer] [^Reader input ^Writer output] rlm@10: (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)] rlm@10: (loop [] rlm@10: (let [size (.read input buffer)] rlm@10: (when (pos? size) rlm@10: (do (.write output buffer 0 size) rlm@10: (recur))))))) rlm@10: rlm@10: (defmethod copy [Reader File] [^Reader input ^File output] rlm@10: (with-open [out (FileOutputStream. output)] rlm@10: (copy input out))) rlm@10: rlm@10: (defmethod copy [File OutputStream] [^File input ^OutputStream output] rlm@10: (with-open [in (FileInputStream. input)] rlm@10: (copy in output))) rlm@10: rlm@10: (defmethod copy [File Writer] [^File input ^Writer output] rlm@10: (with-open [in (FileInputStream. input)] rlm@10: (copy in output))) rlm@10: rlm@10: (defmethod copy [File File] [^File input ^File output] rlm@10: (with-open [in (FileInputStream. input) rlm@10: out (FileOutputStream. output)] rlm@10: (copy in out))) rlm@10: rlm@10: (defmethod copy [String OutputStream] [^String input ^OutputStream output] rlm@10: (copy (StringReader. input) output)) rlm@10: rlm@10: (defmethod copy [String Writer] [^String input ^Writer output] rlm@10: (copy (StringReader. input) output)) rlm@10: rlm@10: (defmethod copy [String File] [^String input ^File output] rlm@10: (copy (StringReader. input) output)) rlm@10: rlm@10: (defmethod copy [*byte-array-type* OutputStream] [^"[B" input ^OutputStream output] rlm@10: (copy (ByteArrayInputStream. input) output)) rlm@10: rlm@10: (defmethod copy [*byte-array-type* Writer] [^"[B" input ^Writer output] rlm@10: (copy (ByteArrayInputStream. input) output)) rlm@10: rlm@10: (defmethod copy [*byte-array-type* File] [^"[B" input ^Writer output] rlm@10: (copy (ByteArrayInputStream. input) output)) rlm@10: rlm@10: rlm@10: (defn make-parents rlm@10: "Creates all parent directories of file." rlm@10: [^File file] rlm@10: (.mkdirs (.getParentFile file))) rlm@10: rlm@10: (defmulti rlm@10: ^{:doc "Converts argument into a Java byte array. Argument may be rlm@10: a String, File, InputStream, or Reader. If the argument is already rlm@10: a byte array, returns it." rlm@10: :arglists '([arg])} rlm@10: to-byte-array type) rlm@10: rlm@10: (defmethod to-byte-array *byte-array-type* [x] x) rlm@10: rlm@10: (defmethod to-byte-array String [^String x] rlm@10: (.getBytes x *default-encoding*)) rlm@10: rlm@10: (defmethod to-byte-array File [^File x] rlm@10: (with-open [input (FileInputStream. x) rlm@10: buffer (ByteArrayOutputStream.)] rlm@10: (copy input buffer) rlm@10: (.toByteArray buffer))) rlm@10: rlm@10: (defmethod to-byte-array InputStream [^InputStream x] rlm@10: (let [buffer (ByteArrayOutputStream.)] rlm@10: (copy x buffer) rlm@10: (.toByteArray buffer))) rlm@10: rlm@10: (defmethod to-byte-array Reader [^Reader x] rlm@10: (.getBytes (slurp* x) *default-encoding*)) rlm@10: