updating column value for exported csv powershell - powershell

I have following code which is working correctly.
Although I now need to modify the output in one specific column, so I can sort by this column correctly.
Here is my code:
$inputFile = "C:\Data\expPasswords\expPasswords.csv"
$outputFile = "C:\Data\expPasswords\expPasswordsUp.csv"
$result = Import-Csv $inputFile |
Select-Object #{ Name = 'Account'; Expression = { $_.Account } },
#{ Name = 'Days until Expiry'; Expression = { $_.'time until password expires' } },
#{ Name = 'Email address'; Expression = { $_.'email address' } }
# output on screen
$result | Sort-Object -Property 'Days until Expiry' | Format-Table -AutoSize
# output to csv
$result | Sort-Object -Property 'Days until Expiry' | Export-Csv -Path $outputFile -NoTypeInformation
I need to sort by the 'Days until Expiry' column. Although makes it hard when the output is as below:
0 minutes
0 minutes
1 day and 19 hours
1 day and 2 hours
1 day and 20 hours
1 day and 23 hours
13 hours
2 days
20 hours
Basically, what I would like to do is:
- If less than 1 day, make the value: Today
- Remove the hours and minutes blocks.
- So if it is 13 hours, make the value: Today
- If the value is 1 day and 1 hours and 35 minutes, make the value: 1 day
Any assistance will be greatly appreciated. ;-)

Its a shame you should spend time to make some sense out of this rather foolish output, but of course it can be done.
Basically, all you want to do is find out if the string starts with a number followed by the word 'day' or 'days' and cut off all the rest. If this is not the case, the returned value should be 'Today'.
The easiest way to do that I think is by using switch -Regex.
Try
$inputFile = "C:\Data\expPasswords\expPasswords.csv"
$outputFile = "C:\Data\expPasswords\expPasswordsUp.csv"
$result = Import-Csv $inputFile | ForEach-Object {
$daysLeft = switch -Regex ($_.'time until password expires') {
'^(\d+ days?)' { $matches[1] }
default { 'Today' }
}
[PsCustomObject]#{
'Account' = $_.Account
'Days until Expiry' = $daysLeft
'Email address' = $_.'email address'
}
} | Sort-Object -Property 'Days until Expiry'
# output on screen
$result | Format-Table -AutoSize
# output to csv
$result | Export-Csv -Path $outputFile -NoTypeInformation
Regex details:
^ Assert position at the beginning of the string
\d Match a single character that is a “digit” (any decimal number in any Unicode script)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
\ day Match the character string “ day” literally (case sensitive)
s Match the character “s” literally (case sensitive)
? Between zero and one times, as many times as possible, giving back as needed (greedy)
Seeing your comment, I would suggest adding a real DateTime object to sort on.
Something like this:
$today = (Get-Date).Date
$result = Import-Csv 'D:\test.csv' | ForEach-Object {
$expiryString = $_.'time until password expires'
$expiryDate = $today
if ($expiryString -match '(\d+)\s*day') { $expiryDate = $expiryDate.AddDays([int]$matches[1]) }
if ($expiryString -match '(\d+)\s*hour') { $expiryDate = $expiryDate.AddHours([int]$matches[1]) }
if ($expiryString -match '(\d+)\s*minute') { $expiryDate = $expiryDate.AddMinutes([int]$matches[1]) }
if ($expiryString -match '(\d+)\s*second') { $expiryDate = $expiryDate.AddSeconds([int]$matches[1]) }
$daysLeft = if ($expiryDate.Date -eq $today) { 'Today' } else { ($expiryDate - $today).Days}
[PsCustomObject]#{
'Account' = $_.Account
'Email address' = $_.'email address'
'Days until Expiry' = $daysLeft
'Expiration Date' = $expiryDate
}
} | Sort-Object -Property 'Expiration Date'
# output on screen
$result
Output:
Account Email address Days until Expiry Expiration Date
------- ------------- ----------------- ---------------
User1 user1#yourcompany.com Today 6-4-2020 0:00:00
User6 user6#yourcompany.com Today 6-4-2020 0:03:00
User8 user8#yourcompany.com Today 6-4-2020 13:00:00
User4 user4#yourcompany.com Today 6-4-2020 20:00:00
User9 user9#yourcompany.com 1 7-4-2020 2:00:00
User2 user2#yourcompany.com 1 7-4-2020 19:00:00
User5 user5#yourcompany.com 1 7-4-2020 20:00:00
User7 user7#yourcompany.com 1 7-4-2020 23:00:00
User3 user3#yourcompany.com 2 8-4-2020 0:00:00
If you don't want that new property 'Expiration Date' in your output, simply filter it away with:
$result | Select-Object * -ExcludeProperty 'Expiration Date'

