Powershell -- Array Casting Type Issue - arrays

and thank you in advance for taking a look.
I am having an issue in a script I wrote in Powershell. The script below is a little sloppy so please forgive me.
Basically, this script takes input from a directory of text files. Each file has a line in it like so, with the following structure:
GlobalPath, AgencyPath,SitePath,SizeofSite (in bytes)
\\servername\shared, \8055\Single\department, \sitename,524835900000
The line in question is:
# Split full path and peak usage
$CalculationBuffer = $DailyBuffer[$k].Split(",")
Which results in the following error:
Method invocation failed because [System.Char] doesn't contain a method named 'Split'.
At D:\script.ps1:387 char:52
+ $CalculationBuffer = $DailyBuffer[$k].Split <<<< (",")
+ CategoryInfo : InvalidOperation: (Split:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
So my question: Is the array casted incorrectly? Since it is reporting [System.Char] instead of [System.String]?
If the file I am inputting has two lines, it does not result in this error. If the file has only one line, it gets casted as [System.Char] instead.
:Full Script:
# Monthly Output File
[string]$monthoutfile = $ProgPath + "Billing\" + $monthdate + "\_Master_" + $monthdate + ".log"
[string]$currentmonth = $ProgPath + "Billing\" + $monthdate + "\"
# Define what type of files to look for
$files = gci $currentmonth | Where {$_.extension -eq ".log"}
# Create a datastore\dictionary for this month
$MonthDataDictionary = New-Object 'System.Collections.Generic.Dictionary[string,long]'
$MonthAvgDictionary = New-Object 'System.Collections.Generic.Dictionary[string,long]'
# Arrays
$DailyBuffer = #()
$CalculationBuffer = #()
$TempArray = #()
# Counters\Integers
[int]$Linesinday = 1
[int]$DayCounter = 1
[int]$LineCounter = 0
# Strings
[string]$DailyPath = ""
[string]$Outline = ""
# Longs
[long]$DailyPeak = 0
[long]$Value = 0
##########################################################################
# Begin Loop
# Write once...
#$CalcBuffer += "\"
foreach ($file in $files)
{
# First get content from text file and store in buffer
$DailyBuffer = Get-Content $file.pspath
# Determine how many lines are in the file, call function
$Linesinday = linecount $file.pspath
for ($k = 0; $k -lt $Linesinday; $k++ )
{
# Split full path and peak usage
$CalculationBuffer = $DailyBuffer[$k].Split(",")
# Store site path
$DailyPath = $CalculationBuffer[0] + $CalculationBuffer[1] + $CalculationBuffer[2]
# Store peak usage
$DailyPeak = $CalculationBuffer[3]
# Write to dictionary under conditions
# Check if current path is stored or "Site".
# If NOT .ContainsKey($DailyPath)
if (!($MonthDataDictionary.ContainsKey($DailyPath))) {
# Add Key
$MonthDataDictionary.Add($DailyPath, $DailyPeak)
# If it does contain a value
} elseif ($MonthDataDictionary.ContainsKey($DailyPath)) {
# Add the value to the current value for averaging
$MonthDataDictionary.Item($DailyPath) += $DailyPeak
}
}
# Accumulator
$DayCounter ++
}
# Now that each file is tallied up, run an average calculation
$MonthDataDictionary.getenumerator() | Foreach-Object -process {
$Value = $_.Value / $DayCounter
$MonthAvgDictionary.Add($_.Key, $Value)
}
# Debug:
# Write-Host the values
$MonthAvgDictionary
# Output the "Average Peak" values to a file
$MonthAvgDictionary.getenumerator() | Foreach-Object -process {
# Construct output line
$OutLine = $_.Key + "," + $_.Value
$OutLine >> $MonthOutFile
}

This is a known pitfall in Powershell. Just wrap the Get-Content in an array "expression" #() :
$DailyBuffer = #(Get-Content $file.pspath)

I think the problem you're having is that get-content doesn't return an array of strings (lines), but a single string. Thus, when you look at $dailybuffer[k], you're looking at the kth character of the string, not the kth line.

I had the same problem and only the line just like below solved this problem. For your problem it will be:
[string[]] $DailyBuffer = Get-Content $file.pspath

Related

How to create an array like this in powershell?

I need to store data like below for TextToColumns Excel automation.
I need to implement Code-2 or Code-3 or Code-4 is that any way to achieve?
I have more than 350+ data so I cant use Code-1, that's not fair for me.
Code-1: working fine
$var = (1,2),(2,2),(3,2),(4,2),(5,2),(6,2)........(300,2)
$ColumnA.texttocolumns($colrange,1,-412,$false,$false,$false,$false,$false,$true,"|",$var)
Code-2: not Working
$var = #((1,2)..(300,2))
$ColumnA.texttocolumns($colrange,1,-412,$false,$false,$false,$false,$false,$true,"|",$var)
Code-3: not Working
$var = #()
#forloop upto 300
{ $var += ($i,2) }
$ColumnA.texttocolumns($colrange,1,-412,$false,$false,$false,$false,$false,$true,"|",$var)
Code-4: not Working
[array]$var = 1..300 | foreach-object { ,#($_, 2) }
$ColumnA.texttocolumns($colrange,1,-412,$false,$false,$false,$false,$false,$true,"|",$var)
I can't fully explain what happens here but I guess that it is related to the fact that the texttocolumns requires an (deferred) expression rather than an (evaluated) object.
Meaning that the following appears to work for the Minimal, Reproducible Example from #mclayton:
$Var = Invoke-Expression ((1..6 |% { "($_, `$xlTextFormat)" }) -Join ',')
And expect the following to work around the issue in the initial question:
$Var = Invoke-Expression ((1..300 |% { "($_, 2)" }) -Join ',')
Not an answer - just documenting some research to save others some time...
I can repro the issue here with the following code:
$xl = new-object -com excel.application;
$xl.Visible = $true;
$workbook = $xl.Workbooks.Add();
$worksheet = $workbook.Worksheets.Item(1);
$worksheet.Range("A1") = "aaa|111";
$worksheet.Range("A2") = "bbb|222";
$worksheet.Range("A3") = "ccc|333";
$worksheet.Range("A4") = "ddd|444";
$worksheet.Range("A5") = "eee|555";
$worksheet.Range("A6") = "fff|666";
which builds a new spreadsheet like this:
If you then run the following it will parse the contents of column A and put the results into columns B and C:
$range = $worksheet.Range("A:A");
$target = $worksheet.Range("B1");
# XlColumnDataType enumeration
# see https://learn.microsoft.com/en-us/office/vba/api/excel.xlcolumndatatype
$xlTextFormat = 2;
# XlTextParsingType enumeration
# see https://learn.microsoft.com/en-us/office/vba/api/excel.xltextparsingtype
$xlDelimited = 1;
# XlTextQualifier enumeration
# https://learn.microsoft.com/en-us/office/vba/api/excel.xltextqualifier
$xlTextQualifierNone = -4142;
$var = (1,$xlTextFormat),(2,$xlTextFormat),(3,$xlTextFormat),(4,$xlTextFormat),(5,$xlTextFormat),(6,$xlTextFormat);
# parse the values in A1:A6 and puts the values in a 2-dimensional array starting at B1
# see https://learn.microsoft.com/en-us/office/vba/api/excel.range.texttocolumns
$result = $range.TextToColumns(
$target, # Destination
$xlDelimited, # DataType
$xlTextQualifierNone, # TextQualifier
$false, # ConsecutiveDelimiter
$false, # Tab
$false, # Semicolon
$false, # Comma
$false, # Space
$true, # Other
"|", # OtherChar
$var # FieldInfo
);
which then looks like this:
However, if you change the declaration for $var to
$var = 1..6 | % { ,#($_, $xlTextFormat) };
you get the following error:
OperationStopped: The remote procedure call failed. (0x800706BE)
and the Excel instance terminates.
So there's something different about these two declarations:
$var = (1,$xlTextFormat),(2,$xlTextFormat),(3,$xlTextFormat),(4,$xlTextFormat),(5,$xlTextFormat),(6,$xlTextFormat);
$var = 1..6 | % { ,#($_, $xlTextFormat) };
but what that is eludes me :-S

Powershell Get-Content + Array

how's it going?
I'm new on Powershell and I'm trying to simplify my code in order that I need to perform the same action in two files, the only thing that changes is the File Name and ReadCount size (15000 for the first file and 50000 for the second one).
When I run it the error shows:
Get-Content : An object at the specified path
C:\Folder\08_configuration_items 11_CI-Contract-new[0].csv does not
exist, or has been filtered by the -Include or -Exclude parameter. At
line:2 char:7
+ $i=0; Get-Content "C:\Folder\$fileArray[$len].csv" -ReadCount $sizeA ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (System.String[]:String[]) [Get-Content], Exception
+ FullyQualifiedErrorId : ItemNotFound,Microsoft.PowerShell.Commands.GetContentCommand
Get-Content : An object at the specified path
C:\Folder\08_configuration_items 11_CI-Contract-new[0]_1.csv does not
exist, or has been filtered by the -Include or -Exclude parameter. At
line:3 char:20
+ ... bookContent = Get-Content "C:\Folder\$fileArray[$len]_1.csv" | Selec ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (System.String[]:String[]) [Get-Content], Exception
+ FullyQualifiedErrorId : ItemNotFound,Microsoft.PowerShell.Commands.GetContentCommand
That's the code, not sure if I'm accessing the Array using the right way on Powershell.
$sizeArray = #(15000,50000)
$fileArray = #("08_configuration_items", "11_CI-Contract-new")
for($len=0; $len -le 1; $len++) {
$i=0; Get-Content "C:\Folder\$fileArray[$len].csv" -ReadCount $sizeArray[$len] | %{$i++; $_ | Out-File "C:\Folder\$fileArray[$len]_$i.csv" -Encoding "UTF8"}
$WorkbookContent = Get-Content "C:\Folder\$fileArray[$len]_1.csv" | Select -Index 0
for($j=2; $j -le $i; $j++) {
$CurrentFileContent = Get-Content "C:\Folder\$fileArray[$len]_$j.csv"
#($WorkbookContent, $CurrentFileContent) | Set-Content "C:\Folder\$fileArray[$len]_$j.csv"
}
}
Any ideias?
Thanks a lot
The problem here is with string interpolation. A variable name within a string will expand up until it reaches a special character in that name. Then it will append the remainder of the string and any interpolated strings afterwards. This commonly happens with the . character when accessing a property of an object within a string. A simple solution is to use the subexpression operator ($()).
Get-Content "C:\Folder\$($fileArray[$len]).csv"
An alternative is to build the path string another way and then pass it into the command. The method below uses the format operator (-f).
$Path = "C:\Folder\{0}.csv" -f $fileArray[$len]
Get-Content $Path
Your code with the subexpression operator added will look like the following:
$sizeArray = #(15000,50000)
$fileArray = #("08_configuration_items", "11_CI-Contract-new")
for($len=0; $len -le 1; $len++) {
$i=0; Get-Content "C:\Folder\$($fileArray[$len]).csv" -ReadCount $sizeArray[$len] | %{$i++; $_ | Out-File "C:\Folder\$($fileArray[$len])_$i.csv" -Encoding "UTF8"}
$WorkbookContent = Get-Content "C:\Folder\$($fileArray[$len])_1.csv" | Select -Index 0
for($j=2; $j -le $i; $j++) {
$CurrentFileContent = Get-Content "C:\Folder\$($fileArray[$len])_$j.csv"
#($WorkbookContent, $CurrentFileContent) | Set-Content "C:\Folder\$($fileArray[$len])_$j.csv"
}
}
You can see this behavior on a simpler scale using your $fileArray variable.
$filearray
08_configuration_items
11_CI-Contract-new
# Notice how the [0] gets appended to the string-cast $fileArray
"$filearray[0]"
08_configuration_items 11_CI-Contract-new[0]
$filearray[0]
08_configuration_items
"$($filearray[0])"
08_configuration_items
Since $fileArray is an array of strings, you have another unintended effect. With "$fileArray[0]", $fileArray will be interpolated and converted to a string output rather than an array. PowerShell by default will join array elements by a single space when casting as a string. So the resulting output format is arrayItem1 arrayItem2 arrayItem3[0]. [0] is not included as part of the variable evaluation.

Using Get-Random after using Where-Object creates a null valued expression

I'm trying to create a 10 character password that includes a mix of numbers, letters (uppercase and lowercase), and symbols.
Below is the script I am using in the function:
Function Get-TempPassword {
$TempPassword = $null
$ascii = $NULL;For ($a = 33;$a –le 126;$a++) {$ascii +=, ([char][byte]$a | Where-Object {$_ -notin "'",'`','|','_',"`;",'"',','})}
Do {$TempPassword += $ascii | Get-Random; $loop++}
Until ($loop -eq 11)
return $TempPassword
}
If I remove the following section:
| Where-Object {$_ -notin "'",'`','|','_',";",'"',','}
The creation of the password works fine albeit including the symbols I don't want included.
Having the Where-Object function causes the Get-Random function to only use the first 5 characters in the array, and therefore I don't get letters of any case type, or numbers, or any of the other symbols.
I've found that if I use $ascii[26] (being the 25th character in the array) I get a null value, however I would think this would allow any character up to this character to be used, or none at all, not just the first 5. The 25th character just so happens to be a ; (ascii value number 59). I tried adding the symbol to the Where-Object exclusion, and it was removed from the array, but the 25th character still showed as a null value.
I performed a reverse lookup of the ascii value [int[]][char[]] of each character either side of where the ; symbol would appear and it returned values 58 and 60, leading me to believe it was value 59 that was offending, but the symbol at this point should have been excluded.
Adding characters to the 'where-object' exclusion list should be removing them from the array, and it appears to, however running $ascii.Count shows 49 characters, regardless of whether I add or remove characters to the Where-Object exclusion list.
I have looked for information on the web and can't seem to find any, although it may be the search terms I'm using, as it's a bit of a complex case that not many would be reporting on.
Any help is appreciated.
I didn't write this and i can't remember where i got it but i have built this into any scripts to create random secure Windows passwords, you can specify the length of the password returned by the param [int]$PasswordLength ( i have already set it to 10 ).
function New-SWRandomPassword {
[CmdletBinding(DefaultParameterSetName='FixedLength',ConfirmImpact='None')]
[OutputType([String])]
Param
(
# Specifies minimum password length
[Parameter(Mandatory=$false,
ParameterSetName='RandomLength')]
[ValidateScript({$_ -gt 0})]
[Alias('Min')]
[int]$MinPasswordLength = 8,
# Specifies maximum password length
[Parameter(Mandatory=$false,
ParameterSetName='RandomLength')]
[ValidateScript({
if($_ -ge $MinPasswordLength){$true}
else{Throw 'Max value cannot be lesser than min value.'}})]
[Alias('Max')]
[int]$MaxPasswordLength = 11,
# Specifies a fixed password length
[Parameter(Mandatory=$false,
ParameterSetName='FixedLength')]
[ValidateRange(1,2147483647)]
[int]$PasswordLength = 10,
# Specifies an array of strings containing charactergroups from which the password will be generated.
# At least one char from each group (string) will be used.
[String[]]$InputStrings = #('abcdefghijkmnpqrstuvwxyz', 'ABCEFGHJKLMNPQRSTUVWXYZ', '23456789', '!"#%&'),
# Specifies a string containing a character group from which the first character in the password will be generated.
# Useful for systems which requires first char in password to be alphabetic.
[String] $FirstChar,
# Specifies number of passwords to generate.
[ValidateRange(1,2147483647)]
[int]$Count = 1
)
Begin {
Function Get-Seed{
# Generate a seed for randomization
$RandomBytes = New-Object -TypeName 'System.Byte[]' 4
$Random = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider'
$Random.GetBytes($RandomBytes)
[BitConverter]::ToUInt32($RandomBytes, 0)
}
}
Process {
For($iteration = 1;$iteration -le $Count; $iteration++){
$Password = #{}
# Create char arrays containing groups of possible chars
[char[][]]$CharGroups = $InputStrings
# Create char array containing all chars
$AllChars = $CharGroups | ForEach-Object {[Char[]]$_}
# Set password length
if($PSCmdlet.ParameterSetName -eq 'RandomLength')
{
if($MinPasswordLength -eq $MaxPasswordLength) {
# If password length is set, use set length
$PasswordLength = $MinPasswordLength
}
else {
# Otherwise randomize password length
$PasswordLength = ((Get-Seed) % ($MaxPasswordLength + 1 - $MinPasswordLength)) + $MinPasswordLength
}
}
# If FirstChar is defined, randomize first char in password from that string.
if($PSBoundParameters.ContainsKey('FirstChar')){
$Password.Add(0,$FirstChar[((Get-Seed) % $FirstChar.Length)])
}
# Randomize one char from each group
Foreach($Group in $CharGroups) {
if($Password.Count -lt $PasswordLength) {
$Index = Get-Seed
While ($Password.ContainsKey($Index)){
$Index = Get-Seed
}
$Password.Add($Index,$Group[((Get-Seed) % $Group.Count)])
}
}
# Fill out with chars from $AllChars
for($i=$Password.Count;$i -lt $PasswordLength;$i++) {
$Index = Get-Seed
While ($Password.ContainsKey($Index)){
$Index = Get-Seed
}
$Password.Add($Index,$AllChars[((Get-Seed) % $AllChars.Count)])
}
Return $(-join ($Password.GetEnumerator() | Sort-Object -Property Name | Select-Object -ExpandProperty Value))
}
}
}
New-SWRandomPassword
EDIT:::
https://gallery.technet.microsoft.com/scriptcenter/Generate-a-random-and-5c879ed5
The script can be found here.
Short version (best method for me):
$possible=36..38 + 40..43 + 45..58 + 60..94 + 97..123 + 125..126 + 33
(get-random -count 10 -input $possible | % {[char]$_}) -join ''
Using the following script seems to have worked exactly how I want.
I removed the comma (,) from after += on this line:
$ascii = $NULL;For ($a = 33;$a –le 126;$a++) {$ascii +=, ([char][byte]$a | Where-Object {$_ -notin "'",'`','|','_',";",'"',','})}
I created a blank array before the array is added to:
$ascii = #()
The full code block is below:
Function Get-TempPassword {
$TempPassword = $null
$ascii = #()
For ($a = 33;$a –le 126; $a++) { $ascii += ([char][byte]$a | Where-Object { $_ -notin "'",'`','|','_',";",'"',',' }) }
Do {$TempPassword += $ascii | Get-Random; $loop++}
Until ($loop -eq 11)
return $TempPassword
}
Recursive method :
Function random-password ($length = 10)
{
$Assembly = Add-Type -AssemblyName System.Web
$password = [System.Web.Security.Membership]::GeneratePassword($length, 2)
$desablechar = "[``'|_;,`"]"
if ($password -match $desablechar )
{
random-password $length
}
else
{
$password
}
}
random-password
try Something like this :
Function random-password ($length = 10)
{
$possible=36..38 + 40..43 + 45..58 + 60..94 + 97..123 + 125..126 + 33
$password = get-random -count $length -input $possible |
% -begin { $aa = $null } -process {$aa += [char]$_} -end {$aa}
return $password
}
random-password
I have rectified my others propositions, but i propose an other method :)
Function random-password2 ($length = 10)
{
$Assembly = Add-Type -AssemblyName System.Web
$password = [System.Web.Security.Membership]::GeneratePassword(50, 2)
$possible=36..38 + 40..43 + 45..58 + 60..94 + 97..123 + 125..126 + 33
$newchar=[char](get-random -count 1 -input $possible)
$password=$password -replace "[`'|_;,]", $newchar
$password.Substring(0, $length)
}
random-password2

Rebuilding registry key from array

I am trying to take a variable, split it and then rebuild it so I can use Test-Path iteratively. I am not sure it is possible. The code I have so far in its basic form looks something like this, the trouble I have is around rebuilding the registry path.
$Key = "HKCU:\SOFTWARE\MyKey\Test"
$SplitKey = $Key -split "\\"
#Write-Host $SplitKey.Length
$i = 0
do {
if ($i -gt 0) {
$x = $i - 1
$sk = $SplitKey[$x] + "\" + $SplitKey[$i] + "\"
Write-Host $sk
} else {
$sk = $SplitKey[$i] + "\"
Write-Host $sk
}
$i++
} until ($i -ge $SplitKey.Length)
The first part of the key is rebuilt exactly how I want. My plan is to incorporate a Test-Path into the loop and where required do a New-Item if Test-Path fails.
Can anyone help with the loop and rebuilding the $SplitKey array step by step?
This is what causes your problem:
$x = $i - 1
$sk = $SplitKey[$x] + "\" + $SplitKey[$i] + "\"
You create $sk just from the previous and the current array element, which basically works like this:
Original string: HKCU:\SOFTWARE\MyKey\Test
1st iteration: HKCU:\
2nd iteration: HKCU:\SOFTWARE\
3rd iteration: SOFTWARE\MyKey\
4th iteration: MyKey\Test\
What you actually want is to concatenate the current array element to everything that came before, i.e. to the current value of $sk:
$sk += "\" + $SplitKey[$i]
Or you could join the array elements up to the current index:
$sk = $SplitKey[0..$i] -join '\'
With that said, personally I prefer recursive algorithms for path creation. Traverse from the full path up to the longest existing path, then create the missing folders when you descend back down as you return from the recursive calls:
function New-Key([string]$Path) {
$drive = Split-Path $Path -Qualifier
$parent = try { Split-Path $Path -NoQualifier | Split-Path -Parent } catch {}
if (-not (Test-Path -LiteralPath $Path)) {
New-Key (Join-Path $drive $parent)
New-Item -Type Directory -Path $Path
}
}

Email Formatted Array

I got a script that creates two arrays (each has 1 column and variable number of lines). I want to format these two arrays and e-mail it to an Outlook account. Code and sample data below.
$Values2 = #(Get-Content *\IdealOutput.csv)
$OutputLookUp2 = #()
foreach ($Value in $Values2) {
$OutputLookUp2 += $Excel.WorksheetFunction.VLookup($Value,$range4,3,$false)
}
$Excel.Workbooks.Close()
$Excel.Quit()
$EmailFrom = "sample#sample.com"
$EmailTo = "sample#sample.com"
$EmailBody = "$Values2 $OutputLookup2"
$EmailSubject = "Test"
$Username = "sample"
$Password = "sample"
$Message = New-Object Net.Mail.MailMessage `
($EmailFrom, $EmailTo, $EmailSubject, $EmailBody)
$SMTPClient = New-Object Net.Mail.SmtpClient `
("smtp.outlook.com", portnumber) #Port can be changed
$SMTPClient.EnableSsl = $true
$SMTPClient.Credentials = New-Object System.Net.NetworkCredential `
($Username, $Password);
$SMTPClient.Send($Message)
Both $OutputLookUp2 and $Values2 are one column with variable number of lines.
Example:
$Outputlookup2 =
X1
X2
$Values2 =
Y1
Y2
I would like the output to the body of the e-mail to be:
X1 Y1
X2 Y2
And I would like to avoid HTML as it will be sent via text as well.
Assuming my interpretation is correct this seems simple enough. For every $Values2, which is just a line from a text file, find its similar value in the open spreadsheet. You are have the loop that you need. Problem is you are building the item lists independent of each other.
$Values2 = #(Get-Content *\IdealOutput.csv)
$OutputLookUp2 = #()
foreach ($Value in $Values2){
$OutputLookUp2 += "$Value $($Excel.WorksheetFunction.VLookup($Value,$range4,3,$false))"
}
Now $OutputLookUp2 should contain your expected output in array form.
If the array does not work you could also just declare it as a string and the add newlines as you are building it. You will notice the "`r`n" at the end of the string.
$Values2 = #(Get-Content *\IdealOutput.csv)
$OutputLookUp2 = ""
foreach ($Value in $Values2){
$OutputLookUp2 += "$Value $($Excel.WorksheetFunction.VLookup($Value,$range4,3,$false))`r`n"
}
In both example you can just flip the order of the $value and the lookup easy. If you need a header you can add that when you declare $OutputLookUp2.
There is always room for improvement
If you want to take this a little further in the direction that Ansgar Wiechers was eluding to...
$OutputLookUp2 = Get-Content *\IdealOutput.csv | ForEach-Object{
"$_ $($Excel.WorksheetFunction.VLookup($_,$range4,3,$false))"
} | Out-String

Resources