How to create a case sensitive Hashtable without generating a PSScriptAnalyzer warning? - powershell

By default hash tables created by PowerShell (e.g. $hashtable = #{}) are case insensitive.
For my script I want to explicitly create a case sensitive hash table.
This can be done by:
$hashtable = [hashtable]::new()
or:
$hashtable = New-Object hashtable
But I want to have my script also compliant with the default PSScriptAnalyzer rules. For the above case sensitive hash table examples, the UseLiteralInitializerForHashtable rule cases a warning:
Use literal initializer, #{{}}, for creating a hashtable as they are case-insensitive by default
I would expect to be able to work arround this by specifying the StringComparer, like:
[HashTable]::New(0, [StringComparer]::Ordinal)
But this still generates an error (although [HashTable]::New(0, [StringComparer]::OrdinalIgnoreCase) doesn't).
AFAIK, there is not something like: [StringComparer]::OrdinalMatchCase, or?
How to create a case sensitive Hashtable without generating a PSScriptAnalyzer warning?
PSScriptAnalyzer version: 1.18.3
Tested both Windows PowerShell (5.1) and PowerShell Core (6.2.3)
Steps to reproduce the warning:
Invoke-ScriptAnalyzer -ScriptDefinition '[HashTable]::New(0, [StringComparer]::Ordinal)'

Have you tried to encapsulate the hashtable using inline c# as a custom static class?
Add-Type -typedef #"
using System;
using System.Collections;
namespace myCsharp
{
//-----------------------------------------
public class myHashtable
//-----------------------------------------
{
//-------------------------------------
public static Hashtable GetHashtable()
//-------------------------------------
{
Hashtable ht = new Hashtable( 0, StringComparer.Ordinal);
return ht;
}
}
}
"#
$x = [myCsharp.myHashtable]::GetHashtable()

Related

Type [System.Windows.Forms.DialogResult] is not found despite the Add-Type cmdlet [duplicate]

I am trying to get my head around PowerShell Class best practices, and running into some confusion with a simple class to handle Balloon Tips.
Add-Type -assemblyName:System.Drawing
Add-Type -assemblyName:System.Windows.Forms
class PxMessage {
static [PxMessage] $instance
static $balloon
static $defaultIcon
static [PxMessage] GetInstance($processIcon) {
if ([PxMessage]::instance -eq $null) {
[PxMessage]::instance = [PxMessage]::new()
#[PxMessage]::balloon = [Windows.Forms.NotifyIcon]::new()
[PxMessage]::balloon = New-Object Windows.Forms.NotifyIcon
[PxMessage]::defaultIcon = $processIcon
}
return [PxMessage]::instance
}
[Void] SendMessage ([String]$title, [String]$message, [String]$messageIcon) {
[PxMessage]::balloon.icon = [PxMessage]::defaultIcon
[PxMessage]::balloon.balloonTipTitle = $title
[PxMessage]::balloon.balloonTipText = $message
[PxMessage]::balloon.balloonTipIcon = $messageIcon
[PxMessage]::balloon.visible = $true
[PxMessage]::balloon.ShowBalloonTip(0)
[PxMessage]::balloon.Dispose
}
}
$processIcon = [System.Drawing.Icon]::ExtractAssociatedIcon($(Get-Process -id:$PID | Select-Object -expandProperty:path))
$message = [PxMessage]::GetInstance($processIcon)
$message.SendMessage('Title', "$(Get-Date)", 'Info')
I have two questions:
1: Why does [PxMessage]::balloon = New-Object Windows.Forms.NotifyIcon work, but [PxMessage]::balloon = [Windows.Forms.NotifyIcon]::new() does not (Unable to find type error)? And does this suggest that using [Type]::new() is not yet fully supported, and for consistency sake I am better off using New-Object everywhere? Or at least everywhere in my own Classes?
2: I would like to type my properties & parameters, but I also get Unable to find type errors when I type the $balloon & $defaultIcon properties, or if I type the $processIcon parameter in the GetInstance method.
Obviously I can type Properties, even with my type being defined. So what is different about the two [System.Drawing] & [System.Windows.Forms], and is this a bug, or a feature? And are there other types that behave similarly?
This is essentially a race condition!
When PowerShell starts executing a script file, it goes through 3 phases:
Parsing
Compilation
Execution
The very first thing that is processed in the compilation phase, (so before execution even begins) is:
All using statements at top of the script
Any type definitions - any class or enum keywords - are compiled separately
So the errors related to resolving the [Windows.Forms.NotifyIcon] type literal inside the class definition are actually thrown before Add-Type -AssemblyName:System.Windows.Forms gets a chance to ever run!
Couple of options:
Nested scripts
Write a separate loader script that loads the dependency:
# loader.ps1
Add-Type -AssemblyName System.Windows.Forms,System.Drawing
. .\scriptWithTypes.ps1
# scriptWithTypes.ps1
class ClassDependentOnForms
{
[Windows.Forms.NotifyIcon]$BalloonTipIcon
}
With modules
With modules it's a bit simpler to manage dependencies ahead of compiling the custom type definitions - just add the assembly names as RequiredAssemblies to the module manifest:
New-ModuleManifest ... -RootModule moduleWithTypes.psm1 -RequiredAssemblies System.Windows.Forms,System.Drawing
Load from disk with using assembly ...
If the required assembly's path is known, you can load it at parse time with a using assembly statement:
using assembly '.\path\to\System.Drawing.dll'
using assembly '.\path\to\System.Windows.Forms.dll'
class FormsDependentClass
{
[Windows.Forms.NotifyIcon]$BallonTipIcon
}
For your use case, this last one is not very attractive because you'd need to hardcode the assembly instead of just loading it from the GAC by name.
Why does this occur in the first place?
This behavior might be slightly confusing because everything else in PowerShell is just straightforward "one statement at a time"-interpretation.
The reason for this exception is to allow scripts and functions to encapsulate custom parameter types:
param(
[CustomEnumType]$Option
)
begin {
enum CustomEnumType {
None
Option1
Option2
}
}
end {
# do something based on $Option
}
Without this preemptive compilation of CustomEnumType at parse time, PowerShell would be unable to offer autocompletion and input validation for the -Option parameter argument - because it's type would not exist

Is there a way to add a method to built-in/native powershell object (or type?/class¿)?

Is there a way to add a method to built-in/native powershell object (or type?/class¿)?
In particular, I'm looking at you [hashtable], but I suppose my question is also a general one... for instance, I could see wanting to add functionality to all my arrays...
For instance, I would like all my [hashtable] objects to have the method: .AddAndOverwrite(..) which would replace the value of the key if it exists; otherwise it creates a new key.
The only way I seem to be able to do this is to:
create an empty hashtable, $HashTableNew
add the ScriptMethod(s) to $HashTableNew (i.e. .AddAndOverwrite(..))
then when I use it, make a copy of $HashTableNew
$SomeOtherHashTable = $HashTableNew.PSObject.Copy()
This just seems like not "the way"...
Note: I will admit, this is not the best example use of a data type extension method (as #SantiagoSquarzon points out)... but it is a simple one, and it allows for a simple example in the accepted answer; so I'm intentionally leaving it as is, rather than changing question / the extension method to .foo() returning widgets...
There is indeed a better and easier way to update a type as a whole by using Update-TypeData.
Here is an example that add an .AddOrOverwrite method to the hashtable.
$TypeParam = #{
TypeName = 'System.Collections.Hashtable'
MemberType = 'ScriptMethod'
MemberName = 'AddOrOverwrite'
Value = { Param($Key, $Value) $this.$key = $Value }
}
Update-TypeData #TypeParam -Force
$SomeHashTable.AddOrOverwrite('aaa','2222222')
$this, in the scriptblock of the method definition, correspond to the object reference that is targeted, in this case, the hashtable.
-Force will overwrite the definition every time without error stating the type was already added.
That method is not super useful as it does something that the hashtable manage pretty well on its own by just using assignment but it demonstrates how to do it.
Bonus example
Here's an example on how you would apply this principle and create 2 script properties (readonly) for a string so you can convert to base 64 back and forth.
$TypeParam = #{
TypeName = 'System.String'
MemberType = 'ScriptProperty'
Force = $true
}
Update-TypeData #TypeParam -MemberName 'Base64' -Value { [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($this)) }
Update-TypeData #TypeParam -MemberName 'Base64Decoded' -Value { [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($this)) }
# Encode the string to base 64 (Output: U29tZVN0cmluZw==)
"SomeString".Base64
# Decode the string from Base64 (Output: SomeString)
"U29tZVN0cmluZw==".Base64Decoded
References
Msdocs - About-Types
Dr Scripto - Easily Update Powershell Type Data by Using a Cmdlet

Powershell parsing of a C# file to get values of constants

I need to parse some C# files to get the values of some constant variables. I know I can do something like
$input = Get-Content C:\somefile.cs ...
then loop over each line and do some text matching.
...but was wondering whether I can utilize some sort of a C# DOM object to get the values of constants?
You can load the type dynamically from the powershell command line, and then evaluate the constant.
Example:
C:\somefile.cs contents:
public static class Foo
{
public const string SOME_CONSTANT = "StackOverflow";
}
Powershell command line:
Add-Type -Path C:\somefile.cs
$constantValue = [Foo]::SOME_CONSTANT

Preventing PowerShell from wrapping value types in PSObjects

I have a .NET API that uses a lot of delegates. My API has a couple methods similar to the following:
public static class MyClass
{
public static void DoSomethingWithString(Func<object> myFunc)
{
string myStringValue = myFunc().ToString();
Console.WriteLine(myStringValue);
}
public static void DoSomethingWithDouble(Func<object> myFunc)
{
object unparsedValue = myFunc();
double parsedValue = Convert.ToDouble(unparsedValue);
Console.WriteLine(parsedValue);
}
}
Now in PowerShell I have the following:
[MyClass]::DoSomethingWithString({ "Hello" }); # No error here
[MyClass]::DoSomethingWithDouble({ 123.4 }); # InvalidCastException - can't convert a PSObject to double
The problem is that my PowerShell scriptblock is returning a PSObject instead of the actual double value. My .NET API doesn't know anything about PowerShell, and I don't want to add a reference to PowerShell DLLs just so I can add special handling for this particular scenario.
Is there a way to get my scriptblocks to return actual value types rather than PSObjects? Or is there a PowerShell-agnostic way for my .NET library to handle PSObjects?
PowerShell will wrap things in PSObject as it sees fit, there's no clean way to avoid that when accepting arbitrary script blocks. With some discipline, you can write script blocks that unwrap the PSObject, for example the following might work fine:
[MyClass]::DoSomethingWithDouble({ (123.4).PSObject.BaseObject })
If possible, a better option is to have your api take a delegate with a more specific return type.
public static void DoSomethingWithDouble(Func<double> myFunc)
In this case, PowerShell will convert the return value to the type expected by the delegate. When the return type is PSObject, PowerShell knows PSObject trivially converts to object so it doesn't unwrap, but if the return type is pretty much anything else, PowerShell is forced to do the conversion by unwrapping the PSObject.
For completeness, another option if you're using PowerShell V3 is to use the C# keyword dynamic, something like:
public static void DoSomethingWithDouble(Func<object> myFunc)
{
dynamic unparsedValue = myFunc();
double parsedValue = (double)(unparsedValue);
Console.WriteLine(parsedValue);
}
When unparsedValue is a PSObject, PowerShell will perform the conversion from to double on the second line even though you don't reference any PowerShell assemblies in your C# code.
Note these last two options may not fit well with your real API, but they are options that are worth understanding.

Can I decorate advanced PowerShell functions with my own custom attributes?

For example:
function TestThis()
{
[MySpecialCustomAttribute]
[CmdletBinding()]
Param(...)
Process{...}
}
Yes, of course you can!
Any type derived from Attribute which allows UsageType.All (or UsageType.Class) can be used on the function itself (i.e. above Param)
Any type derived from Attribute which allows UsageType.Property or UsageType.Field usage can be used on parameters themselves or variables.
It's not uncommon to just be lazy and use UsageType.All (e.g. the built in OutputType attribute does that).
You can even write them in PowerShell classes now
using namespace System.Management.Automation
class ValidateFileExistsAttribute : ValidateArgumentsAttribute {
[void] Validate([object]$arguments, [EngineIntrinsics]$engineIntrinsics) {
if($null -eq $arguments) {
throw [System.ArgumentNullException]::new()
}
if(-not (Test-Path -Path "$arguments" -Type Leaf)) {
throw [System.IO.FileNotFoundException]::new("The specified path is not a file: '$arguments'")
}
}
}
See more examples on Kevin Marquette's blog.
There's an older example here showing how to do it in PowerShell 4 and earlier using Add-Type, although it's a little out of date now, because the particular example it shows has been integrated into PowerShell 6 and is no longer needed 😉
There are also videos