Is it possible to have a module function that modifies parameter values of the caller script? - function

Motivation
Reduce the maintenance of an Azure DevOps task that invokes a Powershell script with a lot of parameters ("a lot" could be 5).
The idea relies on the fact that Azure DevOps generates environment variables to reflect the build variables. So, I devised the following scheme:
Prefix all non secret Azure DevOps variables with MyBuild.
The task powershell script would call a function to check the script parameters against the MyBuild_ environment variables and would automatically assign the value of the MyBuild_xyz environment variable to the script parameter xyz if the latter has no value.
This way the task command line would only contain secret parameters (which are not reflected in the environment). Often, there are no secret parameters and so the command line remains empty. We find this scheme to reduce the maintenance of the tasks driven by a powershell script.
Example
param(
$DBUser,
[ValidateNotNullOrEmpty()]$DBPassword,
$DBServer,
$Configuration,
$Solutions,
$ClientDB = $env:Build_DefinitionName,
$RawBuildVersion = $env:Build_BuildNumber,
$BuildDefinition = $env:Build_DefinitionName,
$Changeset = $env:Build_SourceVersion,
$OutDir = $env:Build_BinariesDirectory,
$TempDir,
[Switch]$EnforceNoMetadataStoreChanges
)
$ErrorActionPreference = "Stop"
. $PSScriptRoot\AutomationBootstrap.ps1
$AutomationScripts = GetToolPackage DevOpsAutomation
. "$AutomationScripts\vNext\DefaultParameterValueBinding.ps1" $PSCommandPath -Required 'ClientDB' -Props #{
OutDir = #{ DefaultValue = [io.path]::GetFullPath("$PSScriptRoot\..\..\bin") }
TempDir = #{ DefaultValue = 'D:\_gctemp' }
DBUser = #{ DefaultValue = 'SomeUser' }
}
The described parameter binding logic is implemented in the script DefaultParameterValueBinding.ps1 which is published in a NuGet package. The code installs the package and thus gets access to the script.
In the example above, some parameters default to predefined Azure Devops variables, like $RawBuildVersion = $env:Build_BuildNumber. Some are left uninitialized, like $DBServer, which means it would default to $env:MyBuild_DBServer.
We can get away without the special function to do the binding, but then the script author would have to write something like this:
$DBServer = $env:MyBuild_DBServer,
$Configuration = $env:MyBuild_Configuration,
$Solutions = $env:MyBuild_Solutions,
I wanted to avoid this, because of the possibility of an accidental name mismatch.
The Problem
The approach does not work when I package the logic of DefaultParameterValueBinding.ps1 into a module function. This is because of the module scope isolation - I just cannot modify the parameters of the caller script.
Is it still possible to do? Is it possible to achieve my goal in a more elegant way? Remember, I want to reduce the cost associated with maintaining the task command line in Azure DevOps.
Right now I am inclined to retreat back to this scheme:
$xyz = $(Resolve-ParameterValue 'xyz' x y z ...)
Where Resolve-ParameterValue would first check $env:MyBuild_xyz and if not found select the first not null value out of x,y,z,...
But if the Resolve-ParameterValue method comes from a module, then the script must assume the module has already been installed, because it has no way to install it before the parameters are evaluated. Or has it?
EDIT 1
Notice the command line used to invoke the DefaultParameterValueBinding.ps1 script does not contain the caller script parameters! It does include $PSCommandPath, which is used to obtain the PSBoundParameters collection.

Yea, but it will require modifications to the calling script and the function. Pass the parameters by reference. Adam B. has a nice piece on passing parameters by reference in the following:
https://mcpmag.com/articles/2015/06/04/reference-variables-in-powershell.aspx
Net-net, the following is an example:
$age = 12;
function birthday {
param([ref]$age)
$age.value += 1
}
birthday -age ([ref]$age)
Write-Output $age
I've got an age of 12. I pass it into a function as a parameter. The function increments the value of $age by 1. You can do the same thing with a function in a module. You get my drift.

Related

How do I get a variable from a module run space in PowerShell?

There is a module that has an "initialize" function that sets a variable that gets used in other scripts/functions in the module to validate that the initialize function was run. Something like
Start-InitializeThing
Connect to the API
$Script:SNOWinit = $true
Then in another script/function it will check:
if ($Script:SNOWinit -eq $true) { Do the thing!}
Is there a way to grab that $Script:SNOWinit in the same PowerShell window, but not the same module?
I want to run the same check but for a different function that is not in the module.
Can I do this, can I "dig" into like the modules runspace and check that variable. I don't have the means to edit the functions in the module so I cant change what type of variable is set once the initialize script has run.
Assuming that the module of interest is named foo and that it has already been imported (loaded):
. (Get-Module foo) { $SNOWinit }
If you want to import the module on demand:
. (Import-Module -PassThru foo) { $SNOWinit }
The above returns the value of the $SNOWinit variable defined in the root scope of module foo.
See this blog post for background information.
Note that it is generally not advisable to use this technique, because it violates the intended encapsulation that modules provide. In the case at hand, $SNOWinit, as a non-public module variable, should be considered an implementation detail, which is why you shouldn't rely on its presence in production code.
From the bible, WPiA. More mysterious uses for the call operator.
# get a variable in module scope
$m = get-module counter
& $m Get-Variable count
& $m Set-Variable count 33

