Cross-Platform Scripting with Lua
Lua runs on everything from embedded devices to game engines to servers, making cross-platform scripting a core concern for anyone writing reusable Lua code. Writing scripts that work across operating systems requires handling a few platform differences: file paths, line endings, executable discovery, and environment variables. Lua’s standard library makes most of this manageable, but you need to know what to watch for.
File Paths
The biggest cross-platform headache in any language is path separators. Windows uses \, macOS and Linux use /. Lua’s standard library handles this inconsistently: io.open and dofile accept both separator styles, but some external tools do not.
Use / in Lua code. It works on all platforms that matter. The one exception is paths passed to Windows system calls — but if you’re calling Windows APIs directly, you’re already outside standard Lua territory.
-- Good: forward slashes work everywhere
dofile("scripts/config.lua")
io.open("data/player.txt")
-- Avoid: hardcoded backslashes
dofile("scripts\\config.lua") -- breaks on Unix
When you need the platform-native separator at runtime — for instance, when building paths to pass to os.execute or when displaying a file path to the user in a platform-appropriate format — you can extract it from Lua’s built-in configuration string. The first character of package.config is always the directory separator for the host platform:
local sep = package.config:sub(1, 1) -- "/" on Unix, "\" on Windows
This tiny snippet is one of the most useful cross-platform tricks in Lua — it lets your code construct paths that are always correct for the host system without any conditional branching. You can use it to join directory names or to split an incoming path into its components.
Paths to Executables
os.execute on Unix-like systems can find executables via PATH. On Windows, executable discovery relies on the PATHEXT environment variable and the current directory. If your script needs to call an external tool:
local function find_executable(name)
local is_windows = package.config:sub(1, 1) == "\\"
if is_windows then
-- Try .exe, .bat, .cmd extensions
local exts = { ".exe", ".bat", ".cmd", ".ps1" }
for _, ext in ipairs(exts) do
local full = name .. ext
local handle = io.popen(full .. " 2>nul", "r")
if handle then
handle:close()
return full
end
end
else
-- Unix: search PATH
local handle = io.popen("which " .. name, "r")
if handle then
local result = handle:read("*a"):gsub("%s+$", "")
handle:close()
return result
end
end
return nil
end
local lua_path = find_executable("lua")
print(lua_path) -- /usr/bin/lua or C:\lua\lua.exe
Line Endings
Text files on Windows end with \r\n, Unix uses \n. This difference can corrupt configuration files, data exports, and any text output that another program will consume. When writing text files that other tools will read, normalise the line endings to match the host platform:
local function write_text(filename, content)
local is_windows = package.config:sub(1, 1) == "\\"
local eol = is_windows and "\r\n" or "\n"
local file = io.open(filename, "w")
if not file then error("cannot open " .. filename) end
file:write(content:gsub("\n", eol))
file:close()
end
Most Lua I/O functions treat both \r\n and \n as line endings on input, so reading Windows text files usually works without special handling. The problem is writing. If you generate a file with \n on Windows, Notepad will display it as one long line — use the normalisation function above whenever your output might land on a different platform than where it was created.
Detecting the Platform
local function get_platform()
local config = package.config
if config:sub(1, 1) == "\\" then
return "windows"
elseif config:sub(1, 1) == "/" then
-- Could be Unix; check for known Unix indicators
local handle = io.popen("uname -s 2>/dev/null", "r")
if handle then
local result = handle:read("*a"):gsub("%s+$", "")
handle:close()
return result:lower()
end
return "unix"
end
return "unknown"
end
print(get_platform()) -- "linux", "macosx", "windows"
package.config is the most reliable built-in signal. uname only works on Unix-like systems.
Handling environment variables
Environment variable names on Windows are case-insensitive; on Unix they are case-sensitive. Lua’s os.getenv and os.setenv work the same way on both platforms, but the variables themselves differ.
local home = os.getenv("HOME") or os.getenv("USERPROFILE") -- Unix vs Windows
local temp = os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp"
local path_sep = package.config:sub(1, 1) == "\\" and ";" or ":"
Common environment variables that differ:
| Purpose | Unix | Windows |
|---|---|---|
| Home directory | HOME | USERPROFILE |
| Temporary files | TMPDIR | TEMP |
| Path separator | : | ; |
| PATH variable | PATH | Path |
Directory Discovery
local function script_dir()
-- Get the directory containing the currently running script
local source = debug.getinfo(1, "S").source
local path = source:match("^@(.*)") or source
return path:match("(.*[/\\])") or "."
end
local function cwd()
local handle = io.popen("cd", "r")
local result = handle:read("*a"):gsub("%s+$", "")
handle:close()
return result
end
debug.getinfo(1, "S").source gives the script path. Strip the filename to get the directory. This works with both lua script.lua and lua /full/path/script.lua. Be aware that on Windows the path includes a drive letter, so the pattern matching above handles both forward slashes and backslashes when extracting the directory portion.
Using LuaFileSystem for portable cross-platform code
LuaFileSystem (lfs) provides portable filesystem operations:
local lfs = require("lfs")
-- Get platform-agnostic attributes
local attr = lfs.attributes("test.lua")
print(attr.mode, attr.size, attr.modification)
-- Iterate directories portably
for file in lfs.dir("/tmp") do
if file ~= "." and file ~= ".." then
print(file)
end
end
-- Set file timestamps
lfs.touch("test.lua") -- updates modification time to now
Install with luarocks install luafilesystem. Most Lua distributions on Windows include it by default. If your project cannot depend on external C libraries, the pure-Lua techniques shown earlier in this guide cover the most common cross-platform scripting needs without any dependencies.
See Also
- /guides/lua-dependency-management/ — managing dependencies across platforms
- /guides/lua-config-files/ — reading configuration files portably
- /guides/lua-lfs-filesystem/ — portable filesystem operations with LuaFileSystem