From e0863d808c1b9f701e9a8db5fee4356787ef7015 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Thu, 16 Oct 2025 16:35:56 +0900 Subject: [PATCH] Initial commit --- .envrc | 1 + .gitignore | 2 + Cargo.lock | 348 ++++++++++++++++++++++++ Cargo.toml | 18 ++ LICENSE | 21 ++ README.md | 5 + default.nix | 39 +++ flake.lock | 48 ++++ flake.nix | 57 ++++ src/main.rs | 762 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1301 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 default.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/main.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8392d15 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07c12f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +result diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7cdf419 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,348 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "clap" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2348487adcd4631696ced64ccdb40d38ac4d31cae7f2eec8817fcea1b9d1c43c" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "condexec" +version = "0.1.0" +dependencies = [ + "clap", + "clap_complete", + "regex", + "rustix", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8f43d1d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "condexec" +version = "0.1.0" +edition = "2024" +license = "MIT" +authors = ["h7x4@nani.wtf"] +readme = "README.md" + +[dependencies] +clap = { version = "4.5.49", features = ["derive"] } +clap_complete = "4.5.59" +regex = "1.12.2" +rustix = { version = "1.1.2", features = ["process"] } + +[profile.release] +strip = true +lto = true +codegen-units = 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..16c2a16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 h7x4 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce35412 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# condexec + +This is a utility for conditionally running commands + +It is meant as a replacement for shells in the cases where you only need the shell for running `if [ ... ]`/`if [[ ... ]]]`/ `if test ...`. diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..8458f29 --- /dev/null +++ b/default.nix @@ -0,0 +1,39 @@ +{ + lib +, rustPlatform +}: +let + cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); +in +rustPlatform.buildRustPackage { + pname = cargoToml.package.name; + version = cargoToml.package.version; + src = builtins.filterSource (path: type: let + baseName = baseNameOf (toString path); + in !(lib.any (b: b) [ + (!(lib.cleanSourceFilter path type)) + (type == "directory" && lib.elem baseName [ + ".direnv" + ".git" + "target" + "result" + ]) + (type == "regular" && lib.elem baseName [ + "flake.nix" + "flake.lock" + "default.nix" + "module.nix" + ".envrc" + ]) + ])) ./.; + + + cargoLock.lockFile = ./Cargo.lock; + + meta = with lib; { + license = licenses.mit; + maintainers = with maintainers; [ h7x4 ]; + platforms = platforms.unix; + mainProgram = "condexec"; + }; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8017111 --- /dev/null +++ b/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1760284886, + "narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1760323082, + "narHash": "sha256-SKhC9tyt+gVgQHnZGMVPSdptlDYNqApT56JF5t8RwBY=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "c73e6874fe8dce0bab82c0387b510875f1eff9f8", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..8d01c1b --- /dev/null +++ b/flake.nix @@ -0,0 +1,57 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + rust-overlay.url = "github:oxalica/rust-overlay"; + rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, nixpkgs, rust-overlay }: + let + inherit (nixpkgs) lib; + + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = f: lib.genAttrs systems (system: let + pkgs = import nixpkgs { + inherit system; + overlays = [ + (import rust-overlay) + ]; + }; + + rust-bin = rust-overlay.lib.mkRustBin { } pkgs.buildPackages; + toolchain = rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" "rust-std" ]; + }; + in f system pkgs toolchain); + in { + devShells = forAllSystems (system: pkgs: toolchain: { + default = pkgs.mkShell { + nativeBuildInputs = [ + toolchain + pkgs.cargo-edit + ]; + + RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library"; + }; + }); + + overlays = { + default = self.overlays.condexec; + condexec = final: prev: { + inherit (self.packages.${prev.system}) condexec; + }; + }; + + packages = forAllSystems (system: pkgs: _: { + default = self.packages.${system}.condexec; + condexec = pkgs.callPackage ./default.nix { }; + }); + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fcc996b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,762 @@ +use std::io::IsTerminal; +use std::os::unix::fs::FileTypeExt; +use std::os::unix::fs::MetadataExt; +use std::path::Path; +use std::path::PathBuf; + +use clap::Parser; +use regex::Regex; +use rustix::fd::{FromRawFd, OwnedFd}; +use rustix::process::{getegid, geteuid}; + +const DELIMITER: char = ':'; + +fn parse_tuple(s: &str) -> Result<(T, T), String> +where + T: std::str::FromStr, + ::Err: std::fmt::Display, +{ + if s.matches(DELIMITER).count() != 1 { + return Err("Expected exactly one delimiter".to_string()); + } + + let mut parts = s.split(DELIMITER); + let first = parts + .next() + .ok_or_else(|| "Missing first part".to_string())? + .parse() + .map_err(|e| format!("Failed to parse first part: {e}").to_string())?; + let second = parts + .next() + .ok_or_else(|| "Missing second part".to_string())? + .parse() + .map_err(|e| format!("Failed to parse second part: {e}").to_string())?; + Ok((first, second)) +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None, disable_help_flag = true)] +struct Args { + /// Display this help and exit + #[arg(long = "help", action = clap::ArgAction::Help)] + help: Option, + + // A combined boolean expression, consisting of the the options below combined with (), ! (not), -a (and), and -o (or). + // #[arg(long = "expr", value_name = "EXPRESSION")] + // expression: Vec, + + /// Negate the result + #[arg(short = '!', long = "not", alias = "neg")] + negate: bool, + + /// Use OR to combine tests (default is AND) + #[arg(short = 'o', long = "or")] + or: bool, + + /// The command to execute if the expression is true + #[arg(trailing_var_arg = true, value_name = "COMMAND")] + command: Vec, + + /// The length of STRING is nonzero + #[arg(short = 'n', value_name = "STRING")] + nonzero: Vec, + + /// The length of STRING is zero + #[arg(short = 'z', value_name = "STRING")] + zero: Vec, + + /// INTEGER1 is equal to INTEGER2 + #[arg(long = "ieq", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::)] + equals_int: Vec<(i64, i64)>, + + /// INTEGER1 is greater than or equal to INTEGER2 + #[arg(long = "ige", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::)] + greater_equals_int: Vec<(i64, i64)>, + + /// INTEGER1 is greater than INTEGER2 + #[arg(long = "igt", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::)] + greater_than_int: Vec<(i64, i64)>, + + /// INTEGER1 is less than or equal to INTEGER2 + #[arg(long = "ile", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::)] + less_equal_int: Vec<(i64, i64)>, + + /// INTEGER1 is less than INTEGER2 + #[arg(long = "ilt", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::)] + less_than_int: Vec<(i64, i64)>, + + /// INTEGER1 is not equal to INTEGER2 + #[arg(long = "ine", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::)] + not_equal_int: Vec<(i64, i64)>, + + /// STRING1 is equal to STRING2 + #[arg(long = "eq", value_name = "STRING1:STRING2", value_parser = parse_tuple::)] + equals_str: Vec<(String, String)>, + + /// STRING1 is greater than or equal to STRING2 in the current locale + #[arg(long = "ge", value_name = "STRING1:STRING2", value_parser = parse_tuple::)] + greater_equals_str: Vec<(String, String)>, + + /// STRING1 is greater than STRING2 in the current locale + #[arg(long = "gt", value_name = "STRING1:STRING2", value_parser = parse_tuple::)] + greater_than_str: Vec<(String, String)>, + + /// STRING1 is less than or equal to STRING2 in the current locale + #[arg(long = "le", value_name = "STRING1:STRING2", value_parser = parse_tuple::)] + less_equal_str: Vec<(String, String)>, + + /// STRING1 is less than STRING2 in the current locale + #[arg(long = "lt", value_name = "STRING1:STRING2", value_parser = parse_tuple::)] + less_than_str: Vec<(String, String)>, + + /// STRING1 is not equal to STRING2 + #[arg(long = "ne", value_name = "STRING1:STRING2", value_parser = parse_tuple::)] + not_equal_str: Vec<(String, String)>, + + /// STRING1 matches REGEX + #[arg(long = "match", value_name = "STRING:REGEX", value_parser = parse_tuple::)] + matches_regex: Vec<(String, String)>, + + /// FILE1 and FILE2 have the same device and inode numbers + #[arg(long = "ef", value_name = "FILE1:FILE2", value_parser = parse_tuple::)] + equal_file: Vec<(PathBuf, PathBuf)>, + + /// FILE1 is newer (modification date) than FILE2 + #[arg(long = "nt", value_name = "FILE1:FILE2", value_parser = parse_tuple::)] + newer_than: Vec<(PathBuf, PathBuf)>, + + /// FILE1 is older than FILE2 + #[arg(long = "ot", value_name = "FILE1:FILE2", value_parser = parse_tuple::)] + older_than: Vec<(PathBuf, PathBuf)>, + + /// File exists and is block special + #[arg(short = 'b', value_name = "FILE")] + block_special: Vec, + + /// File exists and is character special + #[arg(short = 'c', value_name = "FILE")] + char_special: Vec, + + /// File exists and is a directory + #[arg(short = 'd', value_name = "FILE")] + directory: Vec, + + /// File exists + #[arg(short = 'e', value_name = "FILE")] + exists: Vec, + + /// File exists and is a regular file + #[arg(short = 'f', value_name = "FILE")] + regular_file: Vec, + + /// File exists and is set-group-ID + #[arg(short = 'g', value_name = "FILE")] + set_group_id: Vec, + + /// File exists and is owned by the effective group ID + #[arg(short = 'G', value_name = "FILE")] + owned_by_effective_group: Vec, + + /// File exists and is a symbolic link + #[arg(short = 'h', value_name = "FILE", alias = "L")] + symbolic_link: Vec, + + /// File exists and has its sticky bit set + #[arg(short = 'k', value_name = "FILE")] + sticky_bit: Vec, + + /// File exists and has been modified since it was last read + #[arg(short = 'N', value_name = "FILE")] + modified_since_last_read: Vec, + + /// File exists and is owned by the effective user ID + #[arg(short = 'O', value_name = "FILE")] + owned_by_effective_user: Vec, + + /// File exists and is a named pipe + #[arg(short = 'p', value_name = "FILE")] + named_pipe: Vec, + + /// File exists and the user has read access + #[arg(short = 'r', value_name = "FILE")] + read_access: Vec, + + /// File exists and has a size greater than zero + #[arg(short = 's', value_name = "FILE")] + size_greater_than_zero: Vec, + + /// File exists and is a socket + #[arg(short = 'S', value_name = "FILE")] + is_socket: Vec, + + /// File descriptor FD is opened on a terminal + #[arg(short = 't', value_name = "FD")] + fd_on_terminal: Vec, + + /// File exists and its set-user-ID bit is set + #[arg(short = 'u', value_name = "FILE")] + set_user_id: Vec, + + /// File exists and the user has write access + #[arg(short = 'w', value_name = "FILE")] + write_access: Vec, + + /// File exists and the user has execute (or search) access + #[arg(short = 'x', value_name = "FILE")] + execute_access: Vec, + + /// Environment variable VAR is declared + #[arg(short = 'v', value_name = "VAR")] + env_var_declared: Vec, +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + let results = process_results(&args); + if results.should_execute() { + if args.command.is_empty() { + std::process::exit(0); + } + + let command = &args.command[0]; + let command_args = &args.command[1..]; + std::process::Command::new(command) + .args(command_args) + .spawn()? + .wait() + .map(|status| std::process::exit(status.code().unwrap_or(1)))?; + } else { + std::process::exit(1); + } + Ok(()) +} + +/// TODO: This is made as an intermediary step, to allow for nice debug-printing. +/// However, no such debug printing is actually implemented. +#[derive(Debug)] +struct TestResults<'a> { + or: bool, + negate: bool, + // expression: Vec, + nonzero: Vec<(&'a str, bool)>, + zero: Vec<(&'a str, bool)>, + equals_int: Vec<((i64, i64), bool)>, + greater_equals_int: Vec<((i64, i64), bool)>, + greater_than_int: Vec<((i64, i64), bool)>, + less_equal_int: Vec<((i64, i64), bool)>, + less_than_int: Vec<((i64, i64), bool)>, + not_equal_int: Vec<((i64, i64), bool)>, + equals_str: Vec<((&'a str, &'a str), bool)>, + greater_equals_str: Vec<((&'a str, &'a str), bool)>, + greater_than_str: Vec<((&'a str, &'a str), bool)>, + less_equal_str: Vec<((&'a str, &'a str), bool)>, + less_than_str: Vec<((&'a str, &'a str), bool)>, + not_equal_str: Vec<((&'a str, &'a str), bool)>, + matches_regex: Vec<((&'a str, &'a str), bool)>, + equal_file: Vec<((&'a Path, &'a Path), bool)>, + newer_than: Vec<((&'a Path, &'a Path), bool)>, + older_than: Vec<((&'a Path, &'a Path), bool)>, + block_special: Vec<(&'a Path, bool)>, + char_special: Vec<(&'a Path, bool)>, + directory: Vec<(&'a Path, bool)>, + exists: Vec<(&'a Path, bool)>, + regular_file: Vec<(&'a Path, bool)>, + set_group_id: Vec<(&'a Path, bool)>, + owned_by_effective_group: Vec<(&'a Path, bool)>, + symbolic_link: Vec<(&'a Path, bool)>, + sticky_bit: Vec<(&'a Path, bool)>, + modified_since_last_read: Vec<(&'a Path, bool)>, + owned_by_effective_user: Vec<(&'a Path, bool)>, + named_pipe: Vec<(&'a Path, bool)>, + read_access: Vec<(&'a Path, bool)>, + size_greater_than_zero: Vec<(&'a Path, bool)>, + is_socket: Vec<(&'a Path, bool)>, + fd_on_terminal: Vec<(i32, bool)>, + set_user_id: Vec<(&'a Path, bool)>, + write_access: Vec<(&'a Path, bool)>, + execute_access: Vec<(&'a Path, bool)>, + env_var_declared: Vec<(String, bool)>, +} + +impl TestResults<'_> { + fn should_execute(&self) -> bool { + let all_tests = [ + &self.nonzero.iter().map(|(_, r)| r).collect::>(), + &self.zero.iter().map(|(_, r)| r).collect::>(), + &self.equals_int.iter().map(|(_, r)| r).collect::>(), + &self + .greater_equals_int + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .greater_than_int + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .less_equal_int + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .less_than_int + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .not_equal_int + .iter() + .map(|(_, r)| r) + .collect::>(), + &self.equals_str.iter().map(|(_, r)| r).collect::>(), + &self + .greater_equals_str + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .greater_than_str + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .less_equal_str + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .less_than_str + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .not_equal_str + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .matches_regex + .iter() + .map(|(_, r)| r) + .collect::>(), + &self.equal_file.iter().map(|(_, r)| r).collect::>(), + &self.newer_than.iter().map(|(_, r)| r).collect::>(), + &self.older_than.iter().map(|(_, r)| r).collect::>(), + &self + .block_special + .iter() + .map(|(_, r)| r) + .collect::>(), + &self.char_special.iter().map(|(_, r)| r).collect::>(), + &self.directory.iter().map(|(_, r)| r).collect::>(), + &self.exists.iter().map(|(_, r)| r).collect::>(), + &self.regular_file.iter().map(|(_, r)| r).collect::>(), + &self.set_group_id.iter().map(|(_, r)| r).collect::>(), + &self + .owned_by_effective_group + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .symbolic_link + .iter() + .map(|(_, r)| r) + .collect::>(), + &self.sticky_bit.iter().map(|(_, r)| r).collect::>(), + &self + .modified_since_last_read + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .owned_by_effective_user + .iter() + .map(|(_, r)| r) + .collect::>(), + &self.named_pipe.iter().map(|(_, r)| r).collect::>(), + &self.read_access.iter().map(|(_, r)| r).collect::>(), + &self + .size_greater_than_zero + .iter() + .map(|(_, r)| r) + .collect::>(), + &self.is_socket.iter().map(|(_, r)| r).collect::>(), + &self + .fd_on_terminal + .iter() + .map(|(_, r)| r) + .collect::>(), + &self.set_user_id.iter().map(|(_, r)| r).collect::>(), + &self.write_access.iter().map(|(_, r)| r).collect::>(), + &self + .execute_access + .iter() + .map(|(_, r)| r) + .collect::>(), + &self + .env_var_declared + .iter() + .map(|(_, r)| r) + .collect::>(), + ]; + + match (self.or, self.negate) { + (true, true) => !all_tests.into_iter().flatten().any(|&&r| r), + (true, false) => all_tests.into_iter().flatten().any(|&&r| r), + (false, true) => !all_tests.into_iter().flatten().all(|&&r| r), + (false, false) => all_tests.into_iter().flatten().all(|&&r| r), + } + } +} + +// This should store parse-tree and sub-results +// struct ExpressionResult { + +// } + +const S_ISVTX: u32 = 0o1000; +const S_ISGID: u32 = 0o2000; +const S_ISUID: u32 = 0o4000; + +macro_rules! with_file_meta { + ($file:expr, $meta:ident, $result:expr) => {{ + let meta = std::fs::metadata($file); + let result = match meta { + Ok($meta) => $result, + Err(_) => false, + }; + ($file.as_path(), result) + }}; +} + +enum Permission { + Read = 0b100, + Write = 0b010, + Execute = 0b001, +} + +fn has_permission(meta: &std::fs::Metadata, permission: Permission) -> bool { + let mode = meta.mode(); + let user_id = meta.uid(); + let group_id = meta.gid(); + let euid = geteuid().as_raw(); + let egid = getegid().as_raw(); + + let perm_bits = match (euid == user_id, egid == group_id) { + (true, _) => (mode >> 6) & 0o7, + (false, true) => (mode >> 3) & 0o7, + (false, false) => mode & 0o7, + }; + + (perm_bits & (permission as u32)) != 0 +} + +fn process_results(args: &Args) -> TestResults<'_> { + let nonzero = args + .nonzero + .iter() + .map(|s| (s.as_str(), !s.is_empty())) + .collect(); + + let zero = args + .zero + .iter() + .map(|s| (s.as_str(), s.is_empty())) + .collect(); + + let equals_int = args + .equals_int + .iter() + .map(|(a, b)| ((*a, *b), a == b)) + .collect(); + + let greater_equals_int = args + .greater_equals_int + .iter() + .map(|(a, b)| ((*a, *b), a >= b)) + .collect(); + + let greater_than_int = args + .greater_than_int + .iter() + .map(|(a, b)| ((*a, *b), a > b)) + .collect(); + + let less_equal_int = args + .less_equal_int + .iter() + .map(|(a, b)| ((*a, *b), a <= b)) + .collect(); + + let less_than_int = args + .less_than_int + .iter() + .map(|(a, b)| ((*a, *b), a < b)) + .collect(); + + let not_equal_int = args + .not_equal_int + .iter() + .map(|(a, b)| ((*a, *b), a != b)) + .collect(); + + let equals_str = args + .equals_str + .iter() + .map(|(a, b)| ((a.as_str(), b.as_str()), a == b)) + .collect(); + + let greater_equals_str = args + .greater_equals_str + .iter() + .map(|(a, b)| ((a.as_str(), b.as_str()), a >= b)) + .collect(); + + let greater_than_str = args + .greater_than_str + .iter() + .map(|(a, b)| ((a.as_str(), b.as_str()), a > b)) + .collect(); + + let less_equal_str = args + .less_equal_str + .iter() + .map(|(a, b)| ((a.as_str(), b.as_str()), a <= b)) + .collect(); + + let less_than_str = args + .less_than_str + .iter() + .map(|(a, b)| ((a.as_str(), b.as_str()), a < b)) + .collect(); + + let not_equal_str = args + .not_equal_str + .iter() + .map(|(a, b)| ((a.as_str(), b.as_str()), a != b)) + .collect(); + + let matches_regex = args + .matches_regex + .iter() + .map(|(s, regex)| { + ( + (s.as_str(), regex.as_str()), + Regex::new(regex).is_ok_and(|re| re.is_match(s)), + ) + }) + .collect(); + + let equal_file = args + .equal_file + .iter() + .map(|(file1, file2)| { + let meta1 = std::fs::metadata(file1); + let meta2 = std::fs::metadata(file2); + let result = match (meta1, meta2) { + (Ok(m1), Ok(m2)) => m1.ino() == m2.ino() && m1.dev() == m2.dev(), + _ => false, + }; + ((file1.as_path(), file2.as_path()), result) + }) + .collect(); + + let newer_than = args + .newer_than + .iter() + .map(|(file1, file2)| { + let meta1 = std::fs::metadata(file1); + let meta2 = std::fs::metadata(file2); + let result = match (meta1, meta2) { + (Ok(m1), Ok(m2)) => m1.modified().ok() > m2.modified().ok(), + _ => false, + }; + ((file1.as_path(), file2.as_path()), result) + }) + .collect(); + + let older_than = args + .older_than + .iter() + .map(|(file1, file2)| { + let meta1 = std::fs::metadata(file1); + let meta2 = std::fs::metadata(file2); + let result = match (meta1, meta2) { + (Ok(m1), Ok(m2)) => m1.modified().ok() < m2.modified().ok(), + _ => false, + }; + ((file1.as_path(), file2.as_path()), result) + }) + .collect(); + + let block_special = args + .block_special + .iter() + .map(|file| with_file_meta!(file, m, m.file_type().is_block_device())) + .collect(); + + let char_special = args + .char_special + .iter() + .map(|file| with_file_meta!(file, m, m.file_type().is_char_device())) + .collect(); + + let directory = args + .directory + .iter() + .map(|file| with_file_meta!(file, m, m.is_dir())) + .collect(); + + let exists = args + .exists + .iter() + .map(|file| (file.as_path(), file.exists())) + .collect(); + + let regular_file = args + .regular_file + .iter() + .map(|file| with_file_meta!(file, m, m.is_file())) + .collect(); + + let set_group_id = args + .set_group_id + .iter() + .map(|file| with_file_meta!(file, m, (m.mode() & S_ISGID) != 0)) + .collect(); + + let owned_by_effective_group = args + .owned_by_effective_group + .iter() + .map(|file| with_file_meta!(file, m, m.gid() == getegid().as_raw())) + .collect(); + + let symbolic_link = args + .symbolic_link + .iter() + .map(|file| { + let meta = std::fs::symlink_metadata(file); + let result = match meta { + Ok(m) => m.file_type().is_symlink(), + Err(_) => false, + }; + (file.as_path(), result) + }) + .collect(); + + let sticky_bit = args + .sticky_bit + .iter() + .map(|file| with_file_meta!(file, m, (m.mode() & S_ISVTX) != 0)) + .collect(); + + let modified_since_last_read = args + .modified_since_last_read + .iter() + .map(|file| with_file_meta!(file, m, m.mtime() > m.atime())) + .collect(); + + let owned_by_effective_user = args + .owned_by_effective_user + .iter() + .map(|file| with_file_meta!(file, m, m.uid() == geteuid().as_raw())) + .collect(); + + let named_pipe = args + .named_pipe + .iter() + .map(|file| with_file_meta!(file, m, m.file_type().is_fifo())) + .collect(); + + let read_access = args + .read_access + .iter() + .map(|file| with_file_meta!(file, m, has_permission(&m, Permission::Read))) + .collect(); + + let size_greater_than_zero = args + .size_greater_than_zero + .iter() + .map(|file| with_file_meta!(file, m, m.len() > 0)) + .collect(); + + let is_socket = args + .is_socket + .iter() + .map(|file| with_file_meta!(file, m, m.file_type().is_socket())) + .collect(); + + let fd_on_terminal = args + .fd_on_terminal + .iter() + .map(|fd| (*fd, unsafe { OwnedFd::from_raw_fd(*fd) }.is_terminal())) + .collect(); + + let set_user_id = args + .set_user_id + .iter() + .map(|file| with_file_meta!(file, m, (m.mode() & S_ISUID) != 0)) + .collect(); + + let write_access = args + .write_access + .iter() + .map(|file| with_file_meta!(file, m, has_permission(&m, Permission::Write))) + .collect(); + + let execute_access = args + .execute_access + .iter() + .map(|file| with_file_meta!(file, m, has_permission(&m, Permission::Execute))) + .collect(); + + let env_var_declared = args + .env_var_declared + .iter() + .map(|var| (var.to_string(), std::env::var(var).is_ok())) + .collect(); + + TestResults { + or: args.or, + negate: args.negate, + nonzero, + zero, + equals_int, + greater_equals_int, + greater_than_int, + less_equal_int, + less_than_int, + not_equal_int, + equals_str, + greater_equals_str, + greater_than_str, + less_equal_str, + less_than_str, + not_equal_str, + matches_regex, + equal_file, + newer_than, + older_than, + block_special, + char_special, + directory, + exists, + regular_file, + set_group_id, + owned_by_effective_group, + symbolic_link, + sticky_bit, + modified_since_last_read, + owned_by_effective_user, + named_pipe, + read_access, + size_greater_than_zero, + is_socket, + fd_on_terminal, + set_user_id, + write_access, + execute_access, + env_var_declared, + } +}