diff options
| author | Saikari <[email protected]> | 2026-01-31 20:44:22 +0300 |
|---|---|---|
| committer | Saikari <[email protected]> | 2026-01-31 20:44:22 +0300 |
| commit | 9079d266a3182c04be11dfe98a6ea44c66afee13 (patch) | |
| tree | b96c82f2e2b1dbc6e78cbaa04cca9b58730a2bfb | |
| parent | 4aee85b4d3f6537ad1d64472bced22525db6d98f (diff) | |
refactor: implement os.iorun_in_shell for improved command execution in various shells
| -rw-r--r-- | tests/projects/test_stdin/xmake.lua | 190 | ||||
| -rw-r--r-- | xmake/core/base/os.lua | 45 | ||||
| -rw-r--r-- | xmake/core/sandbox/modules/os.lua | 6 |
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, ...) |
