/* Copyright (c) 2026 The Solidite Developers
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. */
#include <bitset>
#include <filesystem>
#include <iostream>
#include <locale>
#include <string>
#include <string_view>
#include <vector>
#include <cstdint>
#include <unistd.h>
inline int is_writable(const std::string &WHAT) {
return access(WHAT.c_str(), W_OK);
}
int main(int argc, char* argv[]) {
// locale work
std::locale::global(std::locale(""));
std::cout.imbue(std::locale());
std::cerr.imbue(std::locale());
int ret = 0;
std::bitset<7> flags = 0b0000000;
/* according to POSIX standards for rm options
* [0]=process arguments (not --)
* [1]=remove empty directories
* [2]=force (no prompt for confirmation, no diagnostic messages, do not
* modify exit status if no file operands, do not modify exit status of
* nonexistent file operands, previous instances of -i should be ignored
* [3]=prompt for confirmation, any previous instances of -f should be ignored
* [4]=remove file hierarchies (recursive)
* [5]=write message to stdout after each file has been removed (verbose)
* [6]=(has force remove message been prompted)
*/
std::vector<std::string> files;
for (int i = 1; i < argc; ++i) {
const std::string_view ARG = argv[i];
if (ARG[0] == '-' && flags.test(0) == 0) {
if (ARG == "--") {
flags.set(0); // do not further process options
}
for (char c : ARG) {
switch (c) {
case 'd':
flags.set(1);
break;
case 'f':
flags.reset(3); // ignore previous instance of -i
flags.set(2);
break;
case 'i':
flags.reset(2); // ignore previous instance of -r
flags.set(3);
break;
case 'r':
case 'R':
flags.set(4);
break;
case 'v':
flags.set(5);
break;
default:
break;
}
}
} else {
files.push_back(std::string(ARG));
}
}
for (const std::string &FILE : files) {
// step1:
if (!std::filesystem::exists(FILE)) {
// if force is not selected then exit with error and dont operate on file
// step1a:
if (flags.test(2) == 0) {
ret = EXIT_FAILURE;
std::cerr << "not found: " + FILE + '\n';
}
continue; // step1b: continue to next file
// step2: check if directory
} else if (std::filesystem::is_directory(FILE)) {
// step2a:
if (flags.test(1) == 0 && flags.test(4) == 0) {
std::cerr << "directory: " + FILE + '\n';
continue;
} else if (flags.test(1) == 1 && flags.test(4) == 0) {
goto step3;
/* "neither the -R nor the -r option is specified, but -d is specified,
* rm shall proceed with step 3" */
} else if (is_writable(FILE) == 0 && std::filesystem::is_empty(FILE)) {
goto step2d;
} else if ((flags.test(6) == 0 && flags.test(2) == 0 &&
is_writable(FILE) != 0 && isatty(STDIN_FILENO)) ||
(flags.test(3) == 1 && flags.test(6) == 0)) {
/* "If the -f option is not specified, and either the permissions of
* the file do not permit writing and the standard input is a terminal
* or the -i option is specified, rm shall write a prompt to standard
* error and read a line from the standard input." */
std::string response;
get_response_force_remove_directory_0:
std::cerr << "remove: " + FILE + " (y/n)";
std::getline(std::cin, response);
if (response == "n") {
flags.set(6);
continue;
} else if (response == "y")
flags.set(6);
else {
std::cerr << "invalid response\n";
goto get_response_force_remove_directory_0;
}
} // from interpretation of step2c, it seems irrelevant to us
step2d:
if (flags.test(6) == 0 && flags.test(3) == 1) {
std::string response;
get_response_force_remove_directory_1:
std::cerr << "remove: " + FILE + " (y/n)";
std::getline(std::cin, response);
if (response == "n") {
flags.set(6);
continue;
}
else if (response == "y")
flags.set(6);
else {
std::cerr << "invalid response\n";
goto get_response_force_remove_directory_1;
}
}
}
step3:
std::string response;
if ((flags.test(2) == 0 && is_writable(FILE) != 0 &&
isatty(STDIN_FILENO) && flags.test(6) == 0) || (flags.test(3) == 1 &&
flags.test(6) == 0)) {
get_response_remove_0:
std::cerr << "remove: " + FILE + " (y/n)";
std::getline(std::cin, response);
if (response == "n") {
flags.set(6);
continue;
} else if (response == "y")
flags.set(6);
else {
std::cerr << "invalid response\n";
goto get_response_remove_0;
}
}
// step4:
try {
std::filesystem::remove_all(FILE);
} catch (...) {
++ret;
}
if (flags.test(5) == 1)
std::cout << "removed: " + FILE + '\n';
}
return ret;
}
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>