Powershell problem with order of execution in script [duplicate] - powershell

This question already has answers here:
PowerShell output is crossing between functions
(1 answer)
How do I prevent Powershell from closing after completion of a script?
(2 answers)
weird delay of the output of an object when followed by start-sleep (or until script end)
(3 answers)
Closed 4 months ago.
I'm having a problem with unexpected order of things being run and returned in the below Powershell script.
The Write-ArrayToTable function is to output the data in the arrays in a pretty table-like fashion via custom object.
The problem is when I call my Write-ArrayToTable function, it then does not return the data until AFTER the Read-Host command returns.
Below is the output from a run of the script, and the code itself is below that. The table output should be displayed BEFORE the Read-Host call but is instead held until the end and then displayed.
What am I missing? Any help greatly appreciated!
Output:
Test: y
Label1 Label2
------ ------
Test1 Test3
Test2 Test4
y
Code:
Function Write-ArrayToTable{
param(
[String[]]$Names,
[Object[][]]$Data
)
for($i = 0;; ++$i){
$Props = [ordered]#{}
for($j = 0; $j -lt $Data.Length; ++$j){
if($i -lt $Data[$j].Length){
$Props.Add($Names[$j], $Data[$j][$i])
}
}
if(!$Props.get_Count()){
break
}
[PSCustomObject]$Props
}
}
$arr1 = #("Test1","Test2")
$arr2 = #("Test3","Test4")
Write-ArrayToTable "Label1","Label2" $arr1,$arr2
Read-Host "Test"

Instead of dropping you objects like this:
[PSCustomObject]$Props
You can be more explicit:
$Props | Out-String
If you want to print all objects in one table, collect them first, before printing:
Function Write-ArrayToTable{
param(
[String[]]$Names,
[Object[][]]$Data
)
$myProps = for($i = 0;; ++$i){
$Props = [ordered]#{}
for($j = 0; $j -lt $Data.Length; ++$j){
if($i -lt $Data[$j].Length){
$Props.Add($Names[$j], $Data[$j][$i])
}
}
if(!$Props.get_Count()){
break
}
[PSCustomObject]$Props
}
$myProps | Format-Table
}

Related

Powershell Script: combine two array in execution command [duplicate]

This question already has answers here:
How can I "zip" two arrays in PowerShell?
(2 answers)
Perform math operations with two equal length arrays
(2 answers)
Powershell foreach regarding multiple collections
(1 answer)
Is there a PowerShell equivalent of `paste` (i.e., horizontal file concatenation)? [duplicate]
(1 answer)
CMD or Powershell command to combine (merge) corresponding lines from two files [duplicate]
(6 answers)
Closed 6 months ago.
still very inexperienced in Powershell, but as is well known, every journey begins with the first steps.
I define two arrays in the script:
$array1 = #("server1", "server2")
$array2 = #("SID1", "SID2")
The SID1 and server1 belong together.
I then want to combine the two arrays using a loop:
Example:
foreach ($i in $array1) {
Write-Host "Server = ${i}"
}
How can I combine the two arrays?
Ideal is:
...
Write-Host "Server=${i}" and SID=${?}"
...
Can the two arrays be built into the foreach so that both values are filled when executing on the write host?
Thanks for your ideas and help.
Many greetings
Carlos
An alternative approach is to use [System.Linq.Enumerable]::Zip which provides convenient API(like python zip function). ie, You could do
$array1 = #("server1", "server2")
$array2 = #("SID1", "SID2")
$doSomethingWithPair = {
param($a, $b)
Write-Output "I have $a and $b"
}
[System.Linq.Enumerable]::Zip(
$array1,
$array2,
[Func[Object, Object, Object]]$doSomethingWithPair
)
This will print
I have server1 and SID1
I have server2 and SID2
Use a for loop to generate a range of valid indices for each array:
for($i = 0; $i -lt $array1.Count; $i++){
Write-Host "Server named '$($array1[$i])' has SID '$($array2[$i])'"
}
But a better solution would be to create a single array of objects that have both pieces of information stored in named properties:
$array = #'
Name,ID
Server1,SID1
Server2,SID2
'# |ConvertFrom-Csv
Now you can use a foreach loop without having to worry about the index:
foreach($computer in $array){
Write-Host "Server named '$($computer.Name)' has SID '$($computer.ID)'"
}
You could use a hash table or ordered dictionary (if you need the keys to keep their order), might be a good approach for your need.
Hash Table
$hash = #{
server1 = "SID1"
server2 = "SID2"
}
Ordered Dictionary
$dict = [ordered]#{
server1 = "SID1"
server2 = "SID2"
}
Then you can iterate over the key / value pairs either using .GetEnumerator():
foreach($pair in $hash.GetEnumerator()) {
Write-Host ("Server={0} and SID={1}" -f $pair.Key, $pair.Value)
}
Or by it's .Keys property:
foreach($key in $hash.PSBase.Keys) {
Write-Host ("Server={0} and SID={1}" -f $key, $hash[$key])
}
$array1 = #("server1", "server2")
$array2 = #("SID1", "SID2")
for($i=0; $i -lt $array1.Count; $i++){
Write-Host $array1[$i] ":"$array2[$i]
}

