luaguides

string.upper

string.upper(s)

Signature and overview

string.upper returns a new string with every ASCII lowercase letter in the input replaced by its uppercase equivalent. Every other byte is left alone, so digits, punctuation, spaces, and non-ASCII bytes survive untouched. The function is a thin wrapper around the C library’s toupper, and the result has the same byte length as the input.

string.upper(s)   -- function form
s:upper()         -- method form, sugar via the string metatable

The two forms call the same underlying C function. Use whichever reads better in context. The method form shows up more often inside data manipulation pipelines, while the function form is common in module code where the input type is not statically known.

Parameters

#NameTypeDefaultDescription
1sstring (also accepts number, boolean, nil)requiredInput to convert. The implementation calls luaL_checklstring, which auto-coerces nil along with booleans and numbers to their string forms. Tables, functions, threads, and userdata raise bad argument #1 to 'upper' (string expected, got X) unless they expose a __tostring metamethod.

The coercion behavior is worth knowing because it differs from a strict type check:

print(string.upper(42))      --> 42
print(string.upper(true))    --> TRUE
print(string.upper(nil))     --> NIL
print(string.upper(3.14))    --> 3.14

If you want to be explicit and avoid surprising readers, call tostring(x) first and pass the result.

Return value

A new string. The input is never modified (Lua strings are immutable values), and the output has the same byte length as the input, since C toupper maps one byte to one byte. The empty string maps to the empty string, so the function is safe to use as a default normalizer in higher-order code.

Examples

Basic conversion

print(string.upper("hello"))            --> HELLO
print(string.upper("Lua 5.4"))          --> LUA 5.4
print(string.upper("MiXeD cAsE 123!"))   --> MIXED CASE 123!

The result is always the same length as the input, which makes the function easy to reason about when you are sizing buffers or hashing. A common trap is forgetting that the function returns a new value rather than mutating the input. Lua strings are immutable, so the original is left exactly as it was after the call. You have to capture the result or use it inline for the change to take effect.

local s = "Hello"
string.upper(s)          -- result discarded
print(s)                 --> Hello

s = string.upper(s)      -- reassign to use the new string
print(s)                 --> HELLO

Case-normalizing a sorted index

The reassignment on the last two lines is the pattern you want in real code. A bare string.upper(s) call with no consumer is a silent no-op that is easy to miss in review. When you need a stable, locale-insensitive sort over mixed-case text, normalize each comparison key first. Without normalization, raw ASCII order puts every capital letter before every lowercase one, which rarely matches what readers expect from an alphabetic sort.

local issues = {
  {id = 3, title = "fix login bug"},
  {id = 1, title = "Add dark mode"},
  {id = 2, title = "REFACTOR API"},
}
table.sort(issues, function(a, b)
  return string.upper(a.title) < string.upper(b.title)
end)
for _, v in ipairs(issues) do print(v.id, v.title) end
--> 1   Add dark mode
--> 3   fix login bug
--> 2   REFACTOR API

For large lists, precompute the uppercased title once and sort on the cached field. Doing the work inside the comparator is O(n log n) calls; doing it during normalization is O(n).

Building a case-insensitive dictionary

string.upper is a natural key normalizer for set membership and tag stores. The same idea shows up constantly in HTTP header matching, which is where most readers first encounter the pattern:

local function normalize_key(s)
  return string.upper(s)
end

local keywords = {}
for _, word in ipairs({"end", "local", "function", "if"}) do
  keywords[normalize_key(word)] = true
end

print(keywords["END"])      --> true
print(keywords["Function"]) --> true
print(keywords["else"])     --> nil

Case-insensitive prefix check

The same pattern works for HTTP header names, environment variable lookups, and tag stores. Compute the uppercased key once at insertion time and lookups stay O(1) regardless of how the caller capitalized the input. A small helper built from string.upper and string.sub reads better than the equivalent string.find with a hand-built character class, especially when the prefix is variable. You will see it in HTTP header parsing, command dispatch, and config-file readers.

