Param with more values - powershell

Early days with CMD and Batch I use the shift command.
How I do that with PowerShell?
Here my sample:
# test.ps1
# start ps script with parameters
param(
[string]$varmon)
if ($varmon) {
foreach ($var in $varmon) {
Write-Host $var
}
}
PS CLI: .\test.ps1 one two three
I get only "one". How can I start the script with more parameters than one?

You could use the automatic variable $args:
# test.ps1
# start ps script with parameters
foreach ($var in $args){
write-host $var
}
Arrays allow you to enter a variable number of arguments - at the end of the day $args is just an automatic array that's created for unassigned variables.
Array parameters are comma delimited, not space - example below.
test.ps1
# test.ps1
# start ps script with parameters
param(
[int[]]$numbers,
[string[]]$names
)
if($numbers){
Write-host "`nYou have entered the following numbers:"
foreach ($num in $numbers){
write-host "Number : $num"
Write-host "Square root: $([system.math]::sqrt($num))"
}
}
if($names){
Write-host "`nYou have entered the following names:"
foreach($name in $names){
Write-host $name
}
}
Example 1: Without using the parameter names, you will need to keep the arrays in order. So $numbers first, and $names second
PS CLI: .\test.ps1 4,9,16 john,jim,jane
Example 1: With parameter names, you can change the order.
PS CLI: .\test.ps1 -names john,jim,jane -numbers 4,9,16

Related

Redirect/Capture Write-Host output even with -NoNewLine

The function Select-WriteHost from an answer to another Stackoverflow question (see code below) will redirect/capture Write-Host output:
Example:
PS> $test = 'a','b','c' |%{ Write-Host $_ } | Select-WriteHost
a
b
c
PS> $test
a
b
c
However, if I add -NoNewLine to Write-Host, Select-WriteHost will ignore it:
PS> $test = 'a','b','c' |%{ Write-Host -NoNewLine $_ } | Select-WriteHost
abc
PS> $test
a
b
c
Can anyone figure out how to modify Select-WriteHost (code below) to also support -NoNewLine?
function Select-WriteHost
{
[CmdletBinding(DefaultParameterSetName = 'FromPipeline')]
param(
[Parameter(ValueFromPipeline = $true, ParameterSetName = 'FromPipeline')]
[object] $InputObject,
[Parameter(Mandatory = $true, ParameterSetName = 'FromScriptblock', Position = 0)]
[ScriptBlock] $ScriptBlock,
[switch] $Quiet
)
begin
{
function Cleanup
{
# Clear out our proxy version of write-host
remove-item function:\write-host -ea 0
}
function ReplaceWriteHost([switch] $Quiet, [string] $Scope)
{
# Create a proxy for write-host
$metaData = New-Object System.Management.Automation.CommandMetaData (Get-Command 'Microsoft.PowerShell.Utility\Write-Host')
$proxy = [System.Management.Automation.ProxyCommand]::create($metaData)
# Change its behavior
$content = if($quiet)
{
# In quiet mode, whack the entire function body,
# simply pass input directly to the pipeline
$proxy -replace '(?s)\bbegin\b.+', '$Object'
}
else
{
# In noisy mode, pass input to the pipeline, but allow
# real Write-Host to process as well
$proxy -replace '(\$steppablePipeline\.Process)', '$Object; $1'
}
# Load our version into the specified scope
Invoke-Expression "function ${scope}:Write-Host { $content }"
}
Cleanup
# If we are running at the end of a pipeline, we need
# to immediately inject our version into global
# scope, so that everybody else in the pipeline
# uses it. This works great, but it is dangerous
# if we don't clean up properly.
if($pscmdlet.ParameterSetName -eq 'FromPipeline')
{
ReplaceWriteHost -Quiet:$quiet -Scope 'global'
}
}
process
{
# If a scriptblock was passed to us, then we can declare
# our version as local scope and let the runtime take
# it out of scope for us. It is much safer, but it
# won't work in the pipeline scenario.
#
# The scriptblock will inherit our version automatically
# as it's in a child scope.
if($pscmdlet.ParameterSetName -eq 'FromScriptBlock')
{
. ReplaceWriteHost -Quiet:$quiet -Scope 'local'
& $scriptblock
}
else
{
# In a pipeline scenario, just pass input along
$InputObject
}
}
end
{
Cleanup
}
}
PS: I tried inserting -NoNewLine to the line below (just to see how it would react) however, its producing the exception, "Missing function body in function declaration"
Invoke-Expression "function ${scope}:Write-Host { $content }"
to:
Invoke-Expression "function ${scope}:Write-Host -NoNewLine { $content }"
(Just to recap) Write-Host is meant for host, i.e. display / console output only, and originally couldn't be captured (in-session) at all. In PowerShell 5, the ability to capture Write-Host output was introduced via the information stream, whose number is 6, enabling techniques such as redirection 6>&1 in order to merge Write-Host output into the success (output) stream (whose number is 1), where it can be captured as usual.
However, due to your desire to use the -NoNewLine switch across several calls, 6>&1 by itself is not enough, because the concept of not emitting a newline only applies to display output, not to distinct objects in the pipeline.
E.g., in the following call -NoNewLine is effectively ignored, because there are multiple Write-Host calls producing multiple output objects (strings) that are captured separately:
'a','b','c' | % { Write-Host $_ -NoNewline } 6>&1
Your Select-WriteHost function - necessary in PowerShell 4 and below only - would have the same problem if you adapted it to support the -NoNewLine switch.
An aside re 6>&1: The strings that Write-Host invariably outputs are wrapped in [System.Management.Automation.InformationRecord] instances, due to being re-routed via the information stream. In display output you will not notice the difference, but to get the actual string you need to access the .MessageData.Message property or simply call .ToString().
There is no general solution I am aware of, but situationally the following may work:
If you know that the code of interest uses only Write-Host -NoNewLine calls:
Simply join the resulting strings after the fact without a separator to emulate -NoNewLine behavior:
# -> 'abc'
# Note: Whether or not you use -NoNewLine here makes no difference.
-join ('a','b','c' | % { Write-Host -NoNewLine $_ })
If you know that all instances of Write-Host -NoNewLine calls apply only to their respective pipeline input, you can write a simplified proxy function that collects all input up front and performs separator-less concatenation of the stringified objects:
# -> 'abc'
$test = & {
# Simplified proxy function
function Write-Host {
param([switch] $NoNewLine)
if ($MyInvocation.ExpectingInput) { $allInput = $Input }
else { $allInput = $args }
if ($NoNewLine) { -join $allInput.ForEach({ "$_" }) }
else { $allInput.ForEach({ "$_" }) }
}
# Important: pipe all input directly.
'a','b','c' | Write-Host -NoNewLine
}

