Get last element of pipeline in powershell - powershell

This might be weird, but stay with me.
I want to get only the last element of a piped result to be assigned to a varaiable.
I know how I would do this in "regular" code of course, but since this must be a one-liner.
More specifically, I'm interested in getting the file extension when getting the result from an FTP request ListDirectoryDetails.
Since this is done within a string expansion, I can't figure out the proper code.
Currently I'm getting the last 3 hars, but that is real nasty.
New-Object PSObject -Property #{
LastWriteTime = [DateTime]::ParseExact($tempDate, "MMM dd HH:mm",[System.Globalization.CultureInfo]::InvariantCulture)
Type = $(if([int]$tempSize -eq 0) { "Directory" } else { $tempName.SubString($tempName.length-3,3) })
Name = $tempName
Size = [int]$tempSize
}
My idea was doing something similar to
$tempName.Split(".") | ? {$_ -eq $input[$input.Length-1]}
that is, iterate over all, but only take out where the element I'm looking at is the last one of the input-array.
What am I missing ?

A few ways to do this:
$name = 'c:\temp\aaa.bbb.ccc'
# way 1
$name.Split('.') | Select-Object -Last 1
# way 2
[System.IO.Path]::GetExtension($name)
# or if the dot is not needed
[System.IO.Path]::GetExtension($name).TrimStart('.')

In general, getting the last element in the pipeline would be done using Select -Last 1 as Roman suggests above. However, an alternate and easier way to do this if the input is a simple array is to use array slicing e.g.:
PS> $name = "c:\temp\aaa.bbb.txt"
PS> $name.Split('.')[-1]
txt
Was your intent to get the file's extension or basename? Because it seems that the Type property already contains the extension for the filename.

Related

How can I provide my own comparison for Sort-Object Cmdlet?

I have a list of paths. The filenames contain a version number and I want to sort the list by this number.
I tried using List<T>.Sort method as well as the Sort-Object Cmdlet but I have a problem with the syntax when using .NET and I don't know whether Sort-Object supports my needs.
$files conatins the paths and is of type List<string>. Being comfortable with C# I tried something like this:
$files.Sort(delegate($pathX, $pathY){
[some code to extract version number from path, so $x,$y contains just the version number]
$versionX = New-Object system.version($x);
$versionY = New-Object system.version($y);
return $versionX.CompareTo($versionY)
})
This fails already because of the delegate keyword, as I don't know how to provide the necessary function to the List<T>.Sort method in Powershell.
Then I stumbled upon the Cmdlet Sort-Object, but I can't figure out how to pass an equivalent of my code above to it.
Try something like this:
$paths = "C:\Files\File1.0.3.txt",
"C:\Files\File4.4.2.txt",
"C:\Files\File2.0.0.txt",
"C:\Files\File0.0.3.txt"
$paths |
Sort-Object {$_ -match "(?<ver>(\d+\.){2}\d+)" | Out-Null; [Version]$matches.ver}
Which give this output:
C:\Files\File0.0.3.txt
C:\Files\File1.0.3.txt
C:\Files\File2.0.0.txt
C:\Files\File4.4.2.txt
Adjust the version extraction code/pattern to suit the actual path format.
EDIT
Based on #LotPings' comment, here is a clearer explanation of why this more complicated process is needed for the sorting:
With my original (poorly chosen) example data, a simple sort would give the expected sort order without any additional code:
$paths = "C:\Files\File1.0.3.txt",
"C:\Files\File4.4.2.txt",
"C:\Files\File2.0.0.txt",
"C:\Files\File0.0.3.txt"
$paths | Sort-Object
C:\Files\File0.0.3.txt
C:\Files\File1.0.3.txt
C:\Files\File2.0.0.txt
C:\Files\File4.4.2.txt
However, this won't work for some other valid version values. For example:
$paths = "C:\Files\File10.0.0.txt",
"C:\Files\File2.0.0.txt",
"C:\Files\File20.0.0.txt",
"C:\Files\File1.0.0.txt"
$paths | Sort-Object
C:\Files\File1.0.0.txt
C:\Files\File10.0.0.txt
C:\Files\File2.0.0.txt
C:\Files\File20.0.0.txt
Using the original code gives the correct order:
C:\Files\File1.0.0.txt
C:\Files\File2.0.0.txt
C:\Files\File10.0.0.txt
C:\Files\File20.0.0.txt

Adding separate values as an array to a combobox

My goal is to obtain a list of printers from my print server by name and add them as separate items in a combobox for a user to select. This is what I have come up with, but it isn't working:
$Hospital = Get-Printer -ComputerName servername | where{$_.Name -like “*Name*”} | format-list name
$ComboBox_Location.Add_Click{
switch ($ComboBox_Location.SelectedItem){
"Hospital"{
$ComboBox_Printer.Clear();
foreach($Name in $Hospital){
$ComboBox_Printer.Items.Add($Name.Name)
}
}
}
}
I figure it has something to do with "foreach", but I can't quite understand it. I have seen things like;
foreach($Name in $Names)
and I don't understand how you can search within a variable by subtracting one letter?? I don't know. There are more than 40 printers in this list and I want each of them to pull up as a separate item in this combobox.
Your code isn't working because you've put | format-list on the end of the $Hospital = .. line. This is changing the object type to format objects, which aren't useful for anything other than display.
Remove the | format-list and it looks to me like your code should otherwise work.
You're using ForEach correctly. The 'magic' is just that whatever variable you decide to use gets populated on each iteration of the loop. So if you have a collection of $Names, then ForEach ($Name in $Names) { .. } will go through the $Names collection and one at a time put each object it contains in $Name, which you can then use inside the curly braces to reference / manipulate / output as you require. $Name can therefore be called whatever you like, so long as you use it consistently inside the ForEach.

