I have a very simple .ps1-script:
$prename = Read-Host 'What is your prename?'
$lastname = Read-Host 'What is your lastname?'
.\migration.exe -name=$prename -password=$lastname
Regardless of what the user enters, $prename and $lastname don't get passed to migration.exe - they are empty!
What am I doing wrong?
$prename and $lastname don't get passed to migration.exe - they are empty!
These variable references are not empty; they're - unexpectedly - not expanded (interpolated), as of PowerShell 7.1; that is, external program .\migration unexpectedly receives the following arguments verbatim: -name=$prename and -password=$lastname.
This should be considered a bug - see GitHub issue #14587.
The workaround is to use explicit string interpolation ("..."):
.\migration.exe -name="$prename" -password="$lastname"
Note: On Windows, the partial quoting is not guaranteed to be passed through as-is to the target program; instead, PowerShell performs its own parsing first and then performs ("-based) re-quoting on demand behind the scenes, depending on whether the resulting argument contains whitespace. For instance, if $prename's value is foo,
verbatim -name=foo is passed to migration.exe; if the value is foo bar,
verbatim "-name=foo bar" is passed - which the target program may or may not process properly; if not, you'll have to use --%, the stop-parsing symbol operator - see this answer.
You are passing the objects $prename and $lastname. You need expand the variables so you pass the value in the variables.
.\migration.exe -name=$($prename) -password=$($lastname)
Related
I'm learning PowerShell so please forgive (what I'm sure is) a simple question.
I'm used to coding BATCH scripts and if I wanted to merge %USERDOMAIN% and %USERNAME% I would:
set zFullUsername=%USERDOMAIN%\%USERNAME%
echo %zFullUsername%
How can I do the same in PowerShell?
Thank you for your time.
On a supported Operating System, I wouldn't even bother with environment variables for this:
$zFullUsername = whoami
Then just access it as required:
$zFullUsername
In PowerShell, you can access environment variables in a few different ways. The way I recommend is to use the $env:VAR variable to access them.
$user = $env:USERNAME
$domain = $env:USERDOMAIN
echo "$domain\$user"
Note: \ is not an escape character in the PowerShell parser, ` is.
Similarly to rendering the echo command (echo is an alias of Write-Output btw) you can create a username variable like so:
$fullUserName = "$domain\$user"
Or you can skip right to creating $fullUserName straight from the environment variables:
$fullUserName = "${env:USERDOMAIN}\${env:USERNAME}"
Note: When variables have non-alphanumeric characters in them, the ${} sequence tells PowerShell everything between the ${} is part of the variable name to expand.
It seems the : in $env:VAR is actually an exception to this rule, as"Username: $env:USERNAME" does render correctly. So the ${} sequence above is optional.
To avoid confusion when trying to apply this answer in other areas, if you needed to insert the value of an object property or some other expression within a string itself, you would use a sub-expression within the string instead, which is the $() sequence:
$someVar = "Name: $($someObject.Name)"
When using either ${} or $(), whitespace is not allowed to pad the outer {} or ().
I try to concatenate string to construct a path:
$SourceDirectoryPath = $(System.DefaultWorkingDirectory) + "/solution/project/bin/Debug"
$TargetFilePath = $(System.DefaultWorkingDirectory) + "/solution/project/bin/Debug/" + $(Release.ReleaseName) +$(Release.EnvironmentName)
but instead of getting string concatenated I get error for the second line:
d:\a\r1\a : The term 'd:\a\r1\a' is not recognized as the name of a
cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the path
is correct and try again. At
D:\a_temp\9de874c9-3acd-4a19-a4dd-763074d38e40.ps1:2 char:25
where obviously d:\a\r1\a is a $(System.DefaultWorkingDirectory) but why it throws this error instead of just concatenating the string?
tl;dr
It is Azure that expands $(System.DefaultWorkingDirectory) before PowerShell sees the resulting commands; if the expanded $(...) value is to be seen as a string by PowerShell, it must be enclosed in quotes ('$(...)'):
Using $(...) (Azure macro syntax) embeds the Azure variable's verbatim value in the command text that PowerShell ends up interpreting.
Note: Azure's macro syntax - which is evaluated before PowerShell sees the resulting command text - is not to be confused with PowerShell's own subexpression operator, $(...).
For string values this means that you situationally have to surround the macro with quotes in order to make it work syntactically in PowerShell code, for which '...'-quoting (single-quoting) is best: '$(System.DefaultWorkingDirectory)'
Shayki Abramczyk's answer provides an effective solution, but let me provide some background information:
The variable expansion (substitution) that Azure performs via macro syntax ($(...)) functions like a preprocessor: it replaces the referenced variable with its verbatim value.
You need to make sure that this verbatim value works syntactically in the context of the target command.
As currently written:
$SourceDirectoryPath = $(System.DefaultWorkingDirectory) + "/solution/project/bin/Debug"
turns into the following command seen by PowerShell, assuming that the value of Azure property System.DefaultWorkingDirectory is d:\a\r1\a:
$SourceDirectoryPath = d:\a\r1\a + "/solution/project/bin/Debug"
This is a broken PowerShell command, because d:\a\r1\a - due to lack of quoting - is interpreted as a command name or path; that is, an attempt is made to execute putative executable d:\a\r1\a - see about_Parsing.
Therefore, in order for PowerShell to recognize the Azure-expanded value d:\a\r1\a as a string, you need to quote it - see about_Quoting_Rules.
Since the expanded-by-Azure value needs no further interpolation, single quotes are the best choice (for both operands, actually):
$SourceDirectoryPath = '$(System.DefaultWorkingDirectory)' + '/solution/project/bin/Debug'
In fact, you don't need string concatenation (+) at all in your case:
$SourceDirectoryPath = '$(System.DefaultWorkingDirectory)/solution/project/bin/Debug'
You could even combine that with expandable PowerShell strings ("..."), as long as the Azure-expanded value doesn't contain $-prefixed tokens that PowerShell could end up interpreting (unless that is your (unusual) intent).
One caveat re something like "$(System.DefaultWorkingDirectory)/$projectRoot/bin/Debug" (mixing an Azure-expanded value with a PowerShell variable reference) is that Azure's macro syntax ($(...)) looks the same as PowerShell's own subexpression operator, which is typically - but not exclusively - used in order to embed expressions in expandable strings (e.g., in pure PowerShell code, "1 + 1 equals $(1 + 1)").
As of this writing, the Define variables Azure help topic doesn't spell it out, but based on the official comment in a GitHub docs issue, ambiguity is avoided as follows:
There is no escape mechanism; instead, $(...) constructs that do not refer to Azure variables are left unchanged and therefore passed through to PowerShell.
In the typical case, PowerShell expressions will not look like an Azure variable reference (e.g, $($foo.bar) rather than $(foo.bar)), though hypothetically there can be ambiguity: $(hostname), which is a valid PowerShell subexpression, could be preempted by Azure if a hostname Azure variable were defined.
In such a corner case, the solution is to avoid use of an inline script and instead place the code in an external script file.
You need to add quotes " " in the variables:
$SourceDirectoryPath = "$(System.DefaultWorkingDirectory)" + "/solution/project/bin/Debug"
$TargetFilePath = "$(System.DefaultWorkingDirectory)" + "/solution/project/bin/Debug/" + "$(Release.ReleaseName)" + "$(Release.EnvironmentName)"
This should work as well. $( ) outside of doublequotes would only be used to combine two or more statements. Most people don't even know about it.
This is actually incorrect. I didn't know Azure Pipeline syntax. It just shows how confusing combining both Powershell and Azure Pipeline can be. This would work if $System were a Powershell object, not an Azure macro.
$SourceDirectoryPath = $System.DefaultWorkingDirectory + '/solution/project/bin/Debug'
So I am using the kind of buggy Sapien powershell studio to make a powershell driven GUI application, and I am attempting to perform an ADSI query.
$nameOfDeviceInput is a System.Windows.Forms.TextBox
On one form, I have the following function:
$buttonPerformAction_Click={
if (FindInAD($nameOfDeviceInput.Text).Count -gt 0)
{
$buttonPerformAction.BackColor = 'Red'
$buttonPerformAction.Text = "System already exists in AD with that name. Try another name"
return
}
.....
}
On the "main" form, I have the function FindInAD
function FindInAd($nameOfSystem)
{
Write-Host "seeking system" $nameOfSystem
([adsisearcher]"(CN=$nameOfSystem)").FindAll()
}
FindInAd() is failing because for whatever reason, $nameOfSystem is set to 1, and if I don't explicitly cast it as a string, it gets implicitly cast to Int32 (obviously)
I have tried the following:
Fully qualifying the textbox input by notating the form it belongs to ( $adObjectModifier )
$buttonPerformAction_Click={
if (FindInAD($adObjectModifier.$nameOfDeviceInput.Text).Count -gt 0)
{
$buttonPerformAction.BackColor = 'Red'
$buttonPerformAction.Text = "System already exists in AD with that name. Try another name"
return
}
.....
}
Explicitly casting the $nameOfSystem parameter as a type of [string]
function FindInAd([string]$nameOfSystem)
{
Write-Host "seeking system" $nameOfSystem
([adsisearcher]"(CN=$nameOfSystem)").FindAll()
}
Passing a raw string into FindInAD from the AdObjectModifier form.
....
if (FindInAD("Test").Count -gt 0)
....
There is nothing else on the output pipeline at the time, (at least not from me) in between the method invocation. It is EventHandler > Function Call with String parameter
Why are the strings I'm passing getting changed to a digit???
EDIT: I think my passed parameter is being automatically replaced with the resulting boolean somehow, but this doesn't make any sense to me....
Your have a syntax problem:
FindInAD($nameOfDeviceInput.Text).Count # WRONG
Note: Wrong in this context means: the syntax is formally valid, but doesn't do what you expect - see the bottom section.
It should be:
(FindInAD $nameOfDeviceInput.Text).Count
PowerShell commands - functions, cmdlets, scripts and external programs - are invoked like shell commands - foo arg1 arg2 - and not like C# methods - foo('arg1', 'arg2').
That is:
Do not put (...) around the list of arguments.
However, you do need (...) around the call as a whole if you want a command call to participate in an expression, as shown above with the access to property .Count - see this answer for more information.
Separate arguments with spaces, both from each other and from the command name - do not use ,
, between arguments functions differently: It constructs an array that is passed as a single argument - see below.
You may pass simple strings (ones that contain neither spaces nor PowerShell metacharacters such as ; or &) as barewords; that is, quoting them is optional; e.g., instead of foo 'bar', you can call foo bar - see this answer for how PowerShell parses unquoted command arguments.
Also, if a target function or script has explicitly declared parameters (which binary cmdlets invariably do), such as -bar and -baz, you can pass your values as named arguments, i.e. by prepending them with the target parameter name; doing so is good practice in scripts: foo -bar arg1 -baz arg2
By contrast, calling methods of objects uses the syntax familiar from regular programming languages such as C# ($obj.foo('arg1', 'arg2'))
This difference relates two PowerShell's two fundamental parsing modes, explained in detail in this answer:
Commands are parsed in argument mode - as in shells.
Method calls and operator-based expressions are parsed in expression mode - as in regular programming languages.
These modes are required in order to allow PowerShell serve double duty: as a shell on the one hand, and as a scripting (programming) language on the other.
PowerShell can help you avoid this syntax problem:
Note that the problem isn't that using method syntax to call a command is invalid syntax, but that it doesn't work as intended, which can be difficult to diagnose.
In short: When you call command foo as foo('foo', 'bar'), ('foo', 'bar')is a 2-element array, which is then passed to foo as a single argument.
To prevent the problem to begin with, you can set Set-StrictMode to -Version 2 or higher, which makes PowerShell report an error if you accidentally use method syntax when calling a command:
# Turn on the check for accidental method syntax.
# Note: This also turns on ADDITIONAL checks - see below.
Set-StrictMode -Version 2
# This call now produces an ERROR, because the proper syntax would be:
# foo 'a' 'b'
foo('a', 'b')
Caveats:
Set-StrictMode -Version 2 comprises additional strictness checks that you must then also conform to, notably:
You must not reference non-existent variables.
You must not reference non-existent properties; see GitHub issue #2798 for an associated pitfall in connection with PowerShell's unified handling of scalars and collections.
An error is reported only for pseudo method calls with multiple arguments (e.g.,
foo('bar', 'baz')), not with only one; e.g., foo('bar') is accepted, because the single-argument case generally still (accidentally) works.
The errors reported for strictness violations are statement-terminating errors: that is, they only terminate the statement at hand, but by default the script continues; to ensure that overall execution aborts - on any type of error - you'd have to set
$ErrorActionPreference = 'Stop' at the start of your code. See this answer for more information.
As for what you tried:
FindInAD($nameOfDeviceInput.Text).Count
is the same as:
FindInAD ($nameOfDeviceInput.Text).Count
That is, the result of expression ($nameOfDeviceInput.Text).Count is passed as an argument to function FindInAD.
I have a script with mandatory parameters which we use to install some SQL components including user name and passwords like below:
param(
[Parameter(Mandatory=$True,HelpMessage="SQL Server password")]
[ValidateNotNullOrEmpty()]
[string] $SqlServerPassword
)
So when a user runs this script, he/she will need to include the -SqlServerPassword 'SpecialCharacters' variable string. I know best practice is to place the string inside a single quote, but its been a hard path training some of our installation managers and it messes up because our password vault includes special characters which without single quotes causes issues.
How can I re-write the above to ensure that even if the user passes the password without it being in single quotes, that it will be in single quotes? Thanks!
What you're asking for cannot be done, if the string is to be passed as an argument, because that would require deactivating the command-line parser - the very mechanism that recognizes individual arguments, evaluates variable references and subexpressions contained in them, and binds them to parameters.
With a limited set of special characters you could ignore the value bound by the parser and manually parse $MyInvocation.Line, the raw command line, but not only is that ill-advised, it would break with characters such as | and ;
However, you can achieve what you want via an interactive prompt.
While you also get such a prompt with your code if the user happens not to pass a -SqlServerPassword argument on the command line, it doesn't prevent potentially incorrect command-line use.
(Also, this automatic prompting is not user-friendly and has quirks, such as not being able to start a value with !).
If feasible, you could require users to enter the value interactively (and not also allow passing a value as an argument) using Read-Host, in which case quoting need not - and must not - be used:
param(
# Do NOT define $SqlServerPassword as a parameter (but define others)
)
# Prompt the user for the password.
# The input is invariably treated as a literal.
Write-Host "Please enter the SQL Server password:"
[string] $SqlServerPassword = Read-Host
# Validate the user's input.
if (-not $SqlServerPassword) { Throw 'Aborted.' }
# ...
Note: Read-Host has a -Prompt parameter, which accepts a prompt string directly, but if you were to use it, you wouldn't be able to enter values that start with !; therefore, the prompt was written with a separate Write-Host statement above.
Note: A summary of this question has since been posted at the PowerShell GitHub repository, since superseded by this more comprehensive issue.
Arguments passed to a command in PowerShell are parsed in argument mode (as opposed to expression mode - see Get-Help about_Parsing).
Conveniently, (double-)quoting arguments that do not contain whitespace or metacharacters is usually optional, even when these arguments involve variable references (e.g. $HOME\sub) or subexpressions (e.g., version=$($PsVersionTable.PsVersion).
For the most part, such unquoted arguments are treated as if they were double-quoted strings, and the usual string-interpolation rules apply (except that metacharacters such as , need escaping).
I've tried to summarize the parsing rules for unquoted tokens in argument mode in this answer, but there are curious edge cases:
Specifically (as of Windows PowerShell v5.1), why is the unquoted argument token in each of the following commands NOT recognized as a single, expandable string, and results in 2 arguments getting passed (with the variable reference / subexpression retaining its type)?
$(...) at the start of a token:
Write-Output $(Get-Date)/today # -> 2 arguments: [datetime] obj. and string '/today'
Note that the following work as expected:
Write-Output $HOME/sub - simple var. reference at the start
Write-Output today/$(Get-Date) - subexpression not at the start
.$ at the start of a token:
Write-Output .$HOME # -> 2 arguments: string '.' and value of $HOME
Note that the following work as expected:
Write-Output /$HOME - different initial char. preceding $
Write-Output .-$HOME - initial . not directly followed by $
Write-Output a.$HOME - . is not the initial char.
As an aside: As of PowerShell Core v6.0.0-alpha.15, a = following a simple var. reference at the start of a token also seems to break the token into 2 arguments, which does not happen in Windows PowerShell v5.1; e.g., Write-Output $HOME=dir.
Note:
I'm primarily looking for a design rationale for the described behavior, or, as the case may be, confirmation that it is a bug. If it's not a bug, I want something to help me conceptualize the behavior, so I can remember it and avoid its pitfalls.
All these edge cases can be avoided with explicit double-quoting, which, given the non-obvious behavior above, may be the safest choice to use routinely.
Optional reading: The state of the documentation and design musings
As of this writing, the v5.1 Get-Help about_Parsing page:
incompletely describes the rules
uses terms that aren't neither defined in the topic nor generally in common use in the world of PowerShell ("expandable string", "value expression" - though one can guess their meaning)
From the linked page (emphasis added):
In argument mode, each value is treated as an expandable string unless it begins with one of the following special characters: dollar sign ($), at sign (#), single quotation mark ('), double quotation mark ("), or an opening parenthesis (().
If preceded by one of these characters, the value is treated as a value expression.
As an aside: A token that starts with " is, of course, by definition, also an expandable string (interpolating string).
Curiously, the conceptual help topic about quoting, Get-Help about_Quoting_Rules, manages to avoid both the terms "expand" and "interpolate".
Note how the passage does not state what happens when (non-meta)characters directly follow a token that starts with these special characters, notably $.
However, the page contains an example that shows that a token that starts with a variable reference is interpreted as an expandable string too:
With $a containing 4, Write-Output $a/H evaluates to (single string argument) 4/H.
Note that the passage does imply that variable references / subexpressions in the interior of an unquoted token (that doesn't start with a special char.) are expanded as if inside a double-quoted string ("treated as an expandable string").
If these work:
$a = 4
Write-Output $a/H # -> '4/H'
Write-Output H/$a # -> 'H/4'
Write-Output H/$(2 + 2) # -> 'H/4'
why shouldn't Write-Output $(2 + 2)/H expand to '4/H' too (instead of being treated as 2 arguments?
Why is a subexpression at the start treated differently than a variable reference?
Such subtle distinctions are hard to remember, especially in the absence of a justification.
A rule that would make more sense to me is to unconditionally treat a token that starts with $ and has additional characters following the variable reference / subexpression as an expandable string as well.
(By contrast, it makes sense for a standalone variable reference / subexpression to retain its type, as it does now.)
Note that the case of a token that starts with .$ getting split into 2 arguments is not covered in the help topic at all.
Even more optional reading: following a token that starts with one of the other special characters with additional characters.
Among the other special token-starting characters, the following unconditionally treat any characters that follow the end of the construct as a separate argument (which makes sense):
( ' "
Write-Output (2 + 2)/H # -> 2 arguments: 4 and '/H'
Write-Output "2 + $a"/H # -> 2 arguments: '2 + 4' and '/H', assuming $a equals 4
Write-Output '2 + 2'/H # -> 2 arguments: '2 + 2' and '/H'
As an aside: This shows that bash-style string concatenation - placing any mix of quoted and unquoted tokens right next to each other - is not generally supported in PowerShell; it only works if the 1st substring / variable reference happens to be unquoted. E.g., Write-Output H/'2 + 2', unlike the substrings-reversed example above, produces only a single argument.
The exception is #: while # does have special meaning (see Get-Help about_Splatting) when followed by just a syntactically valid variable name (e.g., #parms), anything else causes the token to be treated as an expandable string again:
Write-Output #parms # splatting (results in no arguments if $parms is undefined)
Write-Output #parms$a # *expandable string*: '#parms4', if $a equals 4
I think what you're sort of hitting here is more the the type "hinting" than anything else.
You're using Write-Output which specifies in it's Synopsis that it
Sends the specified objects to the next command in the pipeline.
This command is designed to take in an array. When it hits the first item as a string like today/ it treats it like a string. When the first item ends up being the result of a function call, that may or may not be a string, so it starts up an array.
It's telling that if you run the same command to Write-Host (which is designed to take in a string to output) it works as you'd expect it to:
Write-Host $(Get-Date)/today
Outputs
7/25/2018 1:30:43 PM /today
So I think you're edge cases you're running up against are less about the parsing, and mor about the typing that powershell uses (and tries to hide).