PowerShell equivalent for "head -n-3"? - powershell

I've been able to track down basic head/tail functionality:
head -10 myfile <==> cat myfile | select -first 10
tail -10 myfile <==> cat myfile | select -last 10
But if I want to list all lines except the last three or all lines except the first three, how do you do that? In Unix, I could do "head -n-3" or "tail -n+4". It is not obvious how this should be done for PowerShell.

Useful information is spread across other answers here, but I think it is useful to have a concise summary:
All lines except the first three
1..10 | Select-Object -skip 3
returns (one per line): 4 5 6 7 8 9 10
All lines except the last three
1..10 | Select-Object -skip 3 -last 10
returns (one per line): 1 2 3 4 5 6 7
That is, you can do it with built-in PowerShell commands, but there's that annoyance of having to specify the size going in. A simple workaround is to just use a constant larger than any possible input and you will not need to know the size a priori:
1..10 | Select-Object -skip 3 -last 10000000
returns (one per line): 1 2 3 4 5 6 7
A cleaner syntax is to use, as Keith Hill suggested, the Skip-Object cmdlet from PowerShell Community Extensions (the Skip-Last function in Goyuix's answer performs equivalently but using PSCX saves you from having to maintain the code):
1..10 | Skip-Object -last 3
returns (one per line): 1 2 3 4 5 6 7
First three lines
1..10 | Select-Object –first 3
returns (one per line): 1 2 3
Last three lines
1..10 | Select-Object –last 3
returns (one per line): 8 9 10
Middle four lines
(This works because the -skip is processed before the -first, regardless of the order of parameters in the invocation.)
1..10 | Select-Object -skip 3 -first 4
returns (one per line): 4 5 6 7

Like the -First and -Last parameters, there is also a -Skip parameter that will help. It is worth noting that -Skip is 1 based, not zero.
# this will skip the first three lines of the text file
cat myfile | select -skip 3
I am not sure PowerShell has something that gives you back everything except the last n lines pre-built. If you know the length you could just subtract n from the line count and use the -First parameter from select. You could also use a buffer that only passes lines through when it is filled.
function Skip-Last {
param (
[Parameter(Mandatory=$true,ValueFromPipeline=$true)][PsObject]$InputObject,
[Parameter(Mandatory=$true)][int]$Count
)
begin {
$buf = New-Object 'System.Collections.Generic.Queue[string]'
}
process {
if ($buf.Count -eq $Count) { $buf.Dequeue() }
$buf.Enqueue($InputObject)
}
}
As a demo:
# this would display the entire file except the last five lines
cat myfile | Skip-Last -count 5

If you're using the PowerShell Community Extensions, there is a Take-Object cmdlet that will pass thru all output except the last N items e.g.:
30# 1..10 | Skip-Object -Last 4
1
2
3
4
5
6

All but the last n can be done with
... | select -skiplast $n

You can do it like this:
[array]$Service = Get-Service
$Service[0] #First Item
$Service[0..2] #First 3 Items
$Service[3..($Service.Count)] #Skip the first 3 lines
$Service[-1] #Last Item
$Service[-3..-1] #Last 3 Items
$Service[0..($Service.Count -4)] #Skip the last 3 lines

All but the first n can be done with
... | Select -skip $n
However all "but the last m" has nothing inbuilt. It is doable loading the whole input into an array to get the length – of course for large input that can put unreasonable demands on memory.

Related

Keep last x version of folders in every selected directory

I would like to somehow keep the last x amount of directories in the selected directory.
Ie. directory structure:
d:\test\
a
1
2
3
4
b
1
2
3
4
c
1
2
3
4
d
1
2
3
4
CASE 1: If I give this: d:\test or d:\test\* with 2 last versions then the result should be:
c
1
2
3
4
d
1
2
3
4
CASE 2: If I give this: d:\test\*\* with 2 last versions then the result should be:
a
3
4
b
3
4
c
3
4
d
3
4
CASE 3: If I give this: d:\test\*\*\* with 2 last versions then similarly to the previous case, the parents need to stay and only the subfolders need to be removed.
Until now I've found this:
Get-ChildItem -Path D:\test -Directory | Sort-Object -Property CreationTime | Select-Object -SkipLast 2 | Remove-Item
This does work for case 1, but not for cases 2 and 3.
Ok, it seems I've found a version that seems to work with all cases, but I don't know if there is a more efficient way to do this. Here is my version:
$group_dirs = Get-ChildItem -Path $path -Directory -Force | Group-Object -Property Parent
foreach ($group_dir in $group_dirs) {
$group_dir.Group | Sort-Object -Property CreationTime | Select-Object -SkipLast $leftCount | Remove-Item -Force
}

Adding Header to the Variable

This is probably a dumb question but I cant seem to figure it out. How do I add a header to already existing variable? I have a variable with bunch of strings in it and I am trying to make it so it has a header which will simplify the script later on. Just as an example
$test = 1,2,3,4,5,6
Which comes out to be:
PS C:\windows\system32> $test
1
2
3
4
5
6
Where as what I want it to do is:
PS C:\windows\system32> $test
Numbers
--------
1
2
3
4
5
6
Additionally when implementing for each loop is it possible to add a blank header like to existing variable (from which foreach loop is running) and fill it automatically? for example going from original variable:
Letters Value
------- -----
a 10
b 15
c 23
d 25
To after for each loop:
Letters Value Numbers
------- ----- ------
a 10 1
b 15 2
c 23 3
d 25 4
This is a super generic example but basically i have one object with headers and when using a function someone made I am trying to populate the table with output of that function, the issue is that its returning stuff with no header and just returns the output only so I cant even make a hash table.
Thanks in advance.
In your example, your variable is a list of integers.
That's why there's no header.
If your variable were something else, like, a custom object, it would be displayed with headers.
To make your example a list of custom objects:
$test = 1..6
$test | Foreach-Object { [PSCustomObject]#{Number=$_} }
You can save this back to a variable:
$test = 1..6
$testObjects = $test | Foreach-Object { [PSCustomObject]#{Number=$_} }
If an object has four or fewer properties, it will be displayed as a table.
So you could also, say, make an object with two properties and still get headers.
$test = 1..6
$test | Foreach-Object { [PSCustomObject]#{Number=$_;NumberTimesTwo = $_ * 2} }
If you want to control how any object displays in PowerShell, you'll want to learn about writing formatters. There's a module I make called EZOut that makes these a lot easier to work with.
To offer an alternative to Start-Automating's helpful answer:
You can use Select-Object with calculated properties:
To turn a list of numbers into objects ([pscustomobject] instances) with a .Number property, whose display formatting defaults to the tabular display you're looking for:
$objects =
1,2,3,4,5,6 | Select-Object #{ Name='Number'; Expression={ $_ } }
Outputting $objects yields:
Number
------
1
2
3
4
5
6
You can use the same technique for adding additional properties to (copies of) existing objects, filling them at the same time (builds on the $objects variable filled above):
# Values for the new property.
$nums = 6..1 # same as: 6, 5, 4, 3, 2, 1
$i = #{ val = 0 } # Helper hashtable for indexing into $nums
$objects | Select-Object *, #{ Name='NewProperty'; Expression={ $nums[$i.val++] } }
Output:
Numbers NewProperty
------- -----------
1 6
2 5
3 4
4 3
5 2
6 1
If you want to add a property with $null values first and fill them later, use:
$objects | Select-Object *, NewProperty

Powershell Display rows only if column 2 -cge column 3 for a group based on column 1

I have a csv like this:
MPN,Per_Pallet,Customer_Order,Customer_Order_Date,Backordered_by_Pallet,Reserved_Sum
501,116.82,12055,4/28/2021,3.18,1.02
501,116.82,12421,6/7/2021,2.36,1.02
501,116.82,12424,6/7/2021,3.91,1.02
2243,30,12014,4/26/2021,1.4,1
2243,30,12425,6/7/2021,4.8,1
2243,30,12817,7/21/2021,0.4,1
2243,30,13359,9/29/2021,0.6,1
2435,50.22,12014,4/26/2021,1,2
2435,50.22,13311,9/24/2021,1.14,2
218,40,13236,9/15/2021,3,5
218,40,13382,10/4/2021,3,5
7593,64,12670,7/2/2021,5,5
484,8,12582,6/22/2021,0.38,2
484,8,12798,7/16/2021,1.38,2
484,8,13255,9/18/2021,1,2
484,8,13288,9/22/2021,1,2
5647,87,13304,9/23/2021,0.01,1
I need to group by the MPN column then check the oldest order first to see if Backordered_by_Pallet is greater than or equal to Reserved_Sum.
If it is -cge display only that row for that group. if its not, then check to see if the next order plus the first order is and display both of them and so on. until the backorered total is greater than Reserved_Sum
This is what it looks like in my head:
look at oldest order first for matching MPN
if oldest orders Backordered > Reserved Sum
Then only display oldest order
Else if oldest order + second oldest order > Reserved Sum
then display both orders
Else If Less Than, Add Next Order etc
Expected Output:
MPN,Per_Pallet,Customer_Order,Customer_Order_Date,Backordered_by_Pallet,Reserved_Sum
501,116.82,12055,4/28/2021,3.18,1.02
2243,30,12014,4/26/2021,1.4,1
2435,50.22,13311,9/24/2021,1.14,2
218,40,13236,9/15/2021,3,5
218,40,13382,10/4/2021,3,5
7593,64,12670,7/2/2021,5,5
484,8,12582,6/22/2021,0.38,2
484,8,12798,7/16/2021,1.38,2
484,8,13255,9/18/2021,1,2
5647,87,13304,9/23/2021,0.01,1
I have gotten different pieces to work, but i cant figure out how to put it all together:
returning if its greater or not is easy enough:
$Magic | ForEach-Object {
If ($_.Backordered_by_Pallet -cge $_.Reserved_Sum) {$_}
Else {"Nothing To Order"}
}
and i have tried adding in a group by
$Magic | Group-Object MPN | ForEach-Object {
If ($_.group.Backordered_by_Pallet -cge $_.group.Reserved_Sum) {$_}
Else {"Nothing_Left_To_Order"}
}
but that displays the whole group or nothing and im not sure how to combine it all, not to mention how to add the previous rows amount if needed.
I believe i need to do a several layer deep for-each so i group the MPN, make an array for just that one mpn, then a for each on that array (sorted by oldest) (not sure how to pull the previous row to add) then export just the results, then the loop moves on to the next group and so on.
Like this? I know this is not real, i jut cant figure it out
$Magic_Hash = $Magic_File | Group-Object -Property MPN -AsHashTable | Sort $_.group.Customer_Order_Date
ForEach ($item in $Magic_Hash) {
If ($item.group.Backordered_by_Pallet -cge $_.group.Reserved_Sum) {$_}
Elseif ($item.group.Backordered_by_Pallet + $item.group.Backordered_by_Pallett["2nd oldest order"] -cge $_.group.Reserved_Sum) {$_}
else {"Nothing_Left"}
}
```
Thank you so much for all your help this community is amazing
The code itself is quite awful, but I believe this works. I added comments to understand more or less the thought process.
One thing to note is, "Nothing To Order" has no place or is not defined how you want to display this since, it is a string and if you need to display this information it would probably have to be inserted on one of the cells or create a new column for this.
#'
MPN,Per_Pallet,Customer_Order,Customer_Order_Date,Backordered_by_Pallet,Reserved_Sum
501,116.82,12055,4/28/2021,3.18,1.02
501,116.82,12421,6/7/2021,2.36,1.02
501,116.82,12424,6/7/2021,3.91,1.02
2243,30,12014,4/26/2021,1.4,1
2243,30,12425,6/7/2021,4.8,1
2243,30,12817,7/21/2021,0.4,1
2243,30,13359,9/29/2021,0.6,1
2435,50.22,12014,4/26/2021,1,2
2435,50.22,13311,9/24/2021,1.14,2
218,40,13236,9/15/2021,3,5
218,40,13382,10/4/2021,3,5
7593,64,12670,7/2/2021,5,5
484,8,12582,6/22/2021,0.38,2
484,8,12798,7/16/2021,1.38,2
484,8,13255,9/18/2021,1,2
484,8,13288,9/22/2021,1,2
5647,87,13304,9/23/2021,0.01,1
'# |ConvertFrom-Csv |
Group-Object MPN | ForEach-Object {
$skip = $false
[double]$backorderSum = 0
# Sort by Customer_Order_Date, oldest will be first in line
foreach($line in $_.Group | Sort-Object {[datetime]$_.Customer_Order_Date})
{
if($skip)
{
continue
}
# If Backordered_by_Pallet is greater than or equal to Reserved_Sum
if([double]$line.Backordered_by_Pallet -ge [double]$line.Reserved_Sum)
{
# Display this line and skip the rest
$skip = $true
$line
}
else
{
# Display this line
$line
# Keep a record of previous Values
$backorderSum += $line.Backordered_by_Pallet
# Until this record is greater than or equal to Reserved_Sum
if($backorderSum -ge [double]$line.Reserved_Sum)
{
# Skip the rest when this condition is met
$skip = $true
}
}
}
} | FT
OUTPUT
MPN Per_Pallet Customer_Order Customer_Order_Date Backordered_by_Pallet Reserved_Sum
--- ---------- -------------- ------------------- --------------------- ------------
501 116.82 12055 4/28/2021 3.18 1.02
2243 30 12014 4/26/2021 1.4 1
2435 50.22 12014 4/26/2021 1 2
2435 50.22 13311 9/24/2021 1.14 2
218 40 13236 9/15/2021 3 5
218 40 13382 10/4/2021 3 5
7593 64 12670 7/2/2021 5 5
484 8 12582 6/22/2021 0.38 2
484 8 12798 7/16/2021 1.38 2
484 8 13255 9/18/2021 1 2
5647 87 13304 9/23/2021 0.01 1
First step is to group the records based on the MPN column/property, so let's do that first, using the aptly named Group-Object cmdlet:
$records = #'
MPN,Per_Pallet,Customer_Order,Customer_Order_Date,Backordered_by_Pallet,Reserved_Sum
501,116.82,12055,4/28/2021,3.18,1.02
501,116.82,12421,6/7/2021,2.36,1.02
501,116.82,12424,6/7/2021,3.91,1.02
2243,30,12014,4/26/2021,1.4,1
2243,30,12425,6/7/2021,4.8,1
2243,30,12817,7/21/2021,0.4,1
2243,30,13359,9/29/2021,0.6,1
2435,50.22,12014,4/26/2021,1,2
2435,50.22,13311,9/24/2021,1.14,2
218,40,13236,9/15/2021,3,5
218,40,13382,10/4/2021,3,5
7593,64,12670,7/2/2021,5,5
484,8,12582,6/22/2021,0.38,2
484,8,12798,7/16/2021,1.38,2
484,8,13255,9/18/2021,1,2
484,8,13288,9/22/2021,1,2
5647,87,13304,9/23/2021,0.01,1
'# |ConvertFrom-Csv
$groups = $records |Group-Object MPN
Now that they're all grouped together correctly, we can start going through each group, sort the associated records by date/order number, and then output the first one that matches the condition:
foreach($group in $groups){
# sort records by order number
$recordsInGroup = $group.Group |Sort-Object Customer_Order
# filter records based on the criteria, output only the first 1
$recordsInGroup |Where-Object { +$_.Backordered_by_Pallet -ge $_.Reserved_Sum } |Select-Object -First 1
}
The + in front of $_.Backordered_by_Pallet in the Where-Object filter will mae PowerShell convert the value to a [double], ensuring correct numeric comparison with $_.Reserved_Sum

Sum of even rows

I am new in PowerShell and i would like to get a program that sum, the even rows from a text file for example:
12 12
14 15
13 14
14+15=29
I have already tried the get-content | measure -sum , but it did not work.
You can do the following assuming your text file is named sum.txt:
Get-Content sum.txt -ReadCount 2 |
Where Count -eq 2 |
Foreach-Object {
$numbers = $_[-1] -split ' '
[int]$numbers[0] + [int]$numbers[1]
}
Explanation:
The -ReadCount 2 property reads in two lines at a time and passes those two lines as an object into the pipeline.
The Where {} block is to prevent the last line from outputting if it is an odd numbered line.
$_ is the current pipeline object in the Foreach-Object block. Since two lines are passed in, $_[-1] will return the last line of the two.
-split ' ' separates the two numbers into an array because they are space-separated. Since -split returns a string, we need to cast each number as a numeric type ([int]) in order to perform the addition.

Count line lengths in file using powershell

If I have a long file with lots of lines of varying lengths, how can I count the occurrences of each line length?
Example:
this
is
a
sample
file
with
several
lines
of
varying
length
Output:
Length Occurences
1 1
2 2
4 3
5 1
6 2
7 2
Have you got any ideas?
For high-volume work, use Get-Content with -ReadCount
$ht = #{}
Get-Content <file> -ReadCount 1000 |
foreach {
foreach ($line in $_)
{$ht[$line.length]++}
}
$ht.GetEnumerator() | sort Name
How about
get-content <file> | Group-Object -Property Length | sort -Property Name
Depending on how long your file is, you may want to do something more efficient