Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save luskan/089d54c0daade346e2495b31cfb5bc70 to your computer and use it in GitHub Desktop.

Select an option

Save luskan/089d54c0daade346e2495b31cfb5bc70 to your computer and use it in GitHub Desktop.
Run .sh scripts inline in Windows Terminal (PowerShell) via Git Bash

Run .sh scripts inline in Windows Terminal (PowerShell) via Git Bash

Goal: type .\some-script.sh in a Windows Terminal PowerShell tab and have it run in that same window using Git Bash — instead of a separate Git Bash window that flashes open and closes.

Tested on Windows 10/11, Windows PowerShell 5.1 and PowerShell 7, with Git for Windows.


TL;DR

Two ways, pick one (or both):

  1. Explicit helper — call a gbash function: gbash ./scripts/build.sh
  2. Transparent hook (recommended) — a $PROFILE hook so .\scripts/build.sh just works like any command.

Both run Git Bash as a console child process in the current tab, not as a popup window, and not WSL.


Why the popup window happens

When you run .\build.sh in PowerShell, .sh is not in $env:PATHEXT, so PowerShell doesn't treat it as an executable. It falls back to the Windows file association for .sh, which Git for Windows points at git-bash.exe — and git-bash.exe opens a new terminal window (then closes when the script ends).

The fix is to invoke bash.exe (a console program) instead of git-bash.exe (a window launcher):

git-bash.exe                                   # opens a NEW window
& "C:\Program Files\Git\bin\bash.exe" -lc ""  # runs INLINE in the current console

& is PowerShell's call operator (needed because the path has spaces). -l = login shell (sources your profile so e.g. docker is on PATH); -c = run the following command string.


⚠️ The WSL gotcha (important)

On a typical machine there are three bash.exe on PATH, and a bare bash resolves to the first one:

C:\Windows\system32\bash.exe                                  ← WSL bash   (WRONG for these scripts)
C:\Program Files\Git\bin\bash.exe                             ← Git Bash   (what you want)
C:\Users\<you>\AppData\Local\Microsoft\WindowsApps\bash.exe   ← MS-Store WSL stub

system32\bash.exe is WSL, not Git Bash. Scripts written for Git Bash (MSYS/MinGW path handling, Docker Desktop on Windows, python vs python3 quirks) misbehave under WSL. Always pin the full Git path — never rely on a bare bash.

Tell them apart at runtime: Git Bash mounts your drive as /c, /e, … (pwd/e/proj). WSL mounts as /mnt/c, /mnt/e (pwd/mnt/e/proj).


Option 1 — gbash helper function

Put this in your PowerShell profile (notepad $PROFILE; create it first with New-Item -ItemType File -Force $PROFILE if missing):

function gbash {
    param([Parameter(ValueFromRemainingArguments = $true)][string[]] $Command)
    & "C:\Program Files\Git\bin\bash.exe" -lc ($Command -join ' ')
    if ($LASTEXITCODE) { Write-Error "bash exited $LASTEXITCODE" }
}

Usage:

gbash ./scripts/build.sh
gbash "git status && ./scripts/deploy.sh --force"

Simple and explicit. The downside: you have to prefix with gbash.\build.sh alone still pops a window.


Option 2 — Transparent $PROFILE hook (recommended)

This makes .\anything.sh run inline automatically, with no prefix. It uses PowerShell's PreCommandLookupAction, which fires before PowerShell falls back to the file-association popup, and reroutes any *.sh command to Git Bash.

Add to your $PROFILE:

# >>> sh-inline hook >>>
# Run *.sh scripts inline via Git Bash (not a popup window, not WSL).
# To disable: delete this block, or run
#   $ExecutionContext.InvokeCommand.PreCommandLookupAction = $null
$ExecutionContext.InvokeCommand.PreCommandLookupAction = {
    param($CommandName, $eventArgs)
    if ($CommandName -like '*.sh') {
        $scriptPath = $CommandName
        $eventArgs.CommandScriptBlock = {
            $resolved = Resolve-Path -LiteralPath $scriptPath -ErrorAction SilentlyContinue
            $p = if ($resolved) { $resolved.Path -replace '\\','/' } else { $scriptPath -replace '\\','/' }
            & 'C:\Program Files\Git\bin\bash.exe' -l $p @args
        }.GetNewClosure()
        $eventArgs.StopSearch = $true
    }
}
# <<< sh-inline hook <<<

Reload (. $PROFILE) or open a new tab, then:

.\scripts\build.sh                 # inline, in this window
.\scripts\local-start.sh --force   # extra args pass straight through

How it works

  • PreCommandLookupAction runs for every command lookup. When the command ends in .sh, it replaces the invocation with an inline Git Bash call and stops the normal search (StopSearch = $true) — so the popup association never fires.
  • Path fix: Resolve-Path turns .\scripts\build.sh into an absolute path, and -replace '\\','/' swaps backslashes for forward slashes so Git Bash doesn't treat \ as an escape (C:/proj/scripts/build.sh is accepted as-is).
  • .GetNewClosure() captures the script path per-invocation; @args forwards your arguments; $LASTEXITCODE propagates the script's exit code back to PowerShell.
  • -l loads the login profile (so docker, etc. are on PATH); the Git path is pinned, so it never falls through to WSL.

Gotchas

  • PowerShell 5.1 vs 7 have different $PROFILE paths. If you use both, add the block to each:
    • 5.1: …\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
    • 7: …\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
    • Check yours with echo $PROFILE in each. (In Windows Terminal, the built-in "Windows PowerShell" profile is 5.1; "PowerShell" is 7.)
  • Invoke the script as a path PowerShell recognizes: .\foo.sh, a full path, or foo.sh in the current directory. A bare name not in the cwd won't resolve.
  • It only affects PowerShell (it's in your profile). cmd.exe and other shells are untouched.
  • If Git isn't at C:\Program Files\Git, update that one path.
  • WSL vs Git Bash for the script itself: Git Bash covers sed/awk/grep/find/cp/rm/mkdir -p, etc. If a script needs Linux-only things (apt, systemctl, /proc, real symlinks, Docker-in-Linux), use WSL2 instead.

Verify

# Quick check that a .sh runs inline and reports Git Bash (not WSL):
Set-Content .\_t.sh "echo \"inline ok: bash=$(command -v bash) cwd=$(pwd)\""
.\_t.sh                      # expect: cwd=/c/... or /e/...  (NOT /mnt/...)
Remove-Item .\_t.sh

Uninstall

Delete the # >>> sh-inline hook >>><<< block from your $PROFILE (and/or the gbash function), then open a new tab. To disable for just the current session:

$ExecutionContext.InvokeCommand.PreCommandLookupAction = $null
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment