aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.posix
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-30 07:51:23 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-30 07:51:23 +0100
commit034cd404a129103a8dd7747e6bd00ffd5550da93 (patch)
treed27946517d4d9333abd26ac50bbd4a436093e2ce /nihil.posix
parent3e7902f7d790a486d3d9cb978df193f07f3a6ad9 (diff)
downloadnihil-034cd404a129103a8dd7747e6bd00ffd5550da93.tar.gz
nihil-034cd404a129103a8dd7747e6bd00ffd5550da93.tar.bz2
refactoring
Diffstat (limited to 'nihil.posix')
-rw-r--r--nihil.posix/CMakeLists.txt30
-rw-r--r--nihil.posix/argv.cc65
-rw-r--r--nihil.posix/argv.ccm97
-rw-r--r--nihil.posix/argv.test.cc256
-rw-r--r--nihil.posix/ensure_dir.cc30
-rw-r--r--nihil.posix/ensure_dir.ccm16
-rw-r--r--nihil.posix/exec.cc22
-rw-r--r--nihil.posix/exec.ccm53
-rw-r--r--nihil.posix/execl.ccm40
-rw-r--r--nihil.posix/execl.test.cc55
-rw-r--r--nihil.posix/execlp.ccm18
-rw-r--r--nihil.posix/execlp.test.cc67
-rw-r--r--nihil.posix/execshell.ccm21
-rw-r--r--nihil.posix/execshell.test.cc55
-rw-r--r--nihil.posix/execv.cc42
-rw-r--r--nihil.posix/execv.ccm38
-rw-r--r--nihil.posix/execv.test.cc55
-rw-r--r--nihil.posix/execvp.cc50
-rw-r--r--nihil.posix/execvp.ccm28
-rw-r--r--nihil.posix/fd.cc220
-rw-r--r--nihil.posix/fd.ccm278
-rw-r--r--nihil.posix/fd.test.cc (renamed from nihil.posix/test.fd.cc)53
-rw-r--r--nihil.posix/fexecv.cc47
-rw-r--r--nihil.posix/fexecv.ccm33
-rw-r--r--nihil.posix/fexecvp.ccm37
-rw-r--r--nihil.posix/find_in_path.cc52
-rw-r--r--nihil.posix/find_in_path.ccm43
-rw-r--r--nihil.posix/getenv.cc60
-rw-r--r--nihil.posix/getenv.ccm55
-rw-r--r--nihil.posix/getenv.test.cc (renamed from nihil.posix/test.getenv.cc)0
-rw-r--r--nihil.posix/open.cc31
-rw-r--r--nihil.posix/open.ccm85
-rw-r--r--nihil.posix/open_in_path.cc51
-rw-r--r--nihil.posix/open_in_path.ccm46
-rw-r--r--nihil.posix/posix.ccm16
-rw-r--r--nihil.posix/process.cc102
-rw-r--r--nihil.posix/process.ccm132
-rw-r--r--nihil.posix/read_file.ccm2
-rw-r--r--nihil.posix/rename.cc34
-rw-r--r--nihil.posix/rename.ccm21
-rw-r--r--nihil.posix/spawn.ccm15
-rw-r--r--nihil.posix/tempfile.cc128
-rw-r--r--nihil.posix/tempfile.ccm162
-rw-r--r--nihil.posix/tempfile.test.cc (renamed from nihil.posix/test.tempfile.cc)0
-rw-r--r--nihil.posix/test.spawn.cc40
-rw-r--r--nihil.posix/write_file.ccm2
46 files changed, 1400 insertions, 1383 deletions
diff --git a/nihil.posix/CMakeLists.txt b/nihil.posix/CMakeLists.txt
index b04018e..61e83df 100644
--- a/nihil.posix/CMakeLists.txt
+++ b/nihil.posix/CMakeLists.txt
@@ -10,13 +10,15 @@ target_sources(nihil.posix
argv.ccm
ensure_dir.ccm
- exec.ccm
+ execl.ccm
execlp.ccm
+ execshell.ccm
execv.ccm
execvp.ccm
executor.ccm
fd.ccm
fexecv.ccm
+ fexecvp.ccm
find_in_path.ccm
getenv.ccm
open.ccm
@@ -27,31 +29,21 @@ target_sources(nihil.posix
spawn.ccm
tempfile.ccm
write_file.ccm
-
- PRIVATE
- argv.cc
- ensure_dir.cc
- exec.cc
- execv.cc
- execvp.cc
- getenv.cc
- fd.cc
- find_in_path.cc
- open.cc
- open_in_path.cc
- process.cc
- rename.cc
- tempfile.cc
)
if(NIHIL_TESTS)
enable_testing()
add_executable(nihil.posix.test
- test.fd.cc
- test.getenv.cc
+ argv.test.cc
+ execl.test.cc
+ execlp.test.cc
+ execshell.test.cc
+ execv.test.cc
+ fd.test.cc
+ getenv.test.cc
test.spawn.cc
- test.tempfile.cc
+ tempfile.test.cc
)
target_link_libraries(nihil.posix.test PRIVATE
diff --git a/nihil.posix/argv.cc b/nihil.posix/argv.cc
deleted file mode 100644
index e6b1389..0000000
--- a/nihil.posix/argv.cc
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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
index 6f60f4b..de75770 100644
--- a/nihil.posix/argv.ccm
+++ b/nihil.posix/argv.ccm
@@ -16,19 +16,35 @@ 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 {
+export struct argv
+{
+ using value_type = char *;
+ using iterator = value_type const *;
+ using const_iterator = value_type const *;
+
/*
* Create a new argv from a range.
+ *
+ * Delegate to the default constructor to ensure the pointers in
+ * m_args are deleted if we throw when allocating.
*/
argv(std::from_range_t, std::ranges::range auto &&args)
+ : argv()
{
- for (auto &&arg : args)
- add_arg(std::string_view(arg));
+ for (auto &&arg : args) {
+ auto str = std::string_view(arg);
+
+ // Create a nul-terminated C string.
+ auto ptr = std::make_unique<char[]>(str.size() + 1); // NOLINT
+ std::ranges::copy(str, ptr.get());
+ ptr[str.size()] = '\0';
+
+ // Ensure we won't throw when emplacing the pointer.
+ m_args.reserve(m_args.size() + 1);
+ m_args.emplace_back(ptr.release());
+ }
m_args.push_back(nullptr);
}
@@ -36,43 +52,68 @@ export struct argv {
/*
* 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))
+ template <typename T>
+ argv(std::initializer_list<T> const &args)
+ : argv(std::from_range, args)
{
}
+ template <typename T>
+ argv(std::initializer_list<T> &&args)
+ : argv(std::from_range, std::move(args))
+ {
+ }
+
+ // Destructor.
+ ~argv()
+ {
+ for (auto *arg : m_args)
+ delete[] arg; // NOLINT
+ }
+
// Movable.
- argv(argv &&) noexcept;
- auto operator=(this argv &, argv &&other) -> argv &;
+ argv(argv &&) noexcept = default;
+ auto operator=(argv &&other) noexcept -> argv & = default;
// Not copyable. TODO: for completeness, it probably should be.
argv(argv const &) = delete;
- auto operator=(this argv &, argv const &other) -> argv& = delete;
-
- ~argv();
+ auto operator=(argv const &other) -> argv & = delete;
// 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);
+ [[nodiscard]] auto data(this argv const &self) -> value_type const *
+ {
+ return self.m_args.data();
+ }
+
+ [[nodiscard]] auto size(this argv const &self) -> std::size_t
+ {
+ return self.m_args.size();
+ }
+
+ // Access an element by index. Throws std::out_of_range if the index is out of range.
+ [[nodiscard]] auto operator[](this argv const &self, std::size_t index) -> value_type
+ {
+ return self.m_args.at(index);
+ }
// Range access
- [[nodiscard]] auto begin(this argv const &self);
- [[nodiscard]] auto end(this argv const &self);
+ [[nodiscard]] auto begin(this argv const &self) -> const_iterator
+ {
+ return self.data();
+ }
+
+ [[nodiscard]] auto end(this argv const &self) -> const_iterator
+ {
+ return self.data() + self.size(); // NOLINT
+ }
private:
- // Use the from_range() factory method to create new instances.
- argv();
+ // Private since default-constructing an argv isn't useful.
+ argv() = default;
- // 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.
+ // 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/argv.test.cc b/nihil.posix/argv.test.cc
new file mode 100644
index 0000000..3cc218d
--- /dev/null
+++ b/nihil.posix/argv.test.cc
@@ -0,0 +1,256 @@
+/*
+ * This source code is released into the public domain
+ */
+
+#include <algorithm>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.posix;
+
+namespace {
+constexpr auto *test_tags = "[nihil][nihil.posix][nihil.posix.argv]";
+
+TEST_CASE("nihil.posix: argv: invariants", test_tags)
+{
+ static_assert(std::movable<nihil::argv>);
+ static_assert(std::move_constructible<nihil::argv>);
+ static_assert(std::is_nothrow_move_constructible_v<nihil::argv>);
+ static_assert(std::is_nothrow_move_assignable_v<nihil::argv>);
+
+ static_assert(!std::copy_constructible<nihil::argv>);
+
+ static_assert(std::destructible<nihil::argv>);
+
+ static_assert(std::swappable<nihil::argv>);
+ static_assert(std::is_nothrow_swappable_v<nihil::argv>);
+
+ static_assert(std::constructible_from<nihil::argv,
+ std::initializer_list<std::string_view>>);
+ static_assert(std::constructible_from<nihil::argv,
+ std::initializer_list<std::string>>);
+ static_assert(std::constructible_from<nihil::argv,
+ std::initializer_list<char const *>>);
+ static_assert(std::constructible_from<nihil::argv,
+ std::initializer_list<char *>>);
+
+ static_assert(std::ranges::sized_range<nihil::argv>);
+ static_assert(std::ranges::contiguous_range<nihil::argv>);
+}
+
+SCENARIO("nihil::argv::size() works")
+{
+ GIVEN("An argv constructed from 3 elements")
+ {
+ auto argv = nihil::argv{"one", "two", "three"};
+
+ THEN("size() returns 4")
+ {
+ REQUIRE(std::ranges::size(argv) == 4);
+ }
+ }
+}
+
+SCENARIO("nihil::argv::operator[] works")
+{
+ using namespace std::literals;
+
+ GIVEN("An argv constructed from 3 elements")
+ {
+ auto argv = nihil::argv{"one", "two", "three"};
+
+ THEN("operator[] returns the expected elements")
+ {
+ REQUIRE(argv[0] == "one"sv);
+ REQUIRE(argv[1] == "two"sv);
+ REQUIRE(argv[2] == "three"sv);
+ REQUIRE(argv[3] == nullptr);
+ }
+
+ AND_THEN("operator[] on a non-existent element throws std::out_of_range")
+ {
+ REQUIRE_THROWS_AS(argv[4], std::out_of_range);
+ }
+ }
+}
+
+SCENARIO("nihil::argv can be constructed from an initializer_list<char const *>")
+{
+ using namespace std::literals;
+
+ GIVEN("An argv constructed from an initializer_list")
+ {
+ auto argv = nihil::argv{"one", "two", "three"};
+
+ THEN("size() returns 4")
+ {
+ REQUIRE(std::ranges::size(argv) == 4);
+ }
+
+ AND_THEN("The stored values match the initializer_list")
+ {
+ REQUIRE(argv[0] == "one"sv);
+ REQUIRE(argv[1] == "two"sv);
+ REQUIRE(argv[2] == "three"sv);
+ REQUIRE(argv[3] == nullptr);
+ }
+ }
+}
+
+SCENARIO("nihil::argv can be constructed from an initializer_list<std::string_view>")
+{
+ using namespace std::literals;
+
+ GIVEN("An argv constructed from an initializer_list")
+ {
+ auto argv = nihil::argv{"one"sv, "two"sv, "three"sv};
+
+ THEN("size() returns 4")
+ {
+ REQUIRE(std::ranges::size(argv) == 4);
+ }
+
+ AND_THEN("The stored values match the initializer_list")
+ {
+ REQUIRE(argv[0] == "one"sv);
+ REQUIRE(argv[1] == "two"sv);
+ REQUIRE(argv[2] == "three"sv);
+ REQUIRE(argv[3] == nullptr);
+ }
+ }
+}
+
+SCENARIO("nihil::argv can be constructed from a range of std::string")
+{
+ using namespace std::literals;
+
+ GIVEN("An argv constructed from an initializer_list")
+ {
+ auto vec = std::vector{"one"s, "two"s, "three"s};
+ auto argv = nihil::argv(std::from_range, vec);
+
+ THEN("size() returns 4")
+ {
+ REQUIRE(std::ranges::size(argv) == 4);
+ }
+
+ AND_THEN("The stored values match the range")
+ {
+ REQUIRE(argv[0] == "one"sv);
+ REQUIRE(argv[1] == "two"sv);
+ REQUIRE(argv[2] == "three"sv);
+ REQUIRE(argv[3] == nullptr);
+ }
+ }
+}
+
+SCENARIO("nihil::data() returns the correct data")
+{
+ using namespace std::literals;
+
+ GIVEN("An argv")
+ {
+ auto argv = nihil::argv{"one", "two", "three"};
+
+ THEN("The values in data() match the provided data")
+ {
+ REQUIRE(argv.data()[0] == "one"sv);
+ REQUIRE(argv.data()[1] == "two"sv);
+ REQUIRE(argv.data()[2] == "three"sv);
+ REQUIRE(argv.data()[3] == nullptr);
+ }
+ }
+}
+
+SCENARIO("nihil::argv can be used as a range")
+{
+ using namespace std::literals;
+
+ GIVEN("An argv")
+ {
+ auto argv = nihil::argv{"one"sv, "two"sv, "three"sv};
+
+ WHEN("An std::vector is constructed from the argv")
+ {
+ auto vec = std::vector(std::from_range, argv);
+
+ THEN("The argv and the vector contain the same data")
+ {
+ REQUIRE(std::ranges::equal(argv, vec));
+ }
+ }
+
+ WHEN("The argv is copied to an std::vector using a range-based for")
+ {
+ auto vec = std::vector<char *>();
+
+ for (auto &&arg: argv)
+ vec.push_back(arg);
+
+ THEN("The argv and the vector contain the same data")
+ {
+ REQUIRE(std::ranges::equal(argv, vec));
+ }
+ }
+ }
+}
+
+SCENARIO("nihil::argv can be move-constructed")
+{
+ using namespace std::literals;
+
+ GIVEN("An argv object")
+ {
+ auto argv = nihil::argv{"one", "two", "three"};
+
+ WHEN("The argv is moved")
+ {
+ auto argv2 = std::move(argv);
+
+ THEN("The new object contains the data")
+ {
+ REQUIRE(argv2.size() == 4);
+ REQUIRE(argv2[0] == "one"sv);
+ }
+
+ AND_THEN("The old object does not")
+ {
+ REQUIRE(argv.size() == 0);
+ }
+ }
+ }
+}
+
+SCENARIO("nihil::argv can be move-assigned")
+{
+ using namespace std::literals;
+
+ GIVEN("Two argv objects")
+ {
+ auto argv1 = nihil::argv{"one", "two", "three"};
+ auto argv2 = nihil::argv{"x", "y"};
+
+ WHEN("argv2 is move-assigned to argv1")
+ {
+ argv1 = std::move(argv2);
+
+ THEN("argv1 contains the data from argv2")
+ {
+ REQUIRE(argv1.size() == 3);
+ REQUIRE(argv1[0] == "x"sv);
+ REQUIRE(argv1[1] == "y"sv);
+ REQUIRE(argv1[2] == nullptr);
+ }
+
+ AND_THEN("argv2 is empty")
+ {
+ REQUIRE(argv2.size() == 0);
+ }
+ }
+ }
+}
+
+} // anonymous namespace
diff --git a/nihil.posix/ensure_dir.cc b/nihil.posix/ensure_dir.cc
deleted file mode 100644
index 88e8898..0000000
--- a/nihil.posix/ensure_dir.cc
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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
index fa92a90..7eecea8 100644
--- a/nihil.posix/ensure_dir.ccm
+++ b/nihil.posix/ensure_dir.ccm
@@ -6,6 +6,7 @@ module;
#include <expected>
#include <filesystem>
+#include <system_error>
export module nihil.posix:ensure_dir;
@@ -16,8 +17,17 @@ namespace nihil {
/*
* Create the given directory and any parent directories.
*/
-export [[nodiscard]] auto ensure_dir(std::filesystem::path const &dir)
- -> std::expected<void, error>;
+export [[nodiscard]] auto
+ensure_dir(std::filesystem::path const &dir) -> std::expected<void, error>
+{
+ auto err = std::error_code();
-} // namespace nihil
+ std::filesystem::create_directories(dir, err);
+
+ if (err)
+ return std::unexpected(error(err));
+ return {};
+}
+
+} // namespace nihil
diff --git a/nihil.posix/exec.cc b/nihil.posix/exec.cc
deleted file mode 100644
index b4e8732..0000000
--- a/nihil.posix/exec.cc
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * This source code is released into the public domain.
- */
-
-module;
-
-#include <expected>
-#include <format>
-
-module nihil.posix;
-
-import nihil.error;
-import nihil.monad;
-
-namespace nihil {
-
-auto shell(std::string_view const &command) -> std::expected<base_executor_type, error>
-{
- return execl("/bin/sh", "sh", "-c", command);
-}
-
-} // namespace nihil
diff --git a/nihil.posix/exec.ccm b/nihil.posix/exec.ccm
deleted file mode 100644
index cd03117..0000000
--- a/nihil.posix/exec.ccm
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * This source code is released into the public domain.
- */
-
-module;
-
-/*
- * Exec providers, mostly used for spawn().
- */
-
-#include <expected>
-#include <string>
-
-#include "nihil.hh"
-
-export module nihil.posix:exec;
-
-import nihil.error;
-import :execv;
-import :fexecv;
-import :argv;
-import :fd;
-
-namespace nihil {
-
-/*
- * The lowest-level executor type, which is returned by other executors.
- * Prefer fexecve() if it's available, otherwise use execve().
- */
-#ifdef NIHIL_HAVE_FEXECVE
-using base_executor_type = fexecv;
-#else
-using base_executor_type = execv;
-#endif
-
-/*
- * 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<base_executor_type, error>
-{
- return execv(path, 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<base_executor_type, error>;
-
-} // namespace nihil
diff --git a/nihil.posix/execl.ccm b/nihil.posix/execl.ccm
new file mode 100644
index 0000000..f3cbf9a
--- /dev/null
+++ b/nihil.posix/execl.ccm
@@ -0,0 +1,40 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <string>
+
+#include "nihil.hh"
+
+export module nihil.posix:execl;
+
+import nihil.error;
+import :argv;
+import :execv;
+import :fexecv;
+
+namespace nihil {
+
+/*
+ * execl: equivalent to (f)execv, except the arguments are passed as a
+ * variadic pack of string-like objects.
+ */
+
+export [[nodiscard]] auto execl(std::string_view path, auto &&...args) -> execv
+{
+ return execv(path, argv({std::string_view(args)...}));
+}
+
+#ifdef NIHIL_HAVE_FEXECVE
+
+export [[nodiscard]] auto execl(fd &&executable, auto &&...args) -> fexecv
+{
+ return fexecv(std::move(executable), argv({std::string_view(args)...}));
+}
+
+#endif // NIHIL_HAVE_FEXECVE
+
+} // namespace nihil
diff --git a/nihil.posix/execl.test.cc b/nihil.posix/execl.test.cc
new file mode 100644
index 0000000..5aaaa25
--- /dev/null
+++ b/nihil.posix/execl.test.cc
@@ -0,0 +1,55 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.posix;
+
+namespace {
+
+SCENARIO("nihil::execl() can be used to spawn a shell")
+{
+ GIVEN("An execl object")
+ {
+ auto exec = nihil::execl("/bin/sh", "sh", "-c", "x=1; echo $x");
+
+ WHEN("The shell is executed")
+ {
+ auto output = std::string();
+ auto capture = nihil::make_capture(nihil::stdout_fileno, output).value();
+ auto status = nihil::spawn(exec, capture).value().wait().value();
+
+ THEN("The exit code is 0")
+ {
+ REQUIRE(status.status() == 0);
+ }
+ AND_THEN("The expected output was captured")
+ {
+ REQUIRE(output == "1\n");
+ }
+ }
+ }
+}
+
+SCENARIO("nihil::execl() returns the shell's exit code")
+{
+ GIVEN("An execshell object")
+ {
+ auto exec = nihil::execl("/bin/sh", "sh", "-c", "x=42; exit $x");
+
+ WHEN("The shell is executed")
+ {
+ auto output = std::string();
+ auto capture = nihil::make_capture(nihil::stdout_fileno, output).value();
+ auto status = nihil::spawn(exec, capture).value().wait().value();
+
+ THEN("The exit code is 1")
+ {
+ REQUIRE(status.status() == 42);
+ }
+ }
+ }
+}
+
+} // anonymous namespace
diff --git a/nihil.posix/execlp.ccm b/nihil.posix/execlp.ccm
index d0d88d5..ab3737c 100644
--- a/nihil.posix/execlp.ccm
+++ b/nihil.posix/execlp.ccm
@@ -1,28 +1,22 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
-#include <string>
#include <expected>
#include <format>
+#include <string>
export module nihil.posix:execlp;
import nihil.error;
-import :exec;
import :argv;
import :execvp;
namespace nihil {
-/*
- * 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<base_executor_type, error>
+// 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<execv, error>
{
return execvp(file, argv({std::string_view(args)...}));
}
diff --git a/nihil.posix/execlp.test.cc b/nihil.posix/execlp.test.cc
new file mode 100644
index 0000000..cedf871
--- /dev/null
+++ b/nihil.posix/execlp.test.cc
@@ -0,0 +1,67 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.posix;
+
+namespace {
+
+SCENARIO("nihil::execlp() can be used to spawn a shell")
+{
+ GIVEN("An execlp object")
+ {
+ auto exec = nihil::execlp("sh", "sh", "-c", "x=1; echo $x");
+
+ THEN("sh was found in $PATH")
+ {
+ if (!exec)
+ FAIL(exec.error());
+ }
+
+ WHEN("The shell is executed")
+ {
+ auto output = std::string();
+ auto capture = nihil::make_capture(nihil::stdout_fileno, output).value();
+ auto status = nihil::spawn(exec.value(), capture).value().wait().value();
+
+ THEN("The exit code is 0")
+ {
+ REQUIRE(status.status() == 0);
+ }
+ AND_THEN("The expected output was captured")
+ {
+ REQUIRE(output == "1\n");
+ }
+ }
+ }
+}
+
+SCENARIO("nihil::execlp() returns the shell's exit code")
+{
+ GIVEN("An execlp object")
+ {
+ auto exec = nihil::execlp("sh", "sh", "-c", "x=42; exit $x");
+
+ THEN("sh was found in $PATH")
+ {
+ if (!exec)
+ FAIL(exec.error());
+ }
+
+ WHEN("The shell is executed")
+ {
+ auto output = std::string();
+ auto capture = nihil::make_capture(nihil::stdout_fileno, output).value();
+ auto status = nihil::spawn(exec.value(), capture).value().wait().value();
+
+ THEN("The exit code is 1")
+ {
+ REQUIRE(status.status() == 42);
+ }
+ }
+ }
+}
+
+} // anonymous namespace
diff --git a/nihil.posix/execshell.ccm b/nihil.posix/execshell.ccm
new file mode 100644
index 0000000..1fbfccf
--- /dev/null
+++ b/nihil.posix/execshell.ccm
@@ -0,0 +1,21 @@
+// This source code is released into the public domain.
+module;
+
+#include <string>
+
+export module nihil.posix:execshell;
+
+import nihil.error;
+import :execv;
+import :execl;
+
+namespace nihil {
+
+// execshell: a spawn executor which runs the process by invoking /bin/sh -c with the
+// single argument, equivalent to system(3).
+export [[nodiscard]] auto execshell(std::string_view command) -> execv
+{
+ return execl("/bin/sh", "sh", "-c", command);
+}
+
+} // namespace nihil
diff --git a/nihil.posix/execshell.test.cc b/nihil.posix/execshell.test.cc
new file mode 100644
index 0000000..b64953a
--- /dev/null
+++ b/nihil.posix/execshell.test.cc
@@ -0,0 +1,55 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.posix;
+
+namespace {
+
+SCENARIO("nihil::execshell() can be used to spawn a shell")
+{
+ GIVEN("An execshell object")
+ {
+ auto exec = nihil::execshell("x=1; echo $x");
+
+ WHEN("The shell is executed")
+ {
+ auto output = std::string();
+ auto capture = nihil::make_capture(nihil::stdout_fileno, output).value();
+ auto status = nihil::spawn(exec, capture).value().wait().value();
+
+ THEN("The exit code is 0")
+ {
+ REQUIRE(status.status() == 0);
+ }
+ AND_THEN("The expected output was captured")
+ {
+ REQUIRE(output == "1\n");
+ }
+ }
+ }
+}
+
+SCENARIO("nihil::execshell() returns the shell's exit code")
+{
+ GIVEN("An execshell object")
+ {
+ auto exec = nihil::execshell("x=42; exit $x");
+
+ WHEN("The shell is executed")
+ {
+ auto output = std::string();
+ auto capture = nihil::make_capture(nihil::stdout_fileno, output).value();
+ auto status = nihil::spawn(exec, capture).value().wait().value();
+
+ THEN("The exit code is 1")
+ {
+ REQUIRE(status.status() == 42);
+ }
+ }
+ }
+}
+
+} // anonymous namespace
diff --git a/nihil.posix/execv.cc b/nihil.posix/execv.cc
deleted file mode 100644
index 752a96d..0000000
--- a/nihil.posix/execv.cc
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * This source code is released into the public domain.
- */
-
-module;
-
-#include <coroutine>
-#include <expected>
-#include <filesystem>
-#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 {
-
-execv::execv(std::filesystem::path path, argv &&args) noexcept
- : m_path(std::move(path))
- , m_args(std::move(args))
-{
-}
-
-auto execv::exec(this execv &self) -> std::expected<void, error>
-{
- ::execve(self.m_path.string().c_str(), self.m_args.data(), environ);
- return std::unexpected(error("execve failed", error(std::errc(errno))));
-}
-
-execv::execv(execv &&) noexcept = default;
-auto execv::operator=(this execv &, execv &&) noexcept -> execv & = default;
-
-} // namespace nihil
diff --git a/nihil.posix/execv.ccm b/nihil.posix/execv.ccm
index d97754e..ef9d259 100644
--- a/nihil.posix/execv.ccm
+++ b/nihil.posix/execv.ccm
@@ -1,13 +1,12 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
#include <expected>
#include <filesystem>
#include <string>
+#include <unistd.h>
+
export module nihil.posix:execv;
import nihil.error;
@@ -16,32 +15,37 @@ import :executor;
namespace nihil {
-/*
- * execv: use a filename and an argument vector to call ::execve().
- * This is the lowest-level executor which all others are implemented
- * in terms of, if fexecve is not available.
- *
- * TODO: Should have a way to pass the environment (envp).
- */
+// execv: use a filename and an argument vector to call ::execv().
export struct execv final
{
using tag = exec_tag;
- execv(std::filesystem::path, argv &&) noexcept;
+ execv(std::filesystem::path path, argv &&args) noexcept
+ : m_path(std::move(path))
+ , m_args(std::move(args))
+ {
+ }
- [[nodiscard]] auto exec(this execv &) -> std::expected<void, error>;
+ ~execv() = default;
// Movable
- execv(execv &&) noexcept;
- auto operator=(this execv &, execv &&) noexcept -> execv &;
+ execv(execv &&) noexcept = default;
+ auto operator=(execv &&) noexcept -> execv & = default;
// Not copyable (because m_args isn't copyable).
execv(execv const &) = delete;
auto operator=(this execv &, execv const &) -> execv & = delete;
+ // Perform the execv(). This only returns on failure.
+ [[nodiscard]] auto exec(this execv &self) -> std::expected<void, error>
+ {
+ ::execv(self.m_path.string().c_str(), self.m_args.data());
+ return std::unexpected(error("execve failed", error(std::errc(errno))));
+ }
+
private:
- std::filesystem::path m_path;
- argv m_args;
+ std::filesystem::path m_path;
+ argv m_args;
};
} // namespace nihil
diff --git a/nihil.posix/execv.test.cc b/nihil.posix/execv.test.cc
new file mode 100644
index 0000000..aaeead7
--- /dev/null
+++ b/nihil.posix/execv.test.cc
@@ -0,0 +1,55 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.posix;
+
+namespace {
+
+SCENARIO("nihil::execv() can be used to spawn a shell")
+{
+ GIVEN("An execv object")
+ {
+ auto exec = nihil::execv("/bin/sh", nihil::argv{"sh", "-c", "x=1; echo $x"});
+
+ WHEN("The shell is executed")
+ {
+ auto output = std::string();
+ auto capture = nihil::make_capture(nihil::stdout_fileno, output).value();
+ auto status = nihil::spawn(exec, capture).value().wait().value();
+
+ THEN("The exit code is 0")
+ {
+ REQUIRE(status.status() == 0);
+ }
+ AND_THEN("The expected output was captured")
+ {
+ REQUIRE(output == "1\n");
+ }
+ }
+ }
+}
+
+SCENARIO("nihil::execv() returns the shell's exit code")
+{
+ GIVEN("An execshell object")
+ {
+ auto exec = nihil::execv("/bin/sh", nihil::argv{"sh", "-c", "x=42; exit $x"});
+
+ WHEN("The shell is executed")
+ {
+ auto output = std::string();
+ auto capture = nihil::make_capture(nihil::stdout_fileno, output).value();
+ auto status = nihil::spawn(exec, capture).value().wait().value();
+
+ THEN("The exit code is 1")
+ {
+ REQUIRE(status.status() == 42);
+ }
+ }
+ }
+}
+
+} // anonymous namespace
diff --git a/nihil.posix/execvp.cc b/nihil.posix/execvp.cc
deleted file mode 100644
index 5eac315..0000000
--- a/nihil.posix/execvp.cc
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * This source code is released into the public domain.
- */
-
-module;
-
-#include <coroutine>
-#include <expected>
-#include <filesystem>
-#include <format>
-#include <string>
-#include <utility>
-
-#include <err.h>
-#include <fcntl.h>
-#include <unistd.h>
-
-#include "nihil.hh"
-
-module nihil.posix;
-
-import nihil.error;
-import nihil.monad;
-
-namespace nihil {
-#ifdef NIHIL_HAVE_FEXECVE
-
-auto execvp(std::string_view file, argv &&argv) -> std::expected<fexecv, error>
-{
- auto execfd = open_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));
-}
-
-#else // !NIHIL_HAVE_FEXECVE
-
-auto execvp(std::string_view file, nihil::argv &&argv) -> std::expected<nihil::execv, nihil::error>
-{
- auto filename = nihil::find_in_path(file);
- if (!filename)
- return std::unexpected(nihil::error(
- std::format("executable not found in path: {}", file)));
- return execv(std::move(*filename), std::move(argv));
-}
-
-#endif // NIHIL_HAVE_FEXECVE
-
-} // namespace nihil
diff --git a/nihil.posix/execvp.ccm b/nihil.posix/execvp.ccm
index 688ecb6..680a13e 100644
--- a/nihil.posix/execvp.ccm
+++ b/nihil.posix/execvp.ccm
@@ -1,7 +1,4 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
#include <string>
@@ -12,17 +9,22 @@ export module nihil.posix:execvp;
import nihil.error;
import :argv;
-import :exec;
+import :execv;
+import :find_in_path;
namespace nihil {
-/*
- * 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<base_executor_type, error>;
+// execvp: equivalent to execv, 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<execv, error>
+{
+ auto filename = nihil::find_in_path(file);
+ if (!filename)
+ return std::unexpected(nihil::error(
+ std::format("executable not found in path: {}", file)));
+ return execv(std::move(*filename), std::move(argv));
+}
} // namespace nihil
diff --git a/nihil.posix/fd.cc b/nihil.posix/fd.cc
deleted file mode 100644
index 6d5e54f..0000000
--- a/nihil.posix/fd.cc
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * 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
index b937f46..7faf2f1 100644
--- a/nihil.posix/fd.ccm
+++ b/nihil.posix/fd.ccm
@@ -1,7 +1,4 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
#include <coroutine>
@@ -11,56 +8,136 @@ module;
#include <stdexcept>
#include <system_error>
+#include <fcntl.h>
+#include <unistd.h>
+
export module nihil.posix:fd;
+import nihil.flagset;
import nihil.error;
import nihil.monad;
namespace nihil {
-/*
- * fd: a file descriptor.
- */
+// F_{GET,SET}FL flags
+struct fd_flags_tag
+{
+};
+export using fd_flags = nihil::flagset<int, fd_flags_tag>;
+
+export inline constexpr auto fd_none = fd_flags();
+export inline constexpr auto fd_nonblock = fd_flags::mask<O_NONBLOCK>();
+export inline constexpr auto fd_append = fd_flags::mask<O_APPEND>();
+export inline constexpr auto fd_async = fd_flags::mask<O_ASYNC>();
+export inline constexpr auto fd_sync = fd_flags::mask<O_SYNC>();
+export inline constexpr auto fd_dsync = fd_flags::mask<O_DSYNC>();
+
+#ifdef O_DIRECT
+export inline constexpr auto fl_direct = O_DIRECT;
+#endif
+
+// F_{GET,SET}FD flags
+struct fd_fdflags_tag
+{
+};
+export using fd_fdflags = nihil::flagset<int, fd_fdflags_tag>;
-export struct fd final {
+export inline constexpr auto fd_fd_none = fd_fdflags();
+export inline constexpr auto fd_fd_cloexec = fd_fdflags::mask<FD_CLOEXEC>();
+
+// fd: a file descriptor.
+export struct fd final
+{
// Construct an empty (invalid) fd.
- fd() noexcept;
+ fd() noexcept = default;
- // Construct an fd from an exising file destrictor, taking ownership.
- fd(int fd_) noexcept;
+ // Construct an fd from an exising file descriptor, taking ownership.
+ explicit fd(int fileno) noexcept
+ : m_fileno(fileno)
+ {
+ }
// Destructor. Close the fd, discarding any errors.
- ~fd();
+ ~fd()
+ {
+ if (*this)
+ std::ignore = this->close();
+ }
// 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 &;
+ fd(fd &&other) noexcept
+ : m_fileno(std::exchange(other.m_fileno, invalid_fileno))
+ {
+ }
+
+ auto operator=(this fd &self, fd &&other) noexcept -> fd &
+ {
+ if (&self != &other)
+ self.m_fileno = std::exchange(other.m_fileno, invalid_fileno);
+ return self; // NOLINT
+ }
// Not copyable.
fd(fd const &) = delete;
- fd& operator=(this fd &, fd const &) = delete;
+ auto operator=(this fd &, fd const &) -> fd & = delete;
// Return true if this fd is valid (open).
- [[nodiscard]] explicit operator bool(this fd const &self) noexcept;
+ [[nodiscard]] explicit operator bool(this fd const &self) noexcept
+ {
+ return self.m_fileno != invalid_fileno;
+ }
// Close the wrapped fd.
- [[nodiscard]] auto close(this fd &self) -> std::expected<void, error>;
+ [[nodiscard]] auto 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)));
+ }
// Return the stored fd.
- [[nodiscard]] auto get(this fd const &self) -> int;
+ [[nodiscard]] auto get(this fd const &self) -> int
+ {
+ if (self)
+ return self.m_fileno;
+ throw std::logic_error("Attempt to call get() on invalid fd");
+ }
// Release the stored fd and return it. The caller must close it.
- [[nodiscard]] auto release(this fd &&self) -> int;
+ [[nodiscard]] auto 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");
+ }
// 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>;
+ [[nodiscard]] auto
+ 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)));
+ }
// 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>;
+ [[nodiscard]] auto 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)));
+ }
private:
static constexpr int invalid_fileno = -1;
@@ -69,7 +146,14 @@ private:
};
// Create a copy of this fd by calling dup().
-export [[nodiscard]] auto dup(fd const &self) -> std::expected<fd, error>;
+export [[nodiscard]] auto dup(fd const &self) -> std::expected<fd, error>
+{
+ auto const newfd = ::dup(self.get());
+ if (newfd != -1)
+ return fd(newfd);
+
+ return std::unexpected(error(std::errc(errno)));
+}
// 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,
@@ -80,73 +164,137 @@ export [[nodiscard]] auto dup(fd const &self) -> std::expected<fd, error>;
//
// 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>;
+export [[nodiscard]] auto dup(fd const &self, int newfd) -> std::expected<fd, error>
+{
+ auto const ret = ::dup2(self.get(), newfd);
+ if (ret != -1)
+ return fd(newfd);
+
+ return std::unexpected(error(std::errc(errno)));
+}
// Create a copy of this fd by calling dup().
-export [[nodiscard]] auto raw_dup(fd const &self)
- -> std::expected<int, error>;
+export [[nodiscard]] 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)));
+}
// Create a copy of this fd by calling dup2().
-export [[nodiscard]] auto raw_dup(fd const &self, int newfd)
- -> std::expected<int, error>;
+export [[nodiscard]] 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)));
+}
+
+// Call fcntl() on this fd. Prefer one of the type-safe wrappers to this, if available.
+export [[nodiscard]] auto fcntl(fd const &fd, int op, int arg = 0)
+ -> std::expected<int, error>
+{
+ auto const ret = ::fcntl(fd.get(), op, arg);
+ if (ret == -1)
+ return std::unexpected(error(std::errc(errno)));
+ return ret;
+}
// Return the fnctl flags for this fd.
-export [[nodiscard]] auto getflags(fd const &self)
- -> std::expected<int, error>;
+export [[nodiscard]] auto getflags(fd const &fd) -> std::expected<fd_flags, error>
+{
+ auto flags = co_await fcntl(fd, F_GETFL);
+ co_return fd_flags::from_int(flags);
+}
// Replace the fnctl flags for this fd.
-export [[nodiscard]] auto replaceflags(fd &self, int newflags)
- -> std::expected<void, error>;
+export [[nodiscard]] auto replaceflags(fd &fd, fd_flags newflags) -> std::expected<void, error>
+{
+ co_await fcntl(fd, F_SETFL, newflags.value());
+ co_return {};
+}
// 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>;
+export [[nodiscard]] auto setflags(fd &fd, fd_flags newflags) -> std::expected<fd_flags, error>
+{
+ auto flags = co_await getflags(fd);
+
+ flags |= newflags;
+ co_await replaceflags(fd, flags);
+ co_return flags;
+}
// 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>;
+export [[nodiscard]] auto clearflags(fd &fd, fd_flags clrflags) -> std::expected<fd_flags, error>
+{
+ auto flags = co_await getflags(fd);
+
+ flags &= ~clrflags;
+ co_await replaceflags(fd, flags);
+ co_return flags;
+}
// Return the fd flags for this fd.
-export [[nodiscard]] auto getfdflags(fd const &self)
- -> std::expected<int, error>;
+export [[nodiscard]] auto getfdflags(fd const &fd) -> std::expected<fd_fdflags, error>
+{
+ auto const flags = co_await fcntl(fd, F_GETFD);
+ co_return fd_fdflags::from_int(flags);
+}
// Replace the fd flags for this fd.
-export [[nodiscard]] auto replacefdflags(fd &self, int newflags)
- -> std::expected<void, error>;
+export [[nodiscard]] auto replacefdflags(fd &fd, fd_fdflags newflags) -> std::expected<void, error>
+{
+ co_await fcntl(fd, F_SETFD, newflags.value());
+ co_return {};
+}
// 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>;
+export [[nodiscard]] auto setfdflags(fd &fd, fd_fdflags newflags) -> std::expected<fd_fdflags, error>
+{
+ auto flags = co_await getfdflags(fd);
+
+ flags |= newflags;
+ co_await replacefdflags(fd, flags);
+ co_return flags;
+}
// 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>;
+export [[nodiscard]] auto clearfdflags(fd &fd, fd_fdflags clrflags) -> std::expected<fd_fdflags, error>
+{
+ auto flags = co_await getfdflags(fd);
+
+ flags &= ~clrflags;
+ co_await replacefdflags(fd, flags);
+ co_return flags;
+}
// 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)
+export [[nodiscard]] 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])}};
+}
+
+// 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>
+// 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));
diff --git a/nihil.posix/test.fd.cc b/nihil.posix/fd.test.cc
index 5c282af..870ddde 100644
--- a/nihil.posix/test.fd.cc
+++ b/nihil.posix/fd.test.cc
@@ -2,10 +2,10 @@
* This source code is released into the public domain.
*/
+#include <coroutine>
#include <span>
#include <stdexcept>
-#include <stdio.h>
#include <fcntl.h>
#include <catch2/catch_test_macros.hpp>
@@ -23,10 +23,8 @@ auto fd_is_open(int fd) -> bool {
return ret == 0;
}
-} // anonymous namespace
-
TEST_CASE("fd: construct empty", "[fd]") {
- nihil::fd fd;
+ auto const fd = nihil::fd();
REQUIRE(!fd);
REQUIRE_THROWS_AS(fd.get(), std::logic_error);
@@ -111,17 +109,18 @@ TEST_CASE("fd: dup", "[fd]") {
}
TEST_CASE("fd: dup2", "[fd]") {
+ auto constexpr test_fd = 666;
auto file = ::open("/dev/null", O_RDONLY);
REQUIRE(file > 0);
- REQUIRE(!fd_is_open(666));
+ REQUIRE(!fd_is_open(test_fd));
auto fd = nihil::fd(file);
- auto fd2 = dup(fd, 666);
+ auto fd2 = dup(fd, test_fd);
REQUIRE(fd);
REQUIRE(fd2);
- REQUIRE(fd2->get() == 666);
+ REQUIRE(fd2->get() == test_fd);
}
TEST_CASE("fd: flags", "[fd]") {
@@ -131,27 +130,27 @@ TEST_CASE("fd: flags", "[fd]") {
auto fd = nihil::fd(file);
{
- auto const ret = replaceflags(fd, 0);
+ auto const ret = replaceflags(fd, nihil::fd_none);
REQUIRE(ret);
- REQUIRE(getflags(fd) == 0);
+ REQUIRE(getflags(fd) == nihil::fd_none);
}
{
- auto const ret = setflags(fd, O_NONBLOCK);
- REQUIRE(ret == O_NONBLOCK);
- REQUIRE(getflags(fd) == O_NONBLOCK);
+ auto const ret = setflags(fd, nihil::fd_nonblock);
+ REQUIRE(ret == nihil::fd_nonblock);
+ REQUIRE(getflags(fd) == nihil::fd_nonblock);
}
{
- auto const ret = setflags(fd, O_SYNC);
- REQUIRE(ret == (O_NONBLOCK|O_SYNC));
- REQUIRE(getflags(fd) == (O_NONBLOCK|O_SYNC));
+ auto const ret = setflags(fd, nihil::fd_sync);
+ REQUIRE(ret == (nihil::fd_nonblock | nihil::fd_sync));
+ REQUIRE(getflags(fd) == (nihil::fd_nonblock | nihil::fd_sync));
}
{
- auto const ret = clearflags(fd, O_NONBLOCK);
- REQUIRE(ret == O_SYNC);
- REQUIRE(getflags(fd) == O_SYNC);
+ auto const ret = clearflags(fd, nihil::fd_nonblock);
+ REQUIRE(ret == nihil::fd_sync);
+ REQUIRE(getflags(fd) == nihil::fd_sync);
}
}
@@ -162,21 +161,21 @@ TEST_CASE("fd: fdflags", "[fd]") {
auto fd = nihil::fd(file);
{
- auto const ret = replacefdflags(fd, 0);
+ auto const ret = replacefdflags(fd, nihil::fd_fd_none);
REQUIRE(ret);
- REQUIRE(getfdflags(fd) == 0);
+ REQUIRE(getfdflags(fd) == nihil::fd_fd_none);
}
{
- auto const ret = setfdflags(fd, FD_CLOEXEC);
- REQUIRE(ret == FD_CLOEXEC);
- REQUIRE(getfdflags(fd) == FD_CLOEXEC);
+ auto const ret = setfdflags(fd, nihil::fd_fd_cloexec);
+ REQUIRE(ret == nihil::fd_fd_cloexec);
+ REQUIRE(getfdflags(fd) == nihil::fd_fd_cloexec);
}
{
- auto const ret = clearfdflags(fd, FD_CLOEXEC);
- REQUIRE(ret == 0);
- REQUIRE(getfdflags(fd) == 0);
+ auto const ret = clearfdflags(fd, nihil::fd_fd_cloexec);
+ REQUIRE(ret == nihil::fd_fd_none);
+ REQUIRE(getfdflags(fd) == nihil::fd_fd_none);
}
}
@@ -202,3 +201,5 @@ TEST_CASE("fd: pipe, read, write", "[fd]") {
REQUIRE(read_buf);
REQUIRE(std::string_view(*read_buf) == test_string);
}
+
+} // anonymous namespace
diff --git a/nihil.posix/fexecv.cc b/nihil.posix/fexecv.cc
deleted file mode 100644
index ad57d14..0000000
--- a/nihil.posix/fexecv.cc
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * This source code is released into the public domain.
- */
-
-module;
-
-#include <coroutine>
-#include <expected>
-#include <format>
-#include <string>
-#include <utility>
-
-#include <err.h>
-
-#include "nihil.h"
-
-#ifdef NIHIL_HAVE_FEXECVE
-
-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;
-
-} // namespace nihil
-
-#endif // NIHIL_HAVE_FEXECVE
diff --git a/nihil.posix/fexecv.ccm b/nihil.posix/fexecv.ccm
index 1fe57a8..4001726 100644
--- a/nihil.posix/fexecv.ccm
+++ b/nihil.posix/fexecv.ccm
@@ -1,7 +1,4 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
#include <expected>
@@ -27,27 +24,35 @@ namespace nihil {
*
* TODO: Should have a way to pass the environment (envp).
*/
-export struct fexecv final {
+export struct fexecv final
+{
using tag = exec_tag;
- fexecv(fd &&execfd, argv &&args) noexcept;
+ fexecv(fd &&execfd, argv &&args) noexcept
+ : m_execfd(std::move(execfd))
+ , m_args(std::move(args))
+ {
+ }
- [[nodiscard]] auto exec(this fexecv &self)
- -> std::expected<void, error>;
+ [[nodiscard]] auto 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))));
+ }
// Movable
- fexecv(fexecv &&) noexcept;
- auto operator=(this fexecv &, fexecv &&) noexcept -> fexecv&;
+ fexecv(fexecv &&) noexcept = default;
+ auto operator=(this fexecv &, fexecv &&) noexcept -> fexecv & = default;
// Not copyable (because we hold the open fd object)
fexecv(fexecv const &) = delete;
- auto operator=(this fexecv &, fexecv const &) -> fexecv& = delete;
+ auto operator=(this fexecv &, fexecv const &) -> fexecv & = delete;
private:
- fd m_execfd;
- argv m_args;
+ fd m_execfd;
+ argv m_args;
};
-#endif // NIHIL_HAVE_FEXECVE
+#endif // NIHIL_HAVE_FEXECVE
} // namespace nihil
diff --git a/nihil.posix/fexecvp.ccm b/nihil.posix/fexecvp.ccm
new file mode 100644
index 0000000..d61240c
--- /dev/null
+++ b/nihil.posix/fexecvp.ccm
@@ -0,0 +1,37 @@
+// This source code is released into the public domain.
+module;
+
+#include <expected>
+#include <filesystem>
+#include <format>
+#include <string>
+
+#include "nihil.hh"
+
+export module nihil.posix:fexecvp;
+
+#ifdef NIHIL_HAVE_FEXECVE
+
+import nihil.error;
+import :argv;
+import :execv;
+import :open_in_path;
+
+namespace nihil {
+
+// execvp: equivalent to execv, 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
+fexecvp(std::filesystem::path const &file, argv &&argv) -> std::expected<execv, error>
+{
+ auto execfd = open_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));
+}
+
+} // namespace nihil
+
+#endif // NIHIL_HAVE_FEXECVE
diff --git a/nihil.posix/find_in_path.cc b/nihil.posix/find_in_path.cc
deleted file mode 100644
index 7b03faa..0000000
--- a/nihil.posix/find_in_path.cc
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * This source code is released into the public domain.
- */
-
-module;
-
-#include <filesystem>
-#include <optional>
-#include <ranges>
-#include <string>
-
-#include <fcntl.h>
-#include <paths.h>
-#include <unistd.h>
-
-module nihil.posix;
-
-namespace nihil {
-
-auto find_in_path(std::filesystem::path const &file) -> std::optional<std::filesystem::path>
-{
- using namespace std::literals;
-
- auto try_return = [](std::filesystem::path file)
- -> std::optional<std::filesystem::path>
- {
- auto ret = ::access(file.string().c_str(), X_OK);
- if (ret == 0)
- return {std::move(file)};
- return {};
- };
-
- // Absolute pathname skips the search.
- if (file.is_absolute())
- return try_return(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_return(sdir / file); ret)
- return ret;
- }
-
- return {};
-}
-
-} // namespace nihil
diff --git a/nihil.posix/find_in_path.ccm b/nihil.posix/find_in_path.ccm
index 4988a12..7bfa3b9 100644
--- a/nihil.posix/find_in_path.ccm
+++ b/nihil.posix/find_in_path.ccm
@@ -1,24 +1,53 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
#include <filesystem>
#include <optional>
+#include <ranges>
+
+#include <paths.h>
+#include <unistd.h>
export module nihil.posix:find_in_path;
import nihil.error;
import :fd;
+import :getenv;
namespace nihil {
/*
* Find an executable in $PATH and return the full path. If $PATH is not set, uses _PATH_DEFPATH.
- * If the file can't be found or opened, returns std::nullopt.
+ * If the file can't be found or is not executable, returns std::nullopt.
*/
-export [[nodiscard]] auto find_in_path(std::filesystem::path const &file)
- -> std::optional<std::filesystem::path>;
+export [[nodiscard]] auto
+find_in_path(std::filesystem::path const &file) -> std::optional<std::filesystem::path>
+{
+ using namespace std::literals;
+
+ auto try_return = [](std::filesystem::path file) -> std::optional<std::filesystem::path> {
+ auto ret = ::access(file.string().c_str(), X_OK);
+ if (ret == 0)
+ return {std::move(file)};
+ return {};
+ };
+
+ // Absolute pathname skips the search.
+ if (file.is_absolute())
+ return try_return(file);
+
+ auto const path = getenv("PATH").value_or(_PATH_DEFPATH); // NOLINT
+
+ 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_return(sdir / file); ret)
+ return {ret};
+ }
+
+ return {};
+}
} // namespace nihil
diff --git a/nihil.posix/getenv.cc b/nihil.posix/getenv.cc
deleted file mode 100644
index c596902..0000000
--- a/nihil.posix/getenv.cc
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * This source code is released into the public domain.
- */
-
-module;
-
-#include <cstdint>
-#include <expected>
-#include <string>
-#include <system_error>
-#include <vector>
-
-#include <unistd.h>
-
-#include "nihil.hh"
-
-module nihil.posix;
-
-import nihil.error;
-
-namespace nihil {
-
-auto getenv(std::string_view varname) -> std::expected<std::string, error>
-{
- auto cvarname = std::string(varname);
-
-#ifdef NIHIL_HAVE_GETENV_R
- // Start with a buffer of this size, and double it every iteration.
- constexpr auto bufinc = std::size_t{1024};
-
- 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)));
- }
-#else // NIHIL_HAVE_GETENV_R
- errno = 0;
- auto *v = ::getenv(cvarname.c_str());
-
- if (v != nullptr)
- return {std::string(v)};
-
- if (errno != 0)
- return std::unexpected(error(std::errc(errno)));
-
- return std::unexpected(error(std::errc::no_such_file_or_directory));
-#endif // NIHIL_HAVE_GETENV_R
-}
-
-} // namespace nihil
diff --git a/nihil.posix/getenv.ccm b/nihil.posix/getenv.ccm
index 465f7e7..5967bf7 100644
--- a/nihil.posix/getenv.ccm
+++ b/nihil.posix/getenv.ccm
@@ -1,11 +1,15 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
+#include <cerrno>
#include <expected>
#include <string>
+#include <system_error>
+#include <vector>
+
+#include <unistd.h>
+
+#include "nihil.hh"
export module nihil.posix:getenv;
@@ -13,11 +17,44 @@ import nihil.error;
namespace nihil {
-/*
- * Find a variable by the given name in the environment by calling getenv_r().
- */
-export [[nodiscard]] auto getenv(std::string_view varname)
- -> std::expected<std::string, error>;
+// Find a variable by the given name in the environment by calling getenv_r() if available,
+// or getenv() if not. In either case the value is copied, so will not be affected by
+// future calls to setenv().
+export [[nodiscard]] auto getenv(std::string_view varname) -> std::expected<std::string, error>
+{
+ auto cvarname = std::string(varname);
+
+#ifdef NIHIL_HAVE_GETENV_R
+ // Start with a buffer of this size, and double it every iteration.
+ constexpr auto bufinc = std::size_t{1024};
+
+ 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)));
+ }
+#else // NIHIL_HAVE_GETENV_R
+ errno = 0;
+ auto *v = ::getenv(cvarname.c_str()); // NOLINT
+
+ if (v != nullptr)
+ return {std::string(v)};
+
+ if (errno != 0)
+ return std::unexpected(error(std::errc(errno)));
+
+ return std::unexpected(error(std::errc::no_such_file_or_directory));
+#endif // NIHIL_HAVE_GETENV_R
+}
} // namespace nihil
diff --git a/nihil.posix/test.getenv.cc b/nihil.posix/getenv.test.cc
index 9e10c16..9e10c16 100644
--- a/nihil.posix/test.getenv.cc
+++ b/nihil.posix/getenv.test.cc
diff --git a/nihil.posix/open.cc b/nihil.posix/open.cc
deleted file mode 100644
index 9ef6538..0000000
--- a/nihil.posix/open.cc
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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
index eaedacd..59f80af 100644
--- a/nihil.posix/open.ccm
+++ b/nihil.posix/open.ccm
@@ -1,24 +1,87 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
#include <expected>
#include <filesystem>
+#include <fcntl.h>
+#include <unistd.h>
+
export module nihil.posix:open;
import nihil.error;
+import nihil.flagset;
import :fd;
-export namespace nihil {
+namespace nihil {
+
+struct open_flags_tag
+{
+};
+export using open_flags = nihil::flagset<int, open_flags_tag>;
+
+export inline constexpr auto open_none = open_flags();
+
+// Basic flags, exactly one of these is required.
+export inline constexpr auto open_read = open_flags::mask<O_RDONLY>();
+export inline constexpr auto open_write = open_flags::mask<O_WRONLY>();
+export inline constexpr auto open_readwrite = open_flags::mask<O_RDWR>();
+export inline constexpr auto open_search = open_flags::mask<O_SEARCH>();
+export inline constexpr auto open_exec = open_flags::mask<O_EXEC>();
+
+// Modifiers
+export inline constexpr auto open_nonblock = open_flags::mask<O_NONBLOCK>();
+export inline constexpr auto open_append = open_flags::mask<O_APPEND>();
+export inline constexpr auto open_create = open_flags::mask<O_CREAT>();
+export inline constexpr auto open_truncate = open_flags::mask<O_TRUNC>();
+export inline constexpr auto open_exclusive = open_flags::mask<O_EXCL>();
+export inline constexpr auto open_shared_lock = open_flags::mask<O_SHLOCK>();
+export inline constexpr auto open_exclusive_lock = open_flags::mask<O_EXLOCK>();
+export inline constexpr auto open_directory = open_flags::mask<O_DIRECTORY>();
+export inline constexpr auto open_nofollow = open_flags::mask<O_NOFOLLOW>();
+export inline constexpr auto open_nofollow_any = open_flags::mask<O_NOFOLLOW_ANY>();
+export inline constexpr auto open_symlink = open_flags::mask<O_SYMLINK>();
+export inline constexpr auto open_eventonly = open_flags::mask<O_EVTONLY>();
+export inline constexpr auto open_close_on_exec = open_flags::mask<O_CLOEXEC>();
+export inline constexpr auto open_resolve_beneath = open_flags::mask<O_RESOLVE_BENEATH>();
+
+// FreeBSD
+#ifdef O_DIRECT
+export inline constexpr auto open_direct = open_flags::mask<O_DIRECT>();
+#endif
+
+#ifdef O_VERIFY
+export inline constexpr auto open_verify = open_flags::mask<O_VERIFY>();
+#endif
+
+#ifdef O_PATH
+export inline constexpr auto open_path = open_flags::mask<O_PATH>();
+#endif
+
+#ifdef O_EMPTY_PATH
+export inline constexpr auto open_empty_path = open_flags::mask<O_EMPTY_PATH>();
+#endif
+
+// Open the given file and return an fd for it.
+export [[nodiscard]] auto open(std::filesystem::path const &filename, open_flags flags,
+ int mode = 0777) -> std::expected<fd, error>
+{
+ auto fdno = ::open(filename.c_str(), flags.value(), mode);
+ if (fdno != -1)
+ return fd(fdno);
+
+ return std::unexpected(error(std::errc(errno)));
+}
+
+// Like open(), but resolve relative to an open file descriptor, which must refer to a directory.
+export [[nodiscard]] auto openat(fd &where, std::filesystem::path const &filename, open_flags flags,
+ int mode = 0777) -> std::expected<fd, error>
+{
+ auto fdno = ::openat(where.get(), filename.c_str(), flags.value(), mode);
+ if (fdno != -1)
+ return fd(fdno);
-/*
- * 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>;
+ return std::unexpected(error(std::errc(errno)));
+}
} // namespace nihil
diff --git a/nihil.posix/open_in_path.cc b/nihil.posix/open_in_path.cc
deleted file mode 100644
index 30021ca..0000000
--- a/nihil.posix/open_in_path.cc
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 open_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/open_in_path.ccm b/nihil.posix/open_in_path.ccm
index d4c090d..7ff5812 100644
--- a/nihil.posix/open_in_path.ccm
+++ b/nihil.posix/open_in_path.ccm
@@ -1,23 +1,51 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
#include <filesystem>
#include <optional>
+#include <ranges>
+#include <string>
+
+#include <paths.h>
export module nihil.posix:open_in_path;
import nihil.error;
import :fd;
+import :getenv;
+import :open;
namespace nihil {
-/*
- * Find an executable in $PATH and open it with O_EXEC. If $PATH is not set, uses _PATH_DEFPATH.
- * If the file can't be found or opened, returns std::nullopt.
- */
-export [[nodiscard]] auto open_in_path(std::filesystem::path const &file) -> std::optional<fd>;
+// Find an executable in $PATH and open it with O_EXEC. If $PATH is not set, uses _PATH_DEFPATH.
+// If the file can't be found or opened, returns std::nullopt.
+export [[nodiscard]] auto open_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, open_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); // NOLINT
+
+ 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/posix.ccm b/nihil.posix/posix.ccm
index 2ebc1b8..3e13d5a 100644
--- a/nihil.posix/posix.ccm
+++ b/nihil.posix/posix.ccm
@@ -1,28 +1,20 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
-#include <expected>
-#include <filesystem>
-#include <optional>
-#include <string>
-
-#include "nihil.hh"
-
export module nihil.posix;
import nihil.error;
export import :argv;
export import :ensure_dir;
-export import :exec;
+export import :execl;
export import :execlp;
+export import :execshell;
export import :execv;
export import :execvp;
export import :fd;
export import :fexecv;
+export import :fexecvp;
export import :find_in_path;
export import :getenv;
export import :open;
diff --git a/nihil.posix/process.cc b/nihil.posix/process.cc
deleted file mode 100644
index 02642bc..0000000
--- a/nihil.posix/process.cc
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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, 0);
- 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
index 425deac..ee7de15 100644
--- a/nihil.posix/process.ccm
+++ b/nihil.posix/process.ccm
@@ -1,7 +1,4 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
#include <expected>
@@ -9,7 +6,7 @@ module;
#include <system_error>
#include <utility>
-#include <sys/types.h>
+#include <sys/wait.h>
export module nihil.posix:process;
@@ -17,72 +14,119 @@ import nihil.error;
namespace nihil {
-/*
- * wait_result: the exit status of a process.
- */
-export struct wait_result final {
+// 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);
+ [[nodiscard]] auto okay(this wait_result const &self) -> bool
+ {
+ return self.status() == 0;
+ }
+
+ [[nodiscard]] explicit operator bool(this wait_result const &self)
+ {
+ return self.okay();
+ }
// Return the exit status, if any.
- [[nodiscard]] auto status(this wait_result const &self)
- -> std::optional<int>;
+ [[nodiscard]] auto status(this wait_result const &self) -> std::optional<int>
+ {
+ if (WIFEXITED(self.m_status))
+ return WEXITSTATUS(self.m_status);
+ return {};
+ }
// Return the exit signal, if any.
- [[nodiscard]] auto signal(this wait_result const &self)
- -> std::optional<int>;
+ [[nodiscard]] auto signal(this wait_result const &self) -> std::optional<int>
+ {
+ if (WIFSIGNALED(self.m_status))
+ return WTERMSIG(self.m_status);
+ return {};
+ }
private:
friend struct process;
- int _status;
+ int m_status;
// Construct a new wait_result from the output of waitpid().
- wait_result(int status);
+ explicit wait_result(int status)
+ : m_status(status)
+ {
+ }
};
-/*
- * process: represents a process we created, which can be waited for.
- */
-export struct process final {
+// 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);
+ // Create a new process from a pid, which must be a child of the
+ // current process.
+ explicit process(::pid_t pid)
+ : m_pid(pid)
+ {
+ }
// When destroyed, we automatically wait for the process to
// avoid creating zombie processes.
- ~process();
+ ~process()
+ {
+ if (m_pid == -1)
+ return;
+
+ auto status = int{};
+ std::ignore = ::waitpid(m_pid, &status, WEXITED);
+ }
// Movable.
- process(process &&) noexcept;
- auto operator=(this process &, process &&) noexcept -> process &;
+ process(process &&other) noexcept
+ : m_pid(std::exchange(other.m_pid, -1))
+ {}
+
+ auto operator=(this process &self, process &&other) noexcept -> process &
+ {
+ if (&self != &other) {
+ self.m_pid = std::exchange(other.m_pid, -1);
+ }
+
+ return self; // NOLINT
+ }
// 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;
+ [[nodiscard]] auto pid(this process const &self) noexcept -> ::pid_t
+ {
+ return self.m_pid;
+ }
+
+ // 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) // NOLINT
+ -> std::expected<wait_result, error>
+ {
+ auto status = int{};
+ auto ret = waitpid(self.m_pid, &status, 0);
+ if (ret == -1)
+ return std::unexpected(error(std::errc(errno)));
+
+ return wait_result(status);
+ }
+
+ // Release this process so we won't try to wait for it when
+ // destroying this object. This will leave a zombie process
+ // unless the wait is done another way.
+ [[nodiscard]] auto release(this process &&self) -> ::pid_t // NOLINT
+ {
+ auto const ret = self.pid();
+ self.m_pid = -1;
+ return ret;
+ }
private:
::pid_t m_pid;
diff --git a/nihil.posix/read_file.ccm b/nihil.posix/read_file.ccm
index be9e102..3b4fd5b 100644
--- a/nihil.posix/read_file.ccm
+++ b/nihil.posix/read_file.ccm
@@ -32,7 +32,7 @@ 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 file = co_await open(filename, open_read);
auto constexpr bufsize = std::size_t{1024};
auto buffer = std::array<char, bufsize>{};
diff --git a/nihil.posix/rename.cc b/nihil.posix/rename.cc
deleted file mode 100644
index 9203d08..0000000
--- a/nihil.posix/rename.cc
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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
index 796ec5b..a1b292e 100644
--- a/nihil.posix/rename.ccm
+++ b/nihil.posix/rename.ccm
@@ -1,7 +1,4 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
#include <expected>
@@ -13,11 +10,19 @@ import nihil.error;
namespace nihil {
-/*
- * Rename a file (or directory).
- */
+// Rename a file (or directory).
export [[nodiscard]] auto
rename(std::filesystem::path const &oldp, std::filesystem::path const &newp)
- -> std::expected<void, error>;
+ -> 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/spawn.ccm b/nihil.posix/spawn.ccm
index 9fa24e3..a185bb3 100644
--- a/nihil.posix/spawn.ccm
+++ b/nihil.posix/spawn.ccm
@@ -1,7 +1,4 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
/*
@@ -29,7 +26,7 @@ export module nihil.posix:spawn;
import nihil.monad;
import :argv;
-import :exec;
+import :executor;
import :open;
import :process;
@@ -152,7 +149,7 @@ private:
export [[nodiscard]] auto
make_fd_file(int fdno, std::filesystem::path const &file,
- int flags, int mode = 0777)
+ open_flags flags, int mode = 0777)
-> std::expected<fd_file, error>
{
auto fd = co_await open(file, flags, mode);
@@ -166,19 +163,19 @@ make_fd_file(int fdno, std::filesystem::path const &file,
export [[nodiscard]] inline auto
stdin_devnull() -> std::expected<fd_file, error>
{
- return make_fd_file(stdin_fileno, "/dev/null", O_RDONLY);
+ return make_fd_file(stdin_fileno, "/dev/null", open_read);
}
export [[nodiscard]] inline auto
stdout_devnull() -> std::expected<fd_file, error>
{
- return make_fd_file(stdout_fileno, "/dev/null", O_WRONLY);
+ return make_fd_file(stdout_fileno, "/dev/null", open_write);
}
export [[nodiscard]] inline auto
stderr_devnull() -> std::expected<fd_file, error>
{
- return make_fd_file(stderr_fileno, "/dev/null", O_WRONLY);
+ return make_fd_file(stderr_fileno, "/dev/null", open_write);
}
/*
diff --git a/nihil.posix/tempfile.cc b/nihil.posix/tempfile.cc
deleted file mode 100644
index b1d3dee..0000000
--- a/nihil.posix/tempfile.cc
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * This source code is released into the public domain.
- */
-
-module;
-
-#include <algorithm>
-#include <coroutine>
-#include <expected>
-#include <filesystem>
-#include <random>
-#include <string>
-
-#include <fcntl.h>
-#include <unistd.h>
-
-module nihil.posix;
-
-import nihil.flagset;
-import :getenv;
-import :open;
-
-namespace nihil {
-
-temporary_file::temporary_file(nihil::fd &&fd,
- std::filesystem::path path) noexcept
- : m_fd(std::move(fd))
- , m_path(std::move(path))
-{
-}
-
-temporary_file::temporary_file(nihil::fd &&fd) noexcept
- : m_fd(std::move(fd))
-{
-}
-
-temporary_file::temporary_file(temporary_file &&other) noexcept
- : m_fd(std::move(other.m_fd))
- , m_path(std::move(other.m_path))
-{
-}
-
-temporary_file::~temporary_file() //NOLINT(bugprone-exception-escape)
-{
- if (m_fd)
- release();
-}
-
-auto temporary_file::release(this temporary_file &self) -> void
-{
- if (!self.m_fd)
- throw std::logic_error(
- "release() called on already-released tempfile");
-
- if (!self.m_path.empty()) {
- auto ec = std::error_code(); // ignore errors
- remove(self.path(), ec);
-
- self.m_path.clear();
- }
-
- std::ignore = self.m_fd.close();
-}
-
-auto temporary_file::path(this temporary_file const &self)
- -> std::filesystem::path const &
-{
- if (self.m_path.empty())
- throw std::logic_error(
- "path() called on unlinked temporary_file");
-
- return self.m_path;
-}
-
-auto temporary_file::fd(this temporary_file &self) -> nihil::fd &
-{
- if (!self.m_fd)
- throw std::logic_error("fd() called on empty temporary_file");
-
- return self.m_fd;
-}
-
-auto tempfile(tempfile_flags_t flags) -> std::expected<temporary_file, error>
-{
- auto rng = std::default_random_engine(std::random_device{}());
-
- auto random_name = [&] -> std::string {
- auto constexpr length = std::size_t{12};
- auto constexpr randchars = std::string_view(
- "abcdefghijklmnopqrstuvwxyz"
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- "0123456789");
-
- auto dist = std::uniform_int_distribution<>(
- 0, randchars.size() - 1);
- auto ret = std::string(length, 0);
- std::ranges::generate_n(ret.begin(), length,
- [&] -> char {
- return randchars[dist(rng)];
- });
- return ret;
- };
-
- auto dir = std::filesystem::path(getenv("TMPDIR").value_or("/tmp"));
-
- // Keep trying until we don't get EEXIST.
- for (;;) {
- auto filename = dir / (random_name() + ".tmp");
- auto fd = nihil::open(filename, O_RDWR | O_CREAT | O_EXCL,
- 0600);
- if (!fd) {
- if (fd.error() == std::errc::file_exists)
- continue;
- return std::unexpected(fd.error());
- }
-
- if (flags & tempfile_unlink) {
- auto ec = std::error_code();
- remove(filename, ec);
- return temporary_file(std::move(*fd));
- } else {
- return temporary_file(std::move(*fd),
- std::move(filename));
- }
- }
-}
-
-} // namespace nihil
diff --git a/nihil.posix/tempfile.ccm b/nihil.posix/tempfile.ccm
index 82f3be4..e1510e5 100644
--- a/nihil.posix/tempfile.ccm
+++ b/nihil.posix/tempfile.ccm
@@ -1,16 +1,13 @@
-/*
- * This source code is released into the public domain.
- */
-
+// This source code is released into the public domain.
module;
-/*
- * tempfile: create a temporary file.
- */
+// tempfile: create a temporary file.
+#include <algorithm>
#include <cstdint>
#include <expected>
#include <filesystem>
+#include <random>
#include <string>
export module nihil.posix:tempfile;
@@ -18,70 +15,139 @@ export module nihil.posix:tempfile;
import nihil.error;
import nihil.flagset;
import :fd;
+import :getenv;
+import :open;
namespace nihil {
-struct tempfile_flags_tag {};
-export using tempfile_flags_t = flagset<std::uint8_t, tempfile_flags_tag>;
+struct tempfile_flags_tag
+{
+};
+export using tempfile_flags = flagset<std::uint8_t, tempfile_flags_tag>;
// No flags.
-export inline constexpr auto tempfile_none = tempfile_flags_t();
+export inline constexpr auto tempfile_none = tempfile_flags();
// Unlink the tempfile immediately after creating it
-export inline constexpr auto tempfile_unlink = tempfile_flags_t::bit<0>();
-
-export struct temporary_file final {
- /*
- * Fetch the file's fd.
- */
- [[nodiscard]] auto fd(this temporary_file &) -> nihil::fd &;
-
- /*
- * Fetch the name of this file. If tempfile_unlink was specified,
- * throws std::logic_error.
- */
- [[nodiscard]] auto path(this temporary_file const &)
- -> std::filesystem::path const &;
-
- /*
- * Release this temporary file, causing it to be deleted immediately.
- * Throws std::logic_error if the file has already been released.
- */
- auto release(this temporary_file &) -> void;
-
- /*
- * Destructor; unlink the file if we didn't already.
- */
- ~temporary_file();
+export inline constexpr auto tempfile_unlink = tempfile_flags::bit<0>();
+
+export struct temporary_file final
+{
+ // Fetch the file's fd.
+ [[nodiscard]] auto fd(this temporary_file &self) -> nihil::fd &
+ {
+ if (!self.m_fd)
+ throw std::logic_error("fd() called on empty temporary_file");
+
+ return self.m_fd;
+ }
+
+ // Fetch the name of this file. If tempfile_unlink was specified,
+ // throws std::logic_error.
+ [[nodiscard]] auto path(this temporary_file const &self) -> std::filesystem::path const &
+ {
+ if (self.m_path.empty())
+ throw std::logic_error("path() called on unlinked temporary_file");
+
+ return self.m_path;
+ }
+
+ // Release this temporary file, causing it to be closed and deleted immediately
+ // Throws std::logic_error if the file has already been released.
+ auto release(this temporary_file &self) -> void
+ {
+ if (!self.m_fd)
+ throw std::logic_error("release() called on already-released tempfile");
+
+ if (!self.m_path.empty()) {
+ auto ec = std::error_code(); // ignore errors
+ remove(self.path(), ec);
+
+ self.m_path.clear();
+ }
+
+ std::ignore = self.m_fd.close();
+ }
+
+ // Destructor; unlink the file if we didn't already.
+ ~temporary_file() // NOLINT
+ {
+ if (m_fd)
+ release();
+ }
// Not copyable.
temporary_file(temporary_file const &) = delete;
// Movable.
- temporary_file(temporary_file &&other) noexcept;
+ temporary_file(temporary_file &&other) noexcept = default;
// Not assignable.
- auto operator=(this temporary_file &, temporary_file const &)
- -> temporary_file & = delete;
- auto operator=(this temporary_file &, temporary_file &&) noexcept
- -> temporary_file & = delete;
+ auto operator=(this temporary_file &, temporary_file const &) -> temporary_file & = delete;
+ auto
+ operator=(this temporary_file &, temporary_file &&) noexcept -> temporary_file & = delete;
private:
// The file descriptor for the file.
- nihil::fd m_fd;
- std::filesystem::path m_path;
+ nihil::fd m_fd;
+ std::filesystem::path m_path;
+
+ temporary_file(nihil::fd &&fd, std::filesystem::path path) noexcept
+ : m_fd(std::move(fd))
+ , m_path(std::move(path))
+ {
+ }
- temporary_file(nihil::fd &&fd, std::filesystem::path) noexcept;
- temporary_file(nihil::fd &&fd) noexcept;
+ explicit temporary_file(nihil::fd &&fd) noexcept
+ : m_fd(std::move(fd))
+ {
+ }
- friend auto tempfile(tempfile_flags_t flags)
- -> std::expected<temporary_file, error>;
+ friend auto tempfile(tempfile_flags flags) -> std::expected<temporary_file, error>;
};
/*
* Create a temporary file and return it.
*/
-export [[nodiscard]] auto tempfile(tempfile_flags_t flags = tempfile_none)
- -> std::expected<temporary_file, error>;
+export [[nodiscard]] auto
+tempfile(tempfile_flags flags = tempfile_none) -> std::expected<temporary_file, error>
+{
+ auto rng = std::default_random_engine(std::random_device{}());
+
+ auto random_name = [&] -> std::string {
+ auto constexpr length = std::size_t{12};
+ auto constexpr randchars = std::string_view("abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789");
+
+ auto dist = std::uniform_int_distribution<>(0, randchars.size() - 1);
+ auto ret = std::string(length, 0);
+ std::ranges::generate_n(ret.begin(), length,
+ [&] -> char { return randchars[dist(rng)]; });
+ return ret;
+ };
+
+ auto dir = std::filesystem::path(getenv("TMPDIR").value_or("/tmp")); // NOLINT
+
+ // Keep trying until we don't get EEXIST.
+ for (;;) {
+ auto filename = dir / (random_name() + ".tmp");
+ auto fd =
+ nihil::open(filename, open_readwrite | open_create | open_exclusive, 0600);
+ if (!fd) {
+ if (fd.error() == std::errc::file_exists)
+ continue;
+ return std::unexpected(fd.error());
+ }
+
+ if (flags & tempfile_unlink) {
+ auto ec = std::error_code();
+ remove(filename, ec);
+ return temporary_file(std::move(*fd));
+ }
+
+ return temporary_file(std::move(*fd), std::move(filename));
+ }
+}
} // namespace nihil
diff --git a/nihil.posix/test.tempfile.cc b/nihil.posix/tempfile.test.cc
index b1c7604..b1c7604 100644
--- a/nihil.posix/test.tempfile.cc
+++ b/nihil.posix/tempfile.test.cc
diff --git a/nihil.posix/test.spawn.cc b/nihil.posix/test.spawn.cc
index ca6c076..c5b4f53 100644
--- a/nihil.posix/test.spawn.cc
+++ b/nihil.posix/test.spawn.cc
@@ -6,27 +6,6 @@
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;
@@ -69,25 +48,6 @@ TEST_CASE("spawn: execvp", "[spawn]") {
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;
diff --git a/nihil.posix/write_file.ccm b/nihil.posix/write_file.ccm
index 867e0db..ce21e6b 100644
--- a/nihil.posix/write_file.ccm
+++ b/nihil.posix/write_file.ccm
@@ -35,7 +35,7 @@ auto write_file(std::filesystem::path const &filename,
int mode = 0777)
-> std::expected<std::size_t, error>
{
- auto file = co_await open(filename, O_CREAT|O_WRONLY, mode);
+ auto file = co_await open(filename, open_write | open_create, mode);
auto nbytes = co_await write(file, range);
co_return nbytes;
}