Cross-Platform Scripting with Lua
Lua runs on everything from embedded devices to game engines to servers. 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:
local sep = package.config:sub(1, 1) -- "/" on Unix, "\" on Windows
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. When writing text files that other tools will read:
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.
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.
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.
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