|
| 1 | +# Model Binding |
| 2 | + |
| 3 | +Parsing command line arguments is a means to an end. You probably don't really want to think about parsing the command line. You just want some arguments passed to a method, based on some command line arguments. |
| 4 | + |
| 5 | +In C#, the application entry point method has always looked something like this: |
| 6 | + |
| 7 | +```cs |
| 8 | +static void Main(string[] args) |
| 9 | +{ |
| 10 | +} |
| 11 | +``` |
| 12 | + |
| 13 | +The goal of every command line parsing library is to turn the string array passed to `Main` into something more useful. You might ultimately want to call a method that looks like this: |
| 14 | + |
| 15 | +```cs |
| 16 | +void Handle(int anInt) |
| 17 | +{ |
| 18 | +} |
| 19 | +``` |
| 20 | + |
| 21 | +So for example, you might want an input `123` from the command line to be converted into an `int` with the value `123`. This conversion of command line input into variables or arguments that you can use in your code is called "binding." The term "model binding" refers to binding simple types as well as more complex types in order to pass the values to a method. |
| 22 | + |
| 23 | +# Binding parameters to a command handler |
| 24 | + |
| 25 | +The simplest way to bind command line input is to set the `Handler` property on a `Command`. The `System.CommandLine` model binder will look at the options and arguments for the command and attempt to match them to the parameters of the specified handler method. The default convention is that parameters are matched by name, so in the following example, option `--an-int` matches the parameter named `anInt`. Matching ignores hyphens (and other option prefixes, such as `'/'`) and is case insensitive. |
| 26 | + |
| 27 | +``` cs --source-file ./src/Binding/HandlerBindingSample.cs --project ./src/Binding/Binding.csproj --region MultipleArgs --session MultipleArgs |
| 28 | +var command = new RootCommand |
| 29 | + { |
| 30 | + new Option<string>("--a-string"), |
| 31 | + new Option<int>("--an-int") |
| 32 | + }; |
| 33 | + |
| 34 | +command.Handler = CommandHandler.Create( |
| 35 | + (string aString, int anInt) => |
| 36 | + { |
| 37 | + Console.WriteLine(aString); |
| 38 | + Console.WriteLine(anInt); |
| 39 | + }); |
| 40 | + |
| 41 | +await command.InvokeAsync("--an-int 123 --a-string \"Hello world!\" "); |
| 42 | +``` |
| 43 | + |
| 44 | +``` console --session MultipleArgs |
| 45 | +Hello world! |
| 46 | +123 |
| 47 | + |
| 48 | +``` |
| 49 | + |
| 50 | +## Booleans (flags) |
| 51 | + |
| 52 | +If `true` or `false` is passed for an option having a `bool` argument, it is parsed and bound as expected. But an option whose argument type is `bool` doesn't require an argument to be specified. The presence of the option token on the command line, with no argument following it, results in a value of `true`. You can see various examples here: |
| 53 | + |
| 54 | +``` cs --source-file ./src/Binding/HandlerBindingSample.cs --project ./src/Binding/Binding.csproj --region Bool --session Bool |
| 55 | +var command = new RootCommand |
| 56 | + { |
| 57 | + new Option<bool>("--a-bool") |
| 58 | + }; |
| 59 | + |
| 60 | +command.Handler = CommandHandler.Create( |
| 61 | + (bool aBool) => Console.WriteLine(aBool)); |
| 62 | + |
| 63 | +await command.InvokeAsync(""); |
| 64 | +await command.InvokeAsync("--a-bool"); |
| 65 | +await command.InvokeAsync("--a-bool false"); |
| 66 | +await command.InvokeAsync("--a-bool true"); |
| 67 | +``` |
| 68 | + |
| 69 | +``` console --session Bool |
| 70 | +False |
| 71 | +True |
| 72 | +False |
| 73 | +True |
| 74 | + |
| 75 | +``` |
| 76 | + |
| 77 | +## Enums |
| 78 | + |
| 79 | +You can bind `enum` types as well. The values are bound by name, and the binding is case insensitive: |
| 80 | + |
| 81 | +``` cs --source-file ./src/Binding/HandlerBindingSample.cs --project ./src/Binding/Binding.csproj --region Enum --session Enum |
| 82 | +var command = new RootCommand |
| 83 | + { |
| 84 | + new Option<System.IO.FileAccess>("--an-enum") |
| 85 | + }; |
| 86 | + |
| 87 | +command.Handler = CommandHandler.Create( |
| 88 | + (FileAccess anEnum) => Console.WriteLine(anEnum)); |
| 89 | + |
| 90 | +await command.InvokeAsync("--an-enum Read"); |
| 91 | +await command.InvokeAsync("--an-enum READ"); |
| 92 | +``` |
| 93 | + |
| 94 | +``` console --session Enum |
| 95 | +Read |
| 96 | +Read |
| 97 | + |
| 98 | +``` |
| 99 | + |
| 100 | +## Arrays, lists, and other enumerable types |
| 101 | + |
| 102 | +Arguments having various enumerable types can be bound. A number of common types implementing `IEnumerable` are supported. In the next example, try changing the type of the `--items` `Option`'s `Argument` property to `Argument<IEnumerable<string>>` or `Argument<List<string>>`. |
| 103 | + |
| 104 | +``` cs --source-file ./src/Binding/HandlerBindingSample.cs --project ./src/Binding/Binding.csproj --region Enumerables --session Enumerables |
| 105 | +var command = new RootCommand |
| 106 | + { |
| 107 | + new Option<string[]>("--items") |
| 108 | + }; |
| 109 | + |
| 110 | +command.Handler = CommandHandler.Create( |
| 111 | + (IEnumerable<string> items) => |
| 112 | + { |
| 113 | + Console.WriteLine(items.GetType()); |
| 114 | + |
| 115 | + foreach (var item in items) |
| 116 | + { |
| 117 | + Console.WriteLine(item); |
| 118 | + } |
| 119 | + }); |
| 120 | + |
| 121 | +await command.InvokeAsync("--items one two three"); |
| 122 | +``` |
| 123 | + |
| 124 | +``` console --session Enumerables |
| 125 | +System.String[] |
| 126 | +one |
| 127 | +two |
| 128 | +three |
| 129 | + |
| 130 | +``` |
| 131 | + |
| 132 | +## File system types |
| 133 | + |
| 134 | +Since command line applications very often have to work with the file system, `FileInfo` and `DirectoryInfo` are clearly important for binding to support. Run the following code, then try changing the generic type argument to `DirectoryInfo` and running it again. |
| 135 | + |
| 136 | +``` cs --source-file ./src/Binding/HandlerBindingSample.cs --project ./src/Binding/Binding.csproj --region FileSystemTypes --session FileSystemTypes |
| 137 | +var command = new RootCommand |
| 138 | + { |
| 139 | + new Option<FileInfo>("-f").ExistingOnly() |
| 140 | + }; |
| 141 | + |
| 142 | +command.Handler = CommandHandler.Create( |
| 143 | + (FileSystemInfo f) => |
| 144 | + { |
| 145 | + Console.WriteLine($"{f.GetType()}: {f}"); |
| 146 | + }); |
| 147 | + |
| 148 | +await command.InvokeAsync("-f /path/to/something"); |
| 149 | +``` |
| 150 | + |
| 151 | +``` console --session FileSystemTypes |
| 152 | +Usage: |
| 153 | + Binding [options] |
| 154 | + |
| 155 | +Options: |
| 156 | + -f <f> |
| 157 | + --version Show version information |
| 158 | + -?, -h, --help Show help and usage information |
| 159 | + |
| 160 | + |
| 161 | +``` |
| 162 | + |
| 163 | +## Anything with a string constructor |
| 164 | + |
| 165 | +But `FileInfo` and `DirectoryInfo` are not special cases. Any type having a constructor that takes a single string parameter can be bound in this way. Go back to the previous example and try using a `Uri` instead. |
| 166 | + |
| 167 | +## More complex types |
| 168 | + |
| 169 | +Binding also supports creating instances of more complex types. If you have a large number of options, this can be cleaner than adding more parameters to your handler. `System.CommandLine` has the default convention of binding `Option` arguments to either properties or constructor parameters by name. The name matching uses the same strategies that are used when matching parameters on a handler method. |
| 170 | + |
| 171 | +In the next sample, the handler accepts an instance of `ComplexType`. Try removing its setters and uncommenting the constructor. Try adding properties, or changing the types or names of its properties. |
| 172 | + |
| 173 | +``` cs --source-file ./src/Binding/HandlerBindingSample.cs --project ./src/Binding/Binding.csproj --region ComplexTypes --session ComplexTypes |
| 174 | +public static async Task<int> ComplexTypes() |
| 175 | +{ |
| 176 | + var command = new Command("the-command") |
| 177 | + { |
| 178 | + new Option<int>("--an-int"), |
| 179 | + new Option<string>("--a-string") |
| 180 | + }; |
| 181 | + |
| 182 | + command.Handler = CommandHandler.Create( |
| 183 | + (ComplexType complexType) => |
| 184 | + { |
| 185 | + Console.WriteLine(Format(complexType)); |
| 186 | + }); |
| 187 | + |
| 188 | + await command.InvokeAsync("--an-int 123 --a-string 456"); |
| 189 | + |
| 190 | + return 0; |
| 191 | +} |
| 192 | + |
| 193 | +public class ComplexType |
| 194 | +{ |
| 195 | + // public ComplexType(int anInt, string aString) |
| 196 | + // { |
| 197 | + // AnInt = anInt; |
| 198 | + // AString = aString; |
| 199 | + // } |
| 200 | + public int AnInt { get; set; } |
| 201 | + public string AString { get; set; } |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +``` console --session ComplexTypes |
| 206 | +AnInt: 123 (System.Int32) |
| 207 | +AString: 456 (System.String) |
| 208 | + |
| 209 | + |
| 210 | +``` |
| 211 | + |
| 212 | +## System.CommandLine types |
| 213 | + |
| 214 | +Not everything you might want passed to your handler will necessarily come from parsed command line input. There are a number of types provided by `System.CommandLine` that you can bind to. The following example demonstratres injection of `ParseResult` and `IConsole`. Other types can be passed this way as well. |
| 215 | + |
| 216 | +``` cs --source-file ./src/Binding/HandlerBindingSample.cs --project ./src/Binding/Binding.csproj --region DependencyInjection --session DependencyInjection |
| 217 | +var command = new RootCommand |
| 218 | + { |
| 219 | + new Option<string>("--a-string"), |
| 220 | + new Option<int>("--an-int"), |
| 221 | + new Option<System.IO.FileAttributes>("--an-enum"), |
| 222 | + }; |
| 223 | + |
| 224 | +command.Handler = CommandHandler.Create( |
| 225 | + (ParseResult parseResult, IConsole console) => |
| 226 | + { |
| 227 | + console.Out.WriteLine($"{parseResult}"); |
| 228 | + }); |
| 229 | + |
| 230 | +await command.InvokeAsync("--an-int 123 --a-string \"Hello world!\" --an-enum compressed"); |
| 231 | +``` |
| 232 | + |
| 233 | +``` console --session DependencyInjection |
| 234 | +ParseResult: [ Binding [ --an-int <123> ] [ --a-string <Hello world!> ] [ --an-enum <Compressed> ] ] |
| 235 | + |
| 236 | +``` |
0 commit comments