IndexOutOfRange - powershell

I'm getting this error:
Array assignment failed because index '3' was out of range.
At Z:\CSP\deploys\aplicacional\teste.ps1:71 char:12
+ $UNAME[ <<<< $i]= $line
+ CategoryInfo : InvalidOperation: (3:Int32) [], RuntimeException
+ FullyQualifiedErrorId : IndexOutOfRange
I really can't find why the index end there.
$CSNAME = #(KPScript -c:GetEntryString $PASSHOME\$PASSFILE -pw:$PASS -Field:csname $SEARCH)
$UNAME = #()
$i = 0
Write-Host "Length="$CSNAME.Length
while($i -le $CSNAME.Length)
{
Write-Host "Start "$i
#$CSNAME[$i].GetType()
if ($CSNAME[0].StartsWith("OK:")) {
Write-Host "ACES $ACES does not exist" -Foreground "red"
}
if ($CSNAME[$i].StartsWith("OK:")) {
break
}
Write-Host "CSNAME="$CSNAME[$i]
$UNAME = $UNAME + $i
$UNAME = KPScript -c:GetEntryString $PASSHOME\$PASSFILE -pw:$PASS -Field:UserName -ref-csname:$CSNAME[$i]
foreach ($line in $UNAME) {
if (! ($line.StartsWith("OK:"))) {
Write-Host $i
$UNAME = $UNAME + $i
Write-Host "uname var"$i
$UNAME[$i] = $line
} else {
Write-Host "break"
break
}
}
#$UNAME[$i].GetType()
#if ($UNAME[$i].StartWith("OK:*")){
# break
#}
Write-Host "UNAME="$UNAME[$i]
#$UNAME[$i]
Write-Host "End "$i
$i += 1
Write-Host "switch"
}
Since the second while is based in the first array length and it has values, why is the it getting out of range?

PowerShell arrays are zero-based, so an array of length 3 has index values from 0 through 2. Your code, however, would iterate from 0 to 3, because the loop condition checks if the variable is less or equal the length (-le):
while($i -le $CSNAME.Length)
{
...
}
You need to check if the variable is less than the length (or less or equal the length minus one):
while($i -lt $CSNAME.Length)
{
...
}
Also, you'd normally use a for loop for iterating over an array, so you can handle the index variable in one place:
for ($i=0; $i -lt $CSNAME.Length; $i++) {
...
}
Edit: You initialize $UNAME as an array, but inside the loop you assign $UNAME = KPScript ..., which replaces the array with whatever the script returns (another array, a string, $null, ...). Don't use the same variable for different things in a loop. Assign the script output to a different variable. Also, your way of appending to the array is rather convoluted. Instead of $UNAME = $UNAME + $i; $UNAME[$i] = $line simply do $UNAME += $line.
$res = KPScript -c:GetEntryString $PASSHOME\$PASSFILE -pw:$PASS -Field:UserName -ref-csname:$CSNAME[$i]
foreach ($line in $res) {
if (! ($line.StartsWith("OK:"))) {
$UNAME += $line
} else {
break
}
}

Related

Powershell - F5 iRules -- Extracting iRules

