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.