Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions crates/togl-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,32 +47,32 @@ pub struct Cli {
pub recursive: bool,

/// List all section IDs found in files (discovery mode, no toggling)
#[arg(long = "list-sections")]
#[arg(long = "list-sections", group = "operation")]
pub list_sections: bool,

Comment on lines 49 to 52
/// Detail level for --list-sections text output [default: lines]
#[arg(long = "fields", default_value = "lines")]
pub fields: ListFields,

/// Remove a named section (requires -S <ID>). Recursive with -R. See --remove-mode.
#[arg(long = "remove")]
#[arg(long = "remove", group = "operation")]
pub remove: bool,

/// What --remove strips: markers | commented | all [default: commented]
#[arg(long = "remove-mode", default_value = "commented")]
pub remove_mode: RemoveMode,

/// With --remove, exit non-zero if -S <ID> matched no sections.
#[arg(long = "require-match")]
#[arg(long = "require-match", requires = "remove")]
pub require_match: bool,

/// Insert a toggle:start/end marker pair around a single -l range (single file).
/// Requires exactly one -S <ID> and one -l <range>. Leaves the body uncommented.
#[arg(long = "insert")]
#[arg(long = "insert", group = "operation")]
pub insert: bool,

/// Description for the inserted section marker (use with --insert).
#[arg(long = "desc")]
#[arg(long = "desc", requires = "insert")]
pub desc: Option<String>,

/// Force toggle state (on/off/invert)
Expand Down Expand Up @@ -132,15 +132,15 @@ pub struct Cli {
pub backup: Option<String>,

/// Extend the last --line range to the end of file
#[arg(long = "to-end")]
#[arg(long = "to-end", requires = "lines")]
pub to_end: bool,

/// Scan for section IDs without modifying files
#[arg(long = "scan")]
#[arg(long = "scan", group = "operation")]
pub scan: bool,

/// Validate section integrity without modifying files. Requires --scan.
#[arg(long = "check")]
#[arg(long = "check", requires = "scan")]
pub check: bool,

/// Enforce exactly 2 variants in the targeted group; error otherwise.
Expand All @@ -159,7 +159,7 @@ pub struct Cli {

/// Disable backup creation in atomic mode. Only valid with --atomic.
/// WARNING: Without backups, rollback is not possible if the rename phase fails.
#[arg(long = "no-backup")]
#[arg(long = "no-backup", requires = "atomic")]
pub no_backup: bool,

/// Recover from an interrupted atomic operation. Default: rollback.
Expand All @@ -168,7 +168,7 @@ pub struct Cli {

/// Complete an interrupted atomic commit instead of rolling back.
/// Must be combined with --recover.
#[arg(long = "recover-forward")]
#[arg(long = "recover-forward", requires = "recover")]
pub recover_forward: bool,

/// Generate shell completions for the given shell to stdout.
Expand Down
39 changes: 10 additions & 29 deletions crates/togl-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,8 @@ fn run(cli: &Cli) -> Result<()> {
if cli.atomic && cli.dry_run {
return Err(UsageError("--atomic cannot be combined with --dry-run.".into()).into());
}
if cli.no_backup && !cli.atomic {
return Err(UsageError("--no-backup is only valid with --atomic.".into()).into());
}
if cli.recover_forward && !cli.recover {
return Err(UsageError("--recover-forward requires --recover.".into()).into());
}
// --no-backup requires --atomic, --recover-forward requires --recover:
// enforced declaratively in cli.rs via clap `requires`.
// Note: --atomic --stdout is not applicable (no --stdout flag exists yet)
// Note: --atomic --in-place is not applicable (no --in-place flag exists yet)

Expand Down Expand Up @@ -249,9 +245,7 @@ fn run(cli: &Cli) -> Result<()> {
// Handle --scan mode early (read-only, no toggle options needed).
// Per PRD §0.14.2, --scan -S <id> is the detailed group view, so --section is allowed here.
if cli.scan {
if cli.insert {
return Err(UsageError("--scan cannot be combined with --insert".into()).into());
}
// --scan vs --insert: enforced by the `operation` arg-group in cli.rs.
if !cli.lines.is_empty() {
return Err(UsageError("--scan cannot be combined with --line".into()).into());
}
Expand All @@ -261,9 +255,7 @@ fn run(cli: &Cli) -> Result<()> {
return run_scan(cli);
}

if cli.check && !cli.scan {
return Err(UsageError("--check requires --scan".into()).into());
}
// --check requires --scan: enforced declaratively in cli.rs via clap `requires`.

// Validate --comment-style: must be 1 or 3 values
if cli.comment_style.len() == 2 {
Expand All @@ -273,10 +265,7 @@ fn run(cli: &Cli) -> Result<()> {
.into());
}

// Validate --to-end requires --line
if cli.to_end && cli.lines.is_empty() {
return Err(UsageError("--to-end requires at least one --line range".into()).into());
}
// --to-end requires --line: enforced declaratively in cli.rs via clap `requires`.

// Validate --pair: pre-execution guard per PRD §0.13.4
if cli.pair {
Expand All @@ -301,15 +290,11 @@ fn run(cli: &Cli) -> Result<()> {
}
// --remove validation (P06)
if cli.remove {
// Mutual exclusion with other modes (--insert/--list-sections/--scan) is
// enforced by the `operation` arg-group in cli.rs.
if cli.sections.len() != 1 {
return Err(UsageError("--remove requires exactly one -S <ID>".into()).into());
}
if cli.insert || cli.list_sections || cli.scan {
return Err(UsageError(
"--remove cannot be combined with --insert, --list-sections, or --scan".into(),
)
.into());
}
if cli.atomic {
return Err(UsageError("--remove cannot be combined with --atomic".into()).into());
}
Expand All @@ -329,11 +314,8 @@ fn run(cli: &Cli) -> Result<()> {

// ── --insert mode validation (P05) ──
if cli.insert {
if cli.list_sections {
return Err(
UsageError("--insert cannot be combined with --list-sections".into()).into(),
);
}
// Mutual exclusion with --list-sections (and other modes) is enforced by
// the `operation` arg-group in cli.rs.
if cli.force.is_some() {
return Err(UsageError(
"--insert does not take --force (the body is left uncommented)".into(),
Expand All @@ -357,9 +339,8 @@ fn run(cli: &Cli) -> Result<()> {
if cli.lines.len() != 1 {
return Err(UsageError("--insert requires exactly one -l <range>".into()).into());
}
} else if cli.desc.is_some() {
return Err(UsageError("--desc is only valid with --insert".into()).into());
}
// --desc requires --insert: enforced declaratively in cli.rs via clap `requires`.

let opts = ToggleOptions {
force: &effective_force,
Expand Down
12 changes: 8 additions & 4 deletions crates/togl-cli/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ fn test_to_end_without_line_errors() {
.args([path.to_str().unwrap(), "--to-end"])
.assert()
.failure()
.stderr(predicates::str::contains("--to-end requires"));
.stderr(predicates::str::contains(
"required arguments were not provided",
));
}

// ── Multi-line comment support (Phase 2) ──
Expand Down Expand Up @@ -1721,7 +1723,7 @@ fn test_no_backup_without_atomic_errors() {
.assert()
.failure()
.stderr(predicates::str::contains(
"--no-backup is only valid with --atomic",
"required arguments were not provided",
));
}

Expand All @@ -1739,7 +1741,7 @@ fn test_recover_forward_without_recover_errors() {
.assert()
.failure()
.stderr(predicates::str::contains(
"--recover-forward requires --recover",
"required arguments were not provided",
));
}

Expand Down Expand Up @@ -2457,7 +2459,9 @@ fn check_without_scan_errors() {
.args(["--check", dir.path().to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("--check requires --scan"));
.stderr(predicate::str::contains(
"required arguments were not provided",
));
}

#[test]
Expand Down
Loading