How to organize functions into script files to allow call from command line? - function

I have an Advanced Function with a number of helper functions in a .ps1 script file.
How do organize my functions so I can call AdvFunc from the command line and not break the ability of AdvFunc to use the helper functions?
Abbreviated contents of script.ps1:
Function AdvFunc {
[cmdletbinding(DefaultParameterSetName='Scheduled')]
Param (
[Parameter(ValueFromPipeline=$true, ParameterSetName='Scheduled')]$SomeValue
)
Begin {
$value = Helper1 $stuff
}
Process {
# do stuff
}
End {
# call Helper2
}
}
Helper1 {
Param ($stuff)
# do stuff
Return $valueForAdvFunc
}
Helper2 {
# do other stuff
}
# Entry Point
$collection | AdvFunc
script.ps1 is currently launched by a scheduler and processes pre-defined $collection as expected.
The problem is I need to call AdvFunc from the command like with a different parameter set. I've add the AdHoc Parameter Set below. This will be used to send a different collection to AdvFunc. As I understand things, this means the first lines of script.ps1 will now need to be:
Param (
[Parameter(ValueFromPipeline=$true, ParameterSetName='Scheduled')]$SomeValue,
[Parameter(ParameterSetName='AdHoc')][string]$OtherValue1,
[Parameter(ParameterSetName='AdHoc')][string]$OtherValue2
)
Obviously this means the helper functions can no longer be in the same .ps1 file.
Does this mean I will now need 3 script files, each dot-sourcing the other as needed?
Should I use: script.ps1 (containing only AdvFunc), helpers.ps1 (containing the several helper functions), and collection.ps1 (with only $collection being piped to script.ps1) ?
Are there any alternatives?

Proposed solution: Use a launcher script that sources script.ps1. All functions (AdvFunc and all helper functions) reside in script.ps1.
# launcher.ps1
[cmdletbinding(DefaultParameterSetName='Scheduled')]
Param (
[Parameter(ParameterSetName='AdHoc', Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Param1,
[Parameter(ParameterSetName='AdHoc', Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Param2,
[Parameter(ParameterSetName='AdHoc', Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Param3
)
. .\script.ps1
if ($PSBoundParameters.ContainsKey('param1')) {
AdvFunc -Param1 $Param1 -Param2 $Param2 -Param3 $Param3
}
else {
$collection | AdvFunc
}
The idea is to accommodate either no parameter (sending $collection to AdvFunc) or a full 'AdHoc' set of parameters (sending the command-line defined collection to AdvFunc). The empty 'Scheduled' Parameter Set may not be necessary to accommodate the no parameter option.

Related

Cannot use variable in a Where-Object in a function

I am trying to have a function that can count jobs based on the LatestStatus value that I would pass a parameter. So far what I got:
Function JobCountStatus {
Write-Output (Get-VBRJob | ?{$_.Info.LatestStatus -eq $args} | Measure-Object).Count
}
The issue is that as I've read somewhere there will be a subshell(?) executing the where so the argument is not passed.
If I replace the $args with a specific string like "Failed" it will work.
Is there a way to overcome this? I do not want to write separate functions for all possible values.
I would appreciate any comments - Thanks
Well you can just name the value when you run the function as $args is an Automatic Variable
JobCountStatus "Failed"
You can use an advanced function with a parameter, named or not:
function JobCountStatus {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[System.String]
$Status
)
Process {
(Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } | Measure-Object).Count
}
}
And call it like so:
JobCountStatus -Status "Failed"
# OR
JobCountStatus "Failed"
The latter having the same end result as just using $args. The only possible advantage to specifying your own parameter here is you could define a ValidateSet of statuses or an Enum of Status values so you can tab through them. An example of the former would be like so:
function JobCountStatus {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet("Failed", "Running", "Successful", "Unknown")]
[System.String]
$Status
)
Process {
(Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } | Measure-Object).Count
}
}
$args is an array, not a single value. In any case, a ScriptBlock {} is an unnamed function and $args has it's own meaning within it, so you can't use it without some modification in something like Where-Object. You would have to store the elements of $args as another variable or multiple variables to reference within a child ScriptBlock. Not a huge change for this function, but for one where more parameters are expected this can result in a lot of unnecessary code which can be difficult to maintain.
I prefer to recommend defining named parameters in most cases, and this would be a simpler change than making the parent $args work in a child ScriptBlock:
Function JobCountStatus {
Param(
[string]$Status
)
( Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } ).Count
}
I've also made a few more changes in this function, I'll explain below:
Use Param() to strongly define parameters by name. You could use the simpler syntax of function JobCountStatus([string]$Status) {} but for this case it's really a matter of preference for which technique to use. Using Param() is something I recommend as a matter of convention, but you'll need to use either technique to get a named parameter.
I replaced the $args reference with $Status.
Your use of Measure-Object is extraneous and so I've removed it. Where-Object returns a collection which already has the Count property.
You can use ? if you want but it's considered best practice to omit aliases and use full cmdlet names in scripts and modules, so I've replaced ? with Where-Object.
Note that you can invoke the function/cmdlet (the difference is minimal for PowerShell-defined cmdlets) with or without the parameter, as when you don't define a positional order, the order is automatically determined in the order of declaration:
# Either will work
JobCountStatus -Status Running
JobCountStatus Running
Here is some more documentation you may find useful:
About Functions
About Advanced Functions
#Ash's answer gives some more advanced examples of what you can do with param() which are mentioned in the the Advanced Functions link above. You cannot use advanced parameter attributes with the simple function syntax I mentioned in the first bullet point.