I received a config file of a F5 loadbalancer and was asked to parse it with PowerShell so that it creates a .txt file for every iRule it finds. I'm very new to parsing and I can't seem to wrap my head around it.
I managed to extract the name of every rule and create a separate .txt file, but I am unable to wring the content of the rule to it. Since not all rules are identical, I can't seem to use Regex.
Extract from config file:
ltm rule /Common/irule_name1 {
SOME CONTENT
}
ltm rule /Common/irule_name2 {
SOME OTHER CONTENT
}
What I have for now
$infile = "F5\config_F5"
$ruleslist = Get-Content $infile
foreach($cursor in $ruleslist)
{
if($cursor -like "*ltm rule /*") #new object started
{
#reset all variables to be sure
$content=""
#get rulenames
$rulenameString = $cursor.SubString(17)
$rulename = $rulenameString.Substring(0, $rulenameString.Length -2)
$outfile = $rulename + ".irule"
Write-Host $outfile
Write-Host "END Rule"
#$content | Out-File -FilePath "F5/irules/" + $outfile
}
}
How can I make my powershell script read out what's between the brackets of each rule? (In this case "SOME CONTENT" & "SOME OTHER CONTENT")
Generally parsing involves converting a specific input ("string") into an "object" which PowerShell can understand (such as HTML, JSON, XML, etc.) and traverse by "dotting" through each object.
If you are unable to convert it into any known formats (I am unfamiliar with F5 config files...), and need to only find out the content between braces, you can use the below code.
Please note, this code should only be used if you are unable to find any other alternative, because this should only work when the source file used is code-correct which might not give you the expected output otherwise.
# You can Get-Content FileName as well.
$string = #'
ltm rule /Common/irule_name1 {
SOME CONTENT
}
ltm rule /Common/irule_name2 {
SOME OTHER CONTENT
}
'#
function fcn-get-content {
Param (
[ Parameter( Mandatory = $true ) ]
$START,
[ Parameter( Mandatory = $true ) ]
$END,
[ Parameter( Mandatory = $true ) ]
$STRING
)
$found_content = $string[ ( $START + 1 ) .. ( $END - 1 ) ]
$complete_content = $found_content -join ""
return $complete_content
}
for( $i = 0; $i -lt $string.Length; $i++ ) {
# Find opening brace
if( $string[ $i ] -eq '{' ) {
$start = $i
}
# Find ending brace
elseif( $string[ $i ] -eq '}' ) {
$end = $i
fcn-get-content -START $start -END $end -STRING $string
}
}
For getting everything encompassed within braces (even nested braces):
$string | Select-String '[^{\}]+(?=})' -AllMatches | % { $_.Matches } | % { $_.Value }
To parse data with flexible structure, one can use a state machine. That is, read data line by line and save the state in which you are. Is it a start of a rule? Actual rule? End of rule? By knowing the current state, one can perform actions to the data. Like so,
# Sample data
$data = #()
$data += "ltm rule /Common/irule_name1 {"
$data += "SOME CONTENT"
$data += "}"
$data += "ltm rule /Common/irule_withLongName2 {"
$data += "SOME OTHER CONTENT"
$data += "SOME OTHER CONTENT2"
$data += "}"
$data += ""
$data += "ltm rule /Common/irule_name3 {"
$data += "SOME DIFFERENT CONTENT"
$data += "{"
$data += "WELL,"
$data += "THIS ESCALATED QUICKLY"
$data += "}"
$data += "}"
# enum is used for state tracking
enum rulestate {
start
stop
content
}
# hashtable for results
$ht = #{}
# counter for nested rules
$nestedItems = 0
# Loop through data
foreach($l in $data){
# skip empty lines
if([string]::isNullOrEmpty($l)){ continue }
# Pick the right state and keep count of nested constructs
if($l -match "^ltm rule (/.+)\{") {
# Start new rule
$state = [rulestate]::start
} else {
# Process rule contents
if($l -match "^\s*\{") {
# nested construct found
$state = [rulestate]::content
++$nestedItems
} elseif ($l -match "^\s*\}") {
# closing bracket. Is it
# a) closing nested
if($nestedItems -gt 0) {
$state = [rulestate]::content
--$nestedItems
} else {
# b) closing rule
$state = [rulestate]::stop
}
} else {
# ordinary rule data
$state = [rulestate]::content
}
}
# Handle rule contents based on state
switch($state){
start {
$currentRule = $matches[1].trim()
$ruledata = #()
break
}
content {
$ruledata += $l
break
}
stop {
$ht.add($currentRule, $ruledata)
break
}
default { write-host "oops! $state" }
}
write-host "$state => $l"
}
$ht
Output rules
SOME CONTENT
SOME OTHER CONTENT
SOME OTHER CONTENT2
SOME DIFFERENT CONTENT
{
WELL,
THIS ESCALATED QUICKLY
}

How can I increase the maximum number of characters read by Read-Host?

