133

Is it possible to write a single script file which executes in both Windows (treated as .bat) and Linux (via Bash)?

I know the basic syntax of both, but didn't figure out. It could probably exploit some Bash's obscure syntax or some Windows batch processor glitch.

The command to execute may be just a single line to execute other script.

The motivation is to have just a single application boot command for both Windows and Linux.

Update: The need for system's "native" shell script is that it needs to pick the right interpreter version, conform to certain well-known environment variables etc. Installing additional environments like CygWin is not preferable - I'd like to keep the concept "download & run".

The only other language to consider for Windows is Windows Scripting Host - WSH, which is preset by default since 98.

6
  • 12
    Look into Perl or Python. Those are scripting languages available on both platforms. Commented Jul 7, 2013 at 9:10
  • 1
    groovy, python, ruby anyone? stackoverflow.com/questions/257730/… Commented Jun 25, 2015 at 20:38
  • Groovy would be great to have on all systems by default, I'd take it as a shell scripting lang. Maybe some day :) Commented Oct 20, 2016 at 12:35
  • 3
    See also: github.com/BYVoid/Batsh Commented Dec 13, 2017 at 22:39
  • 3
    A compelling use case for this is tutorials - to be able to just tell people "run this little script" without having to explain that "if you are on Windows, use this little script, but if you are on Linux or Mac, try this instead, which I haven't actually tested because I'm on Windows." Sadly Windows doesn't have basic unix-like commands like cp built in, or vice versa so writing two separate scripts may be pedagogically better than the advanced techniques shown here. Commented Jul 1, 2018 at 17:32

14 Answers 14

123

What I have done is use cmd’s label syntax as comment marker. The label character, a colon (:), is equivalent to true in most POSIXish shells. If you immediately follow the label character by another character which can’t be used in a GOTO, then commenting your cmd script should not affect your cmd code.

The hack is to put lines of code after the character sequence “:;”. If you’re writing mostly one-liner scripts or, as may be the case, can write one line of sh for many lines of cmd, the following might be fine. Don’t forget that any use of $? must be before your next colon : because : resets $? to 0.

:; echo "Hi, I’m ${SHELL}."; exit $?
@ECHO OFF
ECHO I'm %COMSPEC%

A very contrived example of guarding $?:

:; false; ret=$?
:; [ ${ret} = 0 ] || { echo "Program failed with code ${ret}." >&2; exit 1; }
:; exit
ECHO CMD code.

Another idea for skipping over cmd code is to use heredocs so that sh treats the cmd code as an unused string and cmd interprets it. In this case, we make sure that our heredoc’s delimiter is both quoted (to stop sh from doing any sort of interpretation on its contents when running with sh) and starts with : so that cmd skips over it like any other line starting with :.

:; echo "I am ${SHELL}"
:<<"::CMDLITERAL"
ECHO I am %COMSPEC%
::CMDLITERAL
:; echo "And ${SHELL} is back!"
:; exit
ECHO And back to %COMSPEC%

Depending on your needs or coding style, interlacing cmd and sh code may or may not make sense. Using heredocs is one method to perform such interlacing. This could, however, be extended with the GOTO technique:

:<<"::CMDLITERAL"
@ECHO OFF
GOTO :CMDSCRIPT
::CMDLITERAL

echo "I can write free-form ${SHELL} now!"
if :; then
  echo "This makes conditional constructs so much easier because"
  echo "they can now span multiple lines."
fi
exit $?

:CMDSCRIPT
ECHO Welcome to %COMSPEC%

Universal comments, of course, can be done with the character sequence : # or :;#. The space or semicolon are necessary because sh considers # to be part of a command name if it is not the first character of an identifier. For example, you might want to write universal comments in the first lines of your file before using the GOTO method to split your code. Then you can inform your reader of why your script is written so oddly:

: # This is a special script which intermixes both sh
: # and cmd code. It is written this way because it is
: # used in system() shell-outs directly in otherwise
: # portable code. See https://stackoverflow.com/questions/17510688
: # for details.
:; echo "This is ${SHELL}"; exit
@ECHO OFF
ECHO This is %COMSPEC%

Thus, some ideas and ways to accomplish sh and cmd-compatible scripts without serious side effects as far as I know (and without having cmd output '#' is not recognized as an internal or external command, operable program or batch file.).

Sign up to request clarification or add additional context in comments.

14 Comments