I think the following might be of help (you will need to edit some of it, off course):
$Timings = #("0 minutes","0 minutes","1 day and 19 hours","1 day and 2 hours","1 day and 20 hours","1 day and 23 hours","13 hours","2 days","20 hours")
foreach ($Timing in $Timings) {
$Output = $null
if ($Timing -like "* minutes") {$Output = 0}
elseif ($Timing -like "* Day and * hours") {$Output = [int](($Timing).Split(' day')[0])}
elseif ($Timing -like "* hours") {$Output = 0}
else {$Output = [int](($Timing).Split(' day')[0]) }
switch ($Output) {
0 {$Result = "Today"}
1 {$Result = "Tomorrow"}
default {$Result = "Over $Output Days"}
}
Write-Output "$timing ==> $Result"
}

The constrains you defined will likely make it more confusing. I would just convert it to a [TimeSpan] structure which makes it easy to sort:
$Result = ConvertFrom-Csv #'
"Account","Days until Expiry", "Email address"
"Account1","0 minutes", "Name1#gmail.com"
"Account2","1 day and 19 hours","Name2#gmail.com"
"Account3","2 days", "Name3#gmail.com"
"Account4","20 hours", "Name4#gmail.com"
"Account5","1 day and 20 hours","Name5#gmail.com"
"Account6","3 minutes", "Name6#gmail.com"
"Account7","1 day and 23 hours","Name7#gmail.com"
"Account8","13 hours", "Name8#gmail.com"
"Account9","1 day and 2 hours", "Name9#gmail.com"
'#
Function ConvertTo-TimeSpan([String]$String) {
$Days = If ($String -Match '\d+(?=\s*day)') {$Matches[0]} Else {0}
$Hours = If ($String -Match '\d+(?=\s*hour)') {$Matches[0]} Else {0}
$Minutes = If ($String -Match '\d+(?=\s*minute)') {$Matches[0]} Else {0}
$Seconds = If ($String -Match '\d+(?=\s*second)') {$Matches[0]} Else {0}
New-TimeSpan -Days $Days -Hours $Hours -Minutes $Minutes -Seconds $Seconds
}
$Result | Sort #{e = {ConvertTo-TimeSpan $_.'Days until Expiry'}}
Result:
Account Days until Expiry Email address
------- ----------------- -------------
Account1 0 minutes Name1#gmail.com
Account6 3 minutes Name6#gmail.com
Account8 13 hours Name8#gmail.com
Account4 20 hours Name4#gmail.com
Account9 1 day and 2 hours Name9#gmail.com
Account2 1 day and 19 hours Name2#gmail.com
Account5 1 day and 20 hours Name5#gmail.com
Account7 1 day and 23 hours Name7#gmail.com
Account3 2 days Name3#gmail.com

Related

Add a calculated field to an imported csv based on datetime

