powershell indentation - powershell

I'm writing a large script that deploys an application. This script is based on several nested function calls.
Is there any way to "ident" the output based on the depth?
For example, I have:
function myFn()
{
Write-Host "Start of myfn"
myFnNested()
Write-Host "End of myfn"
}
function myFnNested()
{
Write-Host "Start of myFnNested"
Write-Host "End of myFnNested"
}
Write-Host "Start of myscript"
Write-Host "End of myscript"
The output of the script will be :
Start of myscript
Start of myfn
Start of myfnNested
End of myFnNested
End of myFn
End of myscript
What I want to achieve is this output :
Start of myscript
Start of myfn
Start of myfnNested
End of myFnNested
End of myFn
End of myscript
As I don't want to hardly code the number of spaces (since I does not know the depth level in complex script). How can I simply reach my goal ?
Maybe something like this?
function myFn()
{
Indent()
Write-Host "Start of myfn"
myFnNested()
Write-Host "End of myfn"
UnIndent()
}
function myFnNested()
{
Indent()
Write-Host "Start of myFnNested"
Write-Host "End of myFnNested"
UnIndent()
}
Write-Host "Start of myscript"
Write-Host "End of myscript"

You could use a wrapper function around write-host which used $MyInvocation to determine the stack depth to create a number of spaces to prefix the message.
Combine this with the -scope ‹n› parameter of Get-Variable to pull out each calling level… something like the showstack function adapted from Windows PowerShell In Action (Payette, 1st Ed):
function ShowStack {
trap { continue; }
0..100 | Foreach-Object {
(Get-Variable -scope $_ 'MyInvocation').Value.PositionMessage -replace "`n"
}
}
You'll need the maximum value of $_ in the pipeline before Get-Variable fails for scope count being too high.

Check out this script http://poshcode.org/scripts/3386.html
If you load up that Write-Verbose wrapper, you can set $WriteHostAutoIndent = $true and then just call Write-Host and it will be indented based on stack depth. So given these functions as you defined them originally:
function myFn()
{
Write-Host "Start of myfn"
myFnNested
Write-Host "End of myfn"
}
function myFnNested()
{
Write-Host "Start of myFnNested"
Write-Host "End of myFnNested"
}
With no changes, you can just dot-source a script file with that Write-Host wrapper function in it:
C:\PS> . C:\Users\Jaykul\Documents\WindowsPowerShell\PoshCode\3386.ps1
And then merely set the preference variable before you call your function:
C:\PS> $WriteHostAutoIndent = $true
C:\PS> myFn
Start of myfn
Start of myFnNested
End of myFnNested
End of myfn
Beautiful indented output, like magic ;-)

You can use Console.CursorLeft to set its position. Be aware that write-output will reset any custom location, so you need to reset it after each output. Here is a sample:
$i = 0
function Indent() {
[console]::CursorLeft += 2
$i = [console]::CursorLeft
$i
}
function UnIndent() {
if($i -gt 0) { $i -= 2 }
[console]::CursorLeft = $i
$i
}
function WriteIndent([string]$s) {
[console]::CursorLeft += $i
write-host $s
# Reset indent, as write-host will set cursor to indent 0
[console]::CursorLeft += $i
}
function myFnNested() {
$i = Indent
WriteIndent "Start of myFnNested"
WriteIndent "End of myFnNested"
$i = UnIndent
}
function myFn() {
$i = Indent
WriteIndent "Start of myfn"
myFnNested
WriteIndent "End of myfn"
$i = UnIndent
}
WriteIndent "Start of myscript"
myFn
WriteIndent "End of myscript"
Output:
PS C:\scripting> .\Indent-Output.ps1
Start of myscript
Start of myfn
Start of myFnNested
End of myFnNested
End of myfn
End of myscript

