Skip to content

LISP - Input − Output

Input and Output (I/O) are fundamental operations that allow a program to interact with the outside world. Common Lisp provides a rich and flexible set of tools for handling I/O, from reading user input to writing formatted data to files. This tutorial covers the essential functions and modern best practices for I/O in Common Lisp.

In Lisp, all I/O operations happen on streams. A stream is a source or sink of data, such as the terminal, a file, or even a string in memory. Lisp provides several standard streams:

  • *standard-input*: The default stream for reading input, typically the keyboard.
  • *standard-output*: The default stream for normal program output, typically the screen.
  • *error-output*: The stream for error messages.
  • *query-io*: The stream for interactive prompting and user conversations.

The following table provides some of the most commonly used input functions in Common Lisp.

RankFunction & Modern Usage
1read &optional input-stream eof-error-p eof-value recursive-p

Reads a full Lisp object (an S-expression) from a stream. This is powerful but can be a security risk if reading from untrusted sources, as it can execute code. Best used for configuration files or internal data.
2read-line &optional input-stream eof-error-p eof-value recursive-p

Reads a line of text as a string. This is the safest and most common way to get user input.
3read-char &optional input-stream eof-error-p eof-value recursive-p

Reads a single character from the stream.
4read-from-string string &key …

Reads a Lisp object from a string, not a stream. Useful for parsing.
5parse-integer string &key :start :end :radix :junk-allowed

Safely parses an integer from a string. This is the recommended way to convert string input to a number.
6peek-char &optional peek-type input-stream …

Looks at the next character in the stream without consuming it. Useful for parsing complex text formats.
7read-byte binary-input-stream &optional …

Reads a single byte from a binary stream. Essential for working with binary files like images or network protocols.

The modern approach to reading user input is to read it as a line of text using read-line and then parse it. This avoids the security and usability issues of read.

Let’s write a program that asks for the user’s name and prints a greeting. This demonstrates clean, interactive I/O.

;;; main.lisp
(defun greet-user ()
"Asks for the user's name and prints a personalized greeting."
;; Use princ for prompts without quotes and terpri for a new line.
(princ "Please enter your name: ")
;; Ensure the prompt is displayed before waiting for input.
(finish-output *query-io*)
;; Read the entire line of input safely.
(let ((name (read-line *query-io*)))
(format t "~%Hello, ~a! Welcome to modern Lisp programming.~%" name)))
(greet-user)
Please enter your name: Alice
Hello, Alice! Welcome to modern Lisp programming.

This example combines reading text, parsing it into a number, performing a calculation, and printing formatted output.

;;; main.lisp
(defun calculate-circle-area ()
"Prompts for a radius, calculates the circle's area, and prints the result."
(princ "Enter the radius of the circle: ")
(finish-output *query-io*)
(let ((radius-string (read-line *query-io*)))
;; Use a handler to gracefully manage invalid input.
(handler-case
;; Safely parse the string to a float.
(let* ((radius (parse-number:parse-number radius-string))
(area (* pi radius radius)))
(format t "A circle with radius ~,2f has an area of ~,2f.~%" radius area))
;; If parsing fails, this error is caught.
(error (c)
(format *error-output* "~%Invalid input: '~a' is not a valid number. ~a~%"
radius-string c)))))
;; Note: This example assumes a library like `parse-number` is available.
;; You could also use `(read-from-string radius-string)` but it's less safe.
;; For a standard library only solution, you might do:
;; (let ((radius (read-from-string radius-string))) ...)
(calculate-circle-area)
Enter the radius of the circle: 5.5
A circle with radius 5.50 has an area of 95.03.
Enter the radius of the circle: five
Invalid input: 'five' is not a valid number. Could not parse "five" as a number.

Common Lisp offers several functions for printing data. The format function is by far the most powerful and versatile.

RankFunction and Description
1format destination control-string &rest arguments

Produces beautifully formatted output to a stream or a string. This is the go-to function for almost all output.
2princ object &optional output-stream

Prints an object in a human-readable format (e.g., strings without quotes). Good for prompts and messages.
3prin1 object &optional output-stream

Prints an object in a machine-readable format (e.g., strings with quotes). The output can be read back into Lisp with read.
4print object &optional output-stream

Like prin1, but adds a newline before and a space after the object.
5pprint object &optional output-stream

The “pretty printer”. It formats complex Lisp objects (like nested lists or structures) with indentation to make them easy to read.
6write-line string &optional output-stream

Writes a string to a stream, followed by a newline character.
7terpri &optional output-stream

(TERminate PRInt) Outputs a newline character.
8finish-output & force-output &optional output-stream

These functions manage output buffering, ensuring that data is actually sent to its destination (e.g., the terminal or a file).

The format function uses a control string with special directives, each starting with a tilde (~), to format its arguments. The first argument to format is the destination: t for *standard-output*, nil to return the formatted text as a string, or a specific stream.

Here are some of the most useful format directives:

DirectiveDescription
~aAesthetic (human-readable) representation. (like princ)
~sStandard (machine-readable) representation. (like prin1)
~dDecimal integer.
~fFixed-format floating-point number. ~,2f rounds to 2 decimal places.
~%Outputs a newline character.
~&Fresh-line. Outputs a newline only if not already at the start of a line.
~~Outputs a literal tilde character.
~tTabulate. Used for creating columns.
~?Indirection. The argument is a format string itself, with its own arguments.

Let’s demonstrate the power of format by creating a simple, well-aligned report from a list of data.

;;; main.lisp
(defun generate-sales-report (data)
"Generates a formatted sales report from a list of (item price quantity) lists."
(format t "~&~va ~20t~va ~30t~va~%" "Item" "Price" "Total")
(format t "~40,,,'-A~%" "") ; Print a separator line
(dolist (record data)
(let* ((item (first record))
(price (second record))
(quantity (third record))
(total (* price quantity)))
(format t "~va ~20t$~,2f ~30t$~,2f~%" item price total))))
(let ((sales-data '(("Keyboard" 75.00 2)
("Mouse" 25.50 3)
("Monitor" 300.00 1))))
(generate-sales-report sales-data))
Item Price Total
----------------------------------------
Keyboard $75.00 $150.00
Mouse $25.50 $76.50
Monitor $300.00 $300.00

In modern Lisp development, you’ll often interact with files in standard formats like JSON or CSV. While Common Lisp has built-in functions for Lisp’s own data syntax, you’ll use libraries for others. Quicklisp is the de-facto package manager for Common Lisp, making it easy to install libraries for tasks like:

  • Parsing JSON: The jonathan or cl-json libraries.
  • Parsing CSV: The cl-csv library.
  • File Path Management: The uiop:merge-pathnames* or pathname-utils library for cross-platform path handling.