Notice this requires the script to have .cmd extension, otherwise it will not run in Windows. Is there any workaround?
If you’re writing batch files, I would recommend just naming them .cmd and marking them executable. That way, executing the script from the default shell on either Windows or unix will work (only tested with bash). Because I cannot think of a way to include a shebang without making cmd complain loudly, you must always invoke the script via shell. I.e., be sure to use execlp() or execvp() (I think system() does this the “right” way for you) rather than execl() or execv() (these latter functions will only work on scripts with shebangs because they go more directly to the OS).
I omitted discussion about file extensions because I was writing my portable code in shellouts (e.g., <Exec/> MSBuild Task) rather than as independent batch files. Maybe if I have some time in the future I’ll update the answer with the information in these comments…
@GabrielStaples please see this comment. To call in sh, use the usual ./file.cmd or put it in PATH and call it using file.cmd. In CMD, call it using .\file.cmd or put it in Path and call it using file.cmd.
|
33

EDIT

The binki's answer is almost perfect but still can be improved:

:<<BATCH
    @echo off
    echo %PATH%
    exit /b
BATCH

echo $PATH

It uses again the : trick and the multi line comment. Looks like cmd.exe (at least on windows10) works without problems with the unix style EOLs so be sure that your script is converted into linux format. (same approach has been seen used before here and here ) . Though using shebang still will produce redundant output...


you can try this:

#|| goto :batch_part
 echo $PATH
#exiting the bash part
exit
:batch_part
 echo %PATH%

Probably you'll need to use /r/n as a new line instead of a unix style.If I remember correct the unix new line is not interpreted as a new line by .bat scripts.Another way is to create an #.exe file in the path that does do nothing in similar manner as my answer here: Is it possible to embed and execute VBScript within a batch file without using a temporary file?

7 Comments

/r/n at the end of lines will cause many headaches in the bash script unless you end each line with a # (i.e. #/r/n) - that way the \r will be treated as part of the comment and ignored.
Actually, I find that # 2>NUL & ECHO Welcome to %COMSPEC%. manages to hide the error about the # command not being found by cmd. This technique is useful when you need to write a standalone line that will only be executed by cmd (e.g., in a Makefile).
How do you write comments?
@GabrielStaples - you should not have problems with using batch style comments in the batch part and linux style comments in the bash part.
@npocmaka, is it possible to have any header-style comments before the Batch part?
|
13

I wanted to comment, but can only add an answer at the moment.

The techniques given are excellent and I use them also.

It is hard to retain a file which has two kinds of line breaks contained within it, that being /n for the bash part and /r/n for the windows part. Most editors try and enforce a common line break scheme by guessing what kind of file you are editing. Also most methods of transferring the file across the internet (particularly as a text or script file) will launder the line breaks, so you could start with one kind of line break and end up with the other. If you made assumptions about line breaks and then gave your script to someone else to use they might find it doesn't work for them.

The other problem is network mounted file systems (or CDs) that are shared between different system types (particularly where you can't control the software available to the user).

One should therefore use the DOS line break of /r/n and also protect the bash script from the DOS /r by putting a comment at the end of each line (#). You also cannot use line continuations in bash because the /r will cause them to break.

In this way whoever uses the script, and in whatever environment, it will then work.

I use this method in conjunction with making portable Makefiles!

Comments

13

The previous answers seem to cover pretty much all the options and helped me a lot. I'm including this answer here just to demonstrate the mechanism I used to include both a Bash script and a Windows CMD script in the same file.

LinuxWindowsScript.bat

echo >/dev/null # >nul & GOTO WINDOWS & rem ^
echo 'Processing for Linux'

# ***********************************************************
# * NOTE: If you modify this content, be sure to remove carriage returns (\r) 
# *       from the Linux part and leave them in together with the line feeds 
# *       (\n) for the Windows part. In summary:
# *           New lines in Linux: \n
# *           New lines in Windows: \r\n 
# ***********************************************************

# Do Linux Bash commands here... for example:
StartDir="$(pwd)"

# Then, when all Linux commands are complete, end the script with 'exit'...
exit 0

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

:WINDOWS
echo "Processing for Windows"

REM Do Windows CMD commands here... for example:
SET StartDir=%cd%

REM Then, when all Windows commands are complete... the script is done.

Summary

In Linux

The first line (echo >/dev/null # >nul & GOTO WINDOWS & rem ^) will be ignored and the script will flow through each line immediately following it until the exit 0 command is executed. Once exit 0 is reached, the script execution will end, ignoring the Windows commands below it.

In Windows

The first line will execute the GOTO WINDOWS command, skipping the Linux commands immediately following it and continuing execution at the :WINDOWS line.

Removing Carriage Returns in Windows

Since I was editing this file in Windows, I had to systematically remove the carriage returns (\r) from the Linux commands or else I got abnormal results when running the Bash portion. To do this, I opened the file in Notepad++ and did the following:

  1. Turn on the option for viewing end of line characters (View> Show Symbol > Show End of Line). Carriage returns will then show as CR characters.

  2. Do a Find & Replace (Search > Replace...) and check the Extended (\n, \r, \t, \0, \x...) option.

  3. Type \r in the Find what : field and blank out the Replace with : field so there's nothing in it.

  4. Starting at the top of the file, click the Replace button until all of the carriage return (CR) characters have been removed from the top Linux portion. Be sure to leave the carriage return (CR) characters for the Windows portion.

The result should be that each Linux command ends in just a line feed (LF) and each Windows command ends in a carriage return and line feed (CR LF).

1 Comment

In Notepad++ just select Edit > EOL conversion > LF, no need to do such replacements. And there's no need to do that everytime, just set LF as default if necessary in Settings > Preferences. Otherwise there are lots of ways to do CRLF conversion on Windows
10

The following works for me without any errors or error messages with Bash 4 and Windows 10, unlike the answers above. I name the file "whatever.cmd", do chmod +x to make it executable in linux, and make it have unix line endings (dos2unix) to keep bash quiet.

:; if [ -z 0 ]; then
  @echo off
  goto :WINDOWS
fi

if [ -z "$2" ]; then
  echo "usage: $0 <firstArg> <secondArg>"
  exit 1
fi

# bash stuff
exit

:WINDOWS
if [%2]==[] (
  SETLOCAL enabledelayedexpansion
  set usage="usage: %0 <firstArg> <secondArg>"
  @echo !usage:"=!
  exit /b 1
)

:: windows stuff

Comments

9

You can share variables:

:;SET() { eval $1; }

SET var=value

:;echo $var
:;exit
ECHO %var%

Comments

7

There are several ways of executing different commands on bash and cmd with the same script.

cmd will ignore lines that start with :;, as mentioned in other answers. It will also ignore the next line if the current line ends with the command rem ^, as the ^ character will escape the line break and the next line will be treated as a comment by rem.

As for making bash ignore the cmd lines, there are multiple ways. I have enumerated some ways to do that without breaking the cmd commands:

Non-existent # command (not recommended)

If there is no # command available on cmd when the script is run, we can do this:

# 2>nul & echo Hello cmd! & rem ^
echo 'Hello bash!' #

The # character at the beginning of the cmd line makes bash treat that line as a comment.

The # character at the end of the bash line is used to comment out the \r character, as Brian Tompsett pointed out in his answer. Without this, bash will throw an error if the file has \r\n line endings, required by cmd.

By doing # 2>nul, we're tricking cmd to ignore the error of some non-existent # command, while still executing the command that follows.

Don't use this solution if there is a # command available on the PATH or if you have no control over the commands available to cmd.


Using echo to ignore the # character on cmd

We can use echo with it's output redirected to insert cmd commands on bash's commented out area:

echo >/dev/null # >nul & echo Hello cmd! & rem ^
echo 'Hello bash!' #

Since the # character has no special meaning on cmd, it is treated as a part of the text to echo. All we had to do is redirect the output of the echo command and insert other commands after it.


Empty #.bat file

echo >/dev/null # 1>nul 2> #.bat
# & echo Hello cmd! & del #.bat & rem ^
echo 'Hello bash!' #

The echo >/dev/null # 1>nul 2> #.bat line creates an empty #.bat file while on cmd (or replaces existing #.bat, if any), and does nothing while on bash.

This file will be used by the cmd line(s) that follows even if there is some other # command on the PATH.

The del #.bat command on the cmd-specific code deletes the file that was created. You only have to do this on the last cmd line.

Don't use this solution if a #.bat file could be on your current working directory, as that file will be erased.


Recomended: using here-document to ignore cmd commands on bash

:; echo 'Hello bash!';<<:
echo Hello cmd! & ^
:

By placing the ^ character at the end of the cmd line we're escaping the line break, and by using : as the here-document delimiter, the delimiter line contents will have no effect on cmd. That way, cmd will only execute its line after the : line is over, having the same behaviour as bash.

If you want to have multiple lines on both platforms and only execute them at the end of the block, you can do this:

:;( #
  :;  echo 'Hello'  #
  :;  echo 'bash!'  #
:; );<<'here-document delimiter'
(
      echo Hello
      echo cmd!
) & rem ^
here-document delimiter

As long as there is no cmd line with exactly here-document delimiter, this solution should work. You can change here-document delimiter to any other text.


In all of the presented solutions, the commands will only be executed after the last line, making their behaviour consistent if they do the same thing on both platforms.

Those solutions must be saved to files with \r\n as line breaks, otherwise they won't work on cmd.

1 Comment

Better late than never... this helped me more than the other answers. Thanks!!
6

I use this technique to create runnable jar files. Since the jar/zip file starts at the zip header, I can put a universal script to run this file at the top:

#!/usr/bin/env sh\n
@ 2>/dev/null # 2>nul & echo off & goto BOF\r\n
:\n
<shell commands go here with \n line endings>
exit\n
\r\n
:BOF\r\n
<cmd commands go here with \r\n line endings>\r\n
exit /B %errorlevel%\r\n
}

It is important to set the line endings as outlined above because they can cause issues on the different platforms. Also the goto statement will not work correctly in some cases if the proper line endings are missing around the jump label.

The technique above is what I use currently. Below is an outdated version with an in-depth explaination:

#!/usr/bin/env sh
@ 2>/dev/null # 2>nul & echo off
:; alias ::=''
:: exec java -jar $JAVA_OPTS "$0" "$@"
:: exit
java -jar %JAVA_OPTS% "%~dpnx0" %*
exit /B
  • The first line does echo off in cmd and doesn't print anything on sh. This is because the @ in sh throws an error that is piped to /dev/null and after that a comment starts. On cmd the pipe to /dev/null fails because the file is not recognized on windows but since windows doesn't detect # as a comment the error is piped to nul. Then it does an echo off. Because the whole line is preceded by an @ it doesn't get printet on cmd.
  • The second one defines ::, which starts a comment in cmd, to noop in sh. This has the benefit that :: does not reset $? to 0. It uses the ":; is a label" trick.
  • Now I can prepend sh commands with :: and they are ignored in cmd
  • On :: exit the sh script ends and I can write cmd commands
  • Only the first line (shebang) is problematic in cmd since it will print command not found. You have to decide yourself if you need it or not.

Comments

2

I needed this for some of my Python package install scripts. Most things between sh and bat file are same but few things like error handling are different. One way to do this is as follows:

common.inc
----------
common statement1
common statement2

Then you call this from bash script:

linux.sh
--------
# do linux specific stuff
...
# call common code
source common.inc

Windows batch file looks like this:

windows.bat
-----------
REM do windows specific things
...
# call common code
call common.inc

Comments

0

Try my BashWin project at https://github.com/skanga/bashwin which uses BusyBox for most Unix commands

Comments

0

Binki's answer was great, but stopped short of a solution for the shebang error output on Windows.

C:\>#!/bin/bash
'#!' is not recognized as an internal or external command,
operable program or batch file.

I found you can get windows to redirect the error output to null, with clever encoding of the line break. Windows honors the '/r/n' and '/n' line breaks, but not '/r'. So if we make the first line break '/r', Windows treats the first two lines as a single line, and redirects the error output to null.

#!/bin/bash
# > nul 2>&1

Windows will still echo that code, but at least it keeps the error output stream clean. MacOS and Linux treat these lines as separate, so the shebang is recognized and the next line is ignored as a comment. I tested this on macOS 13.2.1, Windows 10 22H2, and Ubuntu 22.04.2 LTS.

The downside to this is that playing with line break encoding is just asking for trouble, at least on Windows. For example, Notepad creates new line breaks based on the style of the first line break. But PowerShell ISE uses the line breaks around the modified text to determine what style to use. And even if you get everything right, some scripting platforms will re-write line breaks based on the target OS.

Comments

0

Building upon @npocmaka's answer and @binki's answer, here's a full solution and example, including with comments.

First off, Bash scripts typically end in .sh, but don't have to. They can have any file extension or none at all. Windows Command Prompt (cmd.exe) scripts, on the other hand, appear to have to end in .bat or .cmd.

So, to make it clear that the script is a dual-purpose cmd.exe and Bash script, I like to use .sh.cmd as the file extension.

Second, @binki figured out how to do comments. My quick summary is:

  1. Use :;# for cmd.exe-and-Bash-compatible comments before and in the cmd section.
  2. Use # for Bash comments in the Bash section after the cmd section.

Mistakenly using :: for comments in the cmd section can result in Bash errors when running the script in Bash, especially if you put backticks in your comments after the ::.

Here's a full, dual-compatible program that runs in Windows cmd.exe and in Bash, either in Git Bash in Windows, or in Bash in Linux. In Windows, this program updates the PATH for the current Command Prompt session, and in Bash it just prints the current path. Notice the comments:

prePreBuildSteps.sh.cmd

:;# DESCRIPTION: this file can be run both in Bash as well as in cmd.exe 
:;# (Windows Command Prompt). 
:;# - See: This/my answer: https://stackoverflow.com/a/79126676/4561887
:;# 
:;# Run in bash (two ways): 
:;# 
:;#     chmod +x prePreBuildSteps.sh.cmd
:;#     # then:
:;#     ./prePreBuildSteps.sh.cmd
:;#     bash prePreBuildSteps.sh.cmd
:;# 
:;#
:;# Run in cmd.exe (two ways):
:;# 
:;#     "prePreBuildSteps.sh.cmd"
:;#     .\prePreBuildSteps.sh.cmd
:;#

:<<BATCH
    :;# =======================================================================
    :;# When run in cmd.exe, this section runs
    :;# =======================================================================
    
    @echo off
    echo "This is running in cmd.exe."
    echo "Updating PATH so we can find Git\bin\bash.exe..."

    :;# If running as an admin, also add the Git Bash path to the version 
    :;# of Git Bash that is not in the admin profile.
    :;# - ie: Handle the edge case where a user has installed Git Bash into 
    :;#   their personal `myusername` type user dir but is running a 
    :;#   Command Prompt as their `myusername.admin` type admin user.  
    :;# - NB: if `%USERPROFILE%` is `C:\Users\gabriel.admin` then 
    ;:#   `%USERPROFILE:~0,-6%` slices off the end of the string to produce 
    ;:#   `C:\Users\gabriel`, which is what we want here. 
    if "%USERPROFILE:~-6%"==".admin" PATH %USERPROFILE:~0,-6%\AppData\Local\Programs\Git\bin;%PATH%

    :;# Also update PATH with the two most-common Git Bash paths
    :;# See:
    :;# 1. My answer: https://stackoverflow.com/a/79126782/4561887
    :;# 2. https://stackoverflow.com/a/31165218/4561887
    PATH C:\Program Files\Git\bin;%USERPROFILE%\AppData\Local\Programs\Git\bin;%PATH%
    
    echo "New PATH:"
    echo %PATH%
    
    echo "Done."
    exit /b
BATCH


# =======================================================================
# When run in Bash, this section runs 
# =======================================================================

echo "This is running in Bash."
echo "Current PATH: $PATH"
echo "Nothing else to do..."

Run it in Linux:

chmod +x prePreBuildSteps.sh.cmd

./prePreBuildSteps.sh.cmd
# OR
bash prePreBuildSteps.sh.cmd

Run it in Windows Command Prompt:

"prePreBuildSteps.sh.cmd"
:: OR
.\prePreBuildSteps.sh.cmd

See also

  1. My answer: Adding a directory to the PATH environment variable in Windows
  2. My Q&A that uses the dual-compatible prePreBuildSteps.sh.cmd script technique above: Microchip MPLAB X IDE: configure and run a shared pre-build-step Bash script that runs in both Windows and Linux as part of the build process
  3. My answer: How to detect the OS from a Bash script?

Comments

0

Instead of resorting to these obscure, archaic techniques that produce unmaintainable code for no benefit, you should execute a python or perl script. For example, python can run shell commands via the subprocess package.

You couldn't even remove Python from most versions of Linux if you wanted to these days. And with Windows, you can just install it. I can't think of any good reason for why you wouldn't be able to install python, and yet still have scripting enabled, you can do everything with scripting you can do with python.

import subprocess

subprocess.run(["git", "add", "."])
subprocess.run(["git", "commit", "-m", "commit message"])

1 Comment

The question is explicitly asking for how to write a bash/batch hybrid. An answer suggesting the usage of a different script interpreter like Python or Perl or Node.js or PowerShell available for Windows, Linux, Mac does not answer this question. The original poster wrote additionally that the installation of other applications not installed by default on Windows and Linux is also no option.
-7

There is a platform independent build tools like Ant or Maven with xml syntax (based on Java). So, you could rewrite all your scripts in Ant or Maven an run them despite os type. Or you could just create Ant wrapper script, which will analyze os type and run appropriate bat or bash script.

1 Comment

That seems like a gross misuse of those tools. If you wanted to avoid the actual question (running in the native shell) then you should suggest a universal scripting language, like python, not a build tool that happens to be capable of being misused as a replacement for a proper scripting language.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.