diff --git a/Makefile b/Makefile
index 5e2c70d..b7bce98 100644
--- a/Makefile
+++ b/Makefile
@@ -32,7 +32,7 @@ RUSTLIBS = --extern getopt=build/o/libgetopt.rlib \
CFLAGS += -I$(SYSEXITS)
.PHONY: all
-all: dj false fop hru intcmp mm npc rpn scrut str strcmp swab true
+all: dj false fileis fop hru intcmp mm npc rpn scrut str strcmp swab true
# keep build/include until bindgen(1) has stdin support
# https://github.com/rust-lang/rust-bindgen/issues/2703
@@ -97,6 +97,11 @@ false: build/bin/false
build/bin/false: src/false.c build
$(CC) $(CFLAGS) -o $@ src/false.c
+.PHONY: fileis
+fileis: build/bin/fileis
+build/bin/fileis: src/fileis.rs build
+ $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/fileis.rs
+
.PHONY: fop
fop: build/bin/fop
build/bin/fop: src/fop.rs build rustlibs
@@ -127,11 +132,6 @@ rpn: build/bin/rpn
build/bin/rpn: src/rpn.rs build rustlibs
$(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/rpn.rs
-.PHONY: scrut
-scrut: build/bin/scrut
-build/bin/scrut: src/scrut.c build
- $(CC) $(CFLAGS) -o $@ src/scrut.c
-
.PHONY: str
str: build/bin/str
build/bin/str: src/str.c build
diff --git a/docs/scrut.1 b/docs/fileis.1
similarity index 95%
rename from docs/scrut.1
rename to docs/fileis.1
index 2b95bee..ed578a6 100644
--- a/docs/scrut.1
+++ b/docs/fileis.1
@@ -4,12 +4,12 @@
.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license,
.\" visit .
.\"
-.TH SCRUT 1 2024-06-06 "Harakit X.X.X"
+.TH SCRUT 1 2024-07-18 "Harakit X.X.X"
.SH NAME
-scrut \(en scrutinize file properties
+fileis \(en scrutinize file properties
.SH SYNOPSIS
-scrut
+fileis
.RB [ -LSbcdefgkprsuwx ]
.B file...
.\"
@@ -82,5 +82,6 @@ Copyright \(co 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later
.\"
.SH SEE ALSO
.BR access (3p),
+.BR chown (1p),
.BR lstat (3p),
.BR test (1p)
diff --git a/src/fileis.rs b/src/fileis.rs
new file mode 100644
index 0000000..b81b624
--- /dev/null
+++ b/src/fileis.rs
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2024 DTB
+ * Copyright (c) 2024 Emma Tebibyte
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+
+use std::{
+ env::args,
+ fs::metadata,
+ os::unix::fs::{ FileTypeExt, MetadataExt },
+ process::ExitCode,
+};
+
+extern crate getopt;
+extern crate strerror;
+extern crate sysexits;
+
+use getopt::GetOpt;
+use strerror::StrError;
+use sysexits::EX_USAGE;
+
+const OPTS: &str = "bcdefgkprsuwxLS";
+
+fn usage(argv0: &str) -> ExitCode {
+ eprintln!("Usage: {} [-{}] file...", argv0, OPTS);
+ ExitCode::from(EX_USAGE)
+}
+
+fn main() -> ExitCode {
+ let argv = args().collect::>();
+
+ let mut sel = String::with_capacity(OPTS.len()); // selected options
+ let mut optind: usize = 1; // argv[0]
+
+ while let Some(opt) = argv.getopt(OPTS) {
+ if let Ok(optchr) = opt.opt() { sel.push_str(optchr); }
+ else { return usage(&argv[0]); }
+
+ optind = opt.ind();
+ }
+
+ if optind == argv.len() { return usage(&argv[0]); }
+
+ for arg in argv.iter().skip(optind) {
+ let fmeta = match metadata(arg) {
+ Ok(m) => m,
+ Err(e) => { // no perms or nonexistent
+ eprintln!("{}: {}: {}", argv[0], arg, e.strerror());
+ return ExitCode::FAILURE;
+ },
+ };
+
+ let fmode = fmeta.mode();
+ let ftype = fmeta.file_type();
+
+ for selection in sel.chars() { // run all selected tests
+ match selection {
+ 'b' if ftype.is_block_device() => (),
+ 'c' if ftype.is_char_device() => (),
+ 'e' => (), // exists or metadata would have errored
+ 'd' if fmeta.is_dir() => (),
+ 'f' if fmeta.is_file() => (),
+ 'g' if fmode & 0o2000 /* S_ISGID */ != 0 => (), // setgid
+ 'k' if fmode & 0o1000 /* S_ISVTX */ != 0 => (), // setvtx
+ 'p' if ftype.is_fifo() => (),
+ 'r' if fmode & 0o0400 /* S_IRUSR */ != 0 => (), // read access
+ 'u' if fmode & 0o4000 /* S_ISUID */ != 0 => (), // setuid
+ 'w' if fmode & 0o0200 /* S_IWUSR */ != 0 => (), // write access
+ 'x' if fmode & 0o0100 /* S_IXUSR */ != 0 => (), // exec access
+ 'L' if fmeta.is_symlink() => (),
+ 'S' if ftype.is_socket() => (),
+ _ => { return ExitCode::FAILURE; }
+ }
+ }
+ }
+
+ ExitCode::SUCCESS
+}
diff --git a/src/scrut.c b/src/scrut.c
deleted file mode 100644
index d85d243..0000000
--- a/src/scrut.c
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (c) 2023–2024 DTB
- * Copyright (c) 2024 Emma Tebibyte
- * SPDX-License-Identifier: AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify it under
- * the terms of the GNU Affero General Public License as published by the Free
- * Software Foundation, either version 3 of the License, or (at your option) any
- * later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
- * details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see https://www.gnu.org/licenses/.
- */
-
-#include /* fprintf(3), stderr, NULL */
-#include /* EXIT_FAILURE, EXIT_SUCCESS */
-#include /* memset(3), strchr(3) */
-#ifndef EX_USAGE
-# include
-#endif
-#include /* access(3), getopt(3), F_OK, R_OK, W_OK, X_OK */
-#include /* lstat(3), stat struct, S_ISBLK, S_ISCHR, S_ISDIR,
- * S_ISFIFO, S_ISGID, S_ISREG, S_ISLNK, S_ISSOCK,
- * S_ISUID, S_ISVTX */
-
-static char args[] = "bcdefghkprsuwxLS";
-static char ops[(sizeof args) / (sizeof *args)];
-static char *program_name = "scrut";
-
-int main(int argc, char *argv[]){
- struct stat buf;
- int c;
- size_t i;
- char *p;
-
- if(argc < 2)
- goto usage;
-
- memset(ops, '\0', sizeof ops);
- while((c = getopt(argc, argv, args)) != -1)
- if((p = strchr(args, c)) == NULL)
- goto usage;
- else
- ops[p - args] = c;
- /* straighten out ops */
- for(i = 0, p = ops; i < (sizeof ops) / (sizeof *ops); ++i)
- if(ops[i] != '\0'){
- *p = ops[i];
- if(&ops[i] != p++)
- ops[i] = '\0';
- }
-
- if(optind == argc)
- goto usage;
-
- argv += optind;
- do{ if(access(*argv, F_OK) != 0 || lstat(*argv, &buf) == -1)
- return EXIT_FAILURE; /* doesn't exist or isn't stattable */
-
- for(i = 0; ops[i] != '\0'; ++i)
- if(ops[i] == 'e')
- continue;
- else if(ops[i] == 'h'){
-usage: fprintf(stderr, "Usage: %s [-%s] file...\n",
- argv[0] == NULL
- ? program_name
- : argv[0],
- args);
-
- return EX_USAGE;
- }else if(
- (ops[i] == 'b'
- && !S_ISBLK(buf.st_mode))
- || (ops[i] == 'c'
- && !S_ISCHR(buf.st_mode))
- || (ops[i] == 'd'
- && !S_ISDIR(buf.st_mode))
- || (ops[i] == 'f'
- && !S_ISREG(buf.st_mode))
- || (ops[i] == 'g'
- && !(buf.st_mode & S_ISGID))
- || (ops[i] == 'k'
- && !(buf.st_mode & S_ISVTX))
- || (ops[i] == 'p'
- && !S_ISFIFO(buf.st_mode))
- || (ops[i] == 'r'
- && access(*argv, R_OK) != 0)
- || (ops[i] == 'u'
- && !(buf.st_mode & S_ISUID))
- || (ops[i] == 'w'
- && access(*argv, W_OK) != 0)
- || (ops[i] == 'x'
- && access(*argv, X_OK) != 0)
- || (ops[i] == 'L'
- && !S_ISLNK(buf.st_mode))
- || (ops[i] == 'S'
- && !S_ISSOCK(buf.st_mode)))
- return EXIT_FAILURE;
- }while(*++argv != NULL);
-
- return EXIT_SUCCESS;
-}