Skip to content

Command line arguments with a dollar sign #4024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Tibblez opened this issue Jun 15, 2017 · 15 comments
Closed

Command line arguments with a dollar sign #4024

Tibblez opened this issue Jun 15, 2017 · 15 comments
Assignees
Labels
Breaking-Change breaking change that may affect users Committee-Reviewed PS-Committee has reviewed this and made a decision Resolution-By Design The reported behavior is by design. WG-Interactive-Console the console experience
Milestone

Comments

@Tibblez
Copy link

Tibblez commented Jun 15, 2017

Command line arguments sent to a file do not send in full if the argument contains a dollar sign. I have tried every combination of escape character and quote configuration that I can think of. I am not certain if this is specific to the dollar sign but I was able to use single quotes with an exclamation mark in the argument and that worked fine.

I was not able to find any explanation for this in other open issues or the known issues but I apologize if I'm missing something obvious here.

Steps to reproduce

Create a new ps1 file called test.ps1 with this snippet.

param(
[string]$foo
)
write-host $foo

Then calling it from the command line

powershell ./test.ps1 -foo 'Before$After'

Expected behavior

Before$After

Actual behavior

Before

Environment data

> $PSVersionTable
Name                           Value                                                                           
----                           -----                                                                           
PSVersion                      6.0.0-beta                                                                      
PSEdition                      Core                                                                            
BuildVersion                   3.0.0.0                                                                         
CLRVersion                                                                                                     
GitCommitId                    v6.0.0-beta.2                                                                   
OS                             Linux 3.10.0-514.el7.x86_64 #1 SMP Tue Nov 22 16:42:41 UTC 2016                 
Platform                       Unix                                                                            
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}                                                         
PSRemotingProtocolVersion      2.3                                                                             
SerializationVersion           1.1.0.1                                                                         
WSManStackVersion              3.0                                                                             

@SteveL-MSFT SteveL-MSFT added the Resolution-Answered The question is answered. label Jun 15, 2017
@SteveL-MSFT
Copy link
Member

I believe this is by design. Solution is to use the backtick to escape:

powershell ./test.ps1 -foo 'Before`$After'

@lzybkr
Copy link
Contributor

lzybkr commented Jun 16, 2017

I'm not sure it's by design.

Compare:

#1 PS>  ./test.ps1 -foo 'Before$After'
Before$After
#2 PS> echoargs ./test.ps1 -foo 'Before$After'
arg 0: <./test.ps1>
arg 1: <-foo>
arg 2: <Before$After>

CommandLine:
"EchoArgs.exe" ./test.ps1 -foo Before$After

#3 PS> powershell -file  ./test.ps1 -foo 'Before$After'
Before$After
#4 PS> powershell -command  ./test.ps1 -foo 'Before$After'
Before

Notice in 1, 2, and 3, $After is preserved. Only in 4 does $After disappear.

I would say we need some more digging before calling this by design - but it does provide a cleaner workaround - use -file.

@mklement0
Copy link
Contributor

mklement0 commented Jun 20, 2017

To summarize the difference in behavior:

  • -File treats its arguments as literal strings, without further interpretation by PowerShell.

  • -Command interprets its arguments as if they'd been specified inside PowerShell.

In the case at hand:

  • With -File, Before$After is retained as-is, as a literal string.

  • With -Command, Before$After is interpreted as an expandable string - as if you had passed it as an argument in argument mode from within PowerShell - and - given that variable $After is not defined - it expands to just Before.

@SteveL-MSFT SteveL-MSFT added this to the 6.0.0-HighPriority milestone Jun 20, 2017
@SteveL-MSFT SteveL-MSFT self-assigned this Jun 20, 2017
@SteveL-MSFT
Copy link
Member

Getting -File for .ps1 and -Command to parse args the same as -Command (so that variables are string literals) is working. Need to figure out how to get scripts without .ps1 extension to work. With the change I'm proposing, "Before$After" needs to escape the $ otherwise expectation is that PowerShell interprets it as the value of the $After variable.

@SteveL-MSFT SteveL-MSFT added the Breaking-Change breaking change that may affect users label Jun 27, 2017
@mklement0
Copy link
Contributor

mklement0 commented Jun 28, 2017