Could someone please assist with this one.
My current code imports a csv file which has three columns, so far it will update the column names to be more readable. I need to add a fourth column which is a calculated field based on a datetime field.
So need to check the datetime field then display a number of days before it is 90 days old.
e.g. "Today's date" - "03/03/2020 8:00:00 AM" = 31 days
90 days - 31 days = 59 days (the 59 days is to go into the calculated field column
Bit of a newb with powershell and have all other functions working, but this is what I'm left with and need to add it into the below call, when the csv is imported, header columns updated then exported to a new file.
$input = "C:\Data\test\unchanged101.csv"
$output = "C:\Data\test\unchanged101conv.csv"
$checkDate = (Get-Date).AddDays(-90)
$data = Import-Csv $input |
Where-Object {
($_."pwdlastset" -as [DateTime]) -lt $CheckDate
}
$headerConversion = #(
#{ Name = 'User account'; Expression = { $_.'cn' } }
#{ Name = 'Last modified date'; Expression = { $_.'pwdlastset' } }
#{ Name = 'Email address'; Expression = { $_.'mail' } }
)
(Import-Csv -Path $input) |
Select-Object -Property $headerConversion | Select-Object *,"Days Left" |
Export-Csv -Path $output -NoTypeInformation
The new column is the "Days Left" where I need to display the number of days left until it is 90 days old. How to I get the result from the code here, into that column for each row?
$checkDate = (Get-Date).AddDays(-90)
$data = Import-Csv $input |
Where-Object {
($_."pwdlastset" -as [DateTime]) -lt $CheckDate
}
Been working on this one for the past few days and just cant figure the last part out.
First of all, you should not use variable name $input as this is an Automatic variable
If you subtract one datetime object from another, the result is a TimeSpan object which has a property called Days you could use
$inputFile = "C:\Data\test\unchanged101.csv"
$outputFile = "C:\Data\test\unchanged101conv.csv"
$checkDate = (Get-Date).AddDays(-90).Date # .Date sets this to midnight
$result = Import-Csv $inputFile |
Where-Object { [DateTime]$_.pwdlastset -lt $CheckDate } |
Select-Object #{ Name = 'User account'; Expression = { $_.cn } },
#{ Name = 'Last modified date'; Expression = { $_.pwdlastset } },
#{ Name = 'Email address'; Expression = { $_.mail } },
#{ Name = 'Days Left'; Expression = { ($checkDate - [DateTime]$_.pwdlastset).Days } }
# output on screen
$result | Format-Table -AutoSize
# output to csv
$result | Export-Csv -Path $outputFile -NoTypeInformation
I would first add the new column and then go trough all the lines (with a foreach loop) check the remaining days and write them to the new "Days Left" column.
Note: I omitted your header conversion here. You need to run it first and then the "Days Left" code...
$data = Import-Csv $input #import everything
foreach($line in $data){
$daysLeft = 0
$daysSinceLastSet = ((Get-Date) - [DateTime]$line.pwdlastset).Days
if ($daysSinceLastSet -lt 90){
$daysLeft = 90-$daysSinceLastSet
}
$line."Days Left" = $daysLeft
}
$data | Export-Csv -Path $output -NoTypeInformation

How to get the time difference from multiple starttimes and endtime which are coming from a file?

I have file with below data, I want to get the highest time difference from this using PowerShell.
STARTTIME:2018-12-01 04:13:15
ENDTIME:2018-12-01 04:17:15
--
--
STARTTIME:2018-12-01 04:11:15
ENDTIME:2018-12-01 04:13:15
--
STARTTIME:2018-12-01 04:10:15
ENDTIME:2018-12-01 04:10:40
I expect the output to be 00:04:00
Use a Regular Expression to select lines and STARTTIME/ENDTIME
to set a variable with that name and the [datetime] value
## Q:\Test\2019\05\15\SO_56150523.ps1
$FileIn = '.\times.txt'
$RE = [regex]'^(STARTTIME|ENDTIME):(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})'
$Data = (Get-Content $FileIn)| Where-Object {$_ -match $RE}| ForEach-Object{
Set-Variable -Name $Matches[1] -Value (Get-Date $Matches[2])
if ($Matches[1] -eq 'ENDTIME'){
[PSCustomObject]#{
StartTime = $StartTime
EndTime = $EndTime
Duration = New-TimeSpan -Start $StartTime -End $EndTime
}
}
}
$Data
"`nThe longest duration is {0}" -f ($Data|Sort-Object Duration -Descending)[0].Duration
Sample output based on the complete text from question in file times.txt
> Q:\Test\2019\05\15\SO_56150523.ps1
StartTime EndTime Duration
--------- ------- --------
2018-12-01 04:13:15 2018-12-01 04:17:15 00:04:00
2018-12-01 04:11:15 2018-12-01 04:13:15 00:02:00
2018-12-01 04:10:15 2018-12-01 04:10:40 00:00:25
The longest duration is 00:04:00

