How to Write One Script That Runs in Bash/Shell and Powershell

Is it possible to write one script that runs in bash/shell and PowerShell?

It is possible; I don't know how compatible this is, but PowerShell treats strings as text and they end up on screen, Bash treats them as commands and tries to run them, and both support the same function definition syntax. So, put a function name in quotes and only Bash will run it, put "exit" in quotes and only Bash will exit. Then write PowerShell code after.

NB. this works because the syntax in both shells overlaps, and your script is simple - run commands and deal with variables. If you try to use more advanced script (if/then, for, switch, case, etc.) for either language, the other one will probably complain.

Save this as dual.ps1 so PowerShell is happy with it, chmod +x dual.ps1 so Bash will run it

#!/bin/bash

function DoBashThings {
wget http://www.example.org/my.script -O my.script
# set a couple of environment variables
export script_source=http://www.example.org
export some_value=floob
# now execute the downloaded script
bash ./my.script
}

"DoBashThings" # This runs the bash script, in PS it's just a string
"exit" # This quits the bash version, in PS it's just a string


# PowerShell code here
# --------------------
Invoke-WebRequest "http://www.example.org/my.script.ps1" -OutFile my.script.ps1
$env:script_source="http://www.example.org"
$env:some_value="floob"
PowerShell -File ./my.script.ps1

then

./dual.ps1

on either system.


Edit: You can include more complex code by commenting the code blocks with a distinct prefix, then having each language filter out its own code and eval it (usual security caveats apply with eval), e.g. with this approach (incorporating suggestion from Harry Johnston ):

#!/bin/bash

#posh $num = 200
#posh if (150 -lt $num) {
#posh write-host "PowerShell here"
#posh }

#bash thing="xyz"
#bash if [ "$thing" = "xyz" ]
#bash then
#bash echo "Bash here"
#bash fi

function RunBashStuff {
eval "$(grep '^#bash' $0 | sed -e 's/^#bash //')"
}

"RunBashStuff"
"exit"

((Get-Content $MyInvocation.MyCommand.Source) -match '^#posh' -replace '^#posh ') -join "`n" | Invoke-Expression

Run bash script from Windows PowerShell

You should put the script as argument for a *NIX shell you run, equivalent to the *NIXish

sh myscriptfile

Running Powershell script in bash in single line

I'd like to offer an alternative approach to this, which is even more useful when you have a complex set of commands that you'd like to run as a one-liner.

Both PowerShell and PowerShell Core support an -EncodedCommand parameter.
The article above details the parameter as:

Accepts a Base64-encoded string version of a command. Use this parameter to submit commands to PowerShell that require complex, nested quoting. The Base64 representation must be a UTF-16LE encoded string.

With the EncodedCommand parameter, it's possible for us to pass a single Base64 string to the PowerShell or PWSH program and it will decode it and interpret it as though the original commands were run.
I've found this extremely useful in the past when dealing with nested statements and script blocks containing quotes and apostrophes.

As detailed above, the EncodedCommand parameter expects the commands to be converted to a UTF-16LE Base64 string.
There are various online convertors that will handle the conversion if you wish (one such converter can be found here: PowerShell Encoder)

As an example, let's pretend we want to run the following commands:

Import-Module ./myFile.psm1
Set-Values -Body 10
Upload-Values
$result = Confirm-Upload
if ($result -eq "OK") {
Write-Host 'Upload Successful'
} else {
Write-Host 'Upload Failed'
}

After conversion to the Base64 string, we end up with this string:

SQBtAHAAbwByAHQALQBNAG8AZAB1AGwAZQAgAC4ALwBtAHkARgBpAGwAZQAuAHAAcwBtADEADQAKAFMAZQB0AC0AVgBhAGwAdQBlAHMAIAAtAEIAbwBkAHkAIAAxADAADQAKAFUAcABsAG8AYQBkAC0AVgBhAGwAdQBlAHMADQAKACQAcgBlAHMAdQBsAHQAIAA9ACAAQwBvAG4AZgBpAHIAbQAtAFUAcABsAG8AYQBkAA0ACgBpAGYAIAAoACQAcgBlAHMAdQBsAHQAIAAtAGUAcQAgACIATwBLACIAKQAgAHsADQAKACAAIAAgAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAnAFUAcABsAG8AYQBkACAAUwB1AGMAYwBlAHMAcwBmAHUAbAAnAA0ACgB9ACAAZQBsAHMAZQAgAHsADQAKACAAIAAgAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAnAFUAcABsAG8AYQBkACAARgBhAGkAbABlAGQAJwANAAoAfQA=

To execute this, we'd simply do the following:

pwsh -EncodedCommand SQBtAHAAbwByAHQALQBNAG8AZAB1AGwAZQAgAC4ALwBtAHkARgBpAGwAZQAuAHAAcwBtADEADQAKAFMAZQB0AC0AVgBhAGwAdQBlAHMAIAAtAEIAbwBkAHkAIAAxADAADQAKAFUAcABsAG8AYQBkAC0AVgBhAGwAdQBlAHMADQAKACQAcgBlAHMAdQBsAHQAIAA9ACAAQwBvAG4AZgBpAHIAbQAtAFUAcABsAG8AYQBkAA0ACgBpAGYAIAAoACQAcgBlAHMAdQBsAHQAIAAtAGUAcQAgACIATwBLACIAKQAgAHsADQAKACAAIAAgAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAnAFUAcABsAG8AYQBkACAAUwB1AGMAYwBlAHMAcwBmAHUAbAAnAA0ACgB9ACAAZQBsAHMAZQAgAHsADQAKACAAIAAgAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAnAFUAcABsAG8AYQBkACAARgBhAGkAbABlAGQAJwANAAoAfQA=