@SteveL-MSFT: I think it's vital that we align the behavior of -Command with that of -c in POSIX-like shells, which, in short, means:

  • The first argument following -Command should be interpreted like a script.

    • If that script needs access to the subsequent arguments, it must use $args or a param() statement to access them.

      • Note: POSIX-like shells bind the first subsequent argument to $0 (rather than $1), whose PowerShell equivalent would be $MyInvocation.MyCommand.Name, but I think it's sensible not to do that in PowerShell.
    • Note that this is a fundamental, breaking change from how -Command functions today, which basically reassembles a PowerShell command line from all the arguments by mere string concatenation (with spaces) and then invokes the result based on PowerShell rules.

  • Any subsequent arguments should be treated as literals - just as with -File.

In other words: what follows an ad-hoc script (-Command) or a script file (-File, or by default), are arguments to pass to that ad-hoc script/script file, and they should be processed the same:

  • as literals (after potential up-front expansion, depending on the calling shell) - except that $true and $false must also be recognized as Boolean literals (which is currently not the case - see Executing powershell script with bool parameter doesnt work #4036), just as, say 22 is recognized as an [int], if bound to a parameter of that type.

Conversely, anything that should be interpreted according to PowerShell-internal rules, must go directly into the ad-hoc script passed to -Command.

To illustrate the proposed difference:

# Already works this way.
> powershell -noprofile -command '"$HOME"'
/home/jdoe

# This is how it *should* work (currently breaks).
> powershell -noprofile -command '$args' '$HOME'
$HOME 

@SteveL-MSFT SteveL-MSFT added the Review - Committee The PR/Issue needs a review from the PowerShell Committee label Jun 28, 2017
@SteveL-MSFT SteveL-MSFT added Committee-Reviewed PS-Committee has reviewed this and made a decision Resolution-By Design The reported behavior is by design. and removed Review - Committee The PR/Issue needs a review from the PowerShell Committee labels Jun 29, 2017
@SteveL-MSFT
Copy link
Member

@PowerShell/powershell-committee reviewed this and although we agree the argument handling isn't technically correct, it has been this way since inception and would be a big breaking change without significant benefit, so the current behavior is 'by design'.

@mklement0
Copy link
Contributor

mklement0 commented Jun 30, 2017

Thanks for letting me know, @SteveL-MSFT.

I sincerely wish you revisited this issue, however:

While I get that breaking changes are very problematic, I think:

  • that the current behavior is fundamentally broken in itself - not just because it fundamentally differs from the behavior of POSIX-like shells.

  • that this broken behavior will become more evident - and be a perennial pain point - when exposed to the more command-line-savvy, quoting-aware Unix crowd.

  • the switch from -Command to -File already was a big breaking change; v6 is an opportunity to get all aspects of the CLI right.

Consider the following calling-from-bash examples, which highlight the fundamental problem (I'm leaving the aspect of how the first argument should be treated as a mini-script to which the remaining arguments should be passed via $Args aside for now):

# Breaks, because literal `don't` is interpreted as a bareword by PowerShell.
$ powershell -noprofile -command Write-Output "don't" # !! BREAKS
The string is missing the terminator: '. 

#'# With a *literal*, you can work around that, but in addition to the
# outer quoting - WHICH SHOULD BE ENOUGH - it requires TWO EXTRA LAYERS OF 
# QUOTING: 1 for bash (\`) and 1 for PowerShell (the ` passed through).
$ powershell -noprofile -command Write-Output "don\`'t" # works, but extremely cumbersome.
don't

#'# Using a NOT-KNOWN-IN-ADVANCE VALUE is SIMILARLY CUMBERSOME AND COMPLEX:
$ v="don't"; powershell -noprofile -command Write-Output "'${v//\'/\'\'}'" # works, but extremely cumbersome.
don't

Cramming it into the first argument is an option, but equally cumbersome:

$ powershell -noprofile -command "Write-Output  \"don't\""
don't

$ v="don't"; powershell -noprofile -command "Write-Output  '${v//\'/\'\'}'"
don't

PowerShell's current reassemble-all-arguments-into-a-command-line-and-then-reinterpret-it approach severely hampers the ability to pass arguments as-is, as data to the command.

Interpretation as PS source code should be limited to the first argument - the "mini-script".

Separately (as previously stated), so as to align with POSIX-like shells, the remaining arguments should be passed via $Args, which would also provide consistency with how -File handles its arguments; applied to the example above:

# WISHFUL THINKING; note how $v is passes via $Args
$ v="don't"; powershell -command 'Write-Output $Args[0]' "$v"
don't

@SteveL-MSFT
Copy link
Member

@mklement0 as part of the fix for the other related issue #4036 I intend to also make a doc update to hopefully clarify this for the user on the differences between -File and -Command. The general statement is that the user needs to be aware of how the "outer shell" handles escaping (Bash in your examples) and what gets passed to PowerShell and what PowerShell passes to the command. I think that if this behavior becomes a big customer sticking point, we could consider adding another switch that affects the arg passing behavior, but it was considered a bigger impactful breaking change than the switch of -File and -Command where you would at least get a reasonable error message.

@mklement0
Copy link
Contributor

@SteveL-MSFT: I'm glad to hear it's getting documented.

it was considered a bigger impactful breaking change than the switch of -File and -Command

Undoubtedly.

if this behavior becomes a big customer sticking point, we could consider adding another switch that affects the arg passing behavior

Just be aware that you're closing the door on allowing -c to work the same way as in POSIX-like shells; while -e would be the next best thing, the existence of -Command that works very differently will sow confusion.

The general statement is that the user needs to be aware of how the "outer shell" handles escaping

Absolutely, that's a must in any scenario.

But requiring double escaping for things that should be passed as literals (e.g., "don\'t"`) is both cumbersome and confusing.

To repeat myself briefly, interpreting individual arguments passed to a command as part of the command's source code subverts fundamental notions of how arguments are passed in the Unix world.

The saving grace is perhaps that wanting to pass literal data arguments to a single command string that functions as an ad-hoc script is not that common (e.g., sh -c 'echo "$1"' - '$HOME' printing $HOME).

In the absence of passing such arguments, if the current behavior is retained, I suggest documenting that the best use of -Command is to pass the entire command as a single string for conceptual clarity; e.g., instead of:

$ powershell -noprofile -command 'Write-Output' '`$HOME'  # confusing: 2 args 

using:

$ powershell -noprofile -command 'Write-Output `$HOME' # better: 1 arg

@SteveL-MSFT
Copy link
Member

Cc @BrucePay

@mklement0
Copy link
Contributor

@SteveL-MSFT:

When it comes to documenting the current behavior of -Command perhaps the following summary can be helpful to readers with a Unix background:

  • -Command only accepts an ad-hoc PowerShell script (which may be a single statement) and has no mechanism for passing arguments to that script.

  • While you can technically pass additional arguments after the 1st -Command argument, they simply become part of the ad-hoc script by string concatenation with spaces.

  • Thus, in the absence of argument-passing, shell variable-based values to be used in the ad-hoc script must be "baked into" it and therefore require additional quoting (escaping) by PowerShell rules in order to be treated as literals.

  • For conceptual clarity and to avoid additional quoting headaches, it is easier to always pass the entire ad-hoc script as a single argument following -Command - at least in the Unix world.

    • On Windows, the scarcity of shell metacharacters and $ and ' having no special meaning to cmd.exe makes this less of a problem.

@SteveL-MSFT
Copy link
Member

@mklement0 please take a look at my PR for the doc update and add any suggestions on language MicrosoftDocs/PowerShell-Docs#1430

@mklement0
Copy link
Contributor

mklement0 commented Jul 8, 2017

Thank you, @SteveL-MSFT.

To [half-mis-]quote Dame Edna, I mean the following in a caring way:

  • Like most PowerShell documentation, the one you link to falls somewhere along the spectrum of light introduction to confusing to incorrect/misleading.

  • I honestly wouldn't know how to fit the subtleties presented here into the existing format, so I trust you to do the right thing with the information provided here.

Truthfully, I think PowerShell's documentation needs a serious overhaul, and my potential contribution to this particular help topic would feel like rearranging the deck chairs on the Titanic.

@SteveL-MSFT
Copy link
Member

@mklement0 I don't think you should feel like you are constrained to the style that exists in the current docs. They were written years ago. I prefer to avoid a lot of detail as I feel people tend not to read that much content. I'd rather add more examples that show the differences. If you have thoughts on some specific ones, please share :)

@mklement0
Copy link
Contributor

mklement0 commented Jul 10, 2017

@SteveL-MSFT: I appreciate the invitation, but it feels overwhelming to me.

A few more meta observations:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking-Change breaking change that may affect users Committee-Reviewed PS-Committee has reviewed this and made a decision Resolution-By Design The reported behavior is by design. WG-Interactive-Console the console experience
Projects
None yet
Development

No branches or pull requests

4 participants