I wish to create a script which accepts input from the command line and based on the first value, it then determines the next parameters on offer.
Such as to determine if you are to do a single Run or a Batch run for a Password change operation:
./script.ps1 -singleMODE -UserName -Password
./script.ps1 -batchMODE -filename
What i am confused about whilst learning Powershell is what this is? I have looked at Parameters and can read them into variables from command line...but what i want as above has some logic and i am a bit lost. Can someone give me a nudge as to what this is called....and then i can continue my googling! :)
I am thinking somehow i combine Params and Functions so it flicks to different blocks...but i am guessing
any help appreciated! :)
cheers
What you need are Parameter Sets
This is a demo with a function but it works just as well with a script (just put the param block at the top.)
function Demo {
param(
[Parameter(ParameterSetName='Funk')][switch]$Funk,
[Parameter(ParameterSetName='Rock')][switch]$Rock,
[Parameter(ParameterSetName='Funk')][string]$WriteFunk,
[Parameter(ParameterSetName='Rock')][string]$WriteRock
)
if($Funk){
foreach ($C in $WriteFunk.ToCharArray()){
$N = 0..15 | Get-Random
Write-Host $C -ForegroundColor $N -BackgroundColor $(15-$N) -NoNewline
}
Write-Host ''
}
if($Rock){
Write-Host $WriteRock -ForegroundColor Gray -BackgroundColor DarkGray
}
}
Demo -Funk -WriteFunk "Melt your brain"
Demo -Rock -WriteRock "Riders on the storm"
Related
So I'm working on a script and I would like to add an optional parameter. For some reason it doesn't work the way I thought it should, so clearly I'm not getting something here.
I tried 2 methods:
Method 1
function Thing {
[CmdletBinding()]
param (
[Parameter(Mandatory=$false)][bool]$StartThing
)
if ($StartThing -eq $true) {
Write-Host "Thing started" -ForegroundColor Green
}
else {
Write-Host "Thing didn't start" -ForegroundColor Red
}
}
Thing
Then, I'm running: .\test.ps1 -StartThing $true
The output I'm getting is always Thing didn't start. No matter what value I pass in.
Method 2
I tried using switch instead in this form:
function Thing {
param (
[switch] $StartThing
)
switch ($StartThing) {
$true { Write-Host "Thing started" -ForegroundColor Green }
Default { Write-Host "Thing didn't start" -ForegroundColor Red }
}
}
Thing -StartThing
This one doesn't work for me either. When I run .\test.ps1 -StartThing the output is always Thing started. I tried adding $true or $false inputs but the output is the same no matter what.
I'm not sure what I'm doing wrong.
Any help would be great :)
StartThing is not being passed to your function. You always run is without any params (example from method 1, for method 2 the issue is similar):
Thing
If you want to run the file in the form provided, you need to add param block to the script file itself.
NOTE: Only relevant parts of code were added, I haven't changed the working parts. Please see Olaf's helpful answer to improve the script even better:
# Param block for script
param (
[Parameter(Mandatory=$false)][bool]$StartThing
)
function Thing {
[CmdletBinding()]
param (
[Parameter(Mandatory=$false)][bool]$StartThing
)
if ($StartThing -eq $true) {
Write-Host "Thing started" -ForegroundColor Green
}
else {
Write-Host "Thing didn't start" -ForegroundColor Red
}
}
# Function runs with the parameter provided
Thing -StartThing $StartThing
This might clear it up a little:
function Thing {
param (
[switch] $StartThing
)
if ($StartThing) {
"You provided the parameter 'StartThing'"
}
else {
"You did not provide the parameter 'StartThing'"
}
}
Now you simply run your function with
Thing -StartThing
And the result would be:
You provided the parameter 'StartThing'
How can I enter a keystroke programmatically through a PowerShell script?
Write-Host -ForegroundColor Green 'Loading...'
Function EnterKey {
[Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
#Where I want to get "|" keystroke programmatically
[System.Windows.Forms.SendKeys]::SendWait("{|}")
}
Function StartUp {
Write-Host "Environment"
$exe = ([IO.Path]::Combine("D:\7ZipApp\7ZipApp\7ZipApp\bin\Debug","7ZipApp.exe"))
& $exe 3 # argument 3 = 'Run local Sync'
EnterKey
Read-Host -Prompt $exe.ToString()
}
StartUp
Write-Host -ForegroundColor Green 'Loading...'
function StartUp {
Write-Host 'Environment'
$exe = Join-Path "D:\7ZipApp\7ZipApp\7ZipApp\bin\Debug" "7ZipApp.exe"
#& $exe 3 # argument 3 = 'Run local Sync'
start $exe -ArgumentList 3
Write-Host 'Type {|} to continue'
while ((Read-Host) -ne '{|}') {}
Read-Host -Prompt $exe.ToString()
}
StartUp
I have to go with the crowd here (from the comments):
I would abandon your approach. Too problematic.
My question was why you want to do it
The correct solution, then, is to get the author of 7zipapp.exe to fix the program so it stops doing that or to add a command-line parameter that prevents this behavior.
That said, if you want a total hack, and this program only takes ONE input, at the end presumably, then the below appears to work. I would use sparingly, perhaps never use it, but rather get the program fixed, but in my testing, this worked.
PowerShell:
$exe = 'C:\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe'
'\r\n' | & $exe
Annoying C# program:
using static System.Console;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
WriteLine("I will force you to hit Enter to exit.");
ReadLine();
}
}
}
I'd like to get my prompt in powershell to be at the bottom instead of "from top to bottom".
There is a workaround for cmd (https://superuser.com/questions/644326/start-conemu-with-prompt-at-the-bottom) but I can't find a way to make it work in powershell.
Does anyone have an idea?
Thanks a lot!
Thought, more "clean" version of prompt function. No need of New-Object ... Just add/modify your prompt in the $profile.
function prompt {
# put cursor at the bottom of the buffer
$rawUI = (Get-Host).UI.RawUI
$cp = $rawUI.CursorPosition
$cp.Y = $rawUI.BufferSize.Height - 1
$rawUI.CursorPosition = $cp
# and the prompt itself
Write-Host -NoNewline -ForegroundColor Cyan "PS "
Write-Host -NoNewline -ForegroundColor Yellow $(get-location).ProviderPath
return ">"
}
I'm not sure this is what you want, but this works fairly well for me. Save this as a .ps1 file somewhere that makes sense or convert it into a cmdlet if you like. Then stick it in your profile so it runs when you open a powershell session:
cls
$Ui = (Get-Host).UI.RawUI
$Height = $UI.WindowSize.Height
$Coordinates = New-Object System.Management.Automation.Host.Coordinates 0,($Height - 1)
$Ui.CursorPosition = $Coordinates
Create an alias for it.
New-Alias -Name cl -Value \\Path_to_the_script_or_the_cmdlet
Use the alias to clear your screen rather than clear or cls.
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"
}
}
I'm confused about something I saw in the book Learn PowerShell in a Month of lunches. In chapter 21 when the author discusses functions that accept input via parameter binding or the pipeline he gives two patterns.
The first as follows
function someworkerfunction {
# do some work
}
function Get-SomeWork {
param ([string[]]$computername)
BEGIN {
$usedParameter = $False
if($PSBoundParameters.ContainsKey('computername')) {
$usedParameter = $True
}
}
PROCESS {
if($usedParameter) {
foreach($computer in $computername) {
someworkerfunction -computername $comptuer
}
} else {
someworkerfunction -comptuername $_
}
}
END {}
}
The second like this
function someworkerfunction {
# do stuff
}
function Get-Work {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True)]
[Alias('host')]
[string[]]$computername
)
BEGIN {}
PROCESS {
foreach($computer in $computername) {
someworkerfunction -comptuername $computer
}
}
END {}
}
I know the second sample is a standard Powershell 2.0 Advanced function. My question is with Powershell 2.0 support for the cmdletbinding directive would you ever want to use the first pattern. Is that just a legacy from Powershell 1.0? Basically is there ever a time when using Powershell 2.0 that I would want to mess around with the first pattern, when the second pattern is so much cleaner.
Any insight would be appreciated.
Thank you.
If you want to process pipeline input in your function but don't want to add all the parameter attributes or want backwards compatibility go with the cmdletbindingless way.
If you want to use the additional features of PowerShell script cmdlets like the parameter attributes, parameter sets etc... then go with the second one.
If anyone wishes for a very, very simple explanation of how to read from piped input see
How do you write a powershell function that reads from piped input?
Had this ^ existed when I had this question, I would have saved a lot of time because this thread is quite complicated and doesn't actually explain how to handle pipelined input into a function.
No, the first example is not just legacy. In order to create a PowerShell function that uses an array parameter and takes pipeline input you have to do some work.
I will even go as far as to say that the second example does not work. At least I could not get it to work.
Take this example...
function PipelineMadness()
{
[cmdletbinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline=$true)]
[int[]] $InputArray
)
Write-Host ('$InputArray.Count {0}' -f $InputArray.Count)
Write-Host $InputArray
Write-Host ('$input.Count {0}' -f $input.Count)
Write-Host $input
if($input) { Write-Host "input is true" }
else { Write-Host "input is false" }
}
results ...
PS C:\Windows\system32> 1..5 | PipelineMadness
$InputArray.Count 1
5
$input.Count 5
1 2 3 4 5
input is true
PS C:\Windows\system32> PipelineMadness (1..5)
$InputArray.Count 5
1 2 3 4 5
$input.Count 1
input is false
Notice that when the pipeline is used the $InputArray variable is a single value of 5...
Now with BEGIN and PROCESS blocks
function PipelineMadnessProcess()
{
[cmdletbinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline=$true)]
[int[]] $InputArray
)
BEGIN
{
Write-Host 'BEGIN'
Write-Host ('$InputArray.Count {0}' -f $InputArray.Count)
Write-Host $InputArray
Write-Host ('$input.Count {0}' -f $input.Count)
Write-Host $input
if($input) { Write-Host "input is true" }
else { Write-Host "input is false" }
}
PROCESS
{
Write-Host 'PROCESS'
Write-Host ('$InputArray.Count {0}' -f $InputArray.Count)
Write-Host $InputArray
Write-Host ('$input.Count {0}' -f $input.Count)
Write-Host $input
if($input) { Write-Host "input is true" }
else { Write-Host "input is false" }
}
}
Now this is where it gets weird
PS C:\Windows\system32> 1..5 | PipelineMadnessProcess
BEGIN
$InputArray.Count 0
$input.Count 0
input is false
PROCESS
$InputArray.Count 1
1
$input.Count 1
1
input is true
PROCESS
$InputArray.Count 1
2
$input.Count 1
2
input is true
...
PROCESS
$InputArray.Count 1
5
$input.Count 1
5
input is true
The BEGIN block does not have any data in there at all. And the process block works well however if you had a foreach like the example it would actually work but it would be running the foreach with 1 entry X times. Or if you passed in the array it would run the foreach once with the full set.
So I guess technically the example would work but it may not work the way you expect it to.
Also note that even though the BEGIN block had no data the function passed syntax validation.
To answer your question, I would say that the first pattern is just a legacy from PowerShell 1.0, you also can use $input in classical functions without Process script block. As far as you are just writting code for PowerShell 2.0 you can forget it.
Regarding pipeline functions, in powerShell V1.0 they can be handled with filters.
you just have to know that it have been done like that when you take samples from the Net or when you have to debug old Powerhell code.
Personally I still use old functions and filters inside my modules I reserve cmdletbinding for export functions or profile functions.
Powershell is a bit like lego blocks, you can do many things in many different ways.
The first form is expecting one or more computer names as string arguments either from an argumentlist or from the pipeline.
The second form is expecting either an array of string arguments from an argument list, or input objects from the pipeline that have the computer names as a property.