Lua cheatsheet
Get up to speed with Lua before diving into any projects.
Next ChapterBefore we jump into any projects, I want to give you a quick overview of the Lua programming language. A lot of the syntax should feel pretty familiar from other languages, particularly more dynamic ones. If you’re familiar with JavaScript, you’ll probably be right at home, as a lot of Hammerspoon-flavored Lua is very event-driven–e.g. “when I press a key do this,” or “when a file changes alert me.”
Of course, if you’re already familiar with Lua, you can skip this chapter!
And of course, for a complete reference of the language, you can head on over to Lua’s official documentation.
Basic syntax #
Here’s some basic syntax for you:

-- A lua comment
local number = 42
local aFloat = 32.25
local myStr = "a string"
local truthy = true
local falsy = false
myTable = {
foo = "bar",
baz = "bah", -- trailing commas are OK
}
print(myTable.foo) -- => "bar"
print(myTable['foo']) -- => also prints "bar"
-- Setting table values
myTable.another = "value" -- this works
myTable['also'] = "value 2" -- this also works
-- Lua has a `nil` keyword to represent null
local nullValue = nil
-- if/else syntax
if nullValue == nil then
print("This is null")
elseif nullValue == 5 then
print("This is 5")
else
print("In the else clause")
end
-- Not equals
if number ~= 42 then
print("We were expecting number to be 42!")
end
-- Defining a function
function add(a, b)
return a + b
end
local number = add(10, 15) -- => 25
Gotchas #
- Lua is 1-indexed, so don’t start your arrays at
[0]
!
Variable scope #
In Lua, bare variables are considered global:

x = 20
The variable x
, once defined, can be referenced anywhere in your program.
If you want to scope your variable to a certain function, block, or clause, just add the local
keyword in front:

function multiply(a, b)
local result = a * b
return result
end
Garbage collection #
Every so often, the Lua interpreter will garbage collect any variables it thinks are unused. This can be a common footgun in Hammerspoon configs, where an object you’ve created gets GC’d and just randomly stops working one day. However, if your variable is global, Lua will leave it alone.

-- This variable is safe from GC, because it's global.
configWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", hs.reload)
-- Someday, Lua will GC this and make you very sad
local configWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", hs.reload)
My rules of thumb with variable declaration go like this:
- If the variable is short-lived/part of a function, always declare it
local
to limit your scope. - If the variable is top-level in your config, leave it global.
Functions #
Check out the official guides for more details:
The function keyword #
You can define a function with the function
keyword, similar to JS, PHP, and other languages. Values are returned with the return
keyword.

function add(a, b)
return a + b
end
By default, the add
function is in global scope, and callable from anywhere. If you want to restrict the function to the current file, add the local
keyword:

local function add(a, b)
return a + b
end
Anonymous functions #
Functions can be created anonymously and assigned to a variable, like in JS and Ruby:

-- global
add = function(a, b)
return a + b
end
-- private
local add = function(a, b)
return a + b
end
Passing a function as an argument #
Just like JS and Ruby, you can pass a function as an argument to another function:

function printFormatted(str, formatter)
print(formatter(str))
end
-- Prints "olleh"
printFormatted("hello", function(str)
return string.reverse(str)
end)
If you have a function already saved to a variable, you can pass that instead:

function printFormatted(str, formatter)
print(formatter(str))
end
local myFormatter = function(str)
return string.reverse(str)
end
-- Prints "olleh"
printFormatted("hello", myFormatter)
Loops #
n-times #

for i = 1, 10 do
print(i)
end
Prints:
1
2
3
4
5
6
7
8
9
10
Infinite loops #

while true do
print("This will run forever")
end
Strings #
Check out the official Lua string reference here.
Concatenation #

-- Concatenation
local concatString = "Hello " .. "world!" -- => "Hello world!"
String length #
If you put #
before a string variable, it returns the length.

