Create variable using a string + second variable - powershell

In the beginning of this script I've set some variables ($numof0 = 3, $numof1 = 5, etc.). I'd like to write all those variables to the console, but I would like to be more consise than the 10 write-host statements below.
Write-Host "There are $numOf0 0's"
Write-Host "There are $numOf1 1's"
Write-Host "There are $numOf2 2's"
Write-Host "There are $numOf3 3's"
Write-Host "There are $numOf4 4's"
Write-Host "There are $numOf5 5's"
Write-Host "There are $numOf6 6's"
Write-Host "There are $numOf7 7's"
Write-Host "There are $numOf8 8's"
Write-Host "There are $numOf9 9's"
I figured since all the variables have the same beginning ($numof) and all end with an increasing number, I could do something like this..
$j=0
while($j -lt 10){
$final = '$numof'+"$j"
write-host "There are $final $j's"
$j++
}
Obviously the variable $final is just a string though, and when printed to the console does not show the contents of the corresponding $numofX variable I'd like printed.
Is there a way to create one variable ($final) using a string and another variable (string '$numof' and variable '$j') and still have it reference the original contents of the $numOfX variable?

Invoke-Expression can evaluate your string as if it were typed as a command
$j=0
while($j -lt 10){
$final = Invoke-Expression -Command ('$numof'+"$j")
write-host "There are $final $j's"
$j++
}

Related

Is there any way to colour text previously written using "Write-Host" in powershell?

