diff --git a/SabreTools.Library/DatFiles/DatFile.cs b/SabreTools.Library/DatFiles/DatFile.cs
index 3c1394cf..8819ca36 100644
--- a/SabreTools.Library/DatFiles/DatFile.cs
+++ b/SabreTools.Library/DatFiles/DatFile.cs
@@ -1932,7 +1932,7 @@ namespace SabreTools.Library.DatFiles
/// True if original extension should be kept, false otherwise (default)
public void Parse(string filename, int indexId = 0, bool keep = false, bool keepext = false)
{
- ParentablePath path = new ParentablePath(filename);
+ ParentablePath path = new ParentablePath(filename.Trim('"'));
Parse(path, indexId, keep, keepext);
}
diff --git a/SabreTools.Library/IO/DirectoryExtensions.cs b/SabreTools.Library/IO/DirectoryExtensions.cs
index a72d7d07..661de6d1 100644
--- a/SabreTools.Library/IO/DirectoryExtensions.cs
+++ b/SabreTools.Library/IO/DirectoryExtensions.cs
@@ -165,7 +165,7 @@ namespace SabreTools.Library.IO
List outputs = new List();
for (int i = 0; i < inputs.Count; i++)
{
- string input = inputs[i];
+ string input = inputs[i].Trim('"');
// If we have a null or empty path
if (string.IsNullOrEmpty(input))
diff --git a/SabreTools.Library/README.1ST b/SabreTools.Library/README.1ST
index 26193faa..45fb492b 100644
--- a/SabreTools.Library/README.1ST
+++ b/SabreTools.Library/README.1ST
@@ -150,6 +150,14 @@ Formerly included within this tool is a former standalone executable:
- OfflineMerge: Use merged DATs to create DATs used for managing offline
arrays
+For any command below that includes a `field` of any sort, the name are standardized:
+- If the field is in the header (such as filename, author, date),
+ the format will be `header.name`
+- If the field is in the game/machine (such as game name, publisher, manufacturer),
+ the format will be `game.name`
+- If the field is in the dat items (such as CRC, size, or optional flag),
+ the format will be `item.name`
+
Usage:
SabreTools.exe [options] [filename|dirname] ...
@@ -166,6 +174,30 @@ Options:
This flag allows the user to skip clearing the screen on run just like if
the console was being redirected.
+ -bt, --batch Enable batch mode
+ Run a special mode that takes input files as lists of batch commands to
+ run sequentially. Each command has to be its own line and must be followed
+ by a semicolon (`;`). Commented lines may start with either `REM` or `#`.
+ Multiple batch files are allowed but they will be run independently from
+ each other.
+
+ The following commands are currently implemented:
+
+ Set a header field (if default): set(header.field, value);
+ Parse new file(s): input(datpath, ...);
+ Filter on a field and value: filter(machine.field|item.field, value, [negate = false]);
+ Apply a MAME Extra INI for a field: extra(field, inipath);
+ Perform a split/merge: merge(split|merged|nonmerged|full|device);
+ Set game names from description: descname();
+ Run 1G1R on the items: 1g1r(region, ...);
+ Split into one rom per game: orpg();
+ Remove fields from games/items: remove(machine.field|item.field, ...);
+ Remove scene dates from names: sds();
+ Add new output format(s): format(datformat, ...);
+ Set the output directory: output(outdir);
+ Write the internal items: write([overwrite = true]);
+ Reset the internal state: reset();";
+
-d, --d2d, --dfd Create DAT(s) from an input directory
Create a DAT file from an input directory or set of files. By default,
this will output a DAT named based on the input directory and the current
diff --git a/SabreTools/Features/Batch.cs b/SabreTools/Features/Batch.cs
new file mode 100644
index 00000000..80f5981b
--- /dev/null
+++ b/SabreTools/Features/Batch.cs
@@ -0,0 +1,398 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+using SabreTools.Library.Data;
+using SabreTools.Library.DatFiles;
+using SabreTools.Library.DatItems;
+using SabreTools.Library.Filtering;
+using SabreTools.Library.IO;
+using SabreTools.Library.Help;
+using SabreTools.Library.Tools;
+
+namespace SabreTools.Features
+{
+ internal class Batch : BaseFeature
+ {
+ public const string Value = "Batch";
+
+ public Batch()
+ {
+ Name = Value;
+ Flags = new List() { "-bt", "--batch" };
+ Description = "Enable batch mode";
+ _featureType = FeatureType.Flag;
+ LongDescription = @"Run a special mode that takes input files as lists of batch commands to run sequentially. Each command has to be its own line and must be followed by a semicolon (`;`). Commented lines may start with either `REM` or `#`. Multiple batch files are allowed but they will be run independently from each other.
+
+The following commands are currently implemented:
+
+Set a header field (if default): set(header.field, value);
+Parse new file(s): input(datpath, ...);
+Filter on a field and value: filter(machine.field|item.field, value, [negate = false]);
+Apply a MAME Extra INI for a field: extra(field, inipath);
+Perform a split/merge: merge(split|merged|nonmerged|full|device);
+Set game names from description: descname();
+Run 1G1R on the items: 1g1r(region, ...);
+Split into one rom per game: orpg();
+Remove fields from games/items: remove(machine.field|item.field, ...);
+Remove scene dates from names: sds();
+Add new output format(s): format(datformat, ...);
+Set the output directory: output(outdir);
+Write the internal items: write([overwrite = true]);
+Reset the internal state: reset();";
+ Features = new Dictionary();
+ }
+
+ public override void ProcessFeatures(Dictionary features)
+ {
+ base.ProcessFeatures(features);
+
+ // Try to read each input as a batch run file
+ foreach (string path in Inputs)
+ {
+ // If the file doesn't exist, warn but continue
+ if (!File.Exists(path))
+ {
+ Globals.Logger.User($"{path} does not exist. Skipping...");
+ continue;
+ }
+
+ // Try to process the file now
+ try
+ {
+ // Every line is its own command
+ string[] lines = File.ReadAllLines(path);
+
+ // Each batch file has its own state
+ int index = 0;
+ DatFile datFile = DatFile.Create();
+ string outputDirectory = null;
+
+ // Process each command line
+ foreach (string line in lines)
+ {
+ // Skip empty lines
+ if (string.IsNullOrWhiteSpace(line))
+ continue;
+
+ // Skip lines that start with REM or #
+ if (line.StartsWith("REM") || line.StartsWith("#"))
+ continue;
+
+ // Read the command in, if possible
+ var command = BatchCommand.Create(line);
+ if (command == null)
+ {
+ Globals.Logger.User($"Could not process {path} due to the following line: {line}");
+ break;
+ }
+
+ // Now switch on the command
+ Globals.Logger.User($"Attempting to invoke {command.Name} with {(command.Arguments.Count == 0 ? "no arguments" : "the following argument(s): " + string.Join(", ", command.Arguments))}");
+ switch (command.Name.ToLowerInvariant())
+ {
+ // Set a header field
+ case "set":
+ if (command.Arguments.Count != 2)
+ {
+ Globals.Logger.User($"Invoked {command.Name} but no arguments were provided");
+ Globals.Logger.User("Usage: set(header.field, value);");
+ continue;
+ }
+
+ // Read in the individual arguments
+ Field field = command.Arguments[0].AsField();
+ string value = command.Arguments[1];
+
+ // Set the header field
+ datFile.Header.SetFields(new Dictionary { [field] = value });
+
+ break;
+
+ // Parse in new input file(s)
+ case "input":
+ if (command.Arguments.Count == 0)
+ {
+ Globals.Logger.User($"Invoked {command.Name} but no arguments were provided");
+ Globals.Logger.User("Usage: input(datpath, ...);");
+ continue;
+ }
+
+ // Get only files from inputs
+ List onlyFiles = DirectoryExtensions.GetFilesOnly(command.Arguments);
+
+ // Assume there could be multiple
+ foreach (ParentablePath input in onlyFiles)
+ {
+ datFile.Parse(input, index++);
+ }
+
+ break;
+
+ // Apply a filter
+ case "filter":
+ if (command.Arguments.Count < 2 || command.Arguments.Count > 3)
+ {
+ Globals.Logger.User($"Invoked {command.Name} and expected between 2-3 arguments, but {command.Arguments.Count} arguments were provided");
+ Globals.Logger.User("Usage: filter(field, value, [negate = false]);");
+ continue;
+ }
+
+ // Read in the individual arguments
+ Field filterField = command.Arguments[0].AsField();
+ string filterValue = command.Arguments[1];
+ bool filterNegate = (command.Arguments.Count == 3 ? command.Arguments[2].AsYesNo() ?? false : false);
+
+ // Create a filter with this new set of fields
+ Filter filter = new Filter();
+ filter.SetFilter(filterField, filterValue, filterNegate);
+
+ // Apply the filter blindly
+ datFile.ApplyFilter(filter, false);
+
+ break;
+
+ // Apply an extra INI
+ case "extra":
+ if (command.Arguments.Count != 2)
+ {
+ Globals.Logger.User($"Invoked {command.Name} and expected 2 arguments, but {command.Arguments.Count} arguments were provided");
+ Globals.Logger.User("Usage: extra(field, inipath);");
+ continue;
+ }
+
+ // Read in the individual arguments
+ Field extraField = command.Arguments[0].AsField();
+ string extraFile = command.Arguments[1];
+
+ // Create the extra INI
+ ExtraIni extraIni = new ExtraIni();
+ ExtraIniItem extraIniItem = new ExtraIniItem();
+ extraIniItem.PopulateFromFile(extraFile);
+ extraIniItem.Field = extraField;
+ extraIni.Items.Add(extraIniItem);
+
+ // Apply the extra INI blindly
+ datFile.ApplyExtras(extraIni);
+
+ break;
+
+ // Apply internal split/merge
+ case "merge":
+ if (command.Arguments.Count != 1)
+ {
+ Globals.Logger.User($"Invoked {command.Name} and expected 1 argument, but {command.Arguments.Count} arguments were provided");
+ Globals.Logger.User("Usage: merge(split|merged|nonmerged|full|device);");
+ continue;
+ }
+
+ // Read in the individual arguments
+ MergingFlag mergingFlag = command.Arguments[0].AsMergingFlag();
+
+ // Apply the merging flag
+ datFile.ProcessSplitType(mergingFlag);
+
+ break;
+
+ // Apply description-as-name logic
+ case "descname":
+ if (command.Arguments.Count != 0)
+ {
+ Globals.Logger.User($"Invoked {command.Name} and expected no arguments, but {command.Arguments.Count} arguments were provided");
+ Globals.Logger.User("Usage: descname();");
+ continue;
+ }
+
+ // Apply the logic
+ datFile.MachineDescriptionToName();
+
+ break;
+
+ // Apply 1G1R
+ case "1g1r":
+ if (command.Arguments.Count == 0)
+ {
+ Globals.Logger.User($"Invoked {command.Name} but no arguments were provided");
+ Globals.Logger.User("Usage: 1g1r(region, ...);");
+ continue;
+ }
+
+ // Set the region list
+ var tempRegionList = datFile.Header.RegionList;
+ datFile.Header.RegionList = command.Arguments;
+
+ // Run the 1G1R functionality
+ datFile.OneGamePerRegion();
+
+ // Reset the header value
+ datFile.Header.RegionList = tempRegionList;
+
+ break;
+
+ // Apply one rom per game (ORPG)
+ case "orpg":
+ if (command.Arguments.Count != 0)
+ {
+ Globals.Logger.User($"Invoked {command.Name} and expected no arguments, but {command.Arguments.Count} arguments were provided");
+ Globals.Logger.User("Usage: orpg();");
+ continue;
+ }
+
+ // Apply the logic
+ datFile.OneRomPerGame();
+
+ break;
+
+ // Remove a field
+ case "remove":
+ if (command.Arguments.Count == 0)
+ {
+ Globals.Logger.User($"Invoked {command.Name} but no arguments were provided");
+ Globals.Logger.User("Usage: remove(field, ...);");
+ continue;
+ }
+
+ // Set the field list
+ var tempRemoveFields = datFile.Header.ExcludeFields;
+ datFile.Header.ExcludeFields = command.Arguments.Select(s => s.AsField()).ToList();
+
+ // Run the removal functionality
+ datFile.RemoveFieldsFromItems();
+
+ // Reset the header value
+ datFile.Header.ExcludeFields = tempRemoveFields;
+
+ break;
+
+ // Apply scene date stripping
+ case "sds":
+ if (command.Arguments.Count != 0)
+ {
+ Globals.Logger.User($"Invoked {command.Name} and expected no arguments, but {command.Arguments.Count} arguments were provided");
+ Globals.Logger.User("Usage: sds();");
+ continue;
+ }
+
+ // Apply the logic
+ datFile.StripSceneDatesFromItems();
+
+ break;
+
+ // Set new output format(s)
+ case "format":
+ if (command.Arguments.Count == 0)
+ {
+ Globals.Logger.User($"Invoked {command.Name} but no arguments were provided");
+ Globals.Logger.User("Usage: format(datformat, ...);");
+ continue;
+ }
+
+ // Assume there could be multiple
+ datFile.Header.DatFormat = 0x00;
+ foreach (string format in command.Arguments)
+ {
+ datFile.Header.DatFormat |= format.AsDatFormat();
+ }
+
+ break;
+
+ // Set output directory
+ case "output":
+ if (command.Arguments.Count != 1)
+ {
+ Globals.Logger.User($"Invoked {command.Name} and expected exactly 1 argument, but {command.Arguments.Count} arguments were provided");
+ Globals.Logger.User("Usage: output(outdir);");
+ continue;
+ }
+
+ // Only set the first as the output directory
+ outputDirectory = command.Arguments[0];
+ break;
+
+ // Write out the current DatFile
+ case "write":
+ if (command.Arguments.Count > 1)
+ {
+ Globals.Logger.User($"Invoked {command.Name} and expected 0-1 arguments, but {command.Arguments.Count} arguments were provided");
+ Globals.Logger.User("Usage: write([overwrite = true]);");
+ continue;
+ }
+
+ // Get overwrite value, if possible
+ bool overwrite = command.Arguments.Count == 1 ? command.Arguments[0].AsYesNo() ?? true : true;
+
+ // Write out the dat with the current state
+ datFile.Write(outputDirectory, overwrite: overwrite);
+ break;
+
+ // Reset the internal state
+ case "reset":
+ if (command.Arguments.Count != 0)
+ {
+ Globals.Logger.User($"Invoked {command.Name} and expected no arguments, but {command.Arguments.Count} arguments were provided");
+ Globals.Logger.User("Usage: reset();");
+ continue;
+ }
+
+ // Reset all state variables
+ index = 0;
+ datFile = DatFile.Create();
+ outputDirectory = null;
+ break;
+
+ default:
+ Globals.Logger.User($"Could not find a match for '{command.Name}'. Please see the help text for more details.");
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Globals.Logger.Error($"There was an exception processing {path}: {ex}");
+ continue;
+ }
+ }
+ }
+
+ ///
+ /// Internal representation of a single batch command
+ ///
+ private class BatchCommand
+ {
+ public string Name { get; private set; }
+ public List Arguments { get; private set; } = new List();
+
+ ///
+ /// Create a command based on parsing a line
+ ///
+ public static BatchCommand Create(string line)
+ {
+ // Empty lines don't count
+ if (string.IsNullOrEmpty(line))
+ return null;
+
+ // Split into name and arguments
+ string splitRegex = @"^(\S+)\((.*?)\);";
+ var match = Regex.Match(line, splitRegex);
+
+ // If we didn't get a success, just return null
+ if (!match.Success)
+ return null;
+
+ // Otherwise, get the name and arguments
+ string commandName = match.Groups[1].Value;
+ List arguments = match
+ .Groups[2]
+ .Value
+ .Split(',')
+ .Select(s => s.Trim().Trim('"').Trim())
+ .Where(s => !string.IsNullOrWhiteSpace(s)) // TODO: This may interfere with header value replacement
+ .ToList();
+
+ return new BatchCommand { Name = commandName, Arguments = arguments };
+ }
+ }
+ }
+}
diff --git a/SabreTools/Program.cs b/SabreTools/Program.cs
index ca24eca3..634a57dc 100644
--- a/SabreTools/Program.cs
+++ b/SabreTools/Program.cs
@@ -103,6 +103,7 @@ namespace SabreTools
break;
// Require input verification
+ case Batch.Value:
case DatFromDir.Value:
case Extract.Value:
case Restore.Value:
@@ -152,6 +153,7 @@ namespace SabreTools
help.Add(new DisplayHelp());
help.Add(new DisplayHelpDetailed());
help.Add(new Script());
+ help.Add(new Batch());
help.Add(new DatFromDir());
help.Add(new Extract());
help.Add(new Restore());