Redirect standard input (read-host) to a Powershell script

Here's a sample powershell script:
$in = read-host -prompt "input"
write-host $in
Here's a sample 'test.txt' file:
hello
And we want to pass piped input to it from powershell. Here's some I have tried:
.\test.ps1 < test.txt
.\test.ps1 < .\test.txt
.\test.ps1 | test.txt
.\test.ps1 | .\test.txt
test.txt | .\test.ps1
.\test.txt | .\test.ps1
get-content .\test.txt | .\test.ps1
even just trying to echo input doesn't work either:
echo hi | \.test.ps1
Every example above that doesn't produce an error always prompts the user instead of accepting the piped input.
Note: My powershell version table says 4.0.-1.-1
Thanks
Edit/Result: To those looking for a solution, you cannot pipe input to a powershell script. You have to update your PS file. See the snippets below.
The issue is that your script \.test.ps1 is not expecting the value.
Try this:
param(
[parameter(ValueFromPipeline)]$string
)
# Edit: added if statement
if($string){
$in = "$string"
}else{
$in = read-host -prompt "input"
}
Write-Host $in
Alternatively, you can use the magic variable $input without a param part (I don't fully understand this so can't really recommend it):
Write-Host $input
You can't pipe input to Read-Host, but there should be no need to do so.
PowerShell doesn't support input redirection (<) yet. In practice this is not a (significant) limitation because a < b can be rewritten as b | a (i.e., send output of b as input to a).
PowerShell can prompt for input for a parameter if the parameter's value is missing and it is set as a mandatory attribute. For example:
function test {
param(
[parameter(Mandatory=$true)] [String] $TheValue
)
"You entered: $TheValue"
}
If you don't provide input for the $TheValue parameter, PowerShell will prompt for it.
In addition, you can specify that a parameter accepts pipeline input. Example:
function test {
param(
[parameter(ValueFromPipeline=$true)] [String] $TheValue
)
process {
foreach ( $item in $TheValue ) {
"Input: $item"
}
}
}
So if you write
"A","B","C" | test
The function will output the following:
Input: A
Input: B
Input: C
All of this is spelled out pretty concisely in the PowerShell documentation.
Yes; in Powershell 5.1 "<" is not implemented (which sucks)
so, this won't work: tenkeyf < c:\users\marcus\work\data.num
but,
this will: type c:\users\marcus\work\data.num | tenkeyf
...
PowerShell doesn’t have a redirection mechanism, but.NET have.
you can use [System.Diagnostics.Process] implements the purpose of redirecting input.
The relevant Microsoft documents are as follows.
Process Class
This is a sample program that works perfectly on my windows 10 computer
function RunAndInput{
$pi = [System.Diagnostics.ProcessStartInfo]::new()
$pi.FileName ="powershell"
$pi.Arguments = """$PSScriptRoot\receiver.ps1"""
$pi.UseShellExecute = $false
$pi.RedirectStandardInput = $true
$pi.RedirectStandardOutput = $true
$p = [System.Diagnostics.Process]::Start($pi)
$p.StandardInput.WriteLine("abc"+ "`r`n");
$p.StandardOutput.ReadToEnd()
$p.Kill()
}
RunAndInput
# OutPut
Please enter the information: abc
Received:abc
# receiver.ps1
('Received:' + (Read-Host -Prompt 'Please enter the information'))
Hope to help you!

