~nabijaczleweli/voreutils

55c2e292546356226ec24b8c1e8c8f90eed059dd — наб 25 days ago 814f0d0
Add tsort. Staticify all includes
M Makefile => Makefile +1 -1
@@ 118,7 118,7 @@ $(OBJDIR)aliases : cmd/aliases $(patsubst %.c,%,$(patsubst %.cpp,%,$(patsubst cm
$(OBJDIR)man/% : man/%
	@mkdir -p $(dir $@)
	$(SED) 's/^\.Dd/.Dd $(VOREUTILS_DATE)/' $< > $@
	! $(MANDOC) -I os="voreutils $(VOREUTILS_VERSION)" -Tlint $@ 2>&1 | grep -vE -e 'mandoc: outdated mandoc.db' -e 'STYLE: referenced manual not found' -e 'WARNING: cross reference to self'
	! $(MANDOC) -I os="voreutils $(VOREUTILS_VERSION)" -Tlint $@ 2>&1 | grep -vE -e 'mandoc: outdated mandoc.db' -e 'STYLE: referenced manual not found' -e 'WARNING: cross reference to self' -e 'WARNING: undefined string, using "": doc-'

# https://twitter.com/nabijaczleweli/status/1411351513193238530
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=901636#52

M README.md => README.md +1 -1
@@ 97,7 97,7 @@ GNU coreutils provide the following 105 binaries, according to `dpkg -L coreutil
  [x] /usr/bin/timeout
  [x] /usr/bin/tr – implements -C as -c and `[=e=]` as `e`: this matches 4.4BSD and GNU tr, but is nevertheless a missing POSIX feature; could also stand to do buffering lower than fgetc/fputc, as I imagine the overhead of calling those for each byte is noninsignificant
  [x] /usr/bin/truncate
  [ ] /usr/bin/tsort
  [x] /usr/bin/tsort – GNU tsort is [turbofucked](https://bugs.debian.org/990854), and returns 1 for loops
  [x] /usr/bin/tty
  [x] /usr/bin/unexpand – flag handling is mildly (very?) different, and matches NetBSD; the tests are (should be) representative and all pass, but i've had [the worst time of my life](https://twitter.com/nabijaczleweli/status/1403145708543295493) writing it, so it's entirely possible (if unlikely) it's not 100% bug-compatible
  [ ] /usr/bin/uniq

M cmd/sha1sum.cpp => cmd/sha1sum.cpp +48 -2
@@ 29,6 29,52 @@ using namespace std::literals;
#endif


static std::uint8_t hex_nibble(char b) {
	switch(b) {
		case '0':
			return 0x0;
		case '1':
			return 0x1;
		case '2':
			return 0x2;
		case '3':
			return 0x3;
		case '4':
			return 0x4;
		case '5':
			return 0x5;
		case '6':
			return 0x6;
		case '7':
			return 0x7;
		case '8':
			return 0x8;
		case '9':
			return 0x9;
		case 'A':
		case 'a':
			return 0xA;
		case 'B':
		case 'b':
			return 0xB;
		case 'C':
		case 'c':
			return 0xC;
		case 'D':
		case 'd':
			return 0xD;
		case 'E':
		case 'e':
			return 0xE;
		case 'F':
		case 'f':
			return 0xF;
		default:
			__builtin_unreachable();
	}
}


#define USAGE(self, l)                                                                   \
	"usage: %s    [-ztb]%s [--tag] [file]...\n"                                            \
	"       %s -c %s [-w] [--strict] [--quiet] [--status] [--ignore-missing] [file]...\n", \


@@ 357,7 403,7 @@ int main(int argc, char * const * argv) {
							std::uint8_t line_hash[hash_alg_maxbitlength / 8];
							std::uint8_t * line_hash_cur = line_hash;
							for(auto cur = ln.data(); cur != ln.data() + spc_idx; cur += 2)
								*line_hash_cur++ = vore::hex_nibble(*cur) << 4 | vore::hex_nibble(*(cur + 1));
								*line_hash_cur++ = hex_nibble(*cur) << 4 | hex_nibble(*(cur + 1));

							if(!std::memcmp(hashed->data(), line_hash, hash_len / 8))
								status = print_ok ? "OK" : nullptr;


@@ 368,7 414,7 @@ int main(int argc, char * const * argv) {
					} else {
						status = "FAILED (I/O)";
						++badfiles;
						std::fprintf(stderr, "%s: %s: %s: %s\n", argv[0], file.data(), &ln[spc_idx + 2], std::strerror(errno ?: EINPROGRESS)); // generic
						std::fprintf(stderr, "%s: %s: %s: %s\n", argv[0], file.data(), &ln[spc_idx + 2], std::strerror(errno ?: EINPROGRESS));  // generic
					}
					if(status && status != "OK"sv) {
						valid_err = true;

A cmd/tsort.cpp => cmd/tsort.cpp +75 -0
@@ 0,0 1,75 @@
// SPDX-License-Identifier: 0BSD


#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <vore-file>
#include <vore-print>


static bool tree_cmp(const std::map<std::string, std::set<std::string>> & tree, const std::string * lhs, const std::string * rhs) {
	auto nd = tree.find(*lhs);
	if(nd == std::end(tree))
		return false;

	return (nd->second.find(*rhs) != std::end(nd->second)) ||
	       std::any_of(std::begin(nd->second), std::end(nd->second), [&](auto && ch) { return tree_cmp(tree, &ch, rhs); });
}


int main(int, const char * const * argv) {
	if(argv[0] && argv[1] && argv[2]) {
		std::fprintf(stderr, "usage: %s [file]\n", argv[0]);
		return 1;
	}

	auto file = argv[0] && argv[1] ? argv[1] : "-";
	vore::file::FILE<true> input{file, "re"};
	if(!input) {
		std::fprintf(stderr, "%s: %s: %s\n", argv[0], file, std::strerror(errno));
		return 1;
	}


	std::map<std::string, std::set<std::string>> tree;
	std::vector<const std::string *> out;
	auto ins_out = [&](auto && el) {
		if(auto itr = std::find_if(std::begin(out), std::end(out), [&](auto o) { return *o == el; }); itr != std::end(out))
			out.erase(itr);
		out.emplace_back(&el);
	};

	std::string savtok;
	char * line{};
	std::size_t linecap{};
	for(ssize_t len; (len = getline(&line, &linecap, input)) != -1;)
		for(char *saveptr, *tok = strtok_r(line, " \t\n", &saveptr); tok; tok = strtok_r(nullptr, " \t\n", &saveptr))
			if(savtok.empty())
				savtok = tok;
			else {
				auto [itr, _] = tree.emplace(std::move(savtok), std::set<std::string>{});
				ins_out(itr->first);
				std::string_view t{tok};
				if(itr->first != t) {
					auto [linkitr, _] = itr->second.emplace(t);
					if(tree_cmp(tree, &itr->first, &itr->first)) {
						std::fprintf(stderr, "%s: %s: breaking %s -> %s link\n", argv[0], file, itr->first.c_str(), tok);
						ins_out(tree.emplace(std::move(itr->second.extract(linkitr).value()), std::set<std::string>{}).first->first);
					} else
						ins_out(*linkitr);
				}
			}
	std::free(line);
	if(!savtok.empty()) {
		std::fprintf(stderr, "%s: %s: odd token count\n", argv[0], file);
		return 1;
	}


	std::stable_sort(std::begin(out), std::end(out), [&](auto lhs, auto rhs) { return tree_cmp(tree, lhs, rhs); });

	return vore::print_spaced(argv[0], out, false, "\n", '\n');
}

M include/vore-chrono => include/vore-chrono +44 -42
@@ 12,51 12,53 @@
#include <string_view>


namespace vore {
	std::optional<double> parse_duration(const char * self, const char * val) {
		std::string_view delay = val;

		double mult = 1;
		if(!delay.empty() && !std::isdigit(delay.back())) {
			switch(delay.back()) {
				case 'y':
					mult *= 365.25 / 7.;
					[[fallthrough]];
				case 'w':
					mult *= 7;
					[[fallthrough]];
				case 'd':
					mult *= 24;
					[[fallthrough]];
				case 'h':
					mult *= 60;
					[[fallthrough]];
				case 'm':
					mult *= 60;
					[[fallthrough]];
				case 's':
					break;
				default:
					std::fprintf(stderr, "%s: %s: unrecognised suffix %c; must be one of: smhdwy\n", self, val, delay.back());
					return {};
namespace {
	namespace vore {
		std::optional<double> parse_duration(const char * self, const char * val) {
			std::string_view delay = val;

			double mult = 1;
			if(!delay.empty() && !std::isdigit(delay.back())) {
				switch(delay.back()) {
					case 'y':
						mult *= 365.25 / 7.;
						[[fallthrough]];
					case 'w':
						mult *= 7;
						[[fallthrough]];
					case 'd':
						mult *= 24;
						[[fallthrough]];
					case 'h':
						mult *= 60;
						[[fallthrough]];
					case 'm':
						mult *= 60;
						[[fallthrough]];
					case 's':
						break;
					default:
						std::fprintf(stderr, "%s: %s: unrecognised suffix %c; must be one of: smhdwy\n", self, val, delay.back());
						return {};
				}

				delay = delay.substr(0, delay.size() - 1);
			}

			delay = delay.substr(0, delay.size() - 1);
		}
			// libstdc++ in Buster doesn't have std::from_chars for floating-point types! Very cool!
			std::string delay_s{delay};
			char * delay_se{};
			double base = std::strtod(delay_s.c_str(), &delay_se);
			if(base == 0 && delay_se == delay_s.c_str()) {
				std::fprintf(stderr, "%s: %s: invalid delay\n", self, delay_s.c_str());
				return {};
			}
			if(std::isnan(base)) {
				std::fprintf(stderr, "%s: %s: invalid delay: not a number\n", self, delay_s.c_str());
				return {};
			}

		// libstdc++ in Buster doesn't have std::from_chars for floating-point types! Very cool!
		std::string delay_s{delay};
		char * delay_se{};
		double base = std::strtod(delay_s.c_str(), &delay_se);
		if(base == 0 && delay_se == delay_s.c_str()) {
			std::fprintf(stderr, "%s: %s: invalid delay\n", self, delay_s.c_str());
			return {};
		}
		if(std::isnan(base)) {
			std::fprintf(stderr, "%s: %s: invalid delay: not a number\n", self, delay_s.c_str());
			return {};
			return base * mult;
		}

		return base * mult;
	}
}

M include/vore-file => include/vore-file +115 -113
@@ 14,151 14,153 @@
#include <vector>


namespace vore::file {
	template <bool allow_stdio>
	class fd {
	public:
		fd(const char * path, int flags, mode_t mode = 0) noexcept {
			using namespace std::literals;

			if(allow_stdio && path == "-"sv) {
				switch(flags & (O_RDONLY | O_WRONLY)) {
					case O_RDONLY:
						this->desc = 0;
						return;
					case O_WRONLY:
						this->desc = 1;
						return;
					default:
						errno = EINVAL;
						return;
namespace {
	namespace vore::file {
		template <bool allow_stdio>
		class fd {
		public:
			fd(const char * path, int flags, mode_t mode = 0) noexcept {
				using namespace std::literals;

				if(allow_stdio && path == "-"sv) {
					switch(flags & (O_RDONLY | O_WRONLY)) {
						case O_RDONLY:
							this->desc = 0;
							return;
						case O_WRONLY:
							this->desc = 1;
							return;
						default:
							errno = EINVAL;
							return;
					}
				}
			}

			while((this->desc = open(path, flags, mode)) == -1 && errno == EINTR)
				;
			this->opened = this->desc != -1;
		}
				while((this->desc = open(path, flags, mode)) == -1 && errno == EINTR)
					;
				this->opened = this->desc != -1;
			}

		fd(const fd &) = delete;
		constexpr fd(fd && oth) noexcept : desc(oth.desc), opened(oth.opened) { oth.opened = false; }
			fd(const fd &) = delete;
			constexpr fd(fd && oth) noexcept : desc(oth.desc), opened(oth.opened) { oth.opened = false; }

		~fd() {
			if(this->opened)
				close(this->desc);
		}
			~fd() {
				if(this->opened)
					close(this->desc);
			}

		constexpr operator int() const noexcept { return this->desc; }
			constexpr operator int() const noexcept { return this->desc; }

	private:
		int desc    = -1;
		bool opened = false;
	};
		private:
			int desc    = -1;
			bool opened = false;
		};

	template <bool allow_stdio>
	class FILE {
	public:
		FILE(const char * path, const char * opts) noexcept {
			using namespace std::literals;
		template <bool allow_stdio>
		class FILE {
		public:
			FILE(const char * path, const char * opts) noexcept {
				using namespace std::literals;

			if(allow_stdio && path == "-"sv) {
				if(opts[0] && opts[1] == '+') {
					errno = EINVAL;
					return;
				}
				switch(opts[0]) {
					case 'r':
						this->stream = stdin;
						return;
					case 'w':
					case 'a':
						this->stream = stdout;
						return;
					default:
				if(allow_stdio && path == "-"sv) {
					if(opts[0] && opts[1] == '+') {
						errno = EINVAL;
						return;
					}
					switch(opts[0]) {
						case 'r':
							this->stream = stdin;
							return;
						case 'w':
						case 'a':
							this->stream = stdout;
							return;
						default:
							errno = EINVAL;
							return;
					}
				}
			}

			this->stream = fopen(path, opts);
			this->opened = this->stream;
		}
				this->stream = fopen(path, opts);
				this->opened = this->stream;
			}

		FILE(const FILE &) = delete;
		constexpr FILE(FILE && oth) noexcept : stream(oth.stream), opened(oth.opened) { oth.opened = false; }
			FILE(const FILE &) = delete;
			constexpr FILE(FILE && oth) noexcept : stream(oth.stream), opened(oth.opened) { oth.opened = false; }

		~FILE() {
			if(this->opened)
				fclose(this->stream);
		}
			~FILE() {
				if(this->opened)
					fclose(this->stream);
			}

		constexpr operator ::FILE *() const noexcept { return this->stream; }
			constexpr operator ::FILE *() const noexcept { return this->stream; }

	private:
		::FILE * stream = nullptr;
		bool opened     = false;
	};
		private:
			::FILE * stream = nullptr;
			bool opened     = false;
		};


	template <class C = char>
	class mapping {
	public:
		constexpr mapping() noexcept {}
		template <class C = char>
		class mapping {
		public:
			constexpr mapping() noexcept {}

		mapping(void * addr, size_t length, int prot, int flags, int fd, off_t offset) noexcept {
			void * ret = mmap(addr, length, prot, flags, fd, offset);
			if(ret != MAP_FAILED) {
				map    = {static_cast<C *>(ret), length};
				opened = true;
			mapping(void * addr, size_t length, int prot, int flags, int fd, off_t offset) noexcept {
				void * ret = mmap(addr, length, prot, flags, fd, offset);
				if(ret != MAP_FAILED) {
					map    = {static_cast<C *>(ret), length};
					opened = true;
				}
			}
		}

		mapping(const mapping &) = delete;
		constexpr mapping(mapping && oth) noexcept : map(oth.map), opened(oth.opened) { oth.opened = false; }
			mapping(const mapping &) = delete;
			constexpr mapping(mapping && oth) noexcept : map(oth.map), opened(oth.opened) { oth.opened = false; }

		constexpr mapping & operator=(mapping && oth) noexcept {
			this->map    = oth.map;
			this->opened = oth.opened;
			oth.opened   = false;
			return *this;
		}
			constexpr mapping & operator=(mapping && oth) noexcept {
				this->map    = oth.map;
				this->opened = oth.opened;
				oth.opened   = false;
				return *this;
			}

		~mapping() {
			if(opened)
				munmap(const_cast<C *>(this->map.data()), this->map.size());
		}
			~mapping() {
				if(opened)
					munmap(const_cast<C *>(this->map.data()), this->map.size());
			}

		constexpr operator bool() const noexcept { return !this->map.empty(); }
		constexpr operator std::basic_string_view<C>() const noexcept { return this->map; }
		constexpr const std::basic_string_view<C> & operator*() const noexcept { return this->map; }
		constexpr const std::basic_string_view<C> * operator->() const noexcept { return &this->map; }
			constexpr operator bool() const noexcept { return !this->map.empty(); }
			constexpr operator std::basic_string_view<C>() const noexcept { return this->map; }
			constexpr const std::basic_string_view<C> & operator*() const noexcept { return this->map; }
			constexpr const std::basic_string_view<C> * operator->() const noexcept { return &this->map; }


	private:
		std::basic_string_view<C> map = {};
		bool opened                   = false;
	};
		private:
			std::basic_string_view<C> map = {};
			bool opened                   = false;
		};


	template <class B>
	int slurp(std::vector<B> & obuf, int fd) {
		static_assert(sizeof(B) == 1);
		template <class B>
		int slurp(std::vector<B> & obuf, int fd) {
			static_assert(sizeof(B) == 1);

			errno = 0;
			for(B buf[4096]; ssize_t ret = read(fd, buf, sizeof(buf));) {
				if(ret == -1) {
					if(errno == EINTR)
						continue;
					else
						break;
				}

		errno = 0;
		for(B buf[4096]; ssize_t ret = read(fd, buf, sizeof(buf));) {
			if(ret == -1) {
				if(errno == EINTR)
					continue;
				else
				if(ret == 0)
					break;
			}

			if(ret == 0)
				break;
				obuf.insert(std::end(obuf), buf, buf + ret);
			}

			obuf.insert(std::end(obuf), buf, buf + ret);
			return errno;
		}

		return errno;
	}
}

M include/vore-getopt => include/vore-getopt +40 -38
@@ 8,58 8,60 @@
#include <initializer_list>


namespace vore::opt {
	template <std::size_t N>
	struct get_iter;
namespace {
	namespace vore::opt {
		template <std::size_t N>
		struct get_iter;

	struct get_opt {
		int ret       = -1;
		char * optarg = nullptr;
	};
		struct get_opt {
			int ret       = -1;
			char * optarg = nullptr;
		};


	// TODO: this may want to set/reset optind?
	template <std::size_t N>
	struct get {
		using iterator = get_iter<N>;
		// TODO: this may want to set/reset optind?
		template <std::size_t N>
		struct get {
			using iterator = get_iter<N>;


		int argc;
		char * const * argv;
			int argc;
			char * const * argv;

		const char * optstring;
		struct ::option options[N + 1];
			const char * optstring;
			struct ::option options[N + 1];


		constexpr iterator begin() const noexcept { return ++iterator{this}; }
		constexpr iterator end() const noexcept { return {}; }
	};
			constexpr iterator begin() const noexcept { return ++iterator{this}; }
			constexpr iterator end() const noexcept { return {}; }
		};

	template <std::size_t N>
	get(int, const char * const *, const char *, const struct ::option (&)[N]) -> get<N>;
		template <std::size_t N>
		get(int, const char * const *, const char *, const struct ::option (&)[N]) -> get<N>;


	template <std::size_t N>
	struct get_iter {
		const get<N> * opts;
		get_opt last = {};
		template <std::size_t N>
		struct get_iter {
			const get<N> * opts;
			get_opt last = {};


		get_iter & operator++() noexcept {
			this->last.ret    = getopt_long(opts->argc, opts->argv, opts->optstring, opts->options, nullptr);
			this->last.optarg = optarg;
			return *this;
		}
			get_iter & operator++() noexcept {
				this->last.ret    = getopt_long(opts->argc, opts->argv, opts->optstring, opts->options, nullptr);
				this->last.optarg = optarg;
				return *this;
			}

		get_iter operator++(int) noexcept {
			const auto ret = *this;
			++(*this);
			return ret;
		}
			get_iter operator++(int) noexcept {
				const auto ret = *this;
				++(*this);
				return ret;
			}

		constexpr bool operator==(const get_iter & rhs) const noexcept { return this->last.ret == -1 && rhs.opts == nullptr; }
		constexpr bool operator!=(const get_iter & rhs) const noexcept { return !(*this == rhs); }
			constexpr bool operator==(const get_iter & rhs) const noexcept { return this->last.ret == -1 && rhs.opts == nullptr; }
			constexpr bool operator!=(const get_iter & rhs) const noexcept { return !(*this == rhs); }

		constexpr const get_opt & operator*() const noexcept { return this->last; }
	};
			constexpr const get_opt & operator*() const noexcept { return this->last; }
		};
	}
}

M include/vore-numeric => include/vore-numeric +28 -71
@@ 8,77 8,34 @@
#include <limits>


namespace vore {
	template <class T>
	bool parse_uint(const char * val, T & out) {
		if(val[0] == '\0') {
			errno = EINVAL;
			return false;
		}
		if(val[0] == '-') {
			errno = ERANGE;
			return false;
		}

		char * end{};
		errno = 0;
		out   = static_cast<T>(std::strtoull(val, &end, 0));
		if(errno)
			return false;
		if(out > std::numeric_limits<T>::max()) {
			errno = ERANGE;
			return false;
		}
		if(*end != '\0') {
			errno = EINVAL;
			return false;
		}

		return true;
	}

	std::uint8_t hex_nibble(char b) {
		switch(b) {
			case '0':
				return 0x0;
			case '1':
				return 0x1;
			case '2':
				return 0x2;
			case '3':
				return 0x3;
			case '4':
				return 0x4;
			case '5':
				return 0x5;
			case '6':
				return 0x6;
			case '7':
				return 0x7;
			case '8':
				return 0x8;
			case '9':
				return 0x9;
			case 'A':
			case 'a':
				return 0xA;
			case 'B':
			case 'b':
				return 0xB;
			case 'C':
			case 'c':
				return 0xC;
			case 'D':
			case 'd':
				return 0xD;
			case 'E':
			case 'e':
				return 0xE;
			case 'F':
			case 'f':
				return 0xF;
			default:
				__builtin_unreachable();
namespace {
	namespace vore {
		template <class T>
		bool parse_uint(const char * val, T & out) {
			if(val[0] == '\0') {
				errno = EINVAL;
				return false;
			}
			if(val[0] == '-') {
				errno = ERANGE;
				return false;
			}

			char * end{};
			errno = 0;
			out   = static_cast<T>(std::strtoull(val, &end, 0));
			if(errno)
				return false;
			if(out > std::numeric_limits<T>::max()) {
				errno = ERANGE;
				return false;
			}
			if(*end != '\0') {
				errno = EINVAL;
				return false;
			}

			return true;
		}
	}
}

M include/vore-optarg => include/vore-optarg +33 -31
@@ 3,45 3,47 @@

#pragma once

#include <string_view> // rbegin/rend
#include <string_view>  // rbegin/rend


namespace vore {
	template <class T>
	struct backward {
		T & c;
namespace {
	namespace vore {
		template <class T>
		struct backward {
			T & c;

		constexpr auto begin() const noexcept { return std::rbegin(c); }
		constexpr auto end() const noexcept { return std::rend(c); }
	};
			constexpr auto begin() const noexcept { return std::rbegin(c); }
			constexpr auto end() const noexcept { return std::rend(c); }
		};

	template <class T>
	backward(T &) -> backward<T>;
	template <class T>
	backward(const T &) -> backward<const T>;
}
		template <class T>
		backward(T &) -> backward<T>;
		template <class T>
		backward(const T &) -> backward<const T>;
	}

namespace vore::opt {
	class args {
	public:
		using iterator = const char * const *;
	namespace vore::opt {
		class args {
		public:
			using iterator = const char * const *;

		constexpr args(const char * const * argv) noexcept : b(argv), e(argv) {
			while(*e)
				++e;
		}
			constexpr args(const char * const * argv) noexcept : b(argv), e(argv) {
				while(*e)
					++e;
			}

		constexpr args(const args &) noexcept = default;
		constexpr args(args &&) noexcept      = default;
			constexpr args(const args &) noexcept = default;
			constexpr args(args &&) noexcept      = default;

		constexpr iterator begin() const noexcept { return b; }
		constexpr iterator end() const noexcept { return e; }
			constexpr iterator begin() const noexcept { return b; }
			constexpr iterator end() const noexcept { return e; }

		constexpr auto rbegin() const noexcept { return std::reverse_iterator{e}; }
		constexpr auto rend() const noexcept { return std::reverse_iterator{b}; }
			constexpr auto rbegin() const noexcept { return std::reverse_iterator{e}; }
			constexpr auto rend() const noexcept { return std::reverse_iterator{b}; }

	private:
		const char * const * b;
		const char * const * e;
	};
		private:
			const char * const * b;
			const char * const * e;
		};
	}
}

M include/vore-path => include/vore-path +13 -11
@@ 6,17 6,19 @@
#include <string_view>


namespace vore {
	template <class CharT, class Traits>
	constexpr std::basic_string_view<CharT, Traits> basename(const std::basic_string_view<CharT, Traits> & str) noexcept {
		if(size_t idx = str.rfind('/'); idx != std::basic_string_view<CharT, Traits>::npos)
			return str.substr(idx + 1);
		else
			return str;
	}
namespace {
	namespace vore {
		template <class CharT, class Traits>
		constexpr std::basic_string_view<CharT, Traits> basename(const std::basic_string_view<CharT, Traits> & str) noexcept {
			if(size_t idx = str.rfind('/'); idx != std::basic_string_view<CharT, Traits>::npos)
				return str.substr(idx + 1);
			else
				return str;
		}

	template <class CharT>
	constexpr std::basic_string_view<CharT> basename(const CharT * str) {
		return basename(std::basic_string_view<CharT>{str});
		template <class CharT>
		constexpr std::basic_string_view<CharT> basename(const CharT * str) {
			return basename(std::basic_string_view<CharT>{str});
		}
	}
}

M include/vore-print => include/vore-print +40 -28
@@ 9,40 9,52 @@
#include <string_view>


namespace vore {
	int flush_stdout(const char * self) {
		if(std::fflush(stdout)) {
			std::fprintf(stderr, "%s: %s\n", self, std::strerror(errno));
			return 1;
		} else
			return 0;
	}
namespace {
	namespace vore {
		namespace detail {
			template <class T>
			static const char * c_str(const T * str) {
				return str ? str->data() : nullptr;
			}
			template <class = void>
			static const char * c_str(const char * str) {
				return str;
			}
		}

	template <class I>
	int print_spaced(const char * self, I begin, I end, bool always = false, std::string_view newline = "\n", char sep = ' ') {
		bool first = true;
		for(; begin != end; ++begin) {
			const char * str = *begin;
		int flush_stdout(const char * self) {
			if(std::fflush(stdout)) {
				std::fprintf(stderr, "%s: %s\n", self, std::strerror(errno));
				return 1;
			} else
				return 0;
		}

			if(!str)
				continue;
		template <class I>
		int print_spaced(const char * self, I begin, I end, bool always = false, std::string_view newline = "\n", char sep = ' ') {
			bool first = true;
			for(; begin != end; ++begin) {
				auto str = detail::c_str(*begin);
				if(!str)
					continue;

			if(first)
				first = false;
			else
				std::fputc(sep, stdout);
				if(first)
					first = false;
				else
					std::fputc(sep, stdout);

			std::fputs(str, stdout);
		}
				std::fputs(str, stdout);
			}

		if((!first || always) && !newline.empty())
			std::fwrite(newline.data(), 1, newline.size(), stdout);
			if((!first || always) && !newline.empty())
				std::fwrite(newline.data(), 1, newline.size(), stdout);

		return flush_stdout(self);
	}
			return flush_stdout(self);
		}

	template <class C>
	int print_spaced(const char * self, const C & cont, bool always = false, std::string_view newline = "\n", char sep = ' ') {
		return print_spaced(self, std::begin(cont), std::end(cont), always, newline, sep);
		template <class C>
		int print_spaced(const char * self, const C & cont, bool always = false, std::string_view newline = "\n", char sep = ' ') {
			return print_spaced(self, std::begin(cont), std::end(cont), always, newline, sep);
		}
	}
}

M include/vore-signal => include/vore-signal +72 -70
@@ 13,88 13,90 @@
#endif


namespace vore::signal {
namespace {
	namespace vore::signal {
#if __linux__
	std::optional<int> from_name(const char * name) {
		for(auto && [k, v] : std::initializer_list<std::pair<const char *, int>> {
			    {"Signal 0", 0},            // matches NetBSD (4.4BSD)
			        {"HUP", SIGHUP},        // rest taken from linux man-pages' signal(7)
			        {"INT", SIGINT},        //
			        {"QUIT", SIGQUIT},      //
			        {"ILL", SIGILL},        //
			        {"ABRT", SIGABRT},      //
			        {"FPE", SIGFPE},        //
			        {"KILL", SIGKILL},      //
			        {"SEGV", SIGSEGV},      //
			        {"PIPE", SIGPIPE},      //
			        {"ALRM", SIGALRM},      //
			        {"TERM", SIGTERM},      //
			        {"USR1", SIGUSR1},      //
			        {"USR2", SIGUSR2},      //
			        {"CHLD", SIGCHLD},      //
			        {"CONT", SIGCONT},      //
			        {"STOP", SIGSTOP},      //
			        {"TSTP", SIGTSTP},      //
			        {"TTIN", SIGTTIN},      //
			        {"TTOU", SIGTTOU},      //
			        {"BUS", SIGBUS},        //
			        {"POLL", SIGPOLL},      //
			        {"PROF", SIGPROF},      //
			        {"SYS", SIGSYS},        //
			        {"TRAP", SIGTRAP},      //
			        {"URG", SIGURG},        //
			        {"VTALRM", SIGVTALRM},  //
			        {"XCPU", SIGXCPU},      //
			        {"XFSZ", SIGXFSZ},      //
			        {"IOT", SIGIOT},        //
		std::optional<int> from_name(const char * name) {
			for(auto && [k, v] : std::initializer_list<std::pair<const char *, int>> {
				    {"Signal 0", 0},            // matches NetBSD (4.4BSD)
				        {"HUP", SIGHUP},        // rest taken from linux man-pages' signal(7)
				        {"INT", SIGINT},        //
				        {"QUIT", SIGQUIT},      //
				        {"ILL", SIGILL},        //
				        {"ABRT", SIGABRT},      //
				        {"FPE", SIGFPE},        //
				        {"KILL", SIGKILL},      //
				        {"SEGV", SIGSEGV},      //
				        {"PIPE", SIGPIPE},      //
				        {"ALRM", SIGALRM},      //
				        {"TERM", SIGTERM},      //
				        {"USR1", SIGUSR1},      //
				        {"USR2", SIGUSR2},      //
				        {"CHLD", SIGCHLD},      //
				        {"CONT", SIGCONT},      //
				        {"STOP", SIGSTOP},      //
				        {"TSTP", SIGTSTP},      //
				        {"TTIN", SIGTTIN},      //
				        {"TTOU", SIGTTOU},      //
				        {"BUS", SIGBUS},        //
				        {"POLL", SIGPOLL},      //
				        {"PROF", SIGPROF},      //
				        {"SYS", SIGSYS},        //
				        {"TRAP", SIGTRAP},      //
				        {"URG", SIGURG},        //
				        {"VTALRM", SIGVTALRM},  //
				        {"XCPU", SIGXCPU},      //
				        {"XFSZ", SIGXFSZ},      //
				        {"IOT", SIGIOT},        //
#if SIGEMT
			        {"EMT", SIGEMT},
				        {"EMT", SIGEMT},
#endif
			        {"STKFLT", SIGSTKFLT},  //
			        {"IO", SIGIO},          //
			        {"CLD", SIGCLD},        //
			        {"PWR", SIGPWR},        //
				        {"STKFLT", SIGSTKFLT},  //
				        {"IO", SIGIO},          //
				        {"CLD", SIGCLD},        //
				        {"PWR", SIGPWR},        //
#if SIGINFO
			        {"INFO", SIGINFO},
				        {"INFO", SIGINFO},
#endif
#if SIGLOST
			        {"LOST", SIGLOST},
				        {"LOST", SIGLOST},
#endif
			        {"WINCH", SIGWINCH},
				        {"WINCH", SIGWINCH},
#if SIGUNUSED
			        {"UNUSED", SIGUNUSED},
				        {"UNUSED", SIGUNUSED},
#endif
			        {"RTMIN", SIGRTMIN}, {"RTMAX", SIGRTMAX},
		    })
			if(!strcasecmp(name, k))
				return v;
				        {"RTMIN", SIGRTMIN}, {"RTMAX", SIGRTMAX},
			    })
				if(!strcasecmp(name, k))
					return v;

		for(auto && [rmplus, isadd] : std::initializer_list<std::pair<std::string_view, bool>>{{"RT", true},  // RT matches NetBSD, RT{MIN+,MAX-} matches procps
		                                                                                       {"RTMIN+", true},
		                                                                                       {"RTMAX-", false}})
			if(!strncasecmp(name, rmplus.data(), rmplus.size()) && name[rmplus.size()] >= '0' && name[rmplus.size()] <= '9') {
				unsigned rt{};
				const char * c = name + rmplus.size();
				for(; *c >= '0' && *c <= '9'; ++c) {
					rt *= 10;
					rt += *c - '0';
			for(auto && [rmplus, isadd] : std::initializer_list<std::pair<std::string_view, bool>>{{"RT", true},  // RT matches NetBSD, RT{MIN+,MAX-} matches procps
			                                                                                       {"RTMIN+", true},
			                                                                                       {"RTMAX-", false}})
				if(!strncasecmp(name, rmplus.data(), rmplus.size()) && name[rmplus.size()] >= '0' && name[rmplus.size()] <= '9') {
					unsigned rt{};
					const char * c = name + rmplus.size();
					for(; *c >= '0' && *c <= '9'; ++c) {
						rt *= 10;
						rt += *c - '0';
					}
					if(*c || rt > static_cast<unsigned>(SIGRTMAX - SIGRTMIN))
						return {};
					else if(isadd)
						return SIGRTMIN + rt;
					else
						return SIGRTMAX - rt;
				}
				if(*c || rt > static_cast<unsigned>(SIGRTMAX - SIGRTMIN))
					return {};
				else if(isadd)
					return SIGRTMIN + rt;
				else
					return SIGRTMAX - rt;
			}

		return {};
	}
			return {};
		}
#else
	std::optional<int> from_name(const char * name) {
		for(auto i = 0u; i < NSIG; ++i)
			if(!strcasecmp(name, sys_signame[i]))
				return i;
		return {};
	}
		std::optional<int> from_name(const char * name) {
			for(auto i = 0u; i < NSIG; ++i)
				if(!strcasecmp(name, sys_signame[i]))
					return i;
			return {};
		}
#endif
	}
}

M include/vore-span => include/vore-span +12 -10
@@ 4,18 4,20 @@
#pragma once


namespace vore {
	template <class T>
	struct span {
		using iterator = T *;
namespace {
	namespace vore {
		template <class T>
		struct span {
			using iterator = T *;

		iterator b, e;
			iterator b, e;

		constexpr iterator begin() const noexcept { return this->b; }
		constexpr iterator end() const noexcept { return this->e; }
	};
			constexpr iterator begin() const noexcept { return this->b; }
			constexpr iterator end() const noexcept { return this->e; }
		};


	template <class T>
	span(T *, T *) -> span<T>;
		template <class T>
		span(T *, T *) -> span<T>;
	}
}

M include/vore-token => include/vore-token +27 -25
@@ 8,40 8,42 @@
#include <string>


namespace vore {
	struct tokenise_iter {
		const char * delim;
		char * saveptr = nullptr;
		char * token   = nullptr;
namespace {
	namespace vore {
		struct tokenise_iter {
			const char * delim;
			char * saveptr = nullptr;
			char * token   = nullptr;


		tokenise_iter & operator++() noexcept {
			this->token = strsep(&saveptr, delim);
			return *this;
		}
			tokenise_iter & operator++() noexcept {
				this->token = strsep(&saveptr, delim);
				return *this;
			}

		tokenise_iter operator++(int) noexcept {
			const auto ret = *this;
			++(*this);
			return ret;
		}
			tokenise_iter operator++(int) noexcept {
				const auto ret = *this;
				++(*this);
				return ret;
			}

		constexpr bool operator==(const tokenise_iter & rhs) const noexcept { return this->token == rhs.token; }
		constexpr bool operator!=(const tokenise_iter & rhs) const noexcept { return !(*this == rhs); }
			constexpr bool operator==(const tokenise_iter & rhs) const noexcept { return this->token == rhs.token; }
			constexpr bool operator!=(const tokenise_iter & rhs) const noexcept { return !(*this == rhs); }

		constexpr char * operator*() const noexcept { return this->token; }
	};
			constexpr char * operator*() const noexcept { return this->token; }
		};


	struct tokenise {
		using iterator = tokenise_iter;
		struct tokenise {
			using iterator = tokenise_iter;


		std::string str;
		const char * delim;
			std::string str;
			const char * delim;


		iterator begin() noexcept { return ++iterator{this->delim, this->str.data()}; }
		constexpr iterator end() const noexcept { return {}; }
	};
			iterator begin() noexcept { return ++iterator{this->delim, this->str.data()}; }
			constexpr iterator end() const noexcept { return {}; }
		};
	}
}

M include/vore-visit => include/vore-visit +9 -7
@@ 4,11 4,13 @@
#pragma once


namespace vore {
	template <class... Ts>
	struct overload : Ts... {
		using Ts::operator()...;
	};
	template <class... Ts>
	overload(Ts...) -> overload<Ts...>;
namespace {
	namespace vore {
		template <class... Ts>
		struct overload : Ts... {
			using Ts::operator()...;
		};
		template <class... Ts>
		overload(Ts...) -> overload<Ts...>;
	}
}

M man/sum.1 => man/sum.1 +7 -5
@@ 56,23 56,25 @@ The algorithm is a simple, literal, sum of every byte into a 16-bit accumulator.
sees a rewrite in C:
it now uses the standard input stream if no files are specified and an updated output format:
the sum is zero-padded to 5 digits and immediately followed by the block count, space-padded to six digits
(this does mean that a file at least 51.2MB in size would end up no spaces between the sum and the block count,
(this does mean that a file at least 51.2MB in size would end up with no spaces between the sum and the block count,
but is hardly a problem, as disk packs available for the PDP-11 were 10 megabytes in size at the very top end),
followed by a space and filename, if more than one was specified, and a newline.
The algorithm also changed, rotating the accumulator right by one bit before each addition.
.Pp
It also uses stdio and
It also uses
.In stdio.h
and
.Xr getc 3 ,
emulating the block count by dividing by
.Dv BUFSIZ Pq 512 ,
rounded up.
.Dv BUFSIZ
(512), rounded up.
This is the version included in
.Bx 3 ,
which, however, started to define
.Dv BUFSIZ
to 1024 \(em this is the birth of this implementation's default
.Pq Fl r
format used to-day (extended with a fixed space and block count space-padded to a width of 5, for reasons assumed obvious).
format (extended with a fixed space and block count space-padded to a width of 5, for reasons assumed obvious).
.Pp
.At III
introduced an alternative checksum algorithm, which first added all bytes into a 32-bit accumulator, then reduced it twice to the sum of the the high and low words,

A man/tsort.1 => man/tsort.1 +163 -0
@@ 0,0 1,163 @@
.\" SPDX-License-Identifier: 0BSD
.\"
.Dd
.Dt TSORT 1
.Os
.
.Sh NAME
.Nm tsort
.Nd stable total ordering
.Sh SYNOPSIS
.Nm
.Op Ar file
.
.Sh DESCRIPTION
Constructs a directed graph from the pairs of whitespace-separated node relationships provided in
.Ar file Pq standard input if Qo Sy - Qc or none ,
then prints all unique nodes, one per line, sorted according to the total ordering described by that graph,
consistently with the partial ordering of the final occurrence of nodes in the input.
.Pp
An input pair in the form
.Qq Li a a
indicates presence of node
.Li a .
An input pair in the form
.Qq Li a b
indicates an
.Li a \[->] b
relationship:
.Li a
sorts earlier than
.Li b ;
this is transitive: another
.Qq Li b c
node means that
.Li a \[->] b \[->] c ,
i.e.
.Li a
sorts earlier than
.Li b
sorts earlier than
.Li c
.Pq indeed, a node sorts earlier than a different node if the latter can be reached from the former .
.Pp
Edges that would cause a cycle are removed, as they can interfere with preserving the input's partial ordering.
.
.Sh EXIT STATUS
.Sy 1
if
.Ar file Pq the standard input stream
couldn't be read, or contained an odd amount of tokens.
.
.Sh EXAMPLES
The simple case
.Bd -literal -compact -offset Ds
.Li $ Nm cat Pa nodes
A B
A F
B C
B D
D E
.Li $ Nm Pa nodes
A
F
B
C
D
E
.Ed
Corresponds to this graph:
.Bd -literal -compact -offset Ds
A \[->] F
\[da]
B \[->] C
\[da]
D
\[da]
E
.Ed
.Pp
By adding a
.Li D \[->] A
relationship, an
.Li A \[->] B \[->] D \[->] A
loop is created:
.Bd -literal -compact -offset Ds
.Li $ Nm echo Li A B A F B C B D D E D A Sy \&| Nm
.Nm Ns Li : -: breaking D -> A link
A
F
B
C
D
E
.Ed
Corresponding to this graph:
.Bd -literal -compact -offset Ds
A \[->] F
\[ua]\|\| \[da]
\&|\|\| B \[->] C
\&|\|\| \[da]
\&|\[<-]D
 \|\| \[da]
 \|\| E
.Ed
But, as the the looping
.Li D \[->] A
link is removed, it resolves to the same graph as the previous example.
.\" TODO: this sucks ass, but mandoc doesn't understand pic(1), and graphviz -Tpic is just broken
.Pp
.\" Nicked from POSIX (IEEE Std 1003.1-2017)
This input:
.Bd -literal -compact -offset Ds
.Li $ Nm tsort
a b c c d e
g g
f g e f
h h
.Ar ^D
a
b
c
d
e
f
g
h
.Ed
Corresponds to this graph:
.Bd -literal -compact -offset Ds
a  c  d  h
\[da]     \[da]
b     e
      \[da]
      f
      \[da]
      g
.Ed
.
.Sh SEE ALSO
.Xr lorder 1
.
.Sh STANDARDS
Conforms to
.St -p1003.1-2008 .
.
.Sh HISTORY
.\" Copied verbatim from 32V manpage
Appeared in
.At 32v ,
fully formed, as
.Dl tsort - topological sort
pointing at
.Xr lorder 1 ,
which noted:
.Pp
This brash one-liner intends to build a new library
from existing `.o' files.
.D1 ar cr library \`\|lorder *.o | tsort\`
.Pp
It was was imported into
.At v7 ,
and first described in \*[doc-Tn-font-size]X/Open\*[doc-str-St] Portability Guide Issue\~2.
.\" No St -xpg2 for us!

A tests/tsort/data/tsort.1-1 => tests/tsort/data/tsort.1-1 +5 -0
@@ 0,0 1,5 @@
A B
A F
B C
B D
D E

A tests/tsort/data/tsort.1-1.d/out => tests/tsort/data/tsort.1-1.d/out +6 -0
@@ 0,0 1,6 @@
A
F
B
C
D
E

A tests/tsort/data/tsort.1-2 => tests/tsort/data/tsort.1-2 +1 -0
@@ 0,0 1,1 @@
A B A F B C B D D E D A

A tests/tsort/data/tsort.1-2.d/err => tests/tsort/data/tsort.1-2.d/err +0 -0
A tests/tsort/data/tsort.1-2.d/out => tests/tsort/data/tsort.1-2.d/out +6 -0
@@ 0,0 1,6 @@
A
F
B
C
D
E

A tests/tsort/data/tsort.1-3 => tests/tsort/data/tsort.1-3 +4 -0
@@ 0,0 1,4 @@
a b c c d e
g g
f g e f
h h

A tests/tsort/data/tsort.1-3.d/out => tests/tsort/data/tsort.1-3.d/out +8 -0
@@ 0,0 1,8 @@
a
b
c
d
e
f
g
h

A tests/tsort/test => tests/tsort/test +32 -0
@@ 0,0 1,32 @@
#!/bin/sh
# SPDX-License-Identifier: 0BSD

IFS="
"

tmpdir="$(mktemp -dt "tsort.XXXXXXXXXX")/"
tsort="${CMDDIR}tsort"
files="$(for f in data/*; do [ -f "$f" ] && echo "${f##*/}"; done)"

cd data 2>&3 || exit
for f in $files; do
  "$tsort" "$f" 2>"${tmpdir}err" | cmp - "$f.d/out" 2>&3                   || echo "tsort: $f wrong" >&3
  [ "$([ -s "${tmpdir}err" ]; echo $?)" = "$([ -f "$f.d/err" ]; echo $?)" ] || echo "tsort: $f wrong error" >&3

  "$tsort" < "$f" 2>"${tmpdir}err" | cmp - "$f.d/out" 2>&3                 || echo "tsort: < $f wrong" >&3
  [ "$([ -s "${tmpdir}err" ]; echo $?)" = "$([ -f "$f.d/err" ]; echo $?)" ] || echo "tsort: < $f wrong error" >&3

  "$tsort" - < "$f" 2>"${tmpdir}err" | cmp - "$f.d/out" 2>&3               || echo "tsort: - < $f wrong" >&3
  [ "$([ -s "${tmpdir}err" ]; echo $?)" = "$([ -f "$f.d/err" ]; echo $?)" ] || echo "tsort: - < $f wrong error" >&3
done

errstr="$("$tsort" $files 2>&1 > /dev/null < /dev/null)" && echo "tsort: multiple files ok?" >&3
[ -n "$errstr" ]                                         || echo "tsort: multiple files empty stderr?" >&3

[ -w '/dev/full' ] || {
  echo "tsort: skipping error testing, /dev/full unavailable" >&2
  exit
}

errstr="$(echo a a | "$tsort" 2>&1 > /dev/full)" && echo "tsort: /dev/full okay?" >&3
[ -n "$errstr" ]                                 || echo "tsort: stderr empty for /dev/full" >&3