Get Approximate Date from Week Number

I'm getting information on monitors from WMI. There are properties called "WeekOfManufacture" and "YearOfManufacture". In this case it's:
Week : 24
Year : 2009
I'm trying to get the approximate date corresponding to that.
I'm doing this to get a really rough estimate:
(Get-Date -Date $YearOfManufacture/01/01).AddDays(7*$WeekOfManufacture)
But obviously the "week" doesn't necessarily always have 7 days.
What is the best approach?
The complete code I'm using right now is:
$Monitors = Get-WmiObject WmiMonitorID -Namespace root\wmi
foreach ($Monitor in $Monitors) {
$Manufacturer = ($Monitor.ManufacturerName -notmatch 0 | ForEach{[char]$_}) -join ""
$Code = ($Monitor.ProductCodeID -notmatch 0 | ForEach{[char]$_}) -join ""
$Name = ($Monitor.UserFriendlyName -notmatch 0 | ForEach{[char]$_}) -join ""
$Serial = ($Monitor.SerialNumberID -notmatch 0 | ForEach{[char]$_}) -join ""
$WeekOfManufacture = $Monitor.WeekOfManufacture
$YearOfManufacture = $Monitor.YearOfManufacture
$DateManufacture = (get-date -Date $YearOfManufacture/01/01).AddDays(7*$WeekOfManufacture)
"$Manufacturer - $Code - $Name - $Serial - $DateManufacture"
}
which returns something like (info obfuscated):
SAM - 1234 - SycMastr - XXXX - 06/18/2009 00:00:00
SAM - 1234 - SycMastr - XXXX - 09/10/2009 00:00:00
Answer from Technet works perfectly
(https://social.technet.microsoft.com/Forums/en-US/f65c80b0-f74f-4234-870c-c5ffe8d9b1ea/powershell-get-date-from-week-number-of-year?forum=ITCG)
:
Function FirstDateOfWeek
{
param([int]$year, [int]$weekOfYear)
$jan1 = [DateTime]"$year-01-01"
$daysOffset = ([DayOfWeek]::Thursday - $jan1.DayOfWeek)
$firstThursday = $jan1.AddDays($daysOffset)
$calendar = ([CultureInfo]::CurrentCulture).Calendar;
$firstWeek = $calendar.GetWeekOfYear($firstThursday, [System.Globalization.CalendarWeekRule]::FirstFourDayWeek, [DayOfWeek]::Monday)
$weekNum = $weekOfYear
if($firstweek -le 1) { $weekNum -= 1 }
$result = $firstThursday.AddDays($weekNum * 7)
return $result.AddDays(-3)
}
FirstDateOfWeek -year 2009 -weekOfYear 24
8 juin 2009 00:00:00

Last repetition in csv

I have a csv input file like:
date;time;pc;state
25.01.2017;10:30:57;pc1;inactive
25.01.2017;10:35:57;pc2;active
25.01.2017;10:37:34;pc1;active
25.01.2017;10:38:35;pc3;inactive
25.01.2017;10:39:20;pc1;inactive
25.01.2017;10:42:10;pc2;inactive
25.01.2017;10:42:10;pc3;active
So, i need to show only last pc repetition with inactive state and show the difference between time and current time.
So result must be:
pc1 inactive from 10:39:20
pc2 inactive from 10:42:10
Do not know how to realize it in powershell. Need help :)
Here you go, at the moment it only outputs it via Write-Host but you should be able to rebuild it to do with the data what you want:
$csv = import-csv .\test.csv -Delimiter ";"
$pcs = $csv.pc | select -Unique
foreach($pc in $pcs) {
#get all entries for current pc and order by date/time, then select newest entry
$le = $csv | where {$_.pc -eq $pc} | sort date,time -Descending | select -First 1
#if last state is inactive
if($le.state -eq "inactive"){
$writedate = get-date -Date $le.date -Hour $le.time.Substring(0,2) -Minute $le.time.Substring(3,2) -Second $le.time.Substring(6,2)
$td = (get-date) - $writedate
write-host "time difference for $pc : $($td.Days) days $($td.Hours) hours, $($td.Minutes) minutes, $($td.Seconds) seconds"
}
}
If your time field is not allways in HH:MM:DD you will have to change the script to accomodate that
Two solutions, a short one taking the question literal:
$csv = import-csv .\SO_41941150.csv -Delimiter ";"
$csv|group pc|%{$_.group|select -last 1|? state -eq 'inactive'|%{"$($_.pc) inactive from $($_.time)"}}
With the exact output:
pc1 inactive from 10:39:20
pc2 inactive from 10:42:10
A longer one converting date and time to datetime using ::ParseExact and giving elapsed TotalSeconds/Minutes as a result.
$csv = import-csv .\SO_41941150.csv -Delimiter ";"
$csv | Group-Object pc | Foreach-Object {
$_.Group | Select-Object -last 1 | Where-Object state -eq 'inactive'|
ForEach-Object {
$DT = [datetime]::ParseExact("$($_.date) $($_.time)","dd.MM.yyyy HH:mm:ss",$null)
$Secs = [int]((get-date) - $DT).TotalSeconds
$mins = [int]((get-date) - $DT).TotalMinutes
"$($_.pc) inactive since $Mins minutes or $Secs seconds"
}
}
Output:
pc1 inactive since 9160 minutes or 549578 seconds
pc2 inactive since 9157 minutes or 549408 seconds

