Bash and PowerShell in a single script file

I’m not saying it’s all dotnet’s fault, but it was when deploying dotnetcore services to a linux VM that I thought, “what I really, really want is both bash and powershell setup scripts in a single file”. Surely a working incantation can be crafted from such arcane systems of quoting and escaping as the two languages offer?

½ an evening later :

# This file has a bash section followed by a powershell section,
# and a shared section at the end.
echo @'
' > /dev/null
# Bash Start --------------------------------------------------

scriptdir="`dirname "${BASH_SOURCE[0]}"`";
echo BASH. Script is running from $scriptdir

# Bash End ----------------------------------------------------
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
echo > /dev/null <<"out-null" ###
'@ | out-null
# Powershell Start --------------------------------------------

"powershell. Script is running from $scriptdir"

# Powershell End ----------------------------------------------
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

echo "Some lines work in both bash and powershell. Calculating scriptdir=$scriptdir, requires separate sections."

It relies on herestring quoting being different for each platform, as is the escape character ( \ vs ` ). Readibility (ha!) is very much helped by

#comments begin with a hash 

being common to both, so I can do visible dividers between the sections.

My main goal was environment variable setup before launching dotnetcore services. Sadly the incompatible syntaxes for variables and environment:

#powershell syntax
#bash syntax
export variable2=value 

means very little shared code inside the file, but it really cut down errors a lot just by having them in the same file. Almost-a-single-source-of-truth turned out to be much more reliable than not-at-all a single source of truth.

Bash-then-powershell was simpler than Powershell-then-bash. My state-of-the art is powershell named and validated parameters, which allows tab-completion to work in powershell.

` # \
# PowerShell Param
# every line must end in #\ except last line must end in <#\
# And, you can't use backticks in this section        #\
param( [ValidateSet('A','B')]$tabCompletionWorksHere, #\
       [switch]$andHere                               #\
     )                                               <#\
#^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `


Raw: Powershell-or-bash-with-parameters .

Alternatively, do everything in powershell?

Of course, sensible people would do everything in a single scripting language. But it has been well-worth having the tools for both approaches. Especially for short bootstrap scripts.

For a powershell core everywhere approach, my main adaptation is the shebang header on all .ps1 files:

#! /usr/bin/env pwsh

which tells unix machines to what kind of script it is. Powershell itself ignores it as a comment. Finally, you must also chmod a+x *.ps1 to mark them as executable.

PowerShell String.Split() Off-by-Method-Overload Error

This seemed to me an error and I and was on the point of raising it as a bug on the Powershell github repo:

PS> "\this".Split( [char]'\', [StringSplitOptions]::RemoveEmptyEntries).Length
# >> 2

Presumably it is because [StringSplitOptions]::RemoveEmptyEntries is coerced to a [char] and so the line is parsed as:

PS> "\this".Split( ([char]'\', [StringSplitOptions]::RemoveEmptyEntries) ).Length

Instead of as

PS> \this".Split( (,[char]'\'), [StringSplitOptions]::RemoveEmptyEntries).Length

If the first parameter is a string not a character then it works as expected:

PS> "\this".Split( '\', [StringSplitOptions]::RemoveEmptyEntries).Length
# >> 1

But the really unfortunate case is :

PS> "\this".Split( [System.IO.Path]::DirectorySeparatorChar, [StringSplitOptions]::RemoveEmptyEntries).Length
# >> 2

which results in

PS> "\this".Split( [System.IO.Path]::DirectorySeparatorChar, [StringSplitOptions]::RemoveEmptyEntries).[0]
# >> $null
# instead of
# >> "this"

It turns out that it’s fixed in Powershell 6 Beta; or to be more precise, it doesn’t happen in PowerShell 6. What changed is that the underlying .Net framework has added new overloads to String.Split():

string[] Split(char separator, System.StringSplitOptions options)                                                                                    
string[] Split(char separator, int count, System.StringSplitOptions options)                                                                         
string[] Split(string separator, System.StringSplitOptions options)                                                                                  
string[] Split(string separator, int count, System.StringSplitOptions options)                                                                       

Whereas PowerShell 5 only has these overloads available:

string[] Split(Params char[] separator)                                                                                                              
string[] Split(char[] separator, int count)                                                                                                          
string[] Split(char[] separator, System.StringSplitOptions options)                                                                                  
string[] Split(char[] separator, int count, System.StringSplitOptions options)                                                                       
string[] Split(string[] separator, System.StringSplitOptions options)                                                                                
string[] Split(string[] separator, int count, System.StringSplitOptions options)                                                                     

And so the best-match overload that PowerShell 6 chooses is different to PowerShell 5’s best match.