{"id":2627,"date":"2022-06-25T18:58:00","date_gmt":"2022-06-25T17:58:00","guid":{"rendered":"https:\/\/andyjohnson.uk\/blog\/?p=2627"},"modified":"2024-02-01T15:28:47","modified_gmt":"2024-02-01T15:28:47","slug":"adventures-in-yak-shaving-with-system-commandline","status":"publish","type":"post","link":"https:\/\/andyjohnson.uk\/blog\/2022\/06\/25\/adventures-in-yak-shaving-with-system-commandline\/","title":{"rendered":"Adventures in Yak Shaving with System.CommandLine"},"content":{"rendered":"\n<p>Over the last few months I&#8217;ve been <s>using<\/s> wasting odd moments of free time by tinkering with some code to extract pictures from a Google <a href=\"https:\/\/takeout.google.com\">Takeout<\/a> archive. The idea is to use the json metadata in the archive to restore the image timestamps (which Google removes from the embedded image metadata &#8211; for reasons best known to itself), rationalise the file naming, separate edits and originals, etc. The ultimate aim is to be able to grab a Takeout, extract and locally archive all the images from some period of time (say, the last year) so that I can then manually remove those images from my Google Photos collection. And the aim of <em>that <\/em>is to reduce my exposure to Google arbitrarily closing my account and consequently deleting my pics. And because I&#8217;m old fashioned enough to distrust 100% reliance on &#8220;the cloud&#8221;.<\/p>\n\n\n\n<p>So I wrote some code and got it working as a c# .net 6 command-line app. Its a bit rough but it does what I need.<\/p>\n\n\n\n<p>And then I had the genius idea of restructuring it as a set of providers that could be used to extract all the other stuff that you might find in a Takeout archive: contacts, emails, whatever. And of course this would need command-line options that apply to each provider, to allow the output to be customised. Which requires a way of grouping those options &#8211; basically I needed the idea of &#8220;commands&#8221; that delimit groups of options and correspond to the different types of media in the archive. I also needed some options that are global and not associated with a command &#8211; for input and output directories, for example. At this point my old <code>CommandLineParser<\/code> class that I&#8217;ve been dropping into console apps for the decade or so was not going to cut it.<\/p>\n\n\n\n<p>So I did some reading and decided to try <code><a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/standard\/commandline\/\">System.CommandLine<\/a><\/code> &#8211; the shiny new way to parse command line parameters. This is still in beta but my initial impression was favourable. Basically, you create an object model of your command-line syntax, hook it up to handlers, and let the library do the grunt work of parsing the command-line into values, handling errors, automatically generating help text (particularly impressive), and lots of other stuff.<\/p>\n\n\n\n<p>Here&#8217;s a little test app that I made:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>    public static int Main(string&#91;] args)\n    {\n        \/\/ audio command\n        var thresholdOpt = new Option&lt;int&gt;(\"--threshold\");\n        var scaleOpt = new Option&lt;double&gt;(\"--scale\");\n        var audioCommand = new Command(\"audio\") { thresholdOpt , scaleOpt};\n        audioCommand.SetHandler(\n            (int threshold, double scale) =&gt; { Console.WriteLine($\"threshold={threshold}, scale={scale}\"); },\n            thresholdOpt, scaleOpt);\n\n        \/\/ video command\n        var monochromeOpt = new Option&lt;bool&gt;(\"--mono\", description: \"Monochrome\");\n        var colourOpt = new Option&lt;bool&gt;(\"--colour\");\n        var brightnessOpt = new Option&lt;int&gt;(\"--brightness\");\n        var videoCommand = new Command(\"video\") { monochromeOpt, colourOpt, brightnessOpt };\n        videoCommand.SetHandler(\n            (bool mono, bool colour, int brightness) =&gt; { Console.WriteLine($\"mono={mono}, colour={colour}, brightness={brightness}\"); },\n            monochromeOpt, colourOpt, brightnessOpt);\n\n        \/\/ root command\n        var infileOpt = new Option&lt;FileInfo&gt;(\"--i\");\n        var outfileOpt = new Option&lt;FileInfo&gt;(\"--o);\n        var rootCommand = new RootCommand(\"test\");\n        rootCommand.AddOption(infileOpt);\n        rootCommand.AddOption(outfileOpt);\n        rootCommand.AddCommand(audioCommand);\n        rootCommand.AddCommand(videoCommand);\n        rootCommand.SetHandler(\n            (FileInfo infile, FileInfo outfile) =&gt; { Console.WriteLine($\"i={infile}, o={outfile}\"); },\n            infileOpt, outfileOpt);\n\n        return rootCommand.Invoke(args);\n    }<\/code><\/code><\/pre>\n\n\n\n<p>This implements the commands for an entirely fictitious test program that might be invoked with arguments like: <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>test --i \"input.dat\" --o \"output.dat\" audio --threshold 42 --scale 3.14 video --mono --brightness 60<\/code><\/pre>\n\n\n\n<p>Hopefully the similarity to my Takeout extractor should be obvious.<\/p>\n\n\n\n<p>I was initially a bit mystified by the use of lambdas as &#8220;handlers&#8221; that are passed the values of various options. This mean that there was no single place in the code where everything about the parse was &#8220;known&#8221;. I didn&#8217;t know why it was like that but I thought I could work around it.<\/p>\n\n\n\n<p>The first difficulty I encountered was that, while it is possible to associate options with the root command and also associated commands (which have their own options), only the first command is ever parsed. So if I include the <code>audio <\/code>command then the <code>video <\/code>command is ignored. Also, if any command is included in the <code>args<\/code> array then options associated with the root command itself (e.g. <code>--i<\/code> and <code>--o<\/code>) are not parsed. Clearly I was either not understanding something, or I wasn&#8217;t using it in the way that it was designed to be used. I opened an <a href=\"https:\/\/github.com\/dotnet\/command-line-api\/issues\/1739\">issue on github<\/a> and fairly quickly got confirmation that it was the latter.<\/p>\n\n\n\n<p>There was, however, cause for hope: I could split the command-line at command-token boundaries and parse each subset of arguments separately. Since <code>RootCommand.Invoke()<\/code> is actually an extension method (more of this below) I wrote a new extension method to do this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    public static int InvokeMultiCommand(\n        this RootCommand command, \n        string&#91;] args)\n    {\n        var commands = new List&lt;Command&gt;() { command };\n        commands.AddRange(command.Subcommands);\n        foreach (var seg in SegmentArgs(args, commands.ToArray()))\n        {\n<code>            var exitCode = command.Invoke(seg);\n            if (exitCode != 0)\n                return exitCode;\n        }\n        return 0;<\/code>    }<\/code><\/pre>\n\n\n\n<p><code>SegmentArgs()<\/code> does the job of chopping up the <code>string[]<\/code> arguments array into a <code>string[][]<\/code>.<\/p>\n\n\n\n<p>With that working, I looked at how to customise the help output to include all commands and their options. As it stood, invoking the app it with the &#8211;help option gave the following:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"745\" height=\"290\" src=\"https:\/\/andyjohnson.uk\/blog\/wp-content\/uploads\/2022\/06\/image.png\" alt=\"\" class=\"wp-image-2632\" srcset=\"https:\/\/andyjohnson.uk\/blog\/wp-content\/uploads\/2022\/06\/image.png 745w, https:\/\/andyjohnson.uk\/blog\/wp-content\/uploads\/2022\/06\/image-300x117.png 300w\" sizes=\"auto, (max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 984px) 61vw, (max-width: 1362px) 45vw, 600px\" \/><\/figure>\n\n\n\n<p>I needed descriptions for all the commands, and also for their options to be listed.<\/p>\n\n\n\n<p>After reading the <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/standard\/commandline\/customize-help\">documentation<\/a> for help customisation, and digging into how the help is generated, I realised that what I&#8217;d done so far was the easy bit. The library provides a <code>CommandLineBuilder<\/code> class, instances of which can be wired to lambdas that customise how it generates help text. But having done that, the <code>CommandLineBuilder<\/code> instance is responsible for doing the parse via it&#8217;s <code>Invoke()<\/code> method, not the root command. And there didn&#8217;t seem to be a way to make this compatible with the code I&#8217;d already written: I wanted to parse commands separately but have help generation that was aware of the syntax of all commands. There seemed to be a fundamental mismatch.<\/p>\n\n\n\n<p>I tried extending <code>CommandLineBuilder<\/code> by the deeply unfashionable approach of sub-classing, but its <code>Build()<\/code> method (which generates a <code>Parser <\/code>object to actually do the parse) isn&#8217;t virtual so I couldn&#8217;t override it. And many of its key methods are implemented as extension methods, so I couldn&#8217;t override them either.<\/p>\n\n\n\n<p>I tried extending <code>CommandLineBuilder<\/code> instead, but I found that I was having to wrap more and more of its functionality. And because <code>CommandLineBuilder<\/code> is injected as a dependency at various points, my non-overriding extension methods weren&#8217;t being called anyway.<\/p>\n\n\n\n<p>So I gave up <a href=\"https:\/\/seths.blog\/2005\/03\/dont_shave_that\/\">shaving the yak<\/a>. At the top of my stack of requirements, I just wanted to archive photos. At the bottom of the stack I was hacking on a command-line parsing library to extend it in an unusual way. It was an interesting exercise, but I was wasting time. Its always good to know when to give up and pop the stack.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Over the last few months I&#8217;ve been using wasting odd moments of free time by tinkering with some code to extract pictures from a Google Takeout archive. The idea is to use the json metadata in the archive to restore the image timestamps (which Google removes from the embedded image metadata &#8211; for reasons best &hellip; <a href=\"https:\/\/andyjohnson.uk\/blog\/2022\/06\/25\/adventures-in-yak-shaving-with-system-commandline\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Adventures in Yak Shaving with System.CommandLine&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[57,27],"class_list":["post-2627","post","type-post","status-publish","format-standard","hentry","category-uncategorised","tag-takeoutextractor","tag-tech"],"_links":{"self":[{"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/posts\/2627","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/comments?post=2627"}],"version-history":[{"count":10,"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/posts\/2627\/revisions"}],"predecessor-version":[{"id":2639,"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/posts\/2627\/revisions\/2639"}],"wp:attachment":[{"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/media?parent=2627"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/categories?post=2627"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/andyjohnson.uk\/blog\/wp-json\/wp\/v2\/tags?post=2627"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}