~nabijaczleweli/voreutils

3d8728f6b8a7c5e1600690338a2992bf6cd85bf1 — наб 8 days ago 0d1dcab
Add who
9 files changed, 852 insertions(+), 36 deletions(-)

M README.md
M cmd/pinky.cpp
A cmd/who.cpp
M include/vore-column
M include/vore-file
M include/vore-utmpx
M man/pinky.1
M man/tsort.1
A man/who.1
M README.md => README.md +1 -1
@@ 107,7 107,7 @@ GNU coreutils provide the following 106 binaries, according to `dpkg -L coreutil
  * ☑ /usr/bin/unlink
  * ☑ /usr/bin/users
  * ☑ /usr/bin/wc
  * ☐ /usr/bin/who
  * ☑ /usr/bin/who – [#1016456: default isn't -s, -s doesn't force "only name, line, and time" output](//bugs.debian.org/1016456)
  * ☑ /usr/bin/whoami
  * ☑ /usr/bin/yes – we don't ignore `--`, mirroring POSIX echo
  * ☑ /usr/sbin/chroot

M cmd/pinky.cpp => cmd/pinky.cpp +3 -10
@@ 17,7 17,6 @@
#include <vore-pwdgrp>
#include <vore-systime>
#include <vore-utmpx>
#include <vore-visit>

using namespace std::literals;



@@ 131,7 130,7 @@ int main(int argc, const char * const * argv) {
		std::vector<vore::column::field<>> output_fields{output_lines[0].size(), {{}}};


		vore::file::fd<false> dev{"/dev", O_PATH | O_DIRECTORY | O_CLOEXEC};
		int dev = open("/dev", O_PATH | O_DIRECTORY | O_CLOEXEC);
		struct timespec now;
		if(short_idle)
			clock_gettime(CLOCK_REALTIME, &now);


@@ 188,14 187,8 @@ int main(int argc, const char * const * argv) {
			if(short_idle)
				line.emplace_back(std::move(idl));  // Idle

			char logintime[23 + 1];  // +2147483648-12-31 23:59 or 9223372036854775808
			std::size_t logintime_len;
			time_t sec = utx->ut_tv.tv_sec;  // glibc moment!
			if(struct tm tm; localtime_r(&sec, &tm))
				logintime_len = std::strftime(logintime, sizeof(logintime), "%F %R", &tm);
			else
				logintime_len = std::snprintf(logintime, sizeof(logintime), "%" PRId64 "", static_cast<std::int64_t>(sec));
			line.emplace_back(std::string{logintime, logintime_len});  // Since
			char logintime[23 + 1];
			line.emplace_back(std::string{logintime, vore::utmpx::entry_time(utx->ut_tv.tv_sec, logintime)});  // Since

			if(short_host)
				line.emplace_back(std::string{vore::utmpx::host(utx)});  // Host

A cmd/who.cpp => cmd/who.cpp +375 -0
@@ 0,0 1,375 @@
// SPDX-License-Identifier: 0BSD
// TODO: tests :) but that means either injecting LD_PRELOADed data or generating an utmp


#include <algorithm>
#include <clocale>
#include <ctime>
#include <initializer_list>
#include <inttypes.h>
#include <netdb.h>
#include <optional>
#include <sys/socket.h>
#include <vore-column>
#include <vore-dupa>
#include <vore-file>
#include <vore-getopt>
#include <vore-optarg>
#include <vore-print>
#include <vore-pwdgrp>
#include <vore-systime>
#include <vore-utmpx>
#include <vore-visit>

using namespace std::literals;

#ifndef NI_IDN
#define NI_IDN 0
#endif
#ifndef AI_IDN
#define AI_IDN 0
#endif
#ifndef AI_CANONIDN
#define AI_CANONIDN 0
#endif


#define USAGE                                       \
	"%1$s -q                                [utmp]\n" \
	"%1$s [-s] [-{T|w}] [-HamIL] [-ublrpdt] [utmp]\n" \
	"%1$s [-s] [-{T|w}] [-HaIL]  [-ublrpdt] am I\n"


static_assert(USER_PROCESS < 16);
static_assert(BOOT_TIME < 16);
static_assert(LOGIN_PROCESS < 16);
static_assert(RUN_LVL < 16);
static_assert(DEAD_PROCESS < 16);
static_assert(INIT_PROCESS < 16);
static_assert(OLD_TIME < 16);
static_assert(NEW_TIME < 16);
static const constexpr char types_s[]          = {'u', 'b', 'l', 'r', 'p', 'd', 't'};
static const constexpr std::uint16_t types_d[] = {
    1 << USER_PROCESS, 1 << BOOT_TIME, 1 << LOGIN_PROCESS, 1 << RUN_LVL, 1 << INIT_PROCESS, 1 << DEAD_PROCESS, (1 << OLD_TIME) | (1 << NEW_TIME)};
static const constexpr std::uint16_t synthetic_types = (1 << BOOT_TIME) | (1 << RUN_LVL) | (1 << OLD_TIME) | (1 << NEW_TIME);  // -brt
static constexpr bool type_in(short ut_type, std::uint16_t mask) {
	return ut_type >= 0 && ut_type < 16 && (mask & (1 << ut_type));
}


// Name   ''      Line  Time   Idle        PID    Comment    Exit
// <name>[<state>]<line><time>[<activity>][<pid>][<comment>][<exit>]
enum class field_t : std::uint8_t { name, ttywstatus, tty, time, idletime, pid, comment, exit };
static constexpr const char * const field_s[] = {"Name", "", "Line", "Time", "Idle", "PID", "Comment", "Exit"};
constexpr vore::column::alignment_t alignment(field_t f) {
	switch(f) {
		case field_t::pid:
			return vore::column::alignment_t::right;
		case field_t::idletime:
			return vore::column::alignment_t::middle;
		default:
			return vore::column::alignment_t::left;
	}
}

#define PRIpid "d"
static_assert(std::is_same_v<pid_t, int>);
#define PID_LEN 11  // -2147483648

int main(int argc, const char * const * argv) {
	std::setlocale(LC_ALL, "");  // TODO: locale!

	bool quiet{}, shortstyle{}, ttywstatus{}, header{}, ttyidle{}, fortty{}, ips{}, lookup{};
	std::uint16_t mask{};
	for(auto [arg, _] : vore::opt::get{argc,
	                                   argv,
	                                   "qsTwHamILublrpdt",
	                                   {{"count", no_argument, nullptr, 'q'},
	                                    {"short", no_argument, nullptr, 's'},
	                                    {"mesg", no_argument, nullptr, 'T'},
	                                    {"writable", no_argument, nullptr, 'T'},
	                                    {"message", no_argument, nullptr, 'T'},
	                                    {"heading", no_argument, nullptr, 'H'},
	                                    {"all", no_argument, nullptr, 'a'},
	                                    {"ips", no_argument, nullptr, 'I'},
	                                    {"lookup", no_argument, nullptr, 'L'},

	                                    {"users", no_argument, nullptr, 'u'},
	                                    {"boot", no_argument, nullptr, 'b'},
	                                    {"login", no_argument, nullptr, 'l'},
	                                    {"runlevel", no_argument, nullptr, 'r'},
	                                    {"process", no_argument, nullptr, 'p'},
	                                    {"dead", no_argument, nullptr, 'd'},
	                                    {"time", no_argument, nullptr, 't'}}})
		switch(arg) {
			case 'q':
				quiet = true;
				break;

			case 's':
				shortstyle = true;
				break;
			case 'T':
			case 'w':
				ttywstatus = true;
				break;

			case 'H':
				header = true;
				break;
			case 'a':  // -ublrpdt -T
				ttyidle = true;
				for(auto m : types_d)
					mask |= m;
				ttywstatus = true;
				break;
			case 'm':
				fortty = true;
				break;
			case 'I':
				ips = true;
				break;
			case 'L':
				lookup = true;
				break;

			case 'u':
				ttyidle = true;
				[[fallthrough]];
			case 'b':
			case 'l':
			case 'r':
			case 'p':
			case 'd':
			case 't':
				mask |= *(types_d + (std::find(std::begin(types_s), std::end(types_s), arg) - std::begin(types_s)));
				break;

			default:
				return std::fprintf(stderr, USAGE, argv[0]), 1;
		}
	if(*(argv + optind) && *(argv + optind + 1) && *(argv + optind + 2))
		return std::fprintf(stderr, USAGE, argv[0]), 1;
	else if(*(argv + optind) && *(argv + optind + 1))
		fortty = true;
	else if(*(argv + optind))
		if(utmpxname(*(argv + optind)) != UTMPXNAME_SUCCESS)
			return std::fprintf(stderr, "%s: %s: %s\n", argv[0], *(argv + optind), std::strerror(errno ?: EINVAL)), 1;

	if(!mask)
		mask = 1 << USER_PROCESS;


	if(quiet) {
		std::uint64_t users{};
		while(auto utx = getutxent()) {
			if(utx->ut_type != USER_PROCESS)
				continue;
			if(users++)
				vore::fputc(' ', stdout);
			auto user = vore::utmpx::user(utx);
			vore::fwrite(user.data(), 1, user.size(), stdout);
		}
		std::fprintf(stdout, "\n# users=%" PRIu64 "\n", users);
	} else {
		std::vector<vore::column::field<field_t>> fields{field_t::name};
		if(ttywstatus)  // -T
			fields.emplace_back(field_t::ttywstatus);
		fields.emplace_back(field_t::tty);
		fields.emplace_back(field_t::time);
		if(ttyidle || mask & ((1 << LOGIN_PROCESS) | (1 << RUN_LVL) | (1 << DEAD_PROCESS)))  // any of -ulr -d
			fields.emplace_back(field_t::idletime);
		if(mask != (1 << USER_PROCESS))
			fields.emplace_back(field_t::pid);
		if(!shortstyle || mask != (1 << USER_PROCESS))
			fields.emplace_back(field_t::comment);
		if(mask & (1 << DEAD_PROCESS))  // -d
			fields.emplace_back(field_t::exit);


		std::vector<std::vector<vore::column::line>> output_lines;
		if(header) {
			auto & hdr = output_lines.emplace_back();
			for(auto && [f, _] : fields)
				hdr.emplace_back(std::string_view{field_s[static_cast<std::uint8_t>(f)]});
		}


		auto filter = fortty ? std::string_view{ttyname(0) ?: ""} : ""sv;
		if(fortty) {
			if(filter.substr(0, 5) == "/dev/"sv)  // std::string_view::starts_with() is C++20
				filter = filter.substr(5);
			if(filter.empty() || filter.size() > sizeof((struct utmpx){}.ut_line))
				return vore::column::ate(fields, output_lines),
				       vore::flush_stdout(argv[0]);  // fast path: either not on a tty or the tty is unrepresentable => no output => no lines
		}

		int dev = -1;
		struct timespec now;
		if(mask & (1 << USER_PROCESS) &&
		   std::any_of(std::begin(fields), std::end(fields), [](auto && f) { return f.fld == field_t::ttywstatus || f.fld == field_t::idletime; }))
			dev = open("/dev", O_PATH | O_DIRECTORY | O_CLOEXEC);
		if(ttyidle || mask & ((1 << LOGIN_PROCESS) | (1 << RUN_LVL)))
			clock_gettime(CLOCK_REALTIME, &now);

		while(auto utx = getutxent()) {
			if(!type_in(utx->ut_type, mask))
				continue;

			auto ut_line = vore::utmpx::line(utx);
			if(ut_line.substr(0, filter.size()) != filter)  // std::string_view::starts_with() is C++20
				continue;

			std::optional<struct stat> sb;
			auto stat_line = [&] {
				if(sb || !fstatat(dev, MAYBE_DUPA(ut_line), &sb.emplace(), 0))
					return 0;
				else {
					sb = {};
					return -1;
				}
			};

			auto & l = output_lines.emplace_back();
			for(auto && [f, _] : fields)
				switch(f) {
					case field_t::name:
						if(utx->ut_type == DEAD_PROCESS || type_in(utx->ut_type, synthetic_types))
							l.emplace_back(""sv);
						else
							l.emplace_back(std::string{vore::utmpx::user(utx)});
						break;
					case field_t::ttywstatus: {
						auto wstat = " "sv;
						if(utx->ut_type == USER_PROCESS) {
							wstat = "?"sv;
							if(dev != -1 && !stat_line()) {
								if(sb->st_mode & 0020)
									wstat = "+"sv;
								else
									wstat = "-"sv;
							}
						}
						l.emplace_back(wstat);
					} break;
					case field_t::tty:
						switch(utx->ut_type) {
							case BOOT_TIME:
								l.emplace_back("system boot"sv);
								break;
							case RUN_LVL:
								if(utx->ut_pid & 0xFF)  // LSB is the new run-level as a character
									l.emplace_back(std::move((std::string{"run-level"} += ' ') += static_cast<char>(utx->ut_pid & 0xFF)));
								else
									l.emplace_back("run-level"sv);
								break;
							case OLD_TIME:
								l.emplace_back("date before"sv);
								break;
							case NEW_TIME:
								l.emplace_back("date after"sv);
								break;
							default:
								l.emplace_back(std::string{ut_line});
								break;
						}
						break;
					case field_t::time:
						if(utx->ut_tv.tv_sec == 0 && utx->ut_tv.tv_usec == 0)
							l.emplace_back(""sv);
						else {
							char utime[23 + 1];
							l.emplace_back(std::string{utime, vore::utmpx::entry_time(utx->ut_tv.tv_sec, utime)});
						}
						break;
					case field_t::idletime:
						if(utx->ut_type == USER_PROCESS) {
							if(!stat_line()) {
								auto idle = now - sb->st_atim;
								if(idle < (struct timespec){60, 0})
									l.emplace_back("."sv);
								else if(idle < (struct timespec){24 * 60 * 60, 0}) {
									char idl_s[5 + 1];  // HH:MM
									l.emplace_back(std::string{
									    idl_s, static_cast<std::size_t>(std::snprintf(idl_s, sizeof(idl_s), "%02d:%02d",  //
									                                                  static_cast<int>((idle.tv_sec / 60) / 60), static_cast<int>((idle.tv_sec / 60) % 60)))});
								} else
									l.emplace_back("old"sv);
							} else
								l.emplace_back("?"sv);
						} else
							l.emplace_back(""sv);
						break;
					case field_t::pid:
						if(type_in(utx->ut_type, synthetic_types))
							l.emplace_back(""sv);
						else {
							char pid_s[PID_LEN + 1];
							l.emplace_back(std::string{pid_s, static_cast<std::size_t>(std::sprintf(pid_s, "%" PRIpid "", utx->ut_pid))});
						}
						break;
					case field_t::comment:
						switch(utx->ut_type) {
							case BOOT_TIME:
								l.emplace_back(std::string{vore::utmpx::host(utx)});  // on Debian (some combination of glibc/systemd/&c.) this is uname -s; no harm elsewhere
								break;
							case RUN_LVL:
							case OLD_TIME:
							case NEW_TIME:
								l.emplace_back(""sv);
								break;
							case USER_PROCESS: {
								auto host = vore::utmpx::host(utx);

								//               :                                    "(ut_host)"
								// --ips         : getnameinfo(ut_addr, NUMERIC)   -> "result"
								// --ips --lookup: getnameinfo(ut_addr, 0)         -> "result"
								//       --lookup: getaddrinfo(ut_host, CANONNAME) -> "(result->canon)"
								//                 (all errors just fall down to the default output)
								//                 (IN[6]ADDR_ANY with --ips also means default output)
								if(ips) {
									if(struct sockaddr_storage addr{}; vore::utmpx::addr(utx, &addr))
										if(char h[NI_MAXHOST];
										   !getnameinfo(reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr), h, sizeof(h), nullptr, 0, lookup ? NI_IDN : NI_NUMERICHOST)) {
											l.emplace_back(std::string{h});
											break;
										}
								} else if(lookup) {  // && !ips
									struct addrinfo hints {
										.ai_flags = AI_CANONNAME | AI_IDN | AI_CANONIDN
									};
									if(struct addrinfo * ai; !getaddrinfo(MAYBE_DUPA(host), nullptr, &hints, &ai)) {
										l.emplace_back(std::move((std::string{'('} += ai->ai_canonname) += ')'));
										freeaddrinfo(ai);
										break;
									}
								}

								if(!host.empty())
									l.emplace_back(std::move((std::string{'('} += host) += ')'));
								else
									l.emplace_back(""sv);
								break;
							}
							default: {
								auto id = vore::utmpx::id(utx);
								if(id.empty())
									l.emplace_back(""sv);
								else
									l.emplace_back(std::move(std::string{"id="} += id));
							} break;
						}
						break;
					case field_t::exit:
						if(utx->ut_type == DEAD_PROCESS) {
							char exit_s[23 + 1];  // term=-32768 exit=-32768
							l.emplace_back(
							    std::string{exit_s, static_cast<std::size_t>(std::sprintf(exit_s, "term=%hd exit=%hd", utx->ut_exit.e_termination, utx->ut_exit.e_exit))});
						}
						break;
				}
		}
		vore::column::ate(fields, output_lines);
	}
	return vore::flush_stdout(argv[0]);
}

M include/vore-column => include/vore-column +20 -12
@@ 30,7 30,7 @@ namespace vore::column {


		struct default_field_t {};
		enum class alignment_t : bool { left, right };
		enum class alignment_t : std::uint8_t { left, middle, right };
		constexpr alignment_t alignment(default_field_t) {
			return alignment_t::left;
		}


@@ 46,30 46,38 @@ namespace vore::column {

		template <class T>
		void ate(std::vector<field<T>> & fields, std::vector<std::vector<line>> & lines) {
			bool needlastwidth = alignment(fields.back().fld) == alignment_t::right;
			for(auto & l : lines)
			for(auto & l : lines) {
				assert(l.size() <= fields.size());
				bool needlastwidth = alignment(fields[l.size() - 1].fld) != alignment_t::left;
				for(std::size_t i = 0; i < l.size() - !needlastwidth; ++i) {
					assert(l.size() == fields.size());

					l[i].width      = static_cast<std::uint16_t>(std::visit(vore::overload{[](auto && s) { return strwidth(s); }}, l[i].str));
					fields[i].width = std::max(l[i].width, fields[i].width);
				}
			}


			for(auto & l : lines) {
				std::size_t spacc{};
				for(std::size_t i = 0; i < l.size(); ++i) {
					if(i)
						vore::fputc(' ', stdout);

						++spacc;
					auto allpad = fields[i].width - l[i].width, curpad = 0;
					if(alignment(fields[i].fld) == alignment_t::right)
						for(auto pad = fields[i].width - l[i].width; pad; --pad)
						curpad = allpad;
					else if(alignment(fields[i].fld) == alignment_t::middle)  // prefer more spacing on the right
						curpad = allpad / 2;
					allpad -= curpad;
					spacc += curpad;

					if(!std::visit(vore::overload{[&](auto && s) { return s.empty(); }}, l[i].str)) {
						for(auto pad = spacc; pad; --pad)
							vore::fputc(' ', stdout);
						spacc = 0;

					std::visit(vore::overload{[&](auto && s) { return vore::fwrite(s.data(), 1, s.size(), stdout); }}, l[i].str);
						std::visit(vore::overload{[&](auto && s) { return vore::fwrite(s.data(), 1, s.size(), stdout); }}, l[i].str);
					}

					if(alignment(fields[i].fld) == alignment_t::left && i != fields.size() - 1)
						for(auto pad = fields[i].width - l[i].width; pad; --pad)
							vore::fputc(' ', stdout);
					spacc += allpad;
				}
				vore::fputc('\n', stdout);
			}

M include/vore-file => include/vore-file +3 -2
@@ 13,7 13,7 @@
#include <sys/types.h>
#include <unistd.h>

#if __OpenBSD__
#ifndef O_PATH
#define O_PATH 0
#endif



@@ 26,6 26,7 @@ namespace vore::file {
			friend class FILE;

		public:
			constexpr fd() noexcept = default;
			fd(const char * path, int flags, mode_t mode = 0, int from = AT_FDCWD) noexcept {
				if constexpr(allow_stdio)
					if(path[0] == '-' && !path[1]) {  // path == "-"sv but saves a strlen() call on libstdc++


@@ 75,7 76,7 @@ namespace vore::file {
		template <bool allow_stdio>
		class FILE {
		public:
			constexpr FILE() noexcept {}
			constexpr FILE() noexcept = default;

			FILE(const char * path, const char * opts) noexcept {
				if constexpr(allow_stdio)

M include/vore-utmpx => include/vore-utmpx +103 -9
@@ 7,32 7,74 @@
#include <utmp.h>

namespace {
#define USER_PROCESS 0
#define EMPTY 0          // POSIX
#define BOOT_TIME 1      // POSIX
#define OLD_TIME 2       // POSIX
#define NEW_TIME 3       // POSIX
#define USER_PROCESS 4   // POSIX
#define INIT_PROCESS 5   // POSIX
#define LOGIN_PROCESS 6  // POSIX
#define DEAD_PROCESS 7   // POSIX
#define RUN_LVL 8

	struct utmpx {
		short ut_type;
		char ut_user[sizeof((struct utmp){}.ut_name)];
		char ut_id[1];
		char ut_line[sizeof((struct utmp){}.ut_line)];
		char ut_host[sizeof((struct utmp){}.ut_host)];
		struct timeval ut_tv;
		// pid_t ut_pid;
		pid_t ut_pid;
		struct {
			short e_termination, e_exit;
		} ut_exit;
	};

	// https://man.openbsd.org/utmp.5
	static const char * vore_utmpx_file = _PATH_UTMP;
	struct utmpx * getutxent() {
		static struct utmpx ret;
		static int fd = open(_PATH_UTMP, O_RDONLY | O_CLOEXEC);
		static int fd = open(vore_utmpx_file, O_RDONLY | O_CLOEXEC);

		struct utmp ut {};
		while(!*ut.ut_name && !*ut.ut_line)
			if(read(fd, &ut, sizeof(ut)) != sizeof(ut))
				return nullptr;
		struct utmp ut;
		if(read(fd, &ut, sizeof(ut)) != sizeof(ut))
			return nullptr;

		ret.ut_type = USER_PROCESS;
		std::memcpy(ret.ut_user, ut.ut_name, sizeof(ut.ut_name));
		std::memcpy(ret.ut_line, ut.ut_line, sizeof(ut.ut_line));
		std::memcpy(ret.ut_host, ut.ut_host, sizeof(ut.ut_host));
		ret.ut_tv.tv_sec = ut.ut_time;
		ret.ut_pid       = 0;

		if(!*ret.ut_name && !*ret.ut_line)
			ret.ut_type = EMPTY;
		else if(ret.ut_line[1] == 0)
			switch(ret.ut_line[0]) {
				case 'r':  // "reboot"
					ret.ut_type = BOOT_TIME;
					break;
				case 's':  // "shutdown"
					ret.ut_type = RUN_LVL;
					ret.ut_pid  = '0';
					break;
				case '|':  // date
					ret.ut_type = OLD_TIME;
					break;
				case '{':  // date
					ret.ut_type = NEW_TIME;
					break;
			}

		return &ret;
	};

#define UTMPXNAME_SUCCESS 1
	template <class = void>
	int utmpxname(const char * file) {
		vore_utmpx_file = file;
		return UTMPXNAME_SUCCESS;
	}
}
#else
#include <utmpx.h>


@@ 54,20 96,72 @@ namespace {
#endif


#include <cstdio>
#include <cstring>
#include <ctime>
#include <netinet/in.h>
#include <string_view>
#include <sys/socket.h>

namespace vore::utmpx {
	namespace {
#define VIEWER(field)                                                         \
	template <class = void>                                                     \
	std::string_view field(struct utmpx * ut) {                                 \
		return {ut->ut_##field, strnlen(ut->ut_##field, sizeof(ut->ut_##field))}; \
	}

		VIEWER(user)
		VIEWER(id)
		VIEWER(line)
		VIEWER(host)

#undef VIEWER

		namespace detail {
			template <class T, class = std::void_t<decltype(std::declval<T>().ut_addr_v6)>>
			bool addr(T * utx, struct sockaddr_storage * out, int) {  // glibc
				if(!(utx->ut_addr_v6[0] || utx->ut_addr_v6[1] || utx->ut_addr_v6[2] || utx->ut_addr_v6[3]))
					return false;

				if(utx->ut_addr_v6[1] || utx->ut_addr_v6[2] || utx->ut_addr_v6[3]) {
					out->ss_family = AF_INET6;
					std::memcpy(reinterpret_cast<struct sockaddr_in6 *>(out)->sin6_addr.s6_addr, utx->ut_addr_v6, sizeof((struct in6_addr){}.s6_addr));
				} else {
					out->ss_family                                               = AF_INET;
					reinterpret_cast<struct sockaddr_in *>(out)->sin_addr.s_addr = utx->ut_addr_v6[0];
				}
				return true;
			}

			template <class T, class = std::void_t<decltype(std::declval<T>().ut_ss)>>
			bool addr(T * utx, struct sockaddr_storage * out, short) {  // NetBSD
				*out = utx->ut_ss;
				switch(out->ss_family) {
					case AF_INET6:
						return std::memcmp(&reinterpret_cast<struct sockaddr_in6 *>(out)->sin6_addr, &in6addr_any, sizeof(in6addr_any));
					case AF_INET:
						return reinterpret_cast<struct sockaddr_in *>(out)->sin_addr.s_addr != INADDR_ANY;
					default:
						return false;
				}
			}

			template <class T>
			bool addr(T *, struct sockaddr_storage *, ...) {  // fallback
				return false;
			}
		}
		template <class T>
		bool addr(T * utx, struct sockaddr_storage * out) {
			return detail::addr(utx, out, 0);
		}


		std::size_t entry_time(time_t sec, char (&out)[23 + 1]) {  // +2147483648-12-31 23:59 or 9223372036854775808
			// TODO: format
			if(struct tm tm; localtime_r(&sec, &tm))
				return std::strftime(out, sizeof(out), "%F %R", &tm);
			else
				return std::snprintf(out, sizeof(out), "%" PRId64 "", static_cast<std::int64_t>(sec));
		}
	}
}

M man/pinky.1 => man/pinky.1 +2 -1
@@ 20,6 20,7 @@
.
.Sh DESCRIPTION
By default, list information about logged-in users to the standard output stream:
.\" Matches who.1
.Bd -literal -compact -offset 4n
.if 1 Login  Name                   TTY   Idle  Since            Host
.ie t cicada Cicadetta cantilatrix  pts/0       2022-07-28 13:05 10.0.2.2


@@ 46,7 47,7 @@ Plan:
.
.Ss Short Format
For each logged-in user session
.Pq Dv USER_PROCESS Xr utmpx 5 No entries
.Pq Dv USER_PROCESS Xr utmpx 5 No entry
write a row consisting of:
.Bl -tag -compact -offset 4n -width ".No before Sy TTY"
.It Sy Login

M man/tsort.1 => man/tsort.1 +1 -1
@@ 152,7 152,7 @@ fully formed, as
pointing at
.Xr lorder 1 ,
which noted:
.Bd -filled -compact -offset Ds
.Bd -filled -compact -offset offset
This brash one-liner intends to build a new library
from existing `.o' files.
.D1 "ar cr library \`\|lorder *.o | tsort\`"

