I have the following scenario, running on Powershell v5:
A Powershell script pulls several bits of information from an API call to a 3rd party system in a bulk for-each loop and assigns them to Variables. Amongst the information that is pulled is Passwords (this is being done to get rid of said 3rd party system and to migrate it to something that doesn't allow you to retrieve passwords in plain text):
$userset = Invoke-WebRequest -Method Post -Uri "https://$Url/path/to/api.asmx" -Headers $Headers -Body $usercall
$xmluserset = [xml] $userset.Content
$userset2 = $xmluserset.Envelope.Body.UserSettingsResult.settingValues.string
$userpasstemp = $userset2[1].trimstart("password")
$userpass = $userpasstemp.trimstart("=")
These passwords are then used elsewhere in the Script.
For example, they are passed to a different API and need to be in a URL compatible format and so I run this:
$urlescapeduserpass = [uri]::EscapeDataString($userpass)
which works for that section of the Script
The problem is that these passwords can contain any of the special characters:
!"#$%&'()*+,-./:;<=>?#[]^_`{|}~
And when I call another part of the script, the special characters in the password string cause a failure and the script to exit. This occurs when using either the call command:
& .\application.exe --option1 $option1 --user1 $user --password1 $userpass
or when using invoke-expression
$command = "$path\application.exe --option1 $option1 --user1 $user --password1 $userpass"
Invoke-Expression $command
I've tried using Regex, using the -replace cmdlet:
$escapedpass = $userpass -replace ' !"#$%&()*+,-./:;<=>?#[\]^_`{|}~', '`$&'
But no luck, I know similar to the [uri]escapedatastring, there's a similar one for Regex, but there doesn't appear to be one native for Powershell. I'm sure there is either a [contenttype] that will have a native function to escape the special characters or some way to achieve the end-result.
Because PowerShell's handling of embedded " characters in argument passed to external programs is broken (as of PowerShell 7) - see this answer - you need to manually \-escape " characters embedded in your string:
$escapedpass = $userpass -replace , '"', '\"'
In the context of your command:
& .\application.exe --option1 $option1 --user1 $user --password1 ($userpass -replace , '"', '\"')
Let's create a Minimal, Reproducible Example with a round trip based on the answer How to escape special characters in PowerShell? from #mklement0 for this:
(Also take the comment in account that I just made on spaces)
Unfortunately, PowerShell creates an extra handicap as it requires to single quote the string otherwise it will interpret other characters along with the dollar sign ($). I have placed comments in the code where you might leave this out.
Password.ps1
Param (
[String]$Password
)
Write-Host $Password
Password check:
$Input = #'
!"'#$%&()*+,-./:;<=>?#[\]^_`{|}~
'#
Write-Host 'Input:' $Input
$Escape = $Input.Replace('"', '\"')
$Escape = $Escape.Replace("'", "''") # Only required for PowerShell
Write-Host 'Command:' PowerShell .\Password.ps1 "'$Escape'"
# Omit the inner single quotes if it doesn't concern PowerShell
# But note that your .\application.exe might have its own syntax
# to support spaces and special characters from the command line,
# like double quotes: """$Escape"""
$Output = & PowerShell .\Password.ps1 "'$Escape'"
Write-Host 'Output:' $Output
Write-Host 'Input and output are equal?' ($Input -eq $Output)
Results:
Input: !"'#$%&()*+,-./:;<=>?#[\]^_`{|}~
Command: PowerShell .\Password.ps1 "' !\"''#$%&()*+,-./:;<=>?#[\]^_`{|}~'"
Output: !"'#$%&()*+,-./:;<=>?#[\]^_`{|}~
Input and output are equal? True
Related
I'm trying to develop a small script that sends the output of a tree of a certain directory, and I'm having problems with the presentation of the mail; The script already sends the info, but not in the way that I would like. My code is as follows:
# from to info
$MailFrom = ""
$MailTo = ""
# Credentials
$Username = "user"
$Password = "password"
# Server Info
$SmtpServer = "server"
$SmtpPort = "port"
# Menssage
$MessageSubject = "test"
$Message = New-Object System.Net.Mail.MailMessage $MailFrom,$MailTo
$Message.IsBodyHTML = $false
$Message.Subject = $MessageSubject
$Message.Body = tree /F directoryroute
# SMTP Client object
$Smtp = New-Object Net.Mail.SmtpClient($SmtpServer,$SmtpPort)
$Smtp.EnableSsl = $true
$Smtp.Credentials = New-Object System.Net.NetworkCredential($Username,$Password)
$Smtp.Send($Message)
So, the thing is that the mail shows the info like this:
When in fact, I want to see the following:
I am using power shell ISE and the tree is also different there:
What am I missing?
It sounds like you need to set the .BodyEncoding property to a character encoding that can represent the box-drawing characters that the tree.com utility uses for visualization, preferably UTF-8:
$Message.BodyEncoding = [Text.UTF8Encoding]::new()
Additionally, since the .Body property is a single string ([string]), whereas the lines output by tree.com are captured in an array by PowerShell, you need to create a multi-line string representation yourself:[1]
$Message.Body = tree /F directoryroute | Out-String
If you neglect to do that, PowerShell implicitly stringifies the array, which means joining the array elements on a single line with spaces, which results in what you saw.
As for the PowerShell ISE:
It misinterprets output from external programs such as tree.com, because it uses the system's ANSI code page by default for decoding, whereas most external programs use the OEM code page.
The ISE has other limitations, summarized in the bottom section of this answer, is no longer actively developed and notably cannot run the modern, cross-platform PowerShell edition, PowerShell (Core) 7+.
Consider migrating to Visual Studio Code with its PowerShell extension, which is an actively developed, cross-platform editor that offers the best PowerShell development experience.
In case you do want to make tree.com work in the ISE, run the following:
[Console]::OutputEncoding =
Text.Encoding]::GetEncoding([cultureinfo]::CurrentCulture.TextInfo.OEMCodePage)
[1] While using Out-String is convenient, it always appends a trailing newline to its output - see GitHub issue #14444. If you need to avoid that, use (tree /F directoryroute) -join [Environment]::NewLine instead.
Environment:
Windows Server 2016
Windows 10 Pro
PowerShell 5.1
$myVariable is empty, I think and I'm expecting there to be a string value.
Invoke-Command -ComputerName WKSP000D1E3F -Credential $creds -ScriptBlock {
sqlcmd -E -Q "select top 1 FirstName from customers" -d database1 -S "(localdb)\ProjectsV13" | Tee-Object -Variable myVariable
}
Write-Host $myVariable
Cpt.Whale has provided the crucial pointer in a comment: you fundamentally cannot set local variables from a script block being executed remotely (via Invoke-Command -ComputerName) - you must use output from the script block to communicate data back to the caller.
While you could apply Tee-Object locally instead (Invoke-Command ... | Tee-Object), there's a simpler solution, which works with all cmdlets, including cmdlet-like (advanced) functions and scripts:
Use the common -OutVariable (-ov) parameter to capture a cmdlet's output in a self-chosen variable while passing that output through:
# Note the `-OutVariable myVariable` part
# and that the variable name must be specified *without* a leading "$"
# Output is still being passed through.
Invoke-Command -OutVariable myVariable -ComputerName WKSP000D1E3F -Credential $creds -ScriptBlock {
sqlcmd -E -Q "select top 1 FirstName from customers" -d database1 -S "(localdb)\ProjectsV13"
}
# $myVariable now contains the captured content.
By contrast, if you want to capture output only, without also passing it through (to the display, by default), you can heed Santiago Squarzon's advice and simply assign the Invoke-Command call to your variable ($myVariable = Invoke-Command ...).
Notes re -OutVariable(-ov):
As shown above, and as shown with Tee-Object -Variable in your question, the name of the self-chosen target variable must be specified without a leading $, e.g. -OutVariable var, not Out-Variable $var; if you did the latter, the value of a preexisting $var variable (if defined) would be used as the variable name.
Unlike directly captured output, the target variable always receives array(-like) data, specifically, an instance of the System.Collections.ArrayList class - even if only one output object is present; e.g.:
# -> 'types: v1: String vs. v2: ArrayList'
$v1 = Write-Output -OutVariable v2 'one'
"types: v1: $($v1.GetType().Name) vs. v2: $($v2.GetType().Name)"
That is, while directly capturing output captures a single output object as-is, and multiple ones in a regular PowerShell array (of type [object[]], -OutVariable always creates an ArrayList - see GitHub issue #3154 for a discussion of this inconsistency.
With commands that do not support -OutVariable, namely simple scripts and functions as well as external programs:
To pass the output through in streaming fashion, i.e. as it becomes available, pipe to Tee-Object -Variable; e.g.:
# Passes output through as it is being emitted.
some.exe | Tee-Object -Variable myVariable
Otherwise - i.e. if it is acceptable to collect all output first, before passing it through - simply enclose an assignment statement in (...) to pass its value through - this approach performs better than Tee-Object -Variable; e.g.:
# Collects all output first, then passes it through.
($myVariable = some.exe)
I am trying to write a powershell script that opens a remote desktop connection for each machine name saved in a text file. When I run the script, it only connects to the first machine in the list and outputs to the console: CMDKEY: Credential added successfully once (not once for each machine). mstcs seems to terminate the process after executing, and I'm not sure I'm adding credentials the right way. Can anyone point me in the right direction?
Here are some tests I've tried to figure out what's going on:
Print after mstsc. Doesn't print. Process seems to terminate after
mstcs is called. This seems to be the crux of the issue.
cmdkey /list shows all the credentials I have stored and their targets. The output does not include all the targets defined in the text file. Even if I comment out mstsc, then cmdkey /add:$MachineName /user:$User /pass:$Password only seems to execute for the first line, evidenced by the lack of more console outputs and cmdkey /list not yielding the expected targets. In addition, I have added a print statement after this cmdkey line and it prints for each line, so it doesn't terminate after running (which I already knew because mstcs executes after this line when it's not commented out).
# Read from file
$Lines = Get-Content -Path .\machines.txt | Out-String
# For each machine ...
foreach($Line in $Lines){
# Split line, save name and domain
$Tokens = $Line.Split(".")
$MachineName = $Tokens[0]
$Domain = $Tokens[1]
$User = "someDomain\someUsername"
$Password="somePassword"
# Switch username if someOtherDomain
if ($Domain -eq "someOtherDomain"){
$User = "someOtherDomain\someOtherUsername"
}
#set credentials and open connection
cmdkey /add:$MachineName /user:$User /pass:$Password
mstsc /v:$MachineName /console
}
EDIT: I have also tried replacing mstsc /v:$MachineName with Start-Process -FilePath "$env:windir\system32\mstsc.exe" -ArgumentList "/v:$MachineName" -Wait. The result is opening the session and then the script does not finish in the console but nothing additional happens.
This behavior is cause by your use of Out-String.
Get-Content outputs multiple strings, one per line in the file - but Out-String stitches them back together into a single multi-line string:
PS C:\> $machines = Get-Content machines.txt
PS C:\> $machines.GetType().Name # definitely an array
Object[]
PS C:\> $machines.Count # multiple strings in there
4
PS C:\> $machines = Get-Content machines.txt | Out-String
PS C:\> $machines.GetType().Name # now it's just a single string
String
So your foreach(){} loop only runs once, and the value of $MachineName is no longer the name of a single machine, but a multi-line string with all of them at once - which is probably why mstsc exits immediately :)
Remove |Out-String from the first line and your loop will work
I'm having a surprisingly difficult time embedding variables with quotes to an external command with PoSH. For example, this command
dfsradmin membership list /rgname:`"stuff I want`"
gives me the following expected result:
Failed:
Replication group with name stuff I want cannot be found.
This command, however
$group = "stuff I want"
dfsradmin membership list /rgname:`"$group`"
fails with this error:
Failed:
The subobject "/rgname:"stuff is not a valid subobject.
Is this a bug with Powershell or am I missing/misunderstanding something?
Yeah there are known issues in Powershell ( including v2.0) around this: http://connect.microsoft.com/PowerShell/feedback/details/376207/executing-commands-which-require-quotes-and-variables-is-practically-impossible
See if the alternatives discussed in the link above work for you. I cannot try it out as I don't have that executable.
Also echoargs.exe is a useful tool that you can use to see what arguments have been recevied from Powershell.
I found that defining
$quote = '"'
and then using /command$quote"test"$quote works as well
There's no need to add back ticks in front of quotes. Does this work for you?
$group = "stuff I want"
dfsradmin membership list /rgname:"$group"
So I was able to get around this by executing it in CMD.exe and doing string manipulations to get what I need.
$str = &cmd /c 'dfsradmin membership list /rgname:"blah blah"'
$str = &cmd /c "dfsradmin membership list /rgname:$blah" # with vars
Thanks for the help! I hope this has been resolved in Powershell 3.0.
I found a workaround which doesn't call cmd but uses Invoke-Expression instead. The command has to be put in a variable first:
$var = "string with spaces"
$command = "first part " + [char]96 + [char]34 + $var + [char]96 + [char]34 + " second part"
Invoke-Expression $command
Not that pretty but it works. You can replace [char]96 with '`' and [char]34 with '"' if you prefer. Easy to create a function which does it if you use it a lot.
All of the above did not work for me but based on Carlos idea, this is the solution that worked posted here
# get msdeploy exe
$MSDeploy = ${env:ProgramFiles}, ${env:ProgramFiles(x86)} |
ForEach-Object {Get-ChildItem -Path $_ -Filter 'MSDeploy.exe' -Recurse} |
Sort-Object -Property #{Expression={[version]$_.VersionInfo.FileVersion}} -Descending |
Select-Object -First 1 -ExpandProperty FullName
#build deploy command
$deplyCmd = """""$MSDeploy"" -verb:sync -dest:iisApp=""Default Web Site"" -enableRule:DoNotDeleteRule -source:iisApp=""$ExtraWebFilesFolder"""
#execute
&cmd /c $deplyCmd
I know this is old thread but just posting here in case my solution works for somebody as it worked for me.
This particular command (dfsradmin) expects natively seen quotes so I just enclosed value with quotes in single quotes thus passing quotes as well:
dfsradmin membership list /rgname:'"stuff I want"'
or if using through variable:
$group = '"stuff I want"'
dfsradmin membership list /rgname:$group
I have some PowerShell scripts that accept many long parameters, like,
myScript.ps1 -completePathToFile "C:\...\...\...\file.txt" -completePathForOutput "C:\...\...\...\output.log" -recipients ("me#me.com") -etc.
I can't seem to make PowerShell run such scripts unless all the parameters are on a single line. Is there a way to invoke the script more like this?
myScript.ps1
-completePathToFile "C:\...\...\...\file.txt"
-completePathForOutput "C:\...\...\...\output.log"
-recipients (
"me#me.com",
"him#him.com"
)
-etc
The lack of readability is driving me nuts, but the scripts really do need to be this parametric.
PowerShell thinks the command is complete at the end of the line unless it sees certain characters like a pipe, open paren or open curly. Just put a line continuation character `` ` at the end of each line but make sure there are no spaces after that continuation character:
myScript.ps1 `
-completePathToFile "C:\...\...\...\file.txt" `
-completePathForOutput "C:\...\...\...\output.log" `
-recipients (
"me#me.com", `
"him#him.com" `
)
If you're on PowerShell 2.0 you can also put those parameters in a hashtable and use splatting e.g:
$parms = #{
CompletePathToFile = 'C:\...\...\...\file.txt'
CompletPathForOutput = 'C:\...\...\...\output.log'
Recipients = 'me#me.com','him#him.com'
}
myScript.ps1 #parms