I've got a problem with ForEach loop. Im trying to loop through multiple variables of same kind just increment different.
Im trying to change the TextBox Text depending on if Label from same row has text.
This is how I could make it to just write and IF sentence for each Label but I was looking a way to loop each of these blocks through ForEach loop. I've got total of 8 Labels and Textboxes.
Here is the code: ( Im sure you'll figure out what I'm after :) )
IF ( $Label1.Text.Length -ne 0 )
{
$Label1.Visible = $true
$TextBox1.Visible = $true
$TextBox1.Text = ( "Enter new name for " + $Label1.Text )
}
example of ForEach
$Count = 1..8
$Count | ForEach-Object {
IF ( $Label($_).Text.Length -ne 0 )
{
$Label($_).Visible = $true
$TextBox($_).Visible = $true
$TextBox($_).Text = ( "Enter new name for " + $Label($_).Text )
}
}
etc...
I tried putting variables in array and loop through that way but ofcourse array changes the type to string and it doesnt work...
Give this a try, I can't test it using label & textbox object but it can work tuning it better:
1..8 | ForEach-Object {
IF ( (iex "`$Label$_.Text.Length") -ne 0 )
{
iex "`$Label$_.Visible = `$true"
iex "`$TextBox$_.Visible = `$true"
iex "`$TextBox$_.Text = 'Enter new name for ' + `$Label$_.Text"
}
}
You can use the Get-Variable cmdlet for that purpose:
1..8 | ForEach-Object {
if ( (Get-Variable "Label$_").Value.Text.Length -ne 0 )
{
(Get-Variable "Label$_").Value.Visible = $true
(Get-Variable "Label$_").Value.Visible = $true
(Get-Variable "Label$_").Value.Text = ( "Enter new name for " + (Get-Variable "Label$_").Value.Text )
}
}
Related
I am trying to add a column to data I have imported (and will export) as a CSV.
I am importing a CSV:
What I want to do add another column, perhaps "10/15/22" when the process runs, and then update the values under that date.
In effect, the document will grow to the right, adding a column each time it is run.
I have an object called $test. It will have values like:
$test.users = "7"
$test.SomeBSValue = "22"
$test.Computers = "52"
When all is said and done, my output should look like:
Adding to the list any values I have that are not part of the list already, but recording the values I have under the heading for the date.
So, if the script is run and collects 100 data points, those data point would all be in the CSV under the date.
I would have thought this would be easy, but now I am drawing a complete blank.
I've considered (but have not coded) even trying to put into a GUI grid view and then reading the data back and writing the CSV (but there should be an easier way, right?)
Since you don't actually use it as a CSV we can treat it like regular content.
Say we have a file in C:\test called test.csv that looks as follows:
"Settings","08/15/22","09/15/22"
"Users",0,0
"Computers",0,1
"SomeValue1",0,2
"SomeValue2",0,2
"SomeValue3",0,2
"Stat1",0,10
"Stat2",7,0
"Stat3",0,0
"SomeBSValue",1,2
We can import it, add the row from the object to each corresponding row and right the file to test2.csv.
$test = #{
Settings = "10/15/22"
users = "7"
Computers = "52"
SomeValue1 = "22"
SomeValue2 = "24"
SomeValue3 = "25"
Stat1 = "4"
Stat2 = "3"
Stat3 = "2"
SomeBSValue = "1"
}
$content = Get-Content "C:\test\test.csv"
$newContent = #()
foreach($row in $content){
foreach($key in $test.Keys){
if($row -like "*$key*"){
$row = $row + "," + $test."$key"
$newContent += $row
}
}
}
$newContent | Out-File "C:\test\test2.csv"
After running the script it will have added the values from the object:
"Settings","08/15/22","09/15/22",10/15/22
"Users",0,0,7
"Computers",0,1,52
"SomeValue1",0,2,22
"SomeValue2",0,2,22
"SomeValue3",0,2,22
"Stat1",0,10,4
"Stat2",7,0,4
"Stat3",0,0,4
Edit:
If you want the date between quotes, replace $row = $row + "," + $test."$key" with this:
if($key -eq "Settings"){
$row = $row + "," + '"' + $test."$key" + '"'
}else{
$row = $row + "," + $test."$key"
}
This idea is pretty terrible idea, as you stated, "grow to the right" is definitely not a good approach and you should consider a better way of doing it, data should always expand vertically.
As for the solution, you can create new columns easily with Select-Object and dynamically generated calculated properties.
Note, this should definitely not be considered an efficient approach. This will be slow because Select-Object is slow.
function Add-Column {
param(
[Parameter(ValueFromPipeline, DontShow, Mandatory)]
[object] $InputObject,
[Parameter(Mandatory)]
[string] $ColumnName,
[Parameter(Mandatory)]
[string] $ReferenceProperty,
[Parameter(Mandatory)]
[hashtable] $Values
)
begin {
$calculatedProp = #{ N = $ColumnName }
}
process {
$calculatedProp['E'] = { 0 }
if($value = $InputObject.$ReferenceProperty) {
if($Values.ContainsKey($value)) {
$calculatedProp['E'] = { $Values[$value] }
}
}
$InputObject | Select-Object *, $calculatedProp
}
}
Usage
Import-Csv path\to\csv | Add-Column -ColumnName '09/15/22' -ReferenceProperty Settings -Values #{
users = "7"
SomeBSValue = "22"
Computers = "52"
}
Result
Settings 08/15/22 09/15/22
-------- -------- --------
Users 0 7
Computers 0 52
SomeValue1 0 0
SomeValue2 0 0
SomeValue3 0 0
Stat1 0 0
Stat2 7 0
Stat3 0 0
SomeBSValue 1 22
This function allows then pipe into Export-Csv at ease:
Import-Csv path\to\csv | Add-Column ... | Export-Csv path\to\newCsv
guys i need help here, i want to return the $Location002 and $Location003 content look what is in output
$Location = "westus2"
$Location002 = "westeurope"
$Location003 = "eastasia"
[int]$VMCount = Read-Host "How many VMs?"
1..$VMCount | ForEach-Object {
$i = $_
# define name for VM, will be used for other resources
if ($i -eq 1) {
$locname = "$Location"
Write-Output $locname
}
else {
$locname = $("Location00" + "$i")
Write-Output $locname
}
}
output :
PS C:\Users\Marouane\Desktop\testpowershell> c:\Users\Marouane\Desktop\testpowershell\test.ps1
How many VMs?: 3
westus2
Location002
Location003
PS C:\Users\Marouane\Desktop\testpowershell>
i need to output westeurope and eastasia
Using a separate variable for each value in a group or list of things is a bit of an anti-pattern, you'll want to put them all in an array together instead:
# Define array of possible locations
# `$Locations[0]` will resolve to `westus2`
# `$Locations[1]` will resolve to `westeurope`, etc.
$Locations = #(
"westus2"
"westeurope"
"eastasia"
)
[int]$VMCount = Read-Host "How many VMs?"
1..$VMCount | ForEach-Object {
# Define the VM name
$name = "VirtualMachine$_"
# Pick next location from the $Locations array
# the % ensures we "wrap around" when we reach the end
$location = $Locations[($_ - 1) % $Locations.Length]
# Output a new object with Name + Chosen Location
[pscustomobject]#{
VMName = $name
Location = $location
}
}
Output for 3 VMs:
How many VMs?: 3
VMName Location
------ --------
VirtualMachine1 westus2
VirtualMachine2 westeurope
VirtualMachine3 eastasia
This is not how I would do it, but the more immediate problem is you're assigning a concatenated string to $location and writing to the output stream. I think what you want to do is reference the value of the earlier variable.
There are some clever syntaxes for that. I struggle to remember them. However below would be a start.
$Location = "westus2"
$Location002 = "westeurope"
$Location003 = "eastasia"
[int]$VMCount = Read-Host "How many VMs?"
1..$VMCount | ForEach-Object {
$i = $_
# define name for VM, will be used for other resources
if ($i -eq 1) {
$name = "$VMName"
$locname = "$Location"
Write-Output $locname
}
else {
$name = "$VMName" + "00" + "$i"
$locname = (Get-Variable ("Location00" + "$i")).Value
Write-Output $locname
}
}
Update With Alternative:
I'm still not sure what the goal is, but based on the original sample it would seem there's a 1 to 1 relationship between the location# and the VM number. That said if you go past the number of VMs you would have to adjust this to pick according to the intended pattern...
$Locations = 'westus2', 'westeurope', 'eastasia'
[int]$VMCount = Read-Host 'How many VMs?'
For( $i = 0; $i -lt $VMCount ; ++$i )
{
$Locations[$i]
}
Further Update:
Respective to Mathias's good answer :
$Locations = 'westus2', 'westeurope', 'eastasia'
[int]$VMCount = Read-Host 'How many VMs?'
For( $i = 0; $i -lt $VMCount ; ++$i )
{
$Locations[ $i % $Locations.Count ]
}
Using the modulus operator in this pattern is very efficient for distributing one list over another. I wrote a small post about My Modulus Obsession with this and some other uses.
You'll need to retrieve the variable's content with Get-Variable. You can also avoid the extra step of making an $i variable and instead use the automatic variable $_
$Location = "westus2"
$Location002 = "westeurope"
$Location003 = "eastasia"
[int]$VMCount = Read-Host "How many VMs?"
1..$VMCount | ForEach-Object {
# define name for VM, will be used for other resources
if ($_ -eq 1) {
$name = "$VMName"
$locname = "$Location"
Write-Output $locname
}
else {
$name = "$VMName" + "00" + "$_"
$locname = "Location00" + "$_"
Write-Output (get-variable $locname).value
}
}
I stucked in foreach part.I couldn't find any solution for generating 2 different lists in one foreach loop.I used 2 foreach but it didn't help.Below side I shared my desire output.
My code:
$InStuff = #'
a
b
c
'#.Split("`n").Trim()
$InStuff2 = #'
1
2
3
'#.Split("`n").Trim()
$SPart_1 = 'application="'
$SPart_2 = ' path='
$SPart_3 = ' name='
$SPart_4 = ' application'
foreach ($IS_Item in $InStuff) {
foreach ($IS2_Item in $InStuff2) {
$UName = $IS_Item
$UName2 = $IS2_Item
$Sentence = -join (
$SPart_1, $UName,
$SPart_2, $UName2,
$SPart_3, $UName2,
$SPart_4
)
''
$Sentence
}
}
Fail output :
application="a path=1 name=1 application
application="a path=2 name=2 application
application="a path=3 name=3 application
application="b path=1 name=1 application
application="b path=2 name=2 application
application="b path=3 name=3 application
application="c path=1 name=1 application
application="c path=2 name=2 application
application="c path=3 name=3 application
My desire output :
application="a path=1 name=1 application
application="b path=2 name=2 application
application="c path=3 name=3 application
Thank you
use a for loop:
$InStuff = #'
a
b
c
'#.Split("`n").Trim()
$InStuff2 = #'
1
2
3
'#.Split("`n").Trim()
$SPart_1 = 'application="'
$SPart_2 = ' path='
$SPart_3 = ' name='
$SPart_4 = ' application'
for ($i = 0; $i -lt $InStuff.count; $i++) {
$Sentence = -join (
$SPart_1, $InStuff[$i],
$SPart_2, $InStuff2[$i],
$SPart_3, $InStuff2[$i],
$SPart_4
), ''
$Sentence
}
This will likely go wrong if your input arrays are not the same length, so it is not that safe. Perhaps using a hash or custom object would be a better idea:
$arr = #()
$arr += new-object PSCustomObject -property #{application='a';path=1;name=1}
$arr += new-object PSCustomObject -property #{application='b';path=2;name=2}
$arr += new-object PSCustomObject -property #{application='c';path=3;name=3}
$arr | % { 'application="{0} path={1} name={2}' -f $_.application, $_.path, $_.name }
#arco444 is right, no matter what you will have problems if your lists are different lengths. You should reconsider how you are collecting and formatting the data. Here is an alternative method:
$InStuff = "a","b","c"
$InStuff2 = 1,2,3
$listCount = $InStuff.Count
$x = 0
do {
$strOut = "application= `"path = {0} name = {1} application`"" -f $InStuff[$x], $InStuff2[$x]
$strOut
$x++
}
while ( $x -lt $listCount )
Not sure what you want with a stray " in there, I've added one to enclose the output:
application= "path = a name = 1 application"
application= "path = b name = 2 application"
application= "path = c name = 3 application"
If you plan to use this output for further processing by PowerShell, like putting it in a csv with Export-Csv then you should forgo the application text and create an object instead:
$InStuff = "a","b","c"
$InStuff2 = 1,2,3
$listCount = $InStuff.Count
$x = 0
do {
[pscustomobject]#{
path = $InStuff[$x]
name = $InStuff2[$x]
}
$x++
}
while ( $x -lt $listCount )
While that's not exactly what you are asking for, it's been my experience that data in this format is far more useful:
path name
---- ----
a 1
b 2
c 3
you can add lines to
[pscustomobject]#{
path = $InStuff[$x]
name = $InStuff2[$x]
}
for the additional text (if it's a must) and do something like this:
[pscustomobject]#{
type = "application"
path = $InStuff[$x]
name = $InStuff2[$x]
}
and that will add a column for the word application
say I have an array
$something = #(
"first",
"second"
)
how can I display this to the user as
1. first
2. second
Selection :
I am able to do this by hash table and manually mapping
#{
1="first"
2="second"
};
and doing the following
$something.Keys | sort |% { Write-Host $_ ")" $something.Item($_) }
[int32]$constuctPayload.Action = Read-Host
but if need to perform this using an array how can I do this. I.e looping over the item and displaying with index for user selection. ?
You could use the IndexOf() method, to find the index in the array.
$something | ForEach-Object {Write-Host "$([Array]::IndexOf($something, $_)). $_ "}
Standard warning about being careful with Write-Host. Also you might want to look into Out-GridView.
Use a for loop to iterate over the elements of the array and prepend each value with the index + 1.
$something = 'first', 'second'
for ($i = 0; $i -lt $something.Count; $i++) {
Write-Host ('{0}. {1}' -f ($i+1), $something[$i])
}
[int32]$constuctPayload.Action = Read-Host -Prompt 'Selection'
I would recommend using the PromptForChoice() method over Read-Host, though:
$something = '&first', '&second'
$title = 'The title.'
$msg = 'Selection?'
$choices = $something | ForEach-Object {
New-Object Management.Automation.Host.ChoiceDescription $_
}
$options = [Management.Automation.Host.ChoiceDescription[]] $choices
$default = 0
$constuctPayload.Action = $Host.UI.PromptForChoice($title, $msg, $options, $default)
Say I have MyScript.ps1:
[cmdletbinding()]
param (
[Parameter(Mandatory=$true)]
[string] $MyInput
)
function Show-Input {
param ([string] $Incoming)
Write-Output $Incoming
}
function Save-TheWorld {
#ToDo
}
Write-Host (Show-Input $MyInput)
Is it possible to dot source the functions only somehow? The problem is that if the script above is dot sourced, it executes the whole thing...
Is my best option to use Get-Content and parse out the functions and use Invoke-Expression...? Or is there a way to access PowerShell's parser programmatically? I see this might be possible with PSv3 using [System.Management.Automation.Language.Parser]::ParseInput but this isn't an option because it has to work on PSv2.
The reason why I'm asking is that i'm trying out the Pester PowerShell unit testing framework and the way it runs tests on functions is by dot sourcing the file with the functions in the test fixture. The test fixture looks like this:
MyScript.Tests.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
. "$here\$sut"
Describe "Show-Input" {
It "Verifies input 'Hello' is equal to output 'Hello'" {
$output = Show-Input "Hello"
$output.should.be("Hello")
}
}
Using Doug's Get-Function function you could include the functions this way:
$script = get-item .\myscript.ps1
foreach ($function in (get-function $script))
{
$startline = $function.line - 1
$endline = $startline
$successful = $false
while (! $successful)
{
try {
$partialfunction = ((get-content $script)[$startline..$endline]) -join [environment]::newline
invoke-expression $partialfunction
$successful = $true
}
catch [Exception] { $endline++ }
}
}
Edit: [System.Management.Automation.IncompleteParseException] can be used instead of [Exception] in Powershell V2.
Note -- if you find this answer helpful please upvote jonZ's answer as I wouldn't of been able to come up with this if it weren't for his helpful answer.
I created this function extractor function based on the script #jonZ linked to. This uses [System.Management.Automation.PsParser]::Tokenize to traverse all tokens in the input script and parses out functions into function info objects and returns all function info objects as an array. Each object looks like this:
Start : 99
Stop : 182
StartLine : 7
Name : Show-Input
StopLine : 10
StartColumn : 5
StopColumn : 1
Text : {function Show-Input {, param ([string] $Incoming), Write-Output $Incoming, }}
The text property is a string array and can be written to temporary file and dot sourced in or combined into a string using a newline and imported using Invoke-Expression.
Only the function text is extracted so if a line has multiple statements such as: Get-Process ; function foo () { only the part relevant to the function will be extracted.
function Get-Functions {
param (
[Parameter(Mandatory=$true)]
[System.IO.FileInfo] $File
)
try {
$content = Get-Content $File
$PSTokens = [System.Management.Automation.PsParser]::Tokenize($content, [ref] $null)
$functions = #()
#Traverse tokens.
for ($i = 0; $i -lt $PSTokens.Count; $i++) {
if($PSTokens[$i].Type -eq 'Keyword' -and $PSTokens[$i].Content -eq 'Function' ) {
$fxStart = $PSTokens[$i].Start
$fxStartLine = $PSTokens[$i].StartLine
$fxStartCol = $PSTokens[$i].StartColumn
#Skip to the function name.
while (-not ($PSTokens[$i].Type -eq 'CommandArgument')) {$i++}
$functionName = $PSTokens[$i].Content
#Skip to the start of the function body.
while (-not ($PSTokens[$i].Type -eq 'GroupStart') -and -not ($PSTokens[$i].Content -eq '{')) {$i++ }
#Skip to the closing brace.
$startCount = 1
while ($startCount -gt 0) { $i++
if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -eq '{') {$startCount++}
if ($PSTokens[$i].Type -eq 'GroupEnd' -and $PSTokens[$i].Content -eq '}') {$startCount--}
}
$fxStop = $PSTokens[$i].Start
$fxStopLine = $PSTokens[$i].StartLine
$fxStopCol = $PSTokens[$i].StartColumn
#Extract function text. Handle 1 line functions.
$fxText = $content[($fxStartLine -1)..($fxStopLine -1)]
$origLine = $fxText[0]
$fxText[0] = $fxText[0].Substring(($fxStartCol -1), $fxText[0].Length - ($fxStartCol -1))
if ($fxText[0] -eq $fxText[-1]) {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol - ($origLine.Length - $fxText[0].Length)))
} else {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol))
}
$fxInfo = New-Object -TypeName PsObject -Property #{
Name = $functionName
Start = $fxStart
StartLine = $fxStartLine
StartColumn = $fxStartCol
Stop = $fxStop
StopLine = $fxStopLine
StopColumn = $fxStopCol
Text = $fxText
}
$functions += $fxInfo
}
}
return $functions
} catch {
throw "Failed in parse file '{0}'. The error was '{1}'." -f $File, $_
}
}
# Dumping to file and dot sourcing:
Get-Functions -File C:\MyScript.ps1 | Select -ExpandProperty Text | Out-File C:\fxs.ps1
. C:\fxs.ps1
Show-Input "hi"
#Or import without dumping to file:
Get-Functions -File C:\MyScript.ps1 | % {
$_.Text -join [Environment]::NewLine | Invoke-Expression
}
Show-Input "hi"