18

I need to create ONE integrated script that sets some environment variables, downloads a file using wget and runs it.

The challenge is that it needs to be the SAME script that can run on both Windows PowerShell and also bash / shell.

This is the shell script:

#!/bin/bash
# download a script
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

This is the same thing in PowerShell:

wget http://www.example.org/my.script -O my.script.ps1
$env:script_source="http://www.example.org"
$env:some_value="floob"
PowerShell -File ./my.script.ps1

So I wonder if somehow these two scripts can be merged and run successfully on either platform?

I've been trying to find a way to put them in the same script and get bash and PowerShell.exe to ignore errors but have had no success doing so.

Any guesses?

4
  • 2
    Have you read this? stackoverflow.com/questions/17510688/… Commented Sep 9, 2016 at 23:55
  • 1
    you may use PowerShell v6-alpha Commented Sep 10, 2016 at 0:10
  • 1
    Wow, if PowerShell v6 can do that, it would be impressive. With bash available the latest versions of Windows 10, might that solve your problem as well? Commented Sep 10, 2016 at 1:06
  • NB. that's not "the same thing in PowerShell" for wget because -O is ambiguous between -OutputFile, -OutputVariable and -OutputBuffer. (Unless you have installed wget.exe for Windows and removed the wget alias, but that's not default, so you should say if you have). Commented Sep 10, 2016 at 2:56

3 Answers 3

21

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
Sign up to request clarification or add additional context in comments.

2 Comments

Could you combine the two solutions by putting the eval statement into a function block so that only bash runs it?
@HarryJohnston I retried the second script and it didn't even work because I'd broken the quoting in eval. Yes, great idea to combine the two approaches - and that even fixes the reason I changed the quoting so it works even better - thanks. (PowerShell was trying to evaluate the "$(eval)" and complaining - but it doesn't do that when it's inside a function that doesn't get called).
18

While the other answer is great
(thank you TessellatingHeckler and Harry Johnston) we can do better

(and also thank you j-p-hutchins for fixing the error with true)

Yes. Not Just Possible, But Reliable, Minimal, Easy to Edit and Fast

A mere 41 chars, the rest is actual code.

Edit (several years later), 15 chars, we can do it in 15! I don't yet have the time to update the whole breakdown below (which is still valid) but if you really like code-golfing here's the shorter version:

#!/bin/sh
? "\"<#" 2>&-
   echo "Hi Shell"
exit #>
   echo "Hi Powershell"

(For this one, the key is that \" is not an escape in powershell, ? is a command on in powershell, and 2>&- turns off stderr rather than 2>/dev/null redirecting it)

TLDR: Copy Paste

Save this as your_thing.ps1 for it to run as powershell on Windows and run as shell on all other operating systems.

#!/usr/bin/env sh
echo --% >/dev/null;: ' | out-null
<#'


#
# sh part
#
echo "hello from bash/dash/zsh"
echo "do whatver you want just dont use #> directly"
echo "e.g. do #""> or something similar"
# end bash part

exit #>


#
# powershell part
#
echo "hello from powershell"
echo "you literally don't have to escape anything here"

How does it work? (its a simple idea)

  • We want to start a multi-line comment in powershell without causing an error in bash/shell.
  • Powershell has multi-line comments <# but as-is that syntax will cause problems in bash/shell languages. We need to use a string like "<#" for bash. New problem: we need it to NOT be a string in powershell.
  • Powershell has a really werid "stop-parsing" arg --% . That special arg lets us write a single quote without starting a string. E.g.
    echo --% ' blah ' will print out ' blah ' in powershell, but in bash it will print out --% blah
  • We need a command in order to use powershell's stop-parsing-args. Lucky for us both powershell and bash have an echo command
  • So, in bash we echo a string with <#, but powershell the same code finishes the echo command then starts a multi-line comment
  • Finally we add >/dev/null to bash so that it doesn't print out --% every time, and we add | out-null so that powershell doesn't print out >/dev/null;: ' every time.
  • Unlike powershell, bash doesn't parse whole files, it only parses one line at a time as-needed. If we put an exit command before ending the powershell comment, bash will never read past it.

The syntax highlighting tells the story more visually

Powershell Highlighting

All the green stuff is ignored by powershell (comments)
The gray --% is special
The | out-null is special
The white parts are just string-arguments without quotes
(even the single quote is equivlent to "'")
The <# is the start of a multi-line comment

powershell highlighting

Bash Highlighting

For bash its totally different.
Lime green + underline are the commands.
The --% isn't special, its just an argument
But the ; is special
The purple is output-redirection
Then : is just the standard "do nothing" shell command
Then the ' starts a string argument that ends on the next line

bash highlighting

Caveats?

Almost none. Powershell legitimately has no downside. The Bash caveats are easy to fix, and are exceedingly rare. Even better, its not just bash, its POSIX. It works with almost all linux shells.

  1. If you need #> in a bash string, you'll need to escape it somehow.
    changing "#>" to "#"">"
    or from ' blah #> ' to ' blah #''> '.
  2. If you have a comment #> and for some reason you CANNOT change that comment (this is what I mean by exceedingly rare), you can actually just use #>, you just have to add re-add those first two lines (eg true --% etc) right after your #> comment
  3. One even more exceedingly rare case is where you are using the # to remove parts of a string (I bet most don't even know this is a bash feature). Example code below https://man7.org/linux/man-pages/man1/bash.1.html#EXPANSION
var1=">blah"
echo ${var1#>}
# ^ removes the > from var1

To fix this one, well there are alternative ways of removeing chars from the begining of a string, use them instead.

Comments

3

Following up on Jeff Hykin's answer, I have found that the first line, while it is happy in bash, produces this output in PowerShell. Note that it is still fully functional, just noisy.

true : The term 'true' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling    
of the name, or if a path was included, verify that the path is correct and try again.
At C:\Users\jp\scratch\envar.ps1:4 char:1
+ true --% ; : '
+ ~~~~
    + CategoryInfo          : ObjectNotFound: (true:String) [], CommandNotFoundException
 
hello from powershell

I am experimenting with changing the first lines from:

true --% ; : '
<#'

to:

echo --% > /dev/null ; : ' | out-null
<#'

In very limited testing this seems to be working in bash and powershell. For reference, I am "sourcing" the scripts not "calling" them, e.g. . env.ps1 in bash and . ./env.ps1 in powershell.

Comments

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.