← Nmap Scripting Engine (NSE): Writing Your Own Scripts

Lesson 3 of 10

Lua basics I: syntax and control flow

Variables, types, operators, conditionals and loops: the core of the language, with the gotchas that trip up newcomers.

45 minHands-on lab

Lua is a small, fast scripting language designed to be embedded in other programs. Its whole grammar fits on a page, which is why you can learn what NSE needs in two lessons. Recent Nmap releases embed Lua 5.3 or 5.4 (nmap --version tells you which); everything here works in both.

Trying Lua

Nmap doesn’t give you a REPL, but for practice you can install a standalone interpreter (lua5.4 on most Linux distributions, brew install lua on macOS) and run:

lua           # interactive
lua file.lua  # run a file

Or paste snippets into any online Lua 5.4 playground. Everything in these two lessons is plain Lua, not NSE-specific.

Comments, variables and scope

-- a single-line comment
--[[ a multi-line
     comment ]]

local name = "nmap"   -- local: visible in this block only
port = 80             -- global: visible everywhere (avoid!)

Types

Lua has eight types. You’ll use six of them daily:

TypeExampleNotes
nilnil“no value”. An unset variable is nil.
booleantrue, false
number42, 3.14Integers and floats (Lua 5.3+ distinguishes them).
string"text", 'text'Immutable byte strings, so they’re safe for binary data.
table{1, 2, 3}, {a = 1}The only data structure: arrays and dictionaries.
functionfunction() endFunctions are values.
print(type(nil), type(true), type(3), type("x"), type({}), type(print))
--> nil  boolean  number  string  table  function

Truthiness: the rule that surprises everyone

In a condition, only nil and false are false. Everything else is true, including 0 and the empty string.

if 0 then print("zero is true") end       -- prints
if "" then print("empty string is true") end  -- prints
if nil then print("never") end

Numbers and operators

print(7 / 2, 7 // 2, 7 % 2, 2 ^ 10)   --> 3.5  3  1  1024.0
print(10 == 10.0)                      --> true
print(3 ~= 4)                          --> true   (not-equal is ~=, not !=)
print(not nil, true and 5, false or "default")   --> true  5  default

Two operators worth memorising:

  • .. concatenates strings: "port " .. 80 gives "port 80". Numbers are converted automatically, but nil is not: "x" .. nil raises an error. That’s the number-one NSE runtime error.
  • # gives the length of a string or an array-style table: #"nmap" is 4.

The and/or default idiom

a or b returns a if it’s truthy, otherwise b. You’ll use it for defaults everywhere:

local timeout = tonumber(user_value) or 5000
local path = args.path or "/"

The ternary version is cond and x or y. It breaks if x can be false or nil, so use an if when in doubt.

Strings

local s = "Hello, NSE"
print(#s)                   --> 10
print(s:upper(), s:lower()) --> HELLO, NSE   hello, nse
print(s:sub(1, 5))          --> Hello         (1-based, inclusive)
print(("%s:%d"):format("10.0.0.5", 80))   --> 10.0.0.5:80
print(tostring(80) .. "/tcp")             --> 80/tcp
print(tonumber("0x50"), tonumber("abc"))  --> 80  nil

s:method(...) is sugar for string.method(s, ...). The colon passes s as the first argument. Forgetting the colon (s.upper()) is a classic beginner mistake. Long strings use [[ ... ]], which is what NSE uses for description.

Conditionals

local state = "open"

if state == "open" then
  print("scan it")
elseif state == "filtered" then
  print("maybe")
else
  print("skip")
end

Note elseif (one word), then, and the closing end.

Loops

-- numeric for: start, stop (inclusive), optional step
for i = 1, 3 do print(i) end          --> 1 2 3
for i = 10, 1, -3 do print(i) end     --> 10 7 4 1

-- while and repeat
local n = 3
while n > 0 do n = n - 1 end
repeat n = n + 1 until n >= 3

-- iterating tables (next lesson covers tables properly)
local ports = {22, 80, 443}
for i, p in ipairs(ports) do print(i, p) end   -- ordered, stops at first nil
local t = {a = 1, b = 2}
for k, v in pairs(t) do print(k, v) end        -- any keys, unspecified order

break leaves a loop early; goto continue with a ::continue:: label is Lua’s continue.

ipairs versus pairs: use ipairs for arrays (predictable order), pairs for dictionaries. Never rely on pairs order.

Lab

  1. Port classifier. Given a number, print "well-known" for 1 to 1023, "registered" for 1024 to 49151, "dynamic" for 49152 to 65535, and "invalid" otherwise.
  2. Port range expander. Loop from 8000 to 8005 and print "8000/tcp", "8001/tcp" and so on using ...
  3. Default arguments. Set local depth = tonumber(nil) or 3 and print it. Then change it to tonumber("7") or 3.
  4. Truthiness check. Predict, then run: print(0 and "a" or "b"), print(nil and "a" or "b").
Solution sketch for exercise 1
local function classify(p)
  if type(p) ~= "number" or p < 1 or p > 65535 then return "invalid" end
  if p <= 1023 then return "well-known" end
  if p <= 49151 then return "registered" end
  return "dynamic"
end
print(classify(22), classify(8080), classify(60000), classify(0))

Checkpoint

What does print("port " .. nil) do?

It raises “attempt to concatenate a nil value”. Convert with tostring() or check for nil first.

What does print(0 and "a" or "b") print?

a. Zero is truthy in Lua.