Add POSIX-friendly CLI handling: --, --help/--version, exit codes

Keep the existing single-dash switch style and case-insensitive matching,
but add the cheap, non-breaking POSIX wins:

- `--` end-of-options: tokens after a bare `--` are treated as literal operands
- `--help`/`-h` (alongside `-?`) and `-V`/`--version`
- Main returns a real exit code: 2 for usage errors, propagates handler
  return codes, and a top-level catch yields a quiet 1 on unhandled errors

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
jim
2026-06-09 15:12:10 -05:00
co-authored by Claude Opus 4.8
parent 8d6b9212c1
commit 03676432bd
+100 -56
View File
@@ -13,12 +13,27 @@ namespace URLNotesGrabberCORE
internal class Program
{
static void Main(string[] args)
static int Main(string[] args)
{
// Reset console color on exit (including Ctrl+C)
Console.CancelKeyPress += (s, e) => Console.ResetColor();
AppDomain.CurrentDomain.ProcessExit += (s, e) => Console.ResetColor();
try
{
return Run(args);
}
catch (Exception ex)
{
// Turn configuration errors / unguarded indexers into a quiet, deterministic exit code.
Console.Error.WriteLine(ex.Message);
return 1;
}
}
static int Run(string[] args)
{
int exitCode = 0;
IConfiguration config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
@@ -33,6 +48,13 @@ namespace URLNotesGrabberCORE
List<string> filteredArgs = new List<string>();
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "--")
{
// POSIX end-of-options: everything after is a literal operand.
for (int j = i + 1; j < args.Length; j++) filteredArgs.Add(args[j]);
break;
}
if (string.Equals(args[i], "-force", StringComparison.OrdinalIgnoreCase) ||
string.Equals(args[i], "--force", StringComparison.OrdinalIgnoreCase))
{
@@ -147,56 +169,16 @@ namespace URLNotesGrabberCORE
switch (args[0])
{
case "-?":
Console.WriteLine("\t Parse .txt files to find blogs");
Console.WriteLine("-?\t Usage help");
Console.WriteLine("-parse\t Parse .txt files with specified blogname");
Console.WriteLine("-test\t Calls API for given blogname and postID");
Console.WriteLine("-posts\t For each Post in DB, write blogname to file");
Console.WriteLine("-blogs\t For each Blog in DB, write blogname to file");
Console.WriteLine("-collect [0|1] [datetime]\t Collect Notes from API. 1=only posts without notes. 0=full re-check of all posts: a single resumable pass (interrupt & relaunch to resume; stops when complete, retrigger for a new pass). Optional datetime overrides the cutoff and runs as a one-off (bypasses resume tracking).");
Console.WriteLine("-blogsR\t For each Note that is a REPLY, write blogname to file ");
Console.WriteLine("-blogsO\t For each Blog in DB, write blogname to file, but limit via a passed start and stop range ");
Console.WriteLine("-replies\t Fetch and update missing reply text for all replies in database");
Console.WriteLine("-likes\t Fetch likes: initial backfill for new blogs, incremental refresh for blogs past cooldown. Optional blog name forces single-blog run.");
Console.WriteLine("-force\t (with -likes) Ignore cooldown and refresh every fully-backfilled blog");
Console.WriteLine("-urldump\t Scan all posts' text columns and extract suspected URLs to configured file");
Console.WriteLine("-api3\t Use TumblrApi3 settings from appsettings.json");
Console.WriteLine("-api4\t Use TumblrApi4 settings from appsettings.json");
Console.WriteLine("-start [blogname]\t Start traversal alphabetically at this blog name");
Console.WriteLine("-api [section]\t Use a specific API settings section from appsettings.json (e.g. TumblrApi3)");
Console.WriteLine("-ingest [blogname]\t Ingest Tumblr .txt exports from appSettings:PathTTRoot into TL.db (all blogs, or single blog if name given)");
Console.WriteLine("-output\t Export posts from TL.db back to .txt files in each blog's TTFolderPath");
Console.WriteLine("-revert [blogname]\t Recursively scan the PathInput tree and restore *.bak back to *.txt (current .txt saved as next-free .bkN); optional blogname filters by path substring");
Console.WriteLine("-correct [bakPath]\t Dry-run: report multi-line field updates available from a BAK directory");
Console.WriteLine("-correct -apply [bakPath]\t Apply BAK-file corrections to matching posts (prompts yes/no)");
Console.WriteLine("-updatepaths [rootPath]\t Read .tumblr/.tmblrpriv metadata from <root>\\Index and set Blogs.TTFolderPath");
Console.WriteLine("-importposts [path-to-posts.db]\t One-time migration: copy legacy ThreeTxtFileHelper posts.db rows into TL.db");
case "-h":
case "--help":
PrintHelp();
break;
case "-V":
case "--version":
Console.WriteLine(System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown");
break;
case "-parse":
string blogNameToParse = args[1];
int postsAdded = 0;
@@ -236,6 +218,7 @@ namespace URLNotesGrabberCORE
if (args.Length < 2)
{
Console.WriteLine("--Expected WITHOUTNOTESONLY (0, 1) [OPTIONAL: BEFOREDATE]--");
exitCode = 2;
break;
}
@@ -268,6 +251,7 @@ namespace URLNotesGrabberCORE
else
{
Console.WriteLine($"ERROR: Invalid date format '{args[2]}'");
exitCode = 2;
break;
}
}
@@ -313,6 +297,7 @@ namespace URLNotesGrabberCORE
else
{
Console.WriteLine("--Expected FROM TO--");
exitCode = 2;
}
WriteBlogsToFile(settings.GetValue<string>("PathOutputBlogs"), false, from, to, top);
break;
@@ -329,6 +314,7 @@ namespace URLNotesGrabberCORE
else
{
Console.WriteLine("--Expected FROM TO--");
exitCode = 2;
}
WriteBlogsToFileAll(settings.GetValue<string>("PathOutputBlogs"), false, from, to, top);
break;
@@ -348,15 +334,15 @@ namespace URLNotesGrabberCORE
break;
case "-ingest":
IngestMode.Run(config, args.Skip(1).ToArray());
exitCode = IngestMode.Run(config, args.Skip(1).ToArray());
break;
case "-output":
OutputMode.Run(config);
exitCode = OutputMode.Run(config);
break;
case "-revert":
RevertMode.Run(config, args.Length > 1 ? args[1] : null);
exitCode = RevertMode.Run(config, args.Length > 1 ? args[1] : null);
break;
case "-correct":
@@ -365,14 +351,14 @@ namespace URLNotesGrabberCORE
var correctArgs = args.Skip(1)
.Where(a => !string.Equals(a, "-apply", StringComparison.OrdinalIgnoreCase) && !string.Equals(a, "--apply", StringComparison.OrdinalIgnoreCase))
.ToArray();
CorrectMode.Run(config, correctArgs, applyChanges);
exitCode = CorrectMode.Run(config, correctArgs, applyChanges);
break;
}
case "-updatepaths":
{
string rootPath = args.Length > 1 ? args[1] : (settings.GetValue<string>("PathTTRoot") ?? settings.GetValue<string>("PathInput") ?? string.Empty);
UpdateBlogPathsRunner.Run(rootPath);
exitCode = UpdateBlogPathsRunner.Run(rootPath);
break;
}
@@ -381,14 +367,16 @@ namespace URLNotesGrabberCORE
if (args.Length < 2)
{
Console.WriteLine("Usage: -importposts <path-to-legacy-posts.db>");
exitCode = 2;
break;
}
LegacyPostsDbImporter.Run(args[1]);
exitCode = LegacyPostsDbImporter.Run(args[1]);
break;
}
default:
Console.WriteLine("** Unknown Command ** " + args[0]);
exitCode = 2;
break;
}
}
@@ -397,6 +385,62 @@ namespace URLNotesGrabberCORE
System.Console.WriteLine("<fin>:/");
//System.Console.ReadKey();
return exitCode;
}
static void PrintHelp()
{
Console.WriteLine("\t Parse .txt files to find blogs");
Console.WriteLine("-?, -h, --help\t Usage help");
Console.WriteLine("-V, --version\t Print the application version");
Console.WriteLine("--\t End of options: treat every following token as a literal operand");
Console.WriteLine("-parse\t Parse .txt files with specified blogname");
Console.WriteLine("-test\t Calls API for given blogname and postID");
Console.WriteLine("-posts\t For each Post in DB, write blogname to file");
Console.WriteLine("-blogs\t For each Blog in DB, write blogname to file");
Console.WriteLine("-collect [0|1] [datetime]\t Collect Notes from API. 1=only posts without notes. 0=full re-check of all posts: a single resumable pass (interrupt & relaunch to resume; stops when complete, retrigger for a new pass). Optional datetime overrides the cutoff and runs as a one-off (bypasses resume tracking).");
Console.WriteLine("-blogsR\t For each Note that is a REPLY, write blogname to file ");
Console.WriteLine("-blogsO\t For each Blog in DB, write blogname to file, but limit via a passed start and stop range ");
Console.WriteLine("-replies\t Fetch and update missing reply text for all replies in database");
Console.WriteLine("-likes\t Fetch likes: initial backfill for new blogs, incremental refresh for blogs past cooldown. Optional blog name forces single-blog run.");
Console.WriteLine("-force\t (with -likes) Ignore cooldown and refresh every fully-backfilled blog");
Console.WriteLine("-urldump\t Scan all posts' text columns and extract suspected URLs to configured file");
Console.WriteLine("-api3\t Use TumblrApi3 settings from appsettings.json");
Console.WriteLine("-api4\t Use TumblrApi4 settings from appsettings.json");
Console.WriteLine("-start [blogname]\t Start traversal alphabetically at this blog name");
Console.WriteLine("-api [section]\t Use a specific API settings section from appsettings.json (e.g. TumblrApi3)");
Console.WriteLine("-ingest [blogname]\t Ingest Tumblr .txt exports from appSettings:PathTTRoot into TL.db (all blogs, or single blog if name given)");
Console.WriteLine("-output\t Export posts from TL.db back to .txt files in each blog's TTFolderPath");
Console.WriteLine("-revert [blogname]\t Recursively scan the PathInput tree and restore *.bak back to *.txt (current .txt saved as next-free .bkN); optional blogname filters by path substring");
Console.WriteLine("-correct [bakPath]\t Dry-run: report multi-line field updates available from a BAK directory");
Console.WriteLine("-correct -apply [bakPath]\t Apply BAK-file corrections to matching posts (prompts yes/no)");
Console.WriteLine("-updatepaths [rootPath]\t Read .tumblr/.tmblrpriv metadata from <root>\\Index and set Blogs.TTFolderPath");
Console.WriteLine("-importposts [path-to-posts.db]\t One-time migration: copy legacy ThreeTxtFileHelper posts.db rows into TL.db");
}
static void WritePostBlogsToFile(string outPath)