Objects are at the heart of PowerShell and yet there is little or no native support for object literals and no elegant/terse way to add a property or query if an object has a property.
# Make a new object
$o = New-Object PSObject # Ugly
# Check a non-existant property
$o.other # Will crash in Strict Mode
# Add a property
$o | Add-Member "foo" "bar" # Ugly
# Make a new object
$o = {
foo: bar
};
# Check a non-existant property
$o.other # won't crash, just returns null
$o.other -eq $null # true
# Add a property
$o.something = "new" # won't crash, adds member
Generally we work with custom functions to do the lifting like checking if an object has a property:
function HasProp($object, $property, $default) {
if ($object.PSObject.Properties.Item($property)) {
return $object."$property"
} else {
return $default
}
}
If PowerShell is to become widely used it needs to up it's game in this regard. Native JSON support is one of the winning factors of JavaScript IMHO.
Most of these already exist, actually.
# Make new object
$Object = [PSCustomObject]@{
Foo = "Bar"
}
$Object.NonexistentProperty # returns $null
$Object.NonexistentProperty -eq $null # -> true
The only thing "missing" is adding nonexistent properties. But in a lot of cases, that would be detrimental if it was too easy. You _can_ do it with Add-Member
, and you can also do it with $Object.PSObject.Properties.Add()
However, in a lot of cases if you want to play fast and loose with object "properties" like that you more often need a _hashtable_ or other dictionary type:
# Create hashtable
$Table = @{ Foo = "Bar" }
$Table.What # -> $null
$Table.What = 12
$Table.What # -> 12
# Remove key
$Table.Remove("What")
Objects in most languages are _very_ much a fixed item, with properties you can't change after compilation, and PS largely follows this object model. If you want a more mutable collection of named values, a dictionary or hashtable is generally much more effective, fast, and appropriate to the purpose. 馃檪
N.B.: You'll notice creating a custom object is very similar to creating a hashtable -- it _is_ very similar, just transmuting the hashtable to a "property bag" to improve display formatting and ease of handling with cmdlets.
For a more in-depth dive into these features of PS, and a lot of others that might be of interest to you, I'd recommend checking out PSKoans.
There is one caveat here also, which is that accessing nonexistent properties is _disabled by design_ when working in Strict Mode.
Thanks for your feedback - I learned a thing or two and I do use HashTables extensively but we're still not terse/elegant enough with Objects proper.
Look at how easy it is to create a HashTable in PowerShell - why can't the ubiquitous PSObject be as easy? Just using colons :
instead of =
would delineate that this is a PSObject.
As you know, in Strict mode $obj.NonExistantProperty
is not null but throws an error. Should people therefore avoid Strict mode so they can easily query an object's property existence?
If you work with JavaScript much you'll see the beauty of "fast and loose" with properties. As an example: it's common for a function to take an object as a parameter (instead of lots of parameters). This makes the function extensible later without changing it's signature. However, as the developer you have no control over how that object is passed and if users know all the parameters. With JS you can easily default the parameters sensibly as follows:
function DoSomething(opt) {
opt.p1 = opt.p1 || "Hello";
opt.p2 = opt.p1 || "world";
return opt.p1 + ' ' + opt.p2;
}
DoSomething({p1: "Hello"}); // "Hello world"
So if someone using your function doesn't pass in an object with p1 or p2 it won't crash but will use sensible defaults - a robust function.
In PowerShell Strict today:
function DoSomething($opt) {
if ($opt.PSObject.Properties.Item("p1") -eq $null) {$opt | Add-Member "p1" "Hello"
if ($opt.PSObject.Properties.Item("p2") -eq $null) {$opt | Add-Member "p1" "world"
return $opt.p1 + ' ' + $opt.p2;
}
$opt = New-Object PSObject
DoSomething $opt
We can do better.
@cawoodm you can have default parameter values in PowerShell, e.g. your code becomes:
function DoSomething {
Param(
$P1="Hello",
$P2="World"
)
$P1 + ' ' + $P2
}
Then you can call it with P1, P2, both, or neither. If you call it with one, then it takes the value you passed in, otherwise it uses the default value from the Param() block.
PS> DoSomething
Hello World
PS> DoSomething -P2 "Everyone"
Hello Everyone
And if you have a hashtable, you can splat it into the function and its keys match to the parameter names:
PS> $someHashtable = @{ P1 = 'Greetings' }
PS> DoSomething @someHashtable
Greetings World
if the hashtable has a key P1
that will be used for the parameter P1
and if it doesn't have that, then the default value from the param() block will be used instead. So you can auto-generate code which calls functions, without having to write a long DoSomething -P1 foo -P2 blah
string anywhere, and treat it a bit as-if it took one object.
This makes the function extensible later without changing it's signature. However, as the developer you have no control over how that object is passed and if users know all the parameters. With JS you can easily default the parameters sensibly as follows:
In PS you can change the function signature to have a P3
without breaking previous callers, everything from before still works, and new code which wants to use P3
also works.
You can check whether something was passed as a parameter (vs using a default value) by checking if ($PSBoundParameters.ContainsKey('P3')) {..}
, or you can use ParameterSets so that passing P1, P2
does the original thing, and passing P1, P2, P3
changes the way all of them behave (some design and care needed - it's not magic, but it is flexible).
If you go with the approach of using one object as the entire parameter interface, it would break Powershell's autocompletion of parameters on the shell prompt - no hints what parameters are available, no tab completion of them, no auto-completion of values for them, no implicit type-casting (e.g. int to string) for them, no autogenerated help, it would make pipeline input difficult because there's only one parameter - you couldn't feed some data in through the pipeline and specify other parameters on the command line very easily, e.g. write this code and in ISE autocompletion you get these options:
The available parameters are shown, and their types:
The acceptable values for P2 are picked out of the code, and offered to the user:
help DoSomething
shows autogenerated text in the same way:
PS C:\> help DoSomething
NAME
DoSomething
SYNTAX
DoSomething [[-P1] <string>] [[-P2] {Everyone | World | People}]
If you hide the parameters in one opaque blobject, you lose all that introspection and programmer-shell-user-help.
I think that doesn't address "ease of adding things to an object", but it does seem to cover the use cases you have for doing so, and that PS approaches some of these things in a different way because of its nature as an interactive shell as well as a programming language.
You can put almost anything into a Hashtable, even scriptblocks, but once you make a PSObject everything gets kind of fixed in place. It feels like you're saying "I don't want to keep everything in flexible Hashtables, I want to fix them into PSObject .. but objects are too fixed, why can't they be more flexible like hashtables?".
And if that's still not convincing enough, you can drop the check and change:
if ($opt.PSObject.Properties.Item("p1") -eq $null) {$opt | Add-Member "p1" "Hello" }
#to
$opt | Add-Member "p1" "Hello" -ErrorAction SilentlyContinue
#or
$opt | Add-Member "p1" "Hello" -EA 0
Everything you say is valid but at the end of the day I stare at this and wish I was coding JS:
PowerShell:
"description" = (&{if ($_.PSObject.Properties.Item("description") -and $_.description.length) {$_.description[0].text} else {""}})
JavaScript:
description: $_.description && $_.description.length ? $_.description[0].text : ""
There's no need for the double wrapper there. A bare if statement is valid in a key/value declaration:
$hash = @{
Description = if ($condition) { $expression } else { $other }
}
Javascript:
description: $_.description && $_.description.length ? $_.description[0].text : ""
PowerShell:
description = try { $_.description[0].text } catch { "" }
(You don't need ""
around the hashtable key names, or semi-colons on the ends)
description = try { $_.description[0].text } catch { "" }
I do this in fact - I assume it's muuuch slower than
?:
(at scale) because exception objects have to be generated instead of ajne
.
Yeah, I'd be inclined to avoid try/catch
for this when possible.
There are some open issues suggesting null-coalescing operators and similar, which are basically up for grabs for anyone wanting to try to implement them.
I do this in fact - I assume it's muuuch slower than ?: (at scale) because exception objects have to be generated instead of a jne.
I imagine it will be. If you are optimizing for speed, your use of +=
to add your new object to an array is a particularly slow operation, especially if it's inside a loop. Try an approach like:
$list = foreach ($u in $users) {
[pscustomobject]@{
..
}
}
or
$List = [System.Collections.Generic.List[pscustomobject]]::new()
$thing = [pscustomobject]@{...}
$List.Add($thing)
to work around that and get a lot of speedup. Much of this kind of thing is PowerShell being aimed at being a convenient admin scripting language, rather than a fast programming language, although it tries to balance both, but if you want speed in PS, you often have to write more code to get it.
It's frustrating me that I can't find a cleaner member-test than you have, which is also error-free in strictmode, except:
description = if ($_ | get-member -name description) { $_.description[0].Text }
# or
description = if (get-member -InputObject $_ -name description) { $_.description[0].Text }
but if you don't want the overhead of exception handling, you won't want the overhead of a pipeline and cmdlet invocation either.
Generally we work with custom functions to do the lifting like checking if an object has a property:
function HasProp($object, $property, $default) {
But this code is basically the same overhead as the Get-Member; if performance is your concern, you might reconsider using hasprop
as well. Because of all the parameter binding stuff described in my previous comment up there ^, calling functions has a high overhead in PowerShell.
Which is normally fine, because PS is a convenience shell scripting language, before it's a performance language, but if performance is a concern with the exception handling, a back of the envelope test shows me that I can call hasprop
1000 times in 200ms, or try{}catch{} 1000 times with no exceptions in 5 ms. It rises to 250ms for 1000 exceptions, so if description
is present more than 50% of the time, try/catch is probably faster.
If PS had ?.
as an operator, as requested in issue #3240, that's likely the way forward.. but it's bothering me that the original thing you requested $x.thing
to just return $null is exactly what PowerShell does, but then you ask PowerShell to enable strict mode, and are then unhappy that strictness need more code - when what you want to do is to casually ignore the $null and not be strict about it. Is it that you want all the checks StrictMode does - except this one? Or you don't want to code for StrictMode but still want your code to work in StrictMode so that it will work if someone else enables it? Or that you want StrictMode code to be shorter? (Which is a fine request, but it does make me wonder why StrictMode enforces this).
Look at how easy it is to create a HashTable in PowerShell - why can't the ubiquitous PSObject be as easy?
I personally agree that [pscustomobject] @{ ... }
is a lot of ceremony, but I don't know what the right alternative would be; substituting :
for =
seems a little obscure; a different sigil (in lieu of @
) might be better, but introducing an entirely new char. would be very problematic; something _starting_ with @
that currently is a syntax error may be an option;
note that something like @@
is being considered in the RFC about generalized (variable-free) splatting
Re member test without breaking in strict mode:
The best-performing - yet somewhat obscure and verbose - approach at the moment is:
Set-StrictMode -Version Latest
$o = [pscustomobject] @{ one = 1; two = 2; three = 3 }
[bool] $o.psobject.properties['one'] # -> $true
[bool] $o.psobject.properties['nosuch'] # -> $false
Note that there's also .psobject.methods
and the member type-agnostic .psobject.members
.
If we implemented the aforementioned #3240 as well as #3239 (ternary conditionals):
# Still WISHFUL THINKING:
# This JS code:
# description: $_.description && $_.description.length ? $_.description[0].text : ""
# could be implemented as:
@ {
description = $_?.description -and $_?.description.Length ? $_.description[0].text : ''
}
What #3240 doesn't cover is null-conditional _index_ access - something that C# does _not_ offer, by the way - which would enable safe access to _nonexistent array elements_ in strict mode.
If we had a syntax for that - and I'm struggling to come up with the right one - the above could be more concise;
# WISHFUL THINKING, but awkward syntax
description = $_?.description[0?].text ?? ''
Note that placing ?
_after_ [...]
([...]?
) is not an option, because it introduces ambiguity. E.g., in $_?.description[0]?.text ?? ''
, does the ?
after [0]
allow use of index 0
even if there is no such element, or does it ignore an attempt to access non-existent property text
on the existing 0
element? A similar ambiguity would apply to ?[...]
Continuing my "PowerShell is Cool...but weird" series - can anyone say if this is a bug, by design or me being stupid:
[CmdletBinding()]
param(
[string]$P1=$null
)
if ($P1) {Write-Host "Truthy"} else {Write-Host "Falsy"}
if ($null -eq $P1) {Write-Host "P1 is NULL"} else {Write-Host "P1 is NOT NULL"}
if ("" -eq $P1) {Write-Host "P1 is an EMPTY STRING"} else {Write-Host "P1 is NOT an EMPTY STRING"}
if ($P1.length -eq 0) {Write-Host "P1 has no length"} else {Write-Host "P1 has length $($P1.length)"}
Result:
P1 is NOT NULL
P1 is an EMPTY STRING
P1 has no length
Why is $P1 not null??
You cast to string. I think PS tries to convert that and comes back with an empty string, but I could be wrong.
Empty string and null are two different things, but when you apply the type cast PS enforces the conversion and makes sure it's a string value; for null, you get an empty string back, but not null itself.
I'm actually not casting, I'm declaring it to be a string. Can a string object not be null? I prefer to work with $null
for undefined/not set/not known instead of ""
.
Declaring a parameter to be string is the same in powershell as applying a cast. There's no difference.
Whatever value you try to set will either be converted to the specified type, or it will throw an error if it can't be converted. PS generally doesn't like to handle truly null strings, it doesn't consider that to be a valid string value, so it converts it.
In c#, string properties can be null, but PowerShell has some of its own ideas.
Is there a consistent way to query if a parameter has not been supplied? I assumed parameters not passed would always be either $null
or the default value specified. This seems not to be the case so I have to query if ($stringParam -ne "")
for strings and if ($intParam ne 0)
for ints etc...
Just to get back to my original post: I try to use strict mode wherever possible. For my particular case where I am querying objects (deserialized from JSON) which may or may not have properties I am interested in I have found that simply disabling strict mode for that code section or function gets me what I want.
It just seems a pity to mark my whole code block as "potentially crappy code, don't check it too strictly" when I really want clean code with just some foreign objects that I want to query without crashing. Hopefully there is no big performance drop switching strict on and off all the time.
The best way to go for an unambiguous and guaranteed check for if a parameter has been supplied is if ($PSBoundParamaters.ContainsKey('ParamName'))
馃檪
For background information on the inability store $null
in [string]
-typed variables, see this Stack Overflow answer.
Most helpful comment
The best way to go for an unambiguous and guaranteed check for if a parameter has been supplied is
if ($PSBoundParamaters.ContainsKey('ParamName'))
馃檪