đź“–   Chapter 4

Lua cheatsheet

Get up to speed with Lua before diving into any projects.

Next Chapter

Before 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:

copy
-- 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 #

  1. Lua is 1-indexed, so don’t start your arrays at [0]!

Variable scope #

In Lua, bare variables are considered global:

copy
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:

copy
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.

copy
-- 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.

copy
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:

copy
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:

copy
-- 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:

copy
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:

copy
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 #

copy
for i = 1, 10 do
  print(i)
end

Prints:

1
2
3
4
5
6
7
8
9
10

Infinite loops #

copy
while true do
  print("This will run forever")
end

Strings #

Check out the official Lua string reference here.

Concatenation #

copy
-- Concatenation
local concatString = "Hello " .. "world!" -- => "Hello world!"

String length #

If you put # before a string variable, it returns the length.

copy
local myString = "hello"

print("The string is " .. #myString .. " characters long.")

This prints out:

The string is 5 characters long.

Contains value? #

copy
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 #

copy
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 #

copy
local str = "Hello world"

str:sub(1, 5) -- returns "Hello"

Pattern substitution #

More documentation is available here.

copy
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.

copy
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:

copy
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:

copy
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:

copy
myTable = { 'apple', 'orange', 'banana' }

In Lua, these tables are 1-indexed. You heard me.

copy
print(myTable[1]) -- prints "apple"

Looping over the table #

To loop over a table, use the ipairs iterator, which returns (index, value) pairs:

copy
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:

copy
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:

copy
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:

copy
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:

copy
favoriteAnimals = {
  cat = true,
  dog = true,
  tarantula = false,
}

And you can reference those values like so:

copy
print(favoriteAnimals.cat) -- => true
print(favoriteAnimals['tarantula']) -- => false

You can add to the table too:

copy
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:

copy
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:

copy
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 #

copy
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:

copy
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:

copy
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:

copy
-- 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:

copy
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:

copy
local Cat = {}

function Cat.myMethod()
  print("Hello!")
end

Cat.myMethod() -- prints "Hello!"

Or this:

copy
local Cat = {}

Cat.myMethod = function()
  print("Hello!")
end

Cat.myMethod() -- prints "Hello!"

Or even this!

copy
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:

copy
hs.hotkey.bind(hyper, 'h', hs.reload)

Inside your ~/.hammerspoon/init.lua file, just add a call to require this new file:

copy
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:

copy
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:

copy
local prefs = {
  foo = true,
  another = false,
}

return prefs

Then, require the file from your init.lua and print the result out:

copy
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!

copy
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.

copy
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.

copy
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.

copy
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.

copy
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

copy
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:

copy
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:

copy
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:

copy
md5 = require('md5')

-- Prints out "49f68a5c8493ec2c0bf489821c21fc3b"
print(md5.sumhexa("hi"))
Fast audio switcher