unexpected side-effect of 'using namespace' in psm1 - powershell

(PS 5.1) I put a using namespace directive in a psm1 to shorten some code. It then no longer recognizes a custom type I defined. Without the directive, all is peachy. It seems that a using namespace directive overrides things I did not expect it to.
Example: I have a main.ps1, and two modules.
main.ps1
using module .\classes.psm1 # using modules is needed to bring in classes, Import-Module won't cut it
Import-Module .\process.psm1 -Force # force reload
$MyList = Get-List # call function in module
$MyList[0].Bar # show a result
classes.psm1
Class Foo
{
[int]$Bar
}
process.psm1
function Get-List {
$f = New-Object Foo
$f.Bar = 42
$list = [System.Collections.Generic.List[Foo]]::new()
$list.Add($f)
$list
}
This works fine. The trouble starts when I want to shorten things in process.psm1:
using namespace System.Collections.Generic
function Get-List {
$f = New-Object Foo
$f.Bar = 42
$list = [List[Foo]]::new() # I just want a shorter way to declare a list
$list.Add($f)
$list
}
This complains about not recognizing type Foo. Why? (When I bring in using module .\classes.psm1 in process.psm1, all is fine again.)
My point/question is: how does using namespace affect a module's capability to recognize other modules/files within a 'solution'? It find it rather counter-intuitive, but I am not a PS expert.

By default, PowerShell defers type name resolution until runtime - unless a script file contains at least one of the following:
A using statement
A class definition
An enum definition
Without the using namespace System.Collections.Generic statement, PowerShell has no reason to attempt resolving any type names in process.psm1 until after [Foo] has been loaded from classes.psm1 and is resolvable/visible.
With the using ... statement on the other hand, PowerShell attempts to resolve the [Foo] type parameter in [List[Foo]] at parse-time - before process.psm1 has actually been loaded into main.ps1, and so the error you see is thrown.
As you've found, adding an explicit using module reference to the module file containing [Foo] solves it, as process.psm1 then no longer depends on the callers type resolution scope (which is not accessible at parse-time)

Related

PowerShell HtmlAgilityPack within class method

I use HtmlAgilityPack in my PowerShell script. As per the documentation my code is like:
[Reflection.Assembly]::LoadFile("d:\Apl\HtmlAgilityPack.1.11.29\lib\Net40\HtmlAgilityPack.dll")
[HtmlAgilityPack.HtmlDocument]$htmlDoc = #{}
$htmlDoc.LoadHtml($resp.content)
...
This works fine as expected as long as I use simple script with functions only (no classes). Now I have refactored my code so that the [HtmlAgilityPack.HtmlDocument] type is used within a class method.
Now the compiler complains "Unable to find type [HtmlAgilityPack.HtmlDocument].". How do I load/import the type in order to be able to use it within a class method?
I use PowerShell 7.0 in Windows 10 environment.
I have found the solution
Add-Type -path "d:\Apl\HtmlAgilityPack.1.11.29\lib\Net40\HtmlAgilityPack.dll"
$htmlDoc = New-Object HtmlAgilityPack.HtmlDocument

Global constants in Powershell

I'm refactoring some of my older PS scripts to a) improve them b) clean up c) modularize.
In the script I'm working now there are 10-15 functions that work on specific directory - let's call it work directory. Currently it's defined globally and loaded from a config file; it never changes after initialization (does that make it a constant?).
I want to wrap some of the functions in a separate module. The question is: should I rewrite them so the variable is passed explicitly as a parameter, or can I leave it as is, with the assumption that every script I use this module (library?) in will have this variable initialized? If the latter, how to make sure the module can "detect" the variable is uninitialized and throw some error?
And, last but not least, currently it's just a variable - should I use some specific construct so that it's obvious it is global, and not to be modified?
should I rewrite them so the variable is passed explicitly as a parameter
As long as there's no legitimate use case for overriding it in a single call, I wouldn't pass it as a parameter.
If your functions are packaged as a module, I'd strongly recommend utilizing module-scoped variables rather than globals.
Assuming you're talking about a script module, this is as simple as:
Set-Variable -Scope Script -Name ModuleTargetDirectory -Value $config.TargetDirectory
from inside the module file or a module function that runs during import (the script: scope is the same as module-scope inside a module), and then in the consuming functions:
function Get-Something
{
# ...
$targetDirectory = $script:ModuleTargetDirectory
# ...
}
Or wrap the entire config storage in a private helper method:
# don't export this function
function Get-MyModuleConfig
{
# assuming we stored a dictionary or custom object with the config options in a module-scoped variable named `config`
return $script:config
}
And then always just call $config = Get-MyModuleConfig in the begin block of functions that need access to the config data

get powershell variable name from actual variable

