I'm finding it very hard to google the answer to what the difference is between these two way of executing method calls in powershell:
$member = "1.2.3.4:567" # IPaddress + port for demonstration
$vals1 = $member.Split(":") # typical .NET way of executing String.Split
$vals2 = $member.Split( (,":") ) # something else which ive seen in examples which I dont understand
In the above, both $vals1 and $vals2 appear to have the same result (an array with 2 elements). I would typically use the first way, but in examples (of using both Split and other method calls) I see the second used.
My question is what is the second one doing which is different, and is there any advantages to using it which I may not understand?
Edit: Please don't focus on the Split method - I'm not asking about overloads of Split!
The comma operator used as a unary is what you are seeing. It is a shorthand way to create an array. PowerShell will unroll array in pipelines which is usually desired and standard behavior. Where I see this commonly used is to mitigate that feature of PowerShell
What you would then do in some cases though you do not want PowerShell to unroll the complete array is through the comma unary operator in front of that array. Consider the difference in outputs
Using regular array notation
$array = 1,2,3
$array.Count
$array | ForEach-Object{$_.GetType().FullName}
3
System.Int32
System.Int32
System.Int32
Using the unary operator to create a jagged array
$commaArray = ,#(1,2,3)
$commaArray.Count
$commaArray | ForEach-Object{$_.GetType().FullName}
1
System.Object[]
In the second example the array gets passed as a whole. PowerShell still unrolled it from a single array with one element that was itself an array to that single array 1,2,3.
There are other cases for its use as well. I would more commonly see regular arrays declared statically with 1,2,3 or sometimes the #() is needed depending. Result is the same for both of those.
,";" is a trick/shorthand to create an array (try (,";").GetType()). Why would you need this? Well, let's try calling Split with a list of values directly:
"abc".Split('a','b')
Cannot convert argument "count", with value: "b", for "Split" to type
"System.Int32": "Cannot convert value "b" to type "System.Int32".
Error: "Input string was not in a correct format.""
Doesn't work because the parameters are passed separately, rather than as a char[]. So could we use the comma trick to fix this?
"abc".Split((,'a','b'))
Cannot convert argument "separator", with value: "System.Object[]",
for "Split" to type "System.Char[]": "Cannot convert the
"System.Object[]" value of type "System.Object[]" to type
"System.Char"."
No, because we still have a type mismatch. That's because this approach is too clever for its own good. A much more readable way to create an array is the #() operator:
"abc".Split(#('a', 'b'))
And this calls the desired overload of Split.
The Split method has multiple overloads. The second example will create an array of string which will be convertet to an char[] because its only one character in the double quotes. However if you want to split by two characters, the second example won't work thus I wouldn't use it.
However, the PowerShell way to split is using -split:
$vals1 = $member -split ':'
Related
i'm trying to make System.Memory[char] .
[System.Memory[char]]::Memory([char],0,10) * says it can't find System.Memory type .
Also tried *
[System.Memory`3+[[char],0,10]]#()
Solution: The issue seems to be the .NET version used by the Powershell .
It is the static pseudo method ::new(), introduced in PowerShell v5, that provides access to a type's constructors.[1]
# Initialize a [System.Memory[char]] instance with 10 NUL (0x0) chars,
# from a [char[]] array.
[System.Memory[char]]::new(
[char[]]::new(10)
)
Note: The two System.Memory`1 constructors both require a [char[]] array as an argument. The two additional arguments in the 3-parameter overload, start and length, must refer to a range of elements within that array.
The above simply creates a 10-element array to begin with (implicitly using NUL characters), obviating the need for the additional arguments.
If you wanted the input array to use a given character other than NUL, you could use something like , [char] 'x' * 10:
, [char] 'x' create a single-element array with char. 'x', which * 10 then replicates to return a 10-element array. Note that the array will be [object[]]-typed, not [char[]]-typed, but it still works.
Note:
[System.Memory[char]]#() does not work, because in order for PowerShell to translate this cast to a single-parameter constructor call, the operand must be a [char[]] array:
[System.Memory[char]] [char[]] #()
Fundamentally, the System.Memory`1 type is available only in .NET Core 2.1+ / .NET 5+.
The simplest way to check if the type is available in your PowerShell session is if [bool] $IsCoreClr returns $true - in other words: you need to be running PowerShell (Core) 7+, the modern, cross-platform, install-on-demand edition of PowerShell.
[1] In earlier PowerShell versions you need to use the New-Object cmdlet, which uses argument(-parsing) mode, as all cmdlets do. As such, its syntax doesn't map cleanly onto the expression-mode syntax that is familiar from method/constructor calls, especially with respect to passing a single argument that is an array, as in this case:
New-Object System.Memory[char] -ArgumentList (, (New-Object char[] 10))
Note the need to wrap the array constructed by , [char] 0) * 10 in another array, namely a transitory one that is needed to make New-Object treat the original array as a single argument for the target constructor.
Additionally, ::new() performs better, though that will often not matter. See this answer for details.
I'm trying to get a handle on documentation/evidence around when PowerShell does conversions, and when a user needs to be explicit about what they want. A somewhat related question, here, has a broken link that possibly explained scenarios that the type would be adjusted. There are plenty of instances of similar problems though (namely, comparing a string to an int) - for example when you get a number via Read-Host, you're actually getting a string.
Specifically though I've found that certain mechanisms seem to handle a string representation of the number fine - but I want to understand if that is truly because they're handling it as a number, or if the output is correct in appearance, but wrong under the hood. Here's a specific example though, so the question is about Measure-Object and how it handles calculating the Sum of a string property.
PS > $Example = Import-Csv .\Example.csv
PS > $Example
Procedure_Code : 123456789
CPT_Code : J3490
Modifier :
Service_Date : 02/01/2020
Revenue_Code : 259
Units : -100.00
Amount : 55.00
Procedure_Code : 123456789
CPT_Code : J3490
Modifier :
Service_Date : 02/02/2020
Revenue_Code : 259
Units : 100.00
Amount : 55.00
PS > [Math]::Sign($Example[0].Units)
-1
PS > $Example | Measure-Object Amount -Sum
Count : 2
Sum : 110
Property : Amount
PS > $Example | ForEach-Object { $Sum = $Sum + $_.Amount} ; Write-Host "$($Sum)"
55.0055.00 #Plus sign concatenates strings
PS > $Example | ForEach-Object { $Sum = $Sum + [int]$_.Amount} ; Write-Host "$($Sum)"
110 #Expected result if casting the string into an integer type before adding
So basically it seems like [Math]::Sign() works (even though it doesn't accept strings as far as I can tell), and Measure-Object is smarter about strings than a simple plus sign. But is Measure-Object casting my string into an [int]? If I use an example with decimals, the answer is more precise, so maybe it is a [single]?
What is Measure-Object doing to get the right answer, and when shouldn't I trust it?
Measure-Object is a special case, because it is that cmdlet's specific logic that performs automatic type conversion, as opposed to PowerShell's own, general automatic conversions that happen in the context of using operators such as +, passing arguments to commands, calling .NET methods such as [Math]::Sign(), and using values as conditionals.
As your own answer hints at, Measure-Object calls the same code that PowerShell's general automatic type conversions use; these are discussed below.
Automatic type conversions in PowerShell:
In general, PowerShell always attempts automatic type conversion - and a pitfall to those familiar with C# is that even an explicit cast (e.g. [double] '1.2') may not be honored if the target type is ultimately a different one.[1]
Supported conversions:
In addition to supporting .NET's type conversions, PowerShell implements a few built-in custom conversions and supports user-defined conversions by way of custom conversion classes and attribute classes; also, it implicitly tries to call single-parameter target-type constructors and - for string input - a target type's static ::Parse() method, if present; see this answer for details.
To-number conversion and culture-sensitivity when converting to and from strings:
Typically, PowerShell uses culture-invariant conversion, so that, for instance, only . is recognized as the decimal mark in "number strings" that represent floating-point numbers.
String-to-number conversion, including how specific numeric data types are chosen, is covered in this answer.
To-string conversion (including from numbers) is covered in this answer.
Automatic type conversions by context:
Operands (operator arguments):
Some operators, such as -replace and -match, operate on strings only, in which case the operand(s) are invariably converted to strings (which always succeeds):
42 -replace 2, '!' yields '4!': both 42 and 2 were implicitly converted to strings.
Others, such as - and \, operate on numbers only, in which case the operand(s) are invariably converted to numbers (which may fail):
'10' - '2' yields 8, an [int]
Yet others, such as + and *, can operate on either strings or numbers, and it is the type of the LHS that determines the operation type:
'10' + 2 yields '102', the concatenation of string '10' with the to-string conversion of number 2.
By contrast, 10 + '2' yields 12, the addition of the number 10 and the to-number conversion of string '2'.
See this answer for more information.
Command arguments:
If the target parameter has a specific type (other than [object] or [psobject]), PowerShell will attempt conversion.
Otherwise, for literal arguments, the type is inferred from the literal representation; e.g. 42 is passed as an [int], and '42' as a [string].
Values serving as conditionals, such as in if statements:
A conditional must by definition be a Boolean (type [bool]), and PowerShell automatically converts a value of any type to [bool], using the rules described in the bottom section of this answer.
Method arguments:
Automatic type conversion for .NET-method arguments can be tricky, because multiple overloads may have to be considered, and - if the argument's aren't of the expected type - PowerShell has to find the best overload based on supported type conversions - and it may not be obvious which one is chosen.
Executing [Math]::Sign (calling this as-is, without ()) reveals 8(!) different overloads, for various numeric types.
More insidiously, the introduction of additional .NET-method overloads in future .NET versions can break existing PowerShell code, if a new overload then happens to be the best match in a given invocation.
A high-profile example is the [string] type's Split() method - see the bottom section of this answer.
Therefore, for long-term code stability:
Avoid .NET methods in favor of PowerShell-native commands, if possible.
Otherwise, if type conversion is necessary, use casts (e.g. [Math]::Sign([int] '-42')) to guide method-overload resolution to avoid ambiguity.
[1] E.g., the explicit [double] is quietly converted to an [int] in the following statement: & { param([int] $i) $i } ([double] '1.2'). Also, casts to .NET interfaces generally have no effect in PowerShell - except to guide overload resolution in .NET method calls.
I'm not sure this is the be-all and end-all answer, but it works for what I was curious about. For a lot of 'quick script' scenarios where you might use something like Measure-Object - you'll probably get the correct answers you're looking for, albeit maybe slower than other methods.
Measure-Object specifically seems to use [double] for Sum, Average, and StandardDeviation and will indeed throw an error if one of the string values from a CSV Object can't be converted.
I'm still a little surprised that [Math]::Sign() works at all with strings, but seemingly glad it does
if (_measureAverage || _measureSum || _measureStandardDeviation)
{
double numValue = 0.0;
if (!LanguagePrimitives.TryConvertTo(objValue, out numValue))
{
_nonNumericError = true;
ErrorRecord errorRecord = new(
PSTraceSource.NewInvalidOperationException(MeasureObjectStrings.NonNumericInputObject, objValue),
"NonNumericInputObject",
ErrorCategory.InvalidType,
objValue);
WriteError(errorRecord);
return;
}
AnalyzeNumber(numValue, stat);
}
Consider the following hashtable:
$table = #{
6 = '10.11.12.13', '10.11.12.14'
15 = 'domain.tld'
NameServers = '10.11.12.13', '10.11.12.14'
}
Normally when you have a hashtable, you can . reference the key name to work with it in a more object-oriented style vs. using the array-accessor syntax [key]. For example, invoking the NameServers key on the hashtable above with a . results in the value being returned, and I can work with members of that property's value as expected:
$table.NameServers # ========> 10.11.12.13
# ========> 10.11.12.14
$table.NameServers.Count # ========> 2
But if I try accessing the 6 key with a . which contains the same string content, it references the correct value fine, but I cannot invoke any members on the returned object. I must use the traditional array-accessor here:
$table.6 # =====> 10.11.12.13
# =====> 10.11.12.14
$table.6.Count # =====> ParserError:
# =====> Line |
# =====> 1 | $hi.6.Count
# =====> | ~
# =====> | Missing property name after reference operator.
$table[6].Count # =====> 2
Of course, ( $table.6 ).Count works around this issue but this is still a weird quirk of the syntax which I can't explain. Interestingly enough, converting the hashtable to a PSCustomObject yields the same issue. Is this perhaps a parser bug? Or is there something else going on here?
I tested this in Windows PowerShell and PowerShell 7.1 and it happens in both.
Note: This answer originally incorrectly claimed that $table.6 doesn't work, due to the hashtable key being integer-typed, but it works just fine, because the 6 in the .6 "property" access is parsed as an integer as well.
As Gyula Kokas' helpful answer points out, you're seeing problematic behavior in PowerShell's parser, present as of PowerShell 7.2, discussed in detail in GitHub issue #14036. The behavior is unrelated to hashtables per se:
What follows ., the member-access operator, is parsed as a number if it looks like a number literal, such as 6, 6l (a [long]) or even 6.0(!, a [double]).
See below for how that number is then used.
Any attempt to use another property access then triggers the error you saw:
$foo.6.bar # !! Error "Missing property name after reference operator."
In fact, following a property name that starts with a digit with a letter causes the error too - even though 6a can not interpreted as a number literal.
$foo.6a # !! Same error.
As an aside: PowerShell even allows you to use variable references, and even expressions (enclosed in (...)) as property names (e.g., $propName = 'Length'; 'foo'.$propName or ('foo'.('L' + 'ength')
Workarounds:
As you've demonstrated, enclosing the first property access in (...) - ($table.6).Count works, and so does $table.(6).Count
You've also demonstrated $table[6].Count, which works for hashtables.
For accessing an object's actual property, $obj.'6'.Count would work too (given that property names are always strings), as it would with hashtables with string keys (e.g. #{ '6'= 'six' }.'6'.Length)
Considerations specific to hashtables:
As a syntactic convenience, PowerShell allows property-access syntax ($obj.someProperty) to also be used to access the entries of a hashtable, in which case the property "name" is taken as the key of an entry in the hashtable.
The type-native syntax to access hashtable entries is index syntax ($hash[$someKey]).
While property names (referring to members of a .NET type) are invariably strings:
a hashtable's keys can be of any type.
on accessing an entry, the lookup key must not only have the right value, but must also be of the exact same type as the key stored in the hashtable.
Perhaps surprisingly, when defining a hashtable, unquoted keys situationally become either strings or numbers:
If the unquoted word can be interpreted as a number literal (e.g., 6), it is parsed as such, and you end up with a numeric key; in the case of 6, it will be [int]-typed key, because the usual number-literal typing applies (e.g., 1.0 would become a [double] key (not advisable, because binary floating-point values do not always have exact decimal representations), and 2147483648 would become a [long]).
Otherwise (e.g., NameServers), the key is [string]-typed.
Caveat: If the name starts with a digit (but isn't a number literal; e.g. 6a), an error occurs; use quoting ('6a') as a workaround - see GitHub issue #15925
That is, even though strings in expressions normally require quoting, for convenience you may omit the quoting when defining keys in hashtable literals - but the rules for recognizing number literals still apply.
Explicitly typing hashtable keys:
To ensure that a given key is interpreted as a [string], quote it (e.g., '6')
Generally, you may also use a cast to type your keys explicitly. (e.g. [long] 6) or, in number literals, the usual number-type suffix characters, such as L for [long] (e.g. 6L) - see this answer for an overview of the supported suffixes, whose number has grown significantly in PowerShell (Core) 7+.
An example, based on your hashtable:
It follows from the above that your 6 key is of type [int] (and would have to be defined as '6' in your hashtable literal if you wanted it to be a string).
Because the 6 in $name.6 is also parsed as an [int], the lookup succeeds, but note that it wouldn't succeed if different numeric types were at play:
# !! Output is $null, because the entry key is of type [long],
# !! whereas the lookup key is [int].
#{ 6L = '[long] 6' }.6
Considerations specific to property access:
With actual property access (accessing a native member of a .NET type), the fact that names that look like numbers are actually parsed as numbers first - before, of necessity, being converted to strings - can result in surprising behavior:
# !! Output is $null, because the 6L after "." is parsed as a
# !! ([long]) number first, and then converted to a string, which
# !! results in "6" being used.
([pscustomobject] #{ '6L' = 'foo' }).6L
# OK - The quoting forces 6L to be a string.
([pscustomobject] #{ '6L' = 'foo' }).'6L' # -> 'foo'
I think here is the official issue for your question:
https://github.com/PowerShell/PowerShell/issues/14036
I think the best workaround is $table.(6).count
Hashtables in powershell expect key names to be strings. When you type $table.6 based on your original code the 6 is interpreted as an integer when it's expecting it to be a string (a type conversion feature). The simplest way to resolve this would be to replace 6 with 'six', however you can make this work in a round about way.
If you make the alterations to the code like so:
$table = #{
'6' = '10.11.12.13', '10.11.12.14'
'15' = 'domain.tld'
NameServers = '10.11.12.13', '10.11.12.14'
}
You can reference it by implicitly instructing powershell to treat the 6 as a string by doing the following:
$table.'6'.count
2
It's the same principal as adding keynames with special characters, they have to be instantiated as strings to prevent interpolation or type conversion. For example:
$table = #{'bla $something' = 'anything'}
$table.'bla $something'
anything
In your case I'd do the following and move on to the next problem:
$table = #{
Six = '10.11.12.13', '10.11.12.14'
Fifteen = 'domain.tld'
NameServers = '10.11.12.13', '10.11.12.14'
}
$table.Six.count
2
For further investigation into type conversion follow this link: Powershell Type Conversion Reference
I found some strange behavior in PowerShell surrounding arrays and double quotes. If I create and print the first element in an array, such as:
$test = #('testing')
echo $test[0]
Output:
testing
Everything works fine. But if I put double quotes around it:
echo "$test[0]"
Output:
testing[0]
Only the $test variable was evaluated and the array marker [0] was treated literally as a string. The easy fix is to just avoid interpolating array variables in double quotes, or assign them to another variable first. But is this behavior by design?
So when you are using interpolation, by default it interpolates just the next variable in toto. So when you do this:
"$test[0]"
It sees the $test as the next variable, it realizes that this is an array and that it has no good way to display an array, so it decides it can't interpolate and just displays the string as a string. The solution is to explicitly tell PowerShell where the bit to interpolate starts and where it stops:
"$($test[0])"
Note that this behavior is one of my main reasons for using formatted strings instead of relying on interpolation:
"{0}" -f $test[0]
EBGreen's helpful answer contains effective solutions, but only a cursory explanation of PowerShell's string expansion (string interpolation):
Only variables by themselves can be embedded directly inside double-quoted strings ("...") (by contrast, single-quoted strings ('...'), as in many other languages, are for literal contents).
This applies to both regular variables and variables referencing a specific namespace; e.g.:
"var contains: $var", "Path: $env:PATH"
If the first character after the variable name can be mistaken for part of the name - which notably includes : - use {...} around the variable name to disambiguate; e.g.:
"${var}", "${env:PATH}"
To use a $ as a literal, you must escape it with `, PowerShell's escape character; e.g.:
"Variable `$var"
Any character after the variable name - including [ and . is treated as a literal part of the string, so in order to index into embedded variables ($var[0]) or to access a property ($var.Count), you need $(...), the subexpression operator (in fact, $(...) allows you to embed entire statements); e.g.:
"1st element: $($var[0])"
"Element count: $($var.Count)"
"Today's date: $((Get-Date -DisplayHint Date | Out-String).Trim())"
Stringification (to-string conversion) is applied to any variable value / evaluation result that isn't already a string:
Caveat: Where culture-specific formatting can be applied, PowerShell chooses the invariant culture, which largely coincides with the US-English date and number formatting; that is, dates and numbers will be represented in US-like format (e.g., month-first date format and . as the decimal mark).
In essence, the .ToString() method is called on any resulting non-string object or collection (strictly speaking, it is .psobject.ToString(), which overrides .ToString() in some cases, notably for arrays / collections and PS custom objects)
Note that this is not the same representation you get when you output a variable or expression directly, and many types have no meaningful default string representations - they just return their full type name.
However, you can embed $(... | Out-String) in order to explicitly apply PowerShell's default output formatting.
For a more comprehensive discussion of stringification, see this answer.
As stated, using -f, the string-formatting operator (<format-string> -f <arg>[, ...]) is an alternative to string interpolation that separates the literal parts of a string from the variable parts:
'1st element: {0}; count: {1:x}' -f $var[0], $var.Count
Note the use of '...' on the LHS, because the format string (the template) is itself a literal. Using '...' in this case is a good habit to form, both to signal the intent of using literal contents and for the ability to embed $ characters without escaping.
In addition to simple positional placeholders ({0} for the 1st argument. {1} for the 2nd, ...), you may optionally exercise more formatting control over the to-string conversion; in the example above, x requests a hex representation of the number.
For available formats, see the documentation of the .NET framework's String.Format method, which the -f operator is based on.
Pitfall: -f has high precedence, so be sure to enclose RHS expressions other than simple index or property access in (...); e.g., '{0:N2}' -f 1/3 won't work as intended, only '{0:N2}' -f (1/3) will.
Caveats: There are important differences between string interpolation and -f:
Unlike expansion inside "...", the -f operator is culture-sensitive:
Therefore, the following two seemingly equivalent statements do not
yield the same result:
PS> [cultureinfo]::CurrentCulture='fr'; $n=1.2; "expanded: $n"; '-f: {0}' -f $n
expanded: 1.2
-f: 1,2
Note how only the -f-formatted command respected the French (fr) decimal mark (,).
Again, see the previously linked answer for a comprehensive look at when PowerShell is and isn't culture-sensitive.
Unlike expansion inside "...", -f stringifies arrays as <type-name>[]:
PS> $arr = 1, 2, 3; "`$arr: $arr"; '$arr: {0}' -f (, $arr)
$arr: 1 2 3
$arr: System.Object[]
Note how "..." interpolation created a space-separated list of the stringification of all array elements, whereas -f-formatting only printed the array's type name.
(As discussed, $arr inside "..." is equivalent to:
(1, 2, 3).psobject.ToString() and it is the generally invisible helper type [psobject] that provides the friendly representation.)
Also note how (, ...) was used to wrap array $arr in a helper array that ensures that -f sees the expression as a single operand; by default, the array's elements would be treated as individual operands.
In such cases you have to do:
echo "$($test[0])"
Another alternative is to use string formatting
echo "this is {0}" -f $test[0]
Note that this will be the case when you are accessing properties in strings as well. Like "$a.Foo" - should be written as "$($a.Foo)"
I came across the following line of syntax which is rather advanced (to me):
(Get-ADReplicationSubnet -Filter *) -notmatch [String]::Join('|',$c.Subnet)
The above does exactly what I want, retrieve the list of subnets that are not matched against the $c.Subnet variable. I tried recreating the same effect with the line below. This does not work.
Get-ADReplicationSubnet -Filter * | Where {$_.Name -notmatch $c.Subnet}
My question is; Can someone explain in simple English how the first line works? (Not sure on the [String]::Join('|',$c.Subnet) part. It is difficult to search for something you don't know the name of. Besides that, why does my version not work?
I should clarify that $c.Subnet is an array of values.
[String]::Join('|',$c.Subnet) is call to the .NET framework -- the Join() method of the String class. You can take a look at the documentation here.
The documentation leads to the following explanation of the return value: A string that consists of the elements in value delimited by the separator string. If value is an empty array, the method returns String.Empty.
This means your return string will be something like value1|value2|value3 (where each value is value from $c.Subnet), which -notmatch interprets as a regex, with | meaning or. What the first line does is return values that do not match value1 or value2 or value3.
Why (I think) your line doesn't work is because you're using -notmatch with an array rather than a string. This article has some usage info about the two.
[String]::Join('|',$c.Subnet)
The [String] part means the .NET class.
The ::Join part means make a call to a static method of the String class, i.e. you don't have to "new up" an object before you use it.
The Join method takes and array and turns it into a string delineated by the pipe (in this case)
Someone else will have an explanation for the rest.