PowerShell core is my preferred scripting language. I've been excited about it since its early days. Here's a tweet from back in 2016 when PowerShell core was still in beta:
Running #PowerShell on #bash on #Ubuntu on #Windows10 . Just because I can :) pic.twitter.com/VlBppczZ6i
— Hesham A. Amin (@HeshamAmin) August 19, 2016
I've used PowerShell to automate build steps, deployments, and other tasks on both dev environments and CICD pipelines. It's great to write a script on my Windows machine, test it using PowerShell core, and run it on my docker Linux-based build environments with 100% compatibility. Or so I thought until I learned otherwise!
A few years ago, I was automating a process which required creating a folder if it didn't exist. Out of laziness, this is how I implemented this functionality:
mkdir $folder -f
When the folder exists and the -f (or --Force) flag is passed, the command will return the existing directory object without errors. I know this is not the cleanest way -more on this later- but it works on my Windows machine, so it should also work in the docker Linux container, except that it didn't. When the script ran, it resulted in this error:
/bin/mkdir: invalid option -- 'f'
Try '/bin/mkdir --help' for more information.
Why did the behavior differ? It turns out that mkdir means different things depending on whether you're running PowerShell on Windows or Linux. And this can be observed using Get-Command Cmdlet:
# Windows:
Get-Command mkdir
The output is:
CommandType Name Version
----------- ---- -------
Function mkdir
Under Windows, mkdir is a function, and the definition of this function can be obtained using
(Get-Command mkdir).Definition
And the output is:
<#
.FORWARDHELPTARGETNAME New-Item
.FORWARDHELPCATEGORY Cmdlet
#>
[CmdletBinding(DefaultParameterSetName='pathSet',
SupportsShouldProcess=$true,
SupportsTransactions=$true,
ConfirmImpact='Medium')]
[OutputType([System.IO.DirectoryInfo])]
param(
[Parameter(ParameterSetName='nameSet', Position=0, ValueFromPipelineByPropertyName=$true)]
[Parameter(ParameterSetName='pathSet', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
[System.String[]]
${Path},
[Parameter(ParameterSetName='nameSet', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[AllowNull()]
[AllowEmptyString()]
[System.String]
${Name},
[Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[System.Object]
${Value},
[Switch]
${Force},
[Parameter(ValueFromPipelineByPropertyName=$true)]
[System.Management.Automation.PSCredential]
${Credential}
)
begin {
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('New-Item', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd -Type Directory @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline()
$steppablePipeline.Begin($PSCmdlet)
}
process {
$steppablePipeline.Process($_)
}
end {
$steppablePipeline.End()
}
Which as you can see, wraps the New-Item Cmdlet. However under Linux, it's a different story:
# Linux:
Get-Command mkdir
Output:
CommandType Name Version
----------- ---- -------
Application mkdir 0.0.0.0
It's an application, and the source of this applications can be retrieved as:
(Get-Command mkdir).Source
/bin/mkdir
Now that I know the problem, the solution is easy:
New-Item -ItemType Directory $folder -Force
It's generally recommended to use Cmdlets instead of aliases or any kind of shortcuts to improve readability and portability. Unfortunately PSScriptAnalyzer - which integrates well with VSCode- will highlight this issue in scripts but only for aliases (like ls) and not for functions. AvoidUsingCmdletAliases.
I learned my lesson. However, I did it the hard way.