I need to get a very long string input (around 9,000 characters), but Read-Host will truncate after around 8,000 characters. How can I extend this limit?
The following are possible workarounds.
Workaround 1 has the advantage that it will work with PowerShell background jobs that require keyboard input. Note that if you are trying to paste clipboard content containing new lines, Read-HostLine will only read the first line, but Read-Host has this same behavior.
Workaround 1:
<#
.SYNOPSIS
Read a line of input from the host.
.DESCRIPTION
Read a line of input from the host.
.EXAMPLE
$s = Read-HostLine -prompt "Enter something"
.NOTES
Read-Host has a limitation of 1022 characters.
This approach is safe to use with background jobs that require input.
If pasting content with embedded newlines, only the first line will be read.
A downside to the ReadKey approach is that it is not possible to easily edit the input string before pressing Enter as with Read-Host.
#>
function Read-HostLine ($prompt = $null) {
if ($prompt) {
"${prompt}: " | Write-Host
}
$str = ""
while ($true) {
$key = $host.UI.RawUI.ReadKey("NoEcho, IncludeKeyDown");
# Paste the clipboard on CTRL-V
if (($key.VirtualKeyCode -eq 0x56) -and # 0x56 is V
(([int]$key.ControlKeyState -band [System.Management.Automation.Host.ControlKeyStates]::LeftCtrlPressed) -or
([int]$key.ControlKeyState -band [System.Management.Automation.Host.ControlKeyStates]::RightCtrlPressed))) {
$clipboard = Get-Clipboard
$str += $clipboard
Write-Host $clipboard -NoNewline
continue
}
elseif ($key.VirtualKeyCode -eq 0x08) { # 0x08 is Backspace
if ($str.Length -gt 0) {
$str = $str.Substring(0, $str.Length - 1)
Write-Host "`b `b" -NoNewline
}
}
elseif ($key.VirtualKeyCode -eq 13) { # 13 is Enter
Write-Host
break
}
elseif ($key.Character -ne 0) {
$str += $key.Character
Write-Host $key.Character -NoNewline
}
}
return $str
}
Workaround 2:
$maxLength = 65536
[System.Console]::SetIn([System.IO.StreamReader]::new([System.Console]::OpenStandardInput($maxLength), [System.Console]::InputEncoding, $false, $maxLength))
$s = [System.Console]::ReadLine()
Workaround 3:
function Read-Line($maxLength = 65536) {
$str = ""
$inputStream = [System.Console]::OpenStandardInput($maxLength);
$bytes = [byte[]]::new($maxLength);
while ($true) {
$len = $inputStream.Read($bytes, 0, $maxLength);
$str += [string]::new($bytes, 0, $len)
if ($str.EndsWith("`r`n")) {
$str = $str.Substring(0, $str.Length - 2)
return $str
}
}
}
$s = Read-Line
More discussion here:
Console.ReadLine() max length?
Why does Console.Readline() have a limit on the length of text it allows?
https://github.com/PowerShell/PowerShell/issues/16555

check for respective elements in arrays in powershell

