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
44 changes: 43 additions & 1 deletion FileDeduplicator.Test/ExitCodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private static int ExitCodeOfCommandLine(params string[] args)
_ = ConsoleCapture.Run(
captured =>
{
using Parser parser = new(settings => settings.HelpWriter = captured);
using Parser parser = Program.CreateParser(captured);
return Program.Run(parser, args);
},
string.Empty,
Expand Down Expand Up @@ -122,6 +122,48 @@ public void DeduplicateExitsOneWhenADeletionFails()
public void AnUnknownOptionExitsTwo() =>
Assert.AreEqual(2, ExitCodeOfCommandLine("Scan", "--nope"));

[TestMethod]
public void ALowercaseVerbRunsThatVerb()
{
using TempTree tree = new();
string missing = Path.Combine(tree.Root.WeakString, "not-there");

// Scan of a missing directory exits 1; the Menu it used to fall through to exits 0 at end of input
Assert.AreEqual(1, ExitCodeOfCommandLine("scan", "-p", missing));
Assert.AreEqual(0, ExitCodeOfCommandLine("scan", "-p", tree.Root.WeakString));
}

[TestMethod]
public void AnUnknownVerbExitsTwo()
{
using TempTree tree = new();
Assert.AreEqual(2, ExitCodeOfCommandLine("dedupe", "-p", tree.Root.WeakString));
}

[TestMethod]
public void AStrayPositionalArgumentExitsTwo()
{
using TempTree tree = new();
Assert.AreEqual(2, ExitCodeOfCommandLine("Scan", "-p", tree.Root.WeakString, "extra"));
}

[TestMethod]
public void APathGivenWithoutDashPExitsTwoWithoutPrompting()
{
using TempTree tree = new();
string output = ConsoleCapture.Run(
captured =>
{
using Parser parser = Program.CreateParser(captured);
return Program.Run(parser, ["Deduplicate", tree.Root.WeakString]);
},
"y",
out int exitCode);

Assert.AreEqual(2, exitCode);
Assert.IsFalse(output.Contains("Enter the path", StringComparison.Ordinal), output);
}

[TestMethod]
public void AskingForHelpExitsZero() =>
Assert.AreEqual(0, ExitCodeOfCommandLine("--help"));
Expand Down
18 changes: 17 additions & 1 deletion FileDeduplicator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,25 @@ internal static class Program
private static int Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
return Run(Parser.Default, args);
using Parser parser = CreateParser(Console.Error);
return Run(parser, args);
}

/// <summary>
/// Creates the parser <see cref="Main"/> uses.
/// </summary>
/// <remarks>
/// Verb names match in any case. Otherwise <c>scan -p dir</c> names no verb, falls through to the
/// default Menu verb, and exits 0 without scanning (ktsu-dev/FileDeduplicator#195).
/// </remarks>
/// <param name="helpWriter">Where help and parse errors are written.</param>
/// <returns>The parser.</returns>
internal static Parser CreateParser(TextWriter helpWriter) => new(settings =>
{
settings.CaseSensitive = false;
settings.HelpWriter = helpWriter;
});

/// <summary>
/// Parses <paramref name="args"/> with <paramref name="parser"/> and runs the verb they name.
/// </summary>
Expand Down
20 changes: 19 additions & 1 deletion FileDeduplicator/Verbs/BaseVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,25 @@ internal abstract class BaseVerb : ICommand
[Option('p', "path", Required = false, HelpText = "The root path to scan for files.")]
public string? PathString { get; set; }

/// <summary>
/// Gets or sets the positional arguments on the command line, which no verb takes. The parser would
/// otherwise drop them silently: an unknown verb name lands here on the default Menu verb, and a
/// path given without <c>-p</c> would leave the verb prompting for one.
/// </summary>
[Value(0, Hidden = true)]
public IEnumerable<string> UnexpectedArguments { get; set; } = [];

public abstract bool IsActive { get; }

internal AbsoluteDirectoryPath Path => System.IO.Path.GetFullPath(PathString ?? ".").As<AbsoluteDirectoryPath>();

/// <summary>
/// Runs the verb.
/// </summary>
/// <returns>The process exit code: 0 on success, 1 when the run failed or the user aborted.</returns>
/// <returns>
/// The process exit code: 0 on success, 1 when the run failed or the user aborted, and 2 when the
/// command line had <see cref="UnexpectedArguments"/>.
/// </returns>
public abstract int Run();

/// <summary>
Expand Down Expand Up @@ -59,6 +70,13 @@ public override int Run()
try
{
ExitCode = 0;
if (UnexpectedArguments.Any())
{
string commands = string.Join(", ", Program.Verbs.Where(verb => verb != typeof(Menu)).Select(verb => verb.Name));
Console.WriteLine($"Unexpected argument: {string.Join(' ', UnexpectedArguments)}. Commands: {commands}. Give the path with -p.");
return 2;
}

if (!ValidateArgs() || (ScansADirectory && (!IsRepresentablePath() || !FileScanner.IsScannableDirectory(Path))))
{
return 1;
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ FileDeduplicator Deduplicate -p "C:\path\to\directory"
FileDeduplicator Stats -p "C:\path\to\directory"
```

Command names match in any case, so `scan` runs `Scan`. An unknown command, or any argument other
than an option such as a path given without `-p`, is an error and exits 2 rather than being ignored.

### Options

| Option | Description |
Expand Down
Loading