commit e0863d808c1b9f701e9a8db5fee4356787ef7015 Author: h7x4 Date: Thu Oct 16 16:35:56 2025 +0900 Initial commit 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, + } +}