Home > database >  Powershell console different to script
Powershell console different to script

Time:09-23

Can anyone tell me why this command works fine in the Powershell console, returning a single thumbprint, but when run as a script it just returns all the certificate's thumbprints:

$crt = (Get-ChildItem -Path Cert:\LocalMachine\WebHosting\ | Where-Object {$_.Subject.Contains($certcn)}).thumbprint

$certcn is a string containing a domain. eg "www.test.com"

CodePudding user response:

I figured it out. $certcn was derived from $args[0]. It turns out $args[0] is not a string, and even though PS would quite happily use it as a string in other commands, it would not do this with Where-Object. Not sure what type $args[0] actually is, but doing $certcn = $args[0].tostring() fixed it.

CodePudding user response:

The only explanation for your symptom is that the value of variable $certcn:

  • either: is the empty string ('') because 'someString'.Contains('') returns $true for any input string.

  • or: is implicitly converted to the empty string, though that wouldn't happen often in practice; here are some examples (see GitHub issue # for the [pscustomobject] stringification bug mentioned below):

    # An empty array stringifies to ''
    'someString'.Contains(@()) # -> $true 
    
    # A single-element array containing $null stringifies to ''
    'someString'.Contains(@($null)) # -> $true 
    
    # Due to a longstanding bug, [pscustomobject] instances, when
    # stringified via .ToString(), convert to the empty string. 
    # This makes the command equivalent to `.Contains(@(''))`, which is again
    # the same as `.Contains('')`
    'someString'.Contains(@([pscustomobject] @{ foo=1 })) # -> $true 
    

$args[0] is not a string

The automatic $args variable is an array that contains all positional arguments that weren't bound to declared parameters, if any.

$args can contain elements of any data type, and what that type is is solely determined by the caller.

However, if you formally declare a parameter, you can type it, which means that if the caller passes an argument of a different data type, an attempt is made to convert the argument to the parameter's type (which may fail, but at least the failure will be "loud", and the reason obvious).


A robust solution for your script:

param(
  [Parameter(Mandatory)] # Ensure that the caller passes a value.
  [string] $CertCN       # Type-constrain to a string.
  # , ... declare other parameters as needed
)

# $CertCN is now guaranteed to be a *string* that is *non-empty*.
$crt = 
  (Get-ChildItem -Path Cert:\LocalMachine\WebHosting | 
     Where-Object { $_.Subject.Contains($CertCN) }).thumbprint

Note:

  • The use of the [Parameter()] attribute in the parameter declaration block (param(...)) makes your script an advanced one, which means that $args isn't supported, requiring all arguments to bind to explicitly declared parameters; however, you can define a catch-all parameter with [Parameter(ValueFromRemaningArguments)], if needed. (The other thing that makes a script or function an advanced one is use of the [CmdletBinding()] attribute above the param(...) block as a whole.)

  • [Parameter(Mandatory)], in addition to ensuring that the caller passes a value for the parameter, implicitly also prevents passing the empty string (or $null) - though you could explicitly allow that with [AllowEmptyString()]

  • Additionally, advanced scripts and functions automatically prevent passing arrays to [string]-typed parameters, which is desirable. (By contrast, simple functions and scripts simply stringify arrays, as would happen in expandable strings (string interpolation); e.g., & { param([string] $foo) $foo } 1, 2 binds '1 2', which is also what you'd get with "$(1, 2)")


Caveat:

When passing a value to a [string]-typed parameter, PowerShell accepts a scalar (non-collection) of any type, and any non-string type is automatically converted to a string, via .ToString(). This is usually desirable and convenient, but can result in useless stringifications; e.g.:

& { param([string] $str) $str } @{} # -> 'System.Collections.Hashtable'

Instances of hashtables (@{ ... }) stringify to their type name, which is unhelpful, and this behavior is the default behavior for any type that doesn't explicitly implement a meaningful string representation by overriding the .ToString() method.

If that is a concern, you can modify your script to ensure that the argument value being passed already is a string, using a [ValidateScript()] attribute.

param(
  [Parameter(Mandatory)]
  # Ensure that the argument value is a [string] to begin with.
  # Note: The `ErrorMessage` property requires PowerShell (Core) 7 
  [ValidateScript({ $_ -is [string] }, ErrorMessage='Please pass a *string* argument.')]
  $CertCN  # Do not type-constrain, so that the original type can be inspected.
  # , ... declare other parameters as needed
)

# ...

As stated in the code comments, use of the ErrorMessage property in the requires PowerShell (Core) 7 , unfortunately. In Windows PowerShell a standard error message is invariably shown, which isn't user-friendly at all.

  • Related