I have two arrays with three elements(file name parts) each. I need to join first element of first array and first element of second array and test if is not null and if the combination(file name) exists and like wise i need to do it for other two elements in a same manner.
$file_nameone_array = ( table, chair, comp)
$file_nametwo_array = ( top, leg , cpu)
foreach ($input_file in $file_nameone_array) {
foreach ($input_rev in $file_nametwo_array) {
$path = "D:\$input_file-$input_rev.txt"
If (test-path $path -pathtype leaf) {
write-host "$path exists and not null"}
else{
write-host "$path doesnot exist"
exit 1}
I expect to test for "table-top.txt", "chair-leg.txt" , "comp-cpu.txt"
whereas my code checks for "table-leg.txt" and exits saying table-leg.txt doesnot exist.
This sounds like a coding problem for a homework assignment (i.e. something you should figure out), so I'll just give you hints instead of the answer.
Your elements of array need to be wrapped in quotes.
Use Write-Output $path to see what you're actually checking for.
Use a regular for loop
This is the syntax to write output of the first element in the array: Write-Output "$($file_nameone_array[0])"
Hopefully you can get this answer from this.
You can try this way :
$file_nameone_array = ( "table","chair", "comp") # INITILIZING ARRAY
$file_nametwo_array = ( "top", "leg", "cpu") # INITILIZING ARRAY
if ($file_nameone_array.Count -ne $file_nametwo_array.Count ) # CPMPARING BOTH ARRAY'S SIZE
{
Write-Error "Both array must have same size..." # THROW ERROR
exit # EXIT
}
for($ind = 0 ; $ind -lt $file_nameone_array.Count ; $ind++) # FOR LOOP 0 TO ARRAY'S LENGTH - 1
{
$path = "D:\\" + $file_nameone_array[$ind] + "-" + $file_nametwo_array[$ind] + ".txt" # COMBINING BOTH ELEMENTS
If (test-path $path -pathtype leaf) # CHECKING PATH EXIST OR NOT
{
write-host "$path exists and not null" # PRINT PATH EXIST
} # END OF IF
else # ELSE
{
Write-Error "$path doesnot exist" # THROW ERROR : FILE NOT EXIST
exit 1 # EXIT SCRIPT
} # END OF ELSE
} # END OF FOR LOOP
If you change your example slightly to just display the $path variable ike this:
$file_nameone_array = #( "table", "chair", "comp" )
$file_nametwo_array = #( "top", "leg" , "cpu" )
foreach ($input_file in $file_nameone_array) {
foreach ($input_rev in $file_nametwo_array) {
$path = "D:\$input_file-$input_rev.txt"
write-host $path
}
}
you get this output
D:\table-top.txt
D:\table-leg.txt
D:\table-cpu.txt
D:\chair-top.txt
D:\chair-leg.txt
D:\chair-cpu.txt
D:\comp-top.txt
D:\comp-leg.txt
D:\comp-cpu.txt
so you can see why it's looking for "D:\table-top.txt" as the first file.
what you can do instead is this:
$file_nameone_array = #( "table", "chair", "comp" )
$file_nametwo_array = #( "top", "leg" , "cpu" )
for( $index = 0; $index -lt $file_nameone_array.Length; $index++ ) {
$input_file = $file_nameone_array[$index];
$input_rev = $file_nametwo_array[$index];
$path = "D:\$input_file-$input_rev.txt"
write-host $path
}
and now you get this output
D:\table-top.txt
D:\chair-leg.txt
D:\comp-cpu.txt
You can replace the write-host with your original file checking logic and you should get the behaviour you were after.
Note - This requires your arrays to be exactly the same length, so you might need to put some error handling in before this bit of code in your script.

How to add items to an array in a function as a script variable on Powershell?

I am trying to add items to an array variable that I am declaring outside of a function.
Here is the idea of my code in a very simplified way:
function Test($NAME, $SPEED){
$fName = "testName"
$fSpeed = 100
if($status = ($fName -eq $NAME) -and ($fSpeed -eq $SPEED))
{}
else{
if($fName -ne $NAME)
{$errorMessages += "The name is not" + $NAME}
if($fSpeed -ne $SPEED)
{$errorMessages += "The speed is not" + $SPEED}
}
return $status
}
$script:errorMessages=#()
$result=#()
$result += Test -NAME "alice" -SPEED "100"
$result += Test -NAME "bob" -SPEED "90"
#result is an array of booleans that I need later on
$errorMessages
When I display $errorMessages, this is the expected output that I'd like:
The name is not alice
The name is not bob
The speed is not 90
However, when I try to display the variable outside of the function, and even outside of the "else" block, I get nothing printed out. How can I correctly add the error messages to the array?
You want to call errorMessages via the script scope. Therefore you've to use $script:errorMessage (instead of $errorMessage) inside your function.
function Test($NAME, $SPEED) {
$fName = "testName"
$fSpeed = 100
$status = ($fName -eq $NAME) -and ($fSpeed -eq $SPEED)
if (!$status) {
if ($fName -ne $NAME) {
$script:errorMessages += "The name is not" + $NAME
}
if ($fSpeed -ne $SPEED) {
$script:errorMessages += "The speed is not" + $SPEED
}
}
$status
}
$errorMessages = #()
$result = #()
$result += Test -NAME "alice" -SPEED "100"
$result += Test -NAME "bob" -SPEED "90"
#result is an array of booleans that I need later on
$errorMessages
Now you get the expected output:
The name is notalice
The name is notbob
The speed is not90
Also be aware about the return statement in PowerShell -> stackoverflow answer
Hope that helps

Converting strings to timespans, $PSItem in 'switch'?

I have a bunch of strings, in the form of:
'3m 36s', '24m 38s', '59s'
, to be converted to timespans. My current "solution" is:
'3m 36s', '24m 38s', '59s' |ForEach-Object {
$s = 0
$m = 0
$h = 0
$PSItem.Split(' ') |ForEach-Object {
$item = $PSItem
switch ($PSItem[-1])
{
's'
{
$s = $item.TrimEnd('s')
}
'm'
{
$m = $item.TrimEnd('m')
}
'h'
{
$h = $item.TrimEnd('h')
}
Default
{
Write-Error 'Ooops...' -ErrorAction Stop
}
}
}
$timespan = New-TimeSpan -Hours $h -Minutes $m -Seconds $s
# ToString() is used just to get some easy to read output
$timespan.ToString()
}
While it seems to work for me, I have two issues with the above:
Is the general approach
ForEach -> Split(' ') -> ForEach -> switch
OK-ish? Are there any alternative/better ways of doing the conversion?
I tried using $PSItem in the switch
It seems that the switch construct has it's "own pipeline"
# $item = $PSItem
switch ($PSItem[-1])
{
's'
{
$PSItem
}
}
-- in the above $PSItem evaluates to 's'(, 'm', the value matched). What is actually going on? (internaly?)
I would take one ForEach loop out of things by performing that loop with the Switch command. Here's what I'd end up with:
'3m 36s', '59s', '24m 38s' |%{
$TSParams = #{}
Switch($_.Split()){
{$_[-1] -eq 's'}{$TSParams.Add('Seconds', ([int]$_.trim('s')))}
{$_[-1] -eq 'm'}{$TSParams.Add('Minutes', ([int]$_.trim('m')))}
{$_[-1] -eq 'h'}{$TSParams.Add('Hours', ([int]$_.trim('h')))}
}
New-TimeSpan #TSParams
}
For each string it creates an empty hashtable, then loops through each item of the Split() method, adding the appropriate time to the hashtable. Then it splats that to the New-TimeSpan command, and moves to the next item in the ForEach loop. I tried it locally and had some issues initially when the numbers did not cast as an int, and it tried to convert them to a DateTime, which is why I type cast them in the above code.