Initial commit

This commit is contained in:
2025-10-16 16:35:56 +09:00
commit e0863d808c
10 changed files with 1301 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
result

348
Cargo.lock generated Normal file
View File

@@ -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"

18
Cargo.toml Normal file
View File

@@ -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

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 h7x4 <h7x4@nani.wtf>
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.

5
README.md Normal file
View File

@@ -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 ...`.

39
default.nix Normal file
View File

@@ -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";
};
}

48
flake.lock generated Normal file
View File

@@ -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
}

57
flake.nix Normal file
View File

@@ -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 { };
});
};
}

762
src/main.rs Normal file
View File

@@ -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<T>(s: &str) -> Result<(T, T), String>
where
T: std::str::FromStr,
<T as 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<bool>,
// 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<String>,
/// 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<String>,
/// The length of STRING is nonzero
#[arg(short = 'n', value_name = "STRING")]
nonzero: Vec<String>,
/// The length of STRING is zero
#[arg(short = 'z', value_name = "STRING")]
zero: Vec<String>,
/// INTEGER1 is equal to INTEGER2
#[arg(long = "ieq", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::<i64>)]
equals_int: Vec<(i64, i64)>,
/// INTEGER1 is greater than or equal to INTEGER2
#[arg(long = "ige", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::<i64>)]
greater_equals_int: Vec<(i64, i64)>,
/// INTEGER1 is greater than INTEGER2
#[arg(long = "igt", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::<i64>)]
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::<i64>)]
less_equal_int: Vec<(i64, i64)>,
/// INTEGER1 is less than INTEGER2
#[arg(long = "ilt", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::<i64>)]
less_than_int: Vec<(i64, i64)>,
/// INTEGER1 is not equal to INTEGER2
#[arg(long = "ine", value_name = "INTEGER1:INTEGER2", value_parser = parse_tuple::<i64>)]
not_equal_int: Vec<(i64, i64)>,
/// STRING1 is equal to STRING2
#[arg(long = "eq", value_name = "STRING1:STRING2", value_parser = parse_tuple::<String>)]
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::<String>)]
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::<String>)]
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::<String>)]
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::<String>)]
less_than_str: Vec<(String, String)>,
/// STRING1 is not equal to STRING2
#[arg(long = "ne", value_name = "STRING1:STRING2", value_parser = parse_tuple::<String>)]
not_equal_str: Vec<(String, String)>,
/// STRING1 matches REGEX
#[arg(long = "match", value_name = "STRING:REGEX", value_parser = parse_tuple::<String>)]
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::<PathBuf>)]
equal_file: Vec<(PathBuf, PathBuf)>,
/// FILE1 is newer (modification date) than FILE2
#[arg(long = "nt", value_name = "FILE1:FILE2", value_parser = parse_tuple::<PathBuf>)]
newer_than: Vec<(PathBuf, PathBuf)>,
/// FILE1 is older than FILE2
#[arg(long = "ot", value_name = "FILE1:FILE2", value_parser = parse_tuple::<PathBuf>)]
older_than: Vec<(PathBuf, PathBuf)>,
/// File exists and is block special
#[arg(short = 'b', value_name = "FILE")]
block_special: Vec<PathBuf>,
/// File exists and is character special
#[arg(short = 'c', value_name = "FILE")]
char_special: Vec<PathBuf>,
/// File exists and is a directory
#[arg(short = 'd', value_name = "FILE")]
directory: Vec<PathBuf>,
/// File exists
#[arg(short = 'e', value_name = "FILE")]
exists: Vec<PathBuf>,
/// File exists and is a regular file
#[arg(short = 'f', value_name = "FILE")]
regular_file: Vec<PathBuf>,
/// File exists and is set-group-ID
#[arg(short = 'g', value_name = "FILE")]
set_group_id: Vec<PathBuf>,
/// File exists and is owned by the effective group ID
#[arg(short = 'G', value_name = "FILE")]
owned_by_effective_group: Vec<PathBuf>,
/// File exists and is a symbolic link
#[arg(short = 'h', value_name = "FILE", alias = "L")]
symbolic_link: Vec<PathBuf>,
/// File exists and has its sticky bit set
#[arg(short = 'k', value_name = "FILE")]
sticky_bit: Vec<PathBuf>,
/// File exists and has been modified since it was last read
#[arg(short = 'N', value_name = "FILE")]
modified_since_last_read: Vec<PathBuf>,
/// File exists and is owned by the effective user ID
#[arg(short = 'O', value_name = "FILE")]
owned_by_effective_user: Vec<PathBuf>,
/// File exists and is a named pipe
#[arg(short = 'p', value_name = "FILE")]
named_pipe: Vec<PathBuf>,
/// File exists and the user has read access
#[arg(short = 'r', value_name = "FILE")]
read_access: Vec<PathBuf>,
/// File exists and has a size greater than zero
#[arg(short = 's', value_name = "FILE")]
size_greater_than_zero: Vec<PathBuf>,
/// File exists and is a socket
#[arg(short = 'S', value_name = "FILE")]
is_socket: Vec<PathBuf>,
/// File descriptor FD is opened on a terminal
#[arg(short = 't', value_name = "FD")]
fd_on_terminal: Vec<i32>,
/// File exists and its set-user-ID bit is set
#[arg(short = 'u', value_name = "FILE")]
set_user_id: Vec<PathBuf>,
/// File exists and the user has write access
#[arg(short = 'w', value_name = "FILE")]
write_access: Vec<PathBuf>,
/// File exists and the user has execute (or search) access
#[arg(short = 'x', value_name = "FILE")]
execute_access: Vec<PathBuf>,
/// Environment variable VAR is declared
#[arg(short = 'v', value_name = "VAR")]
env_var_declared: Vec<String>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
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<ExpressionResult>,
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::<Vec<_>>(),
&self.zero.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self.equals_int.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self
.greater_equals_int
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.greater_than_int
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.less_equal_int
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.less_than_int
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.not_equal_int
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self.equals_str.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self
.greater_equals_str
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.greater_than_str
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.less_equal_str
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.less_than_str
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.not_equal_str
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.matches_regex
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self.equal_file.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self.newer_than.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self.older_than.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self
.block_special
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self.char_special.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self.directory.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self.exists.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self.regular_file.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self.set_group_id.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self
.owned_by_effective_group
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.symbolic_link
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self.sticky_bit.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self
.modified_since_last_read
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.owned_by_effective_user
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self.named_pipe.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self.read_access.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self
.size_greater_than_zero
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self.is_socket.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self
.fd_on_terminal
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self.set_user_id.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self.write_access.iter().map(|(_, r)| r).collect::<Vec<_>>(),
&self
.execute_access
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
&self
.env_var_declared
.iter()
.map(|(_, r)| r)
.collect::<Vec<_>>(),
];
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,
}
}