local function ci_startswith(s, prefix)
  return string.sub(string.upper(s), 1, #prefix) == string.upper(prefix)
end

print(ci_startswith("Authorization: Bearer ...", "AUTHORIZATION"))  --> true
print(ci_startswith("Content-Type: ...", "AUTHORIZATION"))         --> false

For the general case-insensitive search, string.find with plain = true is usually cleaner.

Locale and character set

“Lowercase letter” is whatever the host C library’s toupper classifies as lowercase in the current locale. On the default "C" / "POSIX" locale, that means ASCII az (bytes 0x610x7A) only.

print(string.upper("café"))   --> CAFé

The trailing é is the two-byte UTF-8 sequence 0xC3 0xA9, and toupper operates on each byte independently, so neither byte is touched. On a host where setlocale(LC_ALL, "en_US.UTF-8") or a similar locale is in effect, single-byte Latin-1 letters (like çÇ in an ISO-8859-1 locale) do get converted. Multi-byte UTF-8 sequences still do not.

Lua exposes no pure-Lua API to read or set the locale. The host program controls it. If you need deterministic output across machines, pin the locale in the C entry point or stick to ASCII.

UTF-8 and the utf8 library

string.upper works byte by byte. For real Unicode text, use the utf8 standard library, which walks code points and applies the Unicode default case folding tables:

local utf8 = require("utf8")
print(utf8.upper("café"))   --> CAFÉ
print(utf8.lower("CAFÉ"))   --> café

A few edges are worth knowing. utf8.upper follows Unicode’s default case folding, which means the German ß becomes SS, and the Turkish dotted/dotless I (İ/i and I/ı) can be surprising if you handle Turkish text. If your data is sensitive to those cases, write a normalization function and test it against a fixture.

Roblox’s Luau exposes a different :upper() method on strings that is Unicode-aware but does not always match utf8.upper on edges like ß. If you target both, normalize explicitly and test on real data.

Case-insensitive equality

utf8.upper is the right tool when both sides of a comparison can contain non-ASCII characters. Here is the idiomatic helper for a case-insensitive equality check:

local utf8 = require("utf8")

local function ci_eq(a, b)
  return utf8.upper(a) == utf8.upper(b)
end

print(ci_eq("Hello", "HELLO"))   --> true
print(ci_eq("café", "CAFÉ"))     --> true
print(ci_eq("ß", "SS"))          --> true   -- Unicode default case folding

For ASCII-only inputs, calling string.upper on both sides is faster and skips the require cost. For mixed-language text, utf8.upper walks code points instead of bytes, so it does the right thing on accented letters and other diacritics. It also handles non-Latin scripts the way a Unicode-aware tool would. Pick the variant that matches the input domain and stick with it across the codebase.

Common mistakes

  1. Forgetting the function returns a new value. Strings are immutable, so the original is unchanged. Capture the result or you have a silent no-op.
  2. Assuming numbers and nil raise an error. In Lua 5.4 they are silently coerced to their string forms. Tables and most userdata do error, unless they have a __tostring metamethod.
  3. Expecting UTF-8 awareness. Multi-byte sequences are passed through unchanged on a "C" locale. Use utf8.upper for Unicode text.
  4. Forgetting the C locale is the authority. Two machines with different locales can produce different output for the same non-ASCII input. Pin the locale on the host if the script is portable.
  5. Mixing it with string.gsub replacement strings. Lua patterns do not auto-uppercase captures. If you need an uppercase replacement, build it with string.upper inside a replacement function.

See also

  • string.lower — the exact mirror, useful when you need to canonicalize for comparison.
  • string.lenstring.len(string.upper(s)) equals string.len(s), useful when reasoning about preallocation.
  • string.findstring.find with plain = true is a common companion for case-insensitive searches.
  • string.match — patterns you can run after normalizing the case.
  • string.format%s with an uppercased argument is a common log-line idiom.
  • string.gsub — call string.upper from inside a replacement function when you need selective uppercasing.
  • tostring — explicit coercion before passing to string.upper.
  • Strings and Patterns — beginner walkthrough of the string library.
  • Pattern Matching — patterns vs. string.upper as a normalization step.
  • Lua String Patterns — pattern cookbook with case-insensitive search examples.