Skip to content

Commit 62b2191

Browse files
committed
fixed grammar to reflect expected/documented behavior of stripping quotes parsed
- added EnvironmentEx.CommandLine to manage platform.runtime behavioral differences
1 parent a195f3a commit 62b2191

9 files changed

Lines changed: 148 additions & 8 deletions

File tree

CommandlineParsing.sln

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Repo Items", "Repo Items",
2626
stylecop.json = stylecop.json
2727
EndProjectSection
2828
EndProject
29+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{D50F6965-5684-404D-A886-CD6E1A7A093E}"
30+
EndProject
31+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleArgs", "SampleArgs\SampleArgs.csproj", "{694CC71C-BD60-4B94-BC64-391949BA269E}"
32+
EndProject
2933
Global
3034
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3135
Debug|Any CPU = Debug|Any CPU
@@ -48,10 +52,17 @@ Global
4852
{54A04023-808D-4F10-9EB2-6741703A6D43}.Debug|Any CPU.Build.0 = Debug|Any CPU
4953
{54A04023-808D-4F10-9EB2-6741703A6D43}.Release|Any CPU.ActiveCfg = Release|Any CPU
5054
{54A04023-808D-4F10-9EB2-6741703A6D43}.Release|Any CPU.Build.0 = Release|Any CPU
55+
{694CC71C-BD60-4B94-BC64-391949BA269E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
56+
{694CC71C-BD60-4B94-BC64-391949BA269E}.Debug|Any CPU.Build.0 = Debug|Any CPU
57+
{694CC71C-BD60-4B94-BC64-391949BA269E}.Release|Any CPU.ActiveCfg = Release|Any CPU
58+
{694CC71C-BD60-4B94-BC64-391949BA269E}.Release|Any CPU.Build.0 = Release|Any CPU
5159
EndGlobalSection
5260
GlobalSection(SolutionProperties) = preSolution
5361
HideSolutionNode = FALSE
5462
EndGlobalSection
63+
GlobalSection(NestedProjects) = preSolution
64+
{694CC71C-BD60-4B94-BC64-391949BA269E} = {D50F6965-5684-404D-A886-CD6E1A7A093E}
65+
EndGlobalSection
5566
GlobalSection(ExtensibilityGlobals) = postSolution
5667
SolutionGuid = {924778B7-EC77-44E2-AB62-05AF147B36F6}
5768
EndGlobalSection

SampleArgs/Program.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
2+
// Licensed under the MIT license. See the LICENSE.md file in the project root for full license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel;
7+
using Ubiquity.CommandlineParsing;
8+
9+
namespace SampleArgs
10+
{
11+
public static class Program
12+
{
13+
public static void Main( )
14+
{
15+
// Run with arguments like:
16+
// >SampleArgs.exe positionalarg0 -Option1 "positional arg 1" -Option2="this is a test" positional2 "positional\foo 3\\"
17+
try
18+
{
19+
var options = Options.ParseFrom( EnvironmentEx.CommandLine );
20+
}
21+
catch(CommandlineParseException ex)
22+
{
23+
Console.Error.WriteLine( ex.Message );
24+
Options.ShowHelp( );
25+
}
26+
}
27+
28+
[DefaultProperty( "PositionalArgs" )]
29+
internal class Options
30+
{
31+
public List<string> PositionalArgs { get; } = new List<string>( );
32+
33+
[CommandlineArg( AllowSpaceDelimitedValue = true )]
34+
public string Option1 { get; set; }
35+
36+
public string Option2 { get; set; }
37+
38+
public static Options ParseFrom( string commandLine )
39+
{
40+
return CommandlineBinder.ParseAndBind<Options>( new Ubiquity.CommandlineParsing.Monad.Parser( ), commandLine );
41+
}
42+
43+
public static void ShowHelp()
44+
{
45+
Console.WriteLine(
46+
"Sample CommandlineParing app\n" +
47+
"Usage:\n" +
48+
" SampeArgs (positionalarg | [option])*\n" +
49+
"where:\n" +
50+
" positionalarg is a sequence of non-whitespace chars or a quoted string (single or double quotes)\n" +
51+
" option is one of the following options:\n" +
52+
" -Option1 value\n"+
53+
" -Option2=value\n"
54+
);
55+
}
56+
}
57+
}
58+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"profiles": {
3+
"SampleArgs": {
4+
"commandName": "Project",
5+
"commandLineArgs": "positionalarg0 -Option1 \"positional arg 1\" -Option2=\"this is a test\" positional2 \"positional\\foo 3\\\\\""
6+
}
7+
}
8+
}

SampleArgs/SampleArgs.csproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFrameworks>net47;netcoreapp2.0</TargetFrameworks>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\Ubiquity.CommandlineParsing.Monad\Ubiquity.CommandlineParsing.Monad.csproj" />
10+
</ItemGroup>
11+
12+
</Project>

Ubiquity.CommandlineParsing.Monad.UT/BinderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ public void CommandLineBinderTest()
5454
Assert.IsNotNull( options );
5555
Assert.AreEqual( 4, options.PositionalArgs.Count );
5656
Assert.AreEqual( 3, options.MultiOption.Count );
57-
Assert.AreEqual( "'space delimited value1'", options.Option1 );
58-
Assert.AreEqual( "'this is a test'", options.Option2 );
57+
Assert.AreEqual( "space delimited value1", options.Option1 );
58+
Assert.AreEqual( "this is a test", options.Option2 );
5959
Assert.AreEqual( TestOptions.Option3Values.Baz, options.Option3 );
6060
Assert.IsTrue( options.Option4 );
6161
}

