Executing Cygwin Bash scripts on Windows

I was reading Jeremy Rothman-Shore’s post regarding kicking off a Cygwin script from a Windows batch file which addresses two things:

  1. Invoking Cygwin’s Bash, passing it a shell script to execute
  2. Resolving the script’s directory in bash so that it can access other resources its directory.

Here I want to improve a bit on the first point and come up with:

  • A general purpose “shim” that will execute a shell script on Windows in Cygwin
  • Will pass through any command-line arguments
  • Doesn’t trip up on any spaces in the path

The idea is that if I have a shell script called foo.sh, I create a Windows equivalent called foo.cmd alongside it that can be called directly on Windows without the caller worrying about Cygwin etc (apart from having it installed):

image

(Or foo.bat – I prefer the *.cmd extension because this isn’t MS-DOS we’re running here).

The CMD script has to:

  1. Find its fully-qualified current location and locate its *.sh counterpart
  2. Translate this Windows-style path into a Unix style path
  3. Pass this to Cygwin’s bash along with any other arguments

Firstly, finding a batch file’s location is similar to how it’s done in Unix: it’s the first argument to the script. So in our case we use %0, and we can extract parts of the path like so: %~dp0 will extract the drive and path from argument 0 (i.e. the directory). See the for command for more information on this funky %~ syntax.

Secondly, the translation from a c:\windows\style\path to a /unix/style/path is done by Cygwin’s cygpath command. We do this in a slightly roundabout way via the ever versatile for command.

Thirdly, arguments passed to a batch file can either be accessed individually using $1, %2, $3 etc, or all in one go using %*, which is what we use here.

In addition, so that we don’t litter the Cygwin environment with temporary variables that we’ve created, we use a little setlocal/endlocal trick.

Here it is:

@echo off
setlocal

if not exist "%~dpn0.sh" echo Script "%~dpn0.sh" not found & exit 2

set _CYGBIN=C:\cygwin\bin
if not exist "%_CYGBIN%" echo Couldn't find Cygwin at "%_CYGBIN%" & exit 3

:: Resolve ___.sh to /cygdrive based *nix path and store in %_CYGSCRIPT%
for /f "delims=" %%A in ('%_CYGBIN%\cygpath.exe "%~dpn0.sh"') do set _CYGSCRIPT=%%A

:: Throw away temporary env vars and invoke script, passing any args that were passed to us
endlocal & %_CYGBIN%\bash --login "%_CYGSCRIPT%" %*

Note that you just name this the same as your shell script with a .cmd or .bat file extension. and then just execute it as normal.

For example for the following shell script called foo.sh:

#!/bin/sh
echo
echo "Hello from bash script $0"
echo "Working dir is $(pwd)"
echo
echo "arg 1 = $1"
echo "arg 2 = $2"
echo

Here’s me calling it from Windows:

image

Hope that helps!

About these ads

9 Responses to Executing Cygwin Bash scripts on Windows

  1. toto says:

    that is what I want. I want to running bash script in windows without virtual machine. thank you for sharing this tutorial

    • Steve says:

      I could not get this to work and the error message is:

      C:\PROJECTS\Quadrant_Trunk\buildscripts>foo abc xyz
      ‘C:\cygwin\bin’ is not recognized as an internal or external command,
      operable program or batch file.
      ‘C:\cygwin\bin’ is not recognized as an internal or external command,
      operable program or batch file.

      • Duncan Smart says:

        Remove the “@echo off” at the top of the script to get a better idea of what’s actually getting executed.

  2. canonical says:

    Thanks! This was exactly what I needed and works perfectly!

  3. Danny says:

    Works great, small change in the .cmd though:

    Change line 11 (a blank line) to:
    for /f “delims=” %%A in (‘%_CYGBIN%\cygpath.exe “%CD%”‘) do set _CYGPATH=%%A

    And line 13 to:
    endlocal & echo cd %_CYGPATH%; “%_CYGSCRIPT%” %* | %_CYGBIN%\bash –login -s

    This changes the working directory to the directory it is called from.

    I wrote a .sh file I used to handle a combination of wget and awk which took some variables from files in the directory, if the files do not exist it would make them and set defaults, and then called the same script from different project directories (most of my work is the same, only with different names here and there). Before I would work with two or three command line consoles open, at least one cygwin and one cmd. However, the first time I used you script I put it in a folder in my %PATH%, and each time I ran it the sh would run from my home (~/). This is the fix I found.

  4. Graham says:

    I replaced line 13 with that suggested by Danny; ie:

    endlocal & echo cd %_CYGPATH%; “_CYGSCRIPT%” %* | %_CYGBIN%\bash –login -s

    and can’t get it to work, for directory d:\dir.

    D:\dir>endlocal & echo cd /cygdrive/d/dir; “_CYGSCRIPT* | C:\cygwin\bin\bash –login -s
    cd /cygdrive/d/dir; “_CYGSCRIPT* | C:\cygwin\bin\bash –login -s

    It’s clearly trying to cd to /cygdrive/d/dir but this is a cygwin command and not a dos command.

  5. anon says:

    thanks

  6. percy says:

    Great little helper! Thanks a lot! :-)

  7. Pingback: Old Click » Running 'Linux scripts' on Mac or Windows systems

Follow

Get every new post delivered to your Inbox.

Join 35 other followers

%d bloggers like this: