Variable Expansion vs. Property Expressions
PowerShell parser interprets strings, Variable Expansion, and Property Expressions in strings differently. This difference changes how the Argument List is generated and passed on to the executable.
That is why generating argument lists:
# By Object Property Access
docker --tag $imageInfo.FullImageName`:$version
# -- Doesn't work --
# By Variable Expansion
docker --tag $imageInfo`:$version
# -- Works --
# By Sub Expression Operator
docker --tag $($imageInfo.FullImageName)`:$version
# -- Doesn't work --
# By Double quoted string + Sub Expression Operator
docker --tag "$($imageInfo.FullImageName):$version"
# -- Works --
Level 0: TLDR
You have to use a combination of the Sub Expression Operator $() to access the object property, and enclose the entire string in double quotes for it to be interpreted correctly. This also removes the need to escape the back tick. So the full solution is:
docker image build --tag "$($imageInfo.FullImageName):$version" --file .\Dockerfile .
Level 1: Interpreting Arguments
PowerShell runs commands through it's own syntax parser first before passing the arguments on to external programs e.g. so it can perform variable substitution etc. At a high level we can see how PowerShell interprets the command and what arguments are being passed by running the tool EchoArgs.exe and echo what arguments are being passed (I have simplified the examples to illustrate what is going on):
#Setup Variables for MCVE:
$imageInfo = New-Object -TypeName psobject
$imageInfo | Add-Member -MemberType NoteProperty -Name FullImageName -Value "ImageName"
$strImageName = "ImageName"
$version = "v1.2.3"
By Object Property Access
PS C:\> EchoArgs.exe docker --tag $imageInfo.FullImageName`:$version
Arg 0 is <docker>
Arg 1 is <--tag>
Arg 2 is <ImageName>
Arg 3 is <:v1.2.3>
Command line:
"C:\EchoArgs.exe" docker --tag ImageName :v1.2.3
Here we can see $imageInfo.FullImageName was evaluated and being passed as one argument (Arg 2) and notice that both the colon and version string are being combined and is being interpreted as a new argument (Arg 3). This then causes the final resulting command line that would be executed to have a space between the image name and version: ImageName :v1.2.3
This will obviously confuse docker and any other similar executable.
By Variable Expansion
PS C:\> EchoArgs.exe docker --tag $strImageName`:$version
Arg 0 is <docker>
Arg 1 is <--tag>
Arg 2 is <ImageName:v1.2.3>
Command line:
"C:\EchoArgs.exe" docker --tag ImageName:v1.2.3
Using a string variable, we can see that the arguments are being interpreted correctly. It expands the string variables and correctly keeps everything together as one argument (Arg 2).
By Sub Expression Operator
PS C:\> EchoArgs.exe docker --tag $($imageInfo.FullImageName)`:$version
Arg 0 is <docker>
Arg 1 is <--tag>
Arg 2 is <ImageName>
Arg 3 is <:v1.2.3>
Command line:
"C:\EchoArgs.exe" docker --tag ImageName :v1.2.3
Similar to the first example, we can see $($imageInfo.FullImageName) was evaluated and being passed as one argument (Arg 2) and that the colon and version string being interpreted as a new argument (Arg 3). This doesn't work.
By Double quoted string + Sub Expression Operator
PS C:\> EchoArgs.exe docker --tag "$($imageInfo.FullImageName):$version"
Arg 0 is <docker>
Arg 1 is <--tag>
Arg 2 is <ImageName:v1.2.3>
Command line:
"C:\EchoArgs.exe" docker --tag ImageName:v1.2.3
In this case, the double quotes contains the entire string. The Sub Expression $($imageInfo.FullImageName) gets evaluated first. Because the colon and $version strings are also contained within the same double quotes, the entire string has to be completely evaluated before being returned as a single argument (Arg 2).
Level 2: Abstract Syntax Tree
I thought $imageInfo.FullImageName and $strImageName
are both strings, so why are they treated differently?
We can use EchoArgs.exe to see that these are being treated differently, but to effectively answer this question, we need to understand how PowerShell parses the command. After all, if we know it's a PowerShell parser issue, how is the parser interpreting what we are doing?
Don't try to parse PowerShell code in PowerShell
Use the PowerShell parser instead!
Let's use PowerShell's own Abstract Syntax Tree to interpret what's going on!
By Object Property Access
$ScriptAST = [System.Management.Automation.Language.Parser]::ParseInput('docker --tag $imageInfo.FullImageName`:$version', [ref]$null, [ref]$null)
$CommandAST = $ScriptAST.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)
$CommandAST.CommandElements
StringConstantType : BareWord
Value : docker
StaticType : System.String
Extent : docker
Parent : docker --tag $imageInfo.FullImageName`:$version
StringConstantType : BareWord
Value : --tag
StaticType : System.String
Extent : --tag
Parent : docker --tag $imageInfo.FullImageName`:$version
Expression : $imageInfo
Member : FullImageName
Static : False
StaticType : System.Object
Extent : $imageInfo.FullImageName
Parent : docker --tag $imageInfo.FullImageName`:$version
Value : :$version
StringConstantType : BareWord
NestedExpressions : {$version}
StaticType : System.String
Extent : `:$version
Parent : docker --tag $imageInfo.FullImageName`:$version
Here we see that docker and --tag is being interpreted as StringConstantType : BareWord tokens. $imageInfo.FullImageName is being interpreted as Expression : $imageInfo, which needs that it needs to be evaluated. And finally, :$version is being interpreted as StringConstantType : BareWord as well. This directly corresponds to our 4 Arguments that we saw earlier. So, what about a plain variable?
$ScriptAST = [System.Management.Automation.Language.Parser]::ParseInput('docker --tag $strImageName`:$version', [ref]$null, [ref]$null)
$CommandAST = $ScriptAST.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)
$CommandAST.CommandElements
StringConstantType : BareWord
Value : docker
StaticType : System.String
Extent : docker
Parent : docker --tag $strImageName`:$version
StringConstantType : BareWord
Value : --tag
StaticType : System.String
Extent : --tag
Parent : docker --tag $strImageName`:$version
Value : $strImageName:$version
StringConstantType : BareWord
NestedExpressions : {$strImageName, $version}
StaticType : System.String
Extent : $strImageName`:$version
Parent : docker --tag $strImageName`:$version
Well that's different. It sees $strImageName:$version as a single StringConstantType : BareWord token type. PowerShell understands how to handle simple variable string expansions and still return a single string object. i.e. all variable strings are StaticType : System.String which can all be combined into one vs. the previous example where we had both a StaticType : System.Object and a System.String which, although in the next step turn into strings, they are different types here, and therefore won't be combined.
By Sub Expression Operator
$ScriptAST = [System.Management.Automation.Language.Parser]::ParseInput('docker --tag $($imageInfo.FullImageName)`:$version', [ref]$null, [ref]$null)
$CommandAST = $ScriptAST.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)
$CommandAST.CommandElements
StringConstantType : BareWord
Value : docker
StaticType : System.String
Extent : docker
Parent : docker --tag $($imageInfo.FullImageName)`:$version
StringConstantType : BareWord
Value : --tag
StaticType : System.String
Extent : --tag
Parent : docker --tag $($imageInfo.FullImageName)`:$version
SubExpression : $imageInfo.FullImageName
StaticType : System.Object
Extent : $($imageInfo.FullImageName)
Parent : docker --tag $($imageInfo.FullImageName)`:$version
Value : :$version
StringConstantType : BareWord
NestedExpressions : {$version}
StaticType : System.String
Extent : `:$version
Parent : docker --tag $($imageInfo.FullImageName)`:$version
SubExpression : $imageInfo.FullImageName is being interpreted as a StaticType : System.Object which, can't be combined with System.String.
By Double quoted string + Sub Expression Operator
$ScriptAST = [System.Management.Automation.Language.Parser]::ParseInput('docker --tag "$($imageInfo.FullImageName):$version"', [ref]$null, [ref]$null)
$CommandAST = $ScriptAST.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)
$CommandAST.CommandElements
StringConstantType : BareWord
Value : docker
StaticType : System.String
Extent : docker
Parent : docker --tag "$($imageInfo.FullImageName):$version"
StringConstantType : BareWord
Value : --tag
StaticType : System.String
Extent : --tag
Parent : docker --tag "$($imageInfo.FullImageName):$version"
Value : $($imageInfo.FullImageName):$version
StringConstantType : DoubleQuoted
NestedExpressions : {$($imageInfo.FullImageName), $version}
StaticType : System.String
Extent : "$($imageInfo.FullImageName):$version"
Parent : docker --tag "$($imageInfo.FullImageName):$version"
Here we see the difference. "$($imageInfo.FullImageName):$version" is being interpreted as a single StringConstantType : DoubleQuoted string token. It does have nested expressions, but it is being parsed as a single token, and hence will be returned as a single argument.