Ubiquity.CommandlineParsing.Monad.UT/GrammarLexemeTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public void NonWhitespaceExceptionTest( )
203203
public void SingleQuotedStringTest( )
204204
{
205205
const string singleQuotedString = " 'asdf \"ghijk\" \u0394' ";
206-
Assert.AreEqual( singleQuotedString.Trim( ), QuotedString.Text( ).Parse( singleQuotedString ) );
206+
Assert.AreEqual( singleQuotedString.Trim( ).Trim('\''), QuotedString.Text( ).Parse( singleQuotedString ) );
207207
}
208208

209209
[TestMethod]
@@ -218,7 +218,7 @@ public void SingleQuotedStringExceptionTest( )
218218
public void DoubleQuotedStringTest( )
219219
{
220220
const string doubleQuotedString = "\"asdf ghijk \u0394\"";
221-
Assert.AreEqual( doubleQuotedString, QuotedString.Text( ).Parse( doubleQuotedString ) );
221+
Assert.AreEqual( doubleQuotedString.Trim( ).Trim( '"' ), QuotedString.Text( ).Parse( doubleQuotedString ) );
222222
}
223223

224224
[TestMethod]

Ubiquity.CommandlineParsing.Monad.UT/ParserTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,14 @@ public void CommandLineParserTest( )
6969

7070
Assert.IsInstanceOfType( args[ 2 ], typeof( CommandlineValue ) );
7171
var arg2 = ( CommandlineValue )args[ 2 ];
72-
Assert.AreEqual( "'positional arg 1'", arg2.Text );
72+
Assert.AreEqual( "positional arg 1", arg2.Text );
7373

7474
Assert.IsInstanceOfType( args[ 3 ], typeof( CommandlineOption ) );
7575
var arg3 = ( CommandlineOption )args[ 3 ];
7676
Assert.AreEqual( "-", arg3.SwitchLeader );
7777
Assert.AreEqual( "Option2", arg3.Name );
7878
Assert.AreEqual( "=", arg3.Delimiter );
79-
Assert.AreEqual( "'this is a test'", arg3.Value.Text );
79+
Assert.AreEqual( "this is a test", arg3.Value.Text );
8080

