Wednesday, June 3, 2009

Episode #44: Users & Groups (Part II)

Hal goes first this time:

Last time in Episode #43, Ed presented a challenge to list all groups and the user names that were in each group. But as I was working on Ed's challenge, I realized that there was another way to look at this data. What if instead of a list of "users per group", you wanted to get a list of "groups per user"?

This is actually straightforward in Unix:

$ for u in `cut -f1 -d: /etc/passwd`; do echo -n $u:; groups $u; done | sort
avahi-autoipd:avahi-autoipd
avahi:avahi
backup:backup
bin:bin
bind:bind
daemon:daemon
games:games
gdm:gdm
gnats:gnats
haldaemon:haldaemon
hal:hal adm dialout cdrom plugdev lpadmin admin sambashare
[...]

Here we're using "cut" to pull all the user names from /etc/passwd and then running a loop over them. Inside we output the user name and a trailing colon, but use the "-n" option on the "echo" statement so the we don't output a newline. This means that the output of the "groups $u" command will appear on the same line as the username, immediately after the colon. Finally we're piping the output of the entire loop into "sort", so we get the information sorted by the user names.

I wonder if Ed's going to have to go to as much trouble answering my challenge as I went through answering his...

Ed responds:
When I first read your challenge, Hal, I was thinking, "Uh-oh... Windows is gonna make this hard." My gut told me that while mapping groups to users was easy ("net localgroup [groupname]"), going the other way was gonna be tough.

But, whenever I need to find something out about users on a Windows box, I almost always start out by running "net user [username]". (Yes, yes, there is "wmic useraccount list full" but I usually go there second). So, I ran that command and smiled with glee when I saw a list of all of the associated groups for that user in the output. I started to formulate a FOR /F loop that would run "net user" to get a list of users and then inside the body of the loop would run "net user [username]" on each... when...

I stopped in my tracks. The "net user" command does this really annoying thing where it puts the user names in columns, as follows:

C:\> net user

User accounts for \\MYCOMPUTER

-------------------------------------------------------------------------
Administrator cheetah Guest
jane tarzan

Sure, I could parse those columns with Yet Another FOR Loop (YAFL), but that would be annoying and tiresome. I decided to go for a cleaner way to get a list of users than "net user", solving the challenge as follows:

C:\> for /F "skip=1" %i in ('wmic useraccount get name') do @echo. & echo %i &
net user %i | find "*"

Administrator
Local Group Memberships *Administrators
Global Group memberships *None

cheetah
Local Group Memberships *Users
Global Group memberships *None

Guest
Local Group Memberships *Guests
Global Group memberships *None

jane
Local Group Memberships *Administrators *Backup Operators
Global Group memberships *None

tarzan
Local Group Memberships *Users *HelpServicesGroup
Global Group memberships *None
My command here is simply running "wmic useraccount get name", which is a clean way to get a list of account names from the local box. I use a FOR /F loop to iterate over the output of this command, skipping the first line (which contains the "Name" column header from the wmic output). At each iteration through the loop, I skip a line (echo.) to make our output prettier. Then, I display the username (echo %i) and run the "net user [username]" command to get all of the details associated with that account. Now, I pipe the output of the "net user [username]" command through the find command to locate lines that have a * in them. Yes, that annoying little * that I was complaining about in Episode #43. But, here, I'm using it as a selector to grab the group names. If Windows annoyingly puts *'s in front of group names, darnit, I'm gonna use them to my advantage. No sense trying to pee against the wind.... er... Windows, that is.

Sure, sure, we could parse this output further to remove out the text that says "Local Group Memberships" and "Global Group memberships" (btw, didja note the inconsistency in the capitalization of Membership and membership? Gee, thanks, Microsoft). If I really needed to, I'd parse that stuff out using another FOR /F loop with a delimiter of *. But, that would make the command unnecessarily complicated and ugly, for it already has the information we want, in a relatively useful form.

I also like this solution, because it shows a useful mixture of "wmic useraccount" with "net user". It's not every day that you get to use the two of them together in a convenient fashion like this. So, I'm happy, and that's really what command line kung fu is all about... making people happy.