A man/who.1 => man/who.1 +344 -0
@@ 0,0 1,344 @@
.\" SPDX-License-Identifier: 0BSD
.\"
.Dd
.Dt WHO 1
.Os
.
.Sh NAME
.Nm who
.Nd list login and system state
.Sh SYNOPSIS
.Nm
.Fl q
.Op Ar utmp
.
.Nm
.Op Fl s
.Op Fl Ns Brq Cm T Ns \&| Ns Cm w
.Op Fl HamIL
.Op Fl ublrpdt
.Op Ar utmp
.
.Nm
.Op Fl s
.Op Fl Ns Brq Cm T Ns \&| Ns Cm w
.Op Fl Ha\ IL
.Op Fl ublrpdt
.Cm am I
.
.Sh DESCRIPTION
Processes and filters the default
.Xr utmpx 5
database, or the one specified in
.Ar utmp .
.Pp
With
.Fl q ,
list names of and count currently logged-in users to the standard output stream:
.Bd -literal -compact -offset 4n
.Ar cicada cicada root cicada
.Li # users= Ns Ar 4
.Ed
.Pp
.
Otherwise, filter and output system and teletype status:
.\" Matches pinky.1
.Bd -literal -compact -offset 4n
.if 1 \&Name     Line        Time             Idle      PID Comment Exit
.if 1 \&         system boot 2022-07-23 23:20               5.16.0-4-amd64
.if 1 \&root   - tty1        2022-07-28 14:59 00:10 3847410
.if 1 \&         run-level 5 2022-07-23 23:34
.if 1 \&LOGIN    tty6        2022-07-23 23:42         31294 id=tty6
.ie t \&cicada + pts/0       2022-07-28 13:05   .   2805135 (10.0.2.2)
.el   \&cicada + pts/0       2022-07-28 13:05   .   2805135 (fe80::20a:f7ff:fe63:d512%bridge1)
.if 1 \&         pts/1                              1004883 id=x001 term=0 exit=1
.if 1 \&         pts/2       2022-07-27 18:43       3402614 id=ts/2 term=0 exit=0
.Ed
.
.Ss Quick Format
For each logged-in user session
.Pq Dv USER_PROCESS Xr utmpx 5 No entry
output the login name
.Pq Fa ut_user ,
and a space separator.
The second line lists the amount of users in the first.
.
.Ss Full Format
For each
.Xr utmpx 5
entry of an enabled type
.Pq just logged-in users by default, but see Sx OPTIONS , Full Format
write a row consisting of:
.Bl -tag -compact -offset 4n -width ".No before Sy Line"
.It Sy Name
empty for
.Fl brdt ,
otherwise the login name
.Pq Fa ut_user ;
.
.It before Sy Line
a space if not
.Dv USER_PROCESS ,
otherwise the write status
.Pq cf. Xr mesg 1 :
.Bl -tag -compact -offset 4n -width ".Sy +"
.It Sy +
if allowed
.Pq Li g+w ,
.It Sy -
if blocked, or
.It Sy ?\&
if
.Cm stat Ns Pq Pa /dev/ Ns Fa ut_line
failed;
.El
.
.It Sy line
The entry's teletype
.Pq Fa ut_line ,
except, for:
.Bl -tag -compact -offset 4n -width ".Dv BOOT_TIME"
.It Dv BOOT_TIME
"system boot"
.It Dv RUN_LVL
"run-level",
followed by a space and the lowest byte of
.Fa ut_pid
\(em the new run-level \(em
if nonzero
.It Dv OLD_TIME
"date before"
.It Dv NEW_TIME
"date after";
.El
.
.It Sy Time
time of entry
.Pq Fa ut_tv
in
.Qq Li "%F %R"
.Pq Ar YYYY Ns Sy - Ns Ar MM Ns Sy - Ns Ar DD\ HH Ns Sy :\& Ns Ar MM
format, or empty if epoch;
.
.It Sy Idle
empty if not
.Dv USER_PROCESS ,
otherwise the time since last successful read from the teletype:
.Bl -tag -compact -offset 4n -width ".Ar HH Ns Sy :\& Ns Ar MM"
.It Sy .\&
if under a minute,
.It Ar HH Ns Sy :\& Ns Ar MM
if under a day,
.It Sy old
otherwise, and
.It Sy ?\&
if the aforementioned
.Fn stat
failed;
.El
.
.It Sy PID
empty if
.Fl brt ,
otherwise the entry's process' PID
.Pq Fa ut_pid ;
.
.It Sy Comment
the inittab/entry ID as
.Qq Sy id= Ns Fa ut_id ,
if any, otherwise empty, except for:
.Bl -tag -compact -offset 4n -width ".Dv USER_PROCESS"
.It Dv BOOT_TIME
the
.Fa ut_host
field \(em on some systems this is the
.Nm uname Fl s
of the booted kernel,
.It Dv RUN_LVL  OLD_TIME , NEW_TIME
empty, and
.It Dv USER_PROCESS
.Qq Sy \&( Ns Fa ut_host Ns Sy )\&
by default and if there's no stored IP/the look-up or parse failed with:
.Bl -tag -compact -width ".Fl IL"
.It Fl I
the IP address of the calling remote,
.It Fl IL
the canonical DNS name of the IP address of the calling remote,
.It Cm \  Ns Fl L
the canonical DNS name of the calling remote's hostname
.Pq Fa ut_host ,
parenthesised;
.El
.El
.
.It Sy Exit
for
.Dv DEAD_PROCESS ,
the signal and exit code:
.Qq Sy term= Ns Fa ut_exit.e_termination Ns Sy " exit=" Ns Fa ut_exit.e_exit .
.El
.
The
.Fl b
&c. notation is equivalent to its corresponding entry type
.Pq in this case Dv BOOT_TIME
in the table above.
.
.Sh OPTIONS
.Cm am I
(or any two non-option arguments) is equivalent to
.Fl m .
.
.Ss Quick Format
.Bl -tag -compact -width ".Fl q , -count"
.It Fl q , -count
Override all other flags, output
.Sx Quick Format .
.El
.
.Ss Full Format
.Bl -tag -compact -width ".Fl r , -runlevel"
.It Fl s , -short
If the output filter is just
.Dv USER_PROCESS ,
hide the
.Sy Comment
column.
.
.It Fl T , w , -mesg , -writable , -message
Show the write status column
.Pq before Sy Line .
.
.It Fl H , -heading
Prepend column names as the first output line.
.
.It Fl a , -all
.Fl ublrpdt T
.
.It Fl m
Reject entries for which
.Fa ut_line
is not the teletype attached to the standard input stream.
(Naturally, this rejects all entries if the standard input stream is not a teletype.)
.
.It Fl I , -ips
Use the address of the
.Dv USER_PROCESS
entry as the
.Sy Comment
field or
.Fl -lookup
target (but see the full format description above).
.
.It Fl L , -lookup
Canonicalise the hostname for
.Dv USER_PROCESS
entries (likewise).
.El
.Pp
.
Unless any of the following are specified, the default is
.Dv USER_PROCESS
and
.Sy Name Line Time Comment :
.Bl -tag -compact -width ".Fl r , -runlevel"
.It Fl u , -users
List logged-in users
.Pq Dv USER_PROCESS
.Em and
show the
.Sy Idle
column.
.
.It Fl b , -boot
List system boots
.Pq Dv BOOT_TIME , Sy PID .
.
.It Fl l , -login
List awaiting
.Xr login 8
lines
.Pq Dv LOGIN_PROCESS , Sy Idle PID .
.
.It Fl r , -runlevel
List run-level changes
.Pq Dv RUN_LVL , Sy Idle PID .
.
.It Fl p , -process
List live processes spawned by
.Xr init 8
.Pq Dv INIT_PROCESS , Sy PID .
.
.It Fl d , -dead
List lines with no current process
.Pq Dv DEAD_PROCESS , Sy Idle PID Exit .
.
.It Fl t , -time
List clock change entries
.Pq Dv OLD_TIME , NEW_TIME , Sy PID .
.El
.
.Sh SEE ALSO
.Xr finger 1 ,
.Xr mesg 1 ,
.Xr pinky 1 ,
.Xr w 1 ,
.Xr write 1 ,
.Xr utmpx 5
.
.Sh STANDARDS
Conforms to
.St -p1003.1-2008
\(em the only non-XSI-marked invocation is
.D1 Nm Op Fl mTu
and the output is
.D1 in an implementation-defined format, subject only to the requirement of containing the information described above .
The XSI-marked bits are (largely) equivalent to this implementation's invocation and match
.At V ,
.\" TODO: which?
except for
.Fl -ips , -lookup ,
which are extensions, originating from the GNU system, and
.Fl IL
spellings, which are extensions.
.Pp
The only XSI
.Cm am I
spellings are such and
.Cm am i .
Some implementations allow any two arguments.
.\" TODO: all? some?
Some implementations also allow
.Ar utmp
after.
.Pp
.St -p1003.1-2008
specifies
.Fl s
as
.Bd -filled -compact -offset indent
List only the <name>, <line>, and <time> fields.
This is the default case.
.Ed
It isn't in this implementation and the GNU system.
This isn't an issue since the requirements, even on XSI output, beyond "it should be in these columns in this order",
.Qq Fl b Sy Line No should be Qq system boot
and the format for
.Sy Idle
are somewhere between not of implementable quality and laughable.
.Pp
.Nm
is only useful for human eyes, do
.Em not
attempt to parse the output.
.Pp
The IP address of the calling remote is
.Fa ut_addr_v6
under glibc,
.Fa ut_ss
under
.Nx ,
and
.Pf all- Sy 0
(unavailable)
elsewhere.