From c9d0aa773fa1fb849e0c48eea829817a92249f55 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 18:24:31 +0000 Subject: [PATCH] Scan the current directory for an explicit -p . instead of prompting [patch] "." was both the default for --path and the marker that made each verb ask for one, so -p . prompted and read the next stdin line as the path. With input piped in (echo y | Deduplicate -p .) the confirmation became the directory. PathString is now null when the option is left out, verbs prompt only when it is null or blank, and the run resets it to null. Fixes ktsu-dev/FileDeduplicator#145 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U5RBnMaTjZdMDRNttjeZ3u --- FileDeduplicator.Test/ExplicitPathTests.cs | 123 +++++++++++++++++++++ FileDeduplicator/Verbs/BaseVerb.cs | 11 +- FileDeduplicator/Verbs/Deduplicate.cs | 2 +- FileDeduplicator/Verbs/DryRun.cs | 2 +- FileDeduplicator/Verbs/Scan.cs | 2 +- FileDeduplicator/Verbs/Stats.cs | 2 +- 6 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 FileDeduplicator.Test/ExplicitPathTests.cs diff --git a/FileDeduplicator.Test/ExplicitPathTests.cs b/FileDeduplicator.Test/ExplicitPathTests.cs new file mode 100644 index 0000000..d705f90 --- /dev/null +++ b/FileDeduplicator.Test/ExplicitPathTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.FileDeduplicator.Test; + +using CommandLine; + +using ktsu.FileDeduplicator.Verbs; +using ktsu.Semantics.Paths; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests that an explicit -p . means the current directory rather than "no path given". +/// +/// +/// "." used to be both the option's default and the marker that made a verb prompt, so +/// -p . prompted anyway and read the next stdin line as the path. With input piped in, as in +/// echo y | Deduplicate -p ., the y meant to confirm the deletion became the +/// directory to work on (ktsu-dev/FileDeduplicator#145). +/// +[TestClass] +[DoNotParallelize] +public sealed class ExplicitPathTests +{ + /// + /// Parses a command line the way Program.Main does and returns the verb it selects. + /// + /// The command-line arguments. + /// The parsed verb. + private static BaseVerb Parse(params string[] args) + { + BaseVerb? verb = null; + using Parser parser = new(); + _ = parser.ParseArguments(args, Program.Verbs).WithParsed(parsed => verb = parsed); + Assert.IsNotNull(verb, $"'{string.Join(' ', args)}' did not parse."); + return verb; + } + + /// + /// Runs a verb parsed from with as the + /// current directory. + /// + /// The directory to run in. + /// Lines for the verb's prompts. + /// The command-line arguments. + /// Everything the verb wrote to the console. + private static string RunIn(AbsoluteDirectoryPath directory, string stdin, params string[] args) + { + string original = Environment.CurrentDirectory; + try + { + Environment.CurrentDirectory = directory.WeakString; + return ConsoleCapture.Run(Parse(args), stdin); + } + finally + { + Environment.CurrentDirectory = original; + } + } + + [TestMethod] + public void ScanWithAnExplicitDotScansTheCurrentDirectoryWithoutPrompting() + { + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + _ = tree.Write("aa.txt", "alpha"); + + string output = RunIn(tree.Root, "elsewhere\n", "Scan", "-p", "."); + + Assert.DoesNotContain("Enter the path", output, "An explicit -p . was treated as no path."); + Assert.Contains("Found 1 group(s) of duplicate files", output); + } + + [TestMethod] + public void DeduplicateWithAnExplicitDotReadsTheNextLineAsTheConfirmation() + { + using TempTree tree = new(); + AbsoluteFilePath keeper = tree.Write("a.txt", "alpha"); + AbsoluteFilePath copy = tree.Write("aa.txt", "alpha"); + + string output = RunIn(tree.Root, "y\n", "Deduplicate", "--path", "."); + + Assert.DoesNotContain("Enter the path", output, "An explicit --path . was treated as no path."); + Assert.IsTrue(TempTree.Exists(keeper), "The kept copy was deleted."); + Assert.IsFalse(TempTree.Exists(copy), "The 'y' was not read as the confirmation."); + } + + [TestMethod] + public void DryRunWithAnExplicitDotDoesNotPrompt() + { + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + + string output = RunIn(tree.Root, "elsewhere\n", "DryRun", "-p", "."); + + Assert.DoesNotContain("Enter the path", output); + Assert.Contains("Found 1 file(s).", output); + } + + [TestMethod] + public void StatsWithAnExplicitDotDoesNotPrompt() + { + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + + string output = RunIn(tree.Root, "elsewhere\n", "Stats", "-p", "."); + + Assert.DoesNotContain("Enter the path", output); + Assert.Contains("Total files: 1", output); + } + + [TestMethod] + public void LeavingOutThePathStillPrompts() + { + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + + string output = RunIn(tree.Root, $"{tree.Root.WeakString}\n", "Scan"); + + Assert.Contains("Enter the path to scan:", output); + Assert.Contains("Found 1 file(s).", output); + } +} diff --git a/FileDeduplicator/Verbs/BaseVerb.cs b/FileDeduplicator/Verbs/BaseVerb.cs index 34c0bcf..8017ecf 100644 --- a/FileDeduplicator/Verbs/BaseVerb.cs +++ b/FileDeduplicator/Verbs/BaseVerb.cs @@ -11,12 +11,17 @@ namespace ktsu.FileDeduplicator.Verbs; internal abstract class BaseVerb : ICommand { + /// + /// Gets or sets the path given with -p, or when none was given and the + /// verb should ask for one. An explicit -p . means the current directory, so "." + /// cannot double as the "not supplied" marker. + /// [Option('p', "path", Required = false, HelpText = "The root path to scan for files.")] - public string PathString { get; set; } = "."; + public string? PathString { get; set; } public abstract bool IsActive { get; } - internal AbsoluteDirectoryPath Path => System.IO.Path.GetFullPath(PathString).As(); + internal AbsoluteDirectoryPath Path => System.IO.Path.GetFullPath(PathString ?? ".").As(); public abstract void Run(); @@ -48,7 +53,7 @@ public override void Run() finally { isActive = true; - PathString = "."; + PathString = null; } } diff --git a/FileDeduplicator/Verbs/Deduplicate.cs b/FileDeduplicator/Verbs/Deduplicate.cs index e53eac2..96efc49 100644 --- a/FileDeduplicator/Verbs/Deduplicate.cs +++ b/FileDeduplicator/Verbs/Deduplicate.cs @@ -13,7 +13,7 @@ internal sealed class Deduplicate : BaseVerb { internal override bool ValidateArgs() { - if (PathString is "." or "") + if (string.IsNullOrWhiteSpace(PathString)) { Console.Write("Enter the path to deduplicate: "); string? input = Console.ReadLine()?.Trim(); diff --git a/FileDeduplicator/Verbs/DryRun.cs b/FileDeduplicator/Verbs/DryRun.cs index 3b5d70f..6bdd9b0 100644 --- a/FileDeduplicator/Verbs/DryRun.cs +++ b/FileDeduplicator/Verbs/DryRun.cs @@ -13,7 +13,7 @@ internal sealed class DryRun : BaseVerb { internal override bool ValidateArgs() { - if (PathString is "." or "") + if (string.IsNullOrWhiteSpace(PathString)) { Console.Write("Enter the path to scan: "); string? input = Console.ReadLine()?.Trim(); diff --git a/FileDeduplicator/Verbs/Scan.cs b/FileDeduplicator/Verbs/Scan.cs index a519d91..a2df2b3 100644 --- a/FileDeduplicator/Verbs/Scan.cs +++ b/FileDeduplicator/Verbs/Scan.cs @@ -13,7 +13,7 @@ internal sealed class Scan : BaseVerb { internal override bool ValidateArgs() { - if (PathString is "." or "") + if (string.IsNullOrWhiteSpace(PathString)) { Console.Write("Enter the path to scan: "); string? input = Console.ReadLine()?.Trim(); diff --git a/FileDeduplicator/Verbs/Stats.cs b/FileDeduplicator/Verbs/Stats.cs index 097a25b..45d1acb 100644 --- a/FileDeduplicator/Verbs/Stats.cs +++ b/FileDeduplicator/Verbs/Stats.cs @@ -14,7 +14,7 @@ internal sealed class Stats : BaseVerb { internal override bool ValidateArgs() { - if (PathString is "." or "") + if (string.IsNullOrWhiteSpace(PathString)) { Console.Write("Enter the path to analyze: "); string? input = Console.ReadLine()?.Trim();