local myString = "hello"
print("The string is " .. #myString .. " characters long.")
This prints out:
The string is 5 characters long.
Contains value? #

str = "Some text containing Principal Skinner"
if string.find(str, "Skinner") then
print ("The word Skinner was found.")
else
print ("The word Skinner was not found.")
end
Iterate through characters #

str = "Hello"
for i = 1, #str do
local char = str:sub(i, i)
print(char)
end
This prints out:
H
e
l
l
o
Substrings #

local str = "Hello world"
str:sub(1, 5) -- returns "Hello"
Pattern substitution #
More documentation is available here.

s = string.gsub("Lua is cute", "cute", "great")
print(s) --> Lua is great
s = string.gsub("Lua is great", "perl", "tcl")
print(s) --> Lua is great
Lua has its own regex pattern style, different from what you might be used to with JS, Ruby, Perl, etc.

s = string.gsub("abcdefg", 'b..', 'xxx')
print(s) --> axxxefg
s = string.gsub("foo 123 bar", '%d%d%d', 'xxx') -- %d matches a digit
print(s) --> foo xxx bar
s = string.gsub("text with an Uppercase letter", '%u', '')
print(s) --> text with an ppercase letter
Read more about Lua patterns here.
Pattern matching #
You can use the same Lua patterns from the Pattern substitution section to match strings:

result = string.match("text with an Uppercase letter", '%u')
print(result) --> U
result = string.match("123", '[0-9]')
print(result) --> 1
result = string.match("this is some text with a number 12345 in it", '%d+')
print(result) --> 12345
Splitting a string #
The easiest way to split is to use the built-in hs.fnutils.split()
function:

result = hs.fnutils.split("12:34:56", ":", nil, true)
p(result)
This prints out:
{ "12", "34", "56" }
Tables #
Lua has an associative array mechanism called a table. From the docs:
An associative array is an array that can be indexed not only with numbers, but also with strings or any other value of the language, except
nil
.
If you’ve ever written PHP, this will be familiar to you–all arrays in PHP are also associative.
Using a table like an array #
You can create an array-like table like this:

myTable = { 'apple', 'orange', 'banana' }
In Lua, these tables are 1-indexed. You heard me.

print(myTable[1]) -- prints "apple"
Looping over the table #
To loop over a table, use the ipairs
iterator, which returns (index, value)
pairs:

myTable = { 'apple', 'orange', 'banana' }
for index, value in ipairs(myTable) do
print(index, value)
end
Prints:
1 apple
2 orange
3 banana
Pushing a value to the table #
You can add a value at any time with table.insert
:

myTable = { 'apple', 'orange', 'banana' }
table.insert(myTable, 'grape')
p(myTable) -- contains { 'apple', 'orange', 'banana', 'grape' }
Removing a value from the table #
Just like inserting, you can table.remove
to remove a value at a certain index:

myTable = { 'apple', 'orange', 'banana' }
table.remove(myTable, 1)
p(myTable) -- contains { 'orange', 'banana' }
Checking if table contains value #
There’s no official function for this included in Lua, but you can make your own:

function table.contains(table, element)
for _, value in pairs(table) do
if value == element then
return true
end
end
return false
end
myTable = { 'apple', 'orange', 'banana' }
print(table.contains(myTable, 'apple')) -- => true
print(myTable:contains('apple')) -- => true
Using a table like a hash #
You can assign key-value pairs inside of a table:

favoriteAnimals = {
cat = true,
dog = true,
tarantula = false,
}
And you can reference those values like so:

print(favoriteAnimals.cat) -- => true
print(favoriteAnimals['tarantula']) -- => false
You can add to the table too:

favoriteAnimals.crocodile = true
favoriteAnimals['lion'] = true
Looping over the table #
To loop over a hash-like table, use the pairs
iterator, which returns (key, value)
pairs:

favoriteAnimals = {
cat = true,
dog = true,
tarantula = false,
}
for key, value in pairs(favoriteAnimals) do
print(key, value)
end
This prints:
cat true
dog true
tarantula false
Deleting from the table #
In Lua, it’s considered standard to set the key’s value to nil
to remove an item from a table:

favoriteAnimals = {
cat = true,
dog = true,
tarantula = false,
}
favoriteAnimals.cat = nil
for key, value in pairs(favoriteAnimals) do
print(key, value)
end
Prints out:
dog true
tarantula false
More on tables #
For more on tables, please see the Lua documentation.
Objects #
Lua doesn’t have a default way to define objects. However, Lua has sort of a prototype object model, which lets you define which methods should be attached to a given object. We can do this by changing a table’s metatable, which I’ll show you how to do below.
Defining an object #

local Cat = {}
function Cat:new(color)
-- We create a local table and store whatever data we want to
-- store in the object.
--
-- This would be equivalent to saving off some data in private fields
-- inside of an object constructor.
local cat = {
color = color,
}
-- In this case, "self" is the Cat variable we defined above.
--
-- Once we call this, our `cat` variable here will have all the methods
-- defined on `Cat` attached to it.
setmetatable(cat, self)
self.__index = self
return cat
end
myCat = Cat:new('calico')
print(myCat.color)
Prints out:
calico
Adding instance methods #
You can add additional methods to the above Cat object like this:

function Cat:numberOfLegs()
return 4
end
myCat = Cat:new('calico')
print(myCat:numberOfLegs()) -- prints "4"
Notice the :
syntax when we define the method as well as when we call it. Calling a method with a colon (:
) implicitly passes the object the method is being called on as the very first argument, and assigns it to self
:

function Cat:printColor()
print("My coat is " .. self.color)
end
myCat = Cat:new('calico')
myCat:printColor() -- prints out "My coat is calico"
We could define this function just using dot syntax, and it would be the same thing. However, using the colon syntax saves us a bit of effort:

-- Dot, not colon
function Cat.printColor(self)
print("My coat is " .. self.color)
end
Hell, you can even call it without the colon syntax, and just use a dot instead:

myCat = Cat:new('calico')
Cat.printColor(myCat) -- prints out "My coat is calico"
However, this doesn’t feel very “object-y” to me. I find the receiver:methodCall(args)
format much easier to read.
Adding static (class) methods #
If you ever need to add a static method to a class, you can! Just do this:

local Cat = {}
function Cat.myMethod()
print("Hello!")
end
Cat.myMethod() -- prints "Hello!"
Or this:

local Cat = {}
Cat.myMethod = function()
print("Hello!")
end
Cat.myMethod() -- prints "Hello!"
Or even this!

local Cat = {
myMethod = function()
print("Hello!")
end,
}
Cat.myMethod() -- prints "Hello!"
All these forms are perfectly legal.
Load path, require()
, and return
from files #
If you like to separate your configuration into multiple files, it’s easy to do so in Lua.
Requiring files from init.lua
#
Create a file called ~/.hammerspoon/key-bindings.lua
:

hs.hotkey.bind(hyper, 'h', hs.reload)
Inside your ~/.hammerspoon/init.lua
file, just add a call to require
this new file:

require "key-bindings"
How does require know where to load from? #
The Lua interpreter has a global variable called package.path
. This variable defines what paths require()
should look for files in. This is similar to Bash’s $PATH
variable.
If we print out the value in the Hammerspoon console, you should see something like this:
"/Users/dbalatero/.hammerspoon/Spoons/HyperKey.spoon/lib/?.lua;/Users/dbalatero/.hammerspoon/Spoons/VimMode.spoon/vendor/?/init.lua;/Users/dbalatero/.hammerspoon/?.lua;/Users/dbalatero/.hammerspoon/?/init.lua;/Users/dbalatero/.hammerspoon/Spoons/?.spoon/init.lua;/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;/usr/local/lib/lua/5.4/?.lua;/usr/local/lib/lua/5.4/?/init.lua;./?.lua;./?/init.lua;/Applications/Hammerspoon.app/Contents/Resources/extensions/?.lua;/Applications/Hammerspoon.app/Contents/Resources/extensions/?/init.lua"
If you want to add your own custom paths to this load path, you’re welcome to do so:

package.path = "/path/to/custom/directory/?.lua;" .. package.path
If you create a file at /path/to/custom/directory/foo.lua
, you can now require it with require("foo")
.
Returning a value from a Lua file #
Similar to JavaScript’s module.exports
/ export
syntax, you can return a value from a Lua file when it gets required.
Create a file called ~/.hammerspoon/preferences.lua
:

local prefs = {
foo = true,
another = false,
}
return prefs
Then, require the file from your init.lua
and print the result out:

local preferences = require("preferences")
-- Print it out
p(preferences)
You should see in the Hammerspoon console:
{
foo = true,
another = false
}
hs.fnutils #
Hammerspoon has a handy package called hs.fnutils. This is a collection of functional programming utility functions that you can drop into any of your scripts.
This library is in a similar vein to something like Lodash. It provides such methods as concat
, contains
, copy
, each
, every
, filter
, find
, indexOf
, map
, and more!
contains #
This function returns true
if a table
contains a given element
. Always handy to have around!

myTable = { 'apple', 'orange', 'banana' }
if hs.fnutils.contains(myTable, 'apple') then
print("I love apples!")
else
print("Hmm, I don't love apples")
end
Prints out:
I love apples!
each #
This method executes a given function for each element in a table. Classic.

myTable = { 'apple', 'orange', 'banana' }
hs.fnutils.each(myTable, function(fruit)
print(fruit)
end)
Prints:
apple
orange
banana
It works on key-value based tables too. However, you only get the value
passed to the function, not the key
. Bummer.

favoriteAnimals = {
cat = true,
dog = true,
tarantula = false,
}
hs.fnutils.each(favoriteAnimals, function(isFavorite)
print(isFavorite)
end)
Prints:
true
true
false
filter #
Takes a table and returns a new table only containing elements that match the provided predicate function.

favoriteAnimals = {
cat = true,
dog = true,
tarantula = false
}
onlyFavorites = hs.fnutils.filter(favoriteAnimals, function(isFavorite)
return isFavorite
end)
p(onlyFavorites)
Prints:
{
cat = true,
dog = true
}
find #
Execute a function across a table and return the first element where that function returns true
.

fruits = { 'apple', 'orange', 'banana' }
-- Find the first fruit starting with 'a'
firstA = hs.fnutils.find(fruits, function(fruit)
return fruit:sub(1, 1) == 'a'
end)
print(firstA) -- prints "apple"
map #
Execute a function across a table (in arbitrary order) and collect the results

fruits = { 'apple', 'orange', 'banana' }
reverseFruits = hs.fnutils.map(fruits, function(fruit)
return string.reverse(fruit)
end)
p(reverseFruits)
Prints:
{ "elppa", "egnaro", "ananab" }
More functions #
Check out the documentation and start playing with it in your own scripts!
LuaRocks #
LuaRocks is Lua’s official package manager. To install it, run:

brew install luarocks
Using a library in your configs #
If you want to use a LuaRocks library from Hammerspoon, it’s easy to do so. package.path
should already be correctly set up for you, so once you install a package it’s as easy as calling require('package')
.
Let’s install an example package, an md5 library:

luarocks install md5
You should see output like this:
Installing https://luarocks.org/md5-1.3-1.rockspec
md5 1.3-1 depends on lua >= 5.0 (5.4-1 provided by VM)
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -O2 -fPIC -I/usr/local/opt/lua/include/lua5.4 -c src/compat-5.2.c -o src/compat-5.2.o -Isrc/
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -O2 -fPIC -I/usr/local/opt/lua/include/lua5.4 -c src/md5.c -o src/md5.o -Isrc/
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -O2 -fPIC -I/usr/local/opt/lua/include/lua5.4 -c src/md5lib.c -o src/md5lib.o -Isrc/
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -bundle -undefined dynamic_lookup -all_load -o md5/core.so src/compat-5.2.o src/md5.o src/md5lib.o
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -O2 -fPIC -I/usr/local/opt/lua/include/lua5.4 -c src/compat-5.2.c -o src/compat-5.2.o -Isrc/
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -O2 -fPIC -I/usr/local/opt/lua/include/lua5.4 -c src/des56.c -o src/des56.o -Isrc/
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -O2 -fPIC -I/usr/local/opt/lua/include/lua5.4 -c src/ldes56.c -o src/ldes56.o -Isrc/
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -bundle -undefined dynamic_lookup -all_load -o des56.so src/compat-5.2.o src/des56.o src/ldes56.o
md5 1.3-1 is now installed in /usr/local (license: MIT/X11)
Now that it’s installed, you can require it and use its functions like normal:

md5 = require('md5')
-- Prints out "49f68a5c8493ec2c0bf489821c21fc3b"
print(md5.sumhexa("hi"))