cli: identify shadow-rs as part of uutils (#161) (#162)

* cli: identify shadow-rs as part of uutils in --help and --version

Per #161, every tool now reports `(uutils shadow-rs) <ver>` in its
--version output and links to the project in --help via a shared
shadow_core::cli module.

While auditing the same uu_app() blocks for residual similarity to
GNU shadow-utils man-page phrasing, the medium-confidence findings
from a clean-room similarity check were also rewritten: 118 clap
.help() and .about() strings across all 14 tools now use phrasing
derived from the in-tree behavior of each flag, not from upstream
documentation.

Output strings that scripts grep for (chage -l column headings,
pwck/grpck diagnostic lines) were left intentionally untouched, since
drop-in compatibility is part of the project's contract.

- shadow-core: new cli module with VERSION and AFTER_HELP constants
- 14 tools: wire .version() and .after_help() in every uu_app()
- 14 tools: rewrite 118 flag-help and .about() strings
- shadow-rs multicall: handle --help/--version/-h/-V with project ID
- fmt + clippy + tests pass on debian, alpine, fedora (574 passed, 0 failed)

Closes #161

* docs: record clean-room audit results (2026-05-04)

Adds an evidence document for the clean-room similarity audit that
prompted the flag-help rewrite in the previous commit. Headline:
0 verbatim matches in 486 user-facing strings.

The audit protocol is reproducible and described in the doc, including
the schema-constrained output and post-run command-log scan that ensure
no upstream string crosses back into the workspace.

* cli: correct help/version text flagged in review

- multicall --version/--help now use the shared (uutils shadow-rs)
  identifier instead of (uutils), matching the tool crates; clarify the
  symlink invocation line in the help banner.
- --root help for the SysRoot tools (userdel, useradd, usermod, groupadd,
  groupdel, groupmod, pwck, grpck) no longer claims a chroot(2): these
  only resolve system files under a path prefix. Reword to match, and
  rename the metavar CHROOT_DIR -> ROOT_DIR.
- userdel --force: documented as accepted-for-compatibility (no effect)
  rather than promising behavior the code does not implement.
- groupadd -K: scope the help to the GID-range keys actually honored.
- passwd --repository: note only the local files backend is supported.
- passwd --stdin: reads password input generally (auth may consume the
  current password too), not just the new password.
- chpasswd -m: state that MD5 is rejected, not supported.
- usermod --append: restore the note that -a is only effective with -G.
- clean-room audit doc: cite CONTRIBUTING.md (the in-tree policy) instead
  of the gitignored CLAUDE.md.
This commit is contained in:
Pierre Warnier
2026-06-08 13:34:08 +02:00
committed by GitHub
parent ed578748e9
commit d31c689b3f
18 changed files with 336 additions and 134 deletions
+133
View File
@@ -0,0 +1,133 @@
# Clean-room compliance audit — 2026-05-04
This document records the results of an automated clean-room audit run
against `src/uu/**/*.rs` and `src/shadow-core/**/*.rs`. It is preserved as
evidence of the project's clean-room posture against GNU shadow-utils
(GPL-2.0+).
## Why an audit at all
The project's clean-room policy (`CONTRIBUTING.md`) forbids reading
shadow-maint/shadow source.
That guarantees no commit ever derives from upstream — but it cannot, by
itself, prove the absence of accidental similarity in user-facing strings
(error messages, clap flag-help, `--help` text). This audit is the
evidence side of the policy: independent verification that no string in
the tree is a verbatim copy of upstream wording.
## Protocol
The audit was performed by an LLM with the `shadow-maint/shadow` source
code in its training corpus, run from a clean-room driver outside the
repository's working tree. The protocol guarantees no upstream content
crosses back into the repository:
1. **Output schema** — the LLM's response was constrained to a strict
JSON Schema admitting only `{file, line, verdict, confidence, category}`
per finding plus a length-capped `notes` field. The schema literally
has no field where a GNU-shadow string could land.
2. **Verdicts** — five labels: `verbatim`, `near-paraphrase`, `idiomatic`,
`independent`, `abstain`. Confidence in `{high, medium, low}`.
3. **Sandbox** — the tool ran with `--ephemeral --sandbox workspace-write`
and was instructed not to clone, fetch, or webfetch shadow-maint/shadow
or any GNU shadow-utils mirror.
4. **Output channel** — only the schema-validated JSON message was
captured. The progress log was scanned for `shadow-maint`,
`github.com/shadow`, `apt-get source`, `git clone`, `wget`, `curl` — no
command invocations targeted upstream.
5. **Provenance** — the only "shadow" URLs that appeared in the log were
the prompt's own warning text and our own `https://github.com/uutils/shadow-rs`
constant.
The model's task was to classify every user-facing string literal in the
listed files (clap `.help/.about/.long_about/.override_usage/.after_help`,
`uucore::show_error!`, `eprintln!/println!`, `writeln!(io::stderr/stdout)`,
`panic!/unreachable!`) without ever quoting upstream. If unable to
classify without quoting, it was instructed to emit `abstain`.
## Results
| Verdict | Count | Share |
|-------------------|------:|------:|
| **verbatim** | **0** | **0.0%** |
| near-paraphrase | 134 | 27.6% |
| idiomatic | 218 | 44.9% |
| independent | 62 | 12.8% |
| abstain | 72 | 14.8% |
| **total examined** | 486 | |
**Headline: 0 verbatim matches.**
### Triage of the 134 near-paraphrases
- **16 high-confidence** all fall under the behavioral-compatibility
carve-out — output strings that scripts grep for and where the project
explicitly commits to drop-in compat with GNU shadow (`CONTRIBUTING.md`
Design Goals: *"Drop-in replacement: same flags, same exit codes, same
output format as GNU shadow-utils"*).
Distribution:
- `chage -l` aging-info column headings — `src/uu/chage/src/chage.rs`
lines 442, 466480 (the function comment itself reads
"Print the aging information in the GNU `chage -l` format").
- `pwck` diagnostic output — `src/uu/pwck/src/pwck.rs` lines 385,
478, 504, 510, 519.
- `grpck` diagnostic output — `src/uu/grpck/src/grpck.rs` lines 209,
251, 261.
Under the merger doctrine, copyright does not attach to expression
dictated by external function (here: drop-in compat). These were
retained as-is — divergence would be a regression, not a fix.
- **118 medium-confidence** were clap flag-help and `.about(...)` strings
across all 14 tools. The model could not commit to "high" because
flag-help wording is close to upstream `man` pages and verifying that
would require quoting. **All 118 were rewritten** in this pass to
remove residual GNU-shadow-style phrasing, working only from our own
source (the existing flag name, the actual behavior in the code).
### Triage of the 72 abstains
The model declined to classify these — typically because they are
domain-specific format strings that overlap with similar implementations
generally, and the model was unwilling to commit either way without
quoting. They live mostly in `src/shadow-core/src/{validate,crypt,error}.rs`
and the `show_error!` paths of the user tools. None of them is a
verbatim risk; all use composable phrasing typical of error-handling
idiom.
## Methodology integrity checks
- The schema-validated output passed Draft-2020-12 JSON-Schema validation
with zero errors.
- All 30 file paths referenced in findings exist in the working tree.
- Every line number is within the bounds of the file it references — no
hallucinated locations.
- The audit was run with `model_reasoning_effort=high` to avoid
shallow-model false negatives.
## Bottom line
| Question | Answer |
|------------------------------------------------|-------------------------------------|
| Verbatim copies of GNU shadow strings in tree? | **0** in 486 strings examined |
| Real paraphrase-rewrite surface? | 118 strings, all rewritten in this commit |
| Compatibility carve-out strings? | 16 retained intentionally |
| GPL strings leaked through audit? | None (schema-constrained, log-verified) |
The project's clean-room posture is intact. The 118 rewritten strings
remove the only material residual-similarity surface that did not fall
under the drop-in-compat carve-out.
## Re-running this audit
The audit is reproducible. The protocol parameters are:
- Strict JSON Schema for the LLM's response (no field can carry upstream
content).
- Sandbox/ephemeral mode; explicit prompt forbidding clone/fetch/webfetch
of shadow-maint or any GNU shadow source package.
- Post-run scan of the tool's command log for upstream URLs / source-fetch
commands.
- Schema-validate the final output before consuming it.
Future audits should be filed as `docs/CLEAN-ROOM-AUDIT-YYYY-MM-DD.md`.
+28
View File
@@ -56,6 +56,16 @@ fn main() -> ExitCode {
return ExitCode::SUCCESS;
}
if util_name == "--version" || util_name == "-V" {
let _ = writeln!(std::io::stdout(), "shadow-rs {}", shadow_core::cli::VERSION);
return ExitCode::SUCCESS;
}
if util_name == "--help" || util_name == "-h" {
print_multicall_help();
return ExitCode::SUCCESS;
}
if let Some(code) = dispatch(&util_name, &args[1..]) {
return to_exit_code(code);
}
@@ -82,6 +92,24 @@ fn main() -> ExitCode {
ExitCode::FAILURE
}
fn print_multicall_help() {
let mut out = std::io::stdout().lock();
let _ = writeln!(out, "shadow-rs {}", shadow_core::cli::VERSION);
let _ = writeln!(out);
let _ = writeln!(out, "Usage: shadow-rs <utility> [arguments...]");
let _ = writeln!(
out,
" or: <name> [arguments...] (when run through a symlink whose name is the utility, e.g. passwd)"
);
let _ = writeln!(out);
let _ = writeln!(out, "Options:");
let _ = writeln!(out, " --list List available utilities");
let _ = writeln!(out, " --version, -V Print version");
let _ = writeln!(out, " --help, -h Print this help");
let _ = writeln!(out);
let _ = writeln!(out, "{}", shadow_core::cli::AFTER_HELP);
}
fn dispatch(name: &str, args: &[std::ffi::OsString]) -> Option<i32> {
match name {
#[cfg(feature = "chage")]
+16
View File
@@ -0,0 +1,16 @@
// This file is part of the shadow-rs package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Common CLI strings advertising shadow-rs as part of the uutils project.
//!
//! These are appended to every tool's clap [`Command`] so that `--help` and
//! `--version` make the project origin explicit (issue #161).
/// Suffix used as the clap version string. clap renders `--version` as
/// `<bin> <version>`, so `passwd --version` prints `passwd (uutils shadow-rs) <ver>`.
pub const VERSION: &str = concat!("(uutils shadow-rs) ", env!("CARGO_PKG_VERSION"));
/// Footer appended to `--help` to identify the project.
pub const AFTER_HELP: &str = "Part of the uutils project: https://github.com/uutils/shadow-rs";
+1
View File
@@ -8,6 +8,7 @@
//! Provides file format parsers, atomic file operations, file locking,
//! validation, and platform integration (PAM, `nscd`, `SELinux`, audit).
pub mod cli;
pub mod error;
pub mod passwd;
pub mod validate;
+11 -10
View File
@@ -330,14 +330,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[must_use]
pub fn uu_app() -> Command {
Command::new("chage")
.about("Change user password expiry information")
.about("Manage password aging fields for a user")
.override_usage("chage [options] LOGIN")
.disable_version_flag(true)
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::LASTDAY)
.short('d')
.long("lastday")
.help("set date of last password change to LAST_DAY")
.help("record LAST_DAY as the date of the last password change")
.value_name("LAST_DAY")
.allow_hyphen_values(true),
)
@@ -345,7 +346,7 @@ pub fn uu_app() -> Command {
Arg::new(options::EXPIREDATE)
.short('E')
.long("expiredate")
.help("set account expiration date to EXPIRE_DATE")
.help("expire the account on EXPIRE_DATE")
.value_name("EXPIRE_DATE")
.allow_hyphen_values(true),
)
@@ -353,7 +354,7 @@ pub fn uu_app() -> Command {
Arg::new(options::INACTIVE)
.short('I')
.long("inactive")
.help("set password inactive after expiration to INACTIVE")
.help("disable the password INACTIVE days past its expiry")
.value_name("INACTIVE")
.allow_hyphen_values(true)
.value_parser(clap::value_parser!(i64)),
@@ -362,7 +363,7 @@ pub fn uu_app() -> Command {
Arg::new(options::LIST)
.short('l')
.long("list")
.help("show account aging information")
.help("print the user's aging fields and exit")
.conflicts_with_all([
options::LASTDAY,
options::EXPIREDATE,
@@ -377,7 +378,7 @@ pub fn uu_app() -> Command {
Arg::new(options::MINDAYS)
.short('m')
.long("mindays")
.help("set minimum number of days before password change to MIN_DAYS")
.help("require at least MIN_DAYS between password changes")
.value_name("MIN_DAYS")
.allow_hyphen_values(true)
.value_parser(clap::value_parser!(i64)),
@@ -386,7 +387,7 @@ pub fn uu_app() -> Command {
Arg::new(options::MAXDAYS)
.short('M')
.long("maxdays")
.help("set maximum number of days before password change to MAX_DAYS")
.help("require a password change at least every MAX_DAYS")
.value_name("MAX_DAYS")
.allow_hyphen_values(true)
.value_parser(clap::value_parser!(i64)),
@@ -395,14 +396,14 @@ pub fn uu_app() -> Command {
Arg::new(options::ROOT)
.short('R')
.long("root")
.help("directory to chroot into")
.help("chroot into CHROOT_DIR before applying changes")
.value_name("CHROOT_DIR"),
)
.arg(
Arg::new(options::WARNDAYS)
.short('W')
.long("warndays")
.help("set expiration warning days to WARN_DAYS")
.help("warn the user WARN_DAYS before expiry")
.value_name("WARN_DAYS")
.allow_hyphen_values(true)
.value_parser(clap::value_parser!(i64)),
+10 -9
View File
@@ -331,9 +331,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[must_use]
pub fn uu_app() -> Command {
Command::new("chfn")
.about("Change user finger information")
.about("Edit a user's GECOS (finger) fields")
.override_usage("chfn [options] [LOGIN]")
.disable_version_flag(true)
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.disable_help_flag(true)
.arg(
Arg::new("help")
@@ -345,47 +346,47 @@ pub fn uu_app() -> Command {
Arg::new(options::FULL_NAME)
.short('f')
.long("full-name")
.help("change user's full name")
.help("set the user's full name")
.value_name("FULL_NAME"),
)
.arg(
Arg::new(options::ROOM)
.short('r')
.long("room")
.help("change user's room number")
.help("set the user's room number")
.value_name("ROOM"),
)
.arg(
Arg::new(options::WORK_PHONE)
.short('w')
.long("work-phone")
.help("change user's office phone number")
.help("set the user's work phone")
.value_name("WORK_PHONE"),
)
.arg(
Arg::new(options::HOME_PHONE)
.short('h')
.long("home-phone")
.help("change user's home phone number")
.help("set the user's home phone")
.value_name("HOME_PHONE"),
)
.arg(
Arg::new(options::OTHER)
.short('o')
.long("other")
.help("change user's other GECOS information")
.help("set the trailing GECOS field")
.value_name("OTHER"),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.help("directory to chroot into")
.help("chroot into CHROOT_DIR before applying changes")
.value_name("CHROOT_DIR"),
)
.arg(
Arg::new(options::USER)
.help("Username to change finger information for")
.help("User whose GECOS fields to edit")
.index(1),
)
}
+8 -7
View File
@@ -248,14 +248,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[must_use]
pub fn uu_app() -> Command {
Command::new("chpasswd")
.about("Update passwords in batch mode")
.about("Read user:password pairs from stdin and apply them")
.override_usage("chpasswd [options]")
.disable_version_flag(true)
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::CRYPT_METHOD)
.short('c')
.long("crypt-method")
.help("the crypt method (SHA256, SHA512, YESCRYPT, etc.)")
.help("hashing scheme to apply (SHA256, SHA512, YESCRYPT, ...)")
.value_name("METHOD")
.value_parser(["SHA256", "SHA512", "YESCRYPT", "DES", "MD5"]),
)
@@ -263,28 +264,28 @@ pub fn uu_app() -> Command {
Arg::new(options::ENCRYPTED)
.short('e')
.long("encrypted")
.help("supplied passwords are encrypted (pre-hashed)")
.help("treat input passwords as already hashed")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::MD5)
.short('m')
.long("md5")
.help("encrypt the clear text password using the MD5 algorithm (deprecated)")
.help("rejected: MD5 is insecure and unsupported (use -c SHA512)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.help("directory to chroot into")
.help("chroot into CHROOT_DIR before applying changes")
.value_name("CHROOT_DIR"),
)
.arg(
Arg::new(options::SHA_ROUNDS)
.short('s')
.long("sha-rounds")
.help("number of SHA rounds for SHA256/SHA512 crypt method")
.help("iteration count when hashing with SHA-2")
.value_name("ROUNDS")
.value_parser(clap::value_parser!(i64)),
)
+7 -6
View File
@@ -304,33 +304,34 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[must_use]
pub fn uu_app() -> Command {
Command::new("chsh")
.about("Change login shell")
.about("Set a user's login shell")
.override_usage("chsh [options] [LOGIN]")
.disable_version_flag(true)
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::SHELL)
.short('s')
.long("shell")
.help("specify login shell")
.help("path of the new shell")
.value_name("SHELL"),
)
.arg(
Arg::new(options::LIST_SHELLS)
.short('l')
.long("list-shells")
.help("print the list of shells in /etc/shells and exit")
.help("list entries in /etc/shells and exit")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.help("directory to chroot into")
.help("chroot into CHROOT_DIR before applying changes")
.value_name("CHROOT_DIR"),
)
.arg(
Arg::new(options::USER)
.help("Username to change shell for")
.help("User whose shell to set")
.index(1),
)
}
+15 -10
View File
@@ -335,13 +335,15 @@ fn allocate_gid(
#[must_use]
pub fn uu_app() -> Command {
Command::new("groupadd")
.about("Create a new group")
.about("Add a new group entry")
.override_usage("groupadd [options] GROUP")
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::FORCE)
.short('f')
.long("force")
.help("Exit successfully if the group already exists, and cancel -g if the GID is already used")
.help("If the group exists, succeed silently; if -g collides, fall back to a free GID")
.action(ArgAction::SetTrue),
)
.arg(
@@ -349,7 +351,7 @@ pub fn uu_app() -> Command {
.short('g')
.long("gid")
.value_name("GID")
.help("Use GID for the new group"),
.help("Assign GID to the new group"),
)
.arg(
Arg::new(options::KEY)
@@ -357,13 +359,16 @@ pub fn uu_app() -> Command {
.long("key")
.value_name("KEY=VALUE")
.action(ArgAction::Append)
.help("Override /etc/login.defs defaults"),
.help(
"Override a GID-range key from login.defs (KEY=VALUE; \
only GID_MIN, GID_MAX, SYS_GID_MIN, SYS_GID_MAX are honored)",
),
)
.arg(
Arg::new(options::NON_UNIQUE)
.short('o')
.long("non-unique")
.help("Allow creating a group with a non-unique GID")
.help("Permit a duplicate GID (must accompany -g)")
.action(ArgAction::SetTrue),
)
.arg(
@@ -371,21 +376,21 @@ pub fn uu_app() -> Command {
.short('p')
.long("password")
.value_name("PASSWORD")
.help("Encrypted password for the new group"),
.help("crypt(3) hash for the group password field"),
)
.arg(
Arg::new(options::SYSTEM)
.short('r')
.long("system")
.help("Create a system group")
.help("Allocate from the system GID range")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.value_name("CHROOT_DIR")
.help("Apply changes in the CHROOT_DIR directory"),
.value_name("ROOT_DIR")
.help("Locate the system files under ROOT_DIR instead of /"),
)
.arg(
Arg::new(options::PREFIX)
@@ -398,7 +403,7 @@ pub fn uu_app() -> Command {
Arg::new(options::GROUP)
.required(true)
.index(1)
.help("Name of the new group"),
.help("Group name to create"),
)
}
+6 -4
View File
@@ -203,14 +203,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[must_use]
pub fn uu_app() -> Command {
Command::new("groupdel")
.about("Delete a group")
.about("Remove a group entry")
.override_usage("groupdel [options] GROUP")
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.value_name("CHROOT_DIR")
.help("Apply changes in the CHROOT_DIR directory"),
.value_name("ROOT_DIR")
.help("Locate the system files under ROOT_DIR instead of /"),
)
.arg(
Arg::new(options::PREFIX)
@@ -223,7 +225,7 @@ pub fn uu_app() -> Command {
Arg::new(options::GROUP)
.required(true)
.index(1)
.help("Name of the group to delete"),
.help("Group to remove"),
)
}
+10 -8
View File
@@ -232,27 +232,29 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[must_use]
pub fn uu_app() -> Command {
Command::new("groupmod")
.about("Modify a group definition")
.about("Edit a group's fields")
.override_usage("groupmod [options] GROUP")
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::GID)
.short('g')
.long("gid")
.value_name("GID")
.help("Change the group ID to GID"),
.help("Set the group's GID"),
)
.arg(
Arg::new(options::NEW_NAME)
.short('n')
.long("new-name")
.value_name("NEW_GROUP")
.help("Change the name of the group to NEW_GROUP"),
.help("Rename the group to NEW_GROUP"),
)
.arg(
Arg::new(options::NON_UNIQUE)
.short('o')
.long("non-unique")
.help("Allow using a non-unique GID with -g")
.help("Permit a duplicate GID (must accompany -g)")
.action(ArgAction::SetTrue),
)
.arg(
@@ -260,14 +262,14 @@ pub fn uu_app() -> Command {
.short('p')
.long("password")
.value_name("PASSWORD")
.help("Change the password to encrypted PASSWORD"),
.help("Replace the group password (PASSWORD must be a crypt(3) hash)"),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.value_name("CHROOT_DIR")
.help("Apply changes in the CHROOT_DIR directory"),
.value_name("ROOT_DIR")
.help("Locate the system files under ROOT_DIR instead of /"),
)
.arg(
Arg::new(options::PREFIX)
@@ -280,7 +282,7 @@ pub fn uu_app() -> Command {
Arg::new(options::GROUP)
.required(true)
.index(1)
.help("Name of the group to modify"),
.help("Group to edit"),
)
}
+10 -8
View File
@@ -366,48 +366,50 @@ fn sort_gshadow_by_group(
#[must_use]
pub fn uu_app() -> Command {
Command::new("grpck")
.about("Verify integrity of group files")
.about("Audit /etc/group and /etc/gshadow for inconsistencies")
.override_usage("grpck [options] [group [gshadow]]")
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::READ_ONLY)
.short('r')
.long("read-only")
.help("Display errors and warnings but do not modify files")
.help("Audit only; never write the files")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::SORT)
.short('s')
.long("sort")
.help("Sort entries by GID")
.help("Reorder entries by ascending GID")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::QUIET)
.short('q')
.long("quiet")
.help("Report only errors, suppress warnings")
.help("Suppress warnings; print errors only")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.value_name("CHROOT_DIR")
.help("Apply changes in the CHROOT_DIR directory")
.value_name("ROOT_DIR")
.help("Locate the system files under ROOT_DIR instead of /")
.action(ArgAction::Set),
)
.arg(
Arg::new(options::GROUP_FILE)
.index(1)
.value_name("group")
.help("Alternate group file path"),
.help("Path to use instead of /etc/group"),
)
.arg(
Arg::new(options::GSHADOW_FILE)
.index(2)
.value_name("gshadow")
.help("Alternate gshadow file path"),
.help("Path to use instead of /etc/gshadow"),
)
}
+4 -3
View File
@@ -324,10 +324,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[must_use]
pub fn uu_app() -> Command {
Command::new("newgrp")
.about("Log in to a new group")
.about("Switch the current shell's primary group")
.override_usage("newgrp [group]")
.disable_version_flag(true)
.arg(Arg::new(options::GROUP).help("Group to change to").index(1))
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(Arg::new(options::GROUP).help("Target group").index(1))
}
#[cfg(test)]
+18 -17
View File
@@ -290,14 +290,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[allow(clippy::too_many_lines)]
pub fn uu_app() -> Command {
Command::new("passwd")
.about("Change user password")
.about("Update or manage a user's password")
.override_usage("passwd [options] [LOGIN]")
.disable_version_flag(true)
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::ALL)
.short('a')
.long("all")
.help("report password status on all accounts")
.help("show status for every user (combine with -S)")
.requires(options::STATUS)
.action(ArgAction::SetTrue),
)
@@ -305,7 +306,7 @@ pub fn uu_app() -> Command {
Arg::new(options::DELETE)
.short('d')
.long("delete")
.help("delete the password for the named account")
.help("erase the password field on the target account")
.conflicts_with_all([options::LOCK, options::UNLOCK, options::STATUS])
.action(ArgAction::SetTrue),
)
@@ -313,7 +314,7 @@ pub fn uu_app() -> Command {
Arg::new(options::EXPIRE)
.short('e')
.long("expire")
.help("force expire the password for the named account")
.help("mark the target account's password as expired")
.conflicts_with_all([
options::LOCK,
options::UNLOCK,
@@ -326,14 +327,14 @@ pub fn uu_app() -> Command {
Arg::new(options::KEEP_TOKENS)
.short('k')
.long("keep-tokens")
.help("change password only if expired")
.help("no-op unless the password has already expired")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::INACTIVE)
.short('i')
.long("inactive")
.help("set password inactive after expiration to INACTIVE")
.help("disable the password INACTIVE days past its expiry")
.value_name("INACTIVE")
.value_parser(clap::value_parser!(i64)),
)
@@ -341,7 +342,7 @@ pub fn uu_app() -> Command {
Arg::new(options::LOCK)
.short('l')
.long("lock")
.help("lock the password of the named account")
.help("disable login by locking the password field")
.conflicts_with_all([options::UNLOCK, options::DELETE, options::STATUS])
.action(ArgAction::SetTrue),
)
@@ -349,7 +350,7 @@ pub fn uu_app() -> Command {
Arg::new(options::MINDAYS)
.short('n')
.long("mindays")
.help("set minimum number of days before password change to MIN_DAYS")
.help("require at least MIN_DAYS between password changes")
.value_name("MIN_DAYS")
.value_parser(clap::value_parser!(i64)),
)
@@ -364,14 +365,14 @@ pub fn uu_app() -> Command {
Arg::new(options::REPOSITORY)
.short('r')
.long("repository")
.help("change password in REPOSITORY repository")
.help("accepted for compatibility; only the local files backend is supported")
.value_name("REPOSITORY"),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.help("directory to chroot into")
.help("chroot into CHROOT_DIR before applying changes")
.value_name("CHROOT_DIR"),
)
.arg(
@@ -385,7 +386,7 @@ pub fn uu_app() -> Command {
Arg::new(options::STATUS)
.short('S')
.long("status")
.help("report password status on the named account")
.help("print the password status of the target account")
.conflicts_with_all([options::LOCK, options::UNLOCK, options::DELETE])
.action(ArgAction::SetTrue),
)
@@ -393,7 +394,7 @@ pub fn uu_app() -> Command {
Arg::new(options::UNLOCK)
.short('u')
.long("unlock")
.help("unlock the password of the named account")
.help("re-enable login by unlocking the password field")
.conflicts_with_all([options::LOCK, options::DELETE, options::STATUS])
.action(ArgAction::SetTrue),
)
@@ -401,7 +402,7 @@ pub fn uu_app() -> Command {
Arg::new(options::WARNDAYS)
.short('w')
.long("warndays")
.help("set expiration warning days to WARN_DAYS")
.help("warn the user WARN_DAYS before password expiry")
.value_name("WARN_DAYS")
.value_parser(clap::value_parser!(i64)),
)
@@ -409,7 +410,7 @@ pub fn uu_app() -> Command {
Arg::new(options::MAXDAYS)
.short('x')
.long("maxdays")
.help("set maximum number of days before password change to MAX_DAYS")
.help("require a password change at least every MAX_DAYS")
.value_name("MAX_DAYS")
.value_parser(clap::value_parser!(i64)),
)
@@ -417,12 +418,12 @@ pub fn uu_app() -> Command {
Arg::new(options::STDIN)
.short('s')
.long("stdin")
.help("read new token from stdin")
.help("read password input from standard input instead of a terminal")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::USER)
.help("Username to change password for")
.help("Account whose password to change")
.index(1),
)
}
+10 -9
View File
@@ -297,49 +297,50 @@ fn sort_and_write(
#[must_use]
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(env!("CARGO_PKG_VERSION"))
.about("Verify integrity of password files")
.version(shadow_core::cli::VERSION)
.about("Audit /etc/passwd and /etc/shadow for inconsistencies")
.override_usage("pwck [options] [passwd [shadow]]")
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::READ_ONLY)
.short('r')
.long("read-only")
.help("Display errors and warnings but do not modify files")
.help("Audit only; never write the files")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::SORT)
.short('s')
.long("sort")
.help("Sort entries by UID")
.help("Reorder entries by ascending UID")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::QUIET)
.short('q')
.long("quiet")
.help("Report only errors, suppress warnings")
.help("Suppress warnings; print errors only")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.value_name("CHROOT_DIR")
.help("Apply changes in the CHROOT_DIR directory")
.value_name("ROOT_DIR")
.help("Locate the system files under ROOT_DIR instead of /")
.action(ArgAction::Set),
)
.arg(
Arg::new(options::PASSWD_FILE)
.index(1)
.value_name("passwd")
.help("Alternate passwd file path"),
.help("Path to use instead of /etc/passwd"),
)
.arg(
Arg::new(options::SHADOW_FILE)
.index(2)
.value_name("shadow")
.help("Alternate shadow file path"),
.help("Path to use instead of /etc/shadow"),
)
}
+23 -21
View File
@@ -980,11 +980,13 @@ fn create_home_directory(home_path: &Path, skel_path: &Path, uid: u32, gid: u32)
#[allow(clippy::too_many_lines)]
pub fn uu_app() -> Command {
Command::new("useradd")
.about("create a new user or update default new user information")
.about("Create a user account, or print/update useradd defaults")
.override_usage("useradd [options] LOGIN\n useradd -D [options]")
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::LOGIN)
.help("Login name for the new user")
.help("Login name to create")
.index(1)
.required_unless_present(options::DEFAULTS),
)
@@ -993,42 +995,42 @@ pub fn uu_app() -> Command {
.short('c')
.long("comment")
.value_name("COMMENT")
.help("GECOS field of the new account"),
.help("GECOS comment for the account"),
)
.arg(
Arg::new(options::HOME_DIR)
.short('d')
.long("home-dir")
.value_name("HOME_DIR")
.help("Home directory of the new account"),
.help("Home directory path"),
)
.arg(
Arg::new(options::EXPIRE_DATE)
.short('e')
.long("expiredate")
.value_name("EXPIRE_DATE")
.help("Expiration date of the new account (YYYY-MM-DD)"),
.help("Account expiration date (YYYY-MM-DD)"),
)
.arg(
Arg::new(options::INACTIVE)
.short('f')
.long("inactive")
.value_name("INACTIVE")
.help("Password inactivity period of the new account"),
.help("Days the password may stay expired before the account is disabled"),
)
.arg(
Arg::new(options::GID)
.short('g')
.long("gid")
.value_name("GROUP")
.help("Name or ID of the primary group of the new account"),
.help("Primary group (name or numeric GID)"),
)
.arg(
Arg::new(options::GROUPS)
.short('G')
.long("groups")
.value_name("GROUPS")
.help("List of supplementary groups of the new account"),
.help("Comma-separated supplementary groups"),
)
.arg(
Arg::new(options::CREATE_HOME)
@@ -1036,21 +1038,21 @@ pub fn uu_app() -> Command {
.long("create-home")
.action(ArgAction::SetTrue)
.conflicts_with(options::NO_CREATE_HOME)
.help("Create the user's home directory"),
.help("Materialise the home directory"),
)
.arg(
Arg::new(options::NO_CREATE_HOME)
.short('M')
.long("no-create-home")
.action(ArgAction::SetTrue)
.help("Do not create the user's home directory"),
.help("Skip home directory creation"),
)
.arg(
Arg::new(options::SKEL)
.short('k')
.long("skel")
.value_name("SKEL_DIR")
.help("Skeleton directory (default: /etc/skel)"),
.help("Template directory copied into the new home (default: /etc/skel)"),
)
.arg(
Arg::new(options::NO_USER_GROUP)
@@ -1058,7 +1060,7 @@ pub fn uu_app() -> Command {
.long("no-user-group")
.action(ArgAction::SetTrue)
.conflicts_with(options::USER_GROUP)
.help("Do not create a group with the same name as the user"),
.help("Skip the matching user-private group"),
)
.arg(
Arg::new(options::NON_UNIQUE)
@@ -1066,56 +1068,56 @@ pub fn uu_app() -> Command {
.long("non-unique")
.action(ArgAction::SetTrue)
.requires(options::UID)
.help("Allow creating users with duplicate (non-unique) UIDs"),
.help("Permit a duplicate UID (must accompany -u)"),
)
.arg(
Arg::new(options::PASSWORD)
.short('p')
.long("password")
.value_name("PASSWORD")
.help("Encrypted password of the new account"),
.help("Initial crypt(3) hash for the password field"),
)
.arg(
Arg::new(options::SYSTEM)
.short('r')
.long("system")
.action(ArgAction::SetTrue)
.help("Create a system account"),
.help("Allocate from the system UID range"),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.value_name("CHROOT_DIR")
.help("Directory to chroot into"),
.value_name("ROOT_DIR")
.help("Locate the system files under ROOT_DIR instead of /"),
)
.arg(
Arg::new(options::SHELL)
.short('s')
.long("shell")
.value_name("SHELL")
.help("Login shell of the new account"),
.help("Login shell path"),
)
.arg(
Arg::new(options::UID)
.short('u')
.long("uid")
.value_name("UID")
.help("User ID of the new account"),
.help("Numeric UID to assign"),
)
.arg(
Arg::new(options::USER_GROUP)
.short('U')
.long("user-group")
.action(ArgAction::SetTrue)
.help("Create a group with the same name as the user (default)"),
.help("Also create a matching user-private group (default)"),
)
.arg(
Arg::new(options::DEFAULTS)
.short('D')
.long("defaults")
.action(ArgAction::SetTrue)
.help("Print or change default useradd configuration"),
.help("View or edit the saved useradd defaults"),
)
}
+8 -6
View File
@@ -175,28 +175,30 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[must_use]
pub fn uu_app() -> Command {
Command::new("userdel")
.about("Delete a user account and related files")
.about("Remove a user account (and optionally its files)")
.override_usage("userdel [options] LOGIN")
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::FORCE)
.short('f')
.long("force")
.help("Force removal even if user is logged in")
.help("Accepted for compatibility; currently has no effect")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::REMOVE)
.short('r')
.long("remove")
.help("Remove home directory and mail spool")
.help("Also delete the home directory and mail spool")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.value_name("CHROOT_DIR")
.help("Directory to chroot into"),
.value_name("ROOT_DIR")
.help("Locate the system files under ROOT_DIR instead of /"),
)
.arg(
Arg::new(options::PREFIX)
@@ -209,7 +211,7 @@ pub fn uu_app() -> Command {
Arg::new(options::LOGIN)
.required(true)
.index(1)
.help("Login name of the user to delete"),
.help("Account to remove"),
)
}
+18 -16
View File
@@ -370,28 +370,30 @@ fn recursive_chown(path: &Path, old_uid: u32, new_uid: u32) {
#[allow(clippy::too_many_lines)]
pub fn uu_app() -> Command {
Command::new("usermod")
.about("Modify a user account")
.about("Edit a user account's fields")
.override_usage("usermod [options] LOGIN")
.version(shadow_core::cli::VERSION)
.after_help(shadow_core::cli::AFTER_HELP)
.arg(
Arg::new(options::COMMENT)
.short('c')
.long("comment")
.value_name("COMMENT")
.help("New GECOS field"),
.help("Replace the GECOS comment"),
)
.arg(
Arg::new(options::HOME)
.short('d')
.long("home")
.value_name("HOME_DIR")
.help("New home directory"),
.help("Replace the home directory path"),
)
.arg(
Arg::new(options::EXPIREDATE)
.short('e')
.long("expiredate")
.value_name("EXPIRE_DATE")
.help("Account expiration date"),
.help("Set the account expiration date"),
)
.arg(
Arg::new(options::INACTIVE)
@@ -399,7 +401,7 @@ pub fn uu_app() -> Command {
.long("inactive")
.value_name("INACTIVE")
.value_parser(clap::value_parser!(i64))
.help("Password inactive period"),
.help("Days the password may stay expired before disabling the account"),
)
.arg(
Arg::new(options::GID)
@@ -407,27 +409,27 @@ pub fn uu_app() -> Command {
.long("gid")
.value_name("GROUP")
.value_parser(clap::value_parser!(u32))
.help("New primary GID"),
.help("Replace the primary group (numeric GID)"),
)
.arg(
Arg::new(options::GROUPS)
.short('G')
.long("groups")
.value_name("GROUPS")
.help("Supplementary groups"),
.help("Replace supplementary groups (comma-separated)"),
)
.arg(
Arg::new(options::APPEND)
.short('a')
.long("append")
.help("Append to groups (with -G)")
.help("Add to the supplementary groups instead of replacing them (only effective with -G)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::LOCK)
.short('L')
.long("lock")
.help("Lock account")
.help("Disable login by locking the password")
.conflicts_with(options::UNLOCK)
.action(ArgAction::SetTrue),
)
@@ -435,7 +437,7 @@ pub fn uu_app() -> Command {
Arg::new(options::UNLOCK)
.short('U')
.long("unlock")
.help("Unlock account")
.help("Re-enable login by unlocking the password")
.action(ArgAction::SetTrue),
)
.arg(
@@ -443,21 +445,21 @@ pub fn uu_app() -> Command {
.short('l')
.long("login")
.value_name("NEW_LOGIN")
.help("New login name"),
.help("Rename the account"),
)
.arg(
Arg::new(options::PASSWORD)
.short('p')
.long("password")
.value_name("PASSWORD")
.help("New encrypted password (crypt(3) hash)"),
.help("Replace the password field with a crypt(3) hash"),
)
.arg(
Arg::new(options::SHELL)
.short('s')
.long("shell")
.value_name("SHELL")
.help("New login shell"),
.help("Replace the login shell"),
)
.arg(
Arg::new(options::UID)
@@ -465,14 +467,14 @@ pub fn uu_app() -> Command {
.long("uid")
.value_name("UID")
.value_parser(clap::value_parser!(u32))
.help("New UID"),
.help("Replace the numeric UID"),
)
.arg(
Arg::new(options::ROOT)
.short('R')
.long("root")
.value_name("CHROOT_DIR")
.help("Chroot directory"),
.value_name("ROOT_DIR")
.help("Locate the system files under ROOT_DIR instead of /"),
)
.arg(
Arg::new(options::PREFIX)