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

28,839

Solution 1

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

Solution 2

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.

Solution 3

For gathering information.

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

$result  = &quser
$result -replace '\s{2,}', ',' | ConvertFrom-Csv

Solution 4

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

Solution 5

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.

 USERNAME              SESSIONNAME        ID  STATE   IDLE TIME  LOGON TIME
 rwo                   rdp-sxs22010...   342  Active         48  2/8/2022 1:41 PM
# q2.ps1

$first = 1
quser 2>$null | ForEach-Object {
    if ($first -eq 1) {
        $userPos = $_.IndexOf("USERNAME")
        $sessionPos = $_.IndexOf("SESSIONNAME")
        $idPos = $_.IndexOf("ID") - 2  # id is right justified
        $statePos = $_.IndexOf("STATE")
        $idlePos = $_.IndexOf("IDLE TIME")
        $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
Share:
28,839
Tyler Dickson
Author by

Tyler Dickson

Updated on February 09, 2022

Comments

  • Tyler Dickson
    Tyler Dickson about 2 years

    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 
        { 
    
        }
    
  • sodawillow
    sodawillow over 7 years
    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)
  • Tyler Dickson
    Tyler Dickson over 7 years
    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 @Poorkenny 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.
  • Donald Duck
    Donald Duck about 3 years
    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.
  • vvilin
    vvilin almost 3 years
    @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 ?
  • Daniel
    Daniel about 2 years
    smart! :) \s{2,} is simpler and does the same