I/O Operations in Lua
Learn console output, reading input, file reading and writing, formatted output, and I/O error handling in Lua with Docker-ready examples
Input and output are how a program reaches the outside world—the terminal, files on disk, and pipes between processes. In the Hello World tutorial you met print, but that function is only the friendly front door. Lua keeps its I/O deliberately small: the io library gives you a handful of functions built around file handles, and the same handle-based API works for the console, real files, and sub-processes alike.
Two design choices shape everything below. First, Lua distinguishes print—a convenience that adds tabs between arguments and a trailing newline—from io.write, which writes exactly the bytes you give it and nothing more. Second, and true to Lua’s minimalist, scripting-oriented character, most I/O functions that can fail do not raise an error. Instead they return nil plus a message, leaving you to decide whether to handle it or promote it to an error with assert.
In this tutorial you will learn to control console output precisely, read input from the user, write and read files through handles, produce formatted output with string.format, and handle the failures that come with touching the filesystem. Because Lua’s standard library is small, these few functions cover the vast majority of everyday I/O.
Console Output
print is built for quick output: it converts every argument with tostring, joins them with tab characters, and ends the line. io.write is the lower-level tool—it accepts only strings and numbers, adds no separators or newline, and returns the file handle so calls can be chained. For structured text, string.format provides C-style printf specifiers.
Create a file named console_output.lua:
| |
Running this produces:
print separates with tabs: a b 42
io.write stays on one line
Sum: 5
Name: Ada, Age: 36
Pi to 2 places: 3.14
Hex: ff, Padded: | 42|
Notice the tab characters between a, b, and 42 in the first line—that spacing is print’s doing. The three chained io.write calls, by contrast, land on a single line exactly as written.
Reading Console Input
io.read pulls from standard input. Its argument selects a format: "l" reads one line and discards the newline, "n" reads a number and returns it as a Lua number, and "a" reads everything until end of input. With no argument it defaults to "l".
Create a file named greet_input.lua:
| |
Because the prompts use io.write (no newline) and piped input is not echoed back, feeding Ada and 7 into the program produces:
Enter your name: Enter a number: Hello, Ada!
Its square is 49
When you run this interactively in a terminal, your typed input appears between each prompt and the response. Note that io.read("n") returns an actual number, so n * n is arithmetic—not string concatenation.
Writing and Reading Files
io.open(filename, mode) returns a file handle you call methods on: file:write(...), file:read(format), and file:close(). Common modes are "w" (write, truncating any existing file), "a" (append), and "r" (read, the default). The read formats mirror io.read: "a" slurps the whole file, "l" reads one line, "n" reads a number.
For line-by-line iteration, io.lines(filename) is the idiomatic choice—it opens the file, yields one line per iteration, and closes the file automatically when the loop ends.
Create a file named io_operations.lua:
| |
The -v $(pwd):/app Docker mount (shown below) lets the container write notes.txt back into your current directory, so you can open the file yourself after the run.
I/O Error Handling
Here Lua’s convention shows its character: io.open does not raise when a file is missing. It returns nil, a human-readable message, and a numeric error code. You test the first return value and react accordingly. When you would rather fail loudly, wrap the call in assert, which turns the nil-plus-message pair into a raised error—catchable with pcall.
Create a file named safe_read.lua:
| |
Running this yields:
Error: could not open 'missing.txt'
Reason from OS: missing.txt: No such file or directory
pcall succeeded: false
The exact wording of the OS message comes from the operating system, so it may differ slightly on other platforms, but the structure—filename: reason—is consistent.
Running with Docker
You can run every example without installing Lua locally.
| |
Expected Output
Running io_operations.lua produces:
File written.
Full contents:
Lua I/O Demo
Line 2 of 3
Line 3 of 3
Numbered lines:
1: Lua I/O Demo
2: Line 2 of 3
3: Line 3 of 3
Total lines now: 4
Key Concepts
printvsio.write—printis a convenience thattostrings every argument, joins them with tabs, and adds a newline;io.writewrites exactly the strings and numbers you pass, with no separators or newline, making it the tool for precise output.- File handles drive file I/O —
io.openreturns a handle, and you callhandle:write,handle:read, andhandle:closeon it; always close what you open (or letio.linesdo it for you). - Read formats select what you get —
"a"reads the entire file,"l"reads a line (newline stripped), and"n"reads a number; the same formats work forio.readon standard input. io.linesis the idiomatic iterator — it opens the file, yields one line per loop step, and closes the file automatically when the loop finishes, so it needs no explicit cleanup.- Failure returns
nil, it does not raise —io.openand friends returnnilplus a message on error rather than throwing, so check the return value before using a handle. assertandpcallpromote and catch errors — wrap a fallible call inassert(io.open(...))to raise on failure, and usepcallto catch that error and keep the script running.- Input arrives as text unless you ask otherwise —
io.read("l")gives you a string; useio.read("n"),tonumber, orstring.formatwhen numbers are involved.
Running Today
All examples can be run using Docker:
docker pull nickblah/lua:5.4-alpine
Comments
Loading comments...
Leave a Comment