for loop through 5 textboxes

I have created a GUI with 5 Textboxes. I call them $textboxHost1 - 5.
Now I have an array in which I'm gonna save up to 5 values and then write each value according to the order into the textboxes. The first value in the array should be written into the first $textboxHost1 box.
To do that, I would like to make a for loop and have written this code
#$hostnameneingabe: Array, in which the values are saved.
$hostnameneingabeCount = $hostnameneingabe.Count
for($i = 0; $i -le $hostnameneingabeCount; $i++) {
#code here
}
Now, I'm looking for a way to go down the order, so that the first $textboxHost1 comes firstly and so on.
To be accurate, the variable $textboxHost should be incrementally increased in the loop and the values at the position $i in the array should be written into that textbox.
sth like
for($i = 0; $i -le $hostnameneingabeCount; $i++) {
$textboxHost$i =
}
I suppose you would be liking something like this?
$textboxHosts = Get-Variable | ? {$_.Name -match "textBoxHost[0-9]" -and $_.Value -ne $null} | sort Name
After this you can process that var with eg. a foreach:
foreach ($textboxHost in $textboxHosts) {<# Do some stuff #>}
You have to use an array, because otherwise you can't loop through them:
$textboxHost = #(0..4)
#Textbox 0
$textboxHost[0] = New-Object System.Windows.Forms.TextBox
$textboxHost[0].Text = "test"
#Textbox 1
$textboxHost[1] = New-Object System.Windows.Forms.TextBox
$textboxHost[1].Text = "test"
foreach ($textbox in $textboxHost){
#Do whatever you want with the textbox
$textbox =
}

Powershell script exits ForEach-Object loop prematurely [duplicate]

This question already has answers here:
Why does 'continue' behave like 'break' in a Foreach-Object?
(4 answers)
Closed 5 years ago.
So I've been writing a script that will take all of the data that is stored in 238 spreadsheets and copy it into a master sheet, as well as 9 high level report sheets. I'm really not sure why, but after a specific document, the script ends prematurely without any errors being posted. It's very strange. I'll post some anonymized code below so maybe someone can help me find the error of my ways here.
As far as I can tell, the document that it exits after is fine. I don't see any data errors in it, and the info is copied successfully to the master document before powershell just calls it quits on the script completely.
I've tried changing the size of the data set by limiting only to the folder that contains the problem file. It still ends after the same file with no error output. I cannot upload the file due to company policy, but I really don't see anything different about the data on that one file when compared to any other file of the same nature.
Also, apologies in advance for the crappy code. I'm not a developer and have been relearning powershell since it's the only tool available to me right now.
$StartTime = Get-Date -Format g
Write-Host $StartTime
pushd "Z:\Shared Documents\IO"
$TrackTemplate = "C:\Users\USERNAME\Desktop\IODATA\MasterTemplate.xlsx"
# Initialize the Master Spreadsheet
$xlMaster = New-Object -ComObject Excel.Application
$xlMaster.Visible = $False
$xlMaster.DisplayAlerts = $False
$MasterFilePath = "C:\Users\USERNAME\Desktop\IODATA\Master.xlsx"
Copy-Item $TrackTemplate $MasterFilePath
$wbMaster = $xlMaster.Workbooks.Open($MasterFilePath)
$wsMaster = $wbMaster.Worksheets.Item(2)
$wsMaster.Unprotect("PASSWORD")
$wsMasterRow = 3
# Initialize L4 Document Object
$xlL4 = New-Object -ComObject Excel.Application
$xlL4.Visible = $False
$xlL4.DisplayAlerts = $False
# Initialize object for input documents
$xlInput = New-Object -ComObject Excel.Application
$xlInput.Visible = $False
$xlInput.DisplayAlerts = $False
# Arrays used to create folder path names
$ArrayRoot = #("FOLDER1","FOLDER2","FOLDER3","FOLDER4","FOLDER5","FOLDER6","FOLDER7","FOLDER8","FOLDER9")
$ArrayShort = #("SUB1","SUB2","SUB3","SUB4","SUB5","SUB6","SUB7","SUB8","SUB9")
# $counter is used to iterate inside the loop over the short name array.
$counter = 0
$FileNumber = 0
$TotalFiles = 238
$ArrayRoot | ForEach-Object {
$FilePathL4 = "C:\Users\USERNAME\Desktop\IODATA\ROLLUP\" + $ArrayShort[$counter] + "_DOC_ROLLUP.xlsx"
Copy-Item $TrackTemplate $FilePathL4
$wbL4 = $xlL4.Workbooks.Open($FilePathL4)
$wsL4 = $wbL4.Worksheets.Item(2)
$wsL4.Unprotect("PASSWORD")
$wsL4Row = 3
If ($ArrayShort[$counter] -eq "SUB7") {$FilePath = "Z:\Shared Documents\IO\" + $_ + "\" + $ArrayShort[$counter] + " - DOC v2\"}
Else {$FilePath = "Z:\Shared Documents\IO\" + $_ + "\!" + $ArrayShort[$counter] + " - DOC v2\"}
Get-ChildItem -Path $FilePath | ForEach-Object {
If ($_.Name -eq "SPECIFIC_DOC.xlsx") {Continue}
$FileNumber += 1
Write-Host "$FileNumber / $TotalFiles $_"
$wbInput = $xlInput.Workbooks.Open($_.FullName)
$wsInput = $wbInput.Worksheets.Item(2)
$wsInputLastRow = 0
#Find the last row in the Input document
For ($i = 3; $i -le 10000; $i++) {
If ([string]::IsNullOrEmpty($wsInput.Cells.Item($i,1).Value2)) {
$wsInputLastRow = $i - 1
Break
}
Else { Continue }
}
[void]$wsInput.Range("A3:AC$wsInputLastRow").Copy()
Start-Sleep -Seconds 1
[void]$wsMaster.Range("A$wsMasterRow").PasteSpecial(-4163)
Start-Sleep -Seconds 1
$wsMasterRow += $wsInputLastRow - 2
[void]$wsL4.Range("A$wsL4Row").PasteSpecial(-4163)
Start-Sleep -Seconds 1
$wsL4Row += $wsInputLastRow - 2
$wbInput.Close()
$wbMaster.Save()
}
$counter += 1
$wsL4.Protect("PASSWORD")
$wbL4.Save()
$wbL4.Close()
}
$wsMaster.Protect("PASSWORD")
$wbMaster.Save()
$wbMaster.Close()
$xlMaster.Quit()
$EndTime = Get-Date -Format g
$TimeTotal = New-Timespan -Start $StartTime -End $EndTime
Write-Host $TimeTotal
To continue pipeline processing with the next input object, use return - not continue - in the script block passed to the ForEach-Object cmdlet.
The following simple example skips the 1st object output by Get-ChildItem and passes the remaining ones through:
$i = 0; Get-ChildItem | ForEach-Object{ if ($i++ -eq 0) { return }; $_ }
There is currently (PSv5.1) no direct way to stop the processing of further input objects - for workarounds, see this answer of mine.
By contrast, as you've discovered, break and continue only work as expected in the script block of a for / foreach statement, not directly in the script block passed to the ForeEach-Object cmdlet:
For instance, the following produces no output (using break would have the same effect):
$i = 0; Get-ChildItem | ForEach-Object{ if ($i++ -eq 0) { continue }; $_ }
The reason is that continue and break look for an enclosing for / foreach statement to continue / break out of, and since there is none, the entire command is exited; in a script, the entire script is exited if there's no enclosing for / foreach / switch statement on the call stack.

Returns objects asyn with Format-Table and a specific layout

I want to return an object with a specific layout, each object should be displayed when returned.
If you run this code in PowerShell (not ISE) you see the result not until the function ends.
function Main
{
for ($i = 1; $i -lt 10; $i++)
{
[pscustomobject]#{Index=$i; TimeStamp=(Get-Date)}
Sleep -Seconds 1
}
}
Main | Format-Table #{Label="Index"; Expression={$_.Index};Width=10},
#{Label="TimeStamp"; Expression={$_.TimeStamp};Width=20}
This works, but I want to a specific layout.
function Main
{
for ($i = 1; $i -lt 10; $i++)
{
[pscustomobject]#{Index=$i; TimeStamp=(Get-Date)}
Sleep -Seconds 1
}
}
Main
And why is this behavior different if you execute this script in the ISE?
This line in my powershell profile was responsible for this behavior.
# The following function automatically adds -AutoSize for when I use Format-Wide or Format-Table
$PSDefaultParameterValues['Format-[wt]*:Autosize'] = $True

Simulating `ls` in Powershell

I'm trying to get something that looks like UNIX ls output in PowerShell. This is getting there:
Get-ChildItem | Format-Wide -AutoSize -Property Name
but it's still outputting the items in row-major instead of column-major order:
PS C:\Users\Mark Reed> Get-ChildItem | Format-Wide -AutoSize -Property Name
Contacts Desktop Documents Downloads Favorites
Links Music Pictures Saved Games
Searches Videos
Desired output:
PS C:\Users\Mark Reed> My-List-Files
Contacts Downloads Music Searches
Desktop Favorites Pictures Videos
Documents Links Saved Games
The difference is in the sorting: 1 2 3 4 5/6 7 8 9 reading across the lines, vs 1/2/3 4/5/6 7/8/9 reading down the columns.
I already have a script that will take an array and print it out in column-major order using Write-Host, though I found a lot of PowerShellish idiomatic improvements to it by reading Keith's and Roman's takes. But my impression from reading around is that's the wrong way to go about this. Instead of calling Write-Host, a script should output objects, and let the formatters and outputters take care of getting the right stuff written to the user's console.
When a script uses Write-Host, its output is not capturable; if I assign the result to a variable, I get a null variable and the output is written to the screen anyway. It's like a command in the middle of a UNIX pipeline writing directly to /dev/tty instead of standard output or even standard error.
Admittedly, I may not be able to do much with the array of Microsoft.PowerShell.Commands.Internal.Format.* objects I get back from e.g. Format-Wide, but at least it contains the output, which doesn't show up on my screen in rogue fashion, and which I can recreate at any time by passing the array to another formatter or outputter.
This is a simple-ish function that formats column major. You can do this all in PowerShell Script:
function Format-WideColMajor {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[AllowNull()]
[AllowEmptyString()]
[PSObject]
$InputObject,
[Parameter()]
$Property
)
begin {
$list = new-object System.Collections.Generic.List[PSObject]
}
process {
$list.Add($InputObject)
}
end {
if ($Property) {
$output = $list | Foreach {"$($_.$Property)"}
}
else {
$output = $list | Foreach {"$_"}
}
$conWidth = $Host.UI.RawUI.BufferSize.Width - 1
$maxLen = ($output | Measure-Object -Property Length -Maximum).Maximum
$colWidth = $maxLen + 1
$numCols = [Math]::Floor($conWidth / $colWidth)
$numRows = [Math]::Ceiling($output.Count / $numCols)
for ($i=0; $i -lt $numRows; $i++) {
$line = ""
for ($j = 0; $j -lt $numCols; $j++) {
$item = $output[$i + ($j * $numRows)]
$line += "$item$(' ' * ($colWidth - $item.Length))"
}
$line
}
}
}