Unable to remove item from hash table

In Powershell, I have a hash table that contains data similar to this -
Name Value
---- -----
1-update.bat 1
2-update.bat 2
3-update.bat 3
3.1-update.bat 3.1
4-update.bat 4
I also have an variable that contians a number, for example 3
What I would like to do is loop through the array and remove any entry where the value is less than or equal to 3
I'm thinking that this will be easy, especially as the docs say that has tables contain a .remove method. However, the code I have below fails, yeilding this error -
Exception calling "Remove" with "1" argument(s): "Collection was of a
fixed size."
Here is the code that I used -
$versions = #{}
$updateFiles | ForEach-Object {
$versions.Add($_.name, [decimal]($_.name -split '-')[0])
}
[decimal]$lastUpdate = Get-Content $directory\$updatesFile
$versions | ForEach-Object {
if ( $_.Value -le $lastUpdate ) {
$versions.Remove($version.Name)
}
}
I first tried to loop $versions in a different manner, trying both the foreach and for approaches, but both failed in the same manner.
I also tried to create a temporary array to hold the name of the versions to remove, and then looping that array to remove them, but that also failed.
Next I hit Google, and while I can find several similar questions, none that answer my specific question. Mostly they suggest using a list (New-Object System.Collections.Generic.List[System.Object]), whcih from what I can tell is of no help to me here.
Is anyone able to suggest a fix?
Here you go, you can use .Remove(), you just need a clone of the hashtable so that it will let you remove items as you enumerate.
[hashtable]$ht = #{ '1-update.bat'=1;'2-update.bat'=2;'3-update.bat'=3;'3.1-update.bat'=3.1; '4-update.bat'=4 }
#Clone so that we can remove items as we're enumerating
$ht2 = $ht.Clone()
foreach($k in $ht.GetEnumerator()){
if([decimal]$k.Value -le 3){
#notice, deleting from clone, then return clone at the end
$ht2.Remove($k.Key)
}
}
$ht2
Notice I've cast the original variable too so that it's explicitly a hash table, may not be required, but I like to do it to at least keep things clear.
It looks like you just confused ForEach-Object with foreach but only halfway (maybe it was foreach before and you converted it).
You can't send a [hashtable] directly to ForEach-Object; the $_ in that case will just refer to the single [hashtable] you sent in. You can do:
foreach ($version in $versions.GetEnumerator()) {
$version.Name
$version.Value
}
or you can do something like this:
$versions.Keys | ForEach-Object {
$_ # the name/key
$versions[$_] # the value
$versions.$_ # also the value
}
$ht.Keys # list all keys
$ht[$_] # take an element of hastable
$ht.Remove($_) # remove an element of hastable by his key
what you want:
$ht.Keys | ? { $ht[$_] -le 3 } | %{$ht.Remove($_) }
You need to create a temporary array to hold the name/key of the versions to remove, and then looping that array to remove them from hash table:
$versionKeysToRemove = $versions.Keys | Where-Object { $versions[$_] -le $lastUpdate }
$versionKeysToRemove | ForEach-Object { $versions.Remove($_) }
Or shorter:
($versions.Keys | ? { $versions[$_] -le $lastUpdate }) | % { $versions.Remove($_) }
Please note the parentheses.

How to store an array of hashtables in a text file and then call all of the values for a given key in each hashtable

