PR Summary
Fix argument parsing for hyphen-prefixed tokens containing periods. Tokens like-foo.bar are now passed as a single argument instead of being incorrectly split into-foo and.bar.
PR Context
Fixes#6291
Also partially addresses the splatting issue described in#6360 (closed due to inactivity, not because it was fixed).
Note: This PR fixes the period (.) splitting issue in splatting, but doesnot fix the colon (:) splitting issue also mentioned in#6360. The colon case requires a different fix in NativeCommandParameterBinder.
The Problem
When passing unquoted arguments that start with a hyphen and contain a period, PowerShell incorrectly splits them at the first period:
functionOut-Argument {$Args }Out-Argument-foo.bar# Actual (before fix): arg[0]: -foo, arg[1]: .bar (incorrectly split)# Expected: arg[0]: -foo.bar (single argument)This behavior affects many real-world scenarios, particularly when calling external programs with flags like:
-DVERSION=1.2.3 (compiler defines)-std=c++20 (compiler standards)-fmodule-file=Hello=Hello.pcm (clang modules)--config=path/to.file (various CLI tools)
Committee Decision
ThePowerShell Committee reviewed this issue and confirmed it is a bug:
"@PowerShell/powershell-committee reviewed this, we believe the treatment of the period as an argument separator is a bug that should be fixed. A bucket 3 breaking change."
Before this fix:
# Simple function to echo argumentsfunctionOut-Argument {$i=0;$Args|ForEach-Object {"arg[$i]:$_";$i++ } }Out-Argument-foo.bar# arg[0]: -foo# arg[1]: .barOut-Argument-foo=bar.baz# arg[0]: -foo=bar# arg[1]: .baz# Even worse with splattingfunctionWrapper { cmd/c echo@Args }Wrapper-foo.bar# Output: -foo .bar (space inserted)After this fix:
Out-Argument-foo.bar# arg[0]: -foo.barOut-Argument-foo=bar.baz# arg[0]: -foo=bar.baz# Splatting now works correctlyfunctionWrapper { cmd/c echo@Args }Wrapper-foo.bar# Output: -foo.barTechnical Approach
Root Cause
InScanParameter() method oftokenizer.cs, when the tokenizer encounters a hyphen at the start of a token, it begins scanning for a parameter name. When it encounters a period (.), it terminates the parameter scan and leaves the remaining text (.bar) as a separate token.
This behavior was originally designed for expression mode where. indicates member access (e.g.,$obj.Property). However, in command mode, this special treatment of. causes unintended argument splitting.
Solution
Modified thecase '.' handling inScanParameter() to check if we're in command mode. When in command mode:
- The period is recognized as not being part of a valid parameter name
- The entire token (including the leading
- and everything after the.) is treated as a generic argument - The token is rescanned using
ScanGenericToken() which preserves it as a single unit
This approach mirrors the existing handling of quote characters (',") in the same method, which already use the same pattern to treat quoted content as arguments rather than parameters.
case'.':if(InCommandMode()){// Period is never part of a parameter name. Treat the token as an argument.// This handles cases like -foo.bar which should be a single argument.UngetChar();sb.Insert(0,_script[_tokenStart]);// Insert the '-' that we skipped.returnScanGenericToken(sb);}UngetChar();scanning=false;break;Backward Compatibility Analysis
Why This Change Is Safe
Parameter names cannot contain periods: PowerShell does not allow. in parameter names. Any attempt to declareparam($foo.bar) results in a parse error.
No intentional usage exists: Since-foo.bar cannot match any declared parameter, the current "split at period" behavior is purely accidental. No scripts intentionally rely on-foo.bar being interpreted as-foo with value.bar.
Consistent with user expectations: Users who write-foo.bar expect it to be a single argument (typically for external programs).
Colon syntax remains unchanged: The intentional-param:value syntax continues to work as expected.
Verified Backward Compatibility
The following scenarios were tested and continue to work correctly:
| Scenario | Status |
|---|
Normal parameter binding (-foo bar) | ✅ Works |
Colon syntax (-foo:bar) | ✅ Works |
Switch parameters (-Verbose) | ✅ Works |
Member access in expressions ($obj.foo) | ✅ Works |
Method calls ('hello'.ToUpper()) | ✅ Works |
Negative decimals (-3.14) | ✅ Works |
Range operator (-3..-1) | ✅ Works |
Double-hyphen arguments (--foo.bar) | ✅ Works (already worked) |
Potential Concerns
WG-Engine's Comment
WG-Engine commented:
"WG-Engine looked at this today and agree it is worth pursuing a bug fix for native commands. Attempting to fix this for commands in general, would be too risky."
This PR applies the fix toall commands (not just native commands) because:
Tokenization happens before command resolution: At the tokenizer level, PowerShell doesn't know whether the target is a native command, cmdlet, or function.
No practical difference: Since parameter names cannot contain periods, the "split at period" behavior never provides useful functionality for PowerShell commands either.
Splatting consistency: The fix ensures that$Args and@Args correctly preserve arguments regardless of whether they're ultimately passed to native commands or PowerShell functions.
Extensive testing: All backward compatibility scenarios have been verified with no regressions found.
The PowerShell team should evaluate whether this broader fix is acceptable or if a native-command-only approach is required.
PR Checklist
Test Coverage
Added 25 comprehensive test cases covering:
Basic Cases
-foo.bar - single period-foo=bar.baz - equals sign with period
Edge Cases: Multiple and Consecutive Periods
-foo..bar - consecutive periods-foo...bar - three consecutive periods-foo.bar.baz - multiple periods-foo=1.2.3.4 - IP-address-like pattern
Edge Cases: Leading and Trailing Periods
-.foo - leading period after hyphen-foo. - trailing period
Double Hyphen
--foo.bar - GNU-style long option (already worked, included for completeness)
Real-World Use Cases
-DVERSION=1.2.3 - compiler define-std=c++20 - compiler standard flag
Splatting
@Args with-foo.bar@Args with-foo=bar.baz
Native Commands
- Direct invocation with period-containing arguments
- Via splatting
Backward Compatibility
- Normal parameter binding with space
- Colon syntax (
-foo:bar) - Switch parameters
- Parameter with dot value (
-Path .txt) - Member access in expression mode
- Method calls
- Negative decimal numbers
- Range operator with negative numbers
Test Results
Context Hyphen-prefixed arguments should not be split at dot [+] Argument '-foo.bar' should be passed as a single argument [+] Argument '-foo=bar.baz' should be passed as a single argument [+] Argument '-foo..bar' with consecutive dots should be passed as a single argument [+] Argument '-foo...bar' with three consecutive dots should be passed as a single argument [+] Argument '-foo.bar.baz' with multiple dots should be passed as a single argument [+] Argument '-foo=1.2.3.4' with multiple dots in value should be passed as a single argument [+] Argument '-.foo' with leading dot should be passed as a single argument [+] Argument '-foo.' with trailing dot should be passed as a single argument [+] Argument '--foo.bar' with double hyphen should be passed as a single argument [+] Multiple hyphen-prefixed arguments with dots should each be single arguments [+] Compiler-style argument '-DVERSION=1.2.3' should be passed as a single argument [+] Compiler-style argument '-std=c++20' should be passed as a single argument [+] Splatting should preserve hyphen-prefixed arguments with dots [+] Splatting should preserve arguments with equals and dots [+] Native command should receive hyphen-prefixed argument with dot as single argument [+] Native command should receive argument with equals and dot as single argument [+] Native command via splatting should receive hyphen-prefixed argument with dot as single argument [+] Normal parameter binding with space should still work [+] Parameter binding with colon syntax should still work [+] Switch parameter should not be affected [+] Parameter with dot value using space should still work [+] Member access in expression mode should still work [+] Method call in expression mode should still work [+] Negative decimal number should still work [+] Range operator with negative numbers should still workTests Passed: 25/25
Files Changed
src/System.Management.Automation/engine/parser/tokenizer.cs - Modified period handling inScanParameter()test/powershell/Language/Parser/Parser.Tests.ps1 - Added comprehensive test coverage
PR Summary
Fix argument parsing for hyphen-prefixed tokens containing periods. Tokens like
-foo.barare now passed as a single argument instead of being incorrectly split into-fooand.bar.PR Context
Fixes#6291
Also partially addresses the splatting issue described in#6360 (closed due to inactivity, not because it was fixed).
Note: This PR fixes the period (
.) splitting issue in splatting, but doesnot fix the colon (:) splitting issue also mentioned in#6360. The colon case requires a different fix in NativeCommandParameterBinder.The Problem
When passing unquoted arguments that start with a hyphen and contain a period, PowerShell incorrectly splits them at the first period:
This behavior affects many real-world scenarios, particularly when calling external programs with flags like:
-DVERSION=1.2.3(compiler defines)-std=c++20(compiler standards)-fmodule-file=Hello=Hello.pcm(clang modules)--config=path/to.file(various CLI tools)Committee Decision
ThePowerShell Committee reviewed this issue and confirmed it is a bug:
Before this fix:
After this fix:
Technical Approach
Root Cause
In
ScanParameter()method oftokenizer.cs, when the tokenizer encounters a hyphen at the start of a token, it begins scanning for a parameter name. When it encounters a period (.), it terminates the parameter scan and leaves the remaining text (.bar) as a separate token.This behavior was originally designed for expression mode where
.indicates member access (e.g.,$obj.Property). However, in command mode, this special treatment of.causes unintended argument splitting.Solution
Modified the
case '.'handling inScanParameter()to check if we're in command mode. When in command mode:-and everything after the.) is treated as a generic argumentScanGenericToken()which preserves it as a single unitThis approach mirrors the existing handling of quote characters (
',") in the same method, which already use the same pattern to treat quoted content as arguments rather than parameters.Backward Compatibility Analysis
Why This Change Is Safe
Parameter names cannot contain periods: PowerShell does not allow
.in parameter names. Any attempt to declareparam($foo.bar)results in a parse error.No intentional usage exists: Since
-foo.barcannot match any declared parameter, the current "split at period" behavior is purely accidental. No scripts intentionally rely on-foo.barbeing interpreted as-foowith value.bar.Consistent with user expectations: Users who write
-foo.barexpect it to be a single argument (typically for external programs).Colon syntax remains unchanged: The intentional
-param:valuesyntax continues to work as expected.Verified Backward Compatibility
The following scenarios were tested and continue to work correctly:
-foo bar)-foo:bar)-Verbose)$obj.foo)'hello'.ToUpper())-3.14)-3..-1)--foo.bar)Potential Concerns
WG-Engine's Comment
WG-Engine commented:
This PR applies the fix toall commands (not just native commands) because:
Tokenization happens before command resolution: At the tokenizer level, PowerShell doesn't know whether the target is a native command, cmdlet, or function.
No practical difference: Since parameter names cannot contain periods, the "split at period" behavior never provides useful functionality for PowerShell commands either.
Splatting consistency: The fix ensures that
$Argsand@Argscorrectly preserve arguments regardless of whether they're ultimately passed to native commands or PowerShell functions.Extensive testing: All backward compatibility scenarios have been verified with no regressions found.
The PowerShell team should evaluate whether this broader fix is acceptable or if a native-command-only approach is required.
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright headerTest Coverage
Added 25 comprehensive test cases covering:
Basic Cases
-foo.bar- single period-foo=bar.baz- equals sign with periodEdge Cases: Multiple and Consecutive Periods
-foo..bar- consecutive periods-foo...bar- three consecutive periods-foo.bar.baz- multiple periods-foo=1.2.3.4- IP-address-like patternEdge Cases: Leading and Trailing Periods
-.foo- leading period after hyphen-foo.- trailing periodDouble Hyphen
--foo.bar- GNU-style long option (already worked, included for completeness)Real-World Use Cases
-DVERSION=1.2.3- compiler define-std=c++20- compiler standard flagSplatting
@Argswith-foo.bar@Argswith-foo=bar.bazNative Commands
Backward Compatibility
-foo:bar)-Path .txt)Test Results
Files Changed
src/System.Management.Automation/engine/parser/tokenizer.cs- Modified period handling inScanParameter()test/powershell/Language/Parser/Parser.Tests.ps1- Added comprehensive test coverage