Skip to content

Conversation

@yotsuda
Copy link
Contributor

@yotsuda yotsuda commented Nov 24, 2025

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 does not 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:

function Out-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

The PowerShell 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 arguments
function Out-Argument { $i = 0; $Args | ForEach-Object { "arg[$i]: $_"; $i++ } }

Out-Argument -foo.bar
# arg[0]: -foo
# arg[1]: .bar

Out-Argument -foo=bar.baz
# arg[0]: -foo=bar
# arg[1]: .baz

# Even worse with splatting
function Wrapper { cmd /c echo @Args }
Wrapper -foo.bar
# Output: -foo .bar (space inserted)

After this fix:

Out-Argument -foo.bar
# arg[0]: -foo.bar

Out-Argument -foo=bar.baz
# arg[0]: -foo=bar.baz

# Splatting now works correctly
function Wrapper { cmd /c echo @Args }
Wrapper -foo.bar
# Output: -foo.bar

Technical Approach

Root Cause

In ScanParameter() method of tokenizer.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 in ScanParameter() to check if we're in command mode. When in command mode:

  1. The period is recognized as not being part of a valid parameter name
  2. The entire token (including the leading - and everything after the .) is treated as a generic argument
  3. 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.
        return ScanGenericToken(sb);
    }

    UngetChar();
    scanning = false;
    break;

Backward Compatibility Analysis

Why This Change Is Safe

  1. Parameter names cannot contain periods: PowerShell does not allow . in parameter names. Any attempt to declare param($foo.bar) results in a parse error.

  2. 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.

  3. Consistent with user expectations: Users who write -foo.bar expect it to be a single argument (typically for external programs).

  4. 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 to all commands (not just native commands) because:

  1. Tokenization happens before command resolution: At the tokenizer level, PowerShell doesn't know whether the target is a native command, cmdlet, or function.

  2. No practical difference: Since parameter names cannot contain periods, the "split at period" behavior never provides useful functionality for PowerShell commands either.

  3. 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.

  4. 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 work

Tests Passed: 25/25

Files Changed

  • src/System.Management.Automation/engine/parser/tokenizer.cs - Modified period handling in ScanParameter()
  • test/powershell/Language/Parser/Parser.Tests.ps1 - Added comprehensive test coverage

When passing arguments like -foo.bar or -foo=bar.baz, PowerShell
incorrectly splits them at the first period. This fix modifies the
tokenizer to treat such tokens as single arguments in command mode.

Fixes PowerShell#6291
@yotsuda yotsuda force-pushed the fix-issue-6291-hyphen-dot-parsing branch from b179b78 to 6423fe1 Compare November 24, 2025 10:53
@iSazonov iSazonov added the CL-Engine Indicates that a PR should be marked as an engine change in the Change Log label Nov 24, 2025
@iSazonov iSazonov requested a review from Copilot November 24, 2025 11:05
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a tokenizer bug where hyphen-prefixed arguments containing periods (e.g., -foo.bar, -DVERSION=1.2.3) were incorrectly split at the period character. The fix ensures such arguments are treated as single tokens in command mode, which is critical for passing compiler flags and other command-line arguments to native commands.

Key Changes

  • Modified the ScanParameter() method in tokenizer.cs to handle periods in command mode by treating the entire token as a generic argument rather than attempting to parse it as a parameter name
  • Added comprehensive test coverage with 25 test cases covering basic functionality, edge cases, real-world scenarios (compiler flags), splatting, native commands, and backward compatibility

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/System.Management.Automation/engine/parser/tokenizer.cs Moved period handling from the generic case statement to a dedicated case with command mode detection, mirroring the existing pattern used for quote characters
test/powershell/Language/Parser/Parser.Tests.ps1 Added comprehensive test context "Hyphen-prefixed arguments should not be split at dot" with 25 test cases validating the fix and ensuring backward compatibility

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@microsoft-github-policy-service microsoft-github-policy-service bot added the Review - Needed The PR is being reviewed label Dec 4, 2025
@microsoft-github-policy-service
Copy link
Contributor

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-Engine Indicates that a PR should be marked as an engine change in the Change Log Review - Needed The PR is being reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parameter parsing/passing: an unquoted argument that starts with a "-" (hyphen) is broken in two at the first "." (period)

2 participants