PowerShell: Running executable in variable without misinterpetting argument as part of path - powershell

I need to benchmark two different programs on two diferent corpuses, and in order to get more accurate readings, I want to run the benchmark in a loop and take the mean execution time for each benchmark. In order to simplify for myself, I wrote the following PowerShell function:
Function Benchmark {
Param($progPath, $benchmarkPath, $iters=27)
$time = (Measure-Command { & "$progPath" "$benchmarkPath" }).TotalSeconds
$sum = $lowest = $highest = $time
for($i = 1; $i -lt $iters; $i++) {
$time = (Measure-Command { & "$progPath" "$benchmarkPath" }).TotalSeconds
$sum += $time
if($time -lt $lowest) { $lowest = $time }
elseif($time -gt $highest) {$highest = $time }
}
$sum -= ($lowest + $highest)
$sum / ($iters - 2)
}
In theory, this should execute the program supplied as a command in $progPath with the benchmarking script in $benchmarkPath as its argument, but when I run it like this, I get the following result:
PS > $nonPrivateBenchmark = Benchmark(".\Python\PCbuild\amd64\python", ".\Benchmarks\non_private_access.py")
& : The term '.\Python\PCbuild\amd64\python .\Benchmarks\non_private_access.py' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:3 char:30
+ $time = (Measure-Command { & "$progPath" "$benchmarkPath" }).TotalSeconds
+ ~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (.\Python\PCbuil...ivate_access.py:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
(Plus 26 repetitions of the same error but on line 6.)
However, if assign the three parameter arguments and copy the remaining function body directly into PowerShell, it works and sets $nonPrivateAccess to a reasonable value:
$progPath = ".\Python\PCbuild\amd64\python"
$benchmarkPath = ".\Benchmarks\non_private_access.py"
$iters = 27
$time = (Measure-Command { & "$progPath" "$benchmarkPath" }).TotalSeconds
$sum = $lowest = $highest = $time
for($i = 1; $i -lt $iters; $i++) {
$time = (Measure-Command { & "$progPath" "$benchmarkPath" }).TotalSeconds
$sum += $time
if($time -lt $lowest) { $lowest = $time }
elseif($time -gt $highest) {$highest = $time }
}
$sum -= ($lowest + $highest)
$nonPrivateBenchmark = $sum / ($iters - 2)
I have through experimentation concluded that the problem is that "$progPath" "$benchmarkPath" is concatenated into the single string '.\Python\PCbuild\amd64\python .\Benchmarks\non_private_access.py' before executed with the & operator, and the space separating them is interpretted as a part of the command name, making PowerShell try to execute the entire string as a single command (which can't be executed). I have tried putting escaped quotes both around and inside the argument parameter, but to no avail. Does anyone else have a solution to this problem?
PS:
Quite extensive searching has only given me a lot of hits with people having the opposite problem. Could it be that I have some non-default PowerShell settings activated making it parse the spaces overly aggressively?

Benchmark(".\Python\PCbuild\amd64\python", ".\Benchmarks\non_private_access.py")
This syntax is passing an array to the first parameter of your Benchmark function, which then gets converted to a single string when it's used as a command. This is effectively the same as:
Benchmark ".\Python\PCbuild\amd64\python", ".\Benchmarks\non_private_access.py"
The normal syntax for passing multiple parameters to a PowerShell function is to place a space between the parameters:
Benchmark ".\Python\PCbuild\amd64\python" ".\Benchmarks\non_private_access.py"
You can also use the parameter names:
Benchmark -progPath ".\Python\PCbuild\amd64\python" -benchmarkPath ".\Benchmarks\non_private_access.py"

Related

One powershell script calling another but makes the script being called break

Currently, I am struggling to run a powershell script within another powershell GUI. I am trying to program it so that once a button is pressed, another powershell script would begin to run. The issue is that the second powershell script being ran does not run correctly when called. (It runs perfectly fine on its own.)
Code for the button:
$handler_runBTN_Click =
{
if($compliance){#runs Compliance training scripts
.\"Follow-up Email Generator.ps1"
}
}
$runBTN.Text = "Run"
$System_Drawing_Point.X = 140
$System_Drawing_Point.Y = 170
$runBTN.Location = $System_Drawing_Point
$runBTN.add_Click($handler_runBTN_Click)
$runBTN.FlatStyle = "Standard"
$emailForm.Controls.Add($runBTN)
Code not running properly when being called by button (It is called by other code but that runs correctly):
function csvCheck($intervalDate, $reminderDate) { #gathers all courses due on a specific date for a user through a csv file
for ($j = 0; $j -lt $row_count.Count; $j++) {#goes through every row of CSV file
if ($global:skip -eq $j ) {continue} #if the emails under $j are in the array $skip, the loop will skip it and continue
$requiredDate = $file[$j].'Required Date'
if (($requiredDate -eq $reminderDate)) { #courses that are coming due
$global:skip += $j #skips the iteration so it does not occur again
$bodyText = "<li>" + $file[$j].'Curriculum #' + "- <strong>Due: " + $file[$j].'Required Date' + " </strong></li>"
for ($k = $j + 1; $k -lt $row_count.Count; $k++) { #checks every other date for roccurances
if ($file[$j].'Employee ID' -eq $file[$k].'Employee ID' -and $file[$j].'Required Date' -eq $file[$k].'Required Date') {
#checks for any other courses for the same user due on the same id
$file[$k].'Last Sent' = $today
$bodyText += "<li>" + $file[$k].'Curriculum #' + "- <strong>Due: " + $file[$k].'Required Date' + " </strong></li>" #Adds onto the default bullet form text
$global:skip += $k
}
}
email $bodyText "due in $intervalDate day(s):" "comingDue"
}
}
}
I was wondering if it was my code that was causing this to happen or if it had something to do with the nature of the button.
Thanks,
Turns out that I was calling my second script incorrectly.
This line of code ran the second script under the same context as the first causing $global: variables to not work correctly.
.\"Follow-up Email Generator.ps1"
To solve this, I changed the code that runs the second script to:
Powershell -File ".\Follow-up Email Generator.ps1"
This ran the second script in its separate context allowing the $global: variables to work

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.

Is there a better way to convert all control characters to entities in PowerShell 5?

Context: Azure, Windows Server 2012, PowerShell 5
I've got the following code to convert all control characters (ascii and unicode whitespace other than \x20 itself) to their ampersand-hash equivalents.
function ConvertTo-AmpersandHash {
param ([Parameter(Mandatory)][String]$Value)
# there's got to be a better way of doing this.
$AMPERHASH = '&#'
$SEMICOLON = ';'
for ($i = 0x0; $i -lt 0x20; $i++) {
$value = $value -replace [char]$i,($AMPERHASH + $i + $SEMICOLON)
}
for ($i = 0x7f; $i -le 0xa0; $i++) {
$value = $value -replace [char]$i,($AMPERHASH + $i + $SEMICOLON)
}
return $Value
}
As can be seen by the embedded comment, I'm sure there's a better way to do this. As it stands, one does some 65 iterations for each incoming string. Would regular expressions work better/faster?
LATER
-replace '([\x00-\x1f\x7f-\xa0])',('&#' + [byte][char]$1 + ';')
looks promising but the $1 is evaluating to zero all the time, giving me  all the time.
LATER STILL
Thinking that -replace couldn't internally iterate, I came up with
$t = [char]0 + [char]1 + [char]2 + [char]3 + [char]4 + [char]5 + [char]6
$r = '([\x00-\x1f\x7f-\xa0])'
while ($t -match [regex]$r) {
$t = $t -replace [regex]$r, ('&#' + [byte][char]$1 + ';')
}
echo $t
However out of that I still get

FINALLY
function ConvertTo-AmpersandHash {
param ([Parameter(Mandatory)][String]$Value)
$AMPERHASH = '&#'
$SEMICOLON = ';'
$patt = '([\x00-\x1f\x7f-\xa0]{1})'
while ($Value -match [regex]$patt) {
$Value = $Value -replace $Matches[0], ($AMPERHASH + [byte][char]$Matches[0] + $SEMICOLON)
}
return $Value
}
That works better. Faster too. Any advances on that?
Kory Gill's answer with the library call is surely a better approach, but to address your regex question, you can't evaluate code in the replacement with the -replace operator.
To do that, you need to use the .Net regex replace method, and pass it a scriptblock to evaluate the replacement, which takes a parameter of the match. e.g.
PS C:\> [regex]::Replace([string][char]2,
'([\x00-\x20\x7f-\xa0])',
{param([string]$m) '&#' + [byte][char]$m + ';'})

Your question is a little unclear to me, and could be a duplicate of What is the best way to escape HTML-specific characters in a string (PowerShell)?.
It would be nice if you explicitly stated the exact string you have and what you want it to converted to. One has to read the code to try to guess.
I am guessing one or more of these functions will do what you want:
$a = "http://foo.org/bar?baz & also <value> conversion"
"a"
$a
$b = [uri]::EscapeDataString($a)
"b"
$b
$c = [uri]::UnescapeDataString($b)
"c"
$c
Add-Type -AssemblyName System.Web
$d = [System.Web.HttpUtility]::HtmlEncode($a)
"d"
$d
$e = [System.Web.HttpUtility]::HtmlDecode($d)
"e"
$e
Gives:
a
http://foo.org/bar?baz & also <value> conversion
b
http%3A%2F%2Ffoo.org%2Fbar%3Fbaz%20%26%20also%20%3Cvalue%3E%20conversion
c
http://foo.org/bar?baz & also <value> conversion
d
http://foo.org/bar?baz & also <value> conversion
e
http://foo.org/bar?baz & also <value> conversion
I have one small function which helps me replacing as per my requirement:
$SpecChars are all the characters that are going to be replaced with nothing
Function Convert-ToFriendlyName
{param ($Text)
# Unwanted characters (includes spaces and '-') converted to a regex:
$SpecChars = '\', ' ','\\'
$remspecchars = [string]::join('|', ($SpecChars | % {[regex]::escape($_)}))
# Convert the text given to correct naming format (Uppercase)
$name = (Get-Culture).textinfo.totitlecase(“$Text”.tolower())
# Remove unwanted characters
$name = $name -replace $remspecchars, ""
$name
}
Example: Convert-ToFriendlyName "My\Name\isRana\Dip " will result me "MyNameIsranaDip".
Hope it helps you.

Powershell : Why I cannot use ForEach-Object to modify elements?

I've got an array definition:
> $a={"abc","xyz","hello"}
Then, using foreach to modify it, but seems original elements are not changed:
> foreach($i in $a){$i="kkk"+$i}
> $a
> "abc","xyz","hello"
Why foreach loop doesn't modify elements?
Then I tried to use ForEach-Object, this time, it doesn't even run:
> $a|%{$_=$_+"kkk"}
Method invocation failed because [System.Management.Automation.ScriptBlock] does not contain a method named 'op_Addition'.
At line:1 char:6
+ $a|%{$_=$_+"kkk"}
+ ~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Addition:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Does this has any syntax error? Or my understanding is incorrect?
You don't have an array in $a, you have a [ScriptBlock]. Curly braces {} denote a script block in PowerShell.
The array operator is #().
This is the cause of the error in the second example.
$a=#("abc","xyz","hello")
foreach($i in $a) {
$i = "zzz" + $i
}
$a | % { $_ += "zzz" }
However, this still will not work, because $i and $_ are copies, not references back to the original array location.
Instead, you can iterate over the array using a regular for loop:
for( $i = 0 ; $i -lt $a.Count ; $i++ ) {
$a[$i] += "zzz"
}
In this example you can see that in the loop body, you are referring directly to the array in $a and modifying its actual value.
Also note that ForEach-Object (%) returns a value (or all of the values returned from the block), so you could also do this:
$a = $a | % { "qqq" + $_ }
However this is forming a brand new array and assigning it to $a, not really modifying the original.

Powershell : Cannot compare array.Length with int32

I am creating a ps script that will handle different user commands.
Since every function the user can call has a different number of parameters I just wanted to pass the rest of the usrInput[] array starting from index 1 as a parameter of the function for usrInput[0].
This is what I did:
$function
while($function -ne "backup" -or $function -ne "restore") { #Wait for user to make a valid input
$usrInput = Read-Host "Enter -backup args[] or -restore args[] to backup or restore vms" -split " "
$args
for ($i = 1, $i -le $usrInput.Length, $i++) {
$args[$i -1] = $usrInput[$i]
}
if ($usrInput[0] -eq "-backup") {
backup($args)
}
elseif ($usrInput[0] -eq "-restore") {
restore($args)
}
}
Now what I get is the following (English equivalent to the german shell output):
Enter -backup args[] or -restore args[] to back
"last comment in my code"
"1" couln't be compared to "96 62".
couldn't be converted to "System.Int32"
in line :5 letter:6
+ for ($i = 1, $i -le $inputLength, $i++) {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation:
+ FullyQualifiedErrorId : ComparisonFailure
Enter -backup args[] or -restore args[] to back
Why is that?
I thought array.Lengths type was int!?
(Note: I also tried putting and [int] before it, but it didn't work)
Thank you very much in advance for your help.
The problem is the for statement you should write it like this:
for ($i = 1; $i -le $input.Length; $i++)
with ";" and not ",".
I didn't check the rest of the code as your question was about $input.length and int32.