Skip to content

Instantly share code, notes, and snippets.

@jdarpinian
Last active August 7, 2023 09:42
Show Gist options
  • Save jdarpinian/6860ddfd92b5b458a20ab6055583bc3e to your computer and use it in GitHub Desktop.
Save jdarpinian/6860ddfd92b5b458a20ab6055583bc3e to your computer and use it in GitHub Desktop.
Polyglot files that can run on Windows, Linux, and Mac using bash, cmd, and C.
#if 0 // 2>NUL & GOTO :startbatch
COMPILER_OPTIONS="-g -Wall -Wextra --std=c99 -O1";THIS_FILE="$(cd "$(dirname "$0")"; pwd -P)/$(basename "$0")";OUT_FILE="/tmp/build-cache/$THIS_FILE";mkdir -p "$(dirname "$OUT_FILE")";test "$THIS_FILE" -ot "$OUT_FILE" || $(which clang || which gcc) $COMPILER_OPTIONS -xc "$THIS_FILE" -o "$OUT_FILE" || exit;exec "$OUT_FILE" "$@"
:startbatch
@echo off
setlocal enableextensions enabledelayedexpansion
md %TEMP%%~p0 2>NUL
rem Use xcopy to test if this file is newer than the cached executable.
for /f %%i in ('xcopy %0 %TEMP%%~pnx0.exe /D /Y /Q') do set copied=%%i
if "%copied:~0,1%" neq "0" (
rem Search for Visual Studio env vars. These are set globally if VS <2017 is installed.
for /L %%i in (60,10,140) DO (
set "vs=VS%%i%COMNTOOLS"
if defined !vs! (
set "vsfound=!vs!"
)
)
)
if "%copied:~0,1%" neq "0" (
rem If cl.exe is already in the PATH, use it. Otherwise, look for VS 2017, then older versions.
for %%X in (cl.exe) do (set FOUND_COMPILER=%%~$PATH:X)
if not defined FOUND_COMPILER (
rem Look for VS 2017 using its start menu link, because Microsoft has broken every other reasonable way of finding it.
set "VS_START_MENU=%ProgramData%\Microsoft\Windows\Start Menu\Programs\Visual Studio 2017.lnk"
if exist !VS_START_MENU! (
rem Use "wmic" tool to extract the devenv.exe path from the .lnk file, then run vcvars64.bat
for /f "delims=" %%I in ('wmic path win32_shortcutfile where "name='!VS_START_MENU:\=\\!'" get target /value') do for /f "delims=" %%# in ("%%~I") do set "%%~#"
pushd
call "!target!\..\..\..\VC\Auxiliary\Build\vcvars64.bat" >NUL 2>NUL
popd
) else (
if not defined vsfound (
echo Could not find a Visual Studio installation.
exit /b 1
)
if not exist "!%vsfound%!vsvars32.bat" (
echo Your Visual Studio does not have a vsvars32.bat
)
call "!%vsfound%!vsvars32.bat" >NUL 2>NUL
for %%X in (cl.exe) do (set FOUND_COMPILER=%%~$PATH:X)
if not defined FOUND_COMPILER (
echo Could not find a Visual Studio installation with a C compiler.
exit /b 1
)
)
)
cl /nologo /TC /Fe:%TEMP%%~pnx0.exe %0 >NUL 2>NUL || exit /B %ERRORLEVEL%
)
%TEMP%%~pnx0.exe %*
exit /B
#endif
#include <stdio.h>
int main(int argc, char **argv) {
printf("Hello world!\n");
return 0;
}
#!/usr/bin/env bash
echo off & echo ; wait ; echo -ne "\e[1A\e[K" # > NUL
echo ; function goto { true; } ; function rem { true; } ; echo -ne "\e[1A\e[K\e[1A\e[K" # > NUL
goto startbatch
echo running in bash
exit
:startbatch
echo 
echo running in cmd
exit /B
@jdarpinian
Copy link
Author

The C polyglot builds and runs itself on all platforms. But without a shebang line, the kernel doesn't see it as executable. So while you can execute it from a bash prompt you will not be able to invoke it with e.g. execve. The shebang version fixes this, but is no longer valid C as a result, only bash and cmd.

@TomasMalecek
Copy link

In the shell/cmd polyglot, I do not like the terminal escape sequences very much as the output may not be a terminal. I needed to look them up to actually understand what the script does.

I came up with a way to fix the script even when the output is not a terminal, but I needed to sacrifice the shebang -- in cmd, the shebang generates an error and there is no clean way around that. I also evaded several Bash-specific shell features; still, I did not manage to make it a POSIX shell script due to the need to create a function whose name is at-sign.

:; @() { true; } # Polyglot of shell and cmd
@ goto startbatch
# Start your shell script here

echo running in shell

# End your shell script here
exit
:startbatch
:: Start your cmd script here

@echo off
echo running in cmd

:: End your cmd script here
exit /B

I am thinking of using this boilerplate to implement cross-platform pipelines in Azure DevOps Server using the script task. I like it more than having separate tasks with a condition on Agent.OS as all the tasks are visible in the pipeline run, regardless of the condition. The documentation talks about using the script task with polyglot scripts, but it uses the simplest echo as an example. When I need anything even slightly more complicated, e.g. print a hash character as part of a logging command, I am out of luck with simple tweaks of the syntax (Bash treats # as start of comment, cmd outputs quotes literally, and there is no shared escaping mechanism between the two).

@TomasMalecek
Copy link

This polyshell is interesting too... Though even more convoluted.
https://github.com/llamasoft/polyshell

@Jan69
Copy link

Jan69 commented Jan 1, 2023

@TomasMalecek there's actually no reason to bother making a dummy function for @ or anything, just redirect errors, and no more errors!

#!/usr/bin/env -S 2>/dev/null=2>NUL sh
@goto batch 2>NUL;rm -f NUL

echo running in sh script
echo "you live in ${HOME}"
errors-still-work-fine
exit

:batch
@echo off
echo running in cmd script
echo you live in %USERPROFILE%
errors-still-work-fine
exit /B

the script itself should be posix, but it requires /bin/env that supports -S for the shebang to work (gnu one works, which is default in most places, but it's not posix, and busybox and toybox's env do not work)
or you can just run it directly using dash/ash/bash or whatever other bourne-style shell you like, ignoring the shebang entirely ;P

@Jan69
Copy link

Jan69 commented Jan 1, 2023

I've tested my version with both windows XP, and Wine's cmd
the 2>/dev/null=2>NUL in the shebang is making sure that both wine and real cmd will not error on the shebang, it doesn't affect anything else, and it's also why you need -S
if you don't care about shebang erroring on windows you can remove that and use a normal #!/bin/sh or #!/bin/bash

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment