You create a script, it works for some time, then out of the blue it starts crashing with "You cannot call a method on a null-valued expression" or "The property 'property name' cannot be found on this object. Verify that the property exists and can be set.". What does this mean?
This is a Powershell version of "null pointer exception". This exception arises every time you attempt to query a variable that appears to be null. To determine what variable is null and where, you need to read the stack trace and the line/symbol numbers of the line in question. An example:
You cannot call a method on a null-valued expression.
At E:\temp\testsest.ps1:35 char:12
+ If($Search.value() -contains $SearchString)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
Let's parse the error message. First, there is a wording that is in the title of this question. If you are about to ask a question with this wording, you will get a set of similar questions proposed by StackOverflow. But there is more in the error description. Second line shows script, line and character number of the first character of an expression that generates this exception. Here, a request is made to $Search.value() querying if it -contains $SearchString. The wavy underline separates the expression in full, although the proper way would be underlining only $Search.value(). Next, there is a CategoryInfo and FullyQualifiedErrorId, the latter saying "Invoke method on null", omitting "pointer" or "variable".
Now, let's debug the message. Here, the only method that's about to be called is value(), this means $Search is equal to null. Therefore, we need to get upwards from the line 35 of the script and find a place where a value is last assigned to the variable in question. This particular script had a query to Range.Find() which returns null if there's no match to the searched string. An excerpt:
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = $true
$ExcelWorkBook = $Excel.Workbooks.Open($ExcelPath)
$ExcelWorkSheet = $Excel.WorkSheets.item("$location")
$Range = $ExcelWorkSheet.Range("A1").EntireColumn
$Search = $Range.find($user) # <<< here we get null
If($Search.value() -contains $user)
So, we have found where do we receive a null.
Remedies vary, but all include checks against $null. In this case it's enough to check $Search for null, and return "Nothing found" if it is indeed null. It might be not as simple, there might be more structures that can be null, like in $a.b.c.someMethod() - here either $a, $a.b or $a.b.c is null, so you need to check all of the outcomes. There are also situations where a complex structure is returned, and is expected to have a value in a certain field, but the field is not populated, therefore trying to use that field's value will produce an exception.
The moral is: If you receive an exception speaking about "null-valued", you have not expected something to return null, and you have to add checks for null (or, in fact, any unexpected) values before attempting to use the result.
Related
I am new to PowerShell but I found I can use Substring to count to the right or left of a string within a variable. It appears though it is not supported for the output I am receiving. I am hoping someone can point me in the right direction. Thank you for any help.
Code to retrieve the computer name.
$compname = WmiObject -class Win32_ComputerSystem | Select-Object Name
$compname
$compname.Substring(9,0)
Here is the result and error:
Name
Computer-PC
Method invocation failed because [Selected.System.Management.ManagementObject] does not contain a method named 'Substring'.
At line:3 char:1
$compname.Substring(9,0)
+ CategoryInfo : InvalidOperation: (Substring:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
This error occurs because you're trying to use the Substring method on an object.
Take a look, if i do the same query that you did, it returns me an object with "Name" property:
And as the powershell error shows, you cannot call the substring method directly to an object. You must do it on a string, in this case, the property name. To solve you problem, you just need to call "Name" property in your query. Something like this:
$computerName = (Get-WmiObject Win32_ComputerSystem).Name
After that, you will be able to use "Substring" method because that query returns a string:
If any other problem occurs, i will be glad to help you :)
I have the following code:
$a = gci .\Areas -Recurse
($a[0].EnumerateFileSystemInfos()).Count
This is its output
PS C:\> ($a[0].EnumerateFileSystemInfos()).Count
1
1
1
1
1
1
Why? When I run gm -InputObject $a[0], I clearly see that a collection is returned.
EnumerateFileSystemInfos Method System.Collections.Generic.IEnumerable[System.IO.FileSystemInfo] EnumerateF...
Why is it evaluating .Count against each member of the collection rather than the collection itself? Also worth noting is that
($a[0].EnumerateFileSystemInfos()).Count()
returns an error:
Method invocation failed because [System.IO.FileInfo] does not contain a method named 'Count'.
At line:1 char:1
+ ($a[0].EnumerateFileSystemInfos()).Count()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Which is what I would expect if I were calling it against $a[0][0] but I'm not. What's going on and how can I retrieve the number of items in the collection?
EnumerateFileSystemInfos() returns an IEnumerable, more precisely, a System.IO.FileSystemEnumerableIterator'1, thus each query to it returns a single object. And when you're piping the output to Out-Default, that cmdlet checks if the IEnumerable has more data, if yes, queries it again. That's why you get a sequence of 1's, because each object behind an enumerable is a single object and not an array.
You should instead use GetFileSystemInfos() to get your proper count, it returns an array.
To get the number of items in the the collection$a:
$a.Count
I don't understand the need for added complexity, and a .NET/C# approach. Is there anything this provides that you require?
Can someone tell what I am doing wrong in the below I wrote:
function set-harden {
[CmdletBinding(DefaultParameterSetName='NormalHardening')]
param (
[Parameter(ParameterSetName='DoNotRemoveFromDomain')]
[Parameter(ParameterSetName='PermitHTTP' ,Mandatory=$True)]
[Parameter(ParameterSetName='PermitHTTPS' ,Mandatory=$True)]
[switch]$DONOTRemovefromdomain,
[Parameter(ParameterSetName='PermitHTTP')]
[Parameter(ParameterSetName='DoNotRemoveFromDomain')]
[switch]$Permithttp,
[Parameter(ParameterSetName='PermitHTTPS')]
[Parameter(ParameterSetName='DoNotRemoveFromDomain')]
[switch]$Permithttps,
[Parameter(ParameterSetName='NormalHardening')]
$NormalHardening
)}
If($NormalHardening -eq ""){
Write-Host "Excellent!"
}
All I want to do is to let the user select -DONOTRemovefromdomain or -Permithttp or even -Permithttps. There could be a variety of options the user has to choose from.
When I run this below I get an error:
PS C:\Temp> set-harden -DONOTRemovefromdomain -Permithttp
set-harden : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:1
+ set-harden -DONOTRemovefromdomain -Permithttp
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [set-harden], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,set-harden
Also, if I do not specify anything (so it should just go to the parameter NormalHardening) I get an nothing back:
PS C:\Temp> set-harden
PS C:\Temp>
You've specified two flags, DONOTRemovefromDomain and Permithttp that belong to two parameter sets, DoNotRemoveFromDomain and PermitHttp. The command parser has no way of knowing which parameter set you mean, so you get an error.
The reason you don't get an error when you don't specify anything is because you've set the default parameter set explicitly to NormalHardening. You've not set the Mandatory flag on the single parameter in this parameter set, and by default parameters are not mandatory so you're not seeing an error.
Instead of having all these parameter sets why not just have 2, one for the default and one for all the flags you want to set:
function set-harden {
[CmdletBinding(DefaultParameterSetName='NormalHardening')]
param (
[Parameter(ParameterSetName='Options')]
[switch]$DONOTRemovefromdomain,
[Parameter(ParameterSetName='Options')]
[switch]$Permithttp,
[Parameter(ParameterSetName='Options')]
[switch]$Permithttps,
[Parameter(ParameterSetName='NormalHardening')]
$NormalHardening
)}
If($PSCmdlet.ParameterSetName -eq "Options"){
Write-Host "Excellent!"
}
How, if the parameter set name is set to Options you can check and apply the flags. If it's set to NormalHarding then you know to use the $NormalHardening parameter.
Sean gave a good answer already about what's going on in your specific case, but I want to include some tips for troubleshooting parameter sets.
Get Help
Or more specifically, Get-Help. The parameter set syntax is automatically generated from the param block, so running Get-Help myFunction will show you how PowerShell is interpreting your parameter sets (how many, which parameters are mandatory or not in each set, etc.).
Trace the Call
If the sets look right but you're getting errors and aren't sure why, let PowerShell show you how it's binding parameters:
Trace-Command -Name ParameterBinding -Expression { Set-Harden -Permithttp } -PSHost
That can give you great insight on what's going on, and lead you to how you might fix that (or help you realize that you can't).
With the following 2 lines of code:
$meta = New-Object System.Management.Automation.CommandMetadata (Get-Command Get-Event)
$parametersInCmdlet = $meta.Parameters.GetEnumerator()
The $parametersInCmdlet variable is set as can be seen by referencing it.
$parametersInCmdlet
Key Value
--- -----
SourceIdentifier System.Management.Automation.ParameterMetadata
EventIdentifier System.Management.Automation.ParameterMetadata
When I reference it again immediately after that, it appears empty (and confirmed if piped to Get-Member).
$parametersInCmdlet | gm
gm : No object has been specified to the get-member cmdlet.
At line:1 char:23
+ $parametersInCmdlet | gm
+ ~~
+ CategoryInfo : CloseError: (:) [Get-Member], InvalidOperationException
+ FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand
There is nothing else [that should be] touching that variable in between those references. This occurs in the console and ISE for both PS 2.0 and 3.0 so that makes me think it is more user misunderstanding than a bug.
What would cause the value to be lost in this case?
The object returned by GetEnumerator() methods is pretty much always an IEnumerator. The job of an IEnumerator is to hand back elements of a collection, one at a time, until that collection is depleted. At that point, it is the correct behavior for the IEnumerator to return back nothing when asked for the next item.
Powershell unrolls the entire collection when you look at it the first time. Thus, by default, it is expected that you can't look at the collection again, since the IEnumerator has already been "spent."
The workaround is to call Reset() on the IEnumerator if you want it to start over. Assuming the IEnumerator is properly implemented, this will allow you to re-read the collection from the beginning again.
So, try calling $parametersInCmdlet.Reset() before using it again.
I'm studying powershell functions and its validate parameters. However I can't understand if they are really useful.
I make a simple example.
function get-choice(
[parameter(Mandatory=$true)][String][ValidateSet('Y','N')]$choice
)
{return $choice}
get-choice k
This function returns me this error:
get-choice : Impossibile convalidare l'argomento sul parametro 'choice'. L'argomento "k" non appartiene al set "Y,N" specificato dall'attributo ValidateSet. Fornire un argomento inclu
so nel set ed eseguire di nuovo il comando.
In riga:6 car:11
+ get-choice <<<< k
+ CategoryInfo : InvalidData: (:) [get-choice], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,get-choice
If I don't specify a valid set of values I can check them within my code:
function get-choice2(
[parameter(Mandatory=$true)][String]$choice
) {
if($choice -ne 'y' -and $choice -ne 'n') {
write-host "you must choose between yes and no"
return
}
return $choice
}
get-choice2 k
and I get a more friendly message:
you must choose between yes and no
First of all I'd like to know if it's possible to customize error message using validateset. Then I hope that someone could explain why I'd have to prefer first approach. Thanks in advance.
Some reasons why to use standard validation:
declarative code; much easier to read then the if statement
much shorter (4 lines of code compared to 1 line with only return statement)
the custom code can have some bugs
later in Vx of PowerShell there could be some custom validation messages (?) (just dreaming)
...
Check Better error messages for PowerShell ValidatePattern - post by #jaykul. You will see how you could customize the error message. It's a little bit developer oriented, but worth reading it.
The advantage to using parameter validation is that you don't have to do it yourself. That's a lot of boring, boilerplate code that no longer needs to be written and tested. A big win in my book, even though it results in less-friendly error messages.
You can write some help documentation for your function, so that user's can type help get-choice2, and see an explanation for what that parameter means:
function get-choice(
[parameter(Mandatory=$true)][String][ValidateSet('Y','N')]$choice
)
{
<#
.SYNOPSIS
Gets the user's choice.
.PARAMETER choice
The choice. Must be Y or N.
#>
return $choice
}
Run help about_comment_based_help for more details, or see the MSDN documentation.