Declaring parameters does not work if anything is above param

I have a script with parameters. In order to ease the debug of the script I create a small function I found on the net to list all my variables. In order to do so, I start by getting all existing variables at the top of the script, then I create a function which compares recorded variables before and after getting parameters
Problem is when I put the $AutomaticVariables and the function before param declaration, PowerShell gives me the following error for any parameter where I set a default value. Is there anyway to workaround this … bug? If it's not a bug, why the hell this behavior. I don't see the point.
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.
# Array and function to debug script variable content
$AutomaticVariables = Get-Variable
function check_variables {
Compare-Object (Get-Variable) $AutomaticVariables -Property Name -PassThru |
Where -Property Name -ne "AutomaticVariables"
}
param(
[String]$hostname,
[String]$jobdesc,
[String]$type = "standard",
[String]$repo,
[String]$ocred,
[String]$site,
[String]$cred = "SRC-$($site)-adm",
[String]$sitetype,
[String]$room,
[String]$chsite = "chub"
)
# TEST - Display variables
check_variables
As mentioned in the comments, you should gather the variables you want to exclude in the calling scope:
Define function (could as well be a script), notice the $DebugFunc parameter I've added at the end:
function Do-Stuff
{
param(
[String]$hostname,
[String]$jobdesc,
[String]$type = "standard",
[String]$repo,
[String]$ocred,
[String]$site,
[String]$cred = "SRC-$($site)-adm",
[String]$sitetype,
[String]$room,
[String]$chsite = "chub",
[scriptblock]$DebugFunc
)
if($PSBoundParameters.ContainsKey('DebugFunc')){
. $DebugFunc
}
}
Now, gather the variables and define your function, then inject it into Do-Stuff:
# Array and function to debug script variable content
$AutomaticVariables = Get-Variable
function check_variables {
Compare-Object (Get-Variable) $AutomaticVariables -Property Name -PassThru | Where -Property Name -ne "AutomaticVariables"
}
Do-Stuff -DebugFunc $Function:check_variables
It's not a bug. The param section defines the input parameter of your script thus has to be the first statement (same as with functions). There is no need to perform any action before the param block.
If you explain what you want to achieve with your check_variables (not what it does). We probably can show you how to do it right.

Repeatable Parameters in Powershell Function (Preferably Linked Parameter Sets)

I am wondering if it is possible (and if so how) to create repeatable (and hopefully linked) parameters in a PowerShell function. This is how am looking for this to work:
function foo()
{
[CmdletBinding()]
Params(
[Parameter(Mandatory=$true,ParameterSetName="Default")]
[Parameter(Mandatory=$true,ParameterSetName="Set1")]
[Parameter(Mandatory=$true,ParameterSetName="Set2")]
[string]$SomeParam1,
[Parameter(Mandatory=$true,ParameterSetName="Set1")]
[Parameter(Mandatory=$true,ParameterSetName="Set2")]
*some magic here, likely, to make this repeatable*
[string]$SomeRepeatableParam,
[Parameter(Mandatory=$true,ParameterSetName="Set1")]
[string]$SomeLinkedParam1,
[Parameter(Mandatory=$true,ParameterSetName="Set2")]
[string]$SomeLinkedParam2
)
Begin
{
*some code here*
}
Process
{
foreach ($val in $SomeRepeateableParam)
{
*some code here using param and its linked param*
}
}
End
{
*some code here*
}
}
And then call this function like so:
foo -SomeParam "MyParam" -SomeRepeatableParam "MyProperty1" -SomeLinkedParam1 "Tall" -SomeRepeatableParam "MyProperty2" -SomeLinkedParam2 "Wide"
and so on, being able to use the repeatable parameter as many times in a single call as I feel like it.
Can this be done? And if so how?
Thanks for your time.
EDIT: For clarity, I don't mean an array parameter, but a repeatable parameter in which the linked parameter sets can be matched to each instance of the repeatable parameter.
Since PowerShell supports arrays as parameter values, there is generally no need to repeat a parameter.
There is no syntactic way to enforce the pairing (linking) of parameter values the way you intend, with repeating instances of the same parameter name, because parameter names must be unique (and even they didn't have to be unique, that alone wouldn't enforce the desired pairing).
You can, however, use parallel array parameters, and enforce their symmetry inside the function, e.g.:
function foo
{
[CmdletBinding()]
Param(
[string] $SomeParam1,
[string[]] $SomeRepeatableParam,
[string[]] $SomeLinkedParam
)
if ($SomeRepeatableParam.Count -ne $SomeLinkedParam.Count) {
Throw "Please specify paired values for -SomeRepeatableParam and -SomeLinkedParam"
}
for ($i = 0; $i -lt $SomeRepeatableParam.Count; ++$i) {
$SomeRepeatableParam[$i] + ': ' + $SomeLinkedParam[$i]
}
}
You would then call it as follows (note the , to separate the array elements):
foo -SomeParam1 "MyParam" `
-SomeRepeatableParam "MyProperty1", "MyProperty2" `
-SomeLinkedParam "Tall", "Wide"

