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

12

16

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 
    { 

    }
Fr answered 29/8, 2016 at 17:48 Comment(3)
I find it somewhat strange that I can't find a "pure-Powershell" solution to that. Someone else took the time to create a script to do just what you're looking for, though: gallery.technet.microsoft.com/scriptcenter/…Commandeer
See this also : sysadminasaservice.wordpress.com/2015/08/10/…Pulverize
@poorkenny : a pure Powershell solution is possible using Pinvoke to call WTSQuerySessionInformation (this requires a lot of code, especially for a PS purist who would create this with the ModuleBuilder in .NET instead of embedding C# code).Sumatra
F
5

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 :)

Fr answered 29/8, 2016 at 18:24 Comment(3)
What is "sloppy", and would you want to change here ? (Try to sum up in one comment, we won't have many allowed :-p - For now, I'm trying to polish the code from my comment above)Pulverize
Just the foreach(split){foreach(split){}} going on. I'm sure there's an easier way to parse the information. Honestly, this is the best solution I could come up with without over-complicating the solution. As query user (quser) already does what I would like to do. I'm mainly just looking for a solution to managing the output. Your solution along with the link by @Commandeer both work quite well, I'm just picky for short and sweet solutions :P So I guess in summary, this solution isn't too sloppy - I just wish there was a shorter way to sort these into manageable objects.Fr
@TylerDickson I see an issue with your solution, if nobody is connected to the server (say a remote machine), then the SESSIONNAME might be empty, thus creating a 'gap' resulting in $Parsed_Server[2] being ID instead of [3] ; [3] being STATE instead of [4]... Do you see a solution ?Consternation
C
12

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.

Commonly answered 14/11, 2018 at 15:19 Comment(2)
smart! :) \s{2,} is simpler and does the sameResponsiveness
This works great, except for when a user disconnected their session instead of fully logging off, which causes the SESSIONNAME column to be blank, so the rest of the fields are assigned to the wrong column. My quick hack is '\s{2,18}' for the first replace parameter. A more robust solution would probably be js2010's.Levity
F
5

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 :)

Fr answered 29/8, 2016 at 18:24 Comment(3)
What is "sloppy", and would you want to change here ? (Try to sum up in one comment, we won't have many allowed :-p - For now, I'm trying to polish the code from my comment above)Pulverize
Just the foreach(split){foreach(split){}} going on. I'm sure there's an easier way to parse the information. Honestly, this is the best solution I could come up with without over-complicating the solution. As query user (quser) already does what I would like to do. I'm mainly just looking for a solution to managing the output. Your solution along with the link by @Commandeer both work quite well, I'm just picky for short and sweet solutions :P So I guess in summary, this solution isn't too sloppy - I just wish there was a shorter way to sort these into manageable objects.Fr
@TylerDickson I see an issue with your solution, if nobody is connected to the server (say a remote machine), then the SESSIONNAME might be empty, thus creating a 'gap' resulting in $Parsed_Server[2] being ID instead of [3] ; [3] being STATE instead of [4]... Do you see a solution ?Consternation
S
3

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

Another little one from reddit (do you need the first replace?):

(quser 2>$null) -replace '\s{20,39}', ',,' -replace '\s{2,}', ',' |
  ConvertFrom-Csv
Swart answered 20/6, 2020 at 20:36 Comment(2)
Handle dates better for other Locales, this code failed in UK date formats, e.g. "18/09/2023 09:22" $dateFormat = [System.Globalization.CultureInfo]::CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm" $logon = [datetime]::ParseExact($_.substring($logonPos,$_.length-$logonPos).Trim(), $dateFormat, [System.Globalization.CultureInfo]::CurrentCulture)Derinna
This is the only correct answer.Fabrianne
F
2
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
Famine answered 1/8, 2019 at 4:21 Comment(0)
R
2

For gathering information.

based on https://ss64.com/nt/query-user.html

$result  = &quser
$result -replace '\s{2,}', ',' | ConvertFrom-Csv
Rama answered 20/6, 2020 at 18:40 Comment(0)
P
2

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.

Pontias answered 16/8, 2022 at 16:49 Comment(0)
S
1

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

Spectroscopy answered 1/7, 2022 at 23:41 Comment(0)
S
0

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
Spirillum answered 14/7, 2020 at 6:44 Comment(0)
A
0

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
Absorption answered 14/9, 2022 at 15:44 Comment(0)
R
0

Consider using this general ConvertFrom-SourceTable cmdlet.

$QueryUser = @'
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
'@

$QueryUser | ConvertFrom-SourceTable | Format-Table

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
Rustic answered 15/8, 2023 at 6:38 Comment(0)
B
-1

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
}
Bisset answered 22/2, 2017 at 5:14 Comment(0)
A
-1
$result = (&quser) -replace '\s{2,}', ',' | ConvertFrom-Csv | Select -ExpandProperty USERNAME
$loggedinuser = $result.Trim(">")
Alake answered 12/3, 2021 at 22:34 Comment(1)
While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value.Ascension

© 2022 - 2024 — McMap. All rights reserved.