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 theparam(...)
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.