Lua - Strings

Overview

Estimated time: 30–35 minutes

Strings are sequences of characters used to represent text. Lua provides powerful string manipulation capabilities through string literals, concatenation, pattern matching, and a comprehensive string library. This tutorial covers all aspects of working with strings in Lua.

Learning Objectives

  • Master string creation and different literal formats
  • Learn string concatenation and formatting techniques
  • Explore the string library and common functions
  • Understand Lua pattern matching (regex alternative)
  • Apply string manipulation in practical scenarios

Prerequisites

  • Understanding of Lua variables and operators
  • Knowledge of basic functions and tables

String Literals

Lua supports several ways to create string literals:

Single and Double Quotes

-- Single quotes
local message1 = 'Hello, World!'
local name = 'Alice'

-- Double quotes
local message2 = "Hello, World!"
local greeting = "Welcome to Lua!"

-- Using quotes inside strings
local quote1 = "He said, 'Hello!'"
local quote2 = 'She replied, "Hi there!"'

-- Escaping quotes
local escaped1 = "He said, \"Hello!\""
local escaped2 = 'She replied, \'Hi there!\''

print(message1)
print(quote1)
print(escaped1)

Expected Output:

Hello, World!
He said, 'Hello!'
He said, "Hello!"

Long Strings (Multi-line)

-- Basic long string
local poem = [[
Roses are red,
Violets are blue,
Lua is awesome,
And so are you!
]]

-- Long string with custom delimiters (to avoid conflicts)
local code_snippet = [=[
local function greet()
    print([[Hello, World!]])
end
greet()
]=]

-- Long strings preserve formatting
local formatted_text = [[
    This text preserves
        its indentation
            and spacing
]]

print("Poem:", poem)
print("Code snippet:", code_snippet)
print("Formatted:", formatted_text)

Escape Sequences

Special characters can be included using escape sequences:

-- Common escape sequences
local newline = "Line 1\nLine 2"
local tab_separated = "Name\tAge\tCity"
local backslash = "This is a backslash: \\"
local quote = "He said: \"Hello!\""
local carriage_return = "Windows line ending\r\n"

-- Unicode escapes (Lua 5.3+)
local unicode_char = "\u{1F600}"  -- Emoji
local chinese = "\u{4E2D}\u{6587}"  -- Chinese characters

-- Numeric escapes
local bell = "\a"     -- Alert (bell)
local backspace = "\b"
local form_feed = "\f"
local vertical_tab = "\v"

print("Newline example:")
print(newline)
print("\nTab separated:")
print(tab_separated)
print("\nBackslash:", backslash)
print("Quote:", quote)

Expected Output:

Newline example:
Line 1
Line 2

Tab separated:
Name	Age	City

Backslash:	This is a backslash: \
Quote:	He said: "Hello!"

String Concatenation

Combine strings using the .. operator:

-- Basic concatenation
local first_name = "John"
local last_name = "Doe"
local full_name = first_name .. " " .. last_name

-- Multiple concatenations
local greeting = "Hello, " .. first_name .. "! Welcome to " .. "Lua programming."

-- Number to string conversion (automatic)
local age = 25
local introduction = "My name is " .. full_name .. " and I am " .. age .. " years old."

-- Concatenation with assignment
local message = "Current time: "
message = message .. os.date("%H:%M:%S")

print("Full name:", full_name)
print("Greeting:", greeting)
print("Introduction:", introduction)
print("Message:", message)

Expected Output:

Full name:	John Doe
Greeting:	Hello, John! Welcome to Lua programming.
Introduction:	My name is John Doe and I am 25 years old.
Message:	Current time: 12:34:56

String Length

Get string length using the # operator:

local text = "Hello, Lua!"
local empty_string = ""
local unicode_text = "こんにちは"  -- Japanese