I am trying to figure out how to get the name of a powershell variable from the object, itself.
I'm doing this because I'm making changes to an object passed by reference into a function, so I don't know what the object will be and I am using the Set-Variable cmdlet to change that variable to read only.
# .__NEEDTOGETVARNAMEASSTRING is a placeholder because I don't know how to do that.
function Set-ToReadOnly{
param([ref]$inputVar)
$varName = $inputVar.__NEEDTOGETVARNAMEASSTRING
Set-Variable -Name $varName -Option ReadOnly
}
$testVar = 'foo'
Set-ToReadOnly $testVar
I've looked through a lot of similar questions and can't find anything that answers this specifically. I want to work with the variable entirely inside of the function--I don't want to rely on passing in additional information.
Also, while there may be easier/better ways of setting read-only, I have been wanting to know how to reliably pull the variable name from a variable for a long time, so please focus solving that problem, not my application of it in this example.
Mathias R. Jessen's helpful answer explains why the originating variable cannot be reliably determined if you only pass its value.
The only robust solution to your problem is to pass a variable object rather than its value as an argument:
function Set-ToReadOnly {
param([psvariable] $inputVar) # note the parameter type
$inputVar.Options += 'ReadOnly'
}
$testVar = 'foo'
Set-ToReadOnly (Get-Variable testVar) # pass the variable *object*
If your function is defined in the same scope as the calling code - which is not true if you the function is defined in a (different) module - you can more simply pass just the variable name and retrieve the variable from the parent / an ancestral scope:
# Works ONLY when called from the SAME SCOPE / MODULE
function Set-ToReadOnly {
param([string] $inputVarName)
# Retrieve the variable object via Get-Variable.
# This will implicitly look up the chain of ancestral scopes until
# a variable by that name is found.
$inputVar = Get-Variable $inputVarName
$inputVar.Options += 'ReadOnly'
}
$testVar = 'foo'
Set-ToReadOnly testVar # pass the variable *name*
As noted in this answer to a similar question, what you're asking (resolving the identity of a variable based on its value) can not be done reliably:
The simple reason being that contextual information about a variable
being referenced as a parameter argument will have been stripped away
by the time you can actually inspect the parameter value inside the
function.
Long before the function is actually called, the parser will have
evaluated the value of every single parameter argument, and
(optionally) coerced the type of said value to whatever type is
expected by the parameter it's bound to.
So the thing that is ultimately passed as an argument to the function
is not the variable $myVariable, but the (potentially coerced) value
of $myVariable.
What you could do for reference types is simply go through all variables in the calling scope and check if they have the same value:
function Set-ReadOnlyVariable {
param(
[Parameter(Mandatory=$true)]
[ValidateScript({ -not $_.GetType().IsValueType })]
$value
)
foreach($variable in Get-Variable -Scope 1 |Where-Object {$_.Value -ne $null -and $_.Value.Equals($value)}){
$variable.Options = $variable.Options -bor [System.Management.Automation.ScopedItemOptions]::ReadOnly
}
}
But this will set every single variable in the callers scope with that value to readonly, not just the variable you referenced, and I'd strongly recommend against this kind of thing - you're most likely doing something horribly wrong if you need to do this

How can I specify the type of a parameter when the object type is from a web service?

I'm writing a PowerShell module. I have a Get-MyPerson function which accepts an Identity parameter, calls a web service and returns an object of type PERSON (the return type from the web service).
I'm now working on a Set-MyPerson object to update a couple of properties. What I want to be able to do is:
Set-MyPerson 1234 -GolfHandicap 22
Get-MyPerson JDoe | Set-MyPerson -GolfHandicap 22
(the latter following Get-ADUser | Set-ADUser usage)
This requires Set-MyPerson to accept a parameter of type string for the former and a parameter of type Person for the latter, using parameter sets to distinguish.
I have the basic functionality working for a string but am struggling with the parameter for Person objects.
[Parameter(ParameterSetName="Person",Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[PERSON]$Person,
won't work because PowerShell doesn't recognize PERSON (as expected):
Set-MyPerson : Unable to find type [PERSON]: make sure that the assembly containing this type is loaded.
How can I get PowerShell to recognize my PERSON class?
Do you try with [object] or [psbject] ?
My own solution, which came to me in a moment of echoey isolation, was rather more hassle than #JPBlanc's:
I used the WSDL command to generate a CSharp file:
wsdl http://server.dns.name/webservice/path/service?wsdl
Then I used the CSharp command-line compiler to create an assembly:
csc /target:library PersonService.cs
which created a DLL called PersonService.dll.
And then used:
$assemblyPath = "C:\Windows\System32\WindowsPowerShell\v1.0\Modules\PersonModule\PersonService.dll"
Add-Type -Path $assemblyPath
to load it.

PowerShell - Shorten namespace names so it's easier to access types

Is there a method of shortening PowerShell namespace references?
Typing [RootNameSpace1.NameSpace2.Namepsace3+SomeEnum]::SomeValue is taxing and not a very good user expierence. I realize that you can reference System level objects without a namespace such that [Type]::GetType(... will work. Is there some manifest I could create or command I could use to shorten lengthy namespaces?
Any methods accepting Enums will accept strings, but this is for Enums only and where there is no ambiguity (meaning there are no other overloads with a signature matching strings in this fashion.)
If you're on powershell v2.0, you can (ab)use Type Accelerators. I blogged about this before, and Joel Bennett wrapped up my technique in a handy script:
http://poshcode.org/1869
UPDATE (2020): this link is broken, but for current versions of powershell there is an easier way.
using namespace System.Collections.Generic;
$list = new-object List # also: $list = [list]::new()
-Oisin
Lengthy types can be assigned to variables and then used via those variables:
# enum values
$rvk = [Microsoft.Win32.RegistryValueKind]
$rvk::Binary
$rvk::DWord
# static members
$con = [System.Console]
$con::CursorLeft
$con::WriteLine('Hello there')
# just to be sure, look at types
.{
$rvk::Binary
$con::WriteLine
$con::CursorLeft
} |
% { $_.GetType() }