std/user
import std/user to use it. Source: src/std/user.iyi at v0.18.0.
import std/user::{User, Group} me = User.current # name, id, group_id, home, shell
User.find_by_name("root") # a User, or nil
User.find_by_id("0") # the same, by number
Group.find_by_name("wheel") # name, id, members
me.group_ids # ["1000", "4", "27", ...]The other library's System::User and System::Group, and Go's os/user: the same records and the same lookups, with Crystal's names for the fields. An id is a String, as both keep it, because that is what Windows' is (a SID) and what a lookup is handed back; on Linux and darwin it is the number written in decimal, "0" for root. A name or an id nobody has is nil, never a panic, because both arrive from outside
- a configuration file, a request - and a missing account is data.
full_name is the GECOS field up to its first comma: the field is "a comma separated list of personal data where the first item is the full name" by convention only, and Go and the other library both cut it there; "Ada Lovelace,Room 4,," is "Ada Lovelace". group_ids is the user's primary group first, then every group that lists the user as a member, each once and in the order the group file has them: what glibc's getgrouplist answers, and so Python's os.getgrouplist, and id -G.
On Linux the answer is the files, /etc/passwd and /etc/group, read and parsed here on every lookup, because no libc is linked to ask (prelude.iyi says why). The NSS modules nsswitch.conf may name after files - LDAP, sssd, systemd's dynamic users - are not consulted, and an account only they know is nil here though getent finds it. Go's pure-Go reader (os/user's lookup_unix.go, built without cgo) makes the same choice for the same reason, and its rules for a line are the rules here: blank space trimmed from both ends, a blank line or one starting # skipped, a name that is empty or starts with + or - (NIS compat entries, which glibc's files backend does not find by name either) skipped, a user line of fewer than seven fields or a group line of fewer than four skipped, and an id that is not a number skipped. The first line that matches wins, as glibc's does. Where Go and glibc disagree this follows glibc, which is what every other program on the machine reads through: an id is compared as a number, so "0010" in the file is uid 10 and is found by find_by_id("10") (Go compares the text), an id past uid_t's 32 bits or with a sign is not a number (Go's Atoi takes both), and a group file's membership is read from every group line (Go's group listing skips a line with a colon among its members). A file that cannot be read is a file with no accounts in it.
User.current is the real user id, getuid(2) - what Go's user.Current, Python's os.getuid and id -u answer - looked up as find_by_id would. A process whose uid the files do not have (a container started as --user 4000) is still running as somebody, so it answers what Go's pure-Go Current falls back to: the uid and the kernel's gid, $USER for the name and $HOME for the home, with no full name and no shell. Without $USER there is nothing to call that user, and that is a panic naming the uid, never a made-up name.
On darwin the answers are libSystem's getpwuid_r, getpwnam_r, getgrgid_r, getgrnam_r and getgrouplist, which are Directory Services' and not a file's, and getuid and getgid: Go asks the same calls there, cgo or not. A buffer the call finds too small (ERANGE) is doubled and asked again, up to a mebibyte.
On Windows only the current user is answered. Its name is advapi32's GetUserNameW, the account name alone (Go writes DOMAIN\name, which takes LookupAccountSidW to build); its id and group id are the SIDs of the process token's user and primary group, in S-1-5-21-... form, which is what Go answers; its home is %USERPROFILE%, which Windows sets at logon to the directory GetUserProfileDirectoryW answers - that call is userenv.dll's, which an iyi program does not link (the floor in bench/floor_base.sh lists the DLLs one may). The full name is netapi32's NetUserGetInfo and is "" here (Go answers the account name in its place when it cannot ask); Windows has no login shell, so that is "" too. Every other lookup - by name, by id, a group, a user's groups - is the account database netapi32 answers, and panics on Windows saying so: a program that needs another account there has to be told it cannot have one, not handed nil for an account that exists.
Groupstruct
A group: its name, its id, and the accounts listed as its members. A user whose primary group this is need not be listed, and usually is not: User#group_ids is the question that counts both.
pub struct Group
# The group whose id is *id*, or nil when there is none or *id* is not a
# group id; compared as a number, as `User.find_by_id` compares.
def self.find_by_id(id : String) : Group | ::Nil
# The group called *name*, or nil when there is none.
def self.find_by_name(name : String) : Group | ::Nil
# The group id: a decimal number on Linux and darwin.
def id : String
def initialize(name : String, id : String, members : Array(String))
# The names listed in the group, in the order they are listed.
def members : Array(String)
# The group's name: `"wheel"`.
def name : String
# Every group in *text*, written as `/etc/group` is
# (`name:password:gid:member,member`), in order, without the lines a
# lookup skips.
def self.parse(text : String) : Array(Group)
# One `/etc/group` line, or nil for a line the lookups skip: blank, a
# comment, fewer than four fields, a name empty or starting `+` or `-`,
# or a gid that is not a number. The members are the rest of the line
# split at commas, empty names dropped, as glibc's list parser drops
# them.
protected def self.parse_line(raw : String) : Group | ::Nil
endUserstruct
An account: what /etc/passwd, Directory Services or Windows hold for one user.
pub struct User
# The user this program runs as: the real user id's account. Panics
# only when the system has no account for that id *and* `$USER` does
# not name one.
def self.current : User
# The account whose user id is *id*, or nil when there is none or *id*
# is not a user id. On Linux and darwin an id is compared as the number
# it is: `"0"` and `"00"` are both root.
def self.find_by_id(id : String) : User | ::Nil
# The account called *name*, or nil when there is none.
def self.find_by_name(name : String) : User | ::Nil
# The GECOS field up to its first comma; `""` where it is empty.
def full_name : String
# The id of the user's primary group, in the same form.
def group_id : String
# The ids of the groups out of *groups* this user is in, its primary
# group first, each once: what `group_ids` answers on Linux, for a group
# file the caller read and handed to `Group.parse`.
def group_ids(groups : Array(Group)) : Array(String)
# The ids of the groups this user is in: its primary group first, then
# every other the system lists it in, each once.
def group_ids : Array(String)
# The home directory.
def home : String
# The user id: a decimal number on Linux and darwin, a SID on Windows.
def id : String
def initialize(name : String, id : String, group_id : String, full_name : String, home : String, shell : String)
# The login name: `"root"`, `"ada"`.
def name : String
# Every account in *text*, written as `/etc/passwd` is
# (`name:password:uid:gid:gecos:home:shell`), in the order it has them,
# and without the lines a lookup skips: what the Linux lookups read, for
# a file of the caller's - a container image's, a chroot's.
def self.parse(text : String) : Array(User)
# One `/etc/passwd` line, or nil for a line the lookups skip: blank, a
# comment, fewer than seven fields, a name empty or starting `+` or `-`,
# or a uid or gid that is not a number. The shell is the rest of the
# line, colons and all, as glibc reads it.
protected def self.parse_line(raw : String) : User | ::Nil
# The login shell; `""` where none is set, and on Windows.
def shell : String
end
Users and groups: who this program runs as, and the accounts and groups the machine knows by name or by number.