Working With Files In Lua (Basic)

So far, we’ve learned a lot about working with data within a program, and how to include more Lua into a program, but not how to import and export data from a file. Lua has plenty of builtin features to handle this via the I/O Library. The I/O library works with both text and binary files.

Lua is Unicode agnostic. This means that all strings are treated effectively as binary data rather than any kind of encoding. Because of this, you rarely need to use a Unicode library except for more specific use cases.

Open and Close

Before we get into either reading or writing, we need to discuss opening and closing files. Your workflow will typically involve opening a file, doing whatever with it, then explicitly closing it. When you exit the program, the file should close as well, but that’s considered poor practice.

To open a file:

io.open( [filename] [, [mode]] )

The filename is where the file is, and the mode is listed in the table below (or following the same mode as in C for fopen). If there is no mode, r is implied.

r(Default) Open the file read only.
wOpen the file to write. Overwrites contents or makes a new file.
aAppend the file. Write to the end or make a new file. Repositioning operations are ignored.
r+Open the file in read and write mode. The file must exist.
w+Open the file to write. Clears existing contents or makes a new file.
a+Append the file with read mode. Write to the end or make a new file. Repositioning operations affect read mode.

You can also use the b modifier at the end of the mode in order to open the file in binary mode. For example:

io.open( [file], "r+b" )

io.open returns a file handle object. When working with opening files, you should assign the file handle to a variable to do anything useful.

local filehandle = io.open( "mycontents.txt", "rb" )

Once you’re done with a file, you need to close it.

local filehandle = io.open( "mycontents.txt", "rb" )
…
filehandle:close()

Reading Files

Once you open your file, you can read it. Reading is done via:

filehandle:read( "[option]" [or: [number]] )

The options correspond to the following table for Lua 5.1 and 5.2:

*line*lReads a single line from the file.
*all*aReads all of the content from the file.
*number*nReads a number from the file. This reads until the end of a valid sequence.
[number](integer)Reads [number] characters (bytes) from the file. Does not use quotes.

Lua 5.3 is adds the following (but is backwards compatible):

aReads all of the content from the file.
lReads a single line from the file.
LReads a single line from the file, but keeps the end of line character(s)
nReads a number from the file. This reads until the end of a valid sequence.
[number]Reads [number] characters (bytes) from the file. Does not use quotes.

These basically all return nil if there isn’t a valid sequence, except for the “all” options which return an empty string as long as the file is valid.

Let’s see it all in action with test.txt:

123
line 2
123456789

(Note the new line at the end of test.txt)

Let’s try each of our options out for reading and see what we get. We’re going to start the whole file over and over again to avoid any issues.

#!/usr/bin/lua5.1

local file = io.open( "test.txt", "r" )

local contents = file:read( "*all" )
print( contents )

file:close()

This gets us:

./file1.lua 
123
line 2
123456789

Let’s see all of our options smashed together now that we’ve seen the basic structure:

#!/usr/bin/lua5.1

print( "*all" )
local file = io.open( "test.txt", "r" )
local contents = file:read( "*all" )
print( contents )
file:close()

print()
print( "*line" )
local file = io.open( "test.txt", "r" )
local contents = file:read( "*line" )
print( contents )
file:close()

print()
print( "*number" )
local file = io.open( "test.txt", "r" )
local contents = file:read( "*number" )
print( contents )
file:close()

print()
print( "6" )
local file = io.open( "test.txt", "r" )
local contents = file:read( 6 )
print( contents )
file:close()

Which gets us:

./file1.lua 
*all
123
line 2
123456789


*line
123

*number
123

6
123
li

Let’s do the same with Lua 5.3:

#!/usr/bin/lua5.3

print( "a" )
local file = io.open( "test.txt", "r" )
local contents = file:read( "a" )
print( contents )
file:close()

print()
print( "l" )
local file = io.open( "test.txt", "r" )
local contents = file:read( "l" )
print( contents )
file:close()

print()
print( "L" )
local file = io.open( "test.txt", "r" )
local contents = file:read( "L" )
print( contents )
file:close()

print()
print( "n" )
local file = io.open( "test.txt", "r" )
local contents = file:read( "n" )
print( contents )
file:close()

print()
print( "6" )
local file = io.open( "test.txt", "r" )
local contents = file:read( 6 )
print( contents )
file:close()

And we get:

./file1.lua 
a
123
line 2
123456789


l
123

L
123


n
123

6
123
li

Moving Around In Files

