LISP - Reading Data from Streams
Lisp - Reading Data From Streams
Section titled “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.
The Stream Reading Toolkit
Section titled “The Stream Reading Toolkit”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: Usereadonly 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.
Example: Reading a File Line by Line
Section titled “Example: Reading a File Line by Line”Suppose we have a file named config.txt with the following content:
host=localhostport=8080user=adminHere’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")Output
Section titled “Output”--- Reading config from config.txt ---Read line: "host=localhost"Read line: "port=8080"Read line: "user=admin"--- End of file ---Explanation
Section titled “Explanation”with-open-file: Binds the opened file stream to the variablestream. 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 firstnilmeans 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 fornil, which could be a valid empty line in some contexts.loop ... until (eq line :eof): The loop continues untilread-linereturns our special:eofsymbol.
Advanced Error Handling
Section titled “Advanced Error Handling”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")Output
Section titled “Output”Error: Could not open file non-existent-file.txt. Details: The file non-existent-file.txt does not exist.