Skip to content

Commit 7ec3939

Browse files
committed
Updated documentation.
1 parent 58b2a19 commit 7ec3939

File tree

15 files changed

+412
-244
lines changed

15 files changed

+412
-244
lines changed

J4JCommandLine.sln

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyHelpError", "FancyHelp
1515
EndProject
1616
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{473DDAC4-C8B9-4FBC-97C9-51AB11F038EF}"
1717
ProjectSection(SolutionItems) = preProject
18+
docs\allocator.md = docs\allocator.md
1819
docs\di.md = docs\di.md
1920
docs\diagrams.md = docs\diagrams.md
2021
docs\example-instance.md = docs\example-instance.md
2122
docs\example-static.md = docs\example-static.md
2223
docs\goal-concept.md = docs\goal-concept.md
2324
LICENSE.md = LICENSE.md
24-
docs\parser.md = docs\parser.md
2525
README.md = README.md
26+
docs\terminology.md = docs\terminology.md
2627
docs\text-converters.md = docs\text-converters.md
2728
docs\usage.md = docs\usage.md
29+
docs\validators.md = docs\validators.md
2830
EndProjectSection
2931
EndProject
3032
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{9CABC5BB-C8B6-4BF4-97F2-0B9BC53A88BB}"

J4JCommandLine/allocating/Allocator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ public bool Initialize(
4141
return _prefixer.IsInitialized && _terminator.IsInitialized;
4242
}
4343

44-
public Allocations AllocateCommandLine( string[] args ) => AllocateCommandLine( string.Join( " ", args ) );
44+
public IAllocations AllocateCommandLine( string[] args ) => AllocateCommandLine( string.Join( " ", args ) );
4545

46-
public Allocations AllocateCommandLine( string cmdLine )
46+
public IAllocations AllocateCommandLine( string cmdLine )
4747
{
4848
var retVal = new Allocations(_keyComp);
4949

J4JCommandLine/allocating/IAllocations.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@ namespace J4JSoftware.CommandLine
55
public interface IAllocations : ICollection<IAllocation>
66
{
77
IAllocation Unkeyed { get; }
8+
IAllocation this [ string key ] { get; }
9+
IAllocation this [ int idx ] { get; }
10+
11+
bool Contains( string key );
812
}
913
}

J4JCommandLine/allocating/IAllocator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ bool Initialize(
1313
CommandLineLogger logger,
1414
MasterTextCollection masterText );
1515

16-
Allocations AllocateCommandLine( string[] args );
17-
Allocations AllocateCommandLine( string cmdLine );
16+
IAllocations AllocateCommandLine( string[] args );
17+
IAllocations AllocateCommandLine( string cmdLine );
1818
}
1919
}