Easier way to parse 'query user' in PowerShell (or quser)

I currently have the following query in PowerShell:
query user /server:$server
Which returns output:
USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
svc_chthost 2 Disc 1:05 8/16/2016 12:01 PM
myusername rdp-tcp 3 Active . 8/29/2016 11:29 AM
Currently, I'm using #(query user /server:$server).Count - 1 as a value to represent the number of users logged on (it's not pretty, I know). However now I would like to obtain information such as USERNAME, ID, and LOGON TIME to use in other parts of my script.
My question is surrounding an easier way to parse the information above, or maybe a better solution to my problem all together: Counting and gathering information related to logged on users.
I've found other solutions that seem to work better, but I'm sure there's got to be a simpler way to accomplish this task:
$ComputerName | Foreach-object {
$Computer = $_
try
{
$processinfo = #(Get-WmiObject -class win32_process -ComputerName $Computer -EA "Stop")
if ($processinfo)
{
$processinfo | Foreach-Object {$_.GetOwner().User} |
Where-Object {$_ -ne "NETWORK SERVICE" -and $_ -ne "LOCAL SERVICE" -and $_ -ne "SYSTEM"} |
Sort-Object -Unique |
ForEach-Object { New-Object psobject -Property #{Computer=$Computer;LoggedOn=$_} } |
Select-Object Computer,LoggedOn
}#If
}
catch
{
}
Old question, but it seems a workable solution:
(query user) -split "\n" -replace '\s\s+', ';' | convertfrom-csv -Delimiter ';'
This chunks the output into lines, as the answer above does, but then replaces more than one white space character (\s\s+) with a semi-colon, and then converts that output from csv using the semi-colon as a delimiter.
The reason for more than one white space is that the column headers have spaces in them (idle time, logon time), so with just one space it would try to interpret that as multiple columns. From the output of the command, it looks as if they always preserve at least 2 spaces between items anyway, and the logon time column also has spaces in the field.
Awesome references in the comments, and still open to more answers for this question as it should have an easier solution!
foreach ($s in $servers) #For Each Server
{
foreach($ServerLine in #(query user /server:$s) -split "\n") #Each Server Line
{
#USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
$Parsed_Server = $ServerLine -split '\s+'
$Parsed_Server[1] #USERNAME
$Parsed_Server[2] #SESSIONNAME
$Parsed_Server[3] #ID
$Parsed_Server[4] #STATE
$Parsed_Server[5] #IDLE TIME
$Parsed_Server[6] #LOGON TIME
}
}
This solution solves the problem for now, kind of sloppy.
For more in-depth solutions with more functionalities, check the comments on the original question :)
Function Get-QueryUser(){
Param([switch]$Json) # ALLOWS YOU TO RETURN A JSON OBJECT
$HT = #()
$Lines = #(query user).foreach({$(($_) -replace('\s{2,}',','))}) # REPLACES ALL OCCURENCES OF 2 OR MORE SPACES IN A ROW WITH A SINGLE COMMA
$header=$($Lines[0].split(',').trim()) # EXTRACTS THE FIRST ROW FOR ITS HEADER LINE
for($i=1;$i -lt $($Lines.Count);$i++){ # NOTE $i=1 TO SKIP THE HEADER LINE
$Res = "" | Select-Object $header # CREATES AN EMPTY PSCUSTOMOBJECT WITH PRE DEFINED FIELDS
$Line = $($Lines[$i].split(',')).foreach({ $_.trim().trim('>') }) # SPLITS AND THEN TRIMS ANOMALIES
if($Line.count -eq 5) { $Line = #($Line[0],"$($null)",$Line[1],$Line[2],$Line[3],$Line[4] ) } # ACCOUNTS FOR DISCONNECTED SCENARIO
for($x=0;$x -lt $($Line.count);$x++){
$Res.$($header[$x]) = $Line[$x] # DYNAMICALLY ADDS DATA TO $Res
}
$HT += $Res # APPENDS THE LINE OF DATA AS PSCUSTOMOBJECT TO AN ARRAY
Remove-Variable Res # DESTROYS THE LINE OF DATA BY REMOVING THE VARIABLE
}
if($Json) {
$JsonObj = [pscustomobject]#{ $($env:COMPUTERNAME)=$HT } | convertto-json # CREATES ROOT ELEMENT OF COMPUTERNAME AND ADDS THE COMPLETED ARRAY
Return $JsonObj
} else {
Return $HT
}
}
Get-QueryUser
or
Get-QueryUser -Json
For gathering information.
based on https://ss64.com/nt/query-user.html
$result = &quser
$result -replace '\s{2,}', ',' | ConvertFrom-Csv
My own column based take. I'm not sure how much the ID column can extend to the left. Not sure how wide the end is. This is turning out to be tricky. Maybe this way is better: Convert fixed width txt file to CSV / set-content or out-file -append?
# q.ps1
# USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
# js1111 rdp-tcp#20 136 Active . 6/20/2020 4:26 PM
# jx111 175 Disc . 6/23/2020 1:26 PM
# sm1111 rdp-tcp#126 17 Active . 6/23/2020 1:13 PM
#
# di111111 rdp-tcp#64 189 Active 33 7/1/2020 9:50 AM
# kp111 rdp-tcp#45 253 Active 1:07 7/1/2020 9:43 AM
#
#0, 1-22, 23-40, 41-45, 46-53, 54-64, 65-80/82
$q = quser 2>$null | select -skip 1
$q | foreach {
$result = $_ -match '.(.{22})(.{18})(.{5})(.{8})(.{11})(.{16,18})'
[pscustomobject] #{
USERNAME = $matches[1].trim()
SESSIONNAME = $matches[2].trim()
ID = [int]$matches[3].trim()
STATE = $matches[4].trim()
IdleTime = $matches[5].trim()
LogonTime = [datetime]$matches[6].trim()
}
if (! $matches) {$_}
}
Invoke-command example. This is good if you're using Guacamole.
$c = get-credential
icm comp1,comp2,comp3 q.ps1 -cr $c | ft
USERNAME SESSIONNAME ID STATE IdleTime LogonTime PSComputerName RunspaceId
-------- ----------- -- ----- -------- --------- -------------- ----------
js1 136 Disc . 6/20/2020 4:26:00 PM comp1 a8e670cd-4f31-4fd0-8cab-8aa11ee75a73
js2 137 Disc . 6/20/2020 4:26:00 PM comp2 a8e670cd-4f31-4fd0-8cab-8aa11ee75a74
js3 138 Disc . 6/20/2020 4:26:00 PM comp3 a8e670cd-4f31-4fd0-8cab-8aa11ee75a75
Here's another version. The number in the ID column can be at least 1 column before the header. I figure out where the line ends on every line. The Sessionname ends in 3 dots if it's too long, and at least 2 spaces are between each column. The column headers always start at the same place.
ID can be 4 digits. Tricky.
USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
rwo rdp-sxs22010... 342 Active 48 2/8/2022 1:41 PM
ym326 rdp-sxs22062... 1012 Active 9 9/27/2022 3:42 PM
cw7 rdp-tcp#4 4 Active 11:16 9/26/2022 7:58 AM
# q2.ps1
$first = 1
quser 2>$null | ForEach-Object {
if ($first -eq 1) {
$userPos = $_.IndexOf("USERNAME")
$sessionPos = $_.IndexOf("SESSIONNAME") # max length 15
$idPos = $_.IndexOf("ID") - 4 # id is right justified
# $idPos = $_.IndexOf("SESSIONNAME") + 15
$statePos = $_.IndexOf("STATE") # max length 6
$idlePos = $_.IndexOf("IDLE TIME") - 2 # right justified too
$logonPos = $_.IndexOf("LOGON TIME")
$first = 0
}
else {
$user = $_.substring($userPos,$sessionPos-$userPos).Trim()
$session = $_.substring($sessionPos,$idPos-$sessionPos).Trim()
$id = [int]$_.substring($idPos,$statePos-$idPos).Trim()
$state = $_.substring($statePos,$idlePos-$statePos).Trim()
$idle = $_.substring($idlePos,$logonPos-$idlePos).Trim()
$logon = [datetime]$_.substring($logonPos,$_.length-$logonPos).Trim()
[pscustomobject]#{User = $user; Session = $session; ID = $id;
State = $state; Idle = $idle; Logon = $logon}
}
}
Output:
User Session ID State Idle Logon
---- ------- -- ----- ---- -----
rwo rdp-sxs22010... 342 Active 48 2/8/2022 1:41:00 PM
Edited: Looks like someone have already created a script that actually works pretty well: https://gallery.technet.microsoft.com/scriptcenter/Get-LoggedOnUser-Gathers-7cbe93ea
Cant believe after so many years there is still no native PowerShell for this.
I've touched up what Tyler Dickson has done and ensure the result comes back as PSCustomObject
$Servers = #("10.x.x.x", "10.y.y.y")
$Result = #()
foreach ($Server in $Servers) {
$Lines = #(query user /server:$s) -split "\n"
foreach($Line in $Lines) #Each Server Line
{
if ($Line -match "USERNAME\s+SESSIONNAME\s+ID\s+STATE\s+IDLE TIME\s+LOGON TIME") {
continue # If is the header then skip to next item in array
}
$Parsed_Server = $Line -split '\s+'
$Result += [PSCustomObject]#{
SERVER = $Server
USERNAME = $Parsed_Server[1]
SESSIONNAME = $Parsed_Server[2]
ID = $Parsed_Server[3]
STATE = $Parsed_Server[4]
IDLE_TIME = $Parsed_Server[5]
LOGON_TIME = $Parsed_Server[6]
}
}
}
$Result | Format-Table
Example output:
SERVER USERNAME SESSIONNAME ID STATE IDLE_TIME LOGON_TIME
------ -------- ----------- -- ----- --------- ----------
10.x.x.x user01 rdp-tcp#13 6 Active . 28/06/2020
10.x.x.x user02 rdp-tcp#35 11 Active 59 29/06/2020
10.y.y.y user03 rdp-tcp#38 12 Active . 29/06/2020
10.y.y.y user04 rdp-tcp#43 14 Active 5 29/06/2020
Unfortunately, no one that proposes solutions with replace method didn't notice that it will be a data collision if SESSIONNAME will empty (it will be when user disc)
So you will have SESSIONNAME contain ID, ID contain STATE etc.
It's not good.
So I`ve fixed it by -replace 'rdp-tcp#\d{1,3}' and propose to you solution with headers.
$Header = "UserName", "ID", "State", "Idle", "Logon", "Time"
$Result = $(quser) -replace 'rdp-tcp#\d{1,3}' -replace "^[\s>]", "" -replace "\s+", "," | ConvertFrom-Csv -Header $Header
Now you can access to any object $Result.Username, $Result.Idle
Was looking for the easy solution to the query user problem that also addresses the issue when SessionName is blank. Ended up combining bits and pieces from the above and came up with this. This isn't perfect, but it does seem to work better than most.
$q = (query user) -split "\n" -replace '\s{18}\s+', " blank "
$qasobject = $q -split "\n" -replace '\s\s+', "," | convertfrom-csv
The First pass with -split will replace any chunk of 18 or more spaces with " blank ", NOTE; there are 2 spaces before and after blank.
The second pass with -split will replace anything with 2 or more spaces with a ",", then pass that through convertfrom-csv to make it an object.
If you want a quick solution and don't need all information, you can also do this:
$a = Get-CimInstance -ClassName Win32_UserProfile -ComputerName "Server-1" | where {$_.Loaded -and $_.LocalPath.split('\')[1] -eq "Users" -and $_.Special -eq $false}
$a | ft -a #{N='Name';E={$_.LocalPath.split('\')[2]}},LastUseTime,Loaded
I Further appended the above code to properly format and also consider the Disconnected users
$HaSH = #()
foreach($ServerLine in #(query user) -split "\n") {
$Report = "" | Select-Object UserName, Session, ID, State, IdleTime, LogonTime
$Parsed_Server = $ServerLine -split '\s+'
if($Parsed_Server -like "USERNAME*") {
Continue
}
$Report.UserName = $Parsed_Server[1]
$Report.Session = $Parsed_Server[2]
$Report.ID = $Parsed_Server[3]
$Report.State = $Parsed_Server[4]
$Report.IdleTime = $Parsed_Server[5]
$Report.LogonTime = $Parsed_Server[6]+" " +$Parsed_Server[7]+" "+$Parsed_Server[8]
if($Parsed_Server[3] -eq "Disc") {
$Report.Session = "None"
$Report.ID = $Parsed_Server[2]
$Report.State = $Parsed_Server[3]
$Report.IdleTime = $Parsed_Server[4]
$Report.LogonTime = $Parsed_Server[5]+" " +$Parsed_Server[6]+" "+$Parsed_Server[7]
}
if($Parsed_Server -like ">*") {
$Parsed_Server=$Parsed_Server.Replace(">","")
$Report.UserName = $Parsed_Server[0]
$Report.Session = $Parsed_Server[1]
$Report.ID = $Parsed_Server[2]
$Report.State = $Parsed_Server[3]
$Report.IdleTime = $Parsed_Server[4]
$Report.LogonTime = $Parsed_Server[5]+" " +$Parsed_Server[6]+" "+$Parsed_Server[7]
}
$HaSH+=$Report
}
$result = (&quser) -replace '\s{2,}', ',' | ConvertFrom-Csv | Select -ExpandProperty USERNAME
$loggedinuser = $result.Trim(">")