Does PowerShell call any initialization code when a module is loaded?
I am looking for something like a Perl BEGIN block, or a constructor.
Both NEW-MODULE and IMPORT-MODULE will return a PSCustomObject. I am trying to encapsulate a custom object in a module to avoid lengthy code in scripts. One method that tests well in open code is:
$m = new-module -scriptblock {
New-Object PSCustomObject |
Add-Member NoteProperty -name person -value Frodo -passthru |
Add-Member ScriptMethod Who { $this.person } -passthru |
Add-Member ScriptMethod Mod {
param($x)
$this.person = $x
} -passthru
} -ascustomobject -returnresult
Ideally I would like to drop this code into a module and use something like:
$MyObj = Import-Module -Name ".\MyPackage" -AsCustomObject
and have MyObj be a handle to an object the same as the first snippet provides.
Suggestions appreciated.
It's not clear if you want to run initialization code when a module is loaded (like Perl's BEGIN block) or if you want to create a custom class (which is what "constructor" suggests).
Initialization code in a module is easy. Any code in a module not embedded in a function is executed when the module is imported.
Creating a custom class isn't supported natively in PS. But see: http://psclass.codeplex.com/. You can also write C#, VBScript, etc. and use Add-Type.
Import-module won't work to simulate a class, because you can only have 1 instance of a module with a given name - at best you'd have a singleton class. (BTW, import-module does have a -passthru parameter, which would more or less make your last line of code work - as a singleton. You'd also have to add export-module -variable * -function * to your module code) You could use New-Module to simulate a class though. And you could wrap it in a function named, new-myClass for example.
BTW, if you use the -ASCustomObject parameter you end up with a hashtable, which doesn't support "this" (in words hash table values that are script blocks don't have a built-in way to refer to the hashtable itself). If you use new-module without -AsCustomObject (and potentially use a factory function, for example new-myclass) then you could simulate "this.varInMyModule" with & $myModule $varInMyModule. However if you create a PSCustomObject, using Add-Member, then script method have access to $this and it in general acts a lot more like a typical object with properties and methods.
Modules are really supposed to output cmdlets, not objects. A module should provide a set of related cmdlets. There is a way to send data into the module using Import-Modules's -ArgumentList parameter as show here. You could use the technique to provide a server name for your cmdlets to connect to for example. The PowerCLI module handles that differently using a cmdlet that creates a script scope connection object ($script:connection) that the other cmdlets check for and re-use if it exists similar to this:
#test.psm1
$script:myvar = "hi"
function Show-MyVar {Write-Host $script:myvar}
function Set-MyVar ($Value) {$script:myvar = $Value}
#end test.psm1
Using Modules you can export both innate properties and functions, and don't need to run them through add-member or do much acrobatics. Note however that it has some issues if you don't want to export all properties and methods, and while you can initialize properties to an initial value, you CAN'T call an internal function during initialization without doing some akward acrobatics.
But I think what you really want to do is use Classes which are now available in Powershell 5 (they weren't when you posted). I've provided examples of each.
Sysops has a decent tutorial on the new classes in 4 parts
Here's the older way before powershell 5.0
# powershell 3.0 and above (I think)
$m = new-module -ascustomobject -scriptblock `
{
$person = "Frodo"
function Who
()
{return $this.person}
function Rename
($name)
{$this.person = $name}
Export-ModuleMember -Function * -Variable *
}
write-host "direct property access: $($m.person)"
write-host "method call: $($m.who())"
$m.Rename("Shelob")
write-host "change test: $($m.who())"
Also you can replicate multiple objects from a template like this:
# powershell 3.0 and above (I think)
$template = `
{
$person = "Frodo"
function Who
()
{return $this.person}
function Rename
($name)
{$this.person = $name}
Export-ModuleMember -Function * -Variable *
}
$me = new-module -ascustomobject -scriptblock $template; $me.Rename("Shelob")
$you = new-module -ascustomobject -scriptblock $template
"Me: $($me.Who())"
"You: $($you.Who())"
And in powershell 5 you have actual classes (mostly)
#requires -version 5
Class Person
{
hidden [String] $name #not actually private
[string] Who ()
{return $this.name}
[void] Rename ([string] $name)
{$this.name = $name}
# constructors are weird though, you don't specify return type OR explicitly return value.
Person ([String]$name)
{$this.name = $name}
<#
# The above constructor code is secretly converted to this
[Person] New ([string]$name) #note the added return type and renamed to New
{
$this.name = $name
return $this #note that we are returning ourself, you can exploit this to create chained constructors like [person]::New("gandalf").withWizardLevel("White") but I haven't done so here
}
#>
}
$me = [Person]::new("Shelob")
$you = [Person]::new("Frodo")
# $me|gm # Note that Name doesn't show here
# $me.name # But we can still access it...
# $me|gm -Force # Note that Name DOES show here
"`n"
"Me: $($me.who())"
"You: $($you.who())"
$you.Rename("Dinner")
"You after we meet: $($you.who())"
Related
TLDR
I'm trying to create a function that will take a Multi-Level [PSCustomObject], extract the Key/Value pairs (strings only), and use them to declare Individual Global Variables using Set-Variable.
Current Code
Set-Variable -Name 'NSOneDrive' -Value "D:\OneDrive - New Spectrum"
$StrykerDirs = [PSCustomObject]#{
'OneDrive' = [PSCustomObject]#{
'NSOneDrive' = "D:\OneDrive - New Spectrum"
'MyOneDrive' = "D:\OneDrive"
}
'Dev' = [PSCustomObject]#{
'DevDir' = "${NSOneDrive}\Dev"
'DevToolsDir' = [PSCustomObject]#{
'DevTools' = "${NSOneDrive}\Dev\_DevTools"
'Terminals' = [PSCustomObject]#{
'DT_Terminals' = "${NSOneDrive}\Dev\_DevTools\terminals"
'DT_PowerShell' = "${NSOneDrive}\Dev\_DevTools\terminals\PowerShell"
}
'Editors' = [PSCustomObject]#{
'DT_Editors' = "${NSOneDrive}\Dev\_DevTools\.editors"
}
}
'ProjectsDir' = [PSCustomObject]#{
'NSProjects' = "${NSOneDrive}\Projects\NewSpectrum"
'MyProjects' = "${NSOneDrive}\Projects\Personal"
}
}
}
$StrykerDirs |
ConvertTo-JSON -Depth 25 |
Tee-Object -FilePath ".\JSON\Stryker-Paths.json"
function Set-DirAliases {
[CmdletBinding()]
# I might add parameters after I know how to make the 'Process' work
Begin {
# Begin Process Block
}
Process {
ForEach ( $dir in $StrykerDirs ) {
where ( $_.GetType() -eq 'String' ) |
Set-Variable -Name "${key}" -Value "${value}"
# I know ${key} and ${value} won't work, but I'm not sure how to properly fill them
}
}
End {
# End Process Block
}
}
Goals
Simplifying Set-Location Navigation
First and foremost I obviously need to figure out how to make the above Process block work. Once I do, I'll be able to easily declare Directory Variables for use with Set-Location. This is only for streamlining variable declarations so I don't have to repeatedly declare them with a messy barrage of individual Set-Variable commands while also avoiding the use of long (sometimes very long) $Object.PropertyName 'variables'.
After I get a handle on this script, I'll be able to finish several other scripts and functions that use (more or less) the same basic process.
Add to $PROFILE
This particular script is going to be part of a 'Startups' section in my default $PROFILE (Microsoft.PowerShell_profile.ps1) so I can set the Directory Variables in-bulk and keep the $PROFILE script itself nice and clean.
The other scripts that I mentioned anbove are also going to be included in my $PROFILE Startups.
JSON Output
The script also exports a .json file so that, among other things, I can (hopefully) repeat the process down the road in my WSL Bash Profiles.
Param() Functionality
Eventually I want to add a Param() block so the function can be used outside of the script as well.
In the below sample module file, is there a way to pass the myvar value while importing the module.
For example,
import-module -name .\test.psm1 -?? pass a parameter? e.g value of myvar
#test.psm1
$script:myvar = "hi"
function Show-MyVar {Write-Host $script:myvar}
function Set-MyVar ($Value) {$script:myvar = $Value}
#end test.psm1
(This snippet was copied from another question.)
This worked for me:
You can use the –ArgumentList parameter of the import-module cmdlet to pass arguments when loading a module.
You should use a param block in your module to define your parameters:
param(
[parameter(Position=0,Mandatory=$false)][boolean]$BeQuiet=$true,
[parameter(Position=1,Mandatory=$false)][string]$URL
)
Then call the import-module cmdlet like this:
import-module .\myModule.psm1 -ArgumentList $True,'http://www.microsoft.com'
As may have already noticed, you can only supply values (no names) to –ArgumentList. So you should define you parameters carefully with the position argument.
Reference
The -ArgumentList parameter of Import-Module unfortunately does not accept a [hashtable] or [psobject] or something. A list with fixed postitions is way too static for my liking so I prefer to use a single [hashtable]-argument which has to be "manually dispatched" like this:
param( [parameter(Mandatory=$false)][hashtable]$passedVariables )
# this module uses the following variables that need to be set and passed as [hashtable]:
# BeQuiet, URL, LotsaMore...
$passedVariables.GetEnumerator() |
ForEach-Object { Set-Variable -Name $_.Key -Value $_.Value }
...
The importing module or script does something like this:
...
# variables have been defined at this point
$variablesToPass = #{}
'BeQuiet,URL,LotsaMore' -split ',' |
ForEach-Object { $variablesToPass[$_] = Get-Variable $_ -ValueOnly }
Import-Module TheModule -ArgumentList $variablesToPass
The above code uses the same names in both modules but you could of course easily map the variable names of the importing script arbitrarily to the names that are used in the imported module.
When I use another object in the .net-Framework in C# I can save a lot of typing by using the using directive.
using FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It;
...
var blurb = new Thingamabob();
...
So is there a way in Powershell to do something similiar? I'm accessing a lot of .net objects and am not happy of having to type
$blurb = new-object FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It.Thingamabob;
all the time.
There's really nothing at the namespace level like that. I often assign commonly used types to variables and then instantiate them:
$thingtype = [FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It.Thingamabob];
$blurb = New-Object $thingtype.FullName
Probably not worth it if the type won't be used repeatedly, but I believe it's the best you can do.
PowerShell 5.0 (included in WMF5 or Windows 10 and up), adds the using namespace construct to the language. You can use it in your script like so:
#Require -Version 5.0
using namespace FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It
$blurb = [Thingamabob]::new()
(The #Require statement on the first line is not necessary to use using namespace, but it will prevent the script from running in PS 4.0 and below where using namespace is a syntax error.)
Check out this blog post from a couple years ago: http://blogs.msdn.com/richardb/archive/2007/02/21/add-types-ps1-poor-man-s-using-for-powershell.aspx
Here is add-types.ps1, excerpted from that article:
param(
[string] $assemblyName = $(throw 'assemblyName is required'),
[object] $object
)
process {
if ($_) {
$object = $_
}
if (! $object) {
throw 'must pass an -object parameter or pipe one in'
}
# load the required dll
$assembly = [System.Reflection.Assembly]::LoadWithPartialName($assemblyName)
# add each type as a member property
$assembly.GetTypes() |
where {$_.ispublic -and !$_.IsSubclassOf( [Exception] ) -and $_.name -notmatch "event"} |
foreach {
# avoid error messages in case it already exists
if (! ($object | get-member $_.name)) {
add-member noteproperty $_.name $_ -inputobject $object
}
}
}
And, to use it:
RICBERG470> $tfs | add-types "Microsoft.TeamFoundation.VersionControl.Client"
RICBERG470> $itemSpec = new-object $tfs.itemspec("$/foo", $tfs.RecursionType::none)
Basically what I do is crawl the assembly for nontrivial types, then write a "constructor" that uses Add-Member add them (in a structured way) to the objects I care about.
See also this followup post: http://richardberg.net/blog/?p=38
this is just a joke, joke...
$fullnames = New-Object ( [System.Collections.Generic.List``1].MakeGenericType( [String]) );
function using ( $name ) {
foreach ( $type in [Reflection.Assembly]::LoadWithPartialName($name).GetTypes() )
{
$fullnames.Add($type.fullname);
}
}
function new ( $name ) {
$fullname = $fullnames -like "*.$name";
return , (New-Object $fullname[0]);
}
using System.Windows.Forms
using FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It
$a = new button
$b = new Thingamabob
Here's some code that works in PowerShell 2.0 to add type aliases. But the problem is that it is not scoped. With some extra work you could "un-import" the namespaces, but this should get you off to a good start.
##############################################################################
#.SYNOPSIS
# Add a type accelerator to the current session.
#
#.DESCRIPTION
# The Add-TypeAccelerator function allows you to add a simple type accelerator
# (like [regex]) for a longer type (like [System.Text.RegularExpressions.Regex]).
#
#.PARAMETER Name
# The short form accelerator should be just the name you want to use (without
# square brackets).
#
#.PARAMETER Type
# The type you want the accelerator to accelerate.
#
#.PARAMETER Force
# Overwrites any existing type alias.
#
#.EXAMPLE
# Add-TypeAccelerator List "System.Collections.Generic.List``1"
# $MyList = New-Object List[String]
##############################################################################
function Add-TypeAccelerator {
[CmdletBinding()]
param(
[Parameter(Position=1,Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
[String[]]$Name,
[Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true)]
[Type]$Type,
[Parameter()]
[Switch]$Force
)
process {
$TypeAccelerators = [Type]::GetType('System.Management.Automation.TypeAccelerators')
foreach ($a in $Name) {
if ( $TypeAccelerators::Get.ContainsKey($a) ) {
if ( $Force ) {
$TypeAccelerators::Remove($a) | Out-Null
$TypeAccelerators::Add($a,$Type)
}
elseif ( $Type -ne $TypeAccelerators::Get[$a] ) {
Write-Error "$a is already mapped to $($TypeAccelerators::Get[$a])"
}
}
else {
$TypeAccelerators::Add($a, $Type)
}
}
}
}
If you just need to create an instance of your type, you can store the name of the long namespace in a string:
$st = "System.Text"
$sb = New-Object "$st.StringBuilder"
It's not as powerful as the using directive in C#, but at least it's very easy to use.
Thanks everybody for your input. I've marked Richard Berg's contribution as an answer, because it most closely resembles what I'm looking for.
All your answers brought me on the track that seems most promising: In his blog post Keith Dahlby proposes a Get-Type commandlet that allows easy consutruction of types for generic methods.
I think there is no reason against exetending this to also search through a predefined path of assemblies for a type.
Disclaimer: I haven't built that -- yet ...
Here is how one could use it:
$path = (System.Collections.Generic, FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It)
$type = get-type -Path $path List Thingamabob
$obj = new-object $type
$obj.GetType()
This would result in a nice generic List of Thingamabob. Of course I'd wrap up everthing sans the path definition in just another utility function. The extended get-type would include a step to resolve any given type agains the path.
#Requires -Version 5
using namespace System.Management.Automation.Host
#using module
I realize this is an old post, but I was looking for the same thing and came across this: http://weblogs.asp.net/adweigert/powershell-adding-the-using-statement
Edit: I suppose I should specify that it allows you to use the familiar syntax of...
using ($x = $y) { ... }
We have a Powershell script which loads some configuration for our application from a json file via something like:
$ourApplicationSettings=Get-Content -Raw -Path $EnvironmentFile | ConvertFrom-Json
Now that I have an object which contains all of the settings, I'd like to create a handful of functions which can operate on either the $settings object directly, or some portion of it.
The best practices articles I've read for Powershell state that functions should be of the form: Verb-Noun, which sounds like developers would be expected to write functions like:
Get-OurAppSourceDirectory $ourApplicationSettings
DoSomething-OurApp $ourApplicationSettings
This seems very counter intuitive as it means that there is no way to easily find all of the functions associated with OurApp.
One article proposes one possible way would be to use a function like:
function New-OurAppConfig {
$appConfig = Get-Content -Raw -Path $EnvironmentFile | ConvertFrom-Json
$appConfig
}
but this way I'm not sure how to add member functions so that I could write:
$config = New-OurAppConfig
$config.Get-SrcDirectory
$config.Invoke-ActionABC
Well, you can search for that. Get-Command *ourapp*. So that one is not true. As for the second question:
$config = New-OutAppConfig
$var = Get-SrcDirectory $config
Invoke-ActionABC $var
or alternatively you can do a meta function that calls all those functions, so you could just call it once and thats it.
Also, it seems like what you are trying to achieve can be done with classes in PowerShell. https://blogs.technet.microsoft.com/heyscriptingguy/2015/09/04/adding-methods-to-a-powershell-5-class/
You can use Add-Member to add script methods to an existing object:
$foo = '{"value":23}' | ConvertFrom-Json
$foo | Add-Member -Type ScriptMethod -Name Multiply -Value {
Param($factor)
$this.value * $factor
}
$foo.Multiply(2) # output: 46
$foo.Multiply(3) # output: 69
That approach is a little awkward, though, both because PowerShell prior to v5 wasn't really built for full OO and because calling methods on a configuration object feels odd.
Generally it's more PoSh to read your configuration once at the beginning of your script and then either use it as a global singleton:
$cfg = Get-Content 'C:\path\to\your.json' -Raw | ConvertFrom-Json
function Get-Something {
...
Invoke-Other $cfg.Whatever
...
}
Get-Something
or make the configuration a mandatory parameter for your functions:
$cfg = Get-Content 'C:\path\to\your.json' -Raw | ConvertFrom-Json
function Get-Something {
Param(
[Parameter(Mandatory=$true)
$Config
)
...
Invoke-Other $Config.Whatever
...
}
Get-Something -Config $cfg
I am trying to test some custom DSC resources written as a class with the help of pester. However I am struggling to work out how to make the class available to a different file. This is not a pester issue really, I can not do this in powershell either. I have a module with a class in it much like the following in a SxDeployment.psm1 file
[DscResource()]
class SxWindowsService {
[void]Set(){
}
[bool]Test(){
return $true;
}
[SxWindowsService]Get(){
return $this
}
}
This module has a corresponding .psd1, which is listing the SxWindowsService class as a 'DscResourcesToExport'. It is being registered as a module, I can see this module when I do a Get-Module -ListAvailable and can also perform an Import-Module upon it.
I suppose my question really is how can I create a a reference to this class from another file?
Say I have a different file test.ps1 with the following
Import-Module SxDeployment
$class = [SxWindowsService]::new()
I get the following error, "Unable to find type [SxWindowsService]."
UPDATE
After a little more tinkering, I can create an instance of the class by changing the .psm1 file to a .ps file and changing the import-module statement for . .\SxDeployment.ps1. So it seems the problem is how do you consume resources defined in a DSC resource module file outside of a DSC configuration?
The type literal [SxWindowsService] won't be accessible outside the module. This is by design. Here is a quote from the release notes:
Class keyword >>> Defines a new class. This is a true .NET Framework type.
Class members are public, but only public within the module scope. You can't refer to the type name as a string (for example, New-Object doesn't work), and in this release, you can't use a type literal (for example, [MyClass]) outside the script/module file in which the class is defined.
Edit:
Well after having said the above. Since there is a bit of a mismatch between object activation in .NET and PowerShell then I thought to myself, there might just be a way. And I found it:
Import-Module C:\ps\StackOverflow\32743812\issue.psm1 -Force
function New-PSClassInstance {
param(
[Parameter(Mandatory)]
[String]$TypeName,
[object[]]$ArgumentList = $null
)
$ts = [System.AppDomain]::CurrentDomain.GetAssemblies() |
Where-Object Location -eq $null |
Foreach-Object {
$_.Gettypes()
} | Where-Object name -eq $TypeName |
Select-Object -Last 1
if($ts) {
[System.Activator]::CreateInstance($ts,$ArgumentList )
} else {
$typeException = New-Object TypeLoadException $TypeName
$typeException.Data.Add("ArgumentList",$ArgumentList)
throw $typeException
}
}
New-PSClassinstance -TypeName SxWindowsService