How do I define functions within a CmdletBinding() script?

I'm writing a script that I'd like to use PowerShell's CmdletBinding() with.
Is there a way to define functions in the script? When I try, PowerShell complains about "Unexpected toke 'function' in expression or statement"
Here's a simplified example of what I'm trying to do.
[CmdletBinding()]
param(
[String]
$Value
)
BEGIN {
f("Begin")
}
PROCESS {
f("Process:" + $Value)
}
END {
f("End")
}
Function f() {
param([String]$m)
Write-Host $m
}
In my case, writing a module is wasted overhead. The functions only need to be available to this one script. I don't want to have to mess with the module path or the script location. I just want to run a script with functions defined in it.
You use begin, process, and end blocks when your code is supposed to process pipeline input. The begin block is for pre-processing and runs a single time before processing of the input starts. The end block is for post-processing and runs a single time after processing of the input is completed. If you want to call a function anywhere other than the end block you define it in the begin block (re-defining it over and over again in the process block would be a waste of resources, even if you didn't use it in the begin block).
[CmdletBinding()]
param(
[String]$Value
)
BEGIN {
Function f() {
param([String]$m)
Write-Host $m
}
f("Begin")
}
PROCESS {
f("Process:" + $Value)
}
END {
f("End")
}
Quoting from about_Functions:
Piping Objects to Functions
Any function can take input from the pipeline. You can control how a function processes input from the pipeline using Begin, Process, and End keywords. The following sample syntax shows the three keywords:
function <name> {
begin {<statement list>}
process {<statement list>}
end {<statement list>}
}
The Begin statement list runs one time only, at the beginning of the function.
The Process statement list runs one time for each object in the pipeline. While the Process block is running, each pipeline object is assigned to the $_ automatic variable, one pipeline object at a time.
After the function receives all the objects in the pipeline, the End statement list runs one time. If no Begin, Process, or End keywords are used, all the statements are treated like an End statement list.
If your code doesn't process pipeline input you can drop begin, process, and end blocks entirely and put everything in the script body:
[CmdletBinding()]
param(
[String]$Value
)
Function f() {
param([String]$m)
Write-Host $m
}
f("Begin")
f("Process:" + $Value)
f("End")
Edit: If you want to put the definition of f at the end of your script you need to define the rest of your code as a worker/main/whatever function and call that function at the end of your script, e.g.:
[CmdletBinding()]
param(
[String]$Value
)
function Main {
[CmdletBinding()]
param(
[String]$Param
)
BEGIN { f("Begin") }
PROCESS { f("Process:" + $Param) }
END { f("End") }
}
Function f() {
param([String]$m)
Write-Host $m
}
Main $Value

Function parameter always empty why?

Can someone tell me, why this function call does not work and why the argument is always empty ?
function check([string]$input){
Write-Host $input #empty line
$count = $input.Length #always 0
$test = ([ADSI]::Exists('WinNT://./'+$input)) #exception (empty string)
return $test
}
check 'test'
Trying to get the info if an user or usergroup exists..
Best regards
$input is an automatic variable.
https://technet.microsoft.com/ru-ru/library/hh847768.aspx
$Input
Contains an enumerator that enumerates all input that is passed to a function. The $input variable is available only to functions and script blocks (which are unnamed functions). In the Process block of a function, the $input variable enumerates the object that is currently in the pipeline. When the Process block completes, there are no objects left in the pipeline, so the $input variable enumerates an empty collection. If the function does not have a Process block, then in the End block, the $input variable enumerates the collection of all input to the function.
Perhaps use a param block for parameters.
https://technet.microsoft.com/en-us/magazine/jj554301.aspx
Update: the problem seems to be fixed if you don't use $input as a parameter name, maybe not a bad thing to have proper variable names ;)
Also Powershell doesn't have return keyword, you just push the object as a statement by itself, this will be returned by function:
function Get-ADObjectExists
{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[string]
$ObjectName
)
#return result by just calling the object (no return statement in powershell)
([ADSI]::Exists('WinNT://./'+$ObjectName))
}
Get-ADObjectExists -ObjectName'test'