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();