luaguides

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:

PurposeUnixWindows
Home directoryHOMEUSERPROFILE
Temporary filesTMPDIRTEMP
Path separator:;
PATH variablePATHPath

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