Variables Cleanup in PowerShell Script

When running PowerShell scripts at the console, scoping helps keep script variables from bleeding into the global environment. However, this seems to not quite apply in the PowerShell ISE. Thus, I've been trying to come up with an easy method to track variables in a script so they can be cleaned up whenever the script exits. A test script I've written to demonstrate this is below:
# Start from clean screen.
Clear-Host
# Set initial variables.
$Var1 = 1
$Var2 = 2
$Var3 = 3
# Initialize variables list.
$VarsList = New-Object System.Collections.ArrayList
# Function to add a variable.
function NewVar
{
$VarsList.Add($Args) | Out-Null
}
# Add initial variables to $VarsList.
'Var1','Var2','Var3' | ForEach-Object {NewVar $_}
# Check VarsList
Write-Host "VarsList:"
$VarsList
Write-Host ""
# Add a new variable to test NewVar.
$Var4 = 4
NewVar Var4
# Check VarsList
Write-Host "VarsList after Var4:"
$VarsList
Write-Host ""
# Function to remove variable from environment and variables list.
function KillVar
{
Remove-Variable $Args -Scope 1
$VarsList.Remove($Args[0])
}
# Function to remove all variables.
function VarGenocide
{
$VarsList | ForEach-Object {Remove-Variable $_ -Scope 1}
Remove-Variable VarsList -Scope 1
}
# Try to use KillVar
KillVar Var3
# Check Variables
Write-Host "Variables after KillVar Var3:"
Write-Host "Var1 $Var1"
Write-Host "Var2 $Var2"
Write-Host "Var3 $Var3"
Write-Host "Var4 $Var4"
Write-Host ""
# Check $VarsList
Write-Host "VarsList after KillVar Var3:"
$VarsList
Write-Host ""
# Try to remove all variables
VarGenocide
# Check Variables
Write-Host "Variables after VarGenocide:"
Write-Host "Var1 $Var1"
Write-Host "Var2 $Var2"
Write-Host "Var3 $Var3"
Write-Host "Var4 $Var4"
Write-Host ""
# Check $VarsList
Write-Host "VarsList after VarGenocide:"
$VarsList
Getting the variables removed doesn't seem to be a problem. However, updating $VarsList for removed variables, appears to be more difficult than I'd anticipated. Here's the output of the above script.
As you can see, 'Var3' was not removed from $VarsList when I used KillVar. This resulted in the "Cannot find a variable" error when VarGenocide was attempted.
I have a feeling this has something to do with variable scoping. However, I'm not sure how to properly handle it since I'm not using PowerShell's built-in array class (see here for why). Am I right, and is there a way to work around this? Or is there another problem I'm not seeing?
I'm using PowerShell 4.0 on Windows 8.1, though I'm pretty sure this should be valid down to 2.0 on XP.
The function NewVar has a subtle mistake. $Args is an array, so that $VarsList.Add($Args) does not add strings (variable names), it adds arrays. Later you search for a string Var3 there and it is not found, correctly.
One way to fix this is to use AddRange instead of Add:
# Function to add a variable.
function NewVar
{
$VarsList.AddRange($Args)
}
Note that you can omit | Out-Null because AddRange is void. BTW, the most effective way to discard output is $null = ....

