If I have:
$a=$null
$b=''
$c=#($null,$null)
$d='foo'
write-host $a
write-host $b
write-host $c
write-host $d
the output is
foo
I'd really like to be able to easily get output that shows the variable values, e.g.,
$Null
''
#($Null,$Null)
'foo'
I can write a function to do this, but I'm guessing/hoping there's something built-in that I'm missing. Is there, or does everyone just roll their own function for something like this?
At the moment the quickest thing I've come up with is running a value through ConvertTo-Json before printing it. It doesn't handle a plain $null, but it shows me the other values nicely.
What you're looking for is similar to Ruby's .inspect method. It's something I always loved in Ruby and do miss in PowerShell/.Net.
Unfortunately there is no such thing to my knowledge, so you will somewhat have to roll your own.
The closest you get in .Net is the .ToString() method that, at a minimum, just displays the object type (it's inherited from [System.Object]).
So you're going to have to do some checking on your own. Let's talk about the edge case checks.
Arrays
You should check if you're dealing with an array first, because PowerShell often unrolls arrays and coalesces objects for you so if you start doing other checks you may not handle them correctly.
To check that you have an array:
$obj -is [array]
1 -is [array] # false
1,2,3 -is [array] # true
,1 -is [array] #true
In the case of an array, you'll have to iterate it if you want to properly serialize its elements as well. This is basically the part where your function will end up being recursive.
function Format-MyObject {
param(
$obj
)
if ($obj -is [array]) {
# initial array display, like "#(" or "["
foreach ($o in $obj) {
Format-MyObject $obj
}
# closing array display, like ")" or "]"
}
}
Nulls
Simply check if it's equal to $null:
$obj -eq $null
Strings
You can first test that you're dealing with a string by using -is [string].
For empty, you can compare the string to an empty string, or better, to [string]::Empty. You can also use the .IsNullOrEmpty() method, but only if you've already ruled out a null value (or checked that it is indeed a string):
if ($obj -is [string) {
# pick one
if ([string]::IsNullOrEmpty($obj)) {
# display empty string
}
if ($obj -eq [string]::Empty) {
# display empty string
}
if ($obj -eq "") { # this has no advantage over the previous test
# display empty string
}
}
Alternative
You could use the built-in XML serialization, then parse the XML to get the values out of it.
It's work (enough that I'm not going to do it in an SO answer), but it removes a lot of potential human error, and sort of future-proofs the approach.
The basic idea:
$serialized = [System.Management.Automation.PSSerializer]::Serialize($obj) -as [xml]
Now, use the built in XML methods to parse it and pull out what you need. You still need to convert some stuff to other stuff to display the way you want (like interpreting <nil /> and the list of types to properly display arrays and such), but I like leaving the actual serialization to an official component.
Quick example:
[System.Management.Automation.PSSerializer]::Serialize(#(
$null,
1,
'string',
#(
'start of nested array',
$null,
'2 empty strings next',
'',
([string]::Empty)
)
)
)
And the output:
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<Obj RefId="0">
<TN RefId="0">
<T>System.Object[]</T>
<T>System.Array</T>
<T>System.Object</T>
</TN>
<LST>
<Nil />
<I32>1</I32>
<S>string</S>
<Obj RefId="1">
<TNRef RefId="0" />
<LST>
<S>start of nested array</S>
<Nil />
<S>2 empty strings next</S>
<S></S>
<S></S>
</LST>
</Obj>
</LST>
</Obj>
</Objs>
I shared two functions that reveal PowerShell values (including the empty $Null's, empty arrays etc.) further than the usually do:
One that the serializes the PowerShell objects to a PowerShell
Object Notation (PSON)
which ultimate goal is to be able to reverse everything with the
standard command Invoke-Expression and parse it back to a
PowerShell object.
The other is the ConvertTo-Text (alias CText) function that I used in
my Log-Entry
framework. note the
specific line: Log "Several examples that usually aren't displayed
by Write-Host:" $NotSet #() #(#()) #(#(), #()) #($Null) that I wrote
in the example.
Function Global:ConvertTo-Text1([Alias("Value")]$O, [Int]$Depth = 9, [Switch]$Type, [Switch]$Expand, [Int]$Strip = -1, [String]$Prefix, [Int]$i) {
Function Iterate($Value, [String]$Prefix, [Int]$i = $i + 1) {ConvertTo-Text $Value -Depth:$Depth -Strip:$Strip -Type:$Type -Expand:$Expand -Prefix:$Prefix -i:$i}
$NewLine, $Space = If ($Expand) {"`r`n", ("`t" * $i)} Else{"", ""}
If ($O -eq $Null) {$V = '$Null'} Else {
$V = If ($O -is "Boolean") {"`$$O"}
ElseIf ($O -is "String") {If ($Strip -ge 0) {'"' + (($O -Replace "[\s]+", " ") -Replace "(?<=[\s\S]{$Strip})[\s\S]+", "...") + '"'} Else {"""$O"""}}
ElseIf ($O -is "DateTime") {$O.ToString("yyyy-MM-dd HH:mm:ss")}
ElseIf ($O -is "ValueType" -or ($O.Value.GetTypeCode -and $O.ToString.OverloadDefinitions)) {$O.ToString()}
ElseIf ($O -is "Xml") {(#(Select-XML -XML $O *) -Join "$NewLine$Space") + $NewLine}
ElseIf ($i -gt $Depth) {$Type = $True; "..."}
ElseIf ($O -is "Array") {"#(", #(&{For ($_ = 0; $_ -lt $O.Count; $_++) {Iterate $O[$_]}}), ")"}
ElseIf ($O.GetEnumerator.OverloadDefinitions) {"#{", (#(ForEach($_ in $O.Keys) {Iterate $O.$_ "$_ = "}) -Join "; "), "}"}
ElseIf ($O.PSObject.Properties -and !$O.value.GetTypeCode) {"{", (#(ForEach($_ in $O.PSObject.Properties | Select -Exp Name) {Iterate $O.$_ "$_`: "}) -Join "; "), "}"}
Else {$Type = $True; "?"}}
If ($Type) {$Prefix += "[" + $(Try {$O.GetType()} Catch {$Error.Remove($Error[0]); "$Var.PSTypeNames[0]"}).ToString().Split(".")[-1] + "]"}
"$Space$Prefix" + $(If ($V -is "Array") {$V[0] + $(If ($V[1]) {$NewLine + ($V[1] -Join ", $NewLine") + "$NewLine$Space"} Else {""}) + $V[2]} Else {$V})
}; Set-Alias CText ConvertTo-Text -Scope:Global -Description "Convert value to readable text"
ConvertTo-Text
The ConvertTo-Text function (Alias CText) recursively converts PowerShell object to readable text this includes hash tables, custom objects and revealing type details (like $Null vs an empty string).
Syntax
ConvertTo-Text [<Object>] [[-Depth] <int>] [[-Strip] <int>] <string>] [-Expand] [-Type]
Parameters
<Object>
The object (position 0) that should be converted a readable value.
-Depth <int>
The maximal number of recursive iterations to reveal embedded objects.
The default depth for ConvertTo-Text is 9.
-Strip <int>
Truncates strings at the given length and removes redundant white space characters if the value supplied is equal or larger than 0. Set -Strip -1 prevents truncating and the removal of with space characters.
The default value for ConvertTo-Text is -1.
-Expand
Expands embedded objects over multiple lines for better readability.
-Type
Explicitly reveals the type of the object by adding [<Type>] in front of the objects.
Note: the parameter $Prefix is for internal use.
Examples
The following command returns a string that describes the object contained by the $var variable:
ConvertTo-Text $Var
The following command returns a string containing the hash table as shown in the example (rather then System.Collections.DictionaryEntry...):
ConvertTo-Text #{one = 1; two = 2; three = 3}
The following command reveals values (as e.g. $Null) that are usually not displayed by PowerShell:
ConvertTo-Text #{Null = $Null; EmptyString = ""; EmptyArray = #(); ArrayWithNull = #($Null); DoubleEmptyArray = #(#(), #())} -Expand
The following command returns a string revealing the WinNT User object up to a level of 5 deep and expands the embedded object over multiple lines:
ConvertTo-Text ([ADSI]"WinNT://./$Env:Username") -Depth 5 -Expand
A quick self-rolled option good for some datatypes.
function Format-MyObject {
param(
$obj
)
#equality comparison order is important due to array -eq overloading
if ($null -eq $obj)
{
return 'null'
}
#Specify depth because the default is 2, because powershell
return ConvertTo-Json -Depth 100 $obj
}
Related
Ok. So I thought this would have been easy, but I am hitting a snag.
$var = (Get-ItemProperty "HKCU:\SOFTWARE\SAP\General" -Name "BrowserControl")."BrowserControl"
$var2 = "HKCU:\SOFTWARE\SAP\General"
$var3 = #('1','0')
#if (($var -eq ($var3 -join'')))
#if (Compare-Object -IncludeEqual $var $var3 -SyncWindow 0)
if ($var -eq $var3)
{
Write-Output "Registry hive exists"
exit 1
}
else
{
Write-Output "Registry hive doesn't exists"
#New-ItemProperty -Path $var2 -name "BrowserControl" -Value "1" -PropertyType "DWORD" -Force | Out-Null
}
If 1 or 0 is returned from BrowserControl, I want it to be a match. If anything else is returned, no match.
If BrowserControl is set to 1, it works. If it is set to 0 or any number other than 1 it doesn't match.
I know I can use else-if and add a couple more lines of code, but I was really wanting to get this to work.
As you can see, I have tried different comparison methods. I also tried (0,1), ('0','1'), 0,1 for var3. None of those worked either.
So... what am I missing?
You cannot meaningfully use an array as the RHS (right-hand side) of the -eq operator.[1]
However, PowerShell has dedicated operators for testing whether a given single value is contained in a collection (more accurately: equal to one of the elements of a collection), namely -in and its operands-reversed counterpart, -contains.
In this case, -in makes for more readable code:
if ($var -in $var3) # ...
[1] PowerShell quietly accepts an array (collection) as the RHS, but - uselessly - stringifies it, by concatenating the elements with a single space by default. E.g., '1 2' -eq 1, 2 yields $true.
By contrast, using an array as the LHS of -eq is meaningfully supported: the RHS scalar then acts as a filter, returning the sub-array of equal LHS elements; e.g. 1, 2, 3, 2 -eq 2 returns 2, 2
I found that I do .GetType() and | Get-Member a lot, and sometimes I forget that it's .GetType and I try .Get-Type duh! error!), so I've been writing a function to try and gather that info quickly. This has proven to be quite useful when working on the console (I make sure to put the core command before each output so that I never forget the connection to real commands, so more of a techy summary to keep me connected to the language).
I'm curious if there are additional compound commands to extract useful generic information that we could use to report on the structure of a given object (things that we can quickly obtain in a nicely compact summary format even though more complex commands are required for some useful insight about given objects)?
• $a = #(1,2,"x") ; obj $a. This returns 71 Methods (System.String and System.Int32) Types, so I've removed duplicates down to 50 (good to quickly see what is usable but maybe good to somehow also mention the different Types that are contained in that array?).
• Some input will break the function of course, but even a ScriptBlock example like this also works fine obj {$a; $x}. You can even do things like obj "".GetType() to see the Methods and properties in there.
• The use of .Module in the GetType() might be redundant as usually outputs CommonLanguageRuntimeLibrary, but maybe other useful information from these members (everything is useful at different times of course, but I'm curious for generic summary output)?
• In general any improvements or other compound commands that you use or could be useful to crack open object information in a quick summary view would be great to know? :-)
Updated with -Force that #Clint suggested:
function obj ($cmd) {
if ($cmd -eq $null) { Write-Host "Object is `$null" ; return }
Write-Host "Contents:" -F Cyan
$cmd
""
Write-Host "(`$object).GetType()" -F Cyan -NoNewline ; Write-Host " :: [BaseType|Name|IsPublic|IsSerial|Module]"
($cmd).GetType() | % { "$($_.BaseType), $($_.Name), $($_.IsPublic), $($_.IsSerializable), $($_.Module)" }
""
Write-Host "`$object | Get-Member -Force" -F Cyan
$m = "" ; $p = "" ; $pp = "" ; $np = "" ; $sp = ""
$msum = 0 ; $psum = 0 ; $ppsum = 0 ; $npsum = 0 ; $spsum = 0
$($cmd | Get-Member -Force) | % {
if ($_.MemberType -eq "Method") { if(!($m -like "*$($_.Name),*")) { $m += "$($_.Name), " ; $msum++ } }
if ($_.MemberType -eq "Property") { if(!($p -like "*$($_.Name),*")) { $p += "$($_.Name), " ; $psum++ } }
if ($_.MemberType -eq "ParameterizedProperty") { if(!($pp -like "*$($_.Name),*")) { $pp += "$($_.Name), " ; $ppsum++} }
if ($_.MemberType -eq "NoteProperty") { if(!($np -like "*$($_.Name),*")) { $np += "$($_.Name), " ; $npsum++ } }
if ($_.MemberType -eq "ScriptProperty") { if(!($sp -like "*$($_.Name),*")) { $sp += "$($_.Name), " ; $npsum++ } }
}
if($msum -ne 0) { Write-Host ":: Method [$msum] => $($m.TrimEnd(", "))" }
if($psum -ne 0) { Write-Host ":: Property [$psum] => $($p.TrimEnd(", "))" }
if($ppsum -ne 0) { Write-Host ":: ParameterizedProperty [$ppsum] => $($pp.TrimEnd(", "))" }
if($npsum -ne 0) { Write-Host ":: NoteProperty [$npsum] => $($np.TrimEnd(", "))" }
if($spsum -ne 0) { Write-Host ":: ScriptProperty [$spsum] => $($sp.TrimEnd(", "))" }
""
}
An example of output:
C:\> $a = #(123,"x")
C:\> def $a
Contents:
123
x
($object).GetType() :: [BaseType|Name|IsPublic|IsSerial|Module]
array, Object[], True, True, CommonLanguageRuntimeLibrary
$object | Get-Member -Force
:: Method [50] => CompareTo, Equals, GetHashCode, GetType, GetTypeCode, ToBoolean, ToByte, ToChar, ToDateTime, ToDecimal, ToDouble, ToInt16,
ToInt32, ToInt64, ToSByte, ToSingle, ToString, ToType, ToUInt16, ToUInt32, ToUInt64, Clone, Contains, CopyTo, EndsWith, GetEnumerator,
get_Chars, get_Length, IndexOf, IndexOfAny, Insert, IsNormalized, LastIndexOf, LastIndexOfAny, Normalize, PadLeft, PadRight, Remove, Replace,
Split, StartsWith, Substring, ToCharArray, ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant, Trim, TrimEnd, TrimStart
:: Property [1] => Length
:: ParameterizedProperty [1] => Chars
You have summarized it quite well as it is, you might as well add
object | gm -Force # To add members that are usually hidden by default
The Get-Member command uses the Force parameter to add the intrinsic
members and compiler-generated members of the objects to the display.
Get-Member gets these members, but it hides them by default.
Intrinsic members (PSBase, PSAdapted, PSObject, PSTypeNames)
Compiler-generated get_ and set_ methods
MSDN Doc
object.PSObject #Wraps the information pertaining to different members
object.PSObject | Select-Object -ExpandProperty Properties #You can do the same for other members
$object |gm -force -static #even with force the static members are not listed by default, so we need to explicitly mention static
maybe good to somehow also mention the different Types that are contained in that array?).
$object | ForEach-Object {$_.GetType().FullName}
OffTopic, additionally, if you want to capture the time taken for the function to execute
$sw = [Diagnostics.Stopwatch]::StartNew()
$sw.Stop()
Write-Host "Execution time" $sw.Elapsed.Milliseconds "ms"
Knowing that PowerShell isn't very clear in displaying an object (as e.g.: '', $Null, #() and #{$Null) all result in an empty line, or nothing at all), I think you need start with the first paragraph of you question and visualize the types and the structure of your objects in question and later concern with the methods which might differ on every
level in the object structure.
*note: you might also reveal the types by using .PSTypeNames, which doesn't error on a $Null (presuming that strict mode isn't switch on).
Taken a little bit more advanced example: $a = #($Null, 1, '2', #(3))
Write-Host
The Write-Host cmdlet (used by your function) doesn't reveal much where is concerns types and the structure of the object:
(Besides you should try to avoid Write-Host as it is Host specific)
PS C:\> $a
1
2
3
PS C:\> Write-Host $a
1 2 3
To better understand such an object, you might want to serializes it.
Quote from WikiPedia:
In computer science, in the context of data storage, serialization (or
serialisation) is the process of translating data structures or object
state into a format that can be stored (for example, in a file or
memory buffer) or transmitted (for example, across a network
connection link) and reconstructed later (possibly in a different
computer environment)
This means that the string representation of the object basically contains all the information to rebuild (and to understand) the object (or at least to a certain level).
There are a few serializers that might come at hand here:
ConvertTo-Json
The ConvertTo-Json cmdlet converts any object to a string in JavaScript Object Notation (JSON) format a therefore might be handy to get an idea of the structure but not for the PowerShell types:
PS C:\> ConvertTo-Json $a
[
null,
1,
"2",
[
3
]
]
ConvertTo-Xml
The ConvertTo-Xml cmdlet creates an XML-based representation of an object which is pretty verbose:
PS C:\> ConvertTo-Xml $a -as String
<?xml version="1.0" encoding="utf-8"?>
<Objects>
<Object Type="System.Object[]">
<Property />
<Property Type="System.Int32">1</Property>
<Property Type="System.String">2</Property>
<Property Type="System.Object[]">
<Property Type="System.Int32">3</Property>
</Property>
</Object>
</Objects>**strong text**
PSSerializer
This (.Net) class provides public functionality for serializing a PSObject and is internally used by PowerShell:
PS C:\> [System.Management.Automation.PSSerializer]::Serialize($a)
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<Obj RefId="0">
<TN RefId="0">
<T>System.Object[]</T>
<T>System.Array</T>
<T>System.Object</T>
</TN>
<LST>
<Nil />
<Obj RefId="1">
<I32>1</I32>
</Obj>
<S>2</S>
<Obj RefId="2">
<TNRef RefId="0" />
<LST>
<I32>3</I32>
</LST>
</Obj>
</LST>
</Obj>
</Objs>
ConvertTo-Expression
The ConvertTo-Expression cmdlet originated from the question: Save hash table in PowerShell object notation (PSON) and gives you a PowerShell representation of the object:
Implicit:
PS C:\> ConvertTo-Expression $a
$Null,
1,
'2',
(,3)
Explicit:
PS C:\> ConvertTo-Expression $a -Strong
[array](
$Null,
[int]1,
[string]'2',
[array][int]3
)
To explorer .Net classes that are not native to PowerShell:
*note that you might not reconstruct from -Explore expressions
PS C:\> Get-Service | Select -First 1 | ConvertTo-Expression -Strong -Explore
[System.ServiceProcess.ServiceController]#{
'UserName' = [string]''
'Description' = [string]'Runtime for activating conversational agent applications'
'DelayedAutoStart' = [bool]$False
'BinaryPathName' = [string]'C:\WINDOWS\system32\svchost.exe -k AarSvcGroup -p'
'StartupType' = [Microsoft.PowerShell.Commands.ServiceStartupType]3
'CanPauseAndContinue' = [bool]$False
'CanShutdown' = [bool]$False
'CanStop' = [bool]$False
'DisplayName' = [string]'Agent Activation Runtime_131b90'
'DependentServices' = [System.ServiceProcess.ServiceController[]]#()
'MachineName' = [string]'.'
'ServiceName' = [string]'AarSvc_131b90'
'ServicesDependedOn' = [System.ServiceProcess.ServiceController[]]#()
'StartType' = [System.ServiceProcess.ServiceStartMode]3
'ServiceHandle' = $Null
'Status' = [System.ServiceProcess.ServiceControllerStatus]1
'ServiceType' = [System.ServiceProcess.ServiceType]224
'Site' = $Null
'Container' = $Null
}
To drill down in known (accelerated) PowerShell types:
PS C:\>Get-Date | Select-Object -Property * | ConvertTo-Expression
Date : 1963-10-07 12:00:00 AM
DateTime : Monday, October 7, 1963 10:47:00 PM
Day : 7
DayOfWeek : Monday
DayOfYear : 280
DisplayHint : DateTime
Hour : 22
Kind : Local
Millisecond : 0
Minute : 22
Month : 1
Second : 0
Ticks : 619388596200000000
TimeOfDay : 22:47:00
Year : 1963
I'm a bit confused about some behavior I'm seeing.
The following code is designed to find and replace several strings in the registry.
$keys = #(gci -Path hkcu:\ -recurse -ErrorAction SilentlyContinue)
foreach ($key in $keys)
{
foreach ($vname in $key.GetValueNames())
{
$val = $key.GetValue($vname, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($val -like "c:\users\administrator*")
{
$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%"
write-host "$key\$vname=$val -> $nval"
((Get-Item $Key.PSParentPath).OpenSubKey($Key.PSChildName, "True")).SetValue($vname, $nval, $key.GetValueKind($vname))
}
}
}
I continue to get the following error message when the registry value type is REG_MULTI_SZ. Exception calling "SetValue" with "3" argument(s): "The type of the value object did not match the specified RegistryValueKind or the object could not be properly converted."
If I comment out the -replace portion, i.e.:
$nval = $val #-replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%"
The registry keys are updated without errors (obviously with the same value they were).
So, something in the -replace operation is changing the data type. It appears to change in to type System.Object[].
The registryKey.SetValue method requires a string[] to set REG_MULTI_SZ.
Why is it changing the type? And how do I work around this behavior?
UPDATE:
Applying Option #1 in the answer did not work. $nval was still of type System.Object[] even after adding the cast. Applying option #3 did work.
Here is the final code that correctly searches and replaces a string found at the beginning of a string in the registry.
$keys = #(gci -Path hkcu:\ -recurse -ErrorAction SilentlyContinue)
foreach ($key in $keys)
{
foreach ($vname in $key.GetValueNames())
{
$val = $key.GetValue($vname, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($val -like "c:\users\administrator*")
{
if ($key.GetValueKind($vname) -eq [Microsoft.Win32.RegistryValueKind]::MultiString)
{ $nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" -as [string[]] }
else
{ $nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" }
write-host "$key\$vname=$val -> $nval"
((Get-Item $Key.PSParentPath).OpenSubKey($Key.PSChildName, "True")).SetValue($vname, $nval, $key.GetValueKind($vname))
}
}
}
Most things in PowerShell that return multiple objects return them as an [object[]]; most likely because you could return any number of different objects, and because PowerShell wraps most objects in [PSObject] whether you know it or not.
So although you may start with a [string[]], modifying it would result in an [object[]].
When you need an array of a specific type, the simple way is to just cast it.
You have 3 options for casting it: cast the value of the assignment or cast the variable, or use the -as operator.
Casting the value:
$nval = [string[]]($val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%")
Now $nval will be a [string[]] unless you re-assign something else to it later.
Casting the variable:
[string[]]$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%"
This works a little bit differently. Putting the cast on the variable in PowerShell applies that cast to all values assigned to it.
This is a good option when you know $nval always needs to be [string[]] and you don't want to cast it on every assignment.
-As operator:
$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" -as [string[]]
This is similar to casting the value; the difference is that it doesn't throw an exception if the cast is unsuccessful, it just returns $null.
To deal with the different registry kinds and the casts needed, I recommended a switch statement in the comments, but I came up with a better idea: just use a hashtable:
$kindToType = #{
[Microsoft.Win32.RegistryValueKind]::MultiString = [string[]]
[Microsoft.Win32.RegistryValueKind]::ExpandString = [string]
[Microsoft.Win32.RegistryValueKind]::String = [string]
[Microsoft.Win32.RegistryValueKind]::DWord = [int]
# etc.
}
Then:
$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" -as $kindToType[$key.GetValueKind($vname)]
This creates a lookup table where you can directly get a type from a registry kind.
To complement briantist's helpful answer with a focused summary:
If the LHS of a -replace expression is a collection[1]
, the replacement is performed on each of its elements.
The results are returned as a regular PowerShell array, which is of type [System.Object[]] - irrespective of the specific type of the input collection.
To preserve the input collection type, you must use an explicit cast or call an appropriate constructor - see briantist's answer.
Example:
# Apply -replace to a LHS of type [System.Collections.ArrayList]
$result = ([System.Collections.ArrayList] ('one', 'two')) -replace 'o', '#'
> $result
#ne
tw#
> $result.GetType().FullName # inspect the result's type
System.Object[]
[1] Without having looked at the source code, it seems that "collection" in this sense refers to any type that implements interface [System.Collections.IEnumerable] that doesn't also implement [System.Collections.IDictionary]; for a given instance $coll, you can test as follows:
$coll -is [System.Collections.IEnumerable] -and $coll -isnot [System.Collections.IDictionary].
Notably, this excludes hashtable-like collections, such as [hashtable] (literal syntax #{ ... }) and [System.Collections.Specialized.OrderedDictionary] (literal syntax [ordered] #{ ... }).
I have a Powershell function in which I am trying to allow the user to add or remove items from a list by typing the word "add" or "remove" followed by a space-delimited list of items. I have an example below (slightly edited, so you can just drop the code into a powershell prompt to test it "live").
$Script:ServerList = #("Server01","Server02","Server03")
Function EditServerList (){
$Script:ServerList = $Script:ServerList |Sort -Unique
Write-host -ForegroundColor Green $Script:ServerList
$Inputs = $args
If ($Inputs[0] -eq "start"){
$Edits = Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
#"# EditServerList $Edits
# EditServerList $Edits.split(' ')
EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"})
EditServerList start
} Elseif ($Inputs[0] -eq "add"){
$Script:ServerList += $Inputs |where {$_ -NotLike $Inputs[0]}
EditServerList start
} Elseif ($Inputs[0] -eq "remove"){
$Script:ServerList = $Script:ServerList |Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})}
EditServerList start
} Else {
Write-Host -ForegroundColor Red "ERROR!"
EditServerList start
}
}
EditServerList start
As you can see, the function takes in a list of arguments. The first argument is evaluated in the If/Then statements and then the rest of the arguments are treated as items to add or remove from the list.
I have tried a few different approaches to this, which you can see commented out in the first IF evaluation.
I have two problems.
When I put in something like "add Server05 Server06" (without quotes) it works, but it also drops in the word "add".
When I put in "remove Server02 Server03" (without quotes) it does not edit the array at all.
Can anybody point out where I'm going wrong, or suggest a better approach to this?
To address the title's generic question up front:
When you pass an array to a function (and nothing else), $Args receives a single argument containing the whole array, so you must use $Args[0] to access it.
There is a way to pass an array as individual arguments using splatting, but it requires an intermediate variable - see bottom.
To avoid confusion around such issues, formally declare your parameters.
Try the following:
$Script:ServerList = #("Server01", "Server02", "Server03")
Function EditServerList () {
# Split the arguments, which are all contained in $Args[0],
# into the command (1st token) and the remaining
# elements (as an array).
$Cmd, $Servers = $Args[0]
If ($Cmd -eq "start"){
While ($true) {
Write-host -ForegroundColor Green $Script:ServerList
$Edits = Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
#"# Pass the array of whitespace-separated tokens to the recursive
# invocation to perform the requested edit operation.
EditServerList (-split $Edits)
}
} ElseIf ($Cmd -eq "add") {
# Append the $Servers array to the list, weeding out duplicates and
# keeping the list sorted.
$Script:ServerList = $Script:ServerList + $Servers | Sort-Object -Unique
} ElseIf ($Cmd -eq "remove") {
# Remove all specified $Servers from the list.
# Note that servers that don't exist in the list are quietly ignored.
$Script:ServerList = $Script:ServerList | Where-Object { $_ -notin $Servers }
} Else {
Write-Host -ForegroundColor Red "ERROR!"
}
}
EditServerList start
Note how a loop is used inside the "start" branch to avoid running out of stack space, which could happen if you keep recursing.
$Cmd, $Servers = $Args[0] destructures the array of arguments (contained in the one and only argument that was passed - see below) into the 1st token - (command string add or remove) and the array of the remaining arguments (server names).
Separating the arguments into command and server-name array up front simplifies the remaining code.
The $var1, $var2 = <array> technique to split the RHS into its first element - assigned as a scalar to $var1 - and the remaining elements - assigned as an array to $var2, is commonly called destructuring or unpacking; it is documented in Get-Help about_Assignment Operators, albeit without giving it such a name.
-split $Edits uses the convenient unary form of the -split operator to break the user input into an array of whitespace-separated token and passes that array to the recursive invocation.
Note that EditServerList (-split $Edits) passes a single argument that is an array - which is why $Args[0] must be used to access it.
Using PowerShell's -split operator (as opposed to .Split(' ')) has the added advantage of ignoring leading and trailing whitespace and ignoring multiple spaces between entries.
In general, operator -split is preferable to the [string] type's .Split() method - see this answer of mine.
Not how containment operator -notin, which accepts an array as the RHS, is used in Where-Object { $_ -notin $Servers } in order to filter out values from the server list contained in $Servers.
As for what you tried:
EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"}) (a) mistakenly attempts to remove the command name from the argument array, even though the recursive invocations require it, but (b) actually fails to do so, because the RHS of -like doesn't support arrays. (As an aside: since you're looking for exact strings, -eq would have been the better choice.)
Since you're passing the arguments as an array as the first and only argument, $Inputs[0] actually refers to the entire array (command name + server names), not just to its first element (the command name).
You got away with ($Inputs[0] -eq "add") - even though the entire array was compared - because the -eq operator performs array filtering if its LHS is an array, returning a sub-array of matching elements. Since add was among the elements, a 1-element sub-array was returned, which, in a Boolean context, is "truthy".
However, your attempt to weed out the command name with where {$_ -NotLike $Inputs[0]} then failed, and add was not removed - you'd actually have to compare to $Inputs[0][0] (sic).
Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})} doesn't filter anything out for the following reasons:
($Inputs |Where {$_ -Notlike $Inputs[0]}) always returns an empty array, because, the RHS of -Notlike is an array, which, as stated, doesn't work.
Therefore, the command is the equivalent of Where {$_ -NotLike #() } which returns $True for any scalar on the LHS.
Passing an array as individual arguments using splatting
Argument splatting (see Get-Help about_Splatting) works with arrays, too:
> function foo { $Args.Count } # function that outputs the argument count.
> foo #(1, 2) # pass array
1 # single parameter, containing array
> $arr = #(1, 2); foo #arr # splatting: array elements are passed as indiv. args.
2
Note how an intermediate variable is required, and how it must be prefixed with # rather than $ to perform the splatting.
I'd use parameters to modify the ServerList, this way you can use a single line to both add and remove:
Function EditServerList {
param(
[Parameter(Mandatory=$true)]
[string]$ServerList,
[array]$add,
[array]$remove
)
Write-Host -ForegroundColor Green "ServerList Contains: $ServerList"
$Servers = $ServerList.split(' ')
if ($add) {
$Servers += $add.split(' ')
}
if ($remove) {
$Servers = $Servers | Where-Object { $remove.split(' ') -notcontains $_ }
}
return $Servers
}
Then you can call the function like this:
EditServerList -ServerList "Server01 Server02 Server03" -remove "Server02 Server03" -add "Server09 Server10"
Which will return:
Server01
Server09
Server10
I want to assign a null value to a variable called $dec, but it gives me errors. Here is my code:
import-module activedirectory
$domain = "domain.example.com"
$dec = null
Get-ADComputer -Filter {Description -eq $dec}
These are automatic variables, like $null, $true, $false etc.
about_Automatic_Variables, see https://technet.microsoft.com/en-us/library/hh847768.aspx?f=255&MSPPError=-2147217396
$NULL
$null is an automatic variable that contains a NULL or empty
value. You can use this variable to represent an absent or undefined
value in commands and scripts.
Windows PowerShell treats $null as an object with a value, that is, as
an explicit placeholder, so you can use $null to represent an empty
value in a series of values.
For example, when $null is included in a collection, it is counted as
one of the objects.
C:\PS> $a = ".dir", $null, ".pdf"
C:\PS> $a.count
3
If you pipe the $null variable to the ForEach-Object cmdlet, it
generates a value for $null, just as it does for the other objects.
PS C:\ps-test> ".dir", $null, ".pdf" | Foreach {"Hello"}
Hello
Hello
Hello
As a result, you cannot use $null to mean "no parameter value." A
parameter value of $null overrides the default parameter value.
However, because Windows PowerShell treats the $null variable as a
placeholder, you can use it scripts like the following one, which
would not work if $null were ignored.
$calendar = #($null, $null, “Meeting”, $null, $null, “Team Lunch”, $null)
$days = Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"
$currentDay = 0
foreach($day in $calendar)
{
if($day –ne $null)
{
"Appointment on $($days[$currentDay]): $day"
}
$currentDay++
}
output:
Appointment on Tuesday: Meeting
Appointment on Friday: Team lunch
Use $dec = $null
From the documentation:
$null is an automatic variable that contains a NULL or empty value. You can use this variable to represent an absent or undefined value in commands and scripts.
PowerShell treats $null as an object with a value, that is, as an explicit placeholder, so you can use $null to represent an empty value in a series of values.
If the goal simply is to list all computer objects with an empty description attribute try this
import-module activedirectory
$domain = "domain.example.com"
Get-ADComputer -Filter '*' -Properties Description | where { $_.Description -eq $null }
As others have said, use $null.
However, the handling of $null is not so simple.
In lists (or, more precisely, System.Array objects) $null is treated as a placeholding object when indexing the list, so ($null, $null).count outputs 2.
But otherwise $null is treated as a flag signifying that there is no content (no object; or, more precisely, a "null-valued expression", as reported by .GetType()), so ($null).count outputs 0.
Thus
$null.count; # Output = 0
($null).count; # Output = 0
(, $null).count; # Output = 1
($null, $null).count; # Output = 2
($null, $null, $null).count; # Output = 3
Note: the same output is returned from .count and .length in the above context.
Similarly if explicitly assigning any of the above to a variable, as in
$aaa = $null; $aaa.count
$bbb = ($null, $null); $bbb.count
which output, respectively, 0 and 2.
Similarly if looping with ForEach, as in
$aaa = $null; ForEach ($a in $aaa) {write-host "Foo" -NoNewLine}
$bbb = ($null, $null); ForEach ($b in $bbb) {write-host "Bar" -NoNewLine}
which output, respectively, nothing and BarBar.
However, note well that when operating on an individual item that has been returned from a list $null is again treated as a "null-valued expression", as can be confirmed by running
$xxx = ($null, "foo", $null); ForEach ($x in $xxx) {write-host "C=" $x.count "| " -NoNewLine}
which outputs C= 0 | C= 1 | C= 0 | .