Write a DEBUG function. Two arguments, one is a flag that takes Start, Stop, or Note; the other argument should be the debug text. (I'd use 1, -1, and 0 for the flag, then you can just add the flag to a Indent variable to set increment depth. Stupid but good enough for debugging.)

I used another line of thinking. I set a count variable that is incremented at the start of the function and then used the following code to pad the line I was writing:
write-host ($string).PadLeft( ($string).length + (2 * $count) )
This will indent two spaces for every recursion.

You can override Write-Host to write your indents before calling the original with the '&' operator and the namespace. You might be able to derive the amount of indentation from the current stack scope, but a global variable gives you more control.
$global:writeHostIndent
function Write-Host
{
Microsoft.PowerShell.Utility\Write-Host (' ' * $global:writeHostIndent) -NoNewline
& 'Microsoft.PowerShell.Utility\Write-Host' $args
}

Related

ArrayList not displaying when first referenced in function

Facing a couple logistical issues in PowerShell - clearly I'm missing a basic concept:
Setup: Create the menu.ps1 file (shown below), launch PowerShell 7.2.2 and call the file locally.
Issues:
The first time you choose option 1 for the ArrayList ($psArrayList), it does not display (although we see from the initial screen load that the items are populated). If you return to the menu and choose option 1 again, it will display on the second pass. ($psArray does load fine on first try, so is this is a type issue.?)
When the script ends, $psArrayList and $psArray are still in the current session variables, as indicated by: Get-Variable psArray*. Even if I instantiate them with $script:psArrayList = [System.Collections.ArrayList]#() and $script:psArray = #() they seem to stay within the session scope. Is there a "right" way to clear them when the ps1 ends?
menu.ps1 contents:
$psArrayList = [System.Collections.ArrayList]#()
# example of populating later in function etc...
$psArrayList.Add([pscustomobject]#{name="bird";color="blue"})
$psArrayList.Add([pscustomobject]#{name="cat";color="orange"})
$psArrayList.Add([pscustomobject]#{name="bear";color="brown"})
$psArray = #()
# example of populating later in function etc...
$psArray += "dog"
$psArray += "fish"
$psArray += "squirrel"
function End-Script {
Remove-Variable psArray*
Exit
}
function Display-Menu {
[int]$choice=-1
Write-Host "This is a menu..." -ForegroundColor Green
Write-Host "Here are your options:"
Write-Host
Write-Host "`t1 - ArrayList"
Write-Host "`t2 - Array"
Write-Host "`t0 - quit (do nothing)"
Write-Host
while ($choice -lt 0) { $choice= Read-Host -Prompt "Choose 1-2 (or 0 to quit)" }
Process-Menu($choice)
}
function Process-Menu([int]$choice) {
switch($choice) {
1 { Write-Host "You chose ArrayList:"; Write-Output $psArrayList }
2 { Write-Host "You chose Array:"; Write-Output $psArray }
0 { Write-Host "You chose to quit. Exiting."; End-Script }
}
$yn=""
while ($yn -eq "") { $yn= Read-Host -Prompt "Return to main menu? (y/n)" }
if ($yn -eq "y") { Display-Menu } else { Write-Host "Ending..."; End-Script }
}
Display-Menu
Regarding the first issue, you would need to use Out-Host or Out-Default so that both outputs (Write-Host together with the arrays) are correctly displayed to the console. See these helpful answers for in depth details on this:
https://stackoverflow.com/a/50416448/15339544
https://stackoverflow.com/a/34858911/15339544
Regarding the second issue, your End-Script function would have a scope issue, Remove-Variable is trying to remove variables defined inside the function's scope (Local), if you want to target the variables defined outside it (Script), you would need to use the -Scope parameter, for example:
function End-Script {
Get-Variable psArray* | Remove-Variable -Scope Script
# `Remove-Variable psArray* -Scope Script` would be valid too
}
From the cmdlet's Parameters section we can read the following for the -Scope parameter:
A number relative to the current scope (0 through the number of scopes, where 0 is the current scope and 1 is its parent)
In that sense, -Scope 1 would also work.
Below you can see an example of your script with some improvements as well as input validation:
$psArrayList = [System.Collections.ArrayList]#()
$psArrayList.AddRange(#(
[pscustomobject]#{name="bird";color="blue"}
[pscustomobject]#{name="cat";color="orange"}
[pscustomobject]#{name="bear";color="brown"}
))
$psArray = "dog", "fish", "squirrel"
function End-Script {
Get-Variable psArray* | Remove-Variable -Scope Script
}
function Display-Menu {
Write-Host "This is a menu..." -ForegroundColor Green
Write-Host "Here are your options:"
Write-Host
Write-Host "`t1 - ArrayList"
Write-Host "`t2 - Array"
Write-Host "`t0 - quit (do nothing)"
Write-Host
# one of many methods for input validation is a Recursive Script Block:
$tryInput = {
try {
[ValidateSet(0, 1, 2)] $choice = Read-Host "Choose 1-2 (or 0 to quit)"
$choice
}
catch {
Write-Warning 'Invalid choice!'
& $tryInput
}
}
Process-Menu (& $tryInput)
}
function Process-Menu([int] $choice) {
switch($choice) {
1 {
Write-Host "You chose ArrayList:"
$psArrayList | Out-Host
}
2 {
Write-Host "You chose Array:"
$psArray | Out-Host
}
0 {
Write-Host "You chose to quit. Exiting."
End-Script
Return # => Exit this function
}
}
$tryInput = {
try {
[ValidateSet('y', 'n')] $choice = Read-Host "Return to main menu? (y/n)"
$choice
}
catch {
Write-Warning 'Invalid choice!'
& $tryInput
}
}
# No need to check for `N`
if((& $tryInput) -eq 'y') { Display-Menu }
}
Display-Menu

PowerShell Switch Executing Again On Quit

I'm attempting to write a PowerShell menu with multiple switches, but can't figure out why previously run commands will execute again on quit. Any help would greatly be appreciated. The code I have so far is as follows:
function Show-Menu {
param (
[string]$Title = 'Menu'
)
Clear-Host
Write-Host "`n============= $Title =============`n"
Write-Host "Press 'A' to run all commands"
Write-Host "Press '1' to run foo"
Write-Host "Press '2' to run bar"
Write-Host "Press 'Q' to quit"
}
do {
Show-Menu
$Selection = Read-Host "`nPlease make a selection"
switch ($Selection) {
'A' {
$Actions = #('foo', 'bar')
}
'1' {
$Actions = "foo"
}
'2' {
$Actions = "bar"
}
}
switch ( $Actions ) {
'foo' {
Write-Host "foo executed"
Start-Sleep -Seconds 2
}
'bar' {
Write-Host "bar executed"
Start-Sleep -Seconds 2
}
}
}
until ($Selection -eq 'q')
Simplify.
Instead of saving actions in a variable, and then taking another step of evaluating that variable... do the actions.
Instead of having a loop that checks the exit condition in a separate location (i.e. using until), use an endless loop and an explicit break. This helps keeping the logic in one place.
function foo { Write-Host "foo"; Start-Sleep -Seconds 1 }
function bar { Write-Host "bar"; Start-Sleep -Seconds 1 }
:menuLoop while ($true) {
Clear-Host
Write-Host "`n============= Menu =============`n"
Write-Host "Press 'A' to run all commands"
Write-Host "Press '1' to run foo"
Write-Host "Press '2' to run bar"
Write-Host "Press 'Q' to quit"
switch (Read-Host "`nPlease make a selection") {
'A' { foo; bar }
'1' { foo }
'2' { bar }
'Q' { break menuLoop }
}
}
Your approach does not work correctly because in your code, pressing Q does not immediately exit the loop, and $Actions is still filled from the last iteration.
That's another lesson: Variable values don't reset on their own in loops. Always set your variables to $null at the start of the loop to get a clean state.
Note the :mainLoop label. Without it, break would only apply to the switch statement itself. See MSDN
That being said, PowerShell has a pretty nifty menu system built-in that you can use.
using namespace System.Management.Automation.Host
function foo { Write-Host "foo"; Start-Sleep -Seconds 1 }
function bar { Write-Host "bar"; Start-Sleep -Seconds 1 }
# set up available choices, and a help text for each of them
$choices = #(
[ChoiceDescription]::new('run &all commands', 'Will run foo, and then bar')
[ChoiceDescription]::new('&1 run foo', 'will run foo only')
[ChoiceDescription]::new('&2 run bar', 'will run bar only')
[ChoiceDescription]::new('&Quit', 'aborts the program')
)
# set up script blocks that correspond to each choice
$actions = #(
{ foo; bar }
{ foo }
{ bar }
{ break menuLoop }
)
:menuLoop while ($true) {
$result = $host.UI.PromptForChoice(
"Menu", # menu title
"Please make a selection", # menu prompt
$choices, # list of choices
0 # default choice
)
& $actions[$result] # execute chosen script block
}
Run this in PowerShell ISE and regular PowerShell to see how it behaves in each environment.
This is happening because of your do...until loop. You're committing to executing the loop before the user input is taken. Since this is the case, $Actions is already set from the previous iteration of the loop, so it runs what had previously run.
This means that if you don't override $Actions for every iteration of the loop, this will happen for other commands as well.
A simple fix to this would be to add a case for q to set $Actions to something that isn't in the switch statement evaluating $Actions. In this case, an empty string should do.
If you need it to work in a similar way for other commands as well, rather than having a case specifically for q, you can use the default case to set the $Actions variable.

PowerShell Show Menu

I have the next code written in PowerShell.
When I run the code, the Menu is shown, I enter the option 1..9 and the selected option is not called, the menu is shown again and again(see the screenshot).
When I enter an option I want to be called that function related to each option entered then to display the message "The function has been called" and also to display the menu to enter a new option. (see the scrennshot - code in C++)
Any idea ?
function PORII
{
Write-Host " PORII was called"
}
function DXD-BODY
{
Write-Host " DXD BODY was called"
}
function DXD-PAINT
{
Write-Host " DXD PAINT was called"
}
function DXD-PTO
{
Write-Host " DXD PTO was called"
}
function DXD-TCF
{
Write-Host " DXD TCF was called"
}
function FIS-SERVERS
{
Write-Host " FIS SERVERS was called"
}
function SERVERS
{
Write-Host " SERVERS was called"
}
function Acronis
{
Write-Host " Acronis was called"
}
function Menu
{
param([string]$Title = 'Menu')
Write-Host " ==================== $Title ==================== "
while (1)
{
Clear-Host
Write-Host " Press 1 for PORII: "
Write-Host " Press 2 for DXD BODY: "
Write-Host " Press 3 for DXD PAINT: "
Write-Host " Press 4 for DXD PTO: "
Write-Host " Press 5 for DXD TCF: "
Write-Host " Press 6 for FIS SERVERS: "
Write-Host " Press 7 for SERVERS: "
Write-Host " Press 8 for Acronis Images: "
Write-Host " Press 9 for Exit: "
$a = Read-Host -Prompt "`n Enter your option "
if (($a -eq 1) -or ($a -eq 2) -or ($a -eq 3) -or ($a -eq 4) -or ($a -eq 5) -or ($a -eq 6) -or ($a -eq 7) -or ($a -eq 8) -or ($a -eq 9))
{
switch($a)
{
1{PORII}
2{DXD-BODY}
3{DXD-PAINT}
4{DXD-PTO}
5{DXD-TCF}
6{FIS-SERVERS}
7{SERVERS}
8{Acronis-Images}
9{Exit}
}
}
else
{
continue
}
}
}
Menu
Your functions are being called, but that isn't obvious because Clear-Host is called right after, discarding the called function's output.
Apart from that, your code can be streamlined:
Instead of the if (($a -eq 1) -or ($a -eq 2) ... conditional, you can add a default branch to your switch statement.
Also note that Read-Host always returns a string, whereas your conditionals operate on numbers; thanks to PowerShell's automatic type conversions, this isn't a problem in your particular case (with explicit or implied equality comparison), but it's something to keep in mind.
Through the use of a hash table you can simplify your function while making it easier to maintain.
Function Menu {
param([string]$Title = 'Menu')
while ($TRUE) {
$OptionHT = #{
1="PORII"
2="DXD-BODY"
3="DXD-PAINT"
4="DXD-PTO"
5="DXD-TCF"
6="FIS-SERVERS"
7="SERVERS"
8="Acronis-Images"
9="Exit"
}
Write-Host " ==================== $Title ==================== "
For ($Cntr = 1 ; $Cntr -lt $($OptionHT.Count) + 1; $Cntr++) {
Write-Host "Press $Cntr for $($OptionHT.$($Cntr)):"
}
$a = Read-Host -Prompt "`n Enter your option "
If (($a.length) -eq 1 -and ([byte][char]$a) -ge 49 -and
([byte][char]$a) -le 57) {
& ($OptionHT.[int]$a)
}
} # End While ($True)
} # End Function Menu
By placing your options in the hash table you only have a single point to make changes for called function names.
The if statement vs switch eliminates any value other than a single single number from 1 to 9 (note use of ASCII values to verify number input) from being processed. And since we have eliminated invalid inputs a single statement can be used to search the hash table for the function to execute.
UPDATE: Per the comments below you'll have to either trap the EXIT (9) with an If statement and exit or Create another Function and call it something like Exit-Program and place the Exit command there, I tested a function and it works.
Note: I didn't clear the console between writes of the menu so you could see your selection as mentioned in the comments you can add it where you deem necessary.
HTH

Powershell terminates early after exiting do-until loop

I'm working on a Powershell script, and I'm trying to accept custom user input from a list of options. The issue I'm running into is that when the exit condition (typing the character 'd') is met, the script terminates and does not execute the remaining code (which is supposed to copy shortcuts based on a user-defined array, $OfficeProgramNames)
#OfficeShortcuts -- Creates shortcuts for the main four Office 2016 applications.
#Optional command-line parameters that can be passed to create icons for All (-a) icons, or a CUSTOM set of icons (-c)
param([switch] $a, [switch] $c)
#Creates a shortcut on the desktop that copies from the start menu shortcuts.
function CreateOfficeDesktopShortcut([string] $ShortcutName)
{
Copy-Item -Path "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\$ShortcutName.lnk" "$env:USERPROFILE\Desktop"
}
function Show-Menu
{
Clear-Host
Write-Host "Select which items you would like placed on the desktop:"
Write-Host "1: Word"
Write-Host "2: Excel"
Write-Host "3: PowerPoint"
Write-Host "4: Outlook"
Write-Host "5: Access"
Write-Host "6: OneNote"
Write-Host "7: Publisher"
Write-Host "D: Press 'D' when done."
}
function GetCustomUserSelection($OfficeProgramNames)
{
$OfficeProgramNames = #()
do
{
Clear-Host
Show-Menu
Write-Host "You have selected: $OfficeProgramNames"
$foo = Read-Host "Please make a selection:"
switch($foo)
{
'1'{
$OfficeProgramNames += "Word*"
}
'2'{
$OfficeProgramNames += "Excel*"
}
'3'{
$OfficeProgramNames += "PowerPoint*"
}
'4'{
$OfficeProgramNames += "Outlook*"
}
'5'{
$OfficeProgramNames += "Access*"
}
'6'{
$OfficeProgramNames += "OneNote*"
}
'7'{
$OfficeProgramNames += "Publisher*"
}
'8'{
$OfficeProgramNames += "Skype*"
}
'd'{
return
}
}
}
until ($foo -eq 'd')
Write-Host "Now do something else"
}
#Loop through the array of Shortcut names, and create a shortcut with a matching alias
function SelectOfficeShortcuts($arg1, $arg2)
{
[string[]] $OfficeProgramNames = #("Word", "Excel", "PowerPoint", "Outlook")
if ($a -eq $True)
{
$OfficeProgramNames += "Access", "OneNote*", "Publisher"
}
elseif($c -eq $True)
{
$OfficeProgramNames = #()
GetCustomUserSelection $OfficeProgramNames
}
for ($i=0; $i -lt $OfficeProgramNames.Length; $i++)
{
CreateOfficeDesktopShortcut $OfficeProgramNames[$i]
}
}
SelectOfficeShortcuts($a, $c)
I have tried inserting a couple of "Write-Host" commands to debug and see where the code stops terminating. I'm rather baffled and I'm having trouble seeing where the control flow is, so if someone could help me with some clarification, that would be greatly appreciated!
For further clarity, the output is as follows(slightly modified since there are some Clear-Host commands):
PS C:\Users\atroach\Documents\GitHub\OfficeShortcuts> .\OfficeShortcuts.ps1 -c
...
Select which items you would like placed on the desktop:
1: Word
2: Excel
3: PowerPoint
4: Outlook
5: Access
6: OneNote
7: Publisher
D: Press 'D' when done.
You have selected: Excel PowerPoint
Please make a selection: d
PS C:\Users\username\Documents\GitHub\OfficeShortcuts>
"SelectOfficeShortcuts ($a, $c)" is called at the end of the script, where $a and $c are switch parameters for the script.

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"
}
}