I want to create the "Select-Multiple" function.
The function takes some parameters, but the most important one is the list of options.
Let's say
#("First Option", "Second Option")
Then the function will display something like:
a All
b First Option
c Second Option
d Exit
Choose your option: > ...
The "Choose your option: > ..." text, will be repeated as long as:
User choose "All" or "Exit" option
User will choose all possible options (other than "All" and "Exit")
At the end the function returns the List of options chosen by the user.
Simple. But... I'd like to highlight the options already chosen by the user.
So if the user chose "b", then "b First Option" gets green colour.
Is it possible to do something like that, without using Clear-Host, as I don't want to clear previous steps?
I attach you my "Select-Multiple" function in powershell, sorry if that's ugly written, but I don't use powershell that often.
function Select-Multiple {
Param(
[Parameter(Mandatory=$false)]
[string] $title,
[Parameter(Mandatory=$false)]
[string] $description,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
$options,
[Parameter(Mandatory=$true)]
[string] $question
)
if ($title) {
Write-Host -ForegroundColor Yellow $title
Write-Host -ForegroundColor Yellow ("-"*$title.Length)
}
if ($description) {
Write-Host $description
Write-Host
}
$chosen = #()
$values = #()
$offset = 0
$all = "All"
$values += #($all)
Write-Host -ForegroundColor Yellow "$([char]($offset+97)) " -NoNewline
Write-Host $all
$offset++
$options.GetEnumerator() | ForEach-Object {
Write-Host -ForegroundColor Yellow "$([char]($offset+97)) " -NoNewline
$values += #($_)
Write-Host $_
$offset++
}
$exit = "Exit"
$values += #($exit)
Write-Host -ForegroundColor Yellow "$([char]($offset+97)) " -NoNewline
Write-Host $exit
$answer = -1
while($chosen.Count -ne $options.Count) {
Write-Host "$question " -NoNewline
$selection = (Read-Host).ToLowerInvariant()
if (($selection.Length -ne 1) -or (([int][char]($selection)) -lt 97 -or ([int][char]($selection)) -gt (97+$offset))) {
Write-Host -ForegroundColor Red "Illegal answer. " -NoNewline
}
else {
$answer = ([int][char]($selection))-97
$value = $($values)[$answer]
if ($value -eq $exit) {
return $chosen
}
if ($value -eq $all) {
return $options
}
else {
if ($chosen.Contains($value)) {
Write-Host -ForegroundColor Red "The value $value was already chosen."
}
else {
$chosen += ($value)
}
}
}
if ($answer -eq -1) {
Write-Host -ForegroundColor Red "Please answer one letter, from a to $([char]($offset+97))"
}
$answer = -1;
}
return $chosen
}
Because of how the console window works, you can't just recolor an existing line. Once you've written something to the console, the only way you can modify it is by overwriting it. This is no different when applying colors.
To understand why this is the case, let's go over how text is colored in PowerShell. Let's use the following command as an example:
Write-Host "Test" -ForegroundColor Green
Here is a (simplified) step by step overview of what this command will do:
Set the ForegroundColor property of the console to green
Write "Test" to the console
Set the ForegroundColor property of the console to whatever it was previously
This explains why we are unable to change the color of text that has already been written to the console. If you want to color a piece of text, you are required to set the console color before writing the text to the console.
However, there are a couple ways to create the same visual effect. In fact there are exactly two ways. One of them is clearing the console and re-writing everything which you mentioned you don't want to do, so let's talk about the other way first.
Overwriting Individual Lines
Let me preface this by saying that this does not work very well with the PowerShell ISE. If you decide to use this, you will have to debug it by either using the normal PowerShell console, or an IDE that supports this. This is also the more complicated option, so if you don't want to deal with the complexity of it, the second option would be the way to go.
The console window allows you to retrieve and set the cursor position by using Console.CursorLeft and Console.CursorTop. You can also use Console.SetCursorPosition() if you need to set both of them at the same time. There is also $Host.UI.RawUI.CursorPosition, but it's long and has some strange side effects when paired with Read-Host, so I would not recommend using it. When you write output to the console, it will write the output to wherever the cursor happens to be. We can use this to our advantage by setting the cursor's position to the beginning of the line we want to change the color of, then overwriting the normal text with colored text or vice versa.
In order to do this, all we need to do is keep track of which option is on which line. This is pretty simple, especially if you have an array of options that is in the same order that you printed them to the console in.
Here is a simple script I made that does exactly this:
$options = $("Option 1", "Option 2", "Option 3")
$initialCursorTop = [Console]::CursorTop
# An array to keep track of which options are selected.
# All entries are initially set to $false.
$selectedOptionArr = New-Object bool[] $options.Length
for($i = 0; $i -lt $options.Length; $i++)
{
Write-Host "$($i + 1). $($options[$i])"
}
Write-Host # Add an extra line break to make it look pretty
while($true)
{
Write-Host "Choose an option>" -NoNewline
$input = Read-Host
$number = $input -as [int]
if($number -ne $null -and
$number -le $options.Length -and
$number -gt 0)
{
# Input is a valid number that corresponds to an option.
$oldCursorTop = [Console]::CursorTop
$oldCursorLeft = [Console]::CursorLeft
# Set the cursor to the beginning of the line corresponding to the selected option.
$index = $number - 1
[Console]::SetCursorPosition(0, $index + $initialCursorTop)
$choice = $options[$index]
$isSelected = $selectedOptionArr[$index]
$choiceText = "$($number). $($choice)"
if($isSelected)
{
Write-Host $choiceText -NoNewline
}
else
{
Write-Host $choiceText -ForegroundColor Green -NoNewline
}
$selectedOptionArr[$index] = !$isSelected
[Console]::SetCursorPosition($oldCursorLeft, $oldCursorTop)
}
# Subtract 1 from Y to compensate for the new line created when providing input.
[Console]::SetCursorPosition(0, [Console]::CursorTop - 1)
# Clear the input line.
Write-Host (' ' * $Host.UI.RawUI.WindowSize.Width) -NoNewline
[Console]::CursorLeft = 0
}
The main advantage of this approach is that it doesn't need to clear the entire console in order to update text. This means you can display whatever you want above it without worrying about it being cleared every time the user inputs something. Another advantage is that this performs a minimal number of operations in order to accomplish the task.
The main disadvantage is that this is relatively volatile. This requires you to use exact line numbers, so if something happens that offsets some of the lines (such as one option being multiple lines), it will more than likely cause some major issues.
However, these disadvantages can be overcome. Since you have access to $Host.UI.RawUI.WindowSize.Width which tells you how many characters you can put on a single line, we know that any string with a length greater than this will be wrapped onto multiple lines. Another option is just to keep track of which line the cursor starts on, then you can clear all text between the starting position and where the cursor currently is.
Clearing the Console
This approach is much simpler because you don't have to worry about what is on which line or where the cursor is at all. The idea is that you simply clear the entire console, then re-write everything with the changes you want to make. This is the nuclear approach, but it's also the most reliable.
Here is the same example as above using this approach instead:
$options = $("Option 1", "Option 2", "Option 3")
# An array to keep track of which options are selected.
# All entries are initially set to $false.
$selectedOptionArr = New-Object bool[] $options.Length
while($true)
{
Clear-Host
for($i = 0; $i -lt $options.Length; $i++)
{
if($selectedOptionArr[$i])
{
Write-Host "$($i + 1). $($options[$i])" -ForegroundColor Green
}
else
{
Write-Host "$($i + 1). $($options[$i])"
}
}
Write-Host # Add an extra line break to make it look pretty
Write-Host "Choose an option>" -NoNewline
$input = Read-Host
$number = $input -as [int]
if($number -ne $null -and
$number -le $options.Length -and
$number -gt 0)
{
# Input is a valid number that corresponds to an option.
$index = $number - 1
$choice = $options[$index]
$selectedOptionArr[$index] = !$selectedOptionArr[$index]
}
}
The main advantage of this approach is that it's super simple and easy to understand.
The main disadvantage is that this clears the entire console every time the user inputs something. In most cases this isn't a huge problem, but it can be with large data sets.
I agree with Trevor Winge, I it is most likely not possible to change earlier console outputs appearance, but it is for certain not worth your while. There are certain limitations of the console, and that is ok since that is what GUIs are for. I hope you feel encouraged by this situation to look into Windows.Froms or WPF. Make use of the controls! checkboxes would be interesting for your scenario. I know that is not exactly what you asked for, but it is garanteed a journey that is worth your time. When i started my first GUI with powershell, i was astonished how much i could accomplish with little afford. Stackoverflow is full of examples.

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