What's the proper way to create a case-loop/switch-statement-type PowerShell function?

Because of the possibility that I'm really stuck on an X-Y problem here, what I'm actually doing is writing a rather large script to handle user administration for on-prem and cloud-based users on a Windows platform. This means a handful of on-prem services, authenticated using Active Directory credentials, and a handful of cloud-based services, using Azure Active Directory credentials. Ideally, there would be one set of credentials that could perform all the actions, but I can't rely on that, which leaves me looking for a way to get a number of credentials (I'm up to about 8) that will then be sent on to the appropriate Connect-[Service] function I've written.
I think the proper way to write this function is with a switch statement, along the lines of:
Function Get-Credentials {
Param (
[Parameter(Mandatory=$true)][string]$Service
)
$global:Service1Credentials = $null;
$global:Service2Credentials = $null;
...
switch ($Service)
{
Service1 { $global:Service1Credentials = Get-Credential -Message "Credentials for [Service1]"}
Service2 { $global:Service2Credentials = Get-Credential -Message "Credentials for [Service2]"}
...
}
}
Ignoring the use of global parameters (which I'm doing for better debugability while I write the script), is this the proper approach to the problem of getting credentials for a script to use against several to a dozen different services, or is there a better approach I should be using?
Here is an alternative that will be a little less cumbersome as the switch options grow:
function Get-SvcCred($ServiceName)
{
return (Get-Credential -Message "Credentials for $ServiceName")
}
$services = "Service1", "Service2", "Service3"
$creds = #{}
$services |
ForEach-Object {
$creds[$_] = Get-SvcCred $_
}
Now, all you have to do is update the $services array as new ones are added and this will correctly ask for each one in turn. The added benefit is that you can easily access any set of credentials later in the code by doing something like this:
$creds["Service1"]
Obviously, if you don't want (or need) the user to give all the credentials at once, you can simply make each call as and when required:
$creds["Service3"] = Get-SvcCred "Service3"

Do powershell parameters need to be at the front of the script?

I'm trying to have a script with both executable code and a function, like the following:
function CopyFiles {
Param( ... )
...
}
// Parameter for the script
param ( ... )
// Executable code
However, I run into the following error: "The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept assignments, such as a variable or a property"
When I list my function at the end of the file, it says that the function name is undefined. How do I call a powershell function from executable code within the same script?
The correct order is:
1.Script parameters
# Parameter for the script
param([string]$foo)
2.Function definitons
function CopyFiles {
Param([string]$bar)
...
}
3.Script code
# Executable code
CopyFiles $foo $bar
Why would you want it any other way?
Parameters go first always. I had a similar issue at one point in time with providing parameter input to a script. Your script should go:
param ( . . . )
# functions
# script body
For some reason, the PowerShell parsing engine doesn't appreciate the param keyword not being on the first line of a script, not counting comment lines. You can also do this:
param (
# params must be at the top of the file
)
You can also check to see if your parameters have been declared, or if they have the input you want, using Get-Variable. One other thing; if you want to cast data to a certain type, such as System.Boolean, I would do it AFTER the param block, and BEFORE functions. If you type-cast something to System.Boolean in the parameter declaration, then you'll have errors if people running your script don't submit the input argument in a Boolean value, which is much harder than using the .NET System.Convert static method to convert the value afterwards, and checking to see what it evaluated to.

How to check for functions dependency in powershell scripts. Avoid running the same function multiple times

In my PowerShell script - one function's output is another function's input. For Eg: Function CreateReport($x) cannot run until unless the Function ParseXml($x) runs. What if a user directly runs the 2nd function before running the 1st. How can I check if 1st function is already run to continue with 2nd, i.e, first run the 1st function (generate the txt file) then run the 2nd? if first func is already run do not re-run it.
For Eg: Suppose I have a TestFunc.ps1 file having 2 functions as below
$X = "C:\XmlPath\file1.xml"
Function ParseXml($X)
{
#Read xml and output contents in a txt file
}
#This function should execute only after the function Parsexml($X) and if Pasrsexml() has run before and generated the output, it shouldnot be allowed to re-run here
Function CreateReport($T)
{
#from the txtfile Create csv
}
According to this and your other question How to alias a parameterized function as a flag in powershell script? you are trying to implement a so called build script. Instead of inventing a wheel (implementing task dependencies, watching tasks to be run once, etc.) take a look at some already implemented tools like psake or Invoke-Build. They are designed for PowerShell and they do exactly what you want (run specified task sets, maintain task dependencies, run tasks once, etc.). These tools require a little bit of learning, of course, but in a long run they are worth to be learned.
If ParseXml function output a file, you can, in the CreateReport function, test for the existence of this file with Test-Path cmdlet:
if exists continue with CreateReport function else call the ParseXml function before continue.
Use a flag. Set the flag in ParseXml function and check it in the CreateReport function. If the flag isn't set, print an error and exit, othervise run the reporting code. Remember to clear the flag when the process is complete.
You can use a flag variable. For more persistent flags, consider using flag files or setting the flag in a database.

Conditional PowerShell parameters

Is there a way to have some of the parameters mandatory based on some condition (for example, if one of the parameters is absent or false) in a PowerShell function?
My idea is to be able to call a function in two ways. A concrete example is a function that gets a list from SharePoint - I should be able to call it with relative URL of the list (one and only parameter) OR with a web URL and a list display name (two parameters, both mandatory, but only if list relative URL is not used).
As Christian indicated, this can be accomplished via ParameterSetNames. Take a look at this example:
function Get-MySPWeb {
[CmdletBinding(DefaultParameterSetName="set1")]
param (
[parameter(ParameterSetName="set1")] $RelativeUrl,
[parameter(ParameterSetName="set2")] $WebUrl,
[parameter(ParameterSetName="set2", Mandatory=$true)] $DisplayName
)
Write-Host ("Parameter set in action: " + $PSCmdlet.ParameterSetName)
Write-Host ("RelativeUrl: " + $RelativeUrl)
Write-Host ("WebUrl: " + $WebUrl)
Write-Host ("DisplayName: " + $DisplayName)
}
If you run it with -RelativeUrl Foo it will bind to "set1". If you call this function without any parameters it will also bind to "set1".
(Note - when no parameters are provided in PowerShell v3 (with Windows 8 consumer preview) it will bind to "set1", however it will error binding in PowerShell v2 unless you add [CmdletBinding(DefaultParameterSetName="set1")] to the parameter block. Thanks #x0n for the DefaultParameterSetName tip!)
If you try to run it with a parameter value from both sets you will get an error.
If you run it with -WebUrl Bar it will prompt you for a parameter value for DisplayName, because it's a mandatory parameter.
There is a much more powerful option, called dynamic parameters, which allows to dynamically add parameters depending on the value of other parameters or any other condition.
You must structure your script in a different way, declaring the regular parameters as usual, and including a DynamicParam block to create dynamic parameters, a Begin block to initialize variables using the dynamic parameters , and a Process block with the code run by the script, which can use regular parameters, and variables initialized in Begin. It looks like this:
param(
# Regular parameters here
)
DynamicParam {
# Create a parameter dictionary
$runtimeParams = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Populate it with parameters, with optional attributes
# For example a parameter with mandatory and pattern validation
$attribs = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$mandatoryAttrib = New-Object System.Management.Automation.ParameterAttribute
$mandatoryAttrib.Mandatory = $true
$attribs.Add($mandatory)
$patternAttrib = New-Object System.Management.Automation.ValidatePatternAttribute('your pattern here')
$attribs.Add($patternAttrib)
# Create the parameter itself with desired name and type and attribs
$param = New-Object System.Management.Automation.RuntimeDefinedParameter('ParameterName', String, $attribs)
# Add it to the dictionary
$runtimeParams.Add('ParameterName', $param)
# Return the dictionary
$ruintimeParams
}
Begin {
# If desired, move dynamic parameter values to variables
$ParameterName = $PSBoundParameters['ParameterName']
}
Process {
# Implement the script itself, which can use both regular an dynamic parameters
}
Of course, the interesting part is that you can add conditions on the DynamicParam section and the Begin section to create different parameters depending on anything, for example other parameter values. The dynamic parameters can have any name, type (string, int, bool, object...) an attributes (mandatory, position, validate set...), and they are created before the execution of the script so that you get parameter tab completion (IntelliSense) in any environment which supports it, like the PowerShell console, the PowerShell ISE or the Visual Studio Code editor.
A typical example would be to create a different set of dynamic parameters depending on the value of a regular parameter, by using a simple if in the DynamicParam section.
Google "PowerShell dynamic parameters" for extra information, like showing help for dynamic parameters. For example:
PowerShell Magazine Dynamic Parameters in PowerShell
You need to use parameters set naming.
You can assign an exclusive parameter to a different parameter set name.