Vaccano
Vaccano

Reputation: 82467

Powershell Object Fields in Commands

I have the following function:

function CreateImage
{
    param (
        [string] $version = $(throw "-version is required")
    )

    $imageInfo = GetImageParamatersFromPath     

    docker image build --tag $imageInfo.FullImageName`:$version --file .\Dockerfile .
}

When I call this it fails (the docker command complains about number of parameters).

But if I change it so that I don't access $imageInfo.FullImageName in the docker call, it works fine:

function CreateImage
{
    param (
        [string] $version = $(throw "-version is required")
    )

    $imageInfo = GetImageParamatersFromPath
    $imageInfo = $imageInfo.FullImageName       

    docker image build --tag $imageInfo`:$version --file .\Dockerfile .
}

So, it seems I am not accessing the FullImageName field correctly in my actual docker command.

What is the correct way to access a field in a PowerShell command?

Additional Info: This may have something to do with the colon before the version. If I change first function to have the following docker command:

docker image build --tag $imageInfo.FullImageName --file .\Dockerfile .

Then it works. (Though it uses :latest instead of my version (obviously)).

So it may be something with the way the two are interacting.

In light of that, It tried this:

docker image build --tag ${imageInfo}.{FullImageName}`:$version --file .\Dockerfile .

and this:

docker image build --tag ${imageInfo}.{FullImageName}:$version --file .\Dockerfile .

But neither worked.

Upvotes: 2

Views: 77

Answers (2)

HAL9256
HAL9256

Reputation: 13483

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.

Upvotes: 2

jbsmith
jbsmith

Reputation: 1666

Try --tag $($imageInfo.FullImageName)`:$version This should tell PowerShell to evaluate that expression before passing it along to docker.

Upvotes: 0

Related Questions