Lua Fundamentals

Lua is simple but powerful. Master these basics to configure Neovim effectively.

Variables & Types

Basic Types

-- Numbers (no int/float distinction)
local port = 443
local timeout = 30.5

-- Strings
local hostname = "ise-01.inside.domusdigitalis.dev"
local multiline = [[
This is a
multiline string
]]

-- Booleans
local is_active = true
local has_error = false

-- Nil (absence of value)
local result = nil

-- Check type
print(type(port))      -- "number"
print(type(hostname))  -- "string"
print(type(is_active)) -- "boolean"
print(type(result))    -- "nil"

Local vs Global

-- ALWAYS use local (global is bad practice)
local good_variable = "scoped"

-- Global (avoid in Neovim configs)
bad_variable = "pollutes global namespace"

-- Access global explicitly
_G.my_global = "explicit global"

-- In Neovim, use vim.g for global state
vim.g.my_setting = true

Type Conversion

-- String to number
local port = tonumber("443")        -- 443
local invalid = tonumber("hello")   -- nil

-- Number to string
local port_str = tostring(443)      -- "443"

-- Boolean conversion
-- Only nil and false are falsy
if "" then print("empty string is truthy") end  -- prints!
if 0 then print("zero is truthy") end           -- prints!
if nil then print("nil is falsy") end           -- doesn't print

Strings

String Operations

local hostname = "ise-01.inside.domusdigitalis.dev"

