Use CommandLine library for executables

This commit is contained in:
Matt Nadareski
2025-10-06 09:32:01 -04:00
parent e029fa4833
commit e4fab52489
7 changed files with 206 additions and 250 deletions

View File

@@ -58,6 +58,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.CommandLine" Version="[1.3.2]" />
<PackageReference Include="SabreTools.IO" Version="[1.7.5]" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.9" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
</ItemGroup>

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace ExtractionTool
@@ -14,96 +13,22 @@ namespace ExtractionTool
/// <summary>
/// Enable debug output for relevant operations
/// </summary>
public bool Debug { get; private set; } = false;
/// <summary>
/// Set of input paths to use for operations
/// </summary>
public List<string> InputPaths { get; private set; } = [];
public bool Debug { get; set; }
/// <summary>
/// Output path for archive extraction
/// </summary>
public string OutputPath { get; private set; } = string.Empty;
public string OutputPath { get; set; } = string.Empty;
#endregion
/// <summary>
/// Parse commandline arguments into an Options object
/// </summary>
public static Options? ParseOptions(string[] args)
{
// If we have invalid arguments
if (args == null || args.Length == 0)
return null;
// Create an Options object
var options = new Options();
// Parse the options and paths
for (int index = 0; index < args.Length; index++)
{
string arg = args[index];
switch (arg)
{
case "-?":
case "-h":
case "--help":
return null;
case "-d":
case "--debug":
options.Debug = true;
break;
case "-o":
case "--outdir":
options.OutputPath = index + 1 < args.Length ? args[++index] : string.Empty;
break;
default:
options.InputPaths.Add(arg);
break;
}
}
// Validate we have any input paths to work on
if (options.InputPaths.Count == 0)
{
Console.WriteLine("At least one path is required!");
return null;
}
// Validate the output path
bool validPath = ValidateExtractionPath(options);
if (!validPath)
return null;
return options;
}
/// <summary>
/// Display help text
/// </summary>
public static void DisplayHelp()
{
Console.WriteLine("Extraction Tool");
Console.WriteLine();
Console.WriteLine("ExtractionTool.exe <options> file|directory ...");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine("-?, -h, --help Display this help text and quit");
Console.WriteLine("-d, --debug Enable debug mode");
Console.WriteLine("-o, --outdir [PATH] Set output path for extraction (required)");
}
/// <summary>
/// Validate the extraction path
/// </summary>
private static bool ValidateExtractionPath(Options options)
public bool ValidateExtractionPath()
{
// Null or empty output path
if (string.IsNullOrEmpty(options.OutputPath))
if (string.IsNullOrEmpty(OutputPath))
{
Console.WriteLine("Output directory required for extraction!");
Console.WriteLine();
@@ -113,8 +38,8 @@ namespace ExtractionTool
// Malformed output path or invalid location
try
{
options.OutputPath = Path.GetFullPath(options.OutputPath);
Directory.CreateDirectory(options.OutputPath);
OutputPath = Path.GetFullPath(OutputPath);
Directory.CreateDirectory(OutputPath);
}
catch
{

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.CommandLine;
using SabreTools.CommandLine.Inputs;
using SabreTools.IO.Extensions;
using SabreTools.Serialization;
using SabreTools.Serialization.Wrappers;
@@ -8,6 +11,14 @@ namespace ExtractionTool
{
class Program
{
#region Constants
private const string _debugName = "debug";
private const string _helpName = "help";
private const string _outputPathName = "output-path";
#endregion
static void Main(string[] args)
{
#if NET462_OR_GREATER || NETCOREAPP
@@ -15,30 +26,85 @@ namespace ExtractionTool
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
#endif
// Get the options from the arguments
var options = Options.ParseOptions(args);
// Create the command set
var commandSet = CreateCommands();
// If we have an invalid state
if (options == null)
// If we have no args, show the help and quit
if (args == null || args.Length == 0)
{
Options.DisplayHelp();
commandSet.OutputAllHelp();
return;
}
// Loop through and process the options
int firstFileIndex = 0;
for (; firstFileIndex < args.Length; firstFileIndex++)
{
string arg = args[firstFileIndex];
var input = commandSet.GetTopLevel(arg);
if (input == null)
break;
input.ProcessInput(args, ref firstFileIndex);
}
// If help was specified
if (commandSet.GetBoolean(_helpName))
{
commandSet.OutputAllHelp();
return;
}
// Get the options from the arguments
var options = new Options
{
Debug = commandSet.GetBoolean(_debugName),
OutputPath = commandSet.GetString(_outputPathName) ?? string.Empty,
};
// Validate the output path
if (!options.ValidateExtractionPath())
{
commandSet.OutputAllHelp();
return;
}
// Loop through the input paths
foreach (string inputPath in options.InputPaths)
for (int i = firstFileIndex; i < args.Length; i++)
{
ExtractPath(inputPath, options.OutputPath, options.Debug);
string arg = args[i];
ExtractPath(arg, options);
}
}
/// <summary>
/// Create the command set for the program
/// </summary>
private static CommandSet CreateCommands()
{
List<string> header = [
"Extraction Tool",
string.Empty,
"ExtractionTool <options> file|directory ...",
string.Empty,
];
var commandSet = new CommandSet(header);
commandSet.Add(new FlagInput(_helpName, ["-?", "-h", "--help"], "Display this help text"));
commandSet.Add(new FlagInput(_debugName, ["-d", "--debug"], "Enable debug mode"));
commandSet.Add(new StringInput(_outputPathName, ["-o", "--outdir"], "Set output path for extraction (required)"));
return commandSet;
}
/// <summary>
/// Wrapper to extract data for a single path
/// </summary>
/// <param name="path">File or directory path</param>
/// <param name="outputDirectory">Output directory path</param>
/// <param name="includeDebug">Enable including debug information</param>
private static void ExtractPath(string path, string outputDirectory, bool includeDebug)
/// <param name="options">User-defined options</param>
private static void ExtractPath(string path, Options options)
{
// Normalize by getting the full path
path = Path.GetFullPath(path);
@@ -47,13 +113,13 @@ namespace ExtractionTool
// Check if the file or directory exists
if (File.Exists(path))
{
ExtractFile(path, outputDirectory, includeDebug);
ExtractFile(path, options);
}
else if (Directory.Exists(path))
{
foreach (string file in IOExtensions.SafeEnumerateFiles(path, "*", SearchOption.AllDirectories))
{
ExtractFile(file, outputDirectory, includeDebug);
ExtractFile(file, options);
}
}
else
@@ -65,7 +131,9 @@ namespace ExtractionTool
/// <summary>
/// Print information for a single file, if possible
/// </summary>
private static void ExtractFile(string file, string outputDirectory, bool includeDebug)
/// <param name="path">File path</param>
/// <param name="options">User-defined options</param>
private static void ExtractFile(string file, Options options)
{
Console.WriteLine($"Attempting to extract all files from {file}");
using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
@@ -82,7 +150,7 @@ namespace ExtractionTool
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
if (options.Debug) Console.Error.WriteLine(ex);
return;
}
@@ -91,7 +159,7 @@ namespace ExtractionTool
var wrapper = WrapperFactory.CreateWrapper(ft, stream);
// Create the output directory
Directory.CreateDirectory(outputDirectory);
Directory.CreateDirectory(options.OutputPath);
// Print the preamble
Console.WriteLine($"Attempting to extract from '{wrapper?.Description() ?? "UNKNOWN"}'");
@@ -101,142 +169,142 @@ namespace ExtractionTool
{
// 7-zip
case SevenZip sz:
sz.Extract(outputDirectory, includeDebug);
sz.Extract(options.OutputPath, options.Debug);
break;
// BFPK archive
case BFPK bfpk:
bfpk.Extract(outputDirectory, includeDebug);
bfpk.Extract(options.OutputPath, options.Debug);
break;
// BSP
case BSP bsp:
bsp.Extract(outputDirectory, includeDebug);
bsp.Extract(options.OutputPath, options.Debug);
break;
// bzip2
case BZip2 bzip2:
bzip2.Extract(outputDirectory, includeDebug);
bzip2.Extract(options.OutputPath, options.Debug);
break;
// CFB
case CFB cfb:
cfb.Extract(outputDirectory, includeDebug);
cfb.Extract(options.OutputPath, options.Debug);
break;
// GCF
case GCF gcf:
gcf.Extract(outputDirectory, includeDebug);
gcf.Extract(options.OutputPath, options.Debug);
break;
// gzip
case GZip gzip:
gzip.Extract(outputDirectory, includeDebug);
gzip.Extract(options.OutputPath, options.Debug);
break;
// InstallShield Archive V3 (Z)
case InstallShieldArchiveV3 isv3:
isv3.Extract(outputDirectory, includeDebug);
isv3.Extract(options.OutputPath, options.Debug);
break;
// IS-CAB archive
case InstallShieldCabinet iscab:
iscab.Extract(outputDirectory, includeDebug);
iscab.Extract(options.OutputPath, options.Debug);
break;
// LZ-compressed file, KWAJ variant
case LZKWAJ kwaj:
kwaj.Extract(outputDirectory, includeDebug);
kwaj.Extract(options.OutputPath, options.Debug);
break;
// LZ-compressed file, QBasic variant
case LZQBasic qbasic:
qbasic.Extract(outputDirectory, includeDebug);
qbasic.Extract(options.OutputPath, options.Debug);
break;
// LZ-compressed file, SZDD variant
case LZSZDD szdd:
szdd.Extract(outputDirectory, includeDebug);
szdd.Extract(options.OutputPath, options.Debug);
break;
// Microsoft Cabinet archive
case MicrosoftCabinet mscab:
mscab.Extract(outputDirectory, includeDebug);
mscab.Extract(options.OutputPath, options.Debug);
break;
// MoPaQ (MPQ) archive
case MoPaQ mpq:
mpq.Extract(outputDirectory, includeDebug);
mpq.Extract(options.OutputPath, options.Debug);
break;
// New Executable
case NewExecutable nex:
nex.Extract(outputDirectory, includeDebug);
nex.Extract(options.OutputPath, options.Debug);
break;
// PAK
case PAK pak:
pak.Extract(outputDirectory, includeDebug);
pak.Extract(options.OutputPath, options.Debug);
break;
// PFF
case PFF pff:
pff.Extract(outputDirectory, includeDebug);
pff.Extract(options.OutputPath, options.Debug);
break;
// PKZIP
case PKZIP pkzip:
pkzip.Extract(outputDirectory, includeDebug);
pkzip.Extract(options.OutputPath, options.Debug);
break;
// Portable Executable
case PortableExecutable pex:
pex.Extract(outputDirectory, includeDebug);
pex.Extract(options.OutputPath, options.Debug);
break;
// Quantum
case Quantum quantum:
quantum.Extract(outputDirectory, includeDebug);
quantum.Extract(options.OutputPath, options.Debug);
break;
// RAR
case RAR rar:
rar.Extract(outputDirectory, includeDebug);
rar.Extract(options.OutputPath, options.Debug);
break;
// SGA
case SGA sga:
sga.Extract(outputDirectory, includeDebug);
sga.Extract(options.OutputPath, options.Debug);
break;
// Tape Archive
case TapeArchive tar:
tar.Extract(outputDirectory, includeDebug);
tar.Extract(options.OutputPath, options.Debug);
break;
// VBSP
case VBSP vbsp:
vbsp.Extract(outputDirectory, includeDebug);
vbsp.Extract(options.OutputPath, options.Debug);
break;
// VPK
case VPK vpk:
vpk.Extract(outputDirectory, includeDebug);
vpk.Extract(options.OutputPath, options.Debug);
break;
// WAD3
case WAD3 wad:
wad.Extract(outputDirectory, includeDebug);
wad.Extract(options.OutputPath, options.Debug);
break;
// xz
case XZ xz:
xz.Extract(outputDirectory, includeDebug);
xz.Extract(options.OutputPath, options.Debug);
break;
// XZP
case XZP xzp:
xzp.Extract(outputDirectory, includeDebug);
xzp.Extract(options.OutputPath, options.Debug);
break;
// Everything else

View File

@@ -32,6 +32,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.CommandLine" Version="[1.3.2]" />
<PackageReference Include="SabreTools.IO" Version="[1.7.5]" />
<PackageReference Include="SabreTools.Hashing" Version="[1.5.0]" />
</ItemGroup>

View File

@@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;
namespace InfoPrint
{
/// <summary>
@@ -8,134 +5,26 @@ namespace InfoPrint
/// </summary>
internal sealed class Options
{
#region Properties
/// <summary>
/// Enable debug output for relevant operations
/// </summary>
public bool Debug { get; private set; } = false;
public bool Debug { get; set; }
/// <summary>
/// Output information to file only, skip printing to console
/// </summary>
public bool FileOnly { get; private set; } = false;
public bool FileOnly { get; set; }
/// <summary>
/// Print external file hashes
/// </summary>
public bool Hash { get; private set; } = false;
/// <summary>
/// Set of input paths to use for operations
/// </summary>
public List<string> InputPaths { get; private set; } = [];
public bool Hash { get; set; }
#if NETCOREAPP
/// <summary>
/// Enable JSON output
/// </summary>
public bool Json { get; private set; } = false;
public bool Json { get; set; }
#endif
#endregion
/// <summary>
/// Parse commandline arguments into an Options object
/// </summary>
public static Options? ParseOptions(string[] args)
{
// If we have invalid arguments
if (args == null || args.Length == 0)
return null;
// Create an Options object
var options = new Options();
// Parse the features
int index = 0;
for (; index < args.Length; index++)
{
string arg = args[index];
bool featureFound = false;
switch (arg)
{
case "-?":
case "-h":
case "--help":
return null;
default:
break;
}
// If the flag wasn't a feature
if (!featureFound)
break;
}
// Parse the options and paths
for (; index < args.Length; index++)
{
string arg = args[index];
switch (arg)
{
case "-d":
case "--debug":
options.Debug = true;
break;
case "-c":
case "--hash":
options.Hash = true;
break;
case "-f":
case "--file":
options.FileOnly = true;
break;
case "-j":
case "--json":
#if NETCOREAPP
options.Json = true;
#else
Console.WriteLine("JSON output not available in .NET Framework");
#endif
break;
default:
options.InputPaths.Add(arg);
break;
}
}
// Validate we have any input paths to work on
if (options.InputPaths.Count == 0)
{
Console.WriteLine("At least one path is required!");
return null;
}
return options;
}
/// <summary>
/// Display help text
/// </summary>
public static void DisplayHelp()
{
Console.WriteLine("Information Printing Program");
Console.WriteLine();
Console.WriteLine("InfoPrint <options> file|directory ...");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine("-?, -h, --help Display this help text and quit");
Console.WriteLine("-d, --debug Enable debug mode");
Console.WriteLine("-c, --hash Output file hashes");
Console.WriteLine("-f, --file Print to file only");
#if NETCOREAPP
Console.WriteLine("-j, --json Print info as JSON");
#endif
}
}
}

View File

@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.CommandLine;
using SabreTools.CommandLine.Inputs;
using SabreTools.Hashing;
using SabreTools.IO.Extensions;
using SabreTools.Serialization;
@@ -10,23 +13,92 @@ namespace InfoPrint
{
public static class Program
{
#region Constants
private const string _debugName = "debug";
private const string _fileOnlyName = "file-only";
private const string _hashName = "hash";
private const string _helpName = "help";
#if NETCOREAPP
private const string _jsonName = "json";
#endif
#endregion
public static void Main(string[] args)
{
// Get the options from the arguments
var options = Options.ParseOptions(args);
// Create the command set
var commandSet = CreateCommands();
// If we have an invalid state
if (options == null)
// If we have no args, show the help and quit
if (args == null || args.Length == 0)
{
Options.DisplayHelp();
commandSet.OutputAllHelp();
return;
}
// Loop through the input paths
foreach (string inputPath in options.InputPaths)
// Loop through and process the options
int firstFileIndex = 0;
for (; firstFileIndex < args.Length; firstFileIndex++)
{
PrintPathInfo(inputPath, options);
string arg = args[firstFileIndex];
var input = commandSet.GetTopLevel(arg);
if (input == null)
break;
input.ProcessInput(args, ref firstFileIndex);
}
// If help was specified
if (commandSet.GetBoolean(_helpName))
{
commandSet.OutputAllHelp();
return;
}
// Get the options from the arguments
var options = new Options
{
Debug = commandSet.GetBoolean(_debugName),
Hash = commandSet.GetBoolean(_hashName),
FileOnly = commandSet.GetBoolean(_fileOnlyName),
#if NETCOREAPP
Json = commandSet.GetBoolean(_jsonName),
#endif
};
// Loop through the input paths
for (int i = firstFileIndex; i < args.Length; i++)
{
string arg = args[i];
PrintPathInfo(arg, options);
}
}
/// <summary>
/// Create the command set for the program
/// </summary>
private static CommandSet CreateCommands()
{
List<string> header = [
"Information Printing Program",
string.Empty,
"InfoPrint <options> file|directory ...",
string.Empty,
];
var commandSet = new CommandSet(header);
commandSet.Add(new FlagInput(_helpName, ["-?", "-h", "--help"], "Display this help text"));
commandSet.Add(new FlagInput(_debugName, ["-d", "--debug"], "Enable debug mode"));
commandSet.Add(new FlagInput(_hashName, ["-c", "--hash"], "Output file hashes"));
commandSet.Add(new FlagInput(_fileOnlyName, ["-f", "--file"], "Print to file only"));
#if NETCOREAPP
commandSet.Add(new FlagInput(_jsonName, ["-j", "--json"], "Print info as JSON"));
#endif
return commandSet;
}
/// <summary>

View File

@@ -30,7 +30,7 @@ For the latest WIP build here: [Rolling Release](https://github.com/SabreTools/S
InfoPrint <options> file|directory ...
Options:
-?, -h, --help Display this help text and quit
-?, -h, --help Display this help text
-d, --debug Enable debug mode
-c, --hash Output file hashes
-f, --file Print to file only
@@ -42,10 +42,10 @@ Options:
**ExtractionTool** is a reference implementation for the extraction features of the library, packaged as a standalone executable for all supported platforms. It will attempt to detect and extract many supported file types. See the table below for supported extraction functionality.
```text
ExtractionTool.exe <options> file|directory ...
ExtractionTool <options> file|directory ...
Options:
-?, -h, --help Display this help text and quit
-?, -h, --help Display this help text
-d, --debug Enable debug mode
-o, --outdir [PATH] Set output path for extraction (required)
```