Running powershell script inside a bash script

Are you trying

.\scriptfile.ps1

?
That should be

./scriptfile.ps1

But also, when invoking powershell from a bash script, you'll need to either run the pwsh command like

pwsh ./scriptfile.ps1

or the first line of your Powershell script file should be a shebang (interpreter directive) like:

#!/usr/bin/env pwsh

See How can I use a shebang in a PowerShell script?

How can I direct PowerShell 7 to run a bash (Ubuntu) script in WSL2 on Windows 10?


I have a feeling I'm missing something incredibly simple

Not really. In my experience, calling one scripting language from another is rarely what-I-would-call "simple" ;-).

That's a good attempt to figure it out from the help, but unfortunately there's just not enough detail in the wsl --help output to figure it out from that.

As noted in the help, though, there are a few constructs available for running commands in a WSL instance using the wsl.exe command.

  • wsl <commandline>: Runs the command line as an argument to the default shell.
  • wsl -e <command>: Runs the command in place of the shell

Note that, for the sake of safety, I'm going to replace your command-lines with more benign versions ;-).

for f in $(find -xdev -type l); do echo $f "----" $(readlink $f); done

Side note: As someone once pointed out to me when I used find with for -- Why is looping over find's output bad practice?, so it would really be better off as find -xdev -type l | xargs -I % sh -c "echo -n % '---- '; readlink %", but we're going to leave it in a for loop to demonstrate part of the problem here.

The following two tries fail because ...

  • wsl --cd ~ -c 'for f in $(find -xdev -type l); do readlink $f; done'

    -c is not a flag that is understood by wsl.exe

  • wsl.exe --cd ~ 'for f in $(find -xdev -type l); do readlink $f; done'

    That would be the equivalent of running cd ~; for f in $(find -xdev -type l); do readlink $f; done, which won't work either, of course.

    Actually, in retrospect, this might work for you. It failed for me because my default shell is Fish, but wsl does seem to attempt to run the default shell with -c for whatever command-line is passed in.

    It may have failed for you because you weren't setting the directory (via --cd) on the same command-line before calling it.

  • wsl.exe --cd ~ -e 'for f in $(find -xdev -type l); do readlink $f; done'

    And, while you didn't mention trying this, this particular one won't work either, since -e needs to be a single "command", but that's a full commandline with shell builtins such as for.

"Aaargggh!", you've been saying, right? Catch 22? You need -c to get to the shell, but the wsl command can't pass it.

So, we use:

wsl --cd ~ -e sh -c 'for f in $(find -xdev -type l); do echo $f "----" $(readlink $f); done'

That:

  • Changes to the home directory (you can replace with the $FOLDER variable from PowerShell, of course)
  • Executes the sh shell (you could also use Bash (or any other shell or command) if you need any of its constructs)
  • Passes the commandline via -c to the shell. This is a fairly normal argument for most shells, POSIX or otherwise.

Note (from experience) that quoting rules between PowerShell and WSL/sh can get fairly "unruly". I find that as the example gets more complicated, it's often better to put the shell commands in a script inside WSL, which you then execute from PowerShell. For example, something like:

wsl --cd ~ -e sh -c "~/.local/bin/myscript.sh"
--cd note

Using two separate wsl commands, like in your --cd example:

wsl.exe --cd $Folder
wsl.exe echo "Hello World" # Never executes

Assuming you were executing that via a script, the first line:

  • Changes to the specified directory
  • Runs the default shell, so you are then in an interactive session

If you then exit the shell (Ctrl+D or exit), then you should see the output from the second command.

You can also see this interactively if you run it from PowerShell via:

wsl --cd ~ ; wsl echo Hello

Single script to run in both Windows batch and Linux Bash?

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.).

Windows Shell scripting and powershell


  • Does the script for writing batch file is similar to shell script in Linux/Unix?

No. Batch syntax is completely different from Linux/Unix shell script syntax. Check the homepage of the awesome Rob van der Woude for tutorials on batch.

  • Is using Powershell is an advantage or learning is limited to windows server configuration/version?

Using PowerShell is most likely an advantage. PowerShell is installed by default since Windows Vista/Server 2008, but is also available for Windows XP and Server 2003. If you have to decide whether you want to learn batch or PowerShell: go for PowerShell. It's far more versatile than batch.

How to run a bash script on wsl with powershell?

To run the script on wsl you simply invoke bash

> bash simple_script.sh
hi from simple script

To save it in a variable and have it run as a bash script within wsl or powershell, there is no need for Get-Content

> $simple_script = bash /mnt/c/Users/user-name/path/to/simple_script.sh
> Write-Output $simple_script
hi from simple script

NOTE: Powershell has an alias mapping echo to Write-Output, so you could also use echo

> $simple_script = bash /mnt/c/Users/user-name/path/to/simple_script.sh
> echo $simple_script
hi from simple script

You can also grab the content if that was your initial aim.

> Get-Content simple_script.sh
#!/bin/bash
echo "hi from simple script"

> $content = Get-Content .\simple_script.sh
> Write-Output $content
#!/bin/bash
echo "hi from simple script"



Related Topics



Leave a reply



Submit