I have a shell script file.
./1.ps1
./2.ps1
I generate a string such as env-0 in 1.ps1 and need to export it to 2.ps1.
UPDATE:
1.ps1 generate many string and I couldn't pass my env-0 as argument to 2.ps1.
Do you have another solution?
PowerShell's stream-like I/O model is a little unusual, but any value not captured is basically output - so to output the string env-0 from 1.ps1, this is literally all you need:
"env-0"
Now that 1.ps1 outputs a string value, we need some way to feed it to 2.ps1.
You can either rely on unbound positional arguments, which will automatically be available in the target script/function via the $args variable:
# 2.ps1
$envString = $args[0]
# work with your `$envString` value here
To invoke:
$envString = ./1.ps1
./2.ps1 $envString
# or
./2.ps1 $(./1.ps1)
You can also accept pipeline input by consuming the $input automatic input enumerator variable:
# 2.ps1
$input |ForEach-Object {
$envString = $_
# work with your `$envString` value here
}
To invoke:
./1.ps1 |./2.ps1
This might be useful if you intend to provide multiple inputs to 2.ps1 in succession:
# This will execute `./1.ps1` 100 times, but `./2.ps1` will only be executed once, and the output continuously fed to it as pipeline input
1..100 |ForEach-Object { ./1.ps1 } | ./2.ps1
Finally, if you want to write scripts for more advanced scenarios (multiple input parameters, input validation, argument completion etc.), you'll want to explicitly declare named parameter(s) with a param() block:
# ./2.ps1
param(
[Parameter(Mandatory)]
[string]$Environment
)
# work with $Environment here
To invoke:
./2.ps1 -Environment $(./1.ps1)
It's worth noting that you can use the [Parameter()] attribute to modify the binding behavior so you still get pipeline support:
# ./2.ps1
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[string]$Environment
)
# work with $Environment here
Now that we've added both the Position and a ValueFromPipeline flags, we can pass the string via the pipeline, by name, or positionally all at once:
# These all work now
./2.ps1 -Environment $(./1.ps1)
./2.ps1 $(./1.ps1)
./1.ps1 |./2.ps1
The only caveat is that you can't pipe multiple input values anymore - for that, you'll need to move the script body into a special process block:
# ./2.ps1
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[string]$Environment
)
process {
# work with $Environment here
}
The process block will execute exactly once for each pipeline input, so now piping multiple values works again ()along with the previous 3 examples:
1..100 |ForEach-Object { ./1.ps1 } | ./2.ps1
So that's my solution:
# 1.ps1
"env-0"
# 2.ps1
# ./2.ps1
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[string]$Environment
)
process {
# work with $Environment here
}
For more information about advanced parameters and their binding semantics and how to utilise them, see the about_Functions_Advanced_Parameters help topic
Related
Im wondering if I am tackling this in the best way or if there is a better way of achieving my task.
I've written a function in PowerShell which takes different parameters, but some of the parameters won't work together.
So for example if I'm running the function and specifying a computerName parameter then I can't also pass list of multiple computer names.
I know I can write multiple If statements as along the lines if If(($computerName) - and ($computerList)){Then write and error}
but there are several parameters not just two, so do I need to do an if for each set of parameters someone could type in, or is there a better way of me tackling this?
currently I have multiple Ifs like If $computerName -and !(log file) and $computerlist) then write an error etc.
The PowerShell-idiomatic solution here is to declare a parameter that accepts either:
function Get-Stuff
{
param(
[Parameter(ValueFromPipeline = $true)]
[string[]]$ComputerName
)
process {
foreach($Computer in $ComputerName){
# do stuff with each individual -ComputerName argument
}
}
}
Now the user can do both
Get-Stuff -ComputerName oneComputerName
... and
"many","computer","names" |Get-Stuff
or for that matter
$computers = Get-Content .\Computers.txt
Get-Stuff -ComputerName $computers
# or
$computers |Get-Stuff
I know I can write multiple If statements as along the lines of If(($computerName) -and ($computerList)){Then write and error}
You can, but this is generally a bad idea - to test whether an argument value was passed to a parameter is better done through the automatic variable $PSBoundParameters:
function Get-Stuff
{
param(
[string]$AParameter,
[string]$ADifferentOne
)
if($PSBoundParameters.ContainsKey('AParameter')){
# an argument was definitely provided to $AParameter
}
if($PSBoundParameters.ContainsKey('ADifferentOne')){
# an argument was definitely provided to $ADifferentOne
}
}
The answer to the implied question of "how do I declare and work with mutually exclusive parameters" is parameter sets:
function Verb-Noun
{
param(
[Parameter(Mandatory = $true, ParameterSetName = 'SingleComputer')]
[string]$ComputerName,
[Parameter(Mandatory = $true, ParameterSetName = 'MultipleComputers')]
[string[]]$ComputerList
)
if($PSCmdlet.ParameterSetName -eq 'SingleComputer'){
# just one, we can deal with $ComputerName
}
else {
# we got multiple names via $ComputerList
}
}
PowerShell now recognizes two distinct parameter sets, each of which only accepts one of our parameters:
PS ~> Get-Command Verb-Noun -Syntax
Verb-Noun -ComputerName <string> [<CommonParameters>]
Verb-Noun -ComputerList <string[]> [<CommonParameters>]
I've a simple function:
function Write-Log {
[CmdletBinding()]
param (
# Lines to log
[Parameter(Mandatory , ValueFromPipeline )]
[AllowEmptyString()]
$messages
)
process {
Write-Host $_
}
}
Based on ValueFromPipeline in can use the function with pipeline input, e.g.
"a","b", "c" | Write-Log
a
b
c
That's ok. But if I want to use my function in that way:
Write-Log -message "a", "b", "c"
the automatic $_ variable is empty, and nothing is printed.
I also found these two stackoverflow links:
Handling pipeline and parameter input in a Powershell function
How do I write a PowerShell script that accepts pipeline input?
Both of them suggest the following pattern:
function Write-Log {
[CmdletBinding()]
param (
# Lines to log
[Parameter(Mandatory , ValueFromPipeline )]
[AllowEmptyString()]
$messages
)
process {
foreach ($message in $messages){
Write-Host $message
}
}
}
Above pattern works for my use case. But from my point of view is feels weird to call foreach in the `process´ block, since (as far as I've understood) the process block is called for every pipeline item. As there a better cleaner way to write functions supporting both use cases?
Thx.
That's the way you have to do it if you want to pass an array to a parameter like
Write-Log -Messages a,b,c
Otherwise you can only do
Write-Log -Messages a
You can still pipe an array in without the foreach:
Echo a b c | Write-Log
I appreciate cmdlets that can do both, like get-process.
Is it possible, using PowerShell, to use splatting from hashtable when the hashtable contains more entries that the function accepts ?
My use case is to have config objects I pass from one function to another. However, all functions does not require same parameters.
Ex:
function Process-Something{
param(
[Parameter()]
[string]$Owner
)
}
function Process-SomethingElse{
param(
[Parameter()]
[string]$Owner,
[Parameter()]
[int]$x,
[Parameter()]
[int]$y
)
}
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
Process-Something #config
Process-SomethingElse #config
It fails with these error:
Process-Something : Cannot find a matching parameter « Y ».
The idea is to avoid specifying individual properties for each functions.
As #Ansgar is stating in the comments, the whole idea of having defined your parameters, is to get validation. When you are splatting parameters to your function, you are forcing them to the function. So if a given property of your hashtable doesn't exist as a parameter, you will get an error - just like it is intended.
What you can do, is going into a PSCustomObject and utilize the pipe. If you set all you parameters to accept value from the pipeline, using property name (ValueFromPipelineByPropertyName = $true), then you can actually get the desired behavior.
First I'm redefining your different functions, to have the ValueFromPipelineByPropertyName = $true parameter attribute configured.
function Process-Something{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner
)
$PSBoundParameters
}
function Process-SomethingElse{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$x,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$y
)
$PSBoundParameters
}
With that in place, I'm able to create a hashtable like your example, convert it to a PSCustomObject, and now I can pipe that new object to the different methods and have them pick up only the properties that they need.
I included the PSBoundParameters to showcase that they get what they expect.
Testing is done like this:
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
$psConfig = [PSCustomObject]$config
$psConfig | Process-Something
$psConfig | Process-SomethingElse
In PowerShell, one can generally call a function with arguments as follows:
DoRoutineStuff -Action 'HouseKeeping' -Owner 'Adamma George' -Multiples 4 -SkipEmail
To trap these 4 supplied arguments at runtime, one might place this inside the function definition
""
"ARGUMENTS:"
$PSBoundParameters
And the resulting object displayed might look like so:
ARGUMENTS:
Key Value
--- -----
Action HouseKeeping
Owner Adamma George
Multiples 4
SkipEmail True
Now, my question is: If I were to manually build the $MyObject identical to $PSBoundParameters displayed above, is there a way to say:
RunFunction 'DoRoutineStuff' -oArgument $MyObject
Again, if it were to be a script file rather than the function DoRoutineStuff, does that make any difference?
Why might one need to do this?
Picture a situation where you need to catch the arguments supplied to first script or function, using $PSBoundParameters, like so:
DoRoutineStuff{
param(
[string]$Action,
[string]$Owner,
[Int]$Multiples,
[switch]$SkipEmail
)
$Data = $PSBoundParameters
#Update one object property
$Data.Multiples = 1
#Then, recursively call `DoRoutineStuff` using `$Data`
#Other tasks
exit;
}
It sounds like the language feature you're looking for is splatting.
You simply pack you're named parameter arguments into a hashtable, store that in a variable and then pass the variable using # in front of its name:
$myArguments = #{
Action = 'HouseKeeping'
Owner = 'Adamma George'
Multiples = 4
SkipEmail = $true
}
Do-Stuff #myArguments
You can also use this technique to only pass a partial set of parameter arguments (or none at all), great for passing along conditional arguments:
$myArguments = #{}
if($someCondition){
$myArguments['Multiples'] = 1
$myArguments['SkipEmail'] = $true
}
if($somethingElse){
$myArguments['Multiple'] = 4
}
Do-Stuff -Action 'HouseKeeping' -Owner 'Adamma George' #myArguments
You can also reuse $PSBoundParameters for splatting further - very useful for proxy functions:
function Measure-Files
{
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $false)]
[string]$Filter,
[Parameter(Mandatory = $false)]
[switch]$Recurse
)
return (Get-ChildItem #PSBoundParameters |Measure-Object -Property Length).Sum
}
In my module, I have two functions a 'Get-Data' and 'Add-Data' I'm having trouble passing information between them through the pipeline.
The following simplified code shows my issue.
function Get-Data{
param ($path)
$out = Import-Csv $path
Write-Output $out
}
The data is the following, notice there are 'gaps' in the data and not every object has all three properties, in fact they cant. You can only have Name and Color or Name and Fruit. (In the real code there are many more properties)
Name,Color,Fruit
Jim,red,
Kate,,Apple
Bob,green,
Abby,,Banana
In the Add-Data function I want to use parameter sets as there are lots of parameters, but only 4 parameter sets possible (two in the simplified code, 'A' and 'B'). This function is exported from the module and I don't want the user to be able to input invalid parameter combinations.
Here is the add-data function:
function Add-Data {
[CmdletBinding()]
Param (
[Parameter(
Position = 1,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Name,
[Parameter(
ParameterSetName = 'A',
Position = 2,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Color,
[Parameter(
ParameterSetName = 'B',
Position = 3,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Fruit
)
BEGIN{}
PROCESS{
Write-Output "$Name $Color $Fruit"
}
END{}
}
When I pipe one to the other like below:
Get-Data -Path c:\some.csv | Add-Data
I get the error:
Cannot bind argument to parameter 'Fruit' because it is an empty string
I know that I am passing an empty string to the fruit parameter and it's what is causing me the issue, but it's obviously not the behaviour I want.
I'd like the fact that the property is empty to help resolve the parameter set so it only needs Name and Color.
I can't use [AllowNull()] or [AllowEmptyString()] and remove the parameter sets as I expect users to use 'add-data' from the command line and I want
help add-data
to show them the correct params to enter.
Thanks in advance for any help.