print("Length of 'Hello, Lua!':", #text)
print("Length of empty string:", #empty_string)
print("Length of unicode text:", #unicode_text)

-- Length in a function
local function count_characters(str)
    return "The string '" .. str .. "' has " .. #str .. " characters."
end

print(count_characters("Programming"))
print(count_characters("A"))

-- Comparing lengths
local password = "mypassword123"
if #password >= 8 then
    print("Password is long enough")
else
    print("Password too short, minimum 8 characters")
end

Expected Output:

Length of 'Hello, Lua!':	11
Length of empty string:	0
Length of unicode text:	15
The string 'Programming' has 11 characters.
The string 'A' has 1 characters.
Password is long enough

String Library Functions

Lua provides a rich string library for text manipulation:

Case Conversion

local text = "Hello, World!"

print("Original:", text)
print("Uppercase:", string.upper(text))
print("Lowercase:", string.lower(text))

-- Using string methods (syntactic sugar)
print("Method syntax - upper:", text:upper())
print("Method syntax - lower:", text:lower())

-- Practical example: case-insensitive comparison
local function compare_ignore_case(str1, str2)
    return string.lower(str1) == string.lower(str2)
end

print("Case-insensitive comparison:")
print("'Hello' == 'HELLO':", compare_ignore_case("Hello", "HELLO"))
print("'Lua' == 'Python':", compare_ignore_case("Lua", "Python"))

Expected Output:

Original:	Hello, World!
Uppercase:	HELLO, WORLD!
Lowercase:	hello, world!
Method syntax - upper:	HELLO, WORLD!
Method syntax - lower:	hello, world!
Case-insensitive comparison:
'Hello' == 'HELLO':	true
'Lua' == 'Python':	false

Substring Operations

local text = "Lua Programming"

-- string.sub(string, start, end)
print("Original:", text)
print("First 3 characters:", string.sub(text, 1, 3))
print("Last 4 characters:", string.sub(text, -4))
print("Characters 5-7:", string.sub(text, 5, 7))
print("From position 5 to end:", string.sub(text, 5))

-- Method syntax
print("Method syntax:", text:sub(1, 3))

-- Practical examples
local email = "[email protected]"
local at_pos = string.find(email, "@")
if at_pos then
    local username = string.sub(email, 1, at_pos - 1)
    local domain = string.sub(email, at_pos + 1)
    print("Username:", username)
    print("Domain:", domain)
end

-- Extracting file extension
local filename = "document.pdf"
local dot_pos = string.find(filename, "%.")
if dot_pos then
    local name = string.sub(filename, 1, dot_pos - 1)
    local extension = string.sub(filename, dot_pos + 1)
    print("File name:", name)
    print("Extension:", extension)
end

Expected Output:

Original:	Lua Programming
First 3 characters:	Lua
Last 4 characters:	ming
Characters 5-7:	Pro
From position 5 to end:	Programming
Method syntax:	Lua
Username:	user
Domain:	example.com
File name:	document
Extension:	pdf

String Search Functions

local text = "The quick brown fox jumps over the lazy fox"

-- string.find(string, pattern, start, plain)
local start_pos, end_pos = string.find(text, "fox")
print("First 'fox' found at positions:", start_pos, "to", end_pos)

-- Find with starting position
local second_fox = string.find(text, "fox", start_pos + 1)
print("Second 'fox' found at position:", second_fox)

-- Case-insensitive search using patterns
local case_insensitive = string.find(string.lower(text), string.lower("QUICK"))
print("Case-insensitive 'QUICK' found at:", case_insensitive)

-- string.match - extract matched text
local word = string.match(text, "brown")
print("Matched word:", word)

-- Extract all words
print("All words in the text:")
for word in string.gmatch(text, "%a+") do
    print("-", word)
end

-- Check if string starts/ends with pattern
local function starts_with(str, prefix)
    return string.sub(str, 1, #prefix) == prefix
end

local function ends_with(str, suffix)
    return string.sub(str, -#suffix) == suffix
end

print("Starts with 'The':", starts_with(text, "The"))
print("Ends with 'fox':", ends_with(text, "fox"))

Expected Output:

First 'fox' found at positions:	17	to	19
Second 'fox' found at position:	42
Case-insensitive 'QUICK' found at:	5
Matched word:	brown
All words in the text:
-	The
-	quick
-	brown
-	fox
-	jumps
-	over
-	the
-	lazy
-	fox
Starts with 'The':	true
Ends with 'fox':	true

String Replacement

Replace text within strings using string.gsub:

local text = "Hello, World! Hello, Lua!"

-- Basic replacement
local result1 = string.gsub(text, "Hello", "Hi")
print("Replace 'Hello' with 'Hi':", result1)

-- Limit number of replacements
local result2, count = string.gsub(text, "Hello", "Greetings", 1)
print("Replace first 'Hello' only:", result2)
print("Number of replacements:", count)

-- Case-insensitive replacement function
local function replace_ignore_case(str, old, new)
    local lower_str = string.lower(str)
    local lower_old = string.lower(old)
    local result = ""
    local i = 1
    
    while i <= #str do
        local found_start, found_end = string.find(lower_str, lower_old, i, true)
        if found_start then
            result = result .. string.sub(str, i, found_start - 1) .. new
            i = found_end + 1
        else
            result = result .. string.sub(str, i)
            break
        end
    end
    
    return result
end

-- Practical examples
local code = "var x = 10; var y = 20;"
local lua_code = string.gsub(code, "var", "local")
print("Convert JS to Lua style:", lua_code)

-- Remove extra spaces
local messy_text = "This   has    too   many    spaces"
local cleaned = string.gsub(messy_text, "%s+", " ")
print("Cleaned text:", cleaned)

-- Capitalize first letter of each word
local function capitalize_words(str)
    return string.gsub(str, "(%a)([%a]*)", function(first, rest)
        return string.upper(first) .. string.lower(rest)
    end)
end

local title = "lua programming is fun"
print("Capitalized:", capitalize_words(title))

Expected Output:

Replace 'Hello' with 'Hi':	Hi, World! Hi, Lua!
Replace first 'Hello' only:	Greetings, World! Hello, Lua!
Number of replacements:	1
Convert JS to Lua style:	local x = 10; local y = 20;
Cleaned text:	This has too many spaces
Capitalized:	Lua Programming Is Fun

Lua Patterns (String Patterns)

Lua uses its own pattern matching system (similar to regex but simpler):

Basic Patterns

local text = "The phone number is 123-456-7890 and email is [email protected]"

-- Character classes
print("Find digits:", string.match(text, "%d+"))
print("Find letters:", string.match(text, "%a+"))
print("Find word characters:", string.match(text, "%w+"))

-- Pattern matching examples
local phone = string.match(text, "%d%d%d%-%d%d%d%-%d%d%d%d")
print("Phone number:", phone)

local email = string.match(text, "%w+@%w+%.%w+")
print("Email:", email)

-- Extract all numbers
print("All numbers in text:")
for number in string.gmatch(text, "%d+") do
    print("-", number)
end

-- Pattern replacements
local censored = string.gsub(text, "%d", "X")
print("Censored numbers:", censored)

-- Validate patterns
local function is_valid_email(email)
    return string.match(email, "^%w+@%w+%.%w+$") ~= nil
end

print("Valid emails:")
print("[email protected]:", is_valid_email("[email protected]"))
print("invalid-email:", is_valid_email("invalid-email"))
print("[email protected]:", is_valid_email("[email protected]"))

Expected Output:

Find digits:	123
Find letters:	The
Find word characters:	The
Phone number:	123-456-7890
Email:	[email protected]
All numbers in text:
-	123
-	456
-	7890
Censored numbers:	The phone number is XXX-XXX-XXXX and email is [email protected]
Valid emails:
[email protected]:	true
invalid-email:	false
[email protected]:	true

Pattern Character Classes

-- Common character classes
local examples = {
    ["%a"] = "letters: Hello123",
    ["%d"] = "digits: abc123def",
    ["%w"] = "word chars: hello_world-123",
    ["%s"] = "whitespace: hello world",
    ["%p"] = "punctuation: hello, world!",
    ["%c"] = "control chars: hello\tworld\n",
    ["%x"] = "hex digits: G1A2B3C",
    ["%."] = "literal dot: file.txt"
}

for pattern, text in pairs(examples) do
    local match = string.match(text, pattern)
    print(pattern .. " in '" .. text .. "':", match or "none")
end

-- Negated character classes (uppercase)
local mixed = "Hello123World"
print("Non-letters in '" .. mixed .. "':", string.match(mixed, "%A"))
print("Non-digits in '" .. mixed .. "':", string.match(mixed, "%D"))

-- Custom character sets
local text_with_vowels = "Programming"
local vowels = string.match(text_with_vowels, "[aeiou]")
local consonants = string.match(text_with_vowels, "[^aeiou%s]")
print("First vowel:", vowels)
print("First consonant:", consonants)

String Formatting

Format strings using string.format:

-- Basic formatting
local name = "Alice"
local age = 25
local height = 5.6

local formatted = string.format("Name: %s, Age: %d, Height: %.1f ft", name, age, height)
print("Formatted string:", formatted)

-- Number formatting
local pi = 3.14159265359
print("Pi with 2 decimals:", string.format("%.2f", pi))
print("Pi with 6 decimals:", string.format("%.6f", pi))
print("Pi in scientific notation:", string.format("%.2e", pi))

-- Integer formatting
local number = 42
print("Decimal:", string.format("%d", number))
print("Hexadecimal:", string.format("%x", number))
print("Octal:", string.format("%o", number))
print("With leading zeros:", string.format("%05d", number))

-- String formatting
local text = "Lua"
print("Default:", string.format("%s", text))
print("Right-aligned (10 chars):", string.format("%10s", text))
print("Left-aligned (10 chars):", string.format("%-10s", text))

-- Table formatting helper
local function format_table_row(...)
    local args = {...}
    local formatted_args = {}
    
    for i, arg in ipairs(args) do
        if type(arg) == "number" then
            formatted_args[i] = string.format("%8.2f", arg)
        else
            formatted_args[i] = string.format("%-10s", tostring(arg))
        end
    end
    
    return table.concat(formatted_args, " | ")
end

print("\nFormatted table:")
print(format_table_row("Name", "Age", "Score"))
print(string.rep("-", 35))
print(format_table_row("Alice", 25, 92.5))
print(format_table_row("Bob", 30, 87.3))
print(format_table_row("Charlie", 22, 95.8))

Expected Output:

Formatted string:	Name: Alice, Age: 25, Height: 5.6 ft
Pi with 2 decimals:	3.14
Pi with 6 decimals:	3.141593
Pi in scientific notation:	3.14e+00
Decimal:	42
Hexadecimal:	2a
Octal:	52
With leading zeros:	00042
Default:	Lua
Right-aligned (10 chars):	       Lua
Left-aligned (10 chars):	Lua       

Formatted table:
Name       | Age        |     Score
-----------------------------------
Alice      | 25         |     92.50
Bob        | 30         |     87.30
Charlie    | 22         |     95.80

Practical String Applications

Text Processing Utilities

-- String utility functions
local string_utils = {}

-- Trim whitespace
function string_utils.trim(str)
    return string.match(str, "^%s*(.-)%s*$")
end

-- Split string by delimiter
function string_utils.split(str, delimiter)
    local result = {}
    local pattern = "(.-)" .. (delimiter or "%s") .. "?"
    
    for part in string.gmatch(str .. delimiter, pattern) do
        if part ~= "" then
            table.insert(result, part)
        end
    end
    
    return result
end

-- Check if string is empty or whitespace
function string_utils.is_blank(str)
    return not str or string_utils.trim(str) == ""
end

-- Pad string to specified length
function string_utils.pad_left(str, length, char)
    char = char or " "
    local padding = length - #str
    return padding > 0 and string.rep(char, padding) .. str or str
end

function string_utils.pad_right(str, length, char)
    char = char or " "
    local padding = length - #str
    return padding > 0 and str .. string.rep(char, padding) or str
end

-- Test the utilities
local messy_text = "  hello, world!  "
print("Original: '" .. messy_text .. "'")
print("Trimmed: '" .. string_utils.trim(messy_text) .. "'")

local csv = "apple,banana,orange,grape"
local fruits = string_utils.split(csv, ",")
print("Split CSV:", table.concat(fruits, " | "))

print("Is blank check:")
print("Empty string:", string_utils.is_blank(""))
print("Whitespace only:", string_utils.is_blank("   "))
print("Has content:", string_utils.is_blank("hello"))

print("Padding examples:")
print("Left padded: '" .. string_utils.pad_left("42", 5, "0") .. "'")
print("Right padded: '" .. string_utils.pad_right("Name", 10, ".") .. "'")

URL and Data Processing

-- URL parsing example
local function parse_url(url)
    local protocol, domain, path = string.match(url, "^(%w+)://([^/]+)(.*)$")
    return {
        protocol = protocol,
        domain = domain,
        path = path ~= "" and path or "/"
    }
end

local url = "https://example.com/path/to/page"
local parsed = parse_url(url)
print("URL parsing:")
print("Protocol:", parsed.protocol)
print("Domain:", parsed.domain)
print("Path:", parsed.path)

-- Simple template system
local function render_template(template, data)
    return string.gsub(template, "{{(%w+)}}", function(key)
        return tostring(data[key] or "")
    end)
end

local template = "Hello {{name}}, you are {{age}} years old and live in {{city}}."
local user_data = {
    name = "Alice",
    age = 25,
    city = "New York"
}

local rendered = render_template(template, user_data)
print("Template result:", rendered)

-- Log entry parser
local function parse_log_entry(entry)
    local timestamp, level, message = string.match(entry, "^(%S+)%s+(%w+):%s+(.+)$")
    return {
        timestamp = timestamp,
        level = level,
        message = message
    }
end

local log_entry = "2024-01-15T10:30:45 ERROR: Database connection failed"
local parsed_log = parse_log_entry(log_entry)
print("Log parsing:")
print("Timestamp:", parsed_log.timestamp)
print("Level:", parsed_log.level)
print("Message:", parsed_log.message)

Expected Output:

Original: '  hello, world!  '
Trimmed: 'hello, world!'
Split CSV:	apple | banana | orange | grape
Is blank check:
Empty string:	true
Whitespace only:	true
Has content:	false
Padding examples:
Left padded:	'00042'
Right padded:	'Name......'
URL parsing:
Protocol:	https
Domain:	example.com
Path:	/path/to/page
Template result:	Hello Alice, you are 25 years old and live in New York.
Log parsing:
Timestamp:	2024-01-15T10:30:45
Level:	ERROR
Message:	Database connection failed

Common Pitfalls

  • Pattern vs plain text: Use the 4th parameter true in string.find for literal search
  • String immutability: Strings are immutable; operations return new strings
  • Pattern escaping: Special characters in patterns need escaping with %
  • Unicode handling: Lua's string functions work with bytes, not Unicode characters
  • Performance: Avoid excessive concatenation in loops; use tables and table.concat

Checks for Understanding

  1. How do you create a multi-line string in Lua?
  2. What's the difference between string.find and string.match?
  3. How do you replace all occurrences of a substring?
  4. What does the pattern %d+ match?
  5. How do you format a number to 2 decimal places?
Show answers
  1. Use double square brackets: [[multi-line string]]
  2. string.find returns positions; string.match returns the matched text
  3. Use string.gsub(text, old, new)
  4. One or more consecutive digits
  5. Use string.format("%.2f", number)

Next Steps

Now that you understand string manipulation, you're ready to explore number operations and the math library for numerical computations.