Lua offers a seek function. Seek works the same in 5.1, 5.2, and 5.3.

[file]:seek( "[start]" [, [offset]] )

This has the following starting points:

setSet to start from the beginning of the file.
curMove from the current position the file is at.
endMove from the end of the file.

Since we start at the beginning of the file, we’d get the following with cur:

#!/usr/bin/lua5.3

print( "a" )
local file = io.open( "test.txt", "r" )
file:seek("cur", 3)
local contents = file:read( "a" )
print( contents )
file:close()

print()
print( "l" )
local file = io.open( "test.txt", "r" )
file:seek("cur", 3)
local contents = file:read( "l" )
print( contents )
file:close()

print()
print( "L" )
local file = io.open( "test.txt", "r" )
file:seek("cur", 3)
local contents = file:read( "L" )
print( contents )
file:close()

print()
print( "n" )
local file = io.open( "test.txt", "r" )
file:seek("cur", 3)
local contents = file:read( "n" )
print( contents )
file:close()

print()
print( "6" )
local file = io.open( "test.txt", "r" )
file:seek("set", 3)
local contents = file:read( 6 )
print( contents )
file:close()

Which gets us:

./file2.lua 
a

line 2
123456789


l


L



n
nil

6

line

Let’s test a few other methods:

#!/usr/bin/lua5.3

print( "a" )
local file = io.open( "test.txt", "r" )
file:seek("cur", 3)
local contents = file:read( "a" )
print( contents )

print()
print( "l" )

file:seek("set")
local contents2 = file:read( "l" )
print( contents2 )

print()
print( "n" )

file:seek("end", -3)
local contents3 = file:read( "n" )
print( contents3 )
file:close()

Which gets us:

./file3.lua 
a

line 2
123456789


l
123

n
89

[file]:seek() gets you the current position for later usage.

More File Operations

[file]:flush() syncs the data to disk for the file.

The Lua Manual has a lot more than we can include here. See these for a reference to some of the functions as written in 5.3.

Writing Files

Luckily, in Lua, writing is way easier than reading. You just use [file]:write( [contents] ) to write to the given file.

#!/usr/bin/lua5.1

local file = io.open( "testout.txt", "w" )
file:write( "here's a line\n" )
file:write( "and another\n" )
file:write( "and yet another\n" )
file:close()

local file2 = io.open( "testout.txt", "r" )
local content = file2:read( "*a" )
print( content )
file2:close()

This gets you:

/file3.lua 
here's a line
and another
and yet another

(Note: the extra end line)

What happens if you don’t use /n (the newline control character)?

Making It Cleaner

We’re going to get into Lua Idioms with the assert function. This function basically makes Lua check whether the condition is going to succeed or not and gracefully fail with an error.

Assert:

assert( [condition] [, [error message]] )

Let’s see it in action:

#!/usr/bin/lua5.1

local file = assert( io.open( "testout.txt", "w" ) )
file:write( "here's a line\n" )
file:write( "and another\n" )
file:write( "and yet another\n" )
file:close()

local file2 = assert( io.open( "testout-DOESNOTEXIST.txt", "r" ) )
local content = file2:read( "*a" )
print( content )
file2:close()

Where we get:

/file3.lua 
/usr/bin/lua5.1: ./file3.lua:9: testout-DOESNOTEXIST.txt: No such file or directory
stack traceback:
	[C]: in function 'assert'
	./file3.lua:9: in main chunk
	[C]: ?

We’ve now cleanly errored out of the program without trying to read a file which does not exist.

Let’s learn about io.type( [filehandle] ). This is extremely useful for checking on if a file is able to get a file handle. io.type( [filehandle] ) returns either “file” (the handle is valid), “closed file” (the handle is closed), or nil (it failed). Here’s an example:

#!/usr/bin/lua5.1

local file = assert( io.open( "testout.txt", "w" ) )
file:write( "here's a line\n" )
file:write( "and another\n" )
file:write( "and yet another\n" )
file:close()

local file2 = io.open( "testout-DOESNOTEXIST.txt", "r" )
if( io.type( file2 ) == "file" ) then
	local content = file2:read( "*a" )
	print( content )
	file2:close()
else
	print( "--error--" )
end

This gets us:

./file3.lua 
--error--

Now, instead of just exiting out, we have error handling. This is far cleaner than just using assert and exiting out. The assert function still has its place for debugging, but now we’ve made our program fault resilient and graceful as long as we handle it correctly.