I am using a text file as the backend for an application that I am developing. I first started off leaving the text file in a human-readable format but I decided that there was no sense in that figured it would be best to leave out formatting.
Where I am now in the backend dev process is creating a single-line hashtable with identical keys but different values for each entry. Seems logical and easy to work with.
Here is a mock-up of the entries in the text file:
#{'bName'='1xx'; 'bTotal'='1yy'; 'bSet'='1zz'}
#{'bName'='2xx'; 'bTotal'='2yy'; 'bSet'='2zz'}
#{'bName'='3xx'; 'bTotal'='3yy'; 'bSet'='3zz'}
As you can see, the keys for each entry are identical, however, the values are going to be different. (The numerical and repetitious nature of the values are purely coincidental and put in place for the sake of a mock-up. Actual values will not be numerically-oriented and won't be repetitious as seen in the example.)
I am able to access keys and values by typing:
$hash = Get-Content .\Desktop\Test.txt | Out-String | iex
which outputs:
Name Value
---- -----
bName 1xx
bTotal 1yy
bSet 1zz
bName 2xx
bTotal 2yy
bSet 2zz
bName 3xx
bTotal 3yy
bSet 3zz
What I ultimately want to do is gather each of the values for bName, bTotal, and bSet so that I can append each to a separate WinForms ComboBox. The WinForms part will be simple, I am just having a bit of an issue with getting the values from each hashtable in the text file.
I tried:
$hash.Values | ?{$hash.Keys -contains 'bName'}
but it just prints out every $hash.Value regardless of the $hash.Key match given in the pipe.
I understand that $hash is an array and I figured I may have to pipe out each iteration in a foreach ($hash | %{}) loop but I'm not quite sure the correct way to do this. For example, when I try:
$hash | $_.Keys
or
$hash | $_.Values
it isn't treating each iteration like a hashtable.
What am I doing wrong here? Am I going about it in a convoluted way while there is a much easier way to accomplish this? I am open to all sorts of ideas or suggestions.
As an afterthought: It is kind of funny how often an obvious solution presents itself when you step away and divert your attention towards something else.
I went to grab lunch and I can't, for the life of me, begin to comprehend why I didn't realize that I could just very easily do this:
$hash.bName
or:
$hash.bTotal
or:
$hash.bSet
That will do exact as I was wanting to do. However, considering the answers provided, I may go a different route in terms of using an .ini file in CSV format rather than creating an array of hashtables.
One way of storing hashtables in a text file is the INI format.
[hashtable1]
bName=1xx
bTotal=1yy
bSet=1zz
[hashtable2]
bName=2xx
bTotal=2yy
bSet=2zz
[hashtable3]
bName=3xx
bTotal=3yy
bSet=3zz
INI files are basically a hashtable of hashtables in text form. They can be read like this:
$ht = #{}
Get-Content 'C:\path\to\hashtables.txt' | ForEach-Object {
$_.Trim()
} | Where-Object {
$_ -notmatch '^(;|$)'
} | ForEach-Object {
if ($_ -match '^\[.*\]$') {
$section = $_ -replace '\[|\]'
$ht[$section] = #{}
} else {
$key, $value = $_ -split '\s*=\s*', 2
$ht[$section][$key] = $value
}
}
and written like this:
$ht.Keys | ForEach-Object {
'[{0}]' -f $_
foreach ($key in $ht[$_].Keys) {
'{0}={1}' -f $key, $ht[$_][$key]
}
} | Set-Content 'C:\path\to\hashtables.txt'
Individual values in such a hashtable of hashtables can be accessed like this:
$ht['section']['key']
or like this:
$ht.section.key
Another option would be to store each hashtable in a separate file
hashtable1.txt:
bName=1xx
bTotal=1yy
bSet=1zz
hashtable2.txt.
bName=2xx
bTotal=2yy
bSet=2zz
hashtable3.txt:
bName=3xx
bTotal=3yy
bSet=3zz
That would allow you to import each file into a hashtable via ConvertFrom-StringData:
$ht1 = Get-Content 'C:\path\to\hashtable1.txt' | Out-String |
ConvertFrom-Stringdata
Writing the files would basically be the same as above (there is no ConverTo-StringData cmdlet):
$ht1.Keys | ForEach-Object {
'{0}={1}' -f $_, $ht[$_]
} | Set-Content 'C:\path\to\hashtables1.txt'
PowerShell has built in csv handling so it makes it a good choice to use in this case. So, assuming you had your data stored in a file in the standard csv format with headers:
"bName","bTotal","bSet"
"1xx","1yy","1zz"
"2xx","2yy","2zz"
"3xx","3yy","3zz"
Then you import your data like this:
$data = Import-Csv $path
Now you have an array of PsCustomObject and each header in the csv file is a property of the object. So if, for example, you wanted to get the bTotal of the second object you would do the following:
$data[1].bTotal
2yy

Create Out-File-names using array elements

I need to create .txt/.sap files/shortcuts with changing content. I use a do until loop and the file names should be created with strings from an array. I cannot use the square brackets to access the array because Powershell interprets them as a wild card characters.
The following code shows the principle:
$strSAPSystems = #("Production", "Finance", "Example")
$i = 0
do{
"text1" | Out-File .\SAP_$strSAPSystems[$i].sap
"text2" | Out-File .\SAP_$$strSAPSystems[$i].sap -Append
$i++
}
until($i -eq $strSAPSystems.length)
results in an error: "out-file : cannot perform operation because the wildcard path ... did not resolve to a file"
I tried to add the -literalPath parameter but it didn't work. I am new to Powershell, is there a better way to have the files named after the SAP systems?
Thank you
You need to wrap the string(path) inside a subexpression $(.....) to extract the value of a single element in an array. Atm. the path becomes something like .SAP_Production Finance Example[$i].sap.
Also, you have an extra $ in the second Out-File. Personally I would rewrite everthing to:
$strSAPSystems = #("Production", "Finance", "Example")
$strSAPSystems | ForEach-Object {
"text1" | Out-File ".\SAP_$($_).sap"
"text2" | Out-File ".\SAP_$($_).sap -Append"
}
$_ is the current item in the array, and since it's a single object, I don't really need the subexpression $(), but I included it because it easier to see static and dynamic parts of the path.