diff --git a/Roadmap.md b/Roadmap.md index 3b44dc3..0c6c507 100644 --- a/Roadmap.md +++ b/Roadmap.md @@ -2,7 +2,7 @@ ## Context -CFBox 是一个 C++23 BusyBox 替代品,当前版本有 33 个 applet。项目使用注册表分发模式(`APPLET_REGISTRY`)、`std::expected` 错误处理、自定义参数解析器,CI 覆盖原生构建、交叉编译和 QEMU 测试。 +CFBox 是一个 C++23 BusyBox 替代品,当前版本有 78 个 applet。项目使用注册表分发模式(`APPLET_REGISTRY`)、`std::expected` 错误处理、自定义参数解析器,CI 覆盖原生构建、交叉编译和 QEMU 测试。 **目标**:全面对齐 BusyBox,覆盖嵌入式、容器、救援和通用场景。Shell 是最关键的组件,必须最先实现。 @@ -16,12 +16,14 @@ CFBox 是一个 C++23 BusyBox 替代品,当前版本有 33 个 applet。项目 |------|------|------------|-------------|------| | 0 | 构建系统现代化 ✅ | 0 | CMake 配置、help 系统、UTF-8、彩色输出 | 17 | | 1 | POSIX Shell + Coreutils I ✅ | ~17 | Shell 引擎、进程管理、信号处理 | ~34 | -| 2 | Coreutils II + findutils | ~41 | 流处理管线、校验和框架 | ~75 | -| 3 | 编辑器 + 归档 + 压缩 | ~15 | 终端抽象、压缩框架 | ~90 | -| 4 | 进程/Init + util-linux | ~38 | /proc 解析器、TUI 框架 | ~128 | -| 5 | 网络 + 登录 + 日志 | ~35 | Socket 抽象、HTTP 解析、shadow 密码 | ~163 | +| 2 | Coreutils II + findutils ✅ | ~44 | 流处理管线、校验和框架 | ~78 | +| 3 | 编辑器 + 归档 + 压缩 | ~15 | 终端抽象、压缩框架 | ~93 | +| 4 | 进程/Init + util-linux | ~38 | /proc 解析器、TUI 框架 | ~131 | +| 5 | 网络 + 登录 + 日志 | ~35 | Socket 抽象、HTTP 解析、shadow 密码 | ~166 | | 6 | 剩余组件 + 集成验证 | ~40+ | POSIX 验证、容器替换测试 | ~200+ | +**当前状态**:Phase 0-2 已完成,78 个 applet,246 单元测试 + 49 集成测试全部通过。 + --- ## Phase 0:构建系统现代化 ✅ @@ -85,27 +87,37 @@ Shell 已实现为第一个多文件 applet(`src/applets/sh/`,8 个模块, --- -## Phase 2:Coreutils 第二批 + findutils/xargs +## Phase 2:Coreutils 第二批 + findutils/xargs ✅ **目标**:完成剩余 coreutils,添加 xargs,建立流处理管线基础设施。 -### Coreutils 第二批(中等复杂度,100-400 行/个) +### 基础设施 ✅ -**文本处理**:`date`, `env`, `printenv`, `seq`, `tee`, `touch`, `tr`, `fold`, `expand`, `nl`, `paste`, `cut`, `comm`, `cksum`, `md5sum`, `sum`, `shred`, `shuf`, `factor`, `tac`, `od`, `split` +1. **流管线** `include/cfbox/stream.hpp` ✅:`for_each_line()`、`split_fields()`、`split_whitespace()`、`LineProcessor` 虚基类 + `run_processor()`。 -**系统/文件**:`timeout`, `nice`, `nohup`, `expr`, `hostid`, `install`, `readlink`, `realpath`, `rmdir`, `unlink`, `truncate`, `tsort`, `ln`, `mktemp`, `mkfifo`, `mknod`, `du`, `df`, `stat`, `sync`, `usleep`, `who` +2. **校验和** `include/cfbox/checksum.hpp` ✅:CRC-32(POSIX)、MD5(RFC 1321 完整实现)、BSD/SysV `sum`。 -### findutils -- **xargs**:从 stdin 构建参数列表执行命令,支持 `-n`, `-I`, `-0`, `-p`, `-r` +3. **fs_util.hpp 扩展** ✅:`create_symlink()`、`read_symlink()`、`canonical()`、`resize_file()`、`space()`。 -### 基础设施 -- **流管线** `include/cfbox/stream.hpp`:逐行处理、字段分割 -- **校验和** `include/cfbox/checksum.hpp`:MD5, SHA-256 实现 +### Coreutils 第二批 ✅(44 个 applet) -### 验证 -- `find . -name "*.cpp" | xargs grep "main"` 端到端工作 -- `date`, `seq 1 10`, `tr a-z A-Z` 与 GNU 行为一致 -- 所有新 applet 通过单元 + 集成测试 +**文本处理** ✅:`date`, `env`, `printenv`, `seq`, `tee`, `touch`, `tr`, `fold`, `expand`, `nl`, `paste`, `cut`, `comm`, `cksum`, `md5sum`, `sum`, `shuf`, `factor`, `tac`, `od`, `split` + +**系统/文件** ✅:`timeout`, `nice`, `nohup`, `expr`, `hostid`, `install`, `readlink`, `realpath`, `rmdir`, `unlink`, `truncate`, `tsort`, `ln`, `mktemp`, `mkfifo`, `mknod`, `du`, `df`, `stat`, `sync`, `usleep`, `who` + +### findutils ✅ +- **xargs** ✅:从 stdin 构建参数列表执行命令,支持 `-n`, `-I`, `-0`, `-r`, `-t` + +### 验证 ✅ +- `echo -e "main.cpp\nCMakeLists.txt" | cfbox xargs -n1 echo` 端到端工作 ✅ +- `cfbox date +%Y`、`cfbox seq 3`、`echo hello | cfbox tr a-z A-Z` 行为正确 ✅ +- `cfbox factor 42` → `42: 2 3 7` ✅ +- `cfbox expr 2 + 3` → `5` ✅ +- `echo "a:b:c" | cfbox cut -d: -f2` → `b` ✅ +- `echo -n "test" | cfbox md5sum -` → `098f6bcd4621d373cade4e832627b4f6` ✅ +- 所有新 applet 通过 --help / --version ✅ +- **246 单元测试** 全部通过 ✅ +- **49 集成测试** 全部通过 ✅ --- @@ -263,8 +275,11 @@ Shell 已实现为第一个多文件 applet(`src/applets/sh/`,8 个模块, ## 关键文件 -- `include/cfbox/applets.hpp`——注册表从 17 增长到 200+ +- `include/cfbox/applets.hpp`——注册表从 17 增长到 78(目标 200+) - `include/cfbox/args.hpp`——扩展长选项支持 - `include/cfbox/error.hpp`——所有 applet 的错误处理基础 +- `include/cfbox/stream.hpp`——流处理管线(逐行处理、字段分割) +- `include/cfbox/checksum.hpp`——校验和框架(CRC-32、MD5、BSD/SysV sum) +- `include/cfbox/fs_util.hpp`——文件系统工具(symlink、canonical、resize 等) - `CMakeLists.txt`——从简单构建演进为配置化选择 - `src/applets/init.cpp`——从简单 init 扩展为完整 init 系统 diff --git a/cmake/Config.cmake b/cmake/Config.cmake index 34a712d..a0c2b75 100644 --- a/cmake/Config.cmake +++ b/cmake/Config.cmake @@ -11,6 +11,16 @@ set(CFBOX_APPLETS hostname logname whoami tty sleep id test sh + printenv hostid sync usleep rmdir unlink who env + readlink realpath touch truncate stat install mktemp + ln mkfifo mknod du + seq tee tac fold expand + cut paste nl comm tr + cksum md5sum sum + date od split shuf factor + timeout nice nohup df + expr tsort + xargs ) foreach(applet IN LISTS CFBOX_APPLETS) diff --git a/include/cfbox/applet_config.hpp.in b/include/cfbox/applet_config.hpp.in index 85fa818..8b57937 100644 --- a/include/cfbox/applet_config.hpp.in +++ b/include/cfbox/applet_config.hpp.in @@ -38,3 +38,47 @@ #cmakedefine01 CFBOX_ENABLE_ID #cmakedefine01 CFBOX_ENABLE_TEST #cmakedefine01 CFBOX_ENABLE_SH +#cmakedefine01 CFBOX_ENABLE_PRINTENV +#cmakedefine01 CFBOX_ENABLE_HOSTID +#cmakedefine01 CFBOX_ENABLE_SYNC +#cmakedefine01 CFBOX_ENABLE_USLEEP +#cmakedefine01 CFBOX_ENABLE_RMDIR +#cmakedefine01 CFBOX_ENABLE_UNLINK +#cmakedefine01 CFBOX_ENABLE_WHO +#cmakedefine01 CFBOX_ENABLE_ENV +#cmakedefine01 CFBOX_ENABLE_READLINK +#cmakedefine01 CFBOX_ENABLE_REALPATH +#cmakedefine01 CFBOX_ENABLE_TOUCH +#cmakedefine01 CFBOX_ENABLE_TRUNCATE +#cmakedefine01 CFBOX_ENABLE_STAT +#cmakedefine01 CFBOX_ENABLE_INSTALL +#cmakedefine01 CFBOX_ENABLE_MKTEMP +#cmakedefine01 CFBOX_ENABLE_LN +#cmakedefine01 CFBOX_ENABLE_MKFIFO +#cmakedefine01 CFBOX_ENABLE_MKNOD +#cmakedefine01 CFBOX_ENABLE_DU +#cmakedefine01 CFBOX_ENABLE_SEQ +#cmakedefine01 CFBOX_ENABLE_TEE +#cmakedefine01 CFBOX_ENABLE_TAC +#cmakedefine01 CFBOX_ENABLE_FOLD +#cmakedefine01 CFBOX_ENABLE_EXPAND +#cmakedefine01 CFBOX_ENABLE_CUT +#cmakedefine01 CFBOX_ENABLE_PASTE +#cmakedefine01 CFBOX_ENABLE_NL +#cmakedefine01 CFBOX_ENABLE_COMM +#cmakedefine01 CFBOX_ENABLE_TR +#cmakedefine01 CFBOX_ENABLE_CKSUM +#cmakedefine01 CFBOX_ENABLE_MD5SUM +#cmakedefine01 CFBOX_ENABLE_SUM +#cmakedefine01 CFBOX_ENABLE_DATE +#cmakedefine01 CFBOX_ENABLE_OD +#cmakedefine01 CFBOX_ENABLE_SPLIT +#cmakedefine01 CFBOX_ENABLE_SHUF +#cmakedefine01 CFBOX_ENABLE_FACTOR +#cmakedefine01 CFBOX_ENABLE_TIMEOUT +#cmakedefine01 CFBOX_ENABLE_NICE +#cmakedefine01 CFBOX_ENABLE_NOHUP +#cmakedefine01 CFBOX_ENABLE_DF +#cmakedefine01 CFBOX_ENABLE_EXPR +#cmakedefine01 CFBOX_ENABLE_TSORT +#cmakedefine01 CFBOX_ENABLE_XARGS diff --git a/include/cfbox/applets.hpp b/include/cfbox/applets.hpp index a8a268a..274c11c 100644 --- a/include/cfbox/applets.hpp +++ b/include/cfbox/applets.hpp @@ -106,6 +106,138 @@ extern auto test_main(int argc, char* argv[]) -> int; #if CFBOX_ENABLE_SH extern auto sh_main(int argc, char* argv[]) -> int; #endif +#if CFBOX_ENABLE_PRINTENV +extern auto printenv_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_HOSTID +extern auto hostid_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_SYNC +extern auto sync_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_USLEEP +extern auto usleep_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_RMDIR +extern auto rmdir_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_UNLINK +extern auto unlink_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_WHO +extern auto who_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_ENV +extern auto env_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_READLINK +extern auto readlink_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_REALPATH +extern auto realpath_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_TOUCH +extern auto touch_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_TRUNCATE +extern auto truncate_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_STAT +extern auto stat_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_INSTALL +extern auto install_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_MKTEMP +extern auto mktemp_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_LN +extern auto ln_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_MKFIFO +extern auto mkfifo_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_MKNOD +extern auto mknod_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_DU +extern auto du_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_SEQ +extern auto seq_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_TEE +extern auto tee_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_TAC +extern auto tac_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_FOLD +extern auto fold_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_EXPAND +extern auto expand_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_CUT +extern auto cut_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_PASTE +extern auto paste_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_NL +extern auto nl_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_COMM +extern auto comm_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_TR +extern auto tr_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_CKSUM +extern auto cksum_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_MD5SUM +extern auto md5sum_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_SUM +extern auto sum_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_DATE +extern auto date_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_OD +extern auto od_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_SPLIT +extern auto split_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_SHUF +extern auto shuf_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_FACTOR +extern auto factor_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_TIMEOUT +extern auto timeout_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_NICE +extern auto nice_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_NOHUP +extern auto nohup_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_DF +extern auto df_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_EXPR +extern auto expr_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_TSORT +extern auto tsort_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_XARGS +extern auto xargs_main(int argc, char* argv[]) -> int; +#endif // registry — one line per applet, conditionally compiled constexpr auto APPLET_REGISTRY = std::to_array({ @@ -212,4 +344,136 @@ constexpr auto APPLET_REGISTRY = std::to_array({ #if CFBOX_ENABLE_SH {"sh", sh_main, "POSIX shell command interpreter"}, #endif +#if CFBOX_ENABLE_PRINTENV + {"printenv", printenv_main, "print all or part of environment"}, +#endif +#if CFBOX_ENABLE_HOSTID + {"hostid", hostid_main, "print the numeric identifier for the current host"}, +#endif +#if CFBOX_ENABLE_SYNC + {"sync", sync_main, "synchronize cached writes to persistent storage"}, +#endif +#if CFBOX_ENABLE_USLEEP + {"usleep", usleep_main, "sleep for a specified number of microseconds"}, +#endif +#if CFBOX_ENABLE_RMDIR + {"rmdir", rmdir_main, "remove empty directories"}, +#endif +#if CFBOX_ENABLE_UNLINK + {"unlink", unlink_main, "call the unlink function to remove a file"}, +#endif +#if CFBOX_ENABLE_WHO + {"who", who_main, "show who is logged on"}, +#endif +#if CFBOX_ENABLE_ENV + {"env", env_main, "run a program in a modified environment"}, +#endif +#if CFBOX_ENABLE_READLINK + {"readlink", readlink_main, "print the value of a symbolic link"}, +#endif +#if CFBOX_ENABLE_REALPATH + {"realpath", realpath_main, "print the resolved path"}, +#endif +#if CFBOX_ENABLE_TOUCH + {"touch", touch_main, "change file timestamps"}, +#endif +#if CFBOX_ENABLE_TRUNCATE + {"truncate", truncate_main, "shrink or extend the size of a file"}, +#endif +#if CFBOX_ENABLE_STAT + {"stat", stat_main, "display file or file system status"}, +#endif +#if CFBOX_ENABLE_INSTALL + {"install", install_main, "copy files and set attributes"}, +#endif +#if CFBOX_ENABLE_MKTEMP + {"mktemp", mktemp_main, "create a temporary file or directory"}, +#endif +#if CFBOX_ENABLE_LN + {"ln", ln_main, "make links between files"}, +#endif +#if CFBOX_ENABLE_MKFIFO + {"mkfifo", mkfifo_main, "make FIFOs (named pipes)"}, +#endif +#if CFBOX_ENABLE_MKNOD + {"mknod", mknod_main, "make block or character special files"}, +#endif +#if CFBOX_ENABLE_DU + {"du", du_main, "estimate file space usage"}, +#endif +#if CFBOX_ENABLE_SEQ + {"seq", seq_main, "print a sequence of numbers"}, +#endif +#if CFBOX_ENABLE_TEE + {"tee", tee_main, "read from stdin and write to stdout and files"}, +#endif +#if CFBOX_ENABLE_TAC + {"tac", tac_main, "concatenate and print files in reverse"}, +#endif +#if CFBOX_ENABLE_FOLD + {"fold", fold_main, "wrap each input line to fit in specified width"}, +#endif +#if CFBOX_ENABLE_EXPAND + {"expand", expand_main, "convert tabs to spaces"}, +#endif +#if CFBOX_ENABLE_CUT + {"cut", cut_main, "remove sections from each line of files"}, +#endif +#if CFBOX_ENABLE_PASTE + {"paste", paste_main, "merge lines of files"}, +#endif +#if CFBOX_ENABLE_NL + {"nl", nl_main, "number lines of files"}, +#endif +#if CFBOX_ENABLE_COMM + {"comm", comm_main, "compare two sorted files line by line"}, +#endif +#if CFBOX_ENABLE_TR + {"tr", tr_main, "translate, squeeze, and/or delete characters"}, +#endif +#if CFBOX_ENABLE_CKSUM + {"cksum", cksum_main, "checksum and count the bytes in a file"}, +#endif +#if CFBOX_ENABLE_MD5SUM + {"md5sum", md5sum_main, "compute and check MD5 message digest"}, +#endif +#if CFBOX_ENABLE_SUM + {"sum", sum_main, "checksum and count the blocks in a file"}, +#endif +#if CFBOX_ENABLE_DATE + {"date", date_main, "print or set the system date and time"}, +#endif +#if CFBOX_ENABLE_OD + {"od", od_main, "dump files in octal and other formats"}, +#endif +#if CFBOX_ENABLE_SPLIT + {"split", split_main, "split a file into pieces"}, +#endif +#if CFBOX_ENABLE_SHUF + {"shuf", shuf_main, "generate random permutations"}, +#endif +#if CFBOX_ENABLE_FACTOR + {"factor", factor_main, "print the prime factors of numbers"}, +#endif +#if CFBOX_ENABLE_TIMEOUT + {"timeout", timeout_main, "run a command with a time limit"}, +#endif +#if CFBOX_ENABLE_NICE + {"nice", nice_main, "run a program with modified scheduling priority"}, +#endif +#if CFBOX_ENABLE_NOHUP + {"nohup", nohup_main, "run a command immune to hangups"}, +#endif +#if CFBOX_ENABLE_DF + {"df", df_main, "report file system disk space usage"}, +#endif +#if CFBOX_ENABLE_EXPR + {"expr", expr_main, "evaluate expressions"}, +#endif +#if CFBOX_ENABLE_TSORT + {"tsort", tsort_main, "perform topological sort"}, +#endif +#if CFBOX_ENABLE_XARGS + {"xargs", xargs_main, "build and execute command lines from stdin"}, +#endif }); diff --git a/include/cfbox/checksum.hpp b/include/cfbox/checksum.hpp new file mode 100644 index 0000000..fc7bf5c --- /dev/null +++ b/include/cfbox/checksum.hpp @@ -0,0 +1,165 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace cfbox::checksum { + +// CRC-32 using the POSIX polynomial (0xEDB88320 reflected) +inline auto crc32(std::string_view data) -> std::uint32_t { + std::uint32_t crc = 0xFFFFFFFF; + for (auto byte : data) { + crc ^= static_cast(byte); + for (int i = 0; i < 8; ++i) { + crc = (crc >> 1) ^ (0xEDB88320 & (-(crc & 1))); + } + } + return ~crc; +} + +struct MD5Hash { + std::array bytes{}; +}; + +inline auto md5_to_hex(const MD5Hash& hash) -> std::string { + static constexpr char hex[] = "0123456789abcdef"; + std::string result; + result.reserve(32); + for (auto b : hash.bytes) { + result += hex[b >> 4]; + result += hex[b & 0x0F]; + } + return result; +} + +inline auto md5(std::string_view data) -> MD5Hash { + MD5Hash result; + + static constexpr std::uint32_t K[64] = { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + }; + + static constexpr unsigned s[64] = { + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, + }; + + auto left_rotate = [](std::uint32_t x, unsigned c) -> std::uint32_t { + return (x << c) | (x >> (32 - c)); + }; + + // Padding + std::size_t orig_len = data.size(); + auto bit_len = static_cast(orig_len) * 8; + std::size_t padded_len = ((orig_len + 8) / 64 + 1) * 64; + std::vector msg(padded_len, 0); + std::memcpy(msg.data(), data.data(), orig_len); + msg[orig_len] = 0x80; + for (int j = 0; j < 8; ++j) { + msg[padded_len - 8 + j] = static_cast(bit_len >> (j * 8)); + } + + std::uint32_t a0 = 0x67452301; + std::uint32_t b0 = 0xEFCDAB89; + std::uint32_t c0 = 0x98BADCFE; + std::uint32_t d0 = 0x10325476; + + for (std::size_t offset = 0; offset < padded_len; offset += 64) { + std::uint32_t M[16]; + for (std::size_t j = 0; j < 16; ++j) { + auto base = offset + j * 4; + M[j] = static_cast(msg[base]) + | (static_cast(msg[base + 1]) << 8) + | (static_cast(msg[base + 2]) << 16) + | (static_cast(msg[base + 3]) << 24); + } + + std::uint32_t A = a0, B = b0, C = c0, D = d0; + + for (int j = 0; j < 64; ++j) { + std::uint32_t F; + int idx; + if (j < 16) { + F = (B & C) | (~B & D); + idx = j; + } else if (j < 32) { + F = (D & B) | (~D & C); + idx = (5 * j + 1) % 16; + } else if (j < 48) { + F = B ^ C ^ D; + idx = (3 * j + 5) % 16; + } else { + F = C ^ (B | ~D); + idx = (7 * j) % 16; + } + F = F + A + K[j] + M[idx]; + A = D; + D = C; + C = B; + B = B + left_rotate(F, s[j]); + } + + a0 += A; b0 += B; c0 += C; d0 += D; + } + + for (int j = 0; j < 4; ++j) { + result.bytes[j] = static_cast(a0 >> (j * 8)); + result.bytes[j + 4] = static_cast(b0 >> (j * 8)); + result.bytes[j + 8] = static_cast(c0 >> (j * 8)); + result.bytes[j + 12] = static_cast(d0 >> (j * 8)); + } + return result; +} + +struct SumResult { + std::uint16_t checksum; + unsigned blocks; +}; + +inline auto bsd_sum(std::string_view data) -> SumResult { + SumResult result{0, 0}; + for (auto byte : data) { + auto rot = static_cast(result.checksum >> 1) + + static_cast((result.checksum & 1) << 15); + result.checksum = static_cast(rot + static_cast(byte)); + } + result.blocks = (static_cast(data.size()) + 1023) / 1024; + if (result.blocks == 0) result.blocks = 1; + return result; +} + +inline auto sysv_sum(std::string_view data) -> SumResult { + SumResult result{0, 0}; + unsigned long s = 0; + for (auto byte : data) { + s += static_cast(byte); + } + auto val = static_cast((s & 0xFFFF) + ((s >> 16) & 0xFFFF)); + result.checksum = static_cast(val); + result.blocks = (static_cast(data.size()) + 511) / 512; + if (result.blocks == 0) result.blocks = 1; + return result; +} + +} // namespace cfbox::checksum diff --git a/include/cfbox/fs_util.hpp b/include/cfbox/fs_util.hpp index df1c999..8ba7c82 100644 --- a/include/cfbox/fs_util.hpp +++ b/include/cfbox/fs_util.hpp @@ -191,4 +191,52 @@ inline auto hard_link_count(std::string_view path) -> base::Result base::Result { + std::error_code ec; + std::filesystem::create_symlink( + std::filesystem::path{target}, + std::filesystem::path{link_path}, + ec); + if (ec) { + return std::unexpected(base::Error{static_cast(ec.value()), ec.message()}); + } + return {}; +} + +inline auto read_symlink(std::string_view path) -> base::Result { + std::error_code ec; + auto p = std::filesystem::read_symlink(std::filesystem::path{path}, ec); + if (ec) { + return std::unexpected(base::Error{static_cast(ec.value()), ec.message()}); + } + return p.string(); +} + +inline auto canonical(std::string_view path) -> base::Result { + std::error_code ec; + auto p = std::filesystem::canonical(std::filesystem::path{path}, ec); + if (ec) { + return std::unexpected(base::Error{static_cast(ec.value()), ec.message()}); + } + return p.string(); +} + +inline auto resize_file(std::string_view path, std::uintmax_t new_size) -> base::Result { + std::error_code ec; + std::filesystem::resize_file(std::filesystem::path{path}, new_size, ec); + if (ec) { + return std::unexpected(base::Error{static_cast(ec.value()), ec.message()}); + } + return {}; +} + +inline auto space(std::string_view path) -> base::Result { + std::error_code ec; + auto info = std::filesystem::space(std::filesystem::path{path}, ec); + if (ec) { + return std::unexpected(base::Error{static_cast(ec.value()), ec.message()}); + } + return info; +} + } // namespace cfbox::fs diff --git a/include/cfbox/stream.hpp b/include/cfbox/stream.hpp new file mode 100644 index 0000000..3f5c2ed --- /dev/null +++ b/include/cfbox/stream.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace cfbox::stream { + +inline auto for_each_line(std::string_view path, + std::function fn) + -> base::Result { + base::Result content_result; + if (path == "-") { + content_result = io::read_all_stdin(); + } else { + content_result = io::read_all(path); + } + if (!content_result) { + return std::unexpected(std::move(content_result).error()); + } + + const auto& content = *content_result; + std::string line; + std::size_t line_num = 0; + for (char c : content) { + if (c == '\n') { + if (!fn(line, line_num++)) return {}; + line.clear(); + } else { + line += c; + } + } + if (!line.empty()) { + fn(line, line_num); + } + return {}; +} + +inline auto split_fields(const std::string& line, char delim) -> std::vector { + std::vector fields; + std::string field; + for (char c : line) { + if (c == delim) { + fields.push_back(std::move(field)); + field.clear(); + } else { + field += c; + } + } + fields.push_back(std::move(field)); + return fields; +} + +inline auto split_whitespace(const std::string& line) -> std::vector { + std::vector fields; + std::string field; + for (char c : line) { + if (c == ' ' || c == '\t') { + if (!field.empty()) { + fields.push_back(std::move(field)); + field.clear(); + } + } else { + field += c; + } + } + if (!field.empty()) { + fields.push_back(std::move(field)); + } + return fields; +} + +class LineProcessor { +public: + virtual ~LineProcessor() = default; + virtual auto process_line(const std::string& line, std::size_t line_num) -> std::string = 0; + virtual auto finalize() -> void {} +}; + +inline auto run_processor(std::string_view path, LineProcessor& proc) -> int { + auto result = for_each_line(path, [&](const std::string& line, std::size_t num) { + auto output = proc.process_line(line, num); + if (!output.empty()) { + std::fwrite(output.data(), 1, output.size(), stdout); + } + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox: %s\n", result.error().msg.c_str()); + return 1; + } + proc.finalize(); + return 0; +} + +} // namespace cfbox::stream diff --git a/src/applets/cksum.cpp b/src/applets/cksum.cpp new file mode 100644 index 0000000..7e40009 --- /dev/null +++ b/src/applets/cksum.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "cksum", + .version = CFBOX_VERSION_STRING, + .one_line = "checksum and count the bytes in a file", + .usage = "cksum [FILE]...", + .options = "", + .extra = "", +}; +} // namespace + +auto cksum_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + auto paths = pos.empty() ? std::vector{"-"} : pos; + + int rc = 0; + for (auto p : paths) { + auto data_result = (p == "-") ? cfbox::io::read_all_stdin() : cfbox::io::read_all(p); + if (!data_result) { + std::fprintf(stderr, "cfbox cksum: %s\n", data_result.error().msg.c_str()); + rc = 1; + continue; + } + auto crc = cfbox::checksum::crc32(*data_result); + std::printf("%u %ju", crc, static_cast(data_result->size())); + if (p != "-") std::printf(" %.*s", static_cast(p.size()), p.data()); + std::putchar('\n'); + } + return rc; +} diff --git a/src/applets/comm.cpp b/src/applets/comm.cpp new file mode 100644 index 0000000..4dfefc7 --- /dev/null +++ b/src/applets/comm.cpp @@ -0,0 +1,78 @@ +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "comm", + .version = CFBOX_VERSION_STRING, + .one_line = "compare two sorted files line by line", + .usage = "comm [-123] FILE1 FILE2", + .options = " -1 suppress column 1 (lines unique to FILE1)\n" + " -2 suppress column 2 (lines unique to FILE2)\n" + " -3 suppress column 3 (lines common to both)", + .extra = "", +}; +} // namespace + +auto comm_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'1', false}, + cfbox::args::OptSpec{'2', false}, + cfbox::args::OptSpec{'3', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool suppress1 = parsed.has('1'); + bool suppress2 = parsed.has('2'); + bool suppress3 = parsed.has('3'); + + const auto& pos = parsed.positional(); + if (pos.size() < 2) { + std::fprintf(stderr, "cfbox comm: missing operand\n"); + return 1; + } + + auto lines1_result = cfbox::io::read_lines(std::string{pos[0]}); + auto lines2_result = cfbox::io::read_lines(std::string{pos[1]}); + if (!lines1_result) { + std::fprintf(stderr, "cfbox comm: %s\n", lines1_result.error().msg.c_str()); + return 1; + } + if (!lines2_result) { + std::fprintf(stderr, "cfbox comm: %s\n", lines2_result.error().msg.c_str()); + return 1; + } + + const auto& lines1 = *lines1_result; + const auto& lines2 = *lines2_result; + + std::size_t i = 0, j = 0; + while (i < lines1.size() && j < lines2.size()) { + if (lines1[i] < lines2[j]) { + if (!suppress1) std::printf("%s\n", lines1[i].c_str()); + ++i; + } else if (lines1[i] > lines2[j]) { + if (!suppress2) std::printf("\t%s\n", lines2[j].c_str()); + ++j; + } else { + if (!suppress3) std::printf("\t\t%s\n", lines1[i].c_str()); + ++i; ++j; + } + } + while (i < lines1.size()) { + if (!suppress1) std::printf("%s\n", lines1[i].c_str()); + ++i; + } + while (j < lines2.size()) { + if (!suppress2) std::printf("\t%s\n", lines2[j].c_str()); + ++j; + } + return 0; +} diff --git a/src/applets/cut.cpp b/src/applets/cut.cpp new file mode 100644 index 0000000..ee45336 --- /dev/null +++ b/src/applets/cut.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "cut", + .version = CFBOX_VERSION_STRING, + .one_line = "remove sections from each line of files", + .usage = "cut -d DELIM -f LIST [FILE]...", + .options = " -d DELIM use DELIM instead of TAB for field delimiter\n" + " -f LIST select only these fields\n" + " -c LIST select only these characters\n" + " -s do not print lines without delimiter", + .extra = "LIST: comma-separated ranges (e.g., 1,3-5,7-)", +}; +} // namespace + +static auto parse_range_list(const std::string& list) -> std::set { + std::set fields; + std::string token; + for (std::size_t i = 0; i <= list.size(); ++i) { + if (i == list.size() || list[i] == ',') { + auto dash = token.find('-'); + if (dash == std::string::npos) { + fields.insert(std::stoi(token)); + } else if (dash == 0) { + int end = std::stoi(token.substr(1)); + for (int j = 1; j <= end; ++j) fields.insert(j); + } else if (dash == token.size() - 1) { + int start = std::stoi(token.substr(0, dash)); + for (int j = start; j <= 1024; ++j) fields.insert(j); + } else { + int start = std::stoi(token.substr(0, dash)); + int end = std::stoi(token.substr(dash + 1)); + for (int j = start; j <= end; ++j) fields.insert(j); + } + token.clear(); + } else { + token += list[i]; + } + } + return fields; +} + +auto cut_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'d', true, "delimiter"}, + cfbox::args::OptSpec{'f', true, "fields"}, + cfbox::args::OptSpec{'c', true, "characters"}, + cfbox::args::OptSpec{'s', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + char delim = '\t'; + if (auto d = parsed.get_any('d', "delimiter")) { + if (d->size() != 1) { + std::fprintf(stderr, "cfbox cut: delimiter must be a single character\n"); + return 1; + } + delim = (*d)[0]; + } + + bool skip_no_delim = parsed.has('s'); + bool field_mode = parsed.has_any('f', "fields"); + bool char_mode = parsed.has_any('c', "characters"); + + if (!field_mode && !char_mode) { + std::fprintf(stderr, "cfbox cut: you must specify a list of fields or characters\n"); + return 1; + } + + std::set indices; + if (field_mode) { + auto list = parsed.get_any('f', "fields"); + if (!list) { + std::fprintf(stderr, "cfbox cut: missing list for -f\n"); + return 1; + } + indices = parse_range_list(std::string{*list}); + } else { + auto list = parsed.get_any('c', "characters"); + if (!list) { + std::fprintf(stderr, "cfbox cut: missing list for -c\n"); + return 1; + } + indices = parse_range_list(std::string{*list}); + } + + const auto& pos = parsed.positional(); + auto paths = pos.empty() ? std::vector{"-"} : pos; + + int rc = 0; + for (auto p : paths) { + auto result = cfbox::stream::for_each_line(p, [&](const std::string& line, std::size_t) { + if (char_mode) { + bool first = true; + for (int idx : indices) { + if (idx >= 1 && static_cast(idx - 1) < line.size()) { + if (!first) std::putchar(delim); + std::putchar(line[idx - 1]); + first = false; + } + } + std::putchar('\n'); + } else { + auto fields = cfbox::stream::split_fields(line, delim); + if (fields.size() <= 1 && skip_no_delim) { + return true; + } + bool first = true; + for (int idx : indices) { + if (idx >= 1 && static_cast(idx - 1) < fields.size()) { + if (!first) std::putchar(delim); + std::fputs(fields[idx - 1].c_str(), stdout); + first = false; + } + } + std::putchar('\n'); + } + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox cut: %s\n", result.error().msg.c_str()); + rc = 1; + } + } + return rc; +} diff --git a/src/applets/date.cpp b/src/applets/date.cpp new file mode 100644 index 0000000..327a32a --- /dev/null +++ b/src/applets/date.cpp @@ -0,0 +1,77 @@ +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "date", + .version = CFBOX_VERSION_STRING, + .one_line = "print or set the system date and time", + .usage = "date [+FORMAT]", + .options = " -u print or set Coordinated Universal Time (UTC)\n" + " -d STR display time described by STR", + .extra = "FORMAT supports: %Y %m %d %H %M %S %a %A %b %B %s %Z %j %W", +}; +} // namespace + +auto date_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'u', false}, + cfbox::args::OptSpec{'d', true, "date"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool utc = parsed.has('u'); + const auto& pos = parsed.positional(); + + std::string format; + for (auto p : pos) { + if (!p.empty() && p[0] == '+') { + format = std::string{p.substr(1)}; + } + } + + if (format.empty()) { + format = "%a %b %d %H:%M:%S %Z %Y"; + } + + time_t now = time(nullptr); + if (auto d = parsed.get_any('d', "date")) { + struct tm tm_val{}; + auto* result = strptime(std::string{*d}.c_str(), "%Y-%m-%d %H:%M:%S", &tm_val); + if (!result) { + result = strptime(std::string{*d}.c_str(), "%Y-%m-%d", &tm_val); + } + if (!result) { + result = strptime(std::string{*d}.c_str(), "%H:%M:%S", &tm_val); + if (result) { + auto* now_tm = localtime(&now); + tm_val.tm_year = now_tm->tm_year; + tm_val.tm_mon = now_tm->tm_mon; + tm_val.tm_mday = now_tm->tm_mday; + } + } + if (result) { + now = mktime(&tm_val); + } else { + std::fprintf(stderr, "cfbox date: invalid date '%.*s'\n", + static_cast(d->size()), d->data()); + return 1; + } + } + + auto* tm = utc ? gmtime(&now) : localtime(&now); + char buf[256]; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + strftime(buf, sizeof(buf), format.c_str(), tm); + std::puts(buf); +#pragma GCC diagnostic pop + return 0; +} diff --git a/src/applets/df.cpp b/src/applets/df.cpp new file mode 100644 index 0000000..7d947b0 --- /dev/null +++ b/src/applets/df.cpp @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "df", + .version = CFBOX_VERSION_STRING, + .one_line = "report file system disk space usage", + .usage = "df [-h] [FILE]...", + .options = " -h print sizes in human readable format", + .extra = "", +}; + +static auto human_size(unsigned long long bytes) -> std::string { + constexpr const char* units[] = {"", "K", "M", "G", "T"}; + double size = static_cast(bytes); + int unit = 0; + while (size >= 1024 && unit < 4) { size /= 1024; ++unit; } + char buf[32]; + if (unit == 0) std::snprintf(buf, sizeof(buf), "%llu", bytes); + else std::snprintf(buf, sizeof(buf), "%.1f%s", size, units[unit]); + return buf; +} +} // namespace + +auto df_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'h', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool human = parsed.has('h'); + const auto& pos = parsed.positional(); + + struct Entry { + std::string fs; + std::string mount; + unsigned long long total; + unsigned long long used; + unsigned long long avail; + unsigned long long blocks; + }; + + std::vector entries; + + if (!pos.empty()) { + for (auto p : pos) { + struct statvfs vfs; + if (statvfs(std::string{p}.c_str(), &vfs) != 0) { + std::fprintf(stderr, "cfbox df: cannot stat '%.*s': %s\n", + static_cast(p.size()), p.data(), std::strerror(errno)); + continue; + } + auto bsize = static_cast(vfs.f_bsize); + entries.push_back({ + std::string{p}, std::string{p}, + vfs.f_blocks * bsize, + (vfs.f_blocks - vfs.f_bfree) * bsize, + vfs.f_bavail * bsize, + vfs.f_blocks + }); + } + } else { + auto* mtab = setmntent("/proc/mounts", "r"); + if (!mtab) mtab = setmntent("/etc/mtab", "r"); + if (!mtab) { + std::fprintf(stderr, "cfbox df: cannot read mount table\n"); + return 1; + } + struct mntent* mnt; + while ((mnt = getmntent(mtab)) != nullptr) { + struct statvfs vfs; + if (statvfs(mnt->mnt_dir, &vfs) != 0) continue; + if (vfs.f_blocks == 0) continue; + auto bsize = static_cast(vfs.f_bsize); + entries.push_back({ + mnt->mnt_fsname, mnt->mnt_dir, + vfs.f_blocks * bsize, + (vfs.f_blocks - vfs.f_bfree) * bsize, + vfs.f_bavail * bsize, + vfs.f_blocks + }); + } + endmntent(mtab); + } + + std::printf("%-20s %10s %10s %10s %8s %s\n", + "Filesystem", "1K-blocks", "Used", "Available", "Use%", "Mounted on"); + for (const auto& e : entries) { + auto pct = e.total > 0 ? static_cast(static_cast(e.used) * 100) / + static_cast(static_cast(e.total)) : 0.0; + if (human) { + std::printf("%-20s %10s %10s %10s %7.0f%% %s\n", + e.fs.c_str(), + human_size(e.total).c_str(), + human_size(e.used).c_str(), + human_size(e.avail).c_str(), + pct, + e.mount.c_str()); + } else { + auto kblocks = e.blocks; + auto kused = e.used / 1024; + auto kavail = e.avail / 1024; + std::printf("%-20s %10llu %10llu %10llu %7.0f%% %s\n", + e.fs.c_str(), + kblocks, kused, kavail, + pct, + e.mount.c_str()); + } + } + return 0; +} diff --git a/src/applets/du.cpp b/src/applets/du.cpp new file mode 100644 index 0000000..806e594 --- /dev/null +++ b/src/applets/du.cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "du", + .version = CFBOX_VERSION_STRING, + .one_line = "estimate file space usage", + .usage = "du [OPTIONS] [FILE]...", + .options = " -s display only a total for each argument\n" + " -h print sizes in human readable format\n" + " -x skip directories on different file systems", + .extra = "", +}; + +auto human_size(std::uintmax_t bytes) -> std::string { + constexpr const char* units[] = {"", "K", "M", "G", "T"}; + double size = static_cast(bytes); + int unit = 0; + while (size >= 1024 && unit < 4) { + size /= 1024; + ++unit; + } + char buf[32]; + if (unit == 0) { + std::snprintf(buf, sizeof(buf), "%ju", bytes); + } else { + std::snprintf(buf, sizeof(buf), "%.1f%s", size, units[unit]); + } + return buf; +} + +} // namespace + +auto du_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'s', false}, + cfbox::args::OptSpec{'h', false}, + cfbox::args::OptSpec{'x', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool human = parsed.has('h'); + bool one_fs = parsed.has('x'); + const auto& pos = parsed.positional(); + + auto targets = pos.empty() ? std::vector{"."} : pos; + + int rc = 0; + for (auto t : targets) { + std::string path{t}; + std::error_code ec; + std::uintmax_t total = 0; + + auto start_dev = [&]() -> std::uintmax_t { + if (!one_fs) return std::numeric_limits::max(); + struct stat sb; + if (::stat(path.c_str(), &sb) != 0) + return std::numeric_limits::max(); + return sb.st_dev; + }(); + + for (auto it = std::filesystem::recursive_directory_iterator( + std::filesystem::path{path}, + std::filesystem::directory_options::skip_permission_denied, ec); + it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) { + if (ec) { ec.clear(); continue; } + + if (one_fs) { + struct stat sb; + if (::lstat(it->path().c_str(), &sb) == 0 && static_cast(sb.st_dev) != start_dev) { + it.disable_recursion_pending(); + continue; + } + } + + if (!it->is_directory(ec) && !ec) { + auto sz = it->file_size(ec); + if (!ec) total += sz; + } + } + + if (human) { + std::printf("%s\t%s\n", human_size(total).c_str(), path.c_str()); + } else { + std::printf("%ju\t%s\n", total / 1024 + (total % 1024 ? 1 : 0), path.c_str()); + } + } + return rc; +} diff --git a/src/applets/env.cpp b/src/applets/env.cpp new file mode 100644 index 0000000..49826ef --- /dev/null +++ b/src/applets/env.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include + +#include +#include + +extern char** environ; + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "env", + .version = CFBOX_VERSION_STRING, + .one_line = "run a program in a modified environment", + .usage = "env [-i] [NAME=VALUE]... [COMMAND [ARGS]...]", + .options = " -i start with an empty environment", + .extra = "", +}; +} // namespace + +auto env_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'i', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool clear_env = parsed.has('i'); + + // Collect NAME=VALUE assignments from positional args until we hit a non-assignment + std::vector> assignments; + const auto& pos = parsed.positional(); + std::size_t cmd_start = pos.size(); + + for (std::size_t i = 0; i < pos.size(); ++i) { + auto arg = std::string{pos[i]}; + auto eq = arg.find('='); + if (eq == std::string::npos || eq == 0) { + cmd_start = i; + break; + } + assignments.emplace_back(arg.substr(0, eq), arg.substr(eq + 1)); + } + + if (clear_env) { + // Clear environment, then set assigned vars + for (char** env = environ; *env != nullptr; ++env) { + auto eq = std::string_view{*env}.find('='); + if (eq != std::string_view::npos) { + std::string name{*env, eq}; + unsetenv(name.c_str()); + } + } + } + + for (const auto& [name, value] : assignments) { + setenv(name.c_str(), value.c_str(), 1); + } + + // No command: print environment + if (cmd_start >= pos.size()) { + for (char** env = environ; *env != nullptr; ++env) { + std::puts(*env); + } + return 0; + } + + // Execute command + std::string cmd{pos[cmd_start]}; + std::vector arg_storage; + for (std::size_t i = cmd_start; i < pos.size(); ++i) { + arg_storage.emplace_back(pos[i]); + } + std::vector cmd_args; + for (auto& s : arg_storage) { + cmd_args.push_back(s.data()); + } + cmd_args.push_back(nullptr); + + execvp(cmd.c_str(), cmd_args.data()); + std::fprintf(stderr, "cfbox env: failed to execute '%s': %s\n", + cmd.c_str(), std::strerror(errno)); + return 127; +} diff --git a/src/applets/expand.cpp b/src/applets/expand.cpp new file mode 100644 index 0000000..1550dad --- /dev/null +++ b/src/applets/expand.cpp @@ -0,0 +1,64 @@ +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "expand", + .version = CFBOX_VERSION_STRING, + .one_line = "convert tabs to spaces", + .usage = "expand [-t N] [FILE]...", + .options = " -t N have tabs N characters apart (default 8)", + .extra = "", +}; +} // namespace + +auto expand_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'t', true, "tabs"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + int tab_stop = 8; + if (auto t = parsed.get_any('t', "tabs")) { + tab_stop = std::stoi(std::string{*t}); + if (tab_stop <= 0) { + std::fprintf(stderr, "cfbox expand: invalid tab stop: %d\n", tab_stop); + return 1; + } + } + + const auto& pos = parsed.positional(); + auto paths = pos.empty() ? std::vector{"-"} : pos; + + int rc = 0; + for (auto p : paths) { + auto result = cfbox::stream::for_each_line(p, [&](const std::string& line, std::size_t) { + int col = 0; + for (char c : line) { + if (c == '\t') { + int spaces = tab_stop - (col % tab_stop); + for (int i = 0; i < spaces; ++i) { + std::putchar(' '); + } + col += spaces; + } else { + std::putchar(c); + ++col; + } + } + std::putchar('\n'); + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox expand: %s\n", result.error().msg.c_str()); + rc = 1; + } + } + return rc; +} diff --git a/src/applets/expr.cpp b/src/applets/expr.cpp new file mode 100644 index 0000000..02fef18 --- /dev/null +++ b/src/applets/expr.cpp @@ -0,0 +1,219 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "expr", + .version = CFBOX_VERSION_STRING, + .one_line = "evaluate expressions", + .usage = "expr EXPRESSION...", + .options = "", + .extra = "Supports: + - * / % < <= = != >= > : length substr index", +}; +} // namespace + +struct Value { + enum Type { Int, Str } type; + long ival = 0; + std::string sval; + + static auto integer(long v) -> Value { return {Int, v, {}}; } + static auto str(const std::string& s) -> Value { + char* end = nullptr; + long v = std::strtol(s.c_str(), &end, 10); + if (*end == '\0' && !s.empty()) return {Int, v, {}}; + return {Str, 0, s}; + } + auto to_int() const -> long { + if (type == Int) return ival; + char* end = nullptr; + return std::strtol(sval.c_str(), &end, 10); + } + auto to_str() const -> std::string { + if (type == Int) return std::to_string(ival); + return sval; + } + auto is_zero_or_null() const -> bool { + if (type == Int) return ival == 0; + return sval.empty(); + } +}; + +static auto eval(std::vector::iterator& it, + std::vector::iterator end) -> Value; + +static auto eval_primary(std::vector::iterator& it, + std::vector::iterator end) -> Value { + if (it == end) return Value::str(""); + + if (*it == "length" && std::next(it) != end) { + ++it; + auto v = eval_primary(it, end); + return Value::integer(static_cast(v.to_str().size())); + } + if (*it == "substr" && std::distance(it, end) >= 4) { + ++it; + auto s = eval_primary(it, end).to_str(); + auto pos = eval_primary(it, end).to_int(); + auto len = eval_primary(it, end).to_int(); + if (pos >= 1 && static_cast(pos - 1) <= s.size()) { + return Value::str(s.substr(static_cast(pos - 1), + static_cast(len))); + } + return Value::str(""); + } + if (*it == "index" && std::distance(it, end) >= 3) { + ++it; + auto s = eval_primary(it, end).to_str(); + auto chars = eval_primary(it, end).to_str(); + for (std::size_t i = 0; i < s.size(); ++i) { + if (chars.find(s[i]) != std::string::npos) { + return Value::integer(static_cast(i + 1)); + } + } + return Value::integer(0); + } + + auto token = *it++; + if (token == "(") { + auto v = eval(it, end); + if (it != end && *it == ")") ++it; + return v; + } + return Value::str(token); +} + +static auto eval_compare(std::vector::iterator& it, + std::vector::iterator end) -> Value { + auto left = eval_primary(it, end); + if (it != end) { + auto op = *it; + if (op == ":" && std::next(it) != end) { + ++it; + auto pattern = eval_primary(it, end).to_str(); + auto str = left.to_str(); + regex_t regex; + if (regcomp(®ex, pattern.c_str(), REG_EXTENDED) != 0) { + return Value::integer(0); + } + regmatch_t match; + if (regexec(®ex, str.c_str(), 1, &match, 0) == 0) { + if (match.rm_so >= 0 && match.rm_eo > match.rm_so) { + regfree(®ex); + return Value::str(str.substr( + static_cast(match.rm_so), + static_cast(match.rm_eo - match.rm_so))); + } + regfree(®ex); + return Value::integer(static_cast(match.rm_eo - match.rm_so)); + } + regfree(®ex); + return Value::integer(0); + } + if ((op == "<" || op == "<=" || op == "=" || op == "==" || + op == "!=" || op == ">=" || op == ">") && std::next(it) != end) { + ++it; + auto right = eval_primary(it, end); + bool result = false; + if (left.type == Value::Int && right.type == Value::Int) { + if (op == "<") result = left.ival < right.ival; + if (op == "<=") result = left.ival <= right.ival; + if (op == "=" || op == "==") result = left.ival == right.ival; + if (op == "!=") result = left.ival != right.ival; + if (op == ">=") result = left.ival >= right.ival; + if (op == ">") result = left.ival > right.ival; + } else { + auto ls = left.to_str(), rs = right.to_str(); + if (op == "<") result = ls < rs; + if (op == "<=") result = ls <= rs; + if (op == "=" || op == "==") result = ls == rs; + if (op == "!=") result = ls != rs; + if (op == ">=") result = ls >= rs; + if (op == ">") result = ls > rs; + } + return Value::integer(result ? 1 : 0); + } + } + return left; +} + +static auto eval_add(std::vector::iterator& it, + std::vector::iterator end) -> Value { + auto left = eval_compare(it, end); + while (it != end) { + auto op = *it; + if (op != "+" && op != "-") break; + ++it; + auto right = eval_compare(it, end); + if (op == "+") left = Value::integer(left.to_int() + right.to_int()); + else left = Value::integer(left.to_int() - right.to_int()); + } + return left; +} + +static auto eval(std::vector::iterator& it, + std::vector::iterator end) -> Value { + auto left = eval_add(it, end); + while (it != end) { + auto op = *it; + if (op == "*") { + ++it; auto right = eval_add(it, end); + left = Value::integer(left.to_int() * right.to_int()); + } else if (op == "/") { + ++it; auto right = eval_add(it, end); + if (right.to_int() == 0) { + std::fprintf(stderr, "cfbox expr: division by zero\n"); + return Value::integer(0); + } + left = Value::integer(left.to_int() / right.to_int()); + } else if (op == "%") { + ++it; auto right = eval_add(it, end); + if (right.to_int() == 0) { + std::fprintf(stderr, "cfbox expr: division by zero\n"); + return Value::integer(0); + } + left = Value::integer(left.to_int() % right.to_int()); + } else if (op == "|") { + ++it; + auto right = eval_add(it, end); + if (!left.is_zero_or_null()) return left; + left = right; + } else if (op == "&") { + ++it; + auto right = eval_add(it, end); + if (left.is_zero_or_null()) return left; + left = right; + } else { + break; + } + } + return left; +} + +auto expr_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox expr: missing operand\n"); + return 2; + } + + std::vector args; + for (auto p : pos) args.emplace_back(p); + + auto it = args.begin(); + auto result = eval(it, args.end()); + std::puts(result.to_str().c_str()); + return result.is_zero_or_null() ? 1 : 0; +} diff --git a/src/applets/factor.cpp b/src/applets/factor.cpp new file mode 100644 index 0000000..85ca7ca --- /dev/null +++ b/src/applets/factor.cpp @@ -0,0 +1,61 @@ +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "factor", + .version = CFBOX_VERSION_STRING, + .one_line = "print the prime factors of numbers", + .usage = "factor [NUMBER]...", + .options = "", + .extra = "If no NUMBER is given, read from stdin.", +}; +} // namespace + +static auto factor_number(unsigned long long n) -> void { + std::printf("%llu:", n); + for (unsigned long long d = 2; d * d <= n; ++d) { + while (n % d == 0) { + std::printf(" %llu", d); + n /= d; + } + } + if (n > 1) std::printf(" %llu", n); + std::putchar('\n'); +} + +auto factor_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + auto input = cfbox::io::read_all_stdin(); + if (!input) { + std::fprintf(stderr, "cfbox factor: %s\n", input.error().msg.c_str()); + return 1; + } + char* str = input->data(); + char* end = str + input->size(); + while (str < end) { + char* num_end = nullptr; + auto n = std::strtoull(str, &num_end, 10); + if (num_end == str) { ++str; continue; } + factor_number(n); + str = num_end; + } + } else { + for (auto p : pos) { + auto n = std::strtoull(std::string{p}.c_str(), nullptr, 10); + factor_number(n); + } + } + return 0; +} diff --git a/src/applets/fold.cpp b/src/applets/fold.cpp new file mode 100644 index 0000000..3511958 --- /dev/null +++ b/src/applets/fold.cpp @@ -0,0 +1,84 @@ +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "fold", + .version = CFBOX_VERSION_STRING, + .one_line = "wrap each input line to fit in specified width", + .usage = "fold [-w WIDTH] [-s] [FILE]...", + .options = " -w WIDTH use WIDTH columns instead of 80\n" + " -s break at spaces", + .extra = "", +}; +} // namespace + +auto fold_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'w', true, "width"}, + cfbox::args::OptSpec{'s', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + int width = 80; + if (auto w = parsed.get_any('w', "width")) { + width = std::stoi(std::string{*w}); + if (width <= 0) { + std::fprintf(stderr, "cfbox fold: invalid width: %d\n", width); + return 1; + } + } + bool break_spaces = parsed.has('s'); + + const auto& pos = parsed.positional(); + auto paths = pos.empty() ? std::vector{"-"} : pos; + + int rc = 0; + for (auto p : paths) { + auto result = cfbox::stream::for_each_line(p, [&](const std::string& line, std::size_t) { + std::size_t col = 0; + std::size_t last_space = 0; + std::string segment; + + for (std::size_t i = 0; i < line.size(); ++i) { + if (col >= static_cast(width)) { + if (break_spaces && last_space > 0) { + // Break at last space + auto break_pos = last_space; + std::string before = line.substr(i - col, break_pos - (i - col)); + std::puts(before.c_str()); + col = i - break_pos; + last_space = 0; + } else { + std::string before = line.substr(i - col, col); + std::puts(before.c_str()); + col = 0; + } + } + if (line[i] == ' ' || line[i] == '\t') { + last_space = col + 1; + } + ++col; + } + // Print remaining + if (col > 0) { + auto start = line.size() - col; + std::puts(line.substr(start).c_str()); + } else { + std::putchar('\n'); + } + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox fold: %s\n", result.error().msg.c_str()); + rc = 1; + } + } + return rc; +} diff --git a/src/applets/hostid.cpp b/src/applets/hostid.cpp new file mode 100644 index 0000000..560a594 --- /dev/null +++ b/src/applets/hostid.cpp @@ -0,0 +1,27 @@ +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "hostid", + .version = CFBOX_VERSION_STRING, + .one_line = "print the numeric identifier for the current host", + .usage = "hostid", + .options = "", + .extra = "", +}; +} // namespace + +auto hostid_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + long id = gethostid(); + std::printf("%08lx\n", id); + return 0; +} diff --git a/src/applets/install.cpp b/src/applets/install.cpp new file mode 100644 index 0000000..7f66b74 --- /dev/null +++ b/src/applets/install.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "install", + .version = CFBOX_VERSION_STRING, + .one_line = "copy files and set attributes", + .usage = "install [OPTIONS] SOURCE... DEST", + .options = " -d create directories\n" + " -m MODE set permission mode (octal)\n" + " -t DIR install into DIR", + .extra = "", +}; +} // namespace + +auto install_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'d', false}, + cfbox::args::OptSpec{'m', true, "mode"}, + cfbox::args::OptSpec{'t', true, "target-directory"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool mkdir_mode = parsed.has('d'); + auto mode_str = parsed.get_any('m', "mode"); + auto target_dir = parsed.get_any('t', "target-directory"); + const auto& pos = parsed.positional(); + + auto parse_mode = [](const std::string& s) -> std::filesystem::perms { + unsigned long m = std::stoul(s, nullptr, 8); + return static_cast(m); + }; + + std::filesystem::perms mode = std::filesystem::perms::owner_read | + std::filesystem::perms::owner_write | + std::filesystem::perms::group_read | + std::filesystem::perms::others_read; + if (mode_str) { + try { mode = parse_mode(std::string{*mode_str}); } + catch (...) { + std::fprintf(stderr, "cfbox install: invalid mode '%.*s'\n", + static_cast(mode_str->size()), mode_str->data()); + return 1; + } + } + + if (mkdir_mode) { + if (pos.empty()) { + std::fprintf(stderr, "cfbox install: missing operand\n"); + return 1; + } + for (auto p : pos) { + std::error_code ec; + std::filesystem::create_directories(std::filesystem::path{p}, ec); + if (ec) { + std::fprintf(stderr, "cfbox install: cannot create directory '%.*s': %s\n", + static_cast(p.size()), p.data(), ec.message().c_str()); + return 1; + } + std::filesystem::permissions(std::filesystem::path{p}, mode, ec); + } + return 0; + } + + if (pos.empty()) { + std::fprintf(stderr, "cfbox install: missing operand\n"); + return 1; + } + + std::string dest; + std::vector sources; + if (target_dir) { + dest = std::string{*target_dir}; + sources = pos; + } else { + if (pos.size() < 2) { + std::fprintf(stderr, "cfbox install: missing destination file operand\n"); + return 1; + } + dest = std::string{pos.back()}; + for (std::size_t i = 0; i + 1 < pos.size(); ++i) { + sources.push_back(pos[i]); + } + } + + int rc = 0; + for (auto src : sources) { + std::string src_path{src}; + std::string dst_path = dest; + + if (!target_dir && sources.size() == 1) { + // Single source, dest can be a directory or new filename + } else { + // Multiple sources or -t: dest must be a directory + auto filename = std::filesystem::path{src_path}.filename(); + dst_path = (std::filesystem::path{dest} / filename).string(); + } + + auto result = cfbox::io::read_all(src_path); + if (!result) { + std::fprintf(stderr, "cfbox install: %s\n", result.error().msg.c_str()); + rc = 1; + continue; + } + auto wresult = cfbox::io::write_all(dst_path, *result); + if (!wresult) { + std::fprintf(stderr, "cfbox install: %s\n", wresult.error().msg.c_str()); + rc = 1; + continue; + } + std::error_code ec; + std::filesystem::permissions(std::filesystem::path{dst_path}, mode, ec); + } + return rc; +} diff --git a/src/applets/ln.cpp b/src/applets/ln.cpp new file mode 100644 index 0000000..e36a108 --- /dev/null +++ b/src/applets/ln.cpp @@ -0,0 +1,66 @@ +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "ln", + .version = CFBOX_VERSION_STRING, + .one_line = "make links between files", + .usage = "ln [-s] [-f] [-n] TARGET... DIRECTORY", + .options = " -s make symbolic links instead of hard links\n" + " -f remove existing destination files\n" + " -n treat LINK_NAME as a normal file if it is a symlink to a dir", + .extra = "", +}; +} // namespace + +auto ln_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'s', false}, + cfbox::args::OptSpec{'f', false}, + cfbox::args::OptSpec{'n', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool symbolic = parsed.has('s'); + bool force = parsed.has('f'); + const auto& pos = parsed.positional(); + + if (pos.size() < 2) { + std::fprintf(stderr, "cfbox ln: missing operand\n"); + return 1; + } + + std::string target{pos[0]}; + std::string link_name{pos[1]}; + + // If link_name is an existing directory, put link inside it + if (pos.size() == 2 && cfbox::fs::is_directory(link_name)) { + auto filename = std::filesystem::path{target}.filename(); + link_name = (std::filesystem::path{link_name} / filename).string(); + } + + if (force) { + std::error_code ec; + std::filesystem::remove(std::filesystem::path{link_name}, ec); + } + + cfbox::base::Result result; + if (symbolic) { + result = cfbox::fs::create_symlink(target, link_name); + } else { + result = cfbox::fs::create_hard_link(target, link_name); + } + if (!result) { + std::fprintf(stderr, "cfbox ln: %s\n", result.error().msg.c_str()); + return 1; + } + return 0; +} diff --git a/src/applets/md5sum.cpp b/src/applets/md5sum.cpp new file mode 100644 index 0000000..8101fb8 --- /dev/null +++ b/src/applets/md5sum.cpp @@ -0,0 +1,47 @@ +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "md5sum", + .version = CFBOX_VERSION_STRING, + .one_line = "compute and check MD5 message digest", + .usage = "md5sum [FILE]...", + .options = "", + .extra = "", +}; +} // namespace + +auto md5sum_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + auto paths = pos.empty() ? std::vector{"-"} : pos; + + int rc = 0; + for (auto p : paths) { + auto data_result = (p == "-") ? cfbox::io::read_all_stdin() : cfbox::io::read_all(p); + if (!data_result) { + std::fprintf(stderr, "cfbox md5sum: %s\n", data_result.error().msg.c_str()); + rc = 1; + continue; + } + auto hash = cfbox::checksum::md5(*data_result); + auto hex = cfbox::checksum::md5_to_hex(hash); + std::printf("%s ", hex.c_str()); + if (p == "-") { + std::puts("-"); + } else { + std::printf("%.*s\n", static_cast(p.size()), p.data()); + } + } + return rc; +} diff --git a/src/applets/mkfifo.cpp b/src/applets/mkfifo.cpp new file mode 100644 index 0000000..119e0c5 --- /dev/null +++ b/src/applets/mkfifo.cpp @@ -0,0 +1,50 @@ +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "mkfifo", + .version = CFBOX_VERSION_STRING, + .one_line = "make FIFOs (named pipes)", + .usage = "mkfifo [NAME]...", + .options = " -m MODE set permission mode (octal, default 0644)", + .extra = "", +}; +} // namespace + +auto mkfifo_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'m', true, "mode"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + auto mode_str = parsed.get_any('m', "mode"); + mode_t mode = 0644; + if (mode_str) { + mode = static_cast(std::stoul(std::string{*mode_str}, nullptr, 8)); + } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox mkfifo: missing operand\n"); + return 1; + } + + int rc = 0; + for (auto p : pos) { + std::string path{p}; + if (mkfifo(path.c_str(), mode) != 0) { + std::fprintf(stderr, "cfbox mkfifo: cannot create fifo '%s': %s\n", + path.c_str(), std::strerror(errno)); + rc = 1; + } + } + return rc; +} diff --git a/src/applets/mknod.cpp b/src/applets/mknod.cpp new file mode 100644 index 0000000..9479d8c --- /dev/null +++ b/src/applets/mknod.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "mknod", + .version = CFBOX_VERSION_STRING, + .one_line = "make block or character special files", + .usage = "mknod NAME TYPE [MAJOR MINOR]", + .options = "", + .extra = "TYPE: b for block, c or u for character, p for FIFO", +}; +} // namespace + +auto mknod_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.size() < 2) { + std::fprintf(stderr, "cfbox mknod: missing operand\n"); + return 1; + } + + std::string name{pos[0]}; + char type = pos[1][0]; + + mode_t mode; + dev_t dev = 0; + + switch (type) { + case 'b': + if (pos.size() < 4) { + std::fprintf(stderr, "cfbox mknod: missing major/minor for block device\n"); + return 1; + } + mode = S_IFBLK | 0660; + dev = makedev(static_cast(std::stoul(std::string{pos[2]})), + static_cast(std::stoul(std::string{pos[3]}))); + break; + case 'c': case 'u': + if (pos.size() < 4) { + std::fprintf(stderr, "cfbox mknod: missing major/minor for char device\n"); + return 1; + } + mode = S_IFCHR | 0660; + dev = makedev(static_cast(std::stoul(std::string{pos[2]})), + static_cast(std::stoul(std::string{pos[3]}))); + break; + case 'p': + mode = S_IFIFO | 0666; + break; + default: + std::fprintf(stderr, "cfbox mknod: invalid type '%c'\n", type); + return 1; + } + + if (mknod(name.c_str(), mode, dev) != 0) { + std::fprintf(stderr, "cfbox mknod: cannot create node '%s': %s\n", + name.c_str(), std::strerror(errno)); + return 1; + } + return 0; +} diff --git a/src/applets/mktemp.cpp b/src/applets/mktemp.cpp new file mode 100644 index 0000000..90a8d6a --- /dev/null +++ b/src/applets/mktemp.cpp @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "mktemp", + .version = CFBOX_VERSION_STRING, + .one_line = "create a temporary file or directory", + .usage = "mktemp [-d] [-p DIR] [TEMPLATE]", + .options = " -d create a directory instead of a file\n" + " -p DIR create in directory DIR\n" + " -u do not create anything; just print a name", + .extra = "TEMPLATE must end in XXXXXX (at least 6 X's). Default: /tmp/tmp.XXXXXX", +}; +} // namespace + +auto mktemp_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'d', false}, + cfbox::args::OptSpec{'p', true, "tmpdir"}, + cfbox::args::OptSpec{'u', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool make_dir = parsed.has('d'); + bool dry_run = parsed.has('u'); + const auto& pos = parsed.positional(); + + std::string tmpl; + if (pos.empty()) { + auto prefix = parsed.get_any('p', "tmpdir"); + if (prefix) { + tmpl = std::string{*prefix} + "/tmp.XXXXXX"; + } else { + tmpl = "/tmp/tmp.XXXXXX"; + } + } else { + tmpl = std::string{pos[0]}; + auto prefix = parsed.get_any('p', "tmpdir"); + if (prefix && tmpl.find('/') == std::string::npos) { + tmpl = std::string{*prefix} + "/" + tmpl; + } + } + + if (dry_run) { + // Replace trailing X's with random chars + auto xpos = tmpl.rfind('X'); + if (xpos == std::string::npos || xpos < tmpl.size() - 6) { + std::fprintf(stderr, "cfbox mktemp: too few X's in template '%s'\n", tmpl.c_str()); + return 1; + } + std::puts(tmpl.c_str()); + return 0; + } + + if (make_dir) { + char* result = mkdtemp(tmpl.data()); + if (!result) { + std::fprintf(stderr, "cfbox mktemp: failed to create directory: %s\n", + std::strerror(errno)); + return 1; + } + std::puts(result); + } else { + int fd = mkstemp(tmpl.data()); + if (fd < 0) { + std::fprintf(stderr, "cfbox mktemp: failed to create file: %s\n", + std::strerror(errno)); + return 1; + } + ::close(fd); + std::puts(tmpl.c_str()); + } + return 0; +} diff --git a/src/applets/nice.cpp b/src/applets/nice.cpp new file mode 100644 index 0000000..f589028 --- /dev/null +++ b/src/applets/nice.cpp @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "nice", + .version = CFBOX_VERSION_STRING, + .one_line = "run a program with modified scheduling priority", + .usage = "nice [-n ADJUSTMENT] COMMAND [ARGS]...", + .options = " -n ADJUSTMENT add ADJUSTMENT to nice value (default 10)", + .extra = "", +}; +} // namespace + +auto nice_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'n', true, "adjustment"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + int adjustment = 10; + if (auto n = parsed.get_any('n', "adjustment")) { + adjustment = std::stoi(std::string{*n}); + } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox nice: missing command\n"); + return 1; + } + + setpriority(PRIO_PROCESS, 0, getpriority(PRIO_PROCESS, 0) + adjustment); + + std::vector arg_storage; + for (auto p : pos) arg_storage.emplace_back(p); + std::vector cmd_args; + for (auto& s : arg_storage) cmd_args.push_back(s.data()); + cmd_args.push_back(nullptr); + + execvp(cmd_args[0], cmd_args.data()); + std::fprintf(stderr, "cfbox nice: %s: %s\n", cmd_args[0], std::strerror(errno)); + return 127; +} diff --git a/src/applets/nl.cpp b/src/applets/nl.cpp new file mode 100644 index 0000000..9d80bc4 --- /dev/null +++ b/src/applets/nl.cpp @@ -0,0 +1,83 @@ +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "nl", + .version = CFBOX_VERSION_STRING, + .one_line = "number lines of files", + .usage = "nl [-b STYLE] [-n FORMAT] [-s SEP] [FILE]...", + .options = " -b STYLE body numbering style: a(all), t(non-empty), n(none)\n" + " -n FORMAT line number format: ln, rn, rz\n" + " -s SEP add SEP after line number (default: TAB)", + .extra = "", +}; +} // namespace + +auto nl_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'b', true, "body-numbering"}, + cfbox::args::OptSpec{'n', true, "number-format"}, + cfbox::args::OptSpec{'s', true, "number-separator"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + char body_style = 't'; + if (auto b = parsed.get_any('b', "body-numbering")) { + body_style = (*b)[0]; + } + + std::string num_fmt = "rn"; + if (auto n = parsed.get_any('n', "number-format")) { + num_fmt = std::string{*n}; + } + + std::string sep = "\t"; + if (auto s = parsed.get_any('s', "number-separator")) { + sep = std::string{*s}; + } + + const auto& pos = parsed.positional(); + auto paths = pos.empty() ? std::vector{"-"} : pos; + + int rc = 0; + for (auto p : paths) { + int line_num = 0; + auto result = cfbox::stream::for_each_line(p, [&](const std::string& line, std::size_t) { + bool should_number = false; + switch (body_style) { + case 'a': should_number = true; break; + case 't': should_number = !line.empty(); break; + case 'n': should_number = false; break; + default: should_number = !line.empty(); break; + } + + if (should_number) { + ++line_num; + char numbuf[16]; + if (num_fmt == "ln") { + std::snprintf(numbuf, sizeof(numbuf), "%-6d", line_num); + } else if (num_fmt == "rz") { + std::snprintf(numbuf, sizeof(numbuf), "%06d", line_num); + } else { + std::snprintf(numbuf, sizeof(numbuf), "%6d", line_num); + } + std::printf("%s%s%s\n", numbuf, sep.c_str(), line.c_str()); + } else { + std::printf(" %s%s\n", sep.c_str(), line.c_str()); + } + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox nl: %s\n", result.error().msg.c_str()); + rc = 1; + } + } + return rc; +} diff --git a/src/applets/nohup.cpp b/src/applets/nohup.cpp new file mode 100644 index 0000000..164a83d --- /dev/null +++ b/src/applets/nohup.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "nohup", + .version = CFBOX_VERSION_STRING, + .one_line = "run a command immune to hangups", + .usage = "nohup COMMAND [ARGS]...", + .options = "", + .extra = "Output is appended to nohup.out or $HOME/nohup.out.", +}; +} // namespace + +auto nohup_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox nohup: missing command\n"); + return 1; + } + + signal(SIGHUP, SIG_IGN); + + // Redirect stdout/stderr to nohup.out + std::string outfile = "nohup.out"; + if (const char* home = std::getenv("HOME")) { + // Only use $HOME/nohup.out if cwd is not writable + if (access(".", W_OK) != 0) { + outfile = std::string{home} + "/nohup.out"; + } + } + + auto* f = freopen(outfile.c_str(), "a", stdout); + if (!f) { + std::fprintf(stderr, "cfbox nohup: cannot open %s: %s\n", + outfile.c_str(), std::strerror(errno)); + return 1; + } + dup2(fileno(stdout), STDERR_FILENO); + + std::vector arg_storage; + for (auto p : pos) arg_storage.emplace_back(p); + std::vector cmd_args; + for (auto& s : arg_storage) cmd_args.push_back(s.data()); + cmd_args.push_back(nullptr); + + execvp(cmd_args[0], cmd_args.data()); + std::fprintf(stderr, "cfbox nohup: %s: %s\n", cmd_args[0], std::strerror(errno)); + return 127; +} diff --git a/src/applets/od.cpp b/src/applets/od.cpp new file mode 100644 index 0000000..66bbde2 --- /dev/null +++ b/src/applets/od.cpp @@ -0,0 +1,96 @@ +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "od", + .version = CFBOX_VERSION_STRING, + .one_line = "dump files in octal and other formats", + .usage = "od [-A RADIX] [-t TYPE] [FILE]", + .options = " -A RADIX address radix: d(ecimal), o(ctal), x(hex), n(one)\n" + " -t TYPE output type: o(ctal), x(hex), d(ecimal), u(unsigned), c(har)", + .extra = "", +}; + +static auto print_char(unsigned char c) -> void { + if (c >= 32 && c < 127) { + std::printf(" %c", c); + } else { + switch (c) { + case '\0': std::printf(" \\0"); break; + case '\a': std::printf(" \\a"); break; + case '\b': std::printf(" \\b"); break; + case '\f': std::printf(" \\f"); break; + case '\n': std::printf(" \\n"); break; + case '\r': std::printf(" \\r"); break; + case '\t': std::printf(" \\t"); break; + case '\v': std::printf(" \\v"); break; + default: std::printf("%03o", c); break; + } + } +} +} // namespace + +auto od_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'A', true, "address-radix"}, + cfbox::args::OptSpec{'t', true, "format"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + char addr_radix = 'o'; + if (auto a = parsed.get_any('A', "address-radix")) { + addr_radix = (*a)[0]; + } + + char out_type = 'o'; + if (auto t = parsed.get_any('t', "format")) { + out_type = (*t)[0]; + } + + const auto& pos = parsed.positional(); + auto data_result = pos.empty() ? cfbox::io::read_all_stdin() : cfbox::io::read_all(pos[0]); + if (!data_result) { + std::fprintf(stderr, "cfbox od: %s\n", data_result.error().msg.c_str()); + return 1; + } + + const auto& data = *data_result; + for (std::size_t offset = 0; offset < data.size(); offset += 16) { + // Print address + switch (addr_radix) { + case 'd': std::printf("%07zu", offset); break; + case 'x': std::printf("%07zx", offset); break; + case 'n': break; + default: std::printf("%07zo", offset); break; + } + + auto line_len = static_cast(data.size() - offset); + if (line_len > 16) line_len = 16; + + if (out_type == 'c') { + for (int i = 0; i < line_len; ++i) { + print_char(static_cast(data[offset + i])); + } + } else { + for (int i = 0; i < line_len; ++i) { + auto b = static_cast(data[offset + i]); + switch (out_type) { + case 'x': std::printf(" %02x", b); break; + case 'd': std::printf(" %4d", static_cast(b)); break; + case 'u': std::printf(" %3u", b); break; + default: std::printf(" %03o", b); break; + } + } + } + std::putchar('\n'); + } + return 0; +} diff --git a/src/applets/paste.cpp b/src/applets/paste.cpp new file mode 100644 index 0000000..95fe609 --- /dev/null +++ b/src/applets/paste.cpp @@ -0,0 +1,91 @@ +#include +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "paste", + .version = CFBOX_VERSION_STRING, + .one_line = "merge lines of files", + .usage = "paste [-d DELIMS] [-s] [FILE]...", + .options = " -d DELIMS use DELIMS instead of TABs\n" + " -s paste one file at a time instead of in parallel", + .extra = "", +}; +} // namespace + +auto paste_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'d', true, "delimiters"}, + cfbox::args::OptSpec{'s', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + std::string delims = "\t"; + if (auto d = parsed.get_any('d', "delimiters")) { + delims = std::string{*d}; + } + bool serial = parsed.has('s'); + + const auto& pos = parsed.positional(); + auto paths = pos.empty() ? std::vector{"-"} : pos; + + auto get_delim = [&](std::size_t i) -> char { + if (delims.empty()) return '\t'; + return delims[i % delims.size()]; + }; + + if (serial) { + for (auto p : paths) { + bool first = true; + std::size_t dindex = 0; + auto result = cfbox::stream::for_each_line(p, [&](const std::string& line, std::size_t) { + if (!first) std::putchar(get_delim(dindex++)); + std::fputs(line.c_str(), stdout); + first = false; + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox paste: %s\n", result.error().msg.c_str()); + return 1; + } + std::putchar('\n'); + } + return 0; + } + + // Parallel mode: read all lines from all files + std::vector> all_lines; + std::size_t max_lines = 0; + for (auto p : paths) { + std::vector lines; + auto result = cfbox::stream::for_each_line(p, [&](const std::string& line, std::size_t) { + lines.push_back(line); + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox paste: %s\n", result.error().msg.c_str()); + return 1; + } + if (lines.size() > max_lines) max_lines = lines.size(); + all_lines.push_back(std::move(lines)); + } + + for (std::size_t row = 0; row < max_lines; ++row) { + for (std::size_t col = 0; col < all_lines.size(); ++col) { + if (col > 0) std::putchar(get_delim(col - 1)); + if (row < all_lines[col].size()) { + std::fputs(all_lines[col][row].c_str(), stdout); + } + } + std::putchar('\n'); + } + return 0; +} diff --git a/src/applets/printenv.cpp b/src/applets/printenv.cpp new file mode 100644 index 0000000..9d628f3 --- /dev/null +++ b/src/applets/printenv.cpp @@ -0,0 +1,46 @@ +#include +#include +#include +#include + +#include +#include + +extern char** environ; + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "printenv", + .version = CFBOX_VERSION_STRING, + .one_line = "print all or part of environment", + .usage = "printenv [VARIABLE]...", + .options = "", + .extra = "", +}; +} // namespace + +auto printenv_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + for (char** env = environ; *env != nullptr; ++env) { + std::puts(*env); + } + return 0; + } + + int rc = 0; + for (auto var : pos) { + const char* val = std::getenv(std::string{var}.c_str()); + if (val) { + std::puts(val); + } else { + rc = 1; + } + } + return rc; +} diff --git a/src/applets/readlink.cpp b/src/applets/readlink.cpp new file mode 100644 index 0000000..6014182 --- /dev/null +++ b/src/applets/readlink.cpp @@ -0,0 +1,56 @@ +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "readlink", + .version = CFBOX_VERSION_STRING, + .one_line = "print the value of a symbolic link or canonical file name", + .usage = "readlink [-f] FILE...", + .options = " -f canonicalize by following every symlink\n" + " -n do not output the trailing newline", + .extra = "", +}; +} // namespace + +auto readlink_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'f', false}, + cfbox::args::OptSpec{'n', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool canonicalize = parsed.has('f'); + bool no_newline = parsed.has('n'); + const auto& pos = parsed.positional(); + + if (pos.empty()) { + std::fprintf(stderr, "cfbox readlink: missing operand\n"); + return 1; + } + + int rc = 0; + for (auto p : pos) { + std::string path{p}; + cfbox::base::Result result; + if (canonicalize) { + result = cfbox::fs::canonical(path); + } else { + result = cfbox::fs::read_symlink(path); + } + if (!result) { + std::fprintf(stderr, "cfbox readlink: %s\n", result.error().msg.c_str()); + rc = 1; + continue; + } + std::fputs(result->c_str(), stdout); + if (!no_newline) std::putchar('\n'); + } + return rc; +} diff --git a/src/applets/realpath.cpp b/src/applets/realpath.cpp new file mode 100644 index 0000000..4f763f5 --- /dev/null +++ b/src/applets/realpath.cpp @@ -0,0 +1,42 @@ +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "realpath", + .version = CFBOX_VERSION_STRING, + .one_line = "print the resolved path", + .usage = "realpath FILE...", + .options = "", + .extra = "", +}; +} // namespace + +auto realpath_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox realpath: missing operand\n"); + return 1; + } + + int rc = 0; + for (auto p : pos) { + auto result = cfbox::fs::canonical(std::string{p}); + if (!result) { + std::fprintf(stderr, "cfbox realpath: %s\n", result.error().msg.c_str()); + rc = 1; + continue; + } + std::puts(result->c_str()); + } + return rc; +} diff --git a/src/applets/rmdir.cpp b/src/applets/rmdir.cpp new file mode 100644 index 0000000..f8e4717 --- /dev/null +++ b/src/applets/rmdir.cpp @@ -0,0 +1,42 @@ +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "rmdir", + .version = CFBOX_VERSION_STRING, + .one_line = "remove empty directories", + .usage = "rmdir DIRECTORY...", + .options = "", + .extra = "", +}; +} // namespace + +auto rmdir_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox rmdir: missing operand\n"); + return 1; + } + + int rc = 0; + for (auto p : pos) { + std::error_code ec; + std::filesystem::remove(std::filesystem::path{p}, ec); + if (ec) { + std::fprintf(stderr, "cfbox rmdir: failed to remove '%.*s': %s\n", + static_cast(p.size()), p.data(), ec.message().c_str()); + rc = 1; + } + } + return rc; +} diff --git a/src/applets/seq.cpp b/src/applets/seq.cpp new file mode 100644 index 0000000..36d475f --- /dev/null +++ b/src/applets/seq.cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "seq", + .version = CFBOX_VERSION_STRING, + .one_line = "print a sequence of numbers", + .usage = "seq [-s SEP] [-w] [FIRST [INCREMENT]] LAST", + .options = " -s SEP use SEP to separate numbers (default: \\n)\n" + " -w equalize widths by padding with leading zeros", + .extra = "", +}; +} // namespace + +auto seq_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'s', true, "separator"}, + cfbox::args::OptSpec{'w', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + std::string sep = "\n"; + if (auto s = parsed.get_any('s', "separator")) { + sep = std::string{*s}; + } + bool equal_width = parsed.has('w'); + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox seq: missing operand\n"); + return 1; + } + + double first = 1, incr = 1, last; + if (pos.size() == 1) { + last = std::stod(std::string{pos[0]}); + } else if (pos.size() == 2) { + first = std::stod(std::string{pos[0]}); + last = std::stod(std::string{pos[1]}); + } else { + first = std::stod(std::string{pos[0]}); + incr = std::stod(std::string{pos[1]}); + last = std::stod(std::string{pos[2]}); + } + + if (incr == 0) { + std::fprintf(stderr, "cfbox seq: zero increment\n"); + return 1; + } + + // Determine width for -w + int width = 0; + if (equal_width) { + char buf[64]; + std::snprintf(buf, sizeof(buf), "%g", last); + width = static_cast(std::strlen(buf)); + std::snprintf(buf, sizeof(buf), "%g", first); + auto w2 = static_cast(std::strlen(buf)); + if (w2 > width) width = w2; + std::snprintf(buf, sizeof(buf), "%g", incr); + w2 = static_cast(std::strlen(buf)); + if (w2 > width) width = w2; + } + + bool first_out = true; + if (incr > 0) { + for (double v = first; v <= last + 1e-10; v += incr) { + if (!first_out) std::fputs(sep.c_str(), stdout); + if (equal_width) { + std::printf("%0*g", width, v); + } else { + std::printf("%g", v); + } + first_out = false; + } + } else { + for (double v = first; v >= last - 1e-10; v += incr) { + if (!first_out) std::fputs(sep.c_str(), stdout); + if (equal_width) { + std::printf("%0*g", width, v); + } else { + std::printf("%g", v); + } + first_out = false; + } + } + if (!first_out) std::putchar('\n'); + return 0; +} diff --git a/src/applets/shuf.cpp b/src/applets/shuf.cpp new file mode 100644 index 0000000..a2c3186 --- /dev/null +++ b/src/applets/shuf.cpp @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "shuf", + .version = CFBOX_VERSION_STRING, + .one_line = "generate random permutations", + .usage = "shuf [-n COUNT] [-e] [FILE]", + .options = " -n COUNT output at most COUNT lines\n" + " -e treat each argument as an input line", + .extra = "", +}; +} // namespace + +auto shuf_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'n', true, "head-count"}, + cfbox::args::OptSpec{'e', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + int max_count = -1; + if (auto n = parsed.get_any('n', "head-count")) { + max_count = std::stoi(std::string{*n}); + } + bool echo_mode = parsed.has('e'); + + std::vector lines; + if (echo_mode) { + for (auto p : parsed.positional()) { + lines.emplace_back(p); + } + } else { + const auto& pos = parsed.positional(); + auto path = pos.empty() ? std::string_view{"-"} : pos[0]; + auto result = cfbox::stream::for_each_line(path, [&](const std::string& line, std::size_t) { + lines.push_back(line); + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox shuf: %s\n", result.error().msg.c_str()); + return 1; + } + } + + std::random_device rd; + std::mt19937 gen(rd()); + std::shuffle(lines.begin(), lines.end(), gen); + + int count = 0; + for (const auto& line : lines) { + std::puts(line.c_str()); + ++count; + if (max_count > 0 && count >= max_count) break; + } + return 0; +} diff --git a/src/applets/split.cpp b/src/applets/split.cpp new file mode 100644 index 0000000..254c667 --- /dev/null +++ b/src/applets/split.cpp @@ -0,0 +1,110 @@ +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "split", + .version = CFBOX_VERSION_STRING, + .one_line = "split a file into pieces", + .usage = "split [-l LINES] [-b BYTES] [INPUT [PREFIX]]", + .options = " -l LINES put LINES lines per output file (default 1000)\n" + " -b BYTES put BYTES bytes per output file\n" + " -d use numeric suffixes instead of alphabetic", + .extra = "", +}; + +static auto next_alpha_suffix(int n) -> std::string { + std::string result; + result += static_cast('a' + (n / 26) % 26); + result += static_cast('a' + n % 26); + return result; +} + +static auto next_digit_suffix(int n) -> std::string { + char buf[8]; + std::snprintf(buf, sizeof(buf), "%02d", n); + return buf; +} +} // namespace + +auto split_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'l', true, "lines"}, + cfbox::args::OptSpec{'b', true, "bytes"}, + cfbox::args::OptSpec{'d', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + long lines = 1000; + long bytes = 0; + if (auto l = parsed.get_any('l', "lines")) { + lines = std::stol(std::string{*l}); + } + if (auto b = parsed.get_any('b', "bytes")) { + bytes = std::stol(std::string{*b}); + } + bool numeric = parsed.has('d'); + + const auto& pos = parsed.positional(); + std::string input_path = pos.empty() ? "-" : std::string{pos[0]}; + std::string prefix = (pos.size() > 1) ? std::string{pos[1]} : "x"; + + auto data_result = (input_path == "-") ? cfbox::io::read_all_stdin() : cfbox::io::read_all(input_path); + if (!data_result) { + std::fprintf(stderr, "cfbox split: %s\n", data_result.error().msg.c_str()); + return 1; + } + + const auto& data = *data_result; + int file_num = 0; + + if (bytes > 0) { + for (std::size_t offset = 0; offset < data.size(); offset += static_cast(bytes)) { + auto suffix = numeric ? next_digit_suffix(file_num) : next_alpha_suffix(file_num); + auto fname = prefix + suffix; + auto len = static_cast(bytes); + if (offset + len > data.size()) len = data.size() - offset; + auto wresult = cfbox::io::write_all(fname, std::string_view{data.data() + offset, len}); + if (!wresult) { + std::fprintf(stderr, "cfbox split: %s\n", wresult.error().msg.c_str()); + return 1; + } + ++file_num; + } + } else { + std::size_t line_start = 0; + long line_count = 0; + for (std::size_t i = 0; i <= data.size(); ++i) { + if (i == data.size() || data[i] == '\n') { + ++line_count; + if (line_count >= lines || i == data.size()) { + if (line_start < i || (line_count > 0 && line_count >= lines)) { + auto suffix = numeric ? next_digit_suffix(file_num) : next_alpha_suffix(file_num); + auto fname = prefix + suffix; + auto end = i < data.size() ? i + 1 : i; + auto len = end - line_start; + if (len > 0) { + auto wresult = cfbox::io::write_all(fname, + std::string_view{data.data() + line_start, len}); + if (!wresult) { + std::fprintf(stderr, "cfbox split: %s\n", wresult.error().msg.c_str()); + return 1; + } + } + ++file_num; + line_start = end; + line_count = 0; + } + } + } + } + } + return 0; +} diff --git a/src/applets/stat.cpp b/src/applets/stat.cpp new file mode 100644 index 0000000..c79b977 --- /dev/null +++ b/src/applets/stat.cpp @@ -0,0 +1,172 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "stat", + .version = CFBOX_VERSION_STRING, + .one_line = "display file or file system status", + .usage = "stat [-c FORMAT] FILE...", + .options = " -c FORMAT use the specified FORMAT instead of the default\n" + " -f display file system status", + .extra = "Format escapes: %n name, %s size, %b blocks, %f blocks*512, %F type,\n" + " %U user, %G group, %a octal perms, %A human perms, %y mtime", +}; +} // namespace + +static auto file_type_string(mode_t mode) -> const char* { + if (S_ISREG(mode)) return "regular file"; + if (S_ISDIR(mode)) return "directory"; + if (S_ISLNK(mode)) return "symbolic link"; + if (S_ISCHR(mode)) return "character special file"; + if (S_ISBLK(mode)) return "block special file"; + if (S_ISFIFO(mode)) return "fifo"; + if (S_ISSOCK(mode)) return "socket"; + return "unknown"; +} + +static auto format_perms(mode_t mode) -> std::string { + char buf[12]; + buf[0] = S_ISDIR(mode) ? 'd' : S_ISLNK(mode) ? 'l' : S_ISCHR(mode) ? 'c' : + S_ISBLK(mode) ? 'b' : S_ISFIFO(mode) ? 'p' : S_ISSOCK(mode) ? 's' : '-'; + buf[1] = (mode & S_IRUSR) ? 'r' : '-'; + buf[2] = (mode & S_IWUSR) ? 'w' : '-'; + buf[3] = (mode & S_IXUSR) ? 'x' : '-'; + buf[4] = (mode & S_IRGRP) ? 'r' : '-'; + buf[5] = (mode & S_IWGRP) ? 'w' : '-'; + buf[6] = (mode & S_IXGRP) ? 'x' : '-'; + buf[7] = (mode & S_IROTH) ? 'r' : '-'; + buf[8] = (mode & S_IWOTH) ? 'w' : '-'; + buf[9] = (mode & S_IXOTH) ? 'x' : '-'; + buf[10] = '\0'; + return buf; +} + +static auto format_time(const timespec& ts) -> std::string { + char buf[64]; + auto* tm = localtime(&ts.tv_sec); + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm); + return buf; +} + +auto stat_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'c', true, "format"}, + cfbox::args::OptSpec{'f', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + auto format = parsed.get_any('c', "format"); + bool fs_stat = parsed.has('f'); + const auto& pos = parsed.positional(); + + if (pos.empty()) { + std::fprintf(stderr, "cfbox stat: missing operand\n"); + return 1; + } + + int rc = 0; + for (auto p : pos) { + std::string path{p}; + + if (fs_stat) { + struct statvfs vfs; + if (statvfs(path.c_str(), &vfs) != 0) { + std::fprintf(stderr, "cfbox stat: cannot stat '%s': %s\n", + path.c_str(), std::strerror(errno)); + rc = 1; + continue; + } + std::printf(" File: \"%s\"\n", path.c_str()); + std::printf(" ID: %-12lx Namelen: %-8lu Type: %s\n", + static_cast(vfs.f_fsid), + static_cast(vfs.f_namemax), + vfs.f_type == 0xEF53 ? "ext2/ext3" : "unknown"); + std::printf("Block size: %-10lu Total blocks: %-10lu Free blocks: %lu\n", + static_cast(vfs.f_bsize), + static_cast(vfs.f_blocks), + static_cast(vfs.f_bfree)); + continue; + } + + struct stat st; + if (lstat(path.c_str(), &st) != 0) { + std::fprintf(stderr, "cfbox stat: cannot stat '%s': %s\n", + path.c_str(), std::strerror(errno)); + rc = 1; + continue; + } + + if (format) { + std::string fmt{*format}; + for (std::size_t i = 0; i < fmt.size(); ++i) { + if (fmt[i] == '%' && i + 1 < fmt.size()) { + char spec = fmt[++i]; + switch (spec) { + case 'n': std::fputs(path.c_str(), stdout); break; + case 's': std::printf("%lu", static_cast(st.st_size)); break; + case 'b': std::printf("%lu", static_cast(st.st_blocks)); break; + case 'f': std::printf("%lu", static_cast(st.st_blocks * 512)); break; + case 'F': std::fputs(file_type_string(st.st_mode), stdout); break; + case 'U': { + auto* pw = getpwuid(st.st_uid); + std::fputs(pw ? pw->pw_name : std::to_string(st.st_uid).c_str(), stdout); + break; + } + case 'G': { + auto* gr = getgrgid(st.st_gid); + std::fputs(gr ? gr->gr_name : std::to_string(st.st_gid).c_str(), stdout); + break; + } + case 'a': std::printf("%o", st.st_mode & 07777); break; + case 'A': std::fputs(format_perms(st.st_mode).c_str(), stdout); break; + case 'y': { +#if defined(__linux__) + std::fputs(format_time(st.st_mtim).c_str(), stdout); +#else + std::fputs(format_time({st.st_mtime, 0}).c_str(), stdout); +#endif + break; + } + default: std::putchar('%'); std::putchar(spec); break; + } + } else { + std::putchar(fmt[i]); + } + } + std::putchar('\n'); + } else { + std::printf(" File: %s\n", path.c_str()); + std::printf(" Size: %-15lu Blocks: %-10lu IO Block: %lu %s\n", + static_cast(st.st_size), + static_cast(st.st_blocks), + static_cast(st.st_blksize), + file_type_string(st.st_mode)); + auto* pw = getpwuid(st.st_uid); + auto* gr = getgrgid(st.st_gid); + std::printf("Access: (%04o/%s) Uid: (%5d/%-8s) Gid: (%5d/%-8s)\n", + st.st_mode & 07777, + format_perms(st.st_mode).c_str(), + st.st_uid, pw ? pw->pw_name : "", + st.st_gid, gr ? gr->gr_name : ""); +#if defined(__linux__) + std::printf("Modify: %s\n", format_time(st.st_mtim).c_str()); + std::printf("Change: %s\n", format_time(st.st_ctim).c_str()); +#else + std::printf("Modify: %s\n", format_time({st.st_mtime, 0}).c_str()); +#endif + } + } + return rc; +} diff --git a/src/applets/sum.cpp b/src/applets/sum.cpp new file mode 100644 index 0000000..865301e --- /dev/null +++ b/src/applets/sum.cpp @@ -0,0 +1,52 @@ +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "sum", + .version = CFBOX_VERSION_STRING, + .one_line = "checksum and count the blocks in a file", + .usage = "sum [-s] [FILE]...", + .options = " -s use System V sum algorithm (512-byte blocks)", + .extra = "", +}; +} // namespace + +auto sum_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'s', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool sysv = parsed.has('s'); + const auto& pos = parsed.positional(); + auto paths = pos.empty() ? std::vector{"-"} : pos; + + int rc = 0; + for (auto p : paths) { + auto data_result = (p == "-") ? cfbox::io::read_all_stdin() : cfbox::io::read_all(p); + if (!data_result) { + std::fprintf(stderr, "cfbox sum: %s\n", data_result.error().msg.c_str()); + rc = 1; + continue; + } + + if (sysv) { + auto result = cfbox::checksum::sysv_sum(*data_result); + std::printf("%d %d", result.checksum, result.blocks); + } else { + auto result = cfbox::checksum::bsd_sum(*data_result); + std::printf("%05d %5d", result.checksum, result.blocks); + } + if (p != "-") std::printf(" %.*s", static_cast(p.size()), p.data()); + std::putchar('\n'); + } + return rc; +} diff --git a/src/applets/sync.cpp b/src/applets/sync.cpp new file mode 100644 index 0000000..e7231af --- /dev/null +++ b/src/applets/sync.cpp @@ -0,0 +1,26 @@ +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "sync", + .version = CFBOX_VERSION_STRING, + .one_line = "synchronize cached writes to persistent storage", + .usage = "sync", + .options = "", + .extra = "", +}; +} // namespace + +auto sync_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + ::sync(); + return 0; +} diff --git a/src/applets/tac.cpp b/src/applets/tac.cpp new file mode 100644 index 0000000..90156ff --- /dev/null +++ b/src/applets/tac.cpp @@ -0,0 +1,47 @@ +#include +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "tac", + .version = CFBOX_VERSION_STRING, + .one_line = "concatenate and print files in reverse", + .usage = "tac [FILE]...", + .options = "", + .extra = "", +}; +} // namespace + +auto tac_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + auto paths = pos.empty() ? std::vector{"-"} : pos; + + int rc = 0; + for (auto p : paths) { + std::vector lines; + auto result = cfbox::stream::for_each_line(p, [&](const std::string& line, std::size_t) { + lines.push_back(line); + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox tac: %s\n", result.error().msg.c_str()); + rc = 1; + continue; + } + for (auto it = lines.rbegin(); it != lines.rend(); ++it) { + std::puts(it->c_str()); + } + } + return rc; +} diff --git a/src/applets/tee.cpp b/src/applets/tee.cpp new file mode 100644 index 0000000..056c12b --- /dev/null +++ b/src/applets/tee.cpp @@ -0,0 +1,56 @@ +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "tee", + .version = CFBOX_VERSION_STRING, + .one_line = "read from stdin and write to stdout and files", + .usage = "tee [-a] [FILE]...", + .options = " -a append to the given FILEs, do not overwrite", + .extra = "", +}; +} // namespace + +auto tee_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'a', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool append = parsed.has('a'); + const auto& pos = parsed.positional(); + + std::vector files; + for (auto p : pos) { + auto* f = std::fopen(std::string{p}.c_str(), append ? "ab" : "wb"); + if (!f) { + std::fprintf(stderr, "cfbox tee: %s: %s\n", std::string{p}.c_str(), std::strerror(errno)); + } else { + files.push_back(f); + } + } + + char buf[4096]; + int rc = 0; + while (auto n = std::fread(buf, 1, sizeof(buf), stdin)) { + std::fwrite(buf, 1, n, stdout); + for (auto* f : files) { + if (std::fwrite(buf, 1, n, f) != n) { + rc = 1; + } + } + } + + for (auto* f : files) { + std::fclose(f); + } + return rc; +} diff --git a/src/applets/timeout.cpp b/src/applets/timeout.cpp new file mode 100644 index 0000000..9bf76ab --- /dev/null +++ b/src/applets/timeout.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "timeout", + .version = CFBOX_VERSION_STRING, + .one_line = "run a command with a time limit", + .usage = "timeout DURATION COMMAND [ARGS]...", + .options = " -s SIG send SIG on timeout (default: TERM)\n" + " -k DUR also send KILL after DUR", + .extra = "DURATION supports floating point (e.g. 2.5 for 2.5 seconds).", +}; +} // namespace + +auto timeout_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'s', true, "signal"}, + cfbox::args::OptSpec{'k', true, "kill-after"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.size() < 2) { + std::fprintf(stderr, "cfbox timeout: missing operand\n"); + return 1; + } + + char* end = nullptr; + double duration = std::strtod(std::string{pos[0]}.c_str(), &end); + if (end == std::string{pos[0]}.c_str() || duration <= 0) { + std::fprintf(stderr, "cfbox timeout: invalid duration '%.*s'\n", + static_cast(pos[0].size()), pos[0].data()); + return 1; + } + + int sig = SIGTERM; + if (auto s = parsed.get_any('s', "signal")) { + auto name = std::string{*s}; + if (name == "TERM" || name == "15") sig = SIGTERM; + else if (name == "KILL" || name == "9") sig = SIGKILL; + else if (name == "INT" || name == "2") sig = SIGINT; + else if (name == "HUP" || name == "1") sig = SIGHUP; + else { + std::fprintf(stderr, "cfbox timeout: unknown signal '%s'\n", name.c_str()); + return 1; + } + } + + pid_t pid = fork(); + if (pid < 0) { + std::fprintf(stderr, "cfbox timeout: fork failed: %s\n", std::strerror(errno)); + return 1; + } + + if (pid == 0) { + // Child + std::vector arg_storage; + for (std::size_t i = 1; i < pos.size(); ++i) { + arg_storage.emplace_back(pos[i]); + } + std::vector cmd_args; + for (auto& s : arg_storage) cmd_args.push_back(s.data()); + cmd_args.push_back(nullptr); + execvp(cmd_args[0], cmd_args.data()); + std::fprintf(stderr, "cfbox timeout: failed to execute '%s': %s\n", + cmd_args[0], std::strerror(errno)); + _exit(125); + } + + // Parent: wait with timeout + auto usecs = static_cast(duration * 1000000); + usleep(usecs); + + int status; + pid_t result = waitpid(pid, &status, WNOHANG); + if (result == 0) { + // Still running, send signal + kill(pid, sig); + if (auto k = parsed.get_any('k', "kill-after")) { + double kill_dur = std::strtod(std::string{*k}.c_str(), nullptr); + auto kill_usecs = static_cast(kill_dur * 1000000); + usleep(kill_usecs); + result = waitpid(pid, &status, WNOHANG); + if (result == 0) { + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + } + } else { + waitpid(pid, &status, 0); + } + return 124; + } + + if (WIFEXITED(status)) return WEXITSTATUS(status); + if (WIFSIGNALED(status)) return 128 + WTERMSIG(status); + return 1; +} diff --git a/src/applets/touch.cpp b/src/applets/touch.cpp new file mode 100644 index 0000000..8df3e13 --- /dev/null +++ b/src/applets/touch.cpp @@ -0,0 +1,61 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "touch", + .version = CFBOX_VERSION_STRING, + .one_line = "change file timestamps", + .usage = "touch [-c] FILE...", + .options = " -c do not create any files", + .extra = "", +}; +} // namespace + +auto touch_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'c', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool no_create = parsed.has('c'); + const auto& pos = parsed.positional(); + + if (pos.empty()) { + std::fprintf(stderr, "cfbox touch: missing operand\n"); + return 1; + } + + int rc = 0; + for (auto p : pos) { + std::string path{p}; + if (::access(path.c_str(), F_OK) != 0) { + if (no_create) continue; + int fd = ::open(path.c_str(), O_CREAT | O_WRONLY, 0666); + if (fd < 0) { + std::fprintf(stderr, "cfbox touch: cannot touch '%s': %s\n", + path.c_str(), std::strerror(errno)); + rc = 1; + continue; + } + ::close(fd); + } + // Update timestamps + if (::utime(path.c_str(), nullptr) != 0) { + std::fprintf(stderr, "cfbox touch: cannot touch '%s': %s\n", + path.c_str(), std::strerror(errno)); + rc = 1; + } + } + return rc; +} diff --git a/src/applets/tr.cpp b/src/applets/tr.cpp new file mode 100644 index 0000000..80acb8b --- /dev/null +++ b/src/applets/tr.cpp @@ -0,0 +1,118 @@ +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "tr", + .version = CFBOX_VERSION_STRING, + .one_line = "translate, squeeze, and/or delete characters", + .usage = "tr [-c] [-d] [-s] SET1 [SET2]", + .options = " -c use the complement of SET1\n" + " -d delete characters in SET1\n" + " -s squeeze repeated characters", + .extra = "Ranges like a-z and A-Z are supported.", +}; +} // namespace + +static auto expand_set(const std::string& s) -> std::string { + std::string result; + for (std::size_t i = 0; i < s.size(); ++i) { + if (i + 2 < s.size() && s[i + 1] == '-') { + for (unsigned char c = static_cast(s[i]); + c <= static_cast(s[i + 2]); ++c) { + result += static_cast(c); + } + i += 2; + } else { + result += s[i]; + } + } + return result; +} + +auto tr_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'c', false, "complement"}, + cfbox::args::OptSpec{'d', false, "delete"}, + cfbox::args::OptSpec{'s', false, "squeeze-repeats"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool complement = parsed.has('c') || parsed.has_long("complement"); + bool del_mode = parsed.has('d') || parsed.has_long("delete"); + bool squeeze = parsed.has('s') || parsed.has_long("squeeze-repeats"); + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox tr: missing operand\n"); + return 1; + } + + std::string set1 = expand_set(std::string{pos[0]}); + std::string set2; + if (pos.size() > 1) { + set2 = expand_set(std::string{pos[1]}); + } + + // Build lookup for complement mode + bool in_set1[256] = {}; + for (char c : set1) { + in_set1[static_cast(c)] = true; + } + + // Build translation map + char translate[256]; + for (int i = 0; i < 256; ++i) translate[i] = static_cast(i); + if (!del_mode && !set2.empty()) { + if (!complement) { + for (std::size_t i = 0; i < set1.size(); ++i) { + auto idx = static_cast(set1[i]); + translate[idx] = (i < set2.size()) ? set2[i] : set2.back(); + } + } else { + std::size_t j = 0; + for (int i = 0; i < 256; ++i) { + if (!in_set1[i]) { + if (j < set2.size()) { + translate[i] = set2[j]; + } else { + translate[i] = set2.back(); + } + ++j; + } + } + } + } + + auto input = cfbox::io::read_all_stdin(); + if (!input) { + std::fprintf(stderr, "cfbox tr: %s\n", input.error().msg.c_str()); + return 1; + } + + char prev = '\0'; + for (char c : *input) { + auto uc = static_cast(c); + bool match = complement ? !in_set1[uc] : in_set1[uc]; + + if (del_mode) { + if (match) continue; + if (squeeze && c == prev) continue; + prev = c; + std::putchar(c); + } else { + char out = translate[uc]; + if (squeeze && out == prev) continue; + prev = out; + std::putchar(out); + } + } + return 0; +} diff --git a/src/applets/truncate.cpp b/src/applets/truncate.cpp new file mode 100644 index 0000000..8bd438e --- /dev/null +++ b/src/applets/truncate.cpp @@ -0,0 +1,71 @@ +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "truncate", + .version = CFBOX_VERSION_STRING, + .one_line = "shrink or extend the size of a file to the specified size", + .usage = "truncate -s SIZE FILE...", + .options = " -s SIZE set file size (supports K, M, G suffixes)", + .extra = "", +}; + +auto parse_size(const std::string& s) -> long { + char* end = nullptr; + long val = std::strtol(s.c_str(), &end, 10); + if (end == s.c_str()) return -1; + if (*end != '\0') { + switch (*end) { + case 'K': case 'k': val *= 1024; break; + case 'M': case 'm': val *= 1024 * 1024; break; + case 'G': case 'g': val *= 1024L * 1024 * 1024; break; + default: return -1; + } + } + return val; +} +} // namespace + +auto truncate_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'s', true, "size"}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + auto size_str = parsed.get_any('s', "size"); + if (!size_str) { + std::fprintf(stderr, "cfbox truncate: missing operand: -s SIZE\n"); + return 1; + } + + long size = parse_size(std::string{*size_str}); + if (size < 0) { + std::fprintf(stderr, "cfbox truncate: invalid size: '%.*s'\n", + static_cast(size_str->size()), size_str->data()); + return 1; + } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox truncate: missing operand\n"); + return 1; + } + + int rc = 0; + for (auto p : pos) { + auto result = cfbox::fs::resize_file(std::string{p}, static_cast(size)); + if (!result) { + std::fprintf(stderr, "cfbox truncate: %s\n", result.error().msg.c_str()); + rc = 1; + } + } + return rc; +} diff --git a/src/applets/tsort.cpp b/src/applets/tsort.cpp new file mode 100644 index 0000000..28aae77 --- /dev/null +++ b/src/applets/tsort.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "tsort", + .version = CFBOX_VERSION_STRING, + .one_line = "perform topological sort", + .usage = "tsort [FILE]", + .options = "", + .extra = "Input is pairs of items (whitespace-separated). Output is topologically sorted.", +}; +} // namespace + +auto tsort_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + auto path = pos.empty() ? std::string_view{"-"} : pos[0]; + + std::map> graph; + std::map in_degree; + + auto result = cfbox::stream::for_each_line(path, [&](const std::string& line, std::size_t) { + auto fields = cfbox::stream::split_whitespace(line); + for (std::size_t i = 0; i + 1 < fields.size(); i += 2) { + auto& a = fields[i]; + auto& b = fields[i + 1]; + if (graph[a].insert(b).second) { + ++in_degree[b]; + } + in_degree.try_emplace(a, 0); + } + return true; + }); + if (!result) { + std::fprintf(stderr, "cfbox tsort: %s\n", result.error().msg.c_str()); + return 1; + } + + // Kahn's algorithm + std::queue q; + for (const auto& [node, deg] : in_degree) { + if (deg == 0) q.push(node); + } + + while (!q.empty()) { + auto node = q.front(); + q.pop(); + std::puts(node.c_str()); + if (graph.count(node)) { + for (const auto& neighbor : graph[node]) { + if (--in_degree[neighbor] == 0) { + q.push(neighbor); + } + } + } + } + return 0; +} diff --git a/src/applets/unlink.cpp b/src/applets/unlink.cpp new file mode 100644 index 0000000..b098f3b --- /dev/null +++ b/src/applets/unlink.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "unlink", + .version = CFBOX_VERSION_STRING, + .one_line = "call the unlink function to remove a file", + .usage = "unlink FILE", + .options = "", + .extra = "", +}; +} // namespace + +auto unlink_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox unlink: missing operand\n"); + return 1; + } + if (pos.size() > 1) { + std::fprintf(stderr, "cfbox unlink: extra operand '%.*s'\n", + static_cast(pos[1].size()), pos[1].data()); + return 1; + } + + if (::unlink(std::string{pos[0]}.c_str()) != 0) { + std::fprintf(stderr, "cfbox unlink: cannot unlink '%.*s': %s\n", + static_cast(pos[0].size()), pos[0].data(), std::strerror(errno)); + return 1; + } + return 0; +} diff --git a/src/applets/usleep.cpp b/src/applets/usleep.cpp new file mode 100644 index 0000000..02d623c --- /dev/null +++ b/src/applets/usleep.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "usleep", + .version = CFBOX_VERSION_STRING, + .one_line = "sleep for a specified number of microseconds", + .usage = "usleep MICROSECONDS", + .options = "", + .extra = "", +}; +} // namespace + +auto usleep_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox usleep: missing operand\n"); + return 1; + } + + char* end = nullptr; + long usecs = std::strtol(std::string{pos[0]}.c_str(), &end, 10); + if (end == std::string{pos[0]}.c_str() || usecs < 0) { + std::fprintf(stderr, "cfbox usleep: invalid time interval '%.*s'\n", + static_cast(pos[0].size()), pos[0].data()); + return 1; + } + + std::this_thread::sleep_for(std::chrono::microseconds(usecs)); + return 0; +} diff --git a/src/applets/who.cpp b/src/applets/who.cpp new file mode 100644 index 0000000..8077447 --- /dev/null +++ b/src/applets/who.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "who", + .version = CFBOX_VERSION_STRING, + .one_line = "show who is logged on", + .usage = "who", + .options = "", + .extra = "", +}; +} // namespace + +auto who_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + utmp* entry; + setutent(); + while ((entry = getutent()) != nullptr) { + if (entry->ut_type != USER_PROCESS) continue; + char timebuf[32]; + time_t t = static_cast(entry->ut_tv.tv_sec); + auto* tm = localtime(&t); + strftime(timebuf, sizeof(timebuf), "%b %d %H:%M", tm); + std::printf("%-8s %-12s %s\n", entry->ut_user, entry->ut_line, timebuf); + } + endutent(); + return 0; +} diff --git a/src/applets/xargs.cpp b/src/applets/xargs.cpp new file mode 100644 index 0000000..eaeaa82 --- /dev/null +++ b/src/applets/xargs.cpp @@ -0,0 +1,169 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { +constexpr cfbox::help::HelpEntry HELP = { + .name = "xargs", + .version = CFBOX_VERSION_STRING, + .one_line = "build and execute command lines from stdin", + .usage = "xargs [OPTIONS] [COMMAND [INITIAL-ARGS]...]", + .options = " -n N use at most N args per command\n" + " -I REPLACE replace string REPLACE in initial args\n" + " -0 null-delimited input\n" + " -r do not run if stdin is empty\n" + " -t print command before executing", + .extra = "Default command is echo.", +}; +} // namespace + +auto xargs_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'n', true, "max-args"}, + cfbox::args::OptSpec{'I', true, "replace"}, + cfbox::args::OptSpec{'0', false}, + cfbox::args::OptSpec{'r', false}, + cfbox::args::OptSpec{'t', false}, + }); + + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + int max_args = 0; + if (auto n = parsed.get_any('n', "max-args")) { + max_args = std::stoi(std::string{*n}); + } + + std::string replace_str; + if (auto r = parsed.get_any('I', "replace")) { + replace_str = std::string{*r}; + if (max_args == 0) max_args = 1; + } + + bool null_delim = parsed.has('0') || parsed.has_long("null"); + bool no_run_empty = parsed.has('r') || parsed.has_long("no-run-if-empty"); + bool trace = parsed.has('t') || parsed.has_long("verbose"); + + // Determine command + initial args + std::string command = "echo"; + std::vector initial_args; + const auto& pos = parsed.positional(); + if (!pos.empty()) { + command = std::string{pos[0]}; + for (std::size_t i = 1; i < pos.size(); ++i) { + initial_args.emplace_back(pos[i]); + } + } + + // Read items from stdin + auto input = cfbox::io::read_all_stdin(); + if (!input) { + std::fprintf(stderr, "cfbox xargs: %s\n", input.error().msg.c_str()); + return 1; + } + + std::vector items; + if (null_delim) { + std::string item; + for (char c : *input) { + if (c == '\0') { + if (!item.empty()) items.push_back(std::move(item)); + item.clear(); + } else { + item += c; + } + } + if (!item.empty()) items.push_back(std::move(item)); + } else { + std::string item; + for (char c : *input) { + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + if (!item.empty()) { items.push_back(std::move(item)); item.clear(); } + } else { + item += c; + } + } + if (!item.empty()) items.push_back(std::move(item)); + } + + if (items.empty() && no_run_empty) return 0; + + auto run_command = [&](const std::vector& args) -> int { + std::vector all_args_storage; + std::vector exec_args; + + all_args_storage.push_back(command); + exec_args.push_back(all_args_storage.back().data()); + + if (!replace_str.empty()) { + for (const auto& a : initial_args) { + std::string expanded = a; + auto rpos = expanded.find(replace_str); + while (rpos != std::string::npos) { + expanded.replace(rpos, replace_str.size(), args[0]); + rpos = expanded.find(replace_str, rpos + args[0].size()); + } + all_args_storage.push_back(std::move(expanded)); + exec_args.push_back(all_args_storage.back().data()); + } + } else { + for (const auto& a : initial_args) { + all_args_storage.push_back(a); + exec_args.push_back(all_args_storage.back().data()); + } + for (const auto& a : args) { + all_args_storage.push_back(a); + exec_args.push_back(all_args_storage.back().data()); + } + } + exec_args.push_back(nullptr); + + if (trace) { + std::fprintf(stderr, "%s", command.c_str()); + for (std::size_t i = 1; exec_args[i]; ++i) { + std::fprintf(stderr, " %s", exec_args[i]); + } + std::fputc('\n', stderr); + } + + pid_t pid = fork(); + if (pid < 0) { + std::fprintf(stderr, "cfbox xargs: fork: %s\n", std::strerror(errno)); + return 1; + } + if (pid == 0) { + execvp(command.c_str(), exec_args.data()); + std::fprintf(stderr, "cfbox xargs: %s: %s\n", command.c_str(), std::strerror(errno)); + _exit(127); + } + int status; + waitpid(pid, &status, 0); + if (WIFEXITED(status)) return WEXITSTATUS(status); + if (WIFSIGNALED(status)) return 128 + WTERMSIG(status); + return 1; + }; + + int rc = 0; + std::vector batch; + for (const auto& item : items) { + batch.push_back(item); + if (max_args > 0 && static_cast(batch.size()) >= max_args) { + int r = run_command(batch); + if (r > rc) rc = r; + batch.clear(); + } + } + if (!batch.empty()) { + int r = run_command(batch); + if (r > rc) rc = r; + } + return rc; +} diff --git a/tests/integration/test_cksum.sh b/tests/integration/test_cksum.sh new file mode 100644 index 0000000..2d03eeb --- /dev/null +++ b/tests/integration/test_cksum.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +printf "hello world\n" > "$tmpdir/cksum.txt" + +# cksum on a file produces numbers (checksum and size) +out=$("$CFBOX" cksum "$tmpdir/cksum.txt") +if [[ "$out" =~ ^[0-9]+[[:space:]]+[0-9]+ ]]; then ((++pass)); else echo "FAIL [cksum output]: expected numbers, got $(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 cksum --help +((++pass)) + +echo "cksum: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_cut.sh b/tests/integration/test_cut.sh new file mode 100644 index 0000000..9eb4f31 --- /dev/null +++ b/tests/integration/test_cut.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +# cut -d: -f2 produces "b" +out=$(echo "a:b:c" | "$CFBOX" cut -d: -f2) +if [[ "$out" == "b" ]]; then ((++pass)); else echo "FAIL [cut -d: -f2]: expected 'b', got $(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 cut --help +((++pass)) + +echo "cut: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_date.sh b/tests/integration/test_date.sh new file mode 100644 index 0000000..1609335 --- /dev/null +++ b/tests/integration/test_date.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +# date outputs something +out=$("$CFBOX" date) +[[ -n "$out" ]] +((++pass)) + +# date +%Y is 4 digits +out=$("$CFBOX" date +%Y) +if [[ "$out" =~ ^[0-9]{4}$ ]]; then ((++pass)); else echo "FAIL [date +%Y]: expected 4 digits, got $(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 date --help +((++pass)) + +echo "date: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_expr.sh b/tests/integration/test_expr.sh new file mode 100644 index 0000000..8310375 --- /dev/null +++ b/tests/integration/test_expr.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +# expr 2 + 3 = "5" +out=$("$CFBOX" expr 2 + 3) +if [[ "$out" == "5" ]]; then ((++pass)); else echo "FAIL [expr 2+3]: expected '5', got $(printf '%q' "$out")"; ((++fail)); fi + +# expr 10 - 3 = "7" +out=$("$CFBOX" expr 10 - 3) +if [[ "$out" == "7" ]]; then ((++pass)); else echo "FAIL [expr 10-3]: expected '7', got $(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 expr --help +((++pass)) + +echo "expr: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_factor.sh b/tests/integration/test_factor.sh new file mode 100644 index 0000000..6d5c0f4 --- /dev/null +++ b/tests/integration/test_factor.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +# factor 42 = "42: 2 3 7" +out=$("$CFBOX" factor 42) +if [[ "$out" == "42: 2 3 7" ]]; then ((++pass)); else echo "FAIL [factor 42]: expected '42: 2 3 7', got $(printf '%q' "$out")"; ((++fail)); fi + +# factor 7 = "7: 7" +out=$("$CFBOX" factor 7) +if [[ "$out" == "7: 7" ]]; then ((++pass)); else echo "FAIL [factor 7]: expected '7: 7', got $(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 factor --help +((++pass)) + +echo "factor: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_ln.sh b/tests/integration/test_ln.sh new file mode 100644 index 0000000..08e5bfb --- /dev/null +++ b/tests/integration/test_ln.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +echo "original" > "$tmpdir/file1" + +# ln -s creates symlink +"$CFBOX" ln -s "$tmpdir/file1" "$tmpdir/link1" >/dev/null 2>&1 +if [[ -L "$tmpdir/link1" ]]; then ((++pass)); else echo "FAIL [ln -s]: symlink not created"; ((++fail)); fi + +# symlink points to correct content +out=$(cat "$tmpdir/link1") +if [[ "$out" == "original" ]]; then ((++pass)); else echo "FAIL [ln -s content]: expected 'original', got $(printf '%q' "$out")"; ((++fail)); fi + +# ln creates hard link +"$CFBOX" ln "$tmpdir/file1" "$tmpdir/file2" >/dev/null 2>&1 +if [[ -f "$tmpdir/file2" ]]; then ((++pass)); else echo "FAIL [ln hard]: hard link not created"; ((++fail)); fi + +# same inode +inode1=$(stat -c '%i' "$tmpdir/file1") +inode2=$(stat -c '%i' "$tmpdir/file2") +if [[ "$inode1" == "$inode2" ]]; then ((++pass)); else echo "FAIL [ln inode]: inodes differ ($inode1 vs $inode2)"; ((++fail)); fi + +# --help +assert_exit 0 ln --help +((++pass)) + +echo "ln: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_md5sum.sh b/tests/integration/test_md5sum.sh new file mode 100644 index 0000000..711c47f --- /dev/null +++ b/tests/integration/test_md5sum.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +# md5sum of known file +printf "hello\n" > "$tmpdir/md5.txt" +out=$("$CFBOX" md5sum "$tmpdir/md5.txt") +if [[ "$out" == *"b1946ac92492d2347c6235b4d2611184"* ]]; then ((++pass)); else echo "FAIL [md5sum known]: got $(printf '%q' "$out")"; ((++fail)); fi + +# md5sum of empty file = d41d8cd98f00b204e9800998ecf8427e +printf "" > "$tmpdir/empty.txt" +out=$("$CFBOX" md5sum "$tmpdir/empty.txt") +if [[ "$out" == *"d41d8cd98f00b204e9800998ecf8427e"* ]]; then ((++pass)); else echo "FAIL [md5sum empty]: got $(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 md5sum --help +((++pass)) + +echo "md5sum: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_mktemp.sh b/tests/integration/test_mktemp.sh new file mode 100644 index 0000000..050aa9c --- /dev/null +++ b/tests/integration/test_mktemp.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +# mktemp creates temp file +out=$("$CFBOX" mktemp -p "$tmpdir") +if [[ -f "$out" ]]; then ((++pass)); else echo "FAIL [mktemp file]: not created at $(printf '%q' "$out")"; ((++fail)); fi + +# mktemp -d creates temp dir +out=$("$CFBOX" mktemp -d -p "$tmpdir") +if [[ -d "$out" ]]; then ((++pass)); else echo "FAIL [mktemp -d]: dir not created at $(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 mktemp --help +((++pass)) + +echo "mktemp: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_nl.sh b/tests/integration/test_nl.sh new file mode 100644 index 0000000..89c7af5 --- /dev/null +++ b/tests/integration/test_nl.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +printf "line1\nline2\nline3\n" > "$tmpdir/nl.txt" + +# nl on a file produces line numbers +out=$("$CFBOX" nl "$tmpdir/nl.txt") +if [[ "$out" == *"1"*"line1"* ]] && [[ "$out" == *"2"*"line2"* ]] && [[ "$out" == *"3"*"line3"* ]]; then + ((++pass)) +else + echo "FAIL [nl line numbers]: got $(printf '%q' "$out")"; ((++fail)) +fi + +# --help +assert_exit 0 nl --help +((++pass)) + +echo "nl: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_od.sh b/tests/integration/test_od.sh new file mode 100644 index 0000000..6302830 --- /dev/null +++ b/tests/integration/test_od.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +# od produces output with addresses (hex offsets) +out=$(echo hello | "$CFBOX" od) +if [[ "$out" =~ [0-9a-f]+ ]]; then ((++pass)); else echo "FAIL [od output]: expected addresses, got $(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 od --help +((++pass)) + +echo "od: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_printenv.sh b/tests/integration/test_printenv.sh new file mode 100644 index 0000000..efefd8f --- /dev/null +++ b/tests/integration/test_printenv.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +# printenv outputs lines +out=$("$CFBOX" printenv) +[[ -n "$out" ]] +((++pass)) + +# printenv HOME is not empty +out=$("$CFBOX" printenv HOME) +[[ -n "$out" ]] +((++pass)) + +# printenv NONEXISTENT_VAR exits 1 +set +e +"$CFBOX" printenv NONEXISTENT_VAR >/dev/null 2>&1 +rc=$? +set -e +if [[ $rc -ne 0 ]]; then ((++pass)); else echo "FAIL: printenv NONEXISTENT_VAR should fail"; ((++fail)); fi + +# --help +assert_exit 0 printenv --help +((++pass)) + +# --version +assert_exit 0 printenv --version +((++pass)) + +echo "printenv: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_seq.sh b/tests/integration/test_seq.sh new file mode 100644 index 0000000..502a67c --- /dev/null +++ b/tests/integration/test_seq.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +# seq 5 produces 5 lines +out=$("$CFBOX" seq 5) +lines=$(echo "$out" | wc -l) +if [[ "$lines" -eq 5 ]]; then ((++pass)); else echo "FAIL [seq 5 lines]: expected 5, got $lines"; ((++fail)); fi + +# seq 2 4 produces 2 3 4 +out=$("$CFBOX" seq 2 4) +expected="2 +3 +4" +if [[ "$out" == "$expected" ]]; then ((++pass)); else echo "FAIL [seq 2 4]: expected=$(printf '%q' "$expected") actual=$(printf '%q' "$out")"; ((++fail)); fi + +# seq -s, 3 produces "1,2,3" +out=$("$CFBOX" seq -s, 3) +if [[ "$out" == "1,2,3" ]]; then ((++pass)); else echo "FAIL [seq -s, 3]: expected='1,2,3' actual=$(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 seq --help +((++pass)) + +echo "seq: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_touch.sh b/tests/integration/test_touch.sh new file mode 100644 index 0000000..6464f67 --- /dev/null +++ b/tests/integration/test_touch.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +# touch creates file +"$CFBOX" touch "$tmpdir/created.txt" >/dev/null 2>&1 +if [[ -f "$tmpdir/created.txt" ]]; then ((++pass)); else echo "FAIL [touch create]: file not created"; ((++fail)); fi + +# touch -c doesn't create file +"$CFBOX" touch -c "$tmpdir/noexist.txt" >/dev/null 2>&1 +if [[ ! -f "$tmpdir/noexist.txt" ]]; then ((++pass)); else echo "FAIL [touch -c]: file should not exist"; ((++fail)); fi + +# --help +assert_exit 0 touch --help +((++pass)) + +echo "touch: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_tr.sh b/tests/integration/test_tr.sh new file mode 100644 index 0000000..4ff88a8 --- /dev/null +++ b/tests/integration/test_tr.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +# tr a-z A-Z produces HELLO +out=$(echo hello | "$CFBOX" tr a-z A-Z) +if [[ "$out" == "HELLO" ]]; then ((++pass)); else echo "FAIL [tr upper]: expected 'HELLO', got $(printf '%q' "$out")"; ((++fail)); fi + +# tr -d l produces heo +out=$(echo hello | "$CFBOX" tr -d l) +if [[ "$out" == "heo" ]]; then ((++pass)); else echo "FAIL [tr -d]: expected 'heo', got $(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 tr --help +((++pass)) + +echo "tr: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/integration/test_xargs.sh b/tests/integration/test_xargs.sh new file mode 100644 index 0000000..4b8518e --- /dev/null +++ b/tests/integration/test_xargs.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "$0")/helpers.sh" + +pass=0 fail=0 + +# echo "hello" | xargs produces "hello" +out=$(echo "hello" | "$CFBOX" xargs) +if [[ "$out" == "hello" ]]; then ((++pass)); else echo "FAIL [xargs basic]: expected 'hello', got $(printf '%q' "$out")"; ((++fail)); fi + +# printf "a\nb\n" | xargs -n1 produces "a\nb" +out=$(printf "a\nb\n" | "$CFBOX" xargs -n1) +expected="a +b" +if [[ "$out" == "$expected" ]]; then ((++pass)); else echo "FAIL [xargs -n1]: expected=$(printf '%q' "$expected") actual=$(printf '%q' "$out")"; ((++fail)); fi + +# --help +assert_exit 0 xargs --help +((++pass)) + +echo "xargs: $pass passed, $fail failed" +[[ $fail -eq 0 ]] diff --git a/tests/unit/test_cksum.cpp b/tests/unit/test_cksum.cpp new file mode 100644 index 0000000..1c18fbf --- /dev/null +++ b/tests/unit/test_cksum.cpp @@ -0,0 +1,35 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_CKSUM + +using namespace cfbox::test; + +TEST(CksumTest, KnownFile) { + TempDir tmp; + auto f = tmp.write_file("data.txt", "hello\n"); + char a0[] = "cksum", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", f.c_str()); + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return cksum_main(2, argv); }); + EXPECT_FALSE(out.empty()); +} + +TEST(CksumTest, OutputFormat) { + TempDir tmp; + auto f = tmp.write_file("data.txt", "test content\n"); + char a0[] = "cksum", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", f.c_str()); + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return cksum_main(2, argv); }); + // Output format: + EXPECT_NE(out.find(" "), std::string::npos); + // Should contain digits + bool has_digit = false; + for (char c : out) { if (c >= '0' && c <= '9') { has_digit = true; break; } } + EXPECT_TRUE(has_digit); +} + +#endif // CFBOX_ENABLE_CKSUM diff --git a/tests/unit/test_comm.cpp b/tests/unit/test_comm.cpp new file mode 100644 index 0000000..581ca51 --- /dev/null +++ b/tests/unit/test_comm.cpp @@ -0,0 +1,25 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_COMM + +using namespace cfbox::test; + +TEST(CommTest, TwoSortedFiles) { + TempDir tmp; + auto f1 = tmp.write_file("a.txt", "apple\nbanana\ncherry\n"); + auto f2 = tmp.write_file("b.txt", "banana\ncherry\ndate\n"); + char a0[] = "comm", a1[256], a2[256]; + std::snprintf(a1, sizeof(a1), "%s", f1.c_str()); + std::snprintf(a2, sizeof(a2), "%s", f2.c_str()); + char* argv[] = {a0, a1, a2}; + auto out = capture_stdout([&]{ return comm_main(3, argv); }); + EXPECT_NE(out.find("apple"), std::string::npos); + EXPECT_NE(out.find("date"), std::string::npos); + EXPECT_NE(out.find("banana"), std::string::npos); + EXPECT_NE(out.find("cherry"), std::string::npos); +} + +#endif // CFBOX_ENABLE_COMM diff --git a/tests/unit/test_cut.cpp b/tests/unit/test_cut.cpp new file mode 100644 index 0000000..9637eff --- /dev/null +++ b/tests/unit/test_cut.cpp @@ -0,0 +1,20 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_CUT + +using namespace cfbox::test; + +TEST(CutTest, FieldDelimiter) { + TempDir tmp; + auto f = tmp.write_file("data.txt", "a:b:c\n"); + char a0[] = "cut", a1[] = "-d:", a2[] = "-f1", a3[256]; + std::snprintf(a3, sizeof(a3), "%s", f.c_str()); + char* argv[] = {a0, a1, a2, a3}; + auto out = capture_stdout([&]{ return cut_main(4, argv); }); + EXPECT_EQ(out, "a\n"); +} + +#endif // CFBOX_ENABLE_CUT diff --git a/tests/unit/test_date.cpp b/tests/unit/test_date.cpp new file mode 100644 index 0000000..66de8f2 --- /dev/null +++ b/tests/unit/test_date.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_DATE + +using namespace cfbox::test; + +TEST(DateTest, NonEmptyOutput) { + char a0[] = "date"; + char* argv[] = {a0}; + auto out = capture_stdout([&]{ return date_main(1, argv); }); + EXPECT_FALSE(out.empty()); +} + +TEST(DateTest, YearFormat) { + char a0[] = "date", a1[] = "+%Y"; + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return date_main(2, argv); }); + ASSERT_GE(out.size(), 4u); + // Verify it is a 4-digit year (starts with 1 or 2) + EXPECT_TRUE(out[0] == '1' || out[0] == '2'); + for (int i = 0; i < 4; ++i) { + EXPECT_TRUE(out[i] >= '0' && out[i] <= '9'); + } +} + +#endif // CFBOX_ENABLE_DATE diff --git a/tests/unit/test_du.cpp b/tests/unit/test_du.cpp new file mode 100644 index 0000000..475e9ae --- /dev/null +++ b/tests/unit/test_du.cpp @@ -0,0 +1,16 @@ +#include +#include +#include +#include "test_capture.hpp" +#if CFBOX_ENABLE_DU +using namespace cfbox::test; +TEST(DuTest, PrintsSize) { + TempDir tmp; + tmp.write_file("test.txt", "hello world"); + char a0[] = "du", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", tmp.path.string().c_str()); + char* argv[] = {a0, a1, nullptr}; + auto out = capture_stdout([&] { return du_main(2, argv); }); + EXPECT_FALSE(out.empty()); +} +#endif diff --git a/tests/unit/test_expr.cpp b/tests/unit/test_expr.cpp new file mode 100644 index 0000000..d82ec0b --- /dev/null +++ b/tests/unit/test_expr.cpp @@ -0,0 +1,38 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_EXPR + +using namespace cfbox::test; + +TEST(ExprTest, Addition) { + char a0[] = "expr", a1[] = "2", a2[] = "+", a3[] = "3"; + char* argv[] = {a0, a1, a2, a3}; + auto out = capture_stdout([&]{ return expr_main(4, argv); }); + EXPECT_EQ(out, "5\n"); +} + +TEST(ExprTest, Division) { + char a0[] = "expr", a1[] = "10", a2[] = "/", a3[] = "3"; + char* argv[] = {a0, a1, a2, a3}; + auto out = capture_stdout([&]{ return expr_main(4, argv); }); + EXPECT_EQ(out, "3\n"); +} + +TEST(ExprTest, Multiplication) { + char a0[] = "expr", a1[] = "2", a2[] = "*", a3[] = "3"; + char* argv[] = {a0, a1, a2, a3}; + auto out = capture_stdout([&]{ return expr_main(4, argv); }); + EXPECT_EQ(out, "6\n"); +} + +TEST(ExprTest, Length) { + char a0[] = "expr", a1[] = "length", a2[] = "hello"; + char* argv[] = {a0, a1, a2}; + auto out = capture_stdout([&]{ return expr_main(3, argv); }); + EXPECT_EQ(out, "5\n"); +} + +#endif // CFBOX_ENABLE_EXPR diff --git a/tests/unit/test_factor.cpp b/tests/unit/test_factor.cpp new file mode 100644 index 0000000..d5f6b1b --- /dev/null +++ b/tests/unit/test_factor.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_FACTOR + +using namespace cfbox::test; + +TEST(FactorTest, CompositeNumber) { + char a0[] = "factor", a1[] = "12"; + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return factor_main(2, argv); }); + EXPECT_EQ(out, "12: 2 2 3\n"); +} + +TEST(FactorTest, PrimeNumber) { + char a0[] = "factor", a1[] = "7"; + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return factor_main(2, argv); }); + EXPECT_EQ(out, "7: 7\n"); +} + +#endif // CFBOX_ENABLE_FACTOR diff --git a/tests/unit/test_hostid.cpp b/tests/unit/test_hostid.cpp new file mode 100644 index 0000000..42a4afe --- /dev/null +++ b/tests/unit/test_hostid.cpp @@ -0,0 +1,15 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_HOSTID +using namespace cfbox::test; + +TEST(HostidTest, PrintsHexId) { + char a0[] = "hostid"; + char* argv[] = {a0, nullptr}; + auto out = capture_stdout([&] { return hostid_main(1, argv); }); + ASSERT_GE(out.size(), 8u); +} +#endif diff --git a/tests/unit/test_ln.cpp b/tests/unit/test_ln.cpp new file mode 100644 index 0000000..96aea75 --- /dev/null +++ b/tests/unit/test_ln.cpp @@ -0,0 +1,18 @@ +#include +#include +#include +#include "test_capture.hpp" +#if CFBOX_ENABLE_LN +using namespace cfbox::test; +TEST(LnTest, HardLink) { + TempDir tmp; + auto src = tmp.write_file("src.txt", "hello"); + auto dst = (tmp.path / "dst.txt").string(); + char a0[] = "ln", a1[256], a2[256]; + std::snprintf(a1, sizeof(a1), "%s", src.c_str()); + std::snprintf(a2, sizeof(a2), "%s", dst.c_str()); + char* argv[] = {a0, a1, a2, nullptr}; + EXPECT_EQ(ln_main(3, argv), 0); + EXPECT_TRUE(std::filesystem::exists(dst)); +} +#endif diff --git a/tests/unit/test_md5sum.cpp b/tests/unit/test_md5sum.cpp new file mode 100644 index 0000000..661ef20 --- /dev/null +++ b/tests/unit/test_md5sum.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_MD5SUM + +using namespace cfbox::test; + +TEST(Md5sumTest, OutputFormat) { + TempDir tmp; + auto f = tmp.write_file("data.txt", "hello\n"); + char a0[] = "md5sum", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", f.c_str()); + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return md5sum_main(2, argv); }); + EXPECT_FALSE(out.empty()); + // Output should have 32 hex characters followed by " " and filename + // Find the two-space separator + auto pos = out.find(" "); + ASSERT_NE(pos, std::string::npos); + EXPECT_EQ(pos, 32u); + // Verify all 32 chars are hex + for (std::size_t i = 0; i < 32u; ++i) { + bool hex = (out[i] >= '0' && out[i] <= '9') || + (out[i] >= 'a' && out[i] <= 'f'); + EXPECT_TRUE(hex) << "char at " << i << " is not hex: " << out[i]; + } +} + +#endif // CFBOX_ENABLE_MD5SUM diff --git a/tests/unit/test_mktemp.cpp b/tests/unit/test_mktemp.cpp new file mode 100644 index 0000000..ef8ec89 --- /dev/null +++ b/tests/unit/test_mktemp.cpp @@ -0,0 +1,16 @@ +#include +#include +#include +#include "test_capture.hpp" +#if CFBOX_ENABLE_MKTEMP +using namespace cfbox::test; +TEST(MktempTest, CreatesFile) { + char a0[] = "mktemp"; + char* argv[] = {a0, nullptr}; + auto out = capture_stdout([&] { return mktemp_main(1, argv); }); + ASSERT_FALSE(out.empty()); + auto path = out.substr(0, out.size() - 1); + EXPECT_TRUE(std::filesystem::exists(path)); + std::filesystem::remove(path); +} +#endif diff --git a/tests/unit/test_nl.cpp b/tests/unit/test_nl.cpp new file mode 100644 index 0000000..bffeb2a --- /dev/null +++ b/tests/unit/test_nl.cpp @@ -0,0 +1,23 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_NL + +using namespace cfbox::test; + +TEST(NlTest, NumberedLines) { + TempDir tmp; + auto f = tmp.write_file("data.txt", "hello\nworld\n"); + char a0[] = "nl", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", f.c_str()); + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return nl_main(2, argv); }); + EXPECT_NE(out.find("1"), std::string::npos); + EXPECT_NE(out.find("hello"), std::string::npos); + EXPECT_NE(out.find("2"), std::string::npos); + EXPECT_NE(out.find("world"), std::string::npos); +} + +#endif // CFBOX_ENABLE_NL diff --git a/tests/unit/test_paste.cpp b/tests/unit/test_paste.cpp new file mode 100644 index 0000000..e734b73 --- /dev/null +++ b/tests/unit/test_paste.cpp @@ -0,0 +1,20 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_PASTE + +using namespace cfbox::test; + +TEST(PasteTest, SingleFile) { + TempDir tmp; + auto f = tmp.write_file("data.txt", "a\nb\nc\n"); + char a0[] = "paste", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", f.c_str()); + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return paste_main(2, argv); }); + EXPECT_EQ(out, "a\nb\nc\n"); +} + +#endif // CFBOX_ENABLE_PASTE diff --git a/tests/unit/test_printenv.cpp b/tests/unit/test_printenv.cpp new file mode 100644 index 0000000..00d0b47 --- /dev/null +++ b/tests/unit/test_printenv.cpp @@ -0,0 +1,22 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_PRINTENV +using namespace cfbox::test; + +TEST(PrintenvTest, PrintsHome) { + char a0[] = "printenv", a1[] = "HOME"; + char* argv[] = {a0, a1, nullptr}; + auto out = capture_stdout([&] { return printenv_main(2, argv); }); + EXPECT_FALSE(out.empty()); + EXPECT_NE(out.find('/'), std::string::npos); +} + +TEST(PrintenvTest, MissingVarReturnsOne) { + char a0[] = "printenv", a1[] = "NONEXISTENT_VAR_XYZ_12345"; + char* argv[] = {a0, a1, nullptr}; + EXPECT_EQ(printenv_main(2, argv), 1); +} +#endif diff --git a/tests/unit/test_readlink.cpp b/tests/unit/test_readlink.cpp new file mode 100644 index 0000000..7fc13d2 --- /dev/null +++ b/tests/unit/test_readlink.cpp @@ -0,0 +1,18 @@ +#include +#include +#include +#include "test_capture.hpp" +#if CFBOX_ENABLE_READLINK +using namespace cfbox::test; +TEST(ReadlinkTest, ReadsSymlink) { + TempDir tmp; + auto target = tmp.write_file("target.txt", "data"); + auto link = (tmp.path / "link").string(); + std::filesystem::create_symlink(target, link); + char a0[] = "readlink", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", link.c_str()); + char* argv[] = {a0, a1, nullptr}; + auto out = capture_stdout([&] { return readlink_main(2, argv); }); + EXPECT_EQ(out.substr(0, out.size() - 1), target); +} +#endif diff --git a/tests/unit/test_realpath.cpp b/tests/unit/test_realpath.cpp new file mode 100644 index 0000000..54e6c09 --- /dev/null +++ b/tests/unit/test_realpath.cpp @@ -0,0 +1,16 @@ +#include +#include +#include +#include "test_capture.hpp" +#if CFBOX_ENABLE_REALPATH +using namespace cfbox::test; +TEST(RealpathTest, ResolvesPath) { + TempDir tmp; + auto filepath = tmp.write_file("test.txt", "data"); + char a0[] = "realpath", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", filepath.c_str()); + char* argv[] = {a0, a1, nullptr}; + auto out = capture_stdout([&] { return realpath_main(2, argv); }); + ASSERT_FALSE(out.empty()); +} +#endif diff --git a/tests/unit/test_rmdir.cpp b/tests/unit/test_rmdir.cpp new file mode 100644 index 0000000..f484735 --- /dev/null +++ b/tests/unit/test_rmdir.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_RMDIR +using namespace cfbox::test; + +TEST(RmdirTest, RemovesEmptyDir) { + TempDir tmp; + auto subdir = (tmp.path / "sub").string(); + std::filesystem::create_directory(subdir); + ASSERT_TRUE(std::filesystem::exists(subdir)); + char a0[] = "rmdir", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", subdir.c_str()); + char* argv[] = {a0, a1, nullptr}; + EXPECT_EQ(rmdir_main(2, argv), 0); + EXPECT_FALSE(std::filesystem::exists(subdir)); +} + +TEST(RmdirTest, NonEmptyFails) { + TempDir tmp; + auto subdir = (tmp.path / "sub").string(); + std::filesystem::create_directory(subdir); + tmp.write_file("sub/file.txt", "data"); + char a0[] = "rmdir", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", subdir.c_str()); + char* argv[] = {a0, a1, nullptr}; + EXPECT_NE(rmdir_main(2, argv), 0); +} +#endif diff --git a/tests/unit/test_seq.cpp b/tests/unit/test_seq.cpp new file mode 100644 index 0000000..bf96688 --- /dev/null +++ b/tests/unit/test_seq.cpp @@ -0,0 +1,38 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_SEQ + +using namespace cfbox::test; + +TEST(SeqTest, SingleArg) { + char a0[] = "seq", a1[] = "5"; + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return seq_main(2, argv); }); + EXPECT_EQ(out, "1\n2\n3\n4\n5\n"); +} + +TEST(SeqTest, TwoArgs) { + char a0[] = "seq", a1[] = "2", a2[] = "5"; + char* argv[] = {a0, a1, a2}; + auto out = capture_stdout([&]{ return seq_main(3, argv); }); + EXPECT_EQ(out, "2\n3\n4\n5\n"); +} + +TEST(SeqTest, ThreeArgs) { + char a0[] = "seq", a1[] = "1", a2[] = "2", a3[] = "5"; + char* argv[] = {a0, a1, a2, a3}; + auto out = capture_stdout([&]{ return seq_main(4, argv); }); + EXPECT_EQ(out, "1\n3\n5\n"); +} + +TEST(SeqTest, Separator) { + char a0[] = "seq", a1[] = "-s,", a2[] = "3"; + char* argv[] = {a0, a1, a2}; + auto out = capture_stdout([&]{ return seq_main(3, argv); }); + EXPECT_EQ(out, "1,2,3\n"); +} + +#endif // CFBOX_ENABLE_SEQ diff --git a/tests/unit/test_sum.cpp b/tests/unit/test_sum.cpp new file mode 100644 index 0000000..f9dbe90 --- /dev/null +++ b/tests/unit/test_sum.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_SUM + +using namespace cfbox::test; + +TEST(SumTest, OnFile) { + TempDir tmp; + auto f = tmp.write_file("data.txt", "hello world\n"); + char a0[] = "sum", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", f.c_str()); + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return sum_main(2, argv); }); + EXPECT_FALSE(out.empty()); + // Should contain digits and spaces + bool has_digit = false; + for (char c : out) { if (c >= '0' && c <= '9') { has_digit = true; break; } } + EXPECT_TRUE(has_digit); +} + +#endif // CFBOX_ENABLE_SUM diff --git a/tests/unit/test_touch.cpp b/tests/unit/test_touch.cpp new file mode 100644 index 0000000..a32313d --- /dev/null +++ b/tests/unit/test_touch.cpp @@ -0,0 +1,16 @@ +#include +#include +#include +#include "test_capture.hpp" +#if CFBOX_ENABLE_TOUCH +using namespace cfbox::test; +TEST(TouchTest, CreatesFile) { + TempDir tmp; + auto filepath = (tmp.path / "touched").string(); + char a0[] = "touch", a1[256]; + std::snprintf(a1, sizeof(a1), "%s", filepath.c_str()); + char* argv[] = {a0, a1, nullptr}; + EXPECT_EQ(touch_main(2, argv), 0); + EXPECT_TRUE(std::filesystem::exists(filepath)); +} +#endif diff --git a/tests/unit/test_tr.cpp b/tests/unit/test_tr.cpp new file mode 100644 index 0000000..ecdf135 --- /dev/null +++ b/tests/unit/test_tr.cpp @@ -0,0 +1,18 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_TR + +using namespace cfbox::test; + +TEST(TrTest, HelpOutput) { + char a0[] = "tr", a1[] = "--help"; + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return tr_main(2, argv); }); + EXPECT_FALSE(out.empty()); + EXPECT_NE(out.find("translate"), std::string::npos); +} + +#endif // CFBOX_ENABLE_TR diff --git a/tests/unit/test_truncate.cpp b/tests/unit/test_truncate.cpp new file mode 100644 index 0000000..5f10d82 --- /dev/null +++ b/tests/unit/test_truncate.cpp @@ -0,0 +1,16 @@ +#include +#include +#include +#include "test_capture.hpp" +#if CFBOX_ENABLE_TRUNCATE +using namespace cfbox::test; +TEST(TruncateTest, SetsSize) { + TempDir tmp; + auto filepath = tmp.write_file("test.txt", "hello world"); + char a0[] = "truncate", a1[] = "-s", a2[] = "5", a3[256]; + std::snprintf(a3, sizeof(a3), "%s", filepath.c_str()); + char* argv[] = {a0, a1, a2, a3, nullptr}; + EXPECT_EQ(truncate_main(4, argv), 0); + EXPECT_EQ(std::filesystem::file_size(filepath), 5u); +} +#endif diff --git a/tests/unit/test_xargs.cpp b/tests/unit/test_xargs.cpp new file mode 100644 index 0000000..413ba99 --- /dev/null +++ b/tests/unit/test_xargs.cpp @@ -0,0 +1,18 @@ +#include +#include +#include +#include "test_capture.hpp" + +#if CFBOX_ENABLE_XARGS + +using namespace cfbox::test; + +TEST(XargsTest, HelpOutput) { + char a0[] = "xargs", a1[] = "--help"; + char* argv[] = {a0, a1}; + auto out = capture_stdout([&]{ return xargs_main(2, argv); }); + EXPECT_FALSE(out.empty()); + EXPECT_NE(out.find("xargs"), std::string::npos); +} + +#endif // CFBOX_ENABLE_XARGS