How to get the parameter name in a powershell function

I would like to implement a function to output some debugging information.
I want the function, namely "Write-Foo", to work in the following way:
Write-Foo $SomeThing
# the output will be "SomeThing: (the value of Something)"
Is it possible to implement this function? I don't know how to get the name of the parameter of the function.
Thanks
This is close to what you want:
function Write-Foo($varName)
{
$varVal = $ExecutionContext.InvokeCommand.ExpandString("`$variable:$varName")
Write-Host "$($varName): $varVal"
}
Trying it out:
PS> $SomeThing = 'hello'
PS> Write-Foo SomeThing
SomeThing: hello
Another take:
function Write-Foo($varName)
{
$v = Get-Variable $varName
Write-Host ($v.Name + ": " + $v.Value)
}
Result:
PS C:\> $Something = "nothing"
PS C:\> Write-Foo SomeThing
Something: nothing
Another possibility (this assumes what you want to display is the name of the variable that was passed as an argument, not the name of the parameter it was passed to).
function Write-Foo($varName)
{
$var =
($MyInvocation.line -replace '\s*write-foo\s*\$','').trim()
'{0}: {1}' -f $var,$varName
}
$something = 'Hello'
write-foo $something
something: Hello
It's easier if you pass the name without the $
function Write-Foo($varName)
{
$var = Get-Variable $varName
'{0}: {1}' -f $var.Name,$var.Value
}
$something = 'Hello'
write-foo something
something: Hello
I'll also second #Ansgar Weicher's suggestion of writing that to the Debug stream so you don't pollute the pipeline with it.
You could define a function like this:
function Write-Foo($msg) {
Write-Host ("SomeThing: {0}" -f $msg)
}
or (if you want the output to be available for further processing) like this:
function Write-Foo($msg) {
"SomeThing: {0}" -f $msg
}
However, since you said you want to output debugging information, using the already existing Write-Debug cmdlet might be a better approach:
PS C:\> Write-Debug 'foo'
PS C:\> $DebugPreference
SilentlyContinue
PS C:\> $DebugPreference = 'Continue'
PS C:\> Write-Debug 'foo'
DEBUG: foo
You can access the function's parameters through $MyInvocation.BoundParameters, so this may be what you want:
function Write-Foo($varName) {
foreach ($key in $MyInvocation.BoundParameters.Keys) {
Write-Verbose ("{0}: {1}" -f ($key, $MyInvocation.BoundParameters[$key])) -Verbose
}
}
Write-Foo "Hello"
and the output looks like this:
VERBOSE: varName: Hello
If you want to be able to control when the debug information appears you can turn this into a cmdlet:
function Write-Foo {
[CmdletBinding()]
Param ($varName)
foreach ($key in $MyInvocation.BoundParameters.Keys) {
Write-Verbose ("{0}: {1}" -f ($key, $MyInvocation.BoundParameters[$key]))
}
}
Write-Foo "Hello"
Write-Foo "This will debug" -Verbose
and the first call produces no output while the second will show you:
VERBOSE: Verbose: True
VERBOSE: varName: This will debug
Naturally you can choose how exactly to output the debug information. Probably either Write-Debug (which usually prompts for each line of output) or Write-Verbose (which is usually suppressed) is appropriate here.

