Skip to content

Instantly share code, notes, and snippets.

@f-steff
Created September 10, 2025 19:57
Show Gist options
  • Save f-steff/ca56aaad3ffc7f497c2dc8286b209041 to your computer and use it in GitHub Desktop.
Save f-steff/ca56aaad3ffc7f497c2dc8286b209041 to your computer and use it in GitHub Desktop.
batch/bash polyglot file - useful to have one script file to execute that will work in both a batch and a bash environment.
# All .bat batch files must be saved using DOS/Windows default crlf fline endings.
*.bat text eol=crlf
# Force LF for this one Batch/Bash polyglot
polyglot.bat text eol=lf
# NOTE: If the file already exists in the repository, apply the rule and fix the working tree:
#git add --renormalize polyglot.bat
#git commit -m "Normalize line endings for polyglot.bat to LF"
## (optional) rewrite the working copy from index to get LF immediately:
#git checkout -- polyglot.bat

Bash/Batch Polyglot Launcher

This gist repo contains a tiny setup that lets a single file (polyglot.bat) behave like:

  • a Batch script when run from cmd.exe
  • a Bash script when run via bash polyglot.bat (Git Bash/MSYS2/Cygwin)

The appropriate .gitattributes keep polyglot.bat saved with LF line endings (required for the Bash half), without breaking other .bat files that may need CRLF.

How is this useful?

Many IDE's allow to specify a post-build action for projects. This is for example true for Eclipse based IDE's.

However, the same IDE's exist for both Windows, Mac and Linux, and when using projects designed on one of these systems, post-build execution usualy break on the others.

For Windows the executed is typically Command.com, for Mac and Linux it's typically bash.

This polyglot file allows to specify one file in the projects and ensures it will execute on all systems.

Files

polyglot/
├── polyglot.bat          # the actual polyglot script (LF line endings!)
├── execute_as_bash.bat   # Batch helper that finds and invokes bash.exe
└── .gitattributes        # enforces LF for polyglot.bat, CRLF for other .bat

Quick start

Windows (cmd.exe):

polyglot.bat [args...]

The Batch front calls execute_as_bash.bat, which locates a real Bash and re-executes the same file under Bash.

Bash (Git Bash/MSYS2/Cygwin):

bash polyglot.bat [args...]

Bash skips the Batch block and runs the Bash section directly.

bash executeables

A list of possible locations to find bash is provided and should be edited as needed. Beware that the bash in Windows32 triggers a WLS bash thats not usefull.

Example output

From cmd.exe:

c:\Projects\polyglot>polyglot.bat 1 "2 3" 5 '6 7' 8
 Batch: c:\Projects\polyglot\polyglot.bat
 Bash:  /c/Projects/polyglot/polyglot.bat
 $1: 1
 $2: 2 3
 $3: 5
 $4: '6
 $5: 7'
 $6: 8
 $*: 1 2 3 5 '6 7' 8

From Bash:

INTERNAL+DEVELOPMENT+PC MINGW64 /C/Projects/polyglot
$ bash polyglot.bat 1 "2 3" 5 '6 7' 8
 Bash:  /C/Projects/polyglot/polyglot.bat
 $1: 1
 $2: 2 3
 $3: 5
 $4: 6 7
 $5: 8
 $*: 1 2 3 5 6 7 8

Exit codes (rewrite as needed):

100 – no script path provided

101 – script path not found

102 – no suitable bash.exe found

otherwise – the exit code from the Bash code running.

Notes

Line endings: polyglot.bat must be LF. This repo’s .gitattributes enforces it:

*.bat text eol=crlf
polyglot.bat text eol=lf

If you add the rule later, run:

git add --renormalize polyglot.bat && git commit -m "Normalize polyglot.bat to LF"

Quoting differences: In cmd.exe, only double quotes group arguments; single quotes do not - as shown in the examples.

Unfortunately it's not possible to provide a shebang in the polyglot file.

@echo off
setlocal enabledelayedexpansion
:: Usage: execute_as_bash.bat "<bash_script_to_run>" [args...]
:: Return code: 100 if script to execute was not provided as an argument.
:: 101 if script to execute can not be located in the filesystem.
:: 102 if bash executeable is not found
:: One caviat. When executing from batch, only double quotes " can be used for grouping of arguments with spaces.
:: All other return codes are comming from the script executed.
:: Rewrite as needed.
if "%~1"=="" (
echo.[ERROR] %0 did not receive a script to execute. >&2
exit /b 100
)
if not exist "%~1" (
echo.[ERROR] %0 can not locate %~1 >&2
exit /b 101
)
rem ===== Ordered candidate list =====
set "BASH_CANDIDATE[0]=C:\Program Files\Git\bin\bash.exe"
set "BASH_CANDIDATE[1]=C:\Program Files\Git\usr\bin\bash.exe"
set "BASH_CANDIDATE[2]=C:\Program Files\Git\git-bash.exe"
set "BASH_CANDIDATE[3]=C:\msys64\usr\bin\bash.exe"
set "BASH_CANDIDATE[4]=C:\cygwin64\bin\bash.exe"
set BASH_MAX=4
set "BASH_EXE="
:: Search through the candidates. The first found bash, wins.
for /L %%i in (0,1,%BASH_MAX%) do (
if defined BASH_CANDIDATE[%%i] (
if exist "!BASH_CANDIDATE[%%i]!" (
set "BASH_EXE=!BASH_CANDIDATE[%%i]!"
goto :bash_found
)
)
)
echo.[ERROR] %0 did not find a bash. Looked here: >&2
for /L %%i in (0,1,%BASH_MAX%) do if defined BASH_CANDIDATE[%%i] echo. !BASH_CANDIDATE[%%i]! >&2
exit /b 102
:bash_found
:: %~1 -> the script to run (absolute path from caller)
set "SCRIPT=%~1"
::echo.[NOTICE] "%~f0" is using bash at "!BASH_EXE!" to re-execute "!SCRIPT! in bash mode" >&2
shift
set "ARGS="
:loop
if "%~1"=="" goto run
:: Append the next arg, quoted to preserve spaces
set "ARGS=%ARGS% "%~1""
shift
goto loop
:run
:: Optional hardening: --noprofile --norc to avoid user rc files changing env/cwd
"!BASH_EXE!" -- "!SCRIPT!" %ARGS%
exit /b %ERRORLEVEL%
:<<BASH_SECTION_START
@echo off
:: ----------------------- bash/batch polyglot file. ------------------------
:: ----------- The batch front: re-executes this file under Bash -----------
:: - Implemented using the here-doc technique, combined with a batch label -
:: --- Command.com requires a batch script to use extension .bat or .cmd ---
:: ------ WARNING - THIS FILE MUST BE SAVES WITH LF LINE ENDINGS ONLY ------
:: -------------------------> See .gitattributes! <-------------------------
echo. Batch: %~f0
call "%~dp0execute_as_bash.bat" "%~f0" %*
exit /b %ERRORLEVEL%
BASH_SECTION_START
# ----- Bash section (Batch above is skipped) -----
script_full="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)/$(basename -- "${BASH_SOURCE[0]}")"
printf ' Bash: %s\n' "$script_full"
# Each arg as you'd access it: $1, $2, ... NOTE $0 may not resolve as expected.
for ((i=1; i<=$#; i++)); do
printf ' $%d: %s\n' "$i" "${!i}"
done
printf ' $*: %s\n' "$*"
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment