Part of Learn Lua with NeoVim

Learn Lua in Neovim: Comments & User Input — io.read() & tonumber() | Episode 6

Sandy LaneSandy Lane

Video: Learn Lua in Neovim: Comments & User Input — io.read() & tonumber() | Episode 6 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Lua Comments and User Input: io.read() and tonumber()

-- for single-line comments, --[[ ... ]] for multi-line. io.read() reads a line from stdin (always as a string). tonumber() to convert the string to a number.

Today's episode adds two skills: documenting your code with comments, and making your code interactive by reading input from the user.

Single-line comments

-- This is a single-line comment
print("Hello!")  -- comments can also be at the end of a line

Two dashes start a comment. Everything from -- to the end of the line is ignored by Lua.

Multi-line comments

--[[
  This is a multi-line comment.
  It can span several lines.
  Useful for longer explanations.
]]

--[[ ... ]] is the multi-line variant. Note the trick: --[[ is just -- (start a single-line comment) followed by [[ (start a long string). The whole long string becomes the comment's content. The closing ]] ends the string and the -- of the line above keeps it commented out.

A handy idiom for "comment out a block during debugging":

--[[
print("this code is disabled")
print("for now")
--]]

To turn it back on, just add a - to the opener:

---[[
print("now this runs")
print("yes really")
--]]

The first line is now ---[[ — three dashes — which is just a comment (no long string), so the rest of the lines are real Lua again. Switch between commented and uncommented with one keystroke.

Reading input with io.read()

print("What is your name?")
local name = io.read()
print("Hello, " .. name .. "!")

io.read() blocks until the user types a line and presses Enter. The line (without the trailing newline) is returned as a string.

Run this and you see:

What is your name?
Alice
Hello, Alice!

The user types "Alice", presses Enter, and the program continues with name = "Alice".

Reading numbers

print("What year were you born?")
local year = tonumber(io.read())
local age = 2026 - year
print("You are about " .. age .. " years old")

io.read() always returns a string. To do math with it, convert with tonumber():

local raw = io.read()       -- "1990" (a string)
local year = tonumber(raw)  -- 1990 (a number)

If the input isn't a valid number, tonumber() returns nil. Always validate:

local year = tonumber(io.read())
if year == nil then
  print("That wasn't a number!")
else
  print("Year: " .. year)
end

io.read modes

io.read()        -- one line (default)
io.read("*l")    -- one line (explicit, same as default)
io.read("*n")    -- one number (auto-converts)
io.read("*a")    -- entire input until EOF
io.read(5)       -- 5 characters

For reading numbers, io.read("*n") skips the explicit tonumber step:

print("What year were you born?")
local year = io.read("*n")
local age = 2026 - year

But io.read("*n") returns nil on invalid input, same as tonumber, so you still need to validate.

In Lua 5.4, the modes drop the * (so io.read("l") instead of io.read("*l")). Both still work for compatibility.

Reading multiple values per line

print("Enter two numbers:")
local a, b = io.read("*n", "*n")
print("Sum: " .. (a + b))

io.read accepts multiple format args and returns multiple values. The user types 5 10 (separated by whitespace), and a becomes 5, b becomes 10.

Closing the loop: a small program

print("=== Birthday Calculator ===")

print("What is your name?")
local name = io.read()

print("What year were you born?")
local year = tonumber(io.read())

if year == nil then
  print("Sorry, " .. name .. ", that wasn't a number.")
else
  local age = 2026 - year
  print(name .. ", you are about " .. age .. " years old.")
end

Two io.read calls, one tonumber conversion, one validation, one branching message. Small but real.

Common stumbles

Forgetting tonumber(). 2026 - io.read() errors at runtime — can't subtract a string from a number.

Not validating tonumber result. Bad input silently becomes nil; the next line crashes. Always check.

Trying to read in non-interactive context. When you run lua script.lua < input.txt, io.read() reads from the file. When you run from inside Neovim with :!lua %, stdin is connected to your terminal — input works.

Comments inside long strings. local s = [[ -- not a comment ]] — the dashes are literal characters in the string, not a comment.

Using --[[ without thinking about closing. A stray ]] later in the file ends the comment block early. Use the dash-toggle trick so blocks are easy to flip.

What's next

Episode 7: if statements. if, elseif, else. Build a grade calculator that maps a score to a letter grade.

Recap

Comments: -- for single-line, --[[ ... ]] for multi-line. The ---[[ trick lets you toggle blocks in/out with one keystroke. io.read() reads a line as a string. tonumber() converts the string to a number; returns nil on bad input — always validate. io.read("*n") reads and converts in one step. Multiple values: local a, b = io.read("*n", "*n").

Next episode: if, elseif, else.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.