Skip to content

LISP - Reading Data from Streams

Input/Output (I/O) is fundamental to any application. In Common Lisp, I/O is handled through streams. This tutorial explores the modern and safe ways to read data from streams, such as user input from the console or content from a file.

Common Lisp provides a tiered set of functions for reading, from individual characters to entire Lisp objects.

  • Expression-level: read: Parses a full Lisp S-expression from a stream. It can read numbers, strings, symbols, and lists. Security Warning: Use read only with trusted input, as it can execute code if it parses an expression like #.(+ 2 2).
  • Line-level: read-line: Reads a string of characters until a newline is reached. This is one of the most common ways to read textual data.
  • Character-level: read-char: Reads a single character from the stream. Useful for fine-grained parsing.
  • Binary-level: read-byte: Reads a single byte from a binary stream. Essential for working with files like images or executables.
  • Lookahead: peek-char: Allows you to look at the next character in the stream without consuming it. This is useful for making parsing decisions.

Best Practice: Safe File Handling with with-open-file

Section titled “Best Practice: Safe File Handling with with-open-file”

Manually opening and closing files is error-prone. If an error occurs after opening a file but before closing it, the file handle can be left dangling. The with-open-file macro robustly handles this by guaranteeing the file is closed, even in the event of an error. You should always prefer with-open-file.

Suppose we have a file named config.txt with the following content:

host=localhost
port=8080
user=admin

Here’s the modern, safe way to read and process it:

(defun process-config-file (path)
"Reads a configuration file and prints its contents."
(with-open-file (stream path
:direction :input
:if-does-not-exist :error)
(format t "--- Reading config from ~a ---~%" path)
(loop for line = (read-line stream nil :eof)
until (eq line :eof)
do (format t "Read line: ~s~%" line))
(format t "--- End of file ---~%")))
;; To run it:
;; (process-config-file "config.txt")
--- Reading config from config.txt ---
Read line: "host=localhost"
Read line: "port=8080"
Read line: "user=admin"
--- End of file ---
  • with-open-file: Binds the opened file stream to the variable stream. It automatically closes the stream when the block exits.
  • :direction :input: Specifies that we are opening the file for reading.
  • :if-does-not-exist :error: This is a safe default. If the file is not found, an error is signaled.
  • read-line stream nil :eof: This is a key idiom. The first nil means we don’t want an error at EOF. The second argument, :eof, is a special symbol we choose to be returned when the end of the file is reached. This is more robust than checking for nil, which could be a valid empty line in some contexts.
  • loop ... until (eq line :eof): The loop continues until read-line returns our special :eof symbol.

Lisp’s Condition System is more powerful than traditional exceptions. You can use handler-case to gracefully handle specific errors, like a file not being found.

(defun safe-read-config (path)
(handler-case (process-config-file path)
(file-error (c)
(format *error-output* "Error: Could not open file ~a. Details: ~a~%" path c)
nil))) ;; Return nil on failure
;; (safe-read-config "non-existent-file.txt")
Error: Could not open file non-existent-file.txt. Details: The file non-existent-file.txt does not exist.