Can I resolve PowerShell scriptblock parameters without invoking?

I'm looking at writing some PowerShell code that can either execute immediately, or produce the commands it would execute as generated scripts.
I'd like to avoid this scenario:
if($Generating){
write-Output "somecommand.exe"
}
else{
somecommand.exe
}
I got looking at ScriptBlocks, which at first looked promising because I can write the contents of the ScriptBlock to the console without executing it. Such as:
$sc = { somecommand.exe }
$sc
somecommand.exe
My specific question is, if my scriptblock contains parameters, can I get them to resolve when I'm writing the scriptblock contents to the console, but WITHOUT invoking the scriptblock?
For example given the following scriptblock:
$b2 = { Param([string]$P) Write-Host "$P" }
When I just type "$b2" at the console and hit enter I see this:
Param([string]$P) Write-Host "$P"
What I'd like to see is this (if the parameter value is "Foo"):
Param([string]$P) Write-Host "Foo"
I realize this can be done when it's invoked, either via "&" or using Invoke(), but would there be any way to get the parameters to resolve without invoking to make my script generation a little more elegant without needing a bunch of conditional statements throughout the code?
In PowerShell v3, you can get the param info via the AST property e.g.:
PS> $sb = {param($a,$b) "a is $a b is $b"}
PS> $sb.Ast.ParamBlock
Attributes Parameters Extent Parent
---------- ---------- ------ ------
{} {$a, $b} param($a,$b) {param($a,$b) "a...
Solution suitable for PowerShell v2:
# given the script block
$b2 = { Param([string]$P) Write-Host "$P" }
# make a function of it and "install" in the current scope
Invoke-Expression "function tmp {$b2}"
# get the function and its parameters
(Get-Command tmp).Parameters
When displaying a here-string with double quotes #" , it expands the variables. For the variables that should'nt expand, escape the variable with a backtick ( ` ).
So try this:
$P = "Foo"
$b2 = #"
{ Param([string]`$P) Write-Host "$P" }
"#
Test:
PS-ADMIN > $b2
{ Param([string]$P) Write-Host "Foo" }
If you want to convert it to scriptblock-type again:
#Convert it into scriptblock
$b3 = [Scriptblock]::Create($b2)
PS-ADMIN > $b3
{ Param([string]$P) Write-Host "Foo" }
PS-ADMIN > $b3.GetType().name
ScriptBlock
Using some of the suggestions I think I've found the best solution for my needs. Consider the following code
function TestFunc
{
Param(
[Parameter(Mandatory=$true)]
[string]$Folder,
[Parameter(Mandatory=$true)]
[string]$Foo
)
$code = #"
Write-Host "This is a folder $Folder"
Write-Host "This is the value of Foo $Foo"
"#
$block = [Scriptblock]::Create($code)
Write-Host "Running the block" -BackgroundColor Green -ForegroundColor Black
&$block
Write-Host "Displaying block code" -BackgroundColor Green -ForegroundColor Black
$block
}
And it's output:
Running the block
This is a folder c:\some\folder
This is the value of Foo FOOFOO
Displaying block code
Write-Host "This is a folder c:\some\folder"
Write-Host "This is the value of Foo FOOFOO"
By doing it this way, I still get all the benefit of keeping my existing functions and their parameters, parameter validation, CBH etc. I can also easily generate the code that the function would execute or just let it execute. Thanks for all the input, it's definitely been a good learning experience.
If you want to express your block as a block, not a string, the following works:
$printable = invoke-expression ('"' + ($block -replace '"', '`"') + '"')
Essentially, you're wrapping everything in quotes and then invoking it as an expression. The -replace call ensures any quotes in the block itself are escaped.
I'm using this in this handy function, which also halts execution if the invoked command failed.
# usage: exec { dir $myDir }
function exec($block)
{
# expand variables in block so it's easier to see what we're doing
$printable = invoke-expression ('"' + ($block -replace '"', '`"').Trim() + '"')
write-host "# $printable" -foregroundcolor gray
& $block
if ($lastExitCode -ne 0)
{
throw "Command failed: $printable in $(pwd) returned $lastExitCode"
}
}