I would like to take the following string and parse it in a way to output the length of each step. Time is listed as hr:min:sec.
Edited Desired Output:
[Step 2] took 2 minutes to complete.
[Test 3] took 3 minutes and 3 seconds to complete.
[Example 4] took 3 minutes to complete.
edited to ask that each line item in brackets of $content is output individually rather than simply listing "Step"
$Content =
[Step1]::14:37:11
[Step2]::14:39:11
[Test3]::14:42:14
[Example4]::14:45:14
In this case, I would create an array of TimeSpan objects from the strings in $content and use these for the output.
IF $content is an array, use it straight away.
IF $content is a single string, you need to split it first with $content = $content -split '\r?\n'
# assuming $content is a single string, so we need to split into array:
$Content = #"
[Step1]::14:37:11
[Step2]::14:39:11
[Step3]::14:42:14
[Step4]::14:45:09
[Step5]::14:57:12
"# -split '\r?\n'
Next loop through this string array and remove empty lines if there are any:
$times = $content | Where-Object { $_ -match '\S' } | ForEach-Object {
$h, $m, $s = ($_.Trim() -replace '.*(\d{2}:\d{2}:\d{2})$', '$1') -split ':'
[TimeSpan]::new([int]$h, [int]$m, [int]$s)
}
for ($i = 1; $i -lt $times.Count; $i++) {
$timeDiff = $times[$i] - $times[$i - 1]
# output the times taken for each step
"Step $($i + 1) took $($timeDiff.ToString()) to complete."
}
Output:
Step 2 took 00:02:00 to complete.
Step 3 took 00:03:03 to complete.
Step 4 took 00:02:55 to complete.
Step 5 took 00:12:03 to complete.
Regex details for -replace:
. Match any single character that is not a line break character
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
( Match the regular expression below and capture its match into backreference number 1
\d Match a single digit 0..9
{2} Exactly 2 times
: Match the character “:” literally
\d Match a single digit 0..9
{2} Exactly 2 times
: Match the character “:” literally
\d Match a single digit 0..9
{2} Exactly 2 times
)
$ Assert position at the end of the string (or before the line break at the end of the string, if any)
As per your new requirements in the comment, here's the somewhat altered code:
$Content = #"
[Step1]::14:37:11
[Step2]::14:39:11
[Test3]::14:42:14
[Example4]::14:45:14
"# -split '\r?\n'
$times = $content | Where-Object { $_ -match '\S' } | ForEach-Object {
# capture the action like '[Example4]' and the timestamp part
$action, $processTime = $_.Trim() -split '::'
# split the timestamp into Hours, Minutes, Seconds
$h, $m, $s = ($processTime -replace '.*(\d{2}:\d{2}:\d{2})$', '$1') -split ':'
# output an object with two properties, the action name and a TimeSpan
[PsCustomObject]#{
'Action' = $action
'Time' = [TimeSpan]::new([int]$h, [int]$m, [int]$s)
}
}
# loop through the returned array, get the time difference between steps
for ($i = 1; $i -lt $times.Count; $i++) {
$timeDiff = $times[$i].Time - $times[$i - 1].Time
# output the times taken for each step, in this new requirement rounded to show minutes only
'{0} took {1} minutes to complete' -f $times[$i].Action, [math]::Round($timeDiff.TotalMinutes, 0)
}
Output:
[Step2] took 2 minutes to complete
[Test3] took 3 minutes to complete
[Example4] took 3 minutes to complete
here's a slightly different way to get there ... [grin]
what it does ...
creates a set of time info lines to work with
when ready to do this for real, remove the entire #region/#endregion block and replace it with a call to Get-Content.
filters out the lines that don't have a time info string
iterates thru the lines starting # 1 and ending # the upper bound of the collection
this skips [step1] since your desired output indicates the timing info is the END of each step.
grabs the start time from the previous array item
grabs the step name and end time from the current array item
builds a string using the -f string format operator
calcs the elapsed time by converting the time info to timespan objects & subtracting the start from the end
sends that out to the screen
i was tempted to put the info into an array of PSCustomObjects, but you seem to want only the string output. [grin]
the code ...
#region >>> fake reading in a text file
# in real life, use Get-Content
$InStuff = #'
$Content =
[Step1]::14:37:11
[Step2]::14:39:11
[Step3]::14:42:14
[Step4]::14:45:09
[Step5]::14:57:12
'# -split [System.Environment]::NewLine
#endregion >>> fake reading in a text file
# get rid of the no-time-info lines
$TimeLines = $InStuff -match '::'
foreach ($Index in 1..$TimeLines.GetUpperBound(0))
{
$StartTime = ($TimeLines[$Index - 1] -split '::')[-1]
$Step, $EndTime = $TimeLines[$Index] -split '::'
'{0} took {1} [hh:mm:ss] to complete.' -f $Step, ([timespan]$EndTime - [timespan]$StartTime)
}
output ...
[Step2] took 00:02:00 [hh:mm:ss] to complete.
[Step3] took 00:03:03 [hh:mm:ss] to complete.
[Step4] took 00:02:55 [hh:mm:ss] to complete.
[Step5] took 00:12:03 [hh:mm:ss] to complete.
Related
How can I force conversion to type System.Version in PowerShell, or more likely, better understand why I cannot arbitrarily assign number strings type System.Version?
We ingest some software updates in folders whose titles include version numbers. In trying to get reports on what the latest versions ingested are, I have been doing the following quick and dirty:
ForEach ($Folder in $(Get-ChildItem -Path $SoftwareDirectory -Directory))
{
$CurrentVersion = $Folder -Replace "[^0-9.]"
If ($CurrentVersion -ne $null)
{
If ([System.Version]$CurrentVersion -gt [System.Version]$MaxVersion)
{
$MaxVersion = $CurrentVersion
$MaxFolder = $Folder
}
}
}
This would be fed directory titles like the following,
foo-tools-1.12.file
bar-static-3.4.0.file
Most of the time, this is acceptable. However, when encountering some oddballs with longer numbers, like the following,
applet-4u331r364.file
In which case, System.Version refuses the resulting string as being too long.
Cannot convert value "4331364" to type "System.Version". Error: "Version string portion was too short or too long."
You need to ensure that your version strings have at least two components in order for a cast to [version] to succeed:
(
#(
'oo-tools-1.12.file'
'bar-static-3.4.0.file'
'applet-4u331r364.file'
) -replace '[^0-9.]'
).TrimEnd('.') -replace '^[^.]+$', '$&.0' | ForEach-Object { [version] $_ }
The above transforms 'applet-4u331r364.file' into '4331364.0', which works when cast to [version].
Note that you can avoid the need for .TrimEnd('.') if you exclude the filename extension to begin with: $Folder.BaseName -replace '[^0-9.]'
-replace '^[^.]+$', '$&.0' matches only strings that contain no . chars., in full, i.e. only those that don't already have at least two components; replacement expression $&.0 appends literal .0 to the matched string ($&).
Output (via Format-Table -AutoSize):
Major Minor Build Revision
----- ----- ----- --------
1 12 -1 -1
3 4 0 -1
4331364 0 -1 -1
I have a log file like this :
[2021/04/13 18:21:57.577+02:00][VERBOSE] Finished: 0 file(s), 5.23 GB; Average Speed:17.26 MB/s.
I just want to remove all string between the "," and "/s." I tried many times I can't do it correctly.
Can someone help me to do this on Powershell ?
If you do not only need to get the interesting part from the log line, but also need to be able to do math on the number ('5.23 GB') in your example, you need to do some more splitting:
foreach ($line in (Get-Content -Path 'TheFile.log')) {
$interestingPart = ($line -split ',')[-1].Trim()
$logSize, $logAverage = $interestingPart -split ';'
$size, $unit = $logSize -split '\s+'
# calculate the size from both the number ('5.23') and the unit ('GB')
$size = [double]::Parse($size) * "1$unit"
# now you have the number to do further math on
}
We have a text file of students and their notes and we have to count how many "1" notes all the students have got.
My code shows how many lines contain the "1" note, but when it finds a "1", it jumps to the next line.
Could you help me please?
for example:
Huckleberry Finn 2 1 4 1 1
Tom Sawyer 3 2 1 4 1
It should be 5, but it gets 2.
$ones = 0
$file= Get-Content notes.txt
foreach ($i in $file) {
if ($i.Split(' ') -eq 1){
$ones ++
}
}
$ones
If all the 1 tokens are whitespace-separated in your input file, as the sample content suggests, try:
# With your sample file content, $ones will receive the number 5.
$ones = (-split (Get-Content -Raw notes.txt) -eq '1').Count
The above uses the unary form of -split, the string splitting operator and the -Raw switch of the Get-Content cmdlet, which loads a file into memory as a single, multi-line string.
That is, the command above splits the entire file into white-space separated tokens, and counts all tokens that equal '1', across all lines.
If, instead, you meant to count the number of '1' tokens per line, use the following approach:
# With your sample file content, $ones will receive the following *array*:
# 3, 2
$ones = Get-Content notes.txt | ForEach-Object { ((-split $_) -eq '1').Count }
As for what you tried:
if ($i.Split(' ') -eq 1)
While $i.Split(' ') does return all (single-)space-separated tokens contained in a single input line stored in $i, using that expression in a conditional expression of an if statement only results in one invocation of the associated { ... } block and therefore increments $ones only by a value of 1, not the number of 1 tokens in the line at hand.
Solved!
Thank you mklement0!
I don't understand why, but it works so:
$ones = 0
$file= Get-Content notes.txt
foreach ($i in $file) {
$ones=(-split ($i) -eq '1').Count
}
}
$ones
I have a text file which is comprised of only one line. I have had much trouble with splitting the file into a specific number of characters, then adding a string in front of each chunk of characters.
With a multi-line file, I can add characters to each line very easily using
Get-Content -Path $path | foreach-object {$string + $_} | out-file $output but it is much more complicated with a file with only one line.
For example, if I had a file with these random characters,
(******************************************) and i wanted to add a string to the start of every 10 chars, then it would look like this, (examplestring**********examplestring**********examplestring**********) and so on. I have researched everywhere but I have just managed to add the chars to the end of each chunk of characters.
Does anyone have a way of doing this? Preferably using streamreader and writer as get-content may not work for very large files. Thanks.
Hmm, there are some dynamic parameters applicable to file-system get-content and set-content commands that are close to what you are asking for. For example, if test.txt contains a number of * characters, you might interleave every four * with two + characters with something like this:
get-content .\test.txt -Delimiter "****" | % { "++$_" } | Set-Content test2.txt -NoNewline
I don't know how close that is to a match for what you want, but it's probably useful to know that some of these provider-specific parameters, like '-Delimiter' aren't obvious. See https://technet.microsoft.com/en-us/library/hh847764.aspx under the heading 'splitting large files'.
Alternatively, here's a quick function that reads length-delimited strings from a file.
Set-StrictMode -Version latest
function read-characters( $path, [int]$charCount ) {
begin {
$buffer = [char[]]::new($charCount)
$path = Join-Path $pwd $path
[System.IO.StreamReader]$stream = [System.IO.File]::OpenText($path)
try {
while (!$stream.EndOfStream) {
$len = $stream.ReadBlock($buffer,0,$charCount);
if ($len) {Write-Output ([string]::new($buffer,0,$len))}
}
} catch {
Write-Error -Exception $error[0]
} finally {
[void]$stream.Close()
}
}
}
read-characters .\test.txt -charCount 10 |
% {"+$_"} |
write-host -NoNewline
It could use some parameter checking, but should get you started...
With a manageable file size, you might want to try something like this:
$directory = "C:\\"
$inputFile = "test.txt"
$reader = new-object System.IO.StreamReader("{0}{1}" -f ($directory, $inputFile))
# prefix string of each line
$startString = "examplestring"
# how many chars to put on each line
$range = 10
$outputLine = ""
$line = $reader.ReadLine()
$i = 0
while ($i -lt $line.length) {
$outputLine += $($startString + $line.Substring($i, [math]::min($range, ($line.length - $i))))
$i += $range
}
$reader.Close()
write-output $outputLine
Basically it's using substring to cut out each chunk, prefixing the chumk with given string, and appending to the result variable.
Sample input:
==========================
Sample output:
examplestring==========examplestring==========examplestring======
I'm doing some script in Powershell to automate a task. This script is going to get arguments, such as:
PS > myscript.ps1 par=a,1,2,0.1 par=b,3,4,0.1 par=c,5,6,0.1 bin=file.exe exeargs="fixed args for file.exe"
In short, file.exe is an executable which accept parameters (including a, b and c) and this ps1 script is going to execute file.exe passing args a, b and c within the specified range, varying 'em by the specified precision.
The question is, I first split each $arg in $args by the character "=", and then I should split them by "," to get the specified values.
The thing is, when I do:
foreach ($arg in $args)
{
$parts = ([string] $arg).split("=")
Write-Host $parts[1]
}
The output is
a 1 2 0.1
b 3 4 0.1
c 5 6 0.1
file.exe
fixed args for file.exe
I.e., it already substituted the "," character with a whitespace, so my second split should be with white space, not with comma.
Any guess on why does it happen?
Thanks in advance!
First of all why are you writing it like a C program or something? You don't have to pass arguments like that, use $args and split on = etc. when Powershell has a more powerful concept of parameters, whereby you can pass the named paramters and arguments rather than doing the parsing that you are doing. ( More on parameters here: http://technet.microsoft.com/en-us/library/dd315296.aspx)
With that said, let me answer your question:
What you are doing is when you pass in arguments like:
par=a,1,2,0.1 par=b,3,4,0.1 par=c,5,6,0.1 bin=file.exe exeargs="fixed args for file.exe"
you are passing in array of arrays. The first element is the array with elements:
par=a
1
2
0.1
Ok coming to the split:
When you do [string] $a, you are converting the array into a string. By default this means an array with elements 1,2,3 will become 1 2 3.
So your first argument there par=a,1,2,0.1, becomes par=a 1 2 0.1 and the split on = means parts[1] becomes a 1 2 0.1, which is what you see.
So how can you retain the comma?
Just make an array to be converted into a string with , inbetween than space, right?
Here are some ways to do that:
Using -join operator:
foreach ($arg in $args)
{
$parts = ($arg -join ",").split("=")
Write-Host $parts[1]
}
now you will see the output with the commas that you want.
Another way, using $ofs special variable:
foreach ($arg in $args)
{
$ofs =","
$parts = ([string] $arg).split("=")
Write-Host $parts[1]
}
(more on $ofs here: http://blogs.msdn.com/b/powershell/archive/2006/07/15/what-is-ofs.aspx )
Disclaimer - All this explanation to make you understand what is happening. Please do not continue this and use paramters.
This is happening in the parsing of the command line for your script and not during the split() method. To see this, try putting a "Write-Host $args" at the beginning, like so:
Write-Host $args
foreach ($arg in $args)
{
$parts = ([string] $arg).split("=")
Write-Host $parts[1]
}
This is because the ',' character is used to separate elements in an array. For example:
PS C:\temp> $a = 1,2,3
PS C:\temp> Write-Host $a
1 2 3
PS C:\temp> $a.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Try this command line instead:
.\myscript.ps1 "par=a,1,2,0.1" "par=b,3,4,0.1" "par=c,5,6,0.1" "bin=file.exe" "exeargs=fixed args for file.exe"