aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.posix
diff options
context:
space:
mode:
Diffstat (limited to 'nihil.posix')
-rw-r--r--nihil.posix/CMakeLists.txt51
-rw-r--r--nihil.posix/argv.cc65
-rw-r--r--nihil.posix/argv.ccm78
-rw-r--r--nihil.posix/ensure_dir.cc30
-rw-r--r--nihil.posix/ensure_dir.ccm23
-rw-r--r--nihil.posix/exec.cc71
-rw-r--r--nihil.posix/exec.ccm105
-rw-r--r--nihil.posix/fd.cc220
-rw-r--r--nihil.posix/fd.ccm157
-rw-r--r--nihil.posix/find_in_path.cc52
-rw-r--r--nihil.posix/getenv.cc45
-rw-r--r--nihil.posix/nihil.posix.ccm45
-rw-r--r--nihil.posix/open.cc31
-rw-r--r--nihil.posix/open.ccm24
-rw-r--r--nihil.posix/process.cc102
-rw-r--r--nihil.posix/process.ccm91
-rw-r--r--nihil.posix/read_file.ccm48
-rw-r--r--nihil.posix/rename.cc34
-rw-r--r--nihil.posix/rename.ccm23
-rw-r--r--nihil.posix/spawn.ccm249
-rw-r--r--nihil.posix/test_fd.cc199
-rw-r--r--nihil.posix/test_getenv.cc49
-rw-r--r--nihil.posix/test_spawn.cc117
-rw-r--r--nihil.posix/write_file.ccm82
24 files changed, 1991 insertions, 0 deletions
diff --git a/nihil.posix/CMakeLists.txt b/nihil.posix/CMakeLists.txt
new file mode 100644
index 0000000..db5e5aa
--- /dev/null
+++ b/nihil.posix/CMakeLists.txt
@@ -0,0 +1,51 @@
+# This source code is released into the public domain.
+
+add_library(nihil.posix STATIC)
+target_link_libraries(nihil.posix PRIVATE nihil.error nihil.guard nihil.monad)
+
+target_sources(nihil.posix
+ PUBLIC FILE_SET modules TYPE CXX_MODULES FILES
+ nihil.posix.ccm
+ argv.ccm
+ ensure_dir.ccm
+ exec.ccm
+ fd.ccm
+ open.ccm
+ process.ccm
+ read_file.ccm
+ rename.ccm
+ spawn.ccm
+ write_file.ccm
+
+ PRIVATE
+ argv.cc
+ ensure_dir.cc
+ exec.cc
+ getenv.cc
+ fd.cc
+ find_in_path.cc
+ open.cc
+ process.cc
+ rename.cc
+)
+
+if(NIHIL_TESTS)
+ enable_testing()
+
+ add_executable(nihil.posix.test
+ test_fd.cc
+ test_getenv.cc
+ test_spawn.cc
+ )
+
+ target_link_libraries(nihil.posix.test PRIVATE
+ nihil.posix
+ Catch2::Catch2WithMain
+ )
+
+ find_package(Catch2 REQUIRED)
+
+ include(CTest)
+ include(Catch)
+ catch_discover_tests(nihil.posix.test)
+endif()
diff --git a/nihil.posix/argv.cc b/nihil.posix/argv.cc
new file mode 100644
index 0000000..e6b1389
--- /dev/null
+++ b/nihil.posix/argv.cc
@@ -0,0 +1,65 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <memory>
+#include <ranges>
+#include <string>
+#include <vector>
+
+module nihil.posix;
+
+namespace nihil {
+
+argv::argv() = default;
+argv::argv(argv &&) noexcept = default;
+auto argv::operator=(this argv &, argv &&) -> argv & = default;
+
+argv::~argv()
+{
+ for (auto *arg : m_args)
+ delete[] arg;
+}
+
+auto argv::data(this argv const &self) -> char const * const *
+{
+ return self.m_args.data();
+}
+
+auto argv::data(this argv &self) -> char * const *
+{
+ return self.m_args.data();
+}
+
+auto argv::size(this argv const &self)
+{
+ return self.m_args.size();
+}
+
+auto argv::begin(this argv const &self)
+{
+ return self.m_args.begin();
+}
+
+auto argv::end(this argv const &self)
+{
+ return self.m_args.end();
+}
+
+
+auto argv::add_arg(this argv &self, std::string_view arg) -> void
+{
+ // Create a nul-terminated C string.
+ auto ptr = std::make_unique<char[]>(arg.size() + 1);
+ std::ranges::copy(arg, ptr.get());
+ ptr[arg.size()] = '\0';
+
+ // Ensure we won't throw when emplacing the pointer.
+ self.m_args.reserve(self.m_args.size() + 1);
+ self.m_args.emplace_back(ptr.release());
+}
+
+} // namespace nihil
+
diff --git a/nihil.posix/argv.ccm b/nihil.posix/argv.ccm
new file mode 100644
index 0000000..6f60f4b
--- /dev/null
+++ b/nihil.posix/argv.ccm
@@ -0,0 +1,78 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <memory>
+#include <ranges>
+#include <string>
+#include <vector>
+
+export module nihil.posix:argv;
+
+namespace nihil {
+
+/*
+ * argv: stores a null-terminated array of nul-terminated C strings.
+ * argv::data() is suitable for passing to ::execv().
+ *
+ * Create an argv using argv::from_range(), which takes a range of
+ * string-like objects.
+ */
+
+export struct argv {
+ /*
+ * Create a new argv from a range.
+ */
+ argv(std::from_range_t, std::ranges::range auto &&args)
+ {
+ for (auto &&arg : args)
+ add_arg(std::string_view(arg));
+
+ m_args.push_back(nullptr);
+ }
+
+ /*
+ * Create an argv from an initializer list.
+ */
+ template<typename T>
+ explicit argv(std::initializer_list<T> &&args)
+ : argv(std::from_range, std::forward<decltype(args)>(args))
+ {
+ }
+
+ // Movable.
+ argv(argv &&) noexcept;
+ auto operator=(this argv &, argv &&other) -> argv &;
+
+ // Not copyable. TODO: for completeness, it probably should be.
+ argv(argv const &) = delete;
+ auto operator=(this argv &, argv const &other) -> argv& = delete;
+
+ ~argv();
+
+ // Access the stored arguments.
+ [[nodiscard]] auto data(this argv const &self) -> char const * const *;
+ [[nodiscard]] auto data(this argv &self) -> char * const *;
+ [[nodiscard]] auto size(this argv const &self);
+
+ // Range access
+ [[nodiscard]] auto begin(this argv const &self);
+ [[nodiscard]] auto end(this argv const &self);
+
+private:
+ // Use the from_range() factory method to create new instances.
+ argv();
+
+ // The argument pointers, including the null terminator.
+ // This can't be a vector<unique_ptr> because we need an array of
+ // char pointers to pass to exec.
+ std::vector<char *> m_args;
+
+ // Add a new argument to the array.
+ auto add_arg(this argv &self, std::string_view arg) -> void;
+};
+
+} // namespace nihil
+
diff --git a/nihil.posix/ensure_dir.cc b/nihil.posix/ensure_dir.cc
new file mode 100644
index 0000000..88e8898
--- /dev/null
+++ b/nihil.posix/ensure_dir.cc
@@ -0,0 +1,30 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+#include <format>
+#include <system_error>
+
+module nihil.posix;
+
+import nihil.error;
+
+namespace nihil {
+
+auto ensure_dir(std::filesystem::path const &dir) -> std::expected<void, error>
+{
+ auto err = std::error_code();
+
+ std::filesystem::create_directories(dir, err);
+
+ if (err)
+ return std::unexpected(error(err));
+
+ return {};
+}
+
+} // namespace nihil
diff --git a/nihil.posix/ensure_dir.ccm b/nihil.posix/ensure_dir.ccm
new file mode 100644
index 0000000..fa92a90
--- /dev/null
+++ b/nihil.posix/ensure_dir.ccm
@@ -0,0 +1,23 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+
+export module nihil.posix:ensure_dir;
+
+import nihil.error;
+
+namespace nihil {
+
+/*
+ * Create the given directory and any parent directories.
+ */
+export [[nodiscard]] auto ensure_dir(std::filesystem::path const &dir)
+ -> std::expected<void, error>;
+
+} // namespace nihil
+
diff --git a/nihil.posix/exec.cc b/nihil.posix/exec.cc
new file mode 100644
index 0000000..5bdcbf7
--- /dev/null
+++ b/nihil.posix/exec.cc
@@ -0,0 +1,71 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <coroutine>
+#include <expected>
+#include <format>
+#include <string>
+#include <utility>
+
+#include <err.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+extern char **environ;
+
+module nihil.posix;
+
+import nihil.error;
+import nihil.monad;
+
+namespace nihil {
+
+fexecv::fexecv(fd &&execfd, argv &&args) noexcept
+ : m_execfd(std::move(execfd))
+ , m_args(std::move(args))
+{
+}
+
+auto fexecv::exec(this fexecv &self)
+ -> std::expected<void, error>
+{
+ ::fexecve(self.m_execfd.get(), self.m_args.data(), environ);
+ return std::unexpected(error("fexecve failed",
+ error(std::errc(errno))));
+}
+
+fexecv::fexecv(fexecv &&) noexcept = default;
+auto fexecv::operator=(this fexecv &, fexecv &&) noexcept -> fexecv& = default;
+
+auto execv(std::string_view path, argv &&argv)
+ -> std::expected<fexecv, error>
+{
+ auto file = co_await open(path, O_EXEC)
+ .transform_error([&] (error cause) {
+ return error(std::format("could not open {}", path),
+ std::move(cause));
+ });
+
+ co_return fexecv(std::move(file), std::move(argv));
+}
+
+auto execvp(std::string_view file, argv &&argv)
+ -> std::expected<fexecv, error>
+{
+ auto execfd = find_in_path(file);
+ if (!execfd)
+ return std::unexpected(error(
+ std::format("executable not found in path: {}", file)));
+ return fexecv(std::move(*execfd), std::move(argv));
+}
+
+auto shell(std::string_view const &command)
+ -> std::expected<fexecv, error>
+{
+ return execl("/bin/sh", "sh", "-c", command);
+}
+
+} // namespace nihil
diff --git a/nihil.posix/exec.ccm b/nihil.posix/exec.ccm
new file mode 100644
index 0000000..6098318
--- /dev/null
+++ b/nihil.posix/exec.ccm
@@ -0,0 +1,105 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+/*
+ * Exec providers, mostly used for spawn().
+ */
+
+#include <expected>
+#include <string>
+
+export module nihil.posix:exec;
+
+import nihil.error;
+import :argv;
+import :fd;
+
+namespace nihil {
+
+/*
+ * A concept to mark spawn executors.
+ */
+
+export struct exec_tag{};
+
+export template<typename T>
+concept executor =
+ requires (T e) {
+ std::same_as<exec_tag, typename std::remove_cvref_t<T>::tag>;
+ { e.exec() };
+ };
+
+/*
+ * fexecv: use a file descriptor and an argument vector to call ::fexecve().
+ * This is the lowest-level executor which all others are implemented
+ * in terms of.
+ *
+ * TODO: Should have a way to pass the environment (envp).
+ */
+export struct fexecv final {
+ using tag = exec_tag;
+
+ fexecv(fd &&execfd, argv &&args) noexcept;
+
+ [[nodiscard]] auto exec(this fexecv &self)
+ -> std::expected<void, error>;
+
+ // Movable
+ fexecv(fexecv &&) noexcept;
+ auto operator=(this fexecv &, fexecv &&) noexcept -> fexecv&;
+
+ // Not copyable (because we hold the open fd object)
+ fexecv(fexecv const &) = delete;
+ auto operator=(this fexecv &, fexecv const &) -> fexecv& = delete;
+
+private:
+ fd m_execfd;
+ argv m_args;
+};
+
+/*
+ * execv: equivalent to fexecv(), except the command is passed as
+ * a pathname instead of a file descriptor. Does not search $PATH.
+ */
+export [[nodiscard]] auto execv(std::string_view path, argv &&argv)
+ -> std::expected<fexecv, error>;
+
+/*
+ * execvp: equivalent to fexecv(), except the command is passed as
+ * a filename instead of a file descriptor. If the filename is not
+ * an absolute path, it will be searched for in $PATH.
+ */
+export [[nodiscard]] auto execvp(std::string_view file, argv &&argv)
+ -> std::expected<fexecv, error>;
+
+/*
+ * execl: equivalent to execv, except the arguments are passed as a
+ * variadic pack of string-like objects.
+ */
+export [[nodiscard]] auto execl(std::string_view path, auto &&...args)
+ -> std::expected<fexecv, error>
+{
+ return execv(path, argv({std::string_view(args)...}));
+}
+
+/*
+ * execlp: equivalent to execvp, except the arguments are passed as a
+ * variadic pack of string-like objects.
+ */
+export [[nodiscard]] auto execlp(std::string_view file, auto &&...args)
+ -> std::expected<fexecv, error>
+{
+ return execvp(file, argv({std::string_view(args)...}));
+}
+
+/*
+ * shell: run the process by invoking /bin/sh -c with the single argument,
+ * equivalent to system(3).
+ */
+export [[nodiscard]] auto shell(std::string_view const &command)
+ -> std::expected<fexecv, error>;
+
+} // namespace nihil
diff --git a/nihil.posix/fd.cc b/nihil.posix/fd.cc
new file mode 100644
index 0000000..6d5e54f
--- /dev/null
+++ b/nihil.posix/fd.cc
@@ -0,0 +1,220 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <coroutine>
+#include <expected>
+#include <format>
+#include <stdexcept>
+#include <system_error>
+
+module nihil.posix;
+
+import nihil.error;
+import nihil.monad;
+
+namespace nihil {
+
+fd::fd() noexcept = default;
+
+fd::fd(int fileno) noexcept
+ : m_fileno(fileno)
+{
+}
+
+fd::~fd()
+{
+ if (*this)
+ std::ignore = this->close();
+}
+
+fd::fd(fd &&other) noexcept
+ : m_fileno(std::exchange(other.m_fileno, invalid_fileno))
+{
+}
+
+auto fd::operator=(this fd &self, fd &&other) noexcept -> fd &
+{
+ if (&self != &other)
+ self.m_fileno = std::exchange(other.m_fileno, invalid_fileno);
+ return self;
+}
+
+fd::operator bool(this fd const &self) noexcept
+{
+ return self.m_fileno != invalid_fileno;
+}
+
+auto fd::close(this fd &self) -> std::expected<void, error>
+{
+ auto const ret = ::close(self.get());
+ self.m_fileno = invalid_fileno;
+
+ if (ret == 0)
+ return {};
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto fd::get(this fd const &self) -> int
+{
+ if (self)
+ return self.m_fileno;
+ throw std::logic_error("Attempt to call get() on invalid fd");
+}
+
+auto fd::release(this fd &&self) -> int
+{
+ if (self)
+ return std::exchange(self.m_fileno, invalid_fileno);
+ throw std::logic_error("Attempt to release an invalid fd");
+}
+
+auto dup(fd const &self) -> std::expected<fd, error>
+{
+ auto const newfd = ::dup(self.get());
+ if (newfd != -1)
+ return newfd;
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto dup(fd const &self, int newfd) -> std::expected<fd, error>
+{
+ auto const ret = ::dup2(self.get(), newfd);
+ if (ret != -1)
+ return newfd;
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto raw_dup(fd const &self) -> std::expected<int, error>
+{
+ auto const newfd = ::dup(self.get());
+ if (newfd != -1)
+ return newfd;
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto raw_dup(fd const &self, int newfd) -> std::expected<int, error>
+{
+ auto const ret = ::dup2(self.get(), newfd);
+ if (ret != -1)
+ return newfd;
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto getflags(fd const &self) -> std::expected<int, error>
+{
+ auto const flags = ::fcntl(self.get(), F_GETFL);
+ if (flags != -1)
+ return flags;
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto replaceflags(fd &self, int newflags) -> std::expected<void, error>
+{
+ auto const ret = ::fcntl(self.get(), F_SETFL, newflags);
+ if (ret == 0)
+ return {};
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto setflags(fd &self, int newflags) -> std::expected<int, error>
+{
+ auto flags = co_await getflags(self);
+
+ flags |= newflags;
+ co_await replaceflags(self, flags);
+
+ co_return flags;
+}
+
+auto clearflags(fd &self, int clrflags) -> std::expected<int, error>
+{
+ auto flags = co_await getflags(self);
+
+ flags &= ~clrflags;
+ co_await replaceflags(self, flags);
+
+ co_return flags;
+}
+
+auto getfdflags(fd const &self) -> std::expected<int, error>
+{
+ auto const flags = ::fcntl(self.get(), F_GETFD);
+ if (flags != -1)
+ return flags;
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto replacefdflags(fd &self, int newflags) -> std::expected<void, error>
+{
+ auto const ret = ::fcntl(self.get(), F_SETFD, newflags);
+ if (ret != -1)
+ return {};
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto setfdflags(fd &self, int newflags) -> std::expected<int, error>
+{
+ auto flags = co_await getfdflags(self);
+
+ flags |= newflags;
+ co_await replacefdflags(self, flags);
+
+ co_return flags;
+}
+
+auto clearfdflags(fd &self, int clrflags) -> std::expected<int, error>
+{
+ auto flags = co_await getfdflags(self);
+
+ flags &= ~clrflags;
+ co_await replacefdflags(self, flags);
+
+ co_return flags;
+}
+
+auto pipe() -> std::expected<std::pair<fd, fd>, error>
+{
+ auto fds = std::array<int, 2>{};
+
+ if (auto const ret = ::pipe(fds.data()); ret != 0)
+ return std::unexpected(error(std::errc(errno)));
+
+ return {{fd(fds[0]), fd(fds[1])}};
+}
+
+auto fd::write(this fd &self, std::span<std::byte const> buffer)
+ -> std::expected<std::size_t, error>
+{
+ auto const ret = ::write(self.get(), buffer.data(), buffer.size());
+ if (ret >= 0)
+ return ret;
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+auto fd::read(this fd &self, std::span<std::byte> buffer)
+ -> std::expected<std::span<std::byte>, error>
+{
+ auto const ret = ::read(self.get(), buffer.data(), buffer.size());
+ if (ret >= 0)
+ return buffer.subspan(0, ret);
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+} // namespace nihil
diff --git a/nihil.posix/fd.ccm b/nihil.posix/fd.ccm
new file mode 100644
index 0000000..b937f46
--- /dev/null
+++ b/nihil.posix/fd.ccm
@@ -0,0 +1,157 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <coroutine>
+#include <expected>
+#include <ranges>
+#include <span>
+#include <stdexcept>
+#include <system_error>
+
+export module nihil.posix:fd;
+
+import nihil.error;
+import nihil.monad;
+
+namespace nihil {
+
+/*
+ * fd: a file descriptor.
+ */
+
+export struct fd final {
+ // Construct an empty (invalid) fd.
+ fd() noexcept;
+
+ // Construct an fd from an exising file destrictor, taking ownership.
+ fd(int fd_) noexcept;
+
+ // Destructor. Close the fd, discarding any errors.
+ ~fd();
+
+ // Move from another fd, leaving the moved-from fd in an invalid state.
+ fd(fd &&other) noexcept;
+ auto operator=(this fd &, fd &&other) noexcept -> fd &;
+
+ // Not copyable.
+ fd(fd const &) = delete;
+ fd& operator=(this fd &, fd const &) = delete;
+
+ // Return true if this fd is valid (open).
+ [[nodiscard]] explicit operator bool(this fd const &self) noexcept;
+
+ // Close the wrapped fd.
+ [[nodiscard]] auto close(this fd &self) -> std::expected<void, error>;
+
+ // Return the stored fd.
+ [[nodiscard]] auto get(this fd const &self) -> int;
+
+ // Release the stored fd and return it. The caller must close it.
+ [[nodiscard]] auto release(this fd &&self) -> int;
+
+ // Write data from the provided buffer to the fd. Returns the
+ // number of bytes written.
+ [[nodiscard]] auto write(this fd &self, std::span<std::byte const>)
+ -> std::expected<std::size_t, error>;
+
+ // Read data from the fd to the provided buffer. Returns a
+ // subspan containing the data which was read.
+ [[nodiscard]] auto read(this fd &self, std::span<std::byte>)
+ -> std::expected<std::span<std::byte>, error>;
+
+private:
+ static constexpr int invalid_fileno = -1;
+
+ int m_fileno = invalid_fileno;
+};
+
+// Create a copy of this fd by calling dup().
+export [[nodiscard]] auto dup(fd const &self) -> std::expected<fd, error>;
+
+// Create a copy of this fd by calling dup2(). Note that because this results
+// in the existing fd and the new fd both being managed by an fd instance,
+// there are two potential cases that can cause problems:
+//
+// - dup()ing an fd to itself (a no-op)
+// - dup()ing an fd to an fd which is already managed by an fd instance
+//
+// In both of these cases, either use raw_dup() instead, or immediately call
+// release() on the returned fd to prevent the fd instance from closing it.
+export [[nodiscard]] auto dup(fd const &self, int newfd)
+ -> std::expected<fd, error>;
+
+// Create a copy of this fd by calling dup().
+export [[nodiscard]] auto raw_dup(fd const &self)
+ -> std::expected<int, error>;
+
+// Create a copy of this fd by calling dup2().
+export [[nodiscard]] auto raw_dup(fd const &self, int newfd)
+ -> std::expected<int, error>;
+
+// Return the fnctl flags for this fd.
+export [[nodiscard]] auto getflags(fd const &self)
+ -> std::expected<int, error>;
+
+// Replace the fnctl flags for this fd.
+export [[nodiscard]] auto replaceflags(fd &self, int newflags)
+ -> std::expected<void, error>;
+
+// Add bits to the fcntl flags for this fd. Returns the new flags.
+export [[nodiscard]] auto setflags(fd &self, int newflags)
+ -> std::expected<int, error>;
+
+// Remove bits from the fcntl flags for this fd. Returns the new flags.
+export [[nodiscard]] auto clearflags(fd &self, int clrflags)
+ -> std::expected<int, error>;
+
+// Return the fd flags for this fd.
+export [[nodiscard]] auto getfdflags(fd const &self)
+ -> std::expected<int, error>;
+
+// Replace the fd flags for this fd.
+export [[nodiscard]] auto replacefdflags(fd &self, int newflags)
+ -> std::expected<void, error>;
+
+// Add bits to the fd flags for this fd. Returns the new flags.
+export [[nodiscard]] auto setfdflags(fd &self, int newflags)
+ -> std::expected<int, error>;
+
+// Remove bits from the fd flags for this fd. Returns the new flags.
+export [[nodiscard]] auto clearfdflags(fd &self, int clrflags)
+ -> std::expected<int, error>;
+
+// Create two fds by calling pipe() and return them.
+export [[nodiscard]] auto pipe() -> std::expected<std::pair<fd, fd>, error>;
+
+/*
+ * Write data to a file descriptor from the provided range. Returns the
+ * number of bytes written.
+ */
+export [[nodiscard]] auto write(fd &file,
+ std::ranges::contiguous_range auto &&range)
+ -> std::expected<std::size_t, error>
+requires(sizeof(std::ranges::range_value_t<decltype(range)>) == 1)
+{
+ return file.write(as_bytes(std::span(range)));
+}
+
+/*
+ * Read data from a file descriptor into the provided buffer. Returns a
+ * span containing the data that was read.
+ */
+export [[nodiscard]] auto read(fd &file,
+ std::ranges::contiguous_range auto &&range)
+ -> std::expected<
+ std::span<std::ranges::range_value_t<decltype(range)>>,
+ error>
+requires(sizeof(std::ranges::range_value_t<decltype(range)>) == 1)
+{
+ auto bspan = as_writable_bytes(std::span(range));
+ auto rspan = co_await file.read(bspan);
+ co_return std::span(range).subspan(0, rspan.size());
+}
+
+} // namespace nihil
diff --git a/nihil.posix/find_in_path.cc b/nihil.posix/find_in_path.cc
new file mode 100644
index 0000000..6be963c
--- /dev/null
+++ b/nihil.posix/find_in_path.cc
@@ -0,0 +1,52 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <filesystem>
+#include <optional>
+#include <ranges>
+#include <string>
+
+#include <fcntl.h>
+#include <paths.h>
+
+module nihil.posix;
+
+namespace nihil {
+
+auto find_in_path(std::filesystem::path const &file)
+ -> std::optional<fd>
+{
+ using namespace std::literals;
+
+ auto try_open =
+ [](std::filesystem::path const &file) -> std::optional<fd>
+ {
+ auto ret = open(file, O_EXEC);
+ if (ret)
+ return {std::move(*ret)};
+ return {};
+ };
+
+ // Absolute pathname skips the search.
+ if (file.is_absolute())
+ return try_open(file);
+
+ auto path = getenv("PATH").value_or(_PATH_DEFPATH);
+
+ for (auto &&dir : path | std::views::split(':')) {
+ // An empty $PATH element means cwd.
+ auto sdir = dir.empty()
+ ? std::filesystem::path(".")
+ : std::filesystem::path(std::string_view(dir));
+
+ if (auto ret = try_open(sdir / file); ret)
+ return ret;
+ }
+
+ return {};
+}
+
+} // namespace nihil
diff --git a/nihil.posix/getenv.cc b/nihil.posix/getenv.cc
new file mode 100644
index 0000000..36df950
--- /dev/null
+++ b/nihil.posix/getenv.cc
@@ -0,0 +1,45 @@
+
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <cstdint>
+#include <expected>
+#include <string>
+#include <system_error>
+#include <vector>
+
+#include <unistd.h>
+
+module nihil.posix;
+
+import nihil.error;
+
+namespace nihil {
+
+auto getenv(std::string_view varname) -> std::expected<std::string, error>
+{
+ // Start with a buffer of this size, and double it every iteration.
+ constexpr auto bufinc = std::size_t{1024};
+
+ auto cvarname = std::string(varname);
+ auto buf = std::vector<char>(bufinc);
+ for (;;) {
+ auto const ret = ::getenv_r(cvarname.c_str(),
+ buf.data(), buf.size());
+
+ if (ret == 0)
+ return {std::string(buf.data())};
+
+ if (ret == -1 && errno == ERANGE) {
+ buf.resize(buf.size() * 2);
+ continue;
+ }
+
+ return std::unexpected(error(std::errc(errno)));
+ }
+}
+
+} // namespace nihil
diff --git a/nihil.posix/nihil.posix.ccm b/nihil.posix/nihil.posix.ccm
new file mode 100644
index 0000000..9baecf8
--- /dev/null
+++ b/nihil.posix/nihil.posix.ccm
@@ -0,0 +1,45 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+#include <optional>
+#include <string>
+
+export module nihil.posix;
+
+import nihil.error;
+
+export import :argv;
+export import :ensure_dir;
+export import :exec;
+export import :fd;
+export import :open;
+export import :process;
+export import :read_file;
+export import :rename;
+export import :spawn;
+export import :write_file;
+
+export namespace nihil {
+
+/*
+ * Find a variable by the given name in the environment by calling getenv_r().
+ */
+
+[[nodiscard]] auto getenv(std::string_view varname)
+ -> std::expected<std::string, error>;
+
+
+/*
+ * Find an executable in $PATH, open it with O_EXEC and return the fd.
+ * If $PATH is not set, uses _PATH_DEFPATH. If the file can't be found
+ * or opened, returns std::nullopt.
+ */
+[[nodiscard]] auto find_in_path(std::filesystem::path const &file)
+ -> std::optional<fd>;
+
+} // namespace nihil
diff --git a/nihil.posix/open.cc b/nihil.posix/open.cc
new file mode 100644
index 0000000..9ef6538
--- /dev/null
+++ b/nihil.posix/open.cc
@@ -0,0 +1,31 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+#include <system_error>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+module nihil.posix;
+
+import nihil.error;
+import :fd;
+
+namespace nihil {
+
+auto open(std::filesystem::path const &filename, int flags, int mode)
+ -> std::expected<fd, error>
+{
+ auto fdno = ::open(filename.c_str(), flags, mode);
+ if (fdno != -1)
+ return fd(fdno);
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+} // namespace nihil
diff --git a/nihil.posix/open.ccm b/nihil.posix/open.ccm
new file mode 100644
index 0000000..eaedacd
--- /dev/null
+++ b/nihil.posix/open.ccm
@@ -0,0 +1,24 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+
+export module nihil.posix:open;
+
+import nihil.error;
+import :fd;
+
+export namespace nihil {
+
+/*
+ * Open the given file and return an fd for it.
+ */
+[[nodiscard]] auto open(std::filesystem::path const &filename,
+ int flags, int mode = 0777)
+ -> std::expected<fd, error>;
+
+} // namespace nihil
diff --git a/nihil.posix/process.cc b/nihil.posix/process.cc
new file mode 100644
index 0000000..70e84b7
--- /dev/null
+++ b/nihil.posix/process.cc
@@ -0,0 +1,102 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <cerrno>
+#include <cstring>
+#include <expected>
+#include <format>
+#include <optional>
+#include <system_error>
+#include <utility>
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+module nihil.posix;
+
+import nihil.error;
+
+namespace nihil {
+
+auto wait_result::okay(this wait_result const &self) -> bool
+{
+ return self.status() == 0;
+}
+
+wait_result::operator bool(this wait_result const &self)
+{
+ return self.okay();
+}
+
+auto wait_result::status(this wait_result const &self) -> std::optional<int>
+{
+ if (WIFEXITED(self._status))
+ return WEXITSTATUS(self._status);
+ return {};
+}
+
+auto wait_result::signal(this wait_result const &self) -> std::optional<int>
+{
+ if (WIFSIGNALED(self._status))
+ return WTERMSIG(self._status);
+ return {};
+}
+
+wait_result::wait_result(int status)
+ : _status(status)
+{}
+
+process::process(::pid_t pid)
+ : m_pid(pid)
+{}
+
+process::~process() {
+ if (m_pid == -1)
+ return;
+
+ auto status = int{};
+ std::ignore = waitpid(m_pid, &status, WEXITED);
+}
+
+process::process(process &&other) noexcept
+ : m_pid(std::exchange(other.m_pid, -1))
+{
+}
+
+auto process::operator=(this process &self, process &&other) noexcept
+ -> process &
+{
+ if (&self != &other) {
+ self.m_pid = std::exchange(other.m_pid, -1);
+ }
+
+ return self;
+}
+
+// Get the child's process id.
+auto process::pid(this process const &self) noexcept -> ::pid_t
+{
+ return self.m_pid;
+}
+
+auto process::wait(this process &&self) -> std::expected<wait_result, error>
+{
+ auto status = int{};
+ auto ret = waitpid(self.m_pid, &status, WEXITED);
+ if (ret == -1)
+ return std::unexpected(error(std::errc(errno)));
+
+ return wait_result(status);
+}
+
+auto process::release(this process &&self) -> ::pid_t
+{
+ auto const ret = self.pid();
+ self.m_pid = -1;
+ return ret;
+}
+
+} // namespace nihil
diff --git a/nihil.posix/process.ccm b/nihil.posix/process.ccm
new file mode 100644
index 0000000..425deac
--- /dev/null
+++ b/nihil.posix/process.ccm
@@ -0,0 +1,91 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <optional>
+#include <system_error>
+#include <utility>
+
+#include <sys/types.h>
+
+export module nihil.posix:process;
+
+import nihil.error;
+
+namespace nihil {
+
+/*
+ * wait_result: the exit status of a process.
+ */
+export struct wait_result final {
+ // Return true if the process exited normally with an exit code of
+ // zero, otherwise false.
+ [[nodiscard]] auto okay(this wait_result const &self) -> bool;
+ [[nodiscard]] explicit operator bool(this wait_result const &self);
+
+ // Return the exit status, if any.
+ [[nodiscard]] auto status(this wait_result const &self)
+ -> std::optional<int>;
+
+ // Return the exit signal, if any.
+ [[nodiscard]] auto signal(this wait_result const &self)
+ -> std::optional<int>;
+
+private:
+ friend struct process;
+
+ int _status;
+
+ // Construct a new wait_result from the output of waitpid().
+ wait_result(int status);
+};
+
+/*
+ * process: represents a process we created, which can be waited for.
+ */
+export struct process final {
+ process() = delete;
+
+ /*
+ * Create a new process from a pid, which must be a child of the
+ * current process.
+ */
+ process(::pid_t pid);
+
+ // When destroyed, we automatically wait for the process to
+ // avoid creating zombie processes.
+ ~process();
+
+ // Movable.
+ process(process &&) noexcept;
+ auto operator=(this process &, process &&) noexcept -> process &;
+
+ // Not copyable.
+ process(process const &) = delete;
+ auto operator=(this process &, process const &) -> process & = delete;
+
+ // Get the child's process id.
+ [[nodiscard]] auto pid(this process const &self) noexcept -> ::pid_t;
+
+ /*
+ * Wait for this process to exit (by calling waitpid()) and return
+ * its exit status. This destroys the process state, leaving this
+ * object in a moved-from state.
+ */
+ [[nodiscard]] auto wait(this process &&self)
+ -> std::expected<wait_result, error>;
+
+ /*
+ * Release this process so we won't try to wait for it when
+ * destroying this object.
+ */
+ [[nodiscard]] auto release(this process &&self) -> ::pid_t;
+
+private:
+ ::pid_t m_pid;
+};
+
+} // namespace nihil
diff --git a/nihil.posix/read_file.ccm b/nihil.posix/read_file.ccm
new file mode 100644
index 0000000..c950f67
--- /dev/null
+++ b/nihil.posix/read_file.ccm
@@ -0,0 +1,48 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+#include <iterator>
+#include <ranges>
+#include <span>
+#include <system_error>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+export module nihil.posix:read_file;
+
+import nihil.error;
+import nihil.monad;
+import :fd;
+import :open;
+
+namespace nihil {
+
+/*
+ * Read the contents of a file into an output iterator.
+ */
+export [[nodiscard]] auto
+read_file(std::filesystem::path const &filename,
+ std::output_iterator<char> auto &&iter)
+ -> std::expected<void, error>
+{
+ auto file = co_await open(filename, O_RDONLY);
+
+ auto constexpr bufsize = std::size_t{1024};
+ auto buffer = std::array<char, bufsize>{};
+
+ for (;;) {
+ auto read_buf = co_await(read(file, buffer));
+ if (read_buf.empty())
+ co_return {};
+
+ std::ranges::copy(read_buf, iter);
+ }
+}
+
+} // namespace nihil
diff --git a/nihil.posix/rename.cc b/nihil.posix/rename.cc
new file mode 100644
index 0000000..9203d08
--- /dev/null
+++ b/nihil.posix/rename.cc
@@ -0,0 +1,34 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+
+module nihil.posix;
+
+import nihil.error;
+
+namespace nihil {
+
+/*
+ * Rename a file.
+ */
+auto rename_file(std::filesystem::path const &oldp,
+ std::filesystem::path const &newp)
+ -> std::expected<void, error>
+{
+ auto err = std::error_code();
+
+ std::filesystem::rename(oldp, newp, err);
+
+ if (err)
+ return std::unexpected(error(err));
+
+ return {};
+}
+
+
+} // namespace nihil
diff --git a/nihil.posix/rename.ccm b/nihil.posix/rename.ccm
new file mode 100644
index 0000000..796ec5b
--- /dev/null
+++ b/nihil.posix/rename.ccm
@@ -0,0 +1,23 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+
+export module nihil.posix:rename;
+
+import nihil.error;
+
+namespace nihil {
+
+/*
+ * Rename a file (or directory).
+ */
+export [[nodiscard]] auto
+rename(std::filesystem::path const &oldp, std::filesystem::path const &newp)
+ -> std::expected<void, error>;
+
+} // namespace nihil
diff --git a/nihil.posix/spawn.ccm b/nihil.posix/spawn.ccm
new file mode 100644
index 0000000..5812716
--- /dev/null
+++ b/nihil.posix/spawn.ccm
@@ -0,0 +1,249 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+/*
+ * spawn(): fork and execute a child process.
+ */
+
+#include <algorithm>
+#include <cerrno>
+#include <coroutine>
+#include <expected>
+#include <filesystem>
+#include <format>
+#include <iterator>
+#include <print>
+#include <string>
+#include <utility>
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+export module nihil.posix:spawn;
+
+import nihil.monad;
+import :argv;
+import :exec;
+import :open;
+import :process;
+
+namespace nihil {
+
+// Useful constants
+export inline int constexpr stdin_fileno = STDIN_FILENO;
+export inline int constexpr stdout_fileno = STDOUT_FILENO;
+export inline int constexpr stderr_fileno = STDERR_FILENO;
+
+/*
+ * fd_pipe: create a pipe with one end in the child and the other in the
+ * parent. The child's side will be dup2()'d to the provided fd number.
+ * The parent side fd can be retrieved via parent_fd();
+ */
+export struct fd_pipe final {
+ fd_pipe(int fdno, fd &&child_fd, fd &&parent_fd)
+ : m_fdno(fdno)
+ , m_child_fd(std::move(child_fd))
+ , m_parent_fd(std::move(parent_fd))
+ {
+ }
+
+ auto run_in_child(this fd_pipe &self, process &) -> void
+ {
+ auto err = raw_dup(self.m_child_fd, self.m_fdno);
+ if (!err) {
+ std::print("dup: {}\n", err.error());
+ _exit(1);
+ }
+
+ /*
+ * We don't care about errors from close() since the fd
+ * is still closed.
+ */
+ std::ignore = self.m_parent_fd.close();
+ std::ignore = self.m_child_fd.close();
+ }
+
+ auto run_in_parent(this fd_pipe &self, process &) -> void
+ {
+ std::ignore = self.m_child_fd.close();
+ }
+
+ [[nodiscard]] auto parent_fd(this fd_pipe &self) -> fd &
+ {
+ return self.m_parent_fd;
+ }
+
+private:
+ int m_fdno;
+ fd m_child_fd;
+ fd m_parent_fd;
+};
+
+export [[nodiscard]] auto
+make_fd_pipe(int fdno) -> std::expected<fd_pipe, error>
+{
+ auto fds = co_await pipe();
+ co_return fd_pipe(fdno, std::move(fds.first), std::move(fds.second));
+}
+
+/*
+ * fd_file: open a file and provide it to the child as a file descriptor.
+ * open_flags and open_mode are as for ::open().
+ */
+export struct fd_file final {
+ fd_file(int fdno, fd &&file_fd)
+ : m_fdno(fdno)
+ , m_file_fd(std::move(file_fd))
+ {
+ }
+
+ auto run_in_parent(this fd_file &self, process &) -> void
+ {
+ std::ignore = self.m_file_fd.close();
+ }
+
+ auto run_in_child(this fd_file &self, process &) -> void
+ {
+ auto err = raw_dup(self.m_file_fd, self.m_fdno);
+ if (!err) {
+ std::print("dup: {}\n", err.error());
+ _exit(1);
+ }
+
+ std::ignore = self.m_file_fd.close();
+ }
+
+private:
+ int m_fdno;
+ fd m_file_fd;
+};
+
+export [[nodiscard]] auto
+make_fd_file(int fdno, std::filesystem::path const &file,
+ int flags, int mode = 0777)
+ -> std::expected<fd_file, error>
+{
+ auto fd = co_await open(file, flags, mode);
+ co_return fd_file(fdno, std::move(fd));
+}
+
+/*
+ * Shorthand for fd_file with /dev/null as the file.
+ */
+
+export [[nodiscard]] inline auto
+stdin_devnull() -> std::expected<fd_file, error>
+{
+ return make_fd_file(stdin_fileno, "/dev/null", O_RDONLY);
+}
+
+export [[nodiscard]] inline auto
+stdout_devnull() -> std::expected<fd_file, error>
+{
+ return make_fd_file(stdout_fileno, "/dev/null", O_WRONLY);
+}
+
+export [[nodiscard]] inline auto
+stderr_devnull() -> std::expected<fd_file, error>
+{
+ return make_fd_file(stderr_fileno, "/dev/null", O_WRONLY);
+}
+
+/*
+ * Capture the output of a pipe in the parent and read it into an
+ * output iterator.
+ */
+export template<std::output_iterator<char> Iterator>
+struct fd_capture final {
+ fd_capture(fd_pipe &&pipe, Iterator it)
+ : m_pipe(std::move(pipe))
+ , m_iterator(std::move(it))
+ {
+ }
+
+ auto run_in_child(this fd_capture &self, process &p) -> void
+ {
+ self.m_pipe.run_in_child(p);
+ }
+
+ auto run_in_parent(this fd_capture &self, process &p) -> void
+ {
+ self.m_pipe.run_in_parent(p);
+
+ auto constexpr bufsize = std::size_t{1024};
+ auto buffer = std::array<char, bufsize>();
+ auto &fd = self.m_pipe.parent_fd();
+ for (;;) {
+ auto ret = read(fd, buffer);
+ if (!ret || ret->size() == 0)
+ break;
+
+ std::ranges::copy(*ret, self.m_iterator);
+ }
+
+ // We probably want to handle errors here somehow,
+ // but it's not clear what would be useful behaviour.
+ }
+
+private:
+ fd_pipe m_pipe;
+ Iterator m_iterator;
+};
+
+export [[nodiscard]] auto
+make_capture(int fdno, std::output_iterator<char> auto &&it)
+ -> std::expected<fd_capture<decltype(it)>, error>
+{
+ auto pipe = co_await make_fd_pipe(fdno);
+ co_return fd_capture(std::move(pipe),
+ std::forward<decltype(it)>(it));
+}
+
+export [[nodiscard]] auto
+make_capture(int fdno, std::string &str)
+ -> std::expected<fd_capture<decltype(std::back_inserter(str))>, error>
+{
+ auto pipe = co_await make_fd_pipe(fdno);
+ co_return fd_capture(std::move(pipe), std::back_inserter(str));
+}
+
+/*
+ * Spawn a new process with the given arguments and return a struct process.
+ * Throws exec_error() on failure.
+ */
+export [[nodiscard]] auto
+spawn(executor auto &&executor, auto &&...actions)
+ -> std::expected<process, error>
+{
+ auto const pid = ::fork();
+ if (pid == -1)
+ return std::unexpected(error("fork failed",
+ error(std::errc(errno))));
+
+ auto proc = process(pid);
+
+ if (pid == 0) {
+ // We are in the child. Release the process so we don't
+ // try to wait for ourselves, then run child handlers and
+ // exec the process.
+
+ std::ignore = std::move(proc).release();
+ (actions.run_in_child(proc), ...);
+
+ auto err = executor.exec();
+ std::print("{}\n", err.error());
+ _exit(1);
+ }
+
+ (actions.run_in_parent(proc), ...);
+
+ return proc;
+}
+
+} // namespace nihil
diff --git a/nihil.posix/test_fd.cc b/nihil.posix/test_fd.cc
new file mode 100644
index 0000000..8dff323
--- /dev/null
+++ b/nihil.posix/test_fd.cc
@@ -0,0 +1,199 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <span>
+#include <stdexcept>
+
+#include <stdio.h>
+#include <fcntl.h>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.error;
+import nihil.posix;
+
+using namespace std::literals;
+
+namespace {
+
+// Test if an fd is open.
+auto fd_is_open(int fd) -> bool {
+ auto const ret = ::fcntl(fd, F_GETFL);
+ return ret == 0;
+}
+
+} // anonymous namespace
+
+TEST_CASE("fd: construct empty", "[fd]") {
+ nihil::fd fd;
+
+ REQUIRE(!fd);
+ REQUIRE_THROWS_AS(fd.get(), std::logic_error);
+}
+
+TEST_CASE("fd: construct from fd", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ {
+ auto fd = nihil::fd(file);
+ REQUIRE(fd_is_open(fd.get()));
+ }
+
+ REQUIRE(!fd_is_open(file));
+}
+
+TEST_CASE("fd: close", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+ REQUIRE(fd);
+
+ auto const ret = fd.close();
+ REQUIRE(ret);
+ REQUIRE(!fd_is_open(file));
+}
+
+TEST_CASE("fd: move construct", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd1 = nihil::fd(file);
+ REQUIRE(fd_is_open(fd1.get()));
+
+ auto fd2(std::move(fd1));
+ REQUIRE(!fd1);
+ REQUIRE(fd2);
+ REQUIRE(fd2.get() == file);
+ REQUIRE(fd_is_open(file));
+}
+
+TEST_CASE("fd: move assign", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd1 = nihil::fd(file);
+ REQUIRE(fd_is_open(fd1.get()));
+
+ auto fd2 = nihil::fd();
+ REQUIRE(!fd2);
+
+ fd2 = std::move(fd1);
+
+ REQUIRE(!fd1);
+ REQUIRE(fd2);
+ REQUIRE(fd2.get() == file);
+ REQUIRE(fd_is_open(file));
+}
+
+TEST_CASE("fd: release", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+ auto fdesc = std::move(fd).release();
+ REQUIRE(!fd);
+ REQUIRE(fdesc == file);
+}
+
+TEST_CASE("fd: dup", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+ REQUIRE(fd);
+
+ auto fd2 = dup(fd);
+ REQUIRE(fd2);
+ REQUIRE(fd.get() != fd2->get());
+}
+
+TEST_CASE("fd: dup2", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ REQUIRE(!fd_is_open(666));
+
+ auto fd = nihil::fd(file);
+ auto fd2 = dup(fd, 666);
+
+ REQUIRE(fd);
+ REQUIRE(fd2);
+ REQUIRE(fd2->get() == 666);
+}
+
+TEST_CASE("fd: flags", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+
+ {
+ auto const ret = replaceflags(fd, 0);
+ REQUIRE(ret);
+ REQUIRE(getflags(fd) == 0);
+ }
+
+ {
+ auto const ret = setflags(fd, O_NONBLOCK);
+ REQUIRE(ret == O_NONBLOCK);
+ REQUIRE(getflags(fd) == O_NONBLOCK);
+ }
+
+ {
+ auto const ret = setflags(fd, O_SYNC);
+ REQUIRE(ret == (O_NONBLOCK|O_SYNC));
+ REQUIRE(getflags(fd) == (O_NONBLOCK|O_SYNC));
+ }
+
+ {
+ auto const ret = clearflags(fd, O_NONBLOCK);
+ REQUIRE(ret == O_SYNC);
+ REQUIRE(getflags(fd) == O_SYNC);
+ }
+}
+
+TEST_CASE("fd: fdflags", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+
+ {
+ auto const ret = replacefdflags(fd, 0);
+ REQUIRE(ret);
+ REQUIRE(getfdflags(fd) == 0);
+ }
+
+ {
+ auto const ret = setfdflags(fd, FD_CLOEXEC);
+ REQUIRE(ret == FD_CLOEXEC);
+ REQUIRE(getfdflags(fd) == FD_CLOEXEC);
+ }
+
+ {
+ auto const ret = clearfdflags(fd, FD_CLOEXEC);
+ REQUIRE(ret == 0);
+ REQUIRE(getfdflags(fd) == 0);
+ }
+}
+
+TEST_CASE("fd: pipe, read, write", "[fd]") {
+ auto fds = nihil::pipe();
+ REQUIRE(fds);
+
+ auto [fd1, fd2] = std::move(*fds);
+
+ auto constexpr test_string = "test string"sv;
+
+ auto ret = write(fd1, test_string);
+ REQUIRE(ret);
+ REQUIRE(*ret == test_string.size());
+
+ auto readbuf = std::array<char, test_string.size() * 2>{};
+ auto read_buf = read(fd2, readbuf);
+ REQUIRE(read_buf);
+ REQUIRE(std::string_view(*read_buf) == test_string);
+}
diff --git a/nihil.posix/test_getenv.cc b/nihil.posix/test_getenv.cc
new file mode 100644
index 0000000..fdb5277
--- /dev/null
+++ b/nihil.posix/test_getenv.cc
@@ -0,0 +1,49 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <ranges>
+#include <string>
+#include <system_error>
+
+#include <unistd.h>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.posix;
+
+TEST_CASE("getenv: existing value", "[getenv]")
+{
+ auto constexpr *name = "NIHIL_TEST_VAR";
+ auto constexpr *value = "test is a test";
+
+ REQUIRE(::setenv(name, value, 1) == 0);
+
+ auto const s = nihil::getenv(name);
+ REQUIRE(s);
+ REQUIRE(*s == value);
+}
+
+TEST_CASE("getenv: non-existing value", "[getenv]")
+{
+ auto constexpr *name = "NIHIL_TEST_VAR";
+
+ REQUIRE(::unsetenv(name) == 0);
+
+ auto const s = nihil::getenv(name);
+ REQUIRE(!s);
+ REQUIRE(s.error() == std::errc::no_such_file_or_directory);
+}
+
+// Force the call to getenv_r() to reallocate.
+TEST_CASE("getenv: long value")
+{
+ auto constexpr *name = "NIHIL_TEST_VAR";
+ auto const value = std::string(4096, 'a');
+
+ REQUIRE(::setenv(name, value.c_str(), 1) == 0);
+
+ auto const s = nihil::getenv(name);
+ REQUIRE(s);
+ REQUIRE(*s == value);
+}
diff --git a/nihil.posix/test_spawn.cc b/nihil.posix/test_spawn.cc
new file mode 100644
index 0000000..da321ff
--- /dev/null
+++ b/nihil.posix/test_spawn.cc
@@ -0,0 +1,117 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.posix;
+
+TEST_CASE("spawn: system", "[spawn]")
+{
+ using namespace nihil;
+
+ auto exec = shell("x=1; echo $x");
+ REQUIRE(exec);
+
+ auto output = std::string();
+ auto capture = make_capture(stdout_fileno, output);
+ REQUIRE(capture);
+
+ auto proc = spawn(*exec, *capture);
+ REQUIRE(proc);
+
+ auto status = std::move(*proc).wait();
+ REQUIRE(status);
+
+ REQUIRE(status->okay());
+ REQUIRE(output == "1\n");
+}
+
+TEST_CASE("spawn: execv", "[spawn]") {
+ using namespace nihil;
+
+ auto args = argv({"sh", "-c", "x=1; echo $x"});
+ auto exec = execv("/bin/sh", std::move(args));
+ REQUIRE(exec);
+
+ auto output = std::string();
+ auto capture = make_capture(stdout_fileno, output);
+ REQUIRE(capture);
+
+ auto proc = spawn(*exec, *capture);
+ REQUIRE(proc);
+
+ auto status = std::move(*proc).wait();
+ REQUIRE(status);
+
+ REQUIRE(status->okay());
+ REQUIRE(output == "1\n");
+}
+
+TEST_CASE("spawn: execvp", "[spawn]") {
+ using namespace nihil;
+
+ auto args = argv({"sh", "-c", "x=1; echo $x"});
+ auto exec = execvp("sh", std::move(args));
+ REQUIRE(exec);
+
+ auto output = std::string();
+ auto capture = make_capture(stdout_fileno, output);
+ REQUIRE(capture);
+
+ auto proc = spawn(*exec, *capture);
+ REQUIRE(proc);
+
+ auto status = std::move(*proc).wait();
+ REQUIRE(status);
+
+ REQUIRE(status->okay());
+ REQUIRE(output == "1\n");
+}
+
+TEST_CASE("spawn: execl", "[spawn]") {
+ using namespace nihil;
+
+ auto exec = execl("/bin/sh", "sh", "-c", "x=1; echo $x");
+ REQUIRE(exec);
+
+ auto output = std::string();
+ auto capture = make_capture(stdout_fileno, output);
+ REQUIRE(capture);
+
+ auto proc = spawn(*exec, *capture);
+ REQUIRE(proc);
+
+ auto status = std::move(*proc).wait();
+ REQUIRE(status);
+
+ REQUIRE(status->okay());
+ REQUIRE(output == "1\n");
+}
+
+TEST_CASE("spawn: execlp", "[spawn]") {
+ using namespace nihil;
+
+ auto exec = execlp("sh", "sh", "-c", "x=1; echo $x");
+ REQUIRE(exec);
+
+ auto output = std::string();
+ auto capture = make_capture(stdout_fileno, output);
+ REQUIRE(capture);
+
+ auto proc = spawn(*exec, *capture);
+ REQUIRE(proc);
+
+ auto status = std::move(*proc).wait();
+ REQUIRE(status);
+
+ REQUIRE(status->okay());
+ REQUIRE(output == "1\n");
+}
+
+TEST_CASE("spawn: execlp failure", "[spawn]") {
+ using namespace nihil;
+
+ auto exec = execlp("nihil_no_such_executable", "x");
+ REQUIRE(!exec);
+}
diff --git a/nihil.posix/write_file.ccm b/nihil.posix/write_file.ccm
new file mode 100644
index 0000000..867e0db
--- /dev/null
+++ b/nihil.posix/write_file.ccm
@@ -0,0 +1,82 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <coroutine>
+#include <expected>
+#include <filesystem>
+#include <ranges>
+#include <system_error>
+#include <vector>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+export module nihil.posix:write_file;
+
+import nihil.error;
+import nihil.guard;
+import nihil.monad;
+import :fd;
+import :open;
+import :rename;
+
+namespace nihil {
+
+/*
+ * Write the contents of a range to a file. Returns the number of bytes
+ * written.
+ */
+export [[nodiscard]]
+auto write_file(std::filesystem::path const &filename,
+ std::ranges::contiguous_range auto &&range,
+ int mode = 0777)
+ -> std::expected<std::size_t, error>
+{
+ auto file = co_await open(filename, O_CREAT|O_WRONLY, mode);
+ auto nbytes = co_await write(file, range);
+ co_return nbytes;
+}
+
+/*
+ * Utility wrapper for non-contiguous ranges.
+ */
+export [[nodiscard]]
+auto write_file(std::filesystem::path const &filename,
+ std::ranges::range auto &&range)
+ -> std::expected<std::size_t, error>
+requires(!std::ranges::contiguous_range<decltype(range)>)
+{
+ return write_file(filename, std::vector(std::from_range, range));
+}
+
+/*
+ * Write the contents of a range to a file safely. The data will be written
+ * to "<filename>.tmp", and if the write succeeds, the temporary file will be
+ * renamed to the target filename. If an error occurs, the target file will
+ * not be modified.
+ */
+export [[nodiscard]]
+auto safe_write_file(std::filesystem::path const &filename,
+ std::ranges::range auto &&range)
+ -> std::expected<void, error>
+{
+ auto tmpfile = filename;
+ tmpfile.remove_filename();
+ tmpfile /= (filename.filename().native() + ".tmp");
+
+ auto tmpfile_guard = guard([&tmpfile] {
+ ::unlink(tmpfile.c_str());
+ });
+
+ co_await write_file(tmpfile, range);
+ co_await nihil::rename(tmpfile, filename);
+
+ tmpfile_guard.release();
+ co_return {};
+}
+
+
+} // namespace nihil