From 03676432bddbdcacb34e8021cd3bcb7d2272d5da Mon Sep 17 00:00:00 2001 From: jim Date: Tue, 9 Jun 2026 15:12:10 -0500 Subject: [PATCH] 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 --- URLNotesGrabberCORE/Program.cs | 156 +++++++++++++++++++++------------ 1 file changed, 100 insertions(+), 56 deletions(-) diff --git a/URLNotesGrabberCORE/Program.cs b/URLNotesGrabberCORE/Program.cs index 018bc5f..553cb83 100644 --- a/URLNotesGrabberCORE/Program.cs +++ b/URLNotesGrabberCORE/Program.cs @@ -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 filteredArgs = new List(); 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 \\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("PathOutputBlogs"), false, from, to, top); break; @@ -329,6 +314,7 @@ namespace URLNotesGrabberCORE else { Console.WriteLine("--Expected FROM TO--"); + exitCode = 2; } WriteBlogsToFileAll(settings.GetValue("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("PathTTRoot") ?? settings.GetValue("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 "); + 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(":/"); //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 \\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)