I have the following code in my powershell script to validate user input (The first positional argument in the script):
function validatePath {
Param
(
[Parameter(Mandatory=$true)]
[ValidateScript({
If ($_ -match "^([a-z]:\\(?:[-\\w\\.\\d])*)") {
$True
} Else {
Write-Host "Please enter a valid path,$_ is not a valid path."
Write-debug $_.Exception
Break
}
})]
[string]$filePath
)
Process
{
Write-Host "The path is "$filePath
}
}
validatePath -filePath $args[0]
My problem is, currently when the validation fails, and the code goes in the Else block and hits Break, instead of stopping the entire script from continue running, it goes to the next block and everything continues and more errors come out.
Question is, how can I modify my code so that When the validation fails to match the regex, it will throw an appropriate error message and stops the entire script from running?
Set the $ErrorActionPreference variable inside the script to Stop:
$ErrorActionPreference = 'Stop'
function validatePath {
Param
(
[Parameter(Mandatory=$true)]
[ValidateScript({
If ($_ -match "^([a-z]:\\(?:[-\\w\\.\\d])*)") {
$True
} Else {
Write-Host "Please enter a valid path,$_ is not a valid path."
Write-debug $_.Exception
Break
}
})]
[string]$filePath
)
Process
{
Write-Host "The path is "$filePath
}
}
validatePath -filePath $args[0]
Do-MoreStuff # this won't execute if parameter validation fails in the previous call
See the about_Preference_Variables help file for more information
use "exit" to stop the entire script. Put any message before the "exit".
Related
I have a powershell script which looks to make sure someone is running the script as an admin.
The variable $currentPrincipal returns true or false.
Here is the code:
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
If ($currentPrincipal -eq $true) {
Write-host "yay"
}
Else {
Write-Host "boo"
}
But when running as an admin AND currentPrincipal is true, it still falls through to the else... here is the CLI which shows that:
PS C:\WINDOWS\system32> C:\Users\dogzilla\Desktop\SetSQLServerToManual.ps1
True
boo
My question is, what is the proper way to evaluate a boolean in powershell?
Your currently just calling WindowsPrincipal.IsInRole, which will output the Boolean result. Additionally, $currentPrincipal is of type System.Security.Principal.WindowsPrincipal, which is not a Boolean. I would ammend your code to store the result of IsInRole() as a variable and checking that instead.
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$checkRole = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
If ($checkRole -eq $true) {
Write-host "yay"
}
Else {
Write-Host "boo"
}
This is the way to test it:
if ( ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) -eq $true )
Let's assume the following CmdLets:
Write-MyColl
function Write-MyColl {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[psobject] $InputObject
)
$verb = Get-Command Write-Verbose
$fixedName = $PSCmdlet.MyInvocation.InvocationName
function Write-Verbose ($Object) {
& $verb "[$fixedName] $Object"
}
if ($InputObject.name) {
Write-Verbose "Writing `$InputObject's Name: `"$($InputObject.name)`""
}
}
New-MyColl
function New-MyColl {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]] $Names
)
$verb = Get-Command Write-Verbose
$fixedName = $PSCmdlet.MyInvocation.InvocationName
function Write-Verbose ($Object) {
& $verb "[$fixedName] $Object"
}
Write-Verbose "Names: $Names"
foreach ($name in $Names) {
#{
id = 123;
name = $name
} | Write-MyColl
}
}
As you can see New-MyColl overrides Microsoft.PowerShell.Utility\Write-Verbose with it's own Write-Verbose. This works perfectly and outputs:
VERBOSE: [New-MyColl] Names: [...]
Now, when Write-MyColl is invoked it should override Function:\Write-Verbose from New-MyColl and I want it to output something like:
VERBOSE: [New-MyColl] [Write-MyColl] Writing `$InputObject's Name: "[...]"
As you might already think, it doesn't work. Write-Verbose recursively calls itself until the end of days. I already tried local function definitions and local variables, but they won't be visible to the invoked CmdLet.
I think for my specific use-case, I will use Get-PsCallStack, however I'd love to see a solution to override overriden functions like described above.
What's hard for me to understand is, why the 2nd Write-Verbose calls itself, so $verb seemingly points to a function, which is not yet defined. Shouldn't it point to the prior defined function? I already tried to Remove-Item the funtion at the end of the script, but it didn't change anything.
Do you have any idea how to achieve this call-stack-behavior?
Thanks to PetSerAI's comment, I came to the following solution:
New-Item function::local:Write-Verbose -Value (
New-Module -ScriptBlock { param($verb, $fixedName, $verbose) } -ArgumentList #(
(Get-Command Write-Verbose),
$PSCmdlet.MyInvocation.InvocationName,
$PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent
)
).NewBoundScriptBlock{
param($Message)
if ($verbose) {
& $verb -Message "=>$fixedName $Message" -Verbose
} else {
& $verb -Message "=>$fixedName $Message"
}
} | Write-Verbose
The -Verbose Parameter isn't passed to the newly created function; that's why I added it to the -ArgumentList. I'm sure, it's not very pretty, but it does exactly what it should. Overwrite Write-Verbose for each call.
Now the output looks like:
VERBOSE: =>[New-MyColl] Names: [...]
VERBOSE: =>[New-MyColl]=>[Write-MyColl] Writing `$InputObject's Name: "[...]"
Let's say I have a script:
write-host "Message.Status: Test Message Status";
I managed to run it in a separate process by doing:
powershell.exe -Command
{ write-host "Message.Status: Test Message Status"; }
The problem is I want to pass parameters to the script so that I can achieve something like this:
write-host "I am in main process"
powershell.exe -Command -ArgumentList "I","am","here"
{
write-host "I am in another process"
write-host "Message.Status: $($one) $($two) $($three)";
}
However -ArgumentList doesn't work here
I get:
powershell.exe : -ArgumentList : The term '-ArgumentList' is not recognized as the name of a cmdlet, function, script file, or operable
I need to run some part of PowerShell script file in a different process and I cannot use another file due to the fact that PowerShell script is uploaded to external system.
The -Command parameter is expecting a scriptblock in which you can define your parameters using a Param() block. Then use the -args parameter to pass in the arguments. Your only mistake was to put the -args after -command before you defined the scriptblock.
So this is how it works:
write-host "I am in main process $($pid)"
powershell.exe -Command {
Param(
$one,
$two,
$three
)
write-host "I am in process $($pid)"
write-host "Message.Status: $($one) $($two) $($three)";
} -args "I", "am", "here" | Out-Null
Output:
I am in main process 17900
I am in process 10284
Message.Status: I am here
You can use the -File parameter and follow it by the path to script. Any unnamed arguments which follows will be passed as script parameters. Something like below should do
powershell -File "C:\ScriptFolder\ScriptwithParameters.ps1" "ParameterOneValu" "valuetwo"
Ok so if you need another process entirely but not another file then your best bet is probably .NET runspaces. Basically wrap your code in a scriptblock
$SB = {
*Your Code*
}
Then set up a runspace like below, making sure to use the "UseNewThread" as the thread option. Note that $arg is whatever your argument to be passed to the script is
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "UseNewThread"
$newRunspace.Open()
$psCmd = [PowerShell]::Create().AddScript($SB).AddArgument($arg)
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()
You'll likely need to tweak this if you need to get any data back from the runspace once it is complete but there are a few ways to do that(leave a comment if you need assistance). If you need synchronous execution rather than async then change .BeginInvoke() to .Invoke()
So should get you started, But it will require a few moving parts.
First we define a new function:
function Run-InNewProcess{
param([String] $code)
$code = "function Run{ $code }; Run $args"
$encoded = [Convert]::ToBase64String( [Text.Encoding]::Unicode.GetBytes($code))
start-process PowerShell.exe -argumentlist '-noExit','-encodedCommand',$encoded
}
This function will be what starts the new process. It uses the start-process cmdlet, The -Argumentlist is our arguments applied to the powershell.exe You can remove -noExit to make the new process close on completion or add other powershell flags, and flags on Start-Process to get the windows and behaviours tweaked to your requirements.
Next we define our script block:
$script = {
[CmdletBinding()]
Param (
[Parameter(Position=0)]
[string]$Arg1,
[Parameter(Position=1)]
[string]$Arg2)
write-host "I am in another process"
write-host "Message.Status: $($Arg1) $($Arg2)";
}
Here we define some parameters in the opening part of the block, They have a position and name, so for example any argument in position 0 will be in the variable $arg1 The rest of the code in the block is all executed in the new process.
Now we have defined the script block and the function to run it in a new process, All we have to do is call it:
Run-InNewProcess $script -Arg1 '"WHAT WHAT"' -Arg2 '"In the But"'
Copy past this code all in to your ISE and you will see it in action.
Start-Job will create a process for its scriptblock, and it's straightforward to pass arguments to it.
Write-Host "Process count before starting job: $((Get-Process |? { $_.ProcessName -imatch "powershell" }).Count)"
$job = Start-Job `
-ArgumentList "My argument!" `
-ScriptBlock {
param($arg)
Start-Sleep -Seconds 5;
Write-Host "All Done! Argument: $arg"
}
while ($job.State -ne "Completed")
{
Write-Host "Process count during job: $((Get-Process |? { $_.ProcessName -imatch "powershell" }).Count)"
Start-Sleep -Seconds 1
}
Receive-Job $job -AutoRemoveJob -Wait
Write-Host "Process count after job: $((Get-Process |? { $_.ProcessName -imatch "powershell" }).Count)"
I am trying to test if two PC's are connected by using the following script
$array ='PC1','PC2'
for ($i=0; $i -lt $array.length; $i++) {
Start-Job –Name TestConnection$i –Scriptblock {
if(test-connection $array[$i] -count 1 -quiet){
write-host Success
}
else { write-host No connection
}
}
}
When I try to do Receive-Job for either one I get "Cannot index into a null array".
What am I doing wrong?
You need to pass in the PC name as an argument, as the array does not exist in the context of the script block, like this:
$array ='PC1','PC2'
for ($i=0; $i -lt $array.Length; $i++) {
Start-Job –Name TestConnection –Scriptblock {
param($pcName)
if(Test-Connection $pcName -Count 1 -Quiet) {
Write-Host Success
} else {
Write-Host No connection
}
} -ArgumentList $array[$i]
}
You have to pass $i (and any other variables) via -ArgumentList through the Start-Job Cmdlet since your script block is running in an entirely different powershell host and doesn't have access to anything inside the shell that started the job.
Even though your script block exists inside the original code, Powershell does not expand any variables in it until it's executing the code in the other host. You can define param() at the beginning of your script block to use the variable you pass via -ArgumentList
Is it possible to redirect stdout from an external program to a variable and stderr from external programs to another variable in one run?
For example:
$global:ERRORS = #();
$global:PROGERR = #();
function test() {
# Can we redirect errors to $PROGERR here, leaving stdout for $OUTPUT?
$OUTPUT = (& myprogram.exe 'argv[0]', 'argv[1]');
if ( $OUTPUT | select-string -Pattern "foo" ) {
# do stuff
} else {
$global:ERRORS += "test(): oh noes! 'foo' missing!";
}
}
test;
if ( #($global:ERRORS).length -gt 0 ) {
Write-Host "Script specific error occurred";
foreach ( $err in $global:ERRORS ) {
$host.ui.WriteErrorLine("err: $err");
}
} else {
Write-Host "Script ran fine!";
}
if ( #($global:PROGERR).length -gt 0 ) {
# do stuff
} else {
Write-Host "External program ran fine!";
}
A dull example however I am wondering if that is possible?
One option is to combine the output of stdout and stderr into a single stream, then filter.
Data from stdout will be strings, while stderr produces System.Management.Automation.ErrorRecord objects.
$allOutput = & myprogram.exe 2>&1
$stderr = $allOutput | ?{ $_ -is [System.Management.Automation.ErrorRecord] }
$stdout = $allOutput | ?{ $_ -isnot [System.Management.Automation.ErrorRecord] }
The easiest way to do this is to use a file for the stderr output, e.g.:
$output = & myprogram.exe 'argv[0]', 'argv[1]' 2>stderr.txt
$err = get-content stderr.txt
if ($LastExitCode -ne 0) { ... handle error ... }
I would also use $LastExitCode to check for errors from native console EXE files.
You should be using Start-Process with -RedirectStandardError -RedirectStandardOutput options. This other post has a great example of how to do this (sampled from that post below):
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
This is also an alternative that I have used to redirect stdout and stderr of a command line while still showing the output during PowerShell execution:
$command = "myexecutable.exe my command line params"
Invoke-Expression $command -OutVariable output -ErrorVariable errors
Write-Host "STDOUT"
Write-Host $output
Write-Host "STDERR"
Write-Host $errors
It is just another possibility to supplement what was already given.
Keep in mind this may not always work depending upon how the script is invoked. I have had problems with -OutVariable and -ErrorVariable when invoked from a standard command line rather than a PowerShell command line like this:
PowerShell -File ".\FileName.ps1"
An alternative that seems to work under most circumstances is this:
$stdOutAndError = Invoke-Expression "$command 2>&1"
Unfortunately, you will lose output to the command line during execution of the script and would have to Write-Host $stdOutAndError after the command returns to make it "a part of the record" (like a part of a Jenkins batch file run). And unfortunately it doesn't separate stdout and stderr.
In case you want to get any from a PowerShell script and to pass a function name followed by any arguments you can use dot sourcing to call the function name and its parameters.
Then using part of James answer to get the $output or the $errors.
The .ps1 file is called W:\Path With Spaces\Get-Something.ps1 with a function inside named Get-It and a parameter FilePath.
Both the paths are wrapped in quotes to prevent spaces in the paths breaking the command.
$command = '. "C:\Path Spaces\Get-Something.ps1"; Get-It -FilePath "W:\Apps\settings.json"'
Invoke-Expression $command -OutVariable output -ErrorVariable errors | Out-Null
# This will get its output.
$output
# This will output the errors.
$errors
Copied from my answer on how to capture both output and verbose information in different variables.
Using Where-Object(The alias is symbol ?) is an obvious method, but it's a bit too cumbersome. It needs a lot of code.
In this way, it will not only take longer time, but also increase the probability of error.
In fact, there is a more concise method that separate different streams to different variable in PowerShell(it came to me by accident).
# First, declare a method that outputs both streams at the same time.
function thisFunc {
[cmdletbinding()]
param()
Write-Output 'Output'
Write-Verbose 'Verbose'
}
# The separation is done in a single statement.Our goal has been achieved.
$VerboseStream = (thisFunc -Verbose | Tee-Object -Variable 'String' | Out-Null) 4>&1
Then we verify the contents of these two variables
$VerboseStream.getType().FullName
$String.getType().FullName
The following information should appear on the console:
PS> System.Management.Automation.VerboseRecord
System.String
'4>&1' means to redirect the verboseStream to the success stream, which can then be saved to a variable, of course you can change this number to any number between 2 and 5.
Separately, preserving formatting
cls
function GetAnsVal {
param([Parameter(Mandatory=$true, ValueFromPipeline=$true)][System.Object[]][AllowEmptyString()]$Output,
[Parameter(Mandatory=$false, ValueFromPipeline=$true)][System.String]$firstEncNew="UTF-8",
[Parameter(Mandatory=$false, ValueFromPipeline=$true)][System.String]$secondEncNew="CP866"
)
function ConvertTo-Encoding ([string]$From, [string]$To){#"UTF-8" "CP866" "ASCII" "windows-1251"
Begin{
$encFrom = [System.Text.Encoding]::GetEncoding($from)
$encTo = [System.Text.Encoding]::GetEncoding($to)
}
Process{
$Text=($_).ToString()
$bytes = $encTo.GetBytes($Text)
$bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes)
$encTo.GetString($bytes)
}
}
$all = New-Object System.Collections.Generic.List[System.Object];
$exception = New-Object System.Collections.Generic.List[System.Object];
$stderr = New-Object System.Collections.Generic.List[System.Object];
$stdout = New-Object System.Collections.Generic.List[System.Object]
$i = 0;$Output | % {
if ($_ -ne $null){
if ($_.GetType().FullName -ne 'System.Management.Automation.ErrorRecord'){
if ($_.Exception.message -ne $null){$Temp=$_.Exception.message | ConvertTo-Encoding $firstEncNew $secondEncNew;$all.Add($Temp);$exception.Add($Temp)}
elseif ($_ -ne $null){$Temp=$_ | ConvertTo-Encoding $firstEncNew $secondEncNew;$all.Add($Temp);$stdout.Add($Temp)}
} else {
#if (MyNonTerminatingError.Exception is AccessDeniedException)
$Temp=$_.Exception.message | ConvertTo-Encoding $firstEncNew $secondEncNew;
$all.Add($Temp);$stderr.Add($Temp)
}
}
$i++
}
[hashtable]$return = #{}
$return.Meta0=$all;$return.Meta1=$exception;$return.Meta2=$stderr;$return.Meta3=$stdout;
return $return
}
Add-Type -AssemblyName System.Windows.Forms;
& C:\Windows\System32\curl.exe 'api.ipify.org/?format=plain' 2>&1 | set-variable Output;
$r = & GetAnsVal $Output
$Meta2=""
foreach ($el in $r.Meta2){
$Meta2+=$el
}
$Meta2=($Meta2 -split "[`r`n]") -join "`n"
$Meta2=($Meta2 -split "[`n]{2,}") -join "`n"
[Console]::Write("stderr:`n");
[Console]::Write($Meta2);
[Console]::Write("`n");
$Meta3=""
foreach ($el in $r.Meta3){
$Meta3+=$el
}
$Meta3=($Meta3 -split "[`r`n]") -join "`n"
$Meta3=($Meta3 -split "[`n]{2,}") -join "`n"
[Console]::Write("stdout:`n");
[Console]::Write($Meta3);
[Console]::Write("`n");