My code will not write "No files to process"

My code will not display "No files to process" on the screen. It is supposed to count the files in a directory and if there are none then it should display "No files to process" and then exit.
# Function Measure, counts files to see if there are any to process.
Function Measure
{
$Measure = ( Get-ChildItem C:\temp\BDMDump\ | Measure-Object ).Count
If ($Measure = "0")
{Write-Host "No files to process"|Exit}
else
{Write-Host "There are files to process.."}
}
I expect to see "No files to process".
There are 4 issues here:
You are using the '=' which is only used for assignment. Use '-eq for comparison.
You are enclosing your integer with quotes, converting it into a string. Just remove the quotes.
As mentioned by ineedalife, you're piping to Exit. You should instead use a semi-colon and the return keyword ; return to exit the function. Even this is probably not needed if the function doesn't do anything else.
You are trying to use "Measure" as the function's name. This is an alias to Measure-Object! Simply change the name to something else, like "Measure-Files"
Additionally, you could remove | Measure-Object because the object System.IO.FileInfo which is returned by Get-ChildItem already has a "Count" method.
Here's a revised copy of your code with all the changes:
Function Measure-Files {
$Measure = Get-ChildItem "C:\temp\BDMDump\"
If ($Measure.Count -eq 0)
{ Write-Host "No files to process"; return }
else
{ Write-Host "There are files to process.." }
}
There are 3 problems
1. Exit can not be Piped to. If you want to exit the session useExit-PSSession this will close the window.
2. "Is equal to" should be changed from = to -eq
3. "0" should be changed to 0 as it is an integer
If ($Measure -eq 0)
{Write-Host "No files to process"|Exit-PSSession}
else
{Write-Host "There are files to process.."}

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.