-- Length
print(#hostname)  -- 32

-- Concatenation
local url = "https://" .. hostname .. ":443"

-- Substring (1-indexed!)
local first = string.sub(hostname, 1, 6)   -- "ise-01"
local last = string.sub(hostname, -3)      -- "dev"

-- Find
local pos = string.find(hostname, "inside")  -- 8

-- Replace
local new = string.gsub(hostname, "ise%-01", "ise%-02")

-- Case
local upper = string.upper(hostname)
local lower = string.lower(hostname)

-- Format
local msg = string.format("Host: %s, Port: %d", hostname, 443)

Pattern Matching

Lua uses its own pattern syntax (not regex):

local text = "ISE-01: 10.50.1.20"

-- %d = digit, + = one or more
local ip = string.match(text, "%d+%.%d+%.%d+%.%d+")  -- "10.50.1.20"

-- Patterns
-- %a = letter, %d = digit, %s = space, %w = alphanumeric
-- %l = lowercase, %u = uppercase, %p = punctuation
-- . = any char, * = zero or more, + = one or more, - = zero or more (non-greedy)
-- ^ = start, $ = end, [] = character class

-- Extract parts
local host, ip = string.match(text, "(%w+%-%d+): (%d+%.%d+%.%d+%.%d+)")
print(host, ip)  -- "ISE-01", "10.50.1.20"

-- gmatch iterator
for word in string.gmatch("one two three", "%w+") do
    print(word)
end

Operators

Arithmetic

local a, b = 10, 3

print(a + b)   -- 13
print(a - b)   -- 7
print(a * b)   -- 30
print(a / b)   -- 3.333...
print(a // b)  -- 3 (floor division, Lua 5.3+)
print(a % b)   -- 1 (modulo)
print(a ^ b)   -- 1000 (power)
print(-a)      -- -10 (negation)

Comparison

local a, b = 10, 20

print(a == b)   -- false (equal)
print(a ~= b)   -- true  (NOT equal - note: ~= not !=)
print(a < b)    -- true
print(a <= b)   -- true
print(a > b)    -- false
print(a >= b)   -- false

Logical

-- and, or, not
print(true and false)   -- false
print(true or false)    -- true
print(not true)         -- false

-- Short-circuit evaluation
-- and returns first falsy or last value
print(nil and "hello")     -- nil
print("hello" and "world") -- "world"

-- or returns first truthy or last value
print(nil or "default")    -- "default"
print("value" or "default") -- "value"

-- Common idiom: default value
local name = input_name or "unknown"

-- Ternary equivalent
local result = condition and value_if_true or value_if_false

String

-- Concatenation
local full = "hello" .. " " .. "world"

-- Length
local len = #"hello"  -- 5

Control Flow

if/elseif/else

local status_code = 200

if status_code == 200 then
    print("Success")
elseif status_code == 404 then
    print("Not Found")
elseif status_code >= 500 then
    print("Server Error")
else
    print("Unknown status: " .. status_code)
end

-- Inline conditional (ternary-like)
local result = status_code == 200 and "Success" or "Error"

for Loop (Numeric)

-- for var = start, end, step do
for i = 1, 5 do
    print(i)  -- 1, 2, 3, 4, 5
end

for i = 10, 1, -1 do
    print(i)  -- 10, 9, ..., 1
end

for i = 0, 10, 2 do
    print(i)  -- 0, 2, 4, 6, 8, 10
end

for Loop (Generic/Iterator)

-- ipairs: iterate array part (1, 2, 3...)
local hosts = {"ise-01", "ise-02", "ise-03"}
for i, host in ipairs(hosts) do
    print(i, host)
end
-- 1  ise-01
-- 2  ise-02
-- 3  ise-03

-- pairs: iterate all keys (any order)
local config = {hostname = "ise-01", port = 443}
for key, value in pairs(config) do
    print(key, value)
end
-- hostname  ise-01
-- port      443

while Loop

local count = 0
while count < 5 do
    print(count)
    count = count + 1
end

-- repeat-until (do-while equivalent)
local attempts = 0
repeat
    attempts = attempts + 1
    local success = try_connect()
until success or attempts >= 3

break (No continue!)

-- break exits loop
for i = 1, 10 do
    if i == 5 then
        break
    end
    print(i)  -- 1, 2, 3, 4
end

-- Lua has NO continue keyword
-- Workaround: use conditional
for i = 1, 10 do
    if i % 2 ~= 0 then  -- Skip even
        print(i)  -- 1, 3, 5, 7, 9
    end
end

-- Or use goto (Lua 5.2+)
for i = 1, 10 do
    if i % 2 == 0 then
        goto continue
    end
    print(i)
    ::continue::
end

Error Handling

pcall (Protected Call)

-- pcall catches errors
local success, result = pcall(function()
    return risky_operation()
end)

if success then
    print("Result: " .. result)
else
    print("Error: " .. result)  -- result is error message
end

-- With arguments
local success, result = pcall(risky_function, arg1, arg2)

assert

-- assert throws error if condition is false
local value = get_value()
assert(value, "Value is required")

-- With message
local port = tonumber(port_string)
assert(port and port > 0 and port < 65536, "Invalid port: " .. port_string)

error

-- Raise error
if not valid_input then
    error("Invalid input provided")
end

-- With level (for better stack trace)
error("Something wrong", 2)  -- Points to caller

Neovim Specifics

-- Print in Neovim
vim.notify("Hello from Lua")
print("Also works, goes to :messages")

-- Get/set options
vim.opt.number = true
vim.opt.relativenumber = true
local tabstop = vim.opt.tabstop:get()

-- Global variables
vim.g.mapleader = " "
vim.g.my_plugin_enabled = true

-- Buffer/window options
vim.bo.filetype = "lua"       -- Buffer option
vim.wo.wrap = false           -- Window option

-- Commands
vim.cmd("colorscheme rose-pine")
vim.cmd([[
  augroup MyGroup
    autocmd!
    autocmd BufEnter * echo "Hello"
  augroup END
]])

Practice Exercises

Exercise 1: Parse ISE Hostname

-- Given: "ise-psn-01.inside.domusdigitalis.dev"
-- Extract: role (psn), number (01), domain

local hostname = "ise-psn-01.inside.domusdigitalis.dev"

-- Solution using patterns
local prefix, role, number, domain = string.match(
    hostname,
    "(%w+)%-(%w+)%-(%d+)%.(.+)"
)

print("Role: " .. role)      -- psn
print("Number: " .. number)  -- 01
print("Domain: " .. domain)  -- inside.domusdigitalis.dev

Exercise 2: Validate VLAN

local function is_valid_vlan(vlan_id)
    -- Must be number
    if type(vlan_id) ~= "number" then
        return false
    end
    -- Must be in range
    if vlan_id < 1 or vlan_id > 4094 then
        return false
    end
    -- Not reserved
    local reserved = {1, 1002, 1003, 1004, 1005}
    for _, v in ipairs(reserved) do
        if vlan_id == v then
            return false
        end
    end
    return true
end

print(is_valid_vlan(10))    -- true
print(is_valid_vlan(1))     -- false (reserved)
print(is_valid_vlan(5000))  -- false (out of range)

Next Module

Tables - Arrays, dictionaries, nested structures.