8181
Assert.IsInstanceOfType( args[ 4 ], typeof( CommandlineOption ) );
8282
var arg4 = ( CommandlineOption )args[ 4 ];
@@ -91,7 +91,7 @@ public void CommandLineParserTest( )
9191

9292
Assert.IsInstanceOfType( args[ 6 ], typeof( CommandlineValue ) );
9393
var arg6 = ( CommandlineValue )args[ 6 ];
94-
Assert.AreEqual( @"""positional 3\""", arg6.Text );
94+
Assert.AreEqual( @"positional 3\", arg6.Text );
9595
}
9696
}
9797
}

Ubiquity.CommandlineParsing.Monad/Grammar.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static Parser<IEnumerable<char>> BlockString( Parser<IEnumerable<char>> o
6767
return ( from start in openDelimiter
6868
from content in Parse.AnyChar.Except( closeDelimiter ).Many()
6969
from end in closeDelimiter
70-
select start.Concat( content ).Concat( end )
70+
select content
7171
).Token( );
7272
}
7373

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
2+
// Licensed under the MIT license. See the LICENSE.md file in the project root for full license information.
3+
4+
using System;
5+
using System.Linq;
6+
7+
namespace Ubiquity.CommandlineParsing
8+
{
9+
/// <summary>Provides environment extensions for handling platform differences</summary>
10+
public static class EnvironmentEx
11+
{
12+
/// <summary>Gets the raw unparsed command line (or as close to it as is possible for the given runtime/platform</summary>
13+
/// <remarks>
14+
/// <para>On Desktop .NET 4.7 and earlier <see cref="Environment.CommandLine"/> provides the full command line without any
15+
/// funky escape character processing. But the args passed to main or returned from <see cref="Environment.GetCommandLineArgs()"/>
16+
/// have extra escape character processing. This extra processing is very problematic as it is unique to .NET apps
17+
/// (C and C++ apps don't have this). If the command line `foo.exe "abc\de f\"` is processed with escaping then the
18+
/// result the app see is `abc\de f"` (Note the inclusion of the trailing quote character), which is clearly not the
19+
/// path the user intended to provide. This has always been an annoyance for .NET developers but was easily worked around
20+
/// by not using the args to Main() and instead getting at the full args via Environment.CommandLine and parsing that using
21+
/// whatever rules the app wants to support for args.</para>
22+
/// <para>Unfortunately with .NET Core things behave very differently. <see cref="Environment.CommandLine"/> is no longer
23+
/// the unprocessed command line and instead it is effectively a space delimited join of Environment.GetCommandLineArgs().
24+
/// Meaning that is has all the .NET Specific escaping applied and there is no platform independent means of getting at
25+
/// the unprocessed args.</para>
26+
/// <para>Thus, this implementation is forced to implement the only viable cross platform approach by doing a space delimited
27+
/// join of <see cref="Environment.GetCommandLineArgs()"/> with all it's wonky escaping rules.</para>
28+
/// </remarks>
29+
public static string CommandLine
30+
{
31+
get
32+
{
33+
// On .NET Core - no option exists to get the original command line, it isn't really possible to fully reverse
34+
// the escaping as the process is lossy. e.g. `"foo\"` -> `foo"`, and `foo\"`-> `foo"` with no way to know if
35+
// the opening quote was present. For a value with whitespace it can be inferred but otherwise it can't be known
36+
// which presents a problem as reversing the escaping could creates quotes that are unmatched.
37+
return string.Join( " ", Environment.GetCommandLineArgs( ).Skip( 1 ).Select( Requote ) );
38+
}
39+
}
40+
41+
private static string Requote( string arg )
42+
{
43+
if( arg.Any( Char.IsWhiteSpace ) )
44+
{
45+
return $"\"{arg}\"";
46+
}
47+
48+
return arg;
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)