Skip to content

LISP - String I/O

String I/O refers to operations that treat strings as sources or sinks of data. This is a powerful feature in Lisp, enabling easy data serialization, parsing, and dynamic string construction. This is fundamental for web development, configuration file management, and generating formatted text.

Reading from Strings: Creating Input Streams

Section titled “Reading from Strings: Creating Input Streams”

Common Lisp provides two primary ways to treat a string as an input source.

This is the most common and flexible method. It creates a temporary character input stream from a string, allowing you to use standard stream functions like read, read-line, and read-char on it. The stream is automatically closed when the macro’s body exits.

;; Example: Parsing a string of numbers
(let ((data-string "10 20 30.5 an-atom"))
(with-input-from-string (s data-string)
(loop for item = (read s nil :eof)
until (eq item :eof)
do (format t "Read item: ~a (Type: ~a)~%" item (type-of item)))))
Read item: 10 (Type: (INTEGER 0 4611686018427387903))
Read item: 20 (Type: (INTEGER 0 4611686018427387903))
Read item: 30.5 (Type: SINGLE-FLOAT)
Read item: AN-ATOM (Type: SYMBOL)

The (read s nil :eof) pattern is idiomatic Lisp for reading objects until the end of a stream.

This function parses one Lisp object from a string. It’s useful for quick parsing but less flexible than a full stream.

Security Warning: read and read-from-string are powerful because they use the Lisp reader, which can execute code (e.g., #.(+ 1 2)). Never use them on untrusted user input. For untrusted data, parse it line-by-line with read-line and process it safely.

(let* ((str "(list 1 2 3) and more")
(obj (read-from-string str)))
(print obj))
;; It also returns the position where reading stopped.
(multiple-value-bind (expr pos) (read-from-string str)
(format t "Expression: ~s~%Stopped at index: ~d~%" expr pos))
(LIST 1 2 3)
Expression: (LIST 1 2 3)
Stopped at index: 13

Writing to Strings: Creating Output Streams

Section titled “Writing to Strings: Creating Output Streams”

Similarly, Lisp provides elegant ways to capture output into a string.

format is Lisp’s versatile printing and string formatting powerhouse. By passing nil as its first argument, it returns the formatted result as a new string instead of printing to a stream.

(let* ((name "Alice")
(items 3)
(message (format nil "Hello, ~a! You have ~d item~:p in your cart." name items)))
(print message))
;; Demonstrating with 1 item to show the pluralization directive `~:p`
(let ((message-singular (format nil "Hello, Bob! You have ~d item~:p." 1)))
(print message-singular))
"Hello, Alice! You have 3 items in your cart."
"Hello, Bob! You have 1 item."

When you need to build a string incrementally from multiple print, write, or format calls, this macro is the perfect tool. It creates a temporary character output stream and returns everything written to it as a single string.

;; Example: Generating a simple HTML list
(let ((items '("Apples" "Bananas" "Cherries")))
(let ((html (with-output-to-string (s)
(write-string "<ul>~%" s)
(dolist (item items)
(format s " <li>~a</li>~%" item))
(write-string "</ul>" s))))
(print html)))
"<ul>
<li>Apples</li>
<li>Bananas</li>
<li>Cherries</li>
</ul>"
  • read vs. read-line: Use read when your string contains valid Lisp data structures you want to parse directly. Use read-line when dealing with plain text, line-oriented data (like logs or CSV files), or untrusted input.
  • Performance: For building complex strings in a loop, with-output-to-string is generally more efficient than repeated string concatenation.
  • Integration: The beauty of Lisp’s stream-based I/O is its uniformity. A function that takes a stream argument can work on files, strings, or network connections without modification. This makes code highly reusable.
  • Safety First: Re-iterating the security warning: never pass raw, un-sanitized input from an external source (like a web request) to read or read-from-string.