README.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ var builder = ServiceProvider.GetRequiredService<BindingTargetBuilder>();
1313
1414
// configure the builder (these are the minimum calls you need to make)
1515
builder.Prefixes( "-", "--", "/" )
16+
.Quotes( '\'', '"' )
1617
.HelpKeys( "h", "?" );
1718
1819
// create an instance of BindingTarget
19-
builder.Build<Program>( null, out var binder );
20-
21-
if( binder == null )
20+
var binder = builder.Build<Program>(null);
21+
if (binder == null)
2222
throw new NullReferenceException( nameof(Program) );
2323
2424
// define your options (here they're bound to public static properties)
@@ -27,28 +27,39 @@ if( binder == null )
2727
binder.Bind( x => Program.IntValue, "i" );
2828
binder.Bind( x => Program.TextValue, "t" );
2929
30+
// bind the unkeyed parameters -- command line arguments that are
31+
// not options -- if you want to retrieve them (optional but commonly done)
32+
binder.BindUnkeyed( x => Program.Unkeyed );
33+
3034
// parse the command line
31-
if( binder.Parse( args ) != MappingResults.Success )
35+
if( !binder.Parse( args ) )
3236
{
3337
Environment.ExitCode = 1;
3438
return;
3539
}
3640
3741
Console.WriteLine($"IntValue is {IntValue}");
3842
Console.WriteLine($"TextValue is {TextValue}");
43+
44+
Console.WriteLine( Unkeyed.Count == 0
45+
? "No unkeyed parameters"
46+
: $"Unkeyed parameters: {string.Join( ", ", Unkeyed )}" );
3947
```
4048

4149
### Table of Contents
4250

4351
- [Goal and Concept](docs/goal-concept.md)
52+
- [Terminology](docs/terminology.md)
4453
- [Usage](docs/usage.md)
4554
- Examples
4655
- [Binding to static properties](docs/example-static.md)
4756
- [Binding to a configuration object](docs/example-instance.md)
4857
- [Architectural Notes](docs/diagrams.md)
4958
- [Autofac Dependency Injection Support](docs/di.md)
50-
- [Adding Text Converters](docs/text-converters.md)
51-
- [Notes on the first-stage command line parser](docs/parser.md)
59+
- Extending the framework
60+
- [Adding Text Converters](docs/text-converters.md)
61+
- [Adding Validators](docs/validators.md)
62+
- [Notes on the allocator](docs/allocator.md)
5263

5364
#### Inspiration and Dedication
5465

docs/allocator.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
### The First Stage Command Line Parser
2+
3+
**IAllocator** defines how the allocator -- the object that
4+
allocates elements from the command line into a collection of text values
5+
keyed by option keys -- works.
6+
7+
Given the sometimes oddball nature of what constitutes
8+
a traditional command line in different environments you may want to
9+
replace it with a custom one. This article describes the way the default
10+
implementation works as a means of touching on issues you may need to
11+
consider in writing your own.
12+
13+
Conceptually the **AllocateCommandLine()** method is pretty simple. It reads
14+
characters one by one from the text it was given and decodes them into
15+
option keys and values. Unfortunately that is a somewhat complex process
16+
because the context of what a character means depends on the characters that
17+
have come before it.
18+
19+
Here's the code to the default **AllocateCommandLine()** implementation:
20+
```
21+
public IAllocations AllocateCommandLine( string cmdLine )
22+
{
23+
var retVal = new Allocations(_keyComp);
24+
25+
var accumulator = new StringBuilder();
26+
IAllocation? curResult = null;
27+
var charsProcessed = 0;
28+
var lastElementWasKey = false;
29+
30+
for( var idx = 0; idx < cmdLine.Length; idx++)
31+
{
32+
accumulator.Append( cmdLine[idx] );
33+
charsProcessed++;
34+
35+
var element = accumulator.ToString();
36+
37+
// analyze the sequence as it currently stands to see if it includes
38+
// a prefixed key and/or has been terminated
39+
var maxPrefix = _prefixer.GetMaxPrefixLength(element);
40+
var maxTerminator = _terminator.GetMaxTerminatorLength(element, maxPrefix > 0);
41+
42+
// keep adding characters unless we've encountered a termination
43+
// sequence or we've reached the end of the command line
44+
if( maxTerminator <= 0 && charsProcessed < cmdLine.Length )
45+
continue;
46+
47+
// extract the true element value from the prefix and terminator
48+
element = element[maxPrefix..^maxTerminator];
49+
50+
// key values are identified by the presence of known prefixes
51+
if ( maxPrefix > 0 )
52+
{
53+
// because multiple key references are allowed (e.g., "-x abc -x def") check
54+
// to see if the key is already recorded. We only create and store a new
55+
// Allocation if the key isn't already stored
56+
if( !retVal.Contains( element ) )
57+
retVal.Add( new Allocation( retVal ) { Key = element } );
58+
59+
// store a reference to the current/active Allocation unless the
60+
// element is a request for help (because help options cannot have
61+
// parameters)
62+
if( !_masterText.Contains( element, TextUsageType.HelpOptionKey ) )
63+
{
64+
curResult = retVal[ element ];
65+
lastElementWasKey = true;
66+
}
67+
else lastElementWasKey = false;
68+
}
69+
else
70+
{
71+
if( curResult == null || !lastElementWasKey )
72+
retVal.Unkeyed.Parameters.Add( element );
73+
else curResult.Parameters.Add( element );
74+
75+
lastElementWasKey = false;
76+
}
77+
78+
// clear the accumulator so we can start processing the next character sequence
79+
accumulator.Clear();
80+
}
81+
82+
return retVal;
83+
}
84+
```
85+
Let's walk through a few key areas.
86+
```
87+
var maxPrefix = Prefixer.GetMaxPrefixLength(element);
88+
var maxTerminator = _terminator.GetMaxTerminatorLength(element, maxPrefix > 0);
89+
90+
// keep adding characters unless we've encountered a termination
91+
// sequence or we've reached the end of the command line
92+
if( maxTerminator <= 0 && charsProcessed < cmdLine.Length )
93+
continue;
94+
```
95+
**maxPrefix** and **maxTerminator** are the character positions of the
96+
"furthest" key prefix (e.g., "-", "--" or some such) and termination
97+
sequence (usually a single character). Those are looked for in
98+
the variable **element**, which simply contains whatever is in the **StringBuilder**
99+
instance **accumulator**.
100+
101+
A **maxPrefix** of zero means no prefix has yet been found. A **maxTerminator**
102+
of zero means no termination sequence has yet been found..but in deciding
103+
whether or not to keep reading characters you have to honor the physical
104+
end of the command line, which isn't itself a character.
105+
Once you've found a termination the first thing we do is strip away any
106+
key prefixes and termination sequences (i.e., all we want from here on out
107+
is the "pure" text value) from **element**.
108+
109+
```
110+
// extract the true element value from the prefix and terminator
111+
element = element[maxPrefix..^maxTerminator];
112+
```
113+
We then check to see if the element had an option key prefix (e.g., a "--").
114+
If it does we check to see if we need a new **Allocation** object to hold
115+
parameter values. Since options can appear multiple times we don't want to
116+
create a new **Allocation** each time we find a key. We only create a
117+
new **Allocation** if there's isn't already one with that key in the
118+
**Allocations** collection we're building.
119+
120+
We also check to see if the **Allocation** we just created was for a
121+
help request. We only update the current **Allocation** -- which is
122+
receiving text values -- if it's a non-help option.
123+
```
124+
125+
// key values are identified by the presence of known prefixes
126+
if ( maxPrefix > 0 )
127+
{
128+
// because multiple key references are allowed (e.g., "-x abc -x def") check
129+
// to see if the key is already recorded. We only create and store a new
130+
// Allocation if the key isn't already stored
131+
if( !retVal.Contains( element ) )
132+
retVal.Add( new Allocation( retVal ) { Key = element } );
133+
134+
// store a reference to the current/active Allocation unless the
135+
// element is a request for help (because help options cannot have
136+
// parameters)
137+
if( !_masterText.Contains( element, TextUsageType.HelpOptionKey ) )
138+
{
139+
curResult = retVal[ element ];
140+
lastElementWasKey = true;
141+
}
142+
else lastElementWasKey = false;
143+
}
144+
```
145+
If the element wasn't a key it's just a text value that needs to be
146+
assigned to either the current **Allocation** or the global **Allocation**
147+
representing "unkeyed" text values (which are plain old non-option command line
148+
parameters).
149+
```
150+
else
151+
{
152+
if( curResult == null || !lastElementWasKey )
153+
retVal.Unkeyed.Parameters.Add( element );
154+
else curResult.Parameters.Add( element );
155+
156+
lastElementWasKey = false;
157+
}
158+
159+
// clear the accumulator so we can start processing the next character sequence
160+
accumulator.Clear();
161+
```

docs/diagrams.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,30 @@ a pre-existing **Grandparent** instance -- which has already created its
8686
**Parent** child property -- the framework will allow you to bind to
8787
**IntValue**.
8888

89-
### The Parsing Process
89+
### The Allocating and Parsing Process
9090

9191
![Property Binding Process](assets/binding.png)
9292
*This section pertains to the lower part of the above diagram.*
9393

94-
When the parsing process runs it may or may not succeed and the user may or
94+
What most people think of as "parsing" is, within this library, actually a
95+
combination of the **allocating** process and a **conversion/validation/assignment**
96+
process. Because the latter term is too long I refer to it here as
97+
"parsing"...which is admittedly somewhat confusing. Sorry about that.
98+
99+
"Parsing" takes place after allocation. Converting, validating
100+
and assigning property values to bound properties is inextricably linked
101+
to configuration information about what an option should be (e.g., a text
102+
value, a double, a binary switch). That's what most people think of as the
103+
entirety of parsing a command line.
104+
105+
Allocation doesn't concern itself with any of that detail. It focuses on
106+
breaking up a command line into pieces, determining which pieces are keys
107+
(indicating that an option is being declared) and which are simply text
108+
values.
109+
110+
For more information about this please read [Terminology](terminology.md).
111+
112+
When the overall parsing process runs it may or may not succeed and the user may or
95113
may not have requested help. Requesting help is considered a "failure" but it's
96114
possible the rest of the parsing process succeeded. That means you could continue
97115
with the execution of your program after help is requested...but that'd be odd.

0 commit comments

Comments
 (0)