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
localto 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"))