summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSaikari <[email protected]>2026-01-31 20:44:22 +0300
committerSaikari <[email protected]>2026-01-31 20:44:22 +0300
commit9079d266a3182c04be11dfe98a6ea44c66afee13 (patch)
treeb96c82f2e2b1dbc6e78cbaa04cca9b58730a2bfb
parent4aee85b4d3f6537ad1d64472bced22525db6d98f (diff)
refactor: implement os.iorun_in_shell for improved command execution in various shells
-rw-r--r--tests/projects/test_stdin/xmake.lua190
-rw-r--r--xmake/core/base/os.lua45
-rw-r--r--xmake/core/sandbox/modules/os.lua6
3 files changed, 91 insertions, 150 deletions
diff --git a/tests/projects/test_stdin/xmake.lua b/tests/projects/test_stdin/xmake.lua
index beb40bf5f..a603a08e9 100644
--- a/tests/projects/test_stdin/xmake.lua
+++ b/tests/projects/test_stdin/xmake.lua
@@ -6,151 +6,41 @@ target("test")
local xmake_dir = os.getenv("XMAKE_PROGRAM_DIR")
print("XMAKE_PROGRAM_DIR: " .. (xmake_dir or "nil"))
print("xmake binary: " .. xmake)
-
- local function run_with_env(cmd_str)
- local outfile = os.tmpfile()
- local errfile = os.tmpfile()
-
- -- Normalize paths for sh on Windows (converts \ to /)
- outfile = path.unix(outfile)
- errfile = path.unix(errfile)
- if xmake_dir then xmake_dir = path.unix(xmake_dir) end
-
- local shell_cmd = cmd_str
- if xmake_dir then
- shell_cmd = string.format("export XMAKE_PROGRAM_DIR='%s' && %s", xmake_dir, cmd_str)
- end
- -- Redirect using subshell
- shell_cmd = string.format("(%s) > '%s' 2> '%s'", shell_cmd, outfile, errfile)
-
- local code = 0
- try
- {
- function ()
- os.execv("sh", {"-c", shell_cmd})
- end,
- catch
- {
- function (e)
- code = -1
- end
- }
- }
-
- local out = ""
- if os.isfile(outfile) then
- out = io.readfile(outfile)
- os.rm(outfile)
- end
-
- local err = ""
- if os.isfile(errfile) then
- err = io.readfile(errfile)
- os.rm(errfile)
- end
-
- return (code == 0), out, err
- end
-
- local function run_with_pwsh(cmd_str)
- local outfile = os.tmpfile()
- local errfile = os.tmpfile()
- local shell_cmd = cmd_str
- if xmake_dir then
- shell_cmd = string.format("$env:XMAKE_PROGRAM_DIR='%s'; %s", xmake_dir, cmd_str)
- end
- shell_cmd = string.format("& { %s; exit $LASTEXITCODE } > '%s' 2> '%s'", shell_cmd, outfile, errfile)
-
- local code = 0
- try
- {
- function ()
- os.execv("pwsh", {"-c", shell_cmd})
- end,
- catch
- {
- function (e)
- code = -1
- end
- }
- }
-
- local out = ""
- if os.isfile(outfile) then
- out = io.readfile(outfile)
- os.rm(outfile)
- end
- local err = ""
- if os.isfile(errfile) then
- err = io.readfile(errfile)
- os.rm(errfile)
- end
-
- return (code == 0), out, err
- end
-
- -- FIX: Use .bat file for robust cmd pipe handling
- local function run_with_cmd(cmd_str)
- local batfile = os.tmpfile() .. ".bat"
- local outfile = os.tmpfile()
- local errfile = os.tmpfile()
-
- outfile = outfile:gsub("/", "\\")
- errfile = errfile:gsub("/", "\\")
-
- local batch_content = "@echo off\n"
- if xmake_dir then
- local win_xmake_dir = xmake_dir:gsub("/", "\\")
- batch_content = batch_content .. string.format("set XMAKE_PROGRAM_DIR=%s\n", win_xmake_dir)
- end
-
- -- Write the command redirected to output files
- batch_content = batch_content .. string.format("%s > \"%s\" 2> \"%s\"\n", cmd_str, outfile, errfile)
- batch_content = batch_content .. "if %errorlevel% neq 0 exit /b %errorlevel%\n"
-
- io.writefile(batfile, batch_content)
-
- local code = 0
- try
- {
- function ()
- os.execv(batfile, {})
- end,
- catch
- {
- function (e)
- code = -1
- end
- }
- }
-
- local out = ""
- if os.isfile(outfile) then
- out = io.readfile(outfile)
- os.rm(outfile)
- end
- local err = ""
- if os.isfile(errfile) then
- err = io.readfile(errfile)
- os.rm(errfile)
+
+ if xmake_dir then
+ xmake_dir = path.unix(xmake_dir)
+ if os.host() == "windows" then
+ xmake_dir = xmake_dir:gsub("/", "\\")
end
- os.rm(batfile)
-
- return (code == 0), out, err
+ os.setenv("XMAKE_PROGRAM_DIR", xmake_dir)
end
-
+
-- New Helper: Probe for feature working (handles Windows/pwsh fallback)
local function check_feature()
print("Checking feature: --from-stdin ...")
- -- 1. Try generic sh (preferred if available)
+ local shell = os.shell()
+
+ -- 1. Try detected shell if compatible
+ if shell == "pwsh" or shell == "powershell" then
+ local pwsh_probe = string.format("Write-Output \"print('probe_ok')\" | & '%s' lua --from-stdin", xmake)
+ local ok, out, _ = os.iorun_in_shell(shell, pwsh_probe)
+ if ok and out and out:find("probe_ok") then return true end
+ elseif shell == "cmd" then
+ local win_xmake = xmake:gsub("/", "\\")
+ local cmd_probe = string.format("echo print\"probe_ok\" | \"%s\" lua --from-stdin", win_xmake)
+ local ok, out, _ = os.iorun_in_shell("cmd", cmd_probe)
+ if ok and out and out:find("probe_ok") then return true end
+ end
+
+ -- 2. Try generic sh (preferred if available validation default)
local probe_cmd = string.format("echo 'print(\"probe_ok\")' | %s lua --from-stdin", xmake)
- local ok, out, _ = run_with_env(probe_cmd)
+ local ok, out, _ = os.iorun_in_shell("sh", probe_cmd)
if ok and out and out:find("probe_ok") then return true end
-
- -- 2. On Windows, try pwsh if sh failed
- if os.host() == "windows" then
+
+ -- 3. Fallback: On Windows, try pwsh if sh failed
+ if os.host() == "windows" and shell ~= "pwsh" and shell ~= "powershell" and shell ~= "cmd" then
local pwsh_probe = string.format("Write-Output \"print('probe_ok')\" | & '%s' lua --from-stdin", xmake)
- ok, out, _ = run_with_pwsh(pwsh_probe)
+ local ok, out, _ = os.iorun_in_shell("pwsh", pwsh_probe)
if ok and out and out:find("probe_ok") then return true end
end
return false
@@ -160,7 +50,7 @@ target("test")
if check_feature() then
print("Feature presence check: PASS")
else
- local ok, out, err = run_with_env(string.format("%s lua --help", xmake))
+ local ok, out, err = os.iorun_in_shell("sh", string.format("%s lua --help", xmake))
if out and out:find("--from-stdin", 1, true) then
print("Feature presence check: PASS (via help text)")
else
@@ -172,7 +62,7 @@ target("test")
-- test 1: pipe a few lines of lua code from echo (multiline)
local pipe_cmd = string.format("(echo 'print(\"hello\")'; echo 'print(\"from pipe\")') | '%s' lua --from-stdin", xmake)
print("running: " .. pipe_cmd)
- ok, out, err = run_with_env(pipe_cmd)
+ local ok, out, err = os.iorun_in_shell("sh", pipe_cmd)
print("STDOUT 1:\n" .. (out or ""))
print("STDERR 1:\n" .. (err or ""))
assert(ok, "test 1 failed: command returned error")
@@ -187,7 +77,7 @@ target("test")
local cat_cmd = string.format("cat '%s' | '%s' lua --from-stdin 2>&1", path.unix(scriptfile), xmake)
print("running: " .. cat_cmd)
- ok, out, err = run_with_env(cat_cmd)
+ ok, out, err = os.iorun_in_shell("sh", cat_cmd)
print("STDOUT 2:\n" .. (out or ""))
print("STDERR 2:\n" .. (err or ""))
@@ -200,7 +90,7 @@ target("test")
-- test 3: verify traceback on error via pipe (multiline)
local error_pipe_cmd = string.format("(echo 'print(\"ok step\")'; echo 'raise(\"error_pipe\")') | '%s' lua --from-stdin", xmake)
print("running: " .. error_pipe_cmd)
- ok, out, err = run_with_env(error_pipe_cmd)
+ ok, out, err = os.iorun_in_shell("sh", error_pipe_cmd)
print("STDOUT 3:\n" .. (out or ""))
print("STDERR 3:\n" .. (err or ""))
assert(not ok, "test 3 failed: command should have returned error")
@@ -214,7 +104,7 @@ target("test")
-- FIX: Use cat and merge stderr (2>&1)
local error_file_cmd = string.format("cat '%s' | '%s' lua --from-stdin 2>&1", path.unix(errorfile), xmake)
print("running: " .. error_file_cmd)
- ok, out, err = run_with_env(error_file_cmd)
+ ok, out, err = os.iorun_in_shell("sh", error_file_cmd)
print("STDOUT 4:\n" .. (out or ""))
print("STDERR 4:\n" .. (err or ""))
@@ -231,7 +121,7 @@ target("test")
-- test 5: pwsh pipe success (multiline)
local pwsh_pipe_cmd = string.format("Write-Output \"print(`\"hello`\")`nprint(`\"from pwsh pipe`\")\" | & '%s' lua --from-stdin", xmake)
print("running pwsh: " .. pwsh_pipe_cmd)
- ok, out, err = run_with_pwsh(pwsh_pipe_cmd)
+ ok, out, err = os.iorun_in_shell("pwsh", pwsh_pipe_cmd)
print("STDOUT 5:\n" .. (out or ""))
print("STDERR 5:\n" .. (err or ""))
assert(ok, "test 5 failed: command returned error")
@@ -244,7 +134,7 @@ target("test")
io.writefile(scriptfile, 'print("hello")\nprint("from pwsh file")\n')
local pwsh_redirect_cmd = string.format("Get-Content '%s' | & '%s' lua --from-stdin", scriptfile, xmake)
print("running pwsh: " .. pwsh_redirect_cmd)
- ok, out, err = run_with_pwsh(pwsh_redirect_cmd)
+ ok, out, err = os.iorun_in_shell("pwsh", pwsh_redirect_cmd)
print("STDOUT 6:\n" .. (out or ""))
print("STDERR 6:\n" .. (err or ""))
assert(ok, "test 6 failed: command returned error")
@@ -256,7 +146,7 @@ target("test")
-- test 7: pwsh pipe error (multiline)
local pwsh_error_pipe_cmd = string.format("Write-Output \"print(`\"ok step`\")`nraise(`\"error_pwsh_pipe`\")\" | & '%s' lua --from-stdin", xmake)
print("running pwsh: " .. pwsh_error_pipe_cmd)
- ok, out, err = run_with_pwsh(pwsh_error_pipe_cmd)
+ ok, out, err = os.iorun_in_shell("pwsh", pwsh_error_pipe_cmd)
print("STDOUT 7:\n" .. (out or ""))
print("STDERR 7:\n" .. (err or ""))
assert(not ok, "test 7 failed: command should have returned error")
@@ -268,7 +158,7 @@ target("test")
io.writefile(errorfile, 'print("ok step")\nraise("error_pwsh_file")\n')
local pwsh_error_file_cmd = string.format("Get-Content '%s' | & '%s' lua --from-stdin", errorfile, xmake)
print("running pwsh: " .. pwsh_error_file_cmd)
- ok, out, err = run_with_pwsh(pwsh_error_file_cmd)
+ ok, out, err = os.iorun_in_shell("pwsh", pwsh_error_file_cmd)
print("STDOUT 8:\n" .. (out or ""))
print("STDERR 8:\n" .. (err or ""))
assert(not ok, "test 8 failed: command should have returned error")
@@ -287,7 +177,7 @@ target("test")
-- test 9: cmd pipe success (multiline)
local cmd_pipe_cmd = string.format("(echo print\"hello\" && echo print\"from cmd pipe\") | \"%s\" lua --from-stdin", win_xmake)
print("running cmd: " .. cmd_pipe_cmd)
- ok, out, err = run_with_cmd(cmd_pipe_cmd)
+ ok, out, err = os.iorun_in_shell("cmd", cmd_pipe_cmd)
print("STDOUT 9:\n" .. (out or ""))
print("STDERR 9:\n" .. (err or ""))
assert(ok, "test 9 failed: command returned error")
@@ -301,7 +191,7 @@ target("test")
local cmd_file_cmd = string.format("type \"%s\" | \"%s\" lua --from-stdin", win_scriptfile, win_xmake)
print("running cmd: " .. cmd_file_cmd)
- ok, out, err = run_with_cmd(cmd_file_cmd)
+ ok, out, err = os.iorun_in_shell("cmd", cmd_file_cmd)
print("STDOUT 10:\n" .. (out or ""))
print("STDERR 10:\n" .. (err or ""))
assert(ok, "test 10 failed: command returned error")
@@ -311,7 +201,7 @@ target("test")
-- test 11: cmd pipe error (multiline)
local cmd_err_pipe_cmd = string.format("(echo print\"ok step\" && echo raise\"error_cmd_pipe\") | \"%s\" lua --from-stdin", win_xmake)
print("running cmd: " .. cmd_err_pipe_cmd)
- ok, out, err = run_with_cmd(cmd_err_pipe_cmd)
+ ok, out, err = os.iorun_in_shell("cmd", cmd_err_pipe_cmd)
print("STDOUT 11:\n" .. (out or ""))
print("STDERR 11:\n" .. (err or ""))
assert(not ok, "test 11 failed: command should have returned error")
@@ -325,7 +215,7 @@ target("test")
local cmd_err_file_cmd = string.format("type \"%s\" | \"%s\" lua --from-stdin", win_errorfile, win_xmake)
print("running cmd: " .. cmd_err_file_cmd)
- ok, out, err = run_with_cmd(cmd_err_file_cmd)
+ ok, out, err = os.iorun_in_shell("cmd", cmd_err_file_cmd)
print("STDOUT 12:\n" .. (out or ""))
print("STDERR 12:\n" .. (err or ""))
assert(not ok, "test 12 failed: command should have returned error")
diff --git a/xmake/core/base/os.lua b/xmake/core/base/os.lua
index 9332f2c61..86f04e35d 100644
--- a/xmake/core/base/os.lua
+++ b/xmake/core/base/os.lua
@@ -1106,6 +1106,51 @@ function os.iorunv(program, argv, opt)
return ok == 0, outdata, errdata, errors
end
+-- run command in the given shell and return output and error data
+--
+-- @param shell the shell name (e.g. sh, bash, zsh, cmd, pwsh, powershell)
+-- @param cmd the command string
+-- @param opt the options
+--
+-- @return ok, stdout, stderr, errors
+--
+function os.iorun_in_shell(shell, cmd, opt)
+
+ -- check
+ if not shell or not cmd then
+ return false, nil, nil, "invalid arguments"
+ end
+
+ -- run in pwsh/powershell
+ if shell == "pwsh" or shell == "powershell" then
+ local shell_cmd = string.format("& { %s; exit $LASTEXITCODE }", cmd)
+ return os.iorunv(shell, {"-c", shell_cmd}, opt)
+
+ -- run in cmd
+ elseif shell == "cmd" then
+
+ -- use batfile to robust pipe handling
+ local batfile = os.tmpfile() .. ".bat"
+ local batch_content = "@echo off\n"
+
+ -- append command
+ batch_content = batch_content .. cmd .. "\n"
+
+ -- append exit code check
+ batch_content = batch_content .. "if %errorlevel% neq 0 exit /b %errorlevel%\n"
+
+ io.writefile(batfile, batch_content)
+
+ local ok, out, err, errors = os.iorunv(batfile, {}, opt)
+ os.rm(batfile)
+ return ok, out, err, errors
+
+ -- run in sh/bash/zsh...
+ else
+ return os.iorunv(shell, {"-c", cmd}, opt)
+ end
+end
+
-- raise an exception and abort the current script
--
-- the parent function will capture it if we uses pcall or xpcall
diff --git a/xmake/core/sandbox/modules/os.lua b/xmake/core/sandbox/modules/os.lua
index aebb3cfb0..ec5cb0e1e 100644
--- a/xmake/core/sandbox/modules/os.lua
+++ b/xmake/core/sandbox/modules/os.lua
@@ -324,6 +324,12 @@ function sandbox_os.iorunv(program, argv, opt)
return outdata, errdata
end
+-- run command in shell with io redirection and return (ok, out, err)
+function sandbox_os.iorun_in_shell(shell, cmd, ...)
+ cmd = vformat(cmd, ...)
+ return os.iorun_in_shell(shell, cmd)
+end
+
-- execute command
function sandbox_os.exec(cmd, ...)
cmd = vformat(cmd, ...)