How can I force string variable expansion?
I need to read a string with one or more variable names in it (a template) and then expand it after I read the file. The key is that I must read the contents of the file before I declare the variables that will be used in the expansion. I've tried several ways but I can't get it to work.
It's not an option to read the file after $environment is defined.
Contents of name.txt:
$environment-RPT-INT
#example 1
$name = gc "c:\temp\name.txt"
$environment = "9065DEV"
$expanded = $ExecutionContext.InvokeCommand.ExpandString($name)
$expanded
#example 2
$name = gc "c:\temp\name.txt"
$environment = "9065DEV"
$expanded = $expanded = Invoke-Expression "`"$template`""
$expanded
#example 3
$name = gc "c:\temp\name.txt"
$environment = "9065DEV"
$name = $name.Clone()
$expanded = $ExecutionContext.InvokeCommand.ExpandString($name)
$expanded
Any help is appreciated.
Updated: Example 1 is now working for me.
It looks like you've found some possible solutions, but I'll suggest another that is in my opinion a bit smarter and more robust.
Instead of requiring variable names in your text file, why not use format specifiers. For example, the contents of name.txt:
{0}-RPT-INT
And in your script:
$name = gc "c:\temp\name.txt"
$environment = "9065DEV"
$expanded = $name -f $environment
$expanded
This way, you can rename the variable w/o changing any of your text files. As a bonus, if your text file comes from unknown sources, your script is vulnerable to code injection. For example, say you are given a text file with these contents:
$(rm -whatif -recurse -force c:\)-RPT-INT
Then ExpandString or Invoke-Expression will happily execute that code.
Your Invoke-Expression example is pretty close. Instead of using $template though, you need to use $name.
#example 2
$name = gc 'c:\temp\name.txt';
$environment = '9065DEV';
$expanded = Invoke-Expression -Command "`"$name`"";
$expanded;
If you are willing to store your Setting/Values in a CSV, I wrote a module to pull values from a CSV, put it into a HereString... any variables you put into the CSV become fully expanded inside the Here-String. That way, you can normally address the field names and values.
I might be able to change this to also work with traditional INI's if anyone is interested.
https://github.com/Inventologist?tab=repositories
Look for: HereStringFromCSV
There is a function called ExpandString predefined in powershell. It's inside $ExecutionContext, as follows.
$mystring = #'
This is a here string with some embedded variables.
Here is variable foo -- $foo
Here is variable bar -- $bar
Here is variable bletch -- $bletch
'#
#This displays the here string as is.
$mystring
#now define foo, bar and bletch
$foo = 5
$bar = Get-Date
$bletch = "George Washington"
#now run the here string through Expandstring.
$ExecutionContext.InvokeCommand.ExpandString($mystring)
Related
I have a folder with multiple PDFs I need to print to different printers. I've created variables for each shared printer and depending on the first 2 letters of the PDF the printing will go to the matching printer.
I'm having trouble concatenating 2 strings to form an existing variable to use it later in the printing call.
This is what I have now (all PDFs in the dir starts with 01 for now):
# SumatraPDF path
$SumatraExe = "C:\Users\Administrador.WIN-FPFTEJASDVR\AppData\Local\SumatraPDF\SumatraPDF.exe"
# PDFs to print path
$PDF = "C:\Program Files (x86)\CarrascocreditosPrueba2\CarrascocreditosPrueba2\DTE\BOL"
# Shared printers list
$01 = '\\192.168.1.70\epson'
$02 = '\\192.168.1.113\EPSON1050'
cd $PDF
While ($true) {
Get-ChildItem | Where {!$_.PsIsContainer} | Select-Object Name | %{
$Boleta = $_.Name
$CodSucursal = $Boleta.Substring(0,2)
$CodImpresora = '$' + $CodSucursal
Write-Host $CodImpresora -> This shows literal $01 on PS ISE
Write-Host $01 -> This show the shared printer path
}
Start-Sleep -Seconds 5
}
# Actual PDF printing...
#& $SumatraExe -print-to $CodImpresora $PDF
So basically I need to call an existing variable based on 2 strings. Probably this could be achieved with a Switch but that will be too extensive.
concatenating 2 strings to form an existing variable
That won't work in PowerShell, variable tokens are always treated literally.
I'd suggest you use a hashtable instead:
# Shared printers table
$Impresoras = #{
'01' = '\\192.168.1.70\epson'
'02' = '\\192.168.1.113\EPSON1050'
}
Then inside the loop:
$Boleta = $_.Name
$CodSucursal = $Boleta.Substring(0,2)
$Impresora = $Impresoras[$CodSucursal]
Although the language syntax don't support variable variable names, you can resolve variables by name using either the Get-Variable cmdlet:
# Returns a PSVariable object describing the variable $01
Get-Variable '01'
# Returns the raw value currently assigned to $01
Get-Variable '01' -ValueOnly
... or by querying the Variable: PSDrive:
# Same effect as `Get-Variable 01`
Get-Item Variable:\01
While these alternatives exist, I'd strongly suggest staying clear of using them in scripts - they're slow, makes the code more complicated to read, and I don't think I've ever encountered a situation in which using a hashtable or an array wasn't ultimately easier :)
What is the best practice to create a text-based database for a PowerShell script?
What I really mean exactly?
I have a PowerShell script which use an url. This address may be used in other PS scripts, but I would be pretty happy if a quite simple practice is exists to solve to store this URL (or more) in a file, and the scripts use form this content, what I only have to define, which variable, line should be used, like this:
SharePointUrl = "..."
CcUrl = "..."
And in the script:
$SPUrl = DataFile.SharePointUrl ...
Something like this.
Not sure if I understand your question correctly. Are you looking for something like this?
$SharePointUrl = 'http://www.example.org/...'
New-Object -Type PSObject -Property #{'URL'=$SharePointUrl} |
Export-Csv 'C:\path\to\some.csv' -NoType
$DataFile = Import-Csv 'C:\path\to\some.csv'
$SPUrl = $DataFile.URL
Edit: After re-reading your question it seems you have an input file with key=value pairs. That can be processed like this:
$DataFile = Get-Content 'C:\path\to\data.txt' -Raw | ConvertFrom-StringData
$SPUrl = $DataFile.SharePointUrl
Another option is to write the configuration to a second PowerShell script:
# as variables
$SharePointUrl = "..."
$CcUrl = "..."
# or as a hashtable
$DataFile = #{
SharePointUrl = "..."
CcUrl = "..."
}
and dot-source that script in your original script.
. 'C:\path\to\config.ps1'
Imagine the following code:
# Script Start
$WelcomeMessage = "Hello $UserName, today is $($Date.DayOfWeek)"
..
..
# 100 lines of other functions and what not...
..
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage
}
This is a very basic example, but what it tries to show is a script where there is a $WelcomeMessage that the person running the script can set at the top of the script and controls how/what the message displayed is.
First thing's first: why do something like this? Well, if you're passing your script around to multiple people, they might want different messages. Maybe they don't like $($Date.DayOfWeek) and want to get the full date. Maybe they don't want to show the username, whatever.
Second, why put it at the top of the script? Simplicity. If you have 1000 lines in your script and messages like these spread all over the script, it makes it a nightmare for people to find and change these messages. We already do that for static messages, in the form of localized strings and stuff, so this is nothing new, except for the variable parts in it.
So, now to the issue. If you run that code and invoke Greet-User (assuming the functions/cmdlets for retrieving username and date actually exist and return something proper...) Greet-User will always return Hello , today is.
This is because the string is expanded when you declare it, at the top of the script, when neither $UserName nor $Date objects have a value.
A potential workaround would be to create the strings with single quotes, and use Invoke-Expression to expand them. But because of the spaces, that gets a bit messy. I.e.:
$WelcomeMessage = 'Hello $env:USERNAME'
Invoke-Expression $WelcomeMessage
This throws an error because of the space, to get it to work properly it would have to be declared as such:
$WelcomeMessage = 'Hello $env:USERNAME'
$InvokeExpression = "`"$WelcomeMessage`""
Messy...
Also, there's another problem in the form of code injection. Since we're allowing the user to write their own welcome message with no bounds specified, what's to prevent them from putting in something like...
$WelcomeMessage 'Hello $([void] (Remove-Item C:\Windows -Force -Recurse))'
(Yes, I know this will not delete everything but it is an example)
Granted this is a script and if they can modify that string they can also modify everything else on the script, but whereas the example I gave was someone maliciously taking advantage of the nature of the script, it can also happen that someone accidentally puts something in the string that ends up having unwanted consequences.
So... there's got to be a better way without the use of Invoke-Expression, I just can't quite thing of one so help would be appreciated :)
Embedding variables into strings is not the only way to create dynamic text, the way I would do it is like this:
$WelcomeMessage = 'Hello {0}, today is {1}'
# 100 lines of other functions and what not...
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage -f $Username, $Date
}
The canonical way to delay evaluation of expressions/variables in strings is to define them as single-quoted strings and use $ExecutionContext.InvokeCommand.ExpandString() later on.
Demonstration:
PS C:\> $s = '$env:COMPUTERNAME'
PS C:\> $s
$env:COMPUTERNAME
PS C:\> $ExecutionContext.InvokeCommand.ExpandString($s)
FOO
Applied to your sample code:
$WelcomeMessage = 'Hello $UserName, today is $($Date.DayOfWeek)'
...
...
...
function Greet-User {
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$ExecutionContext.InvokeCommand.ExpandString($WelcomeMessage)
}
Have you considered using a lambda expression; i.e. instead of defining the variable as a string value define it as a function, then invoke that function passing the relevant parameters at runtime.
$WelcomeMessage = {param($UserName,$Date);"Hello $UserName, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}
#...
# 100 lines of other functions and what not...
#...
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage.invoke($username,$date)
}
cls
Greet-User
Update
If you only wish to allow variable replacement the below code would do the trick; but this fails to do more advanced functions (e.g. .DayOfWeek)
$WelcomeMessage = 'Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))'
#...
# 100 lines of other functions and what not...
#...
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
write-output {param($UserName,$Date);"$WelcomeMessage";}
}
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$temp = $WelcomeMessage
get-variable | ?{#('$','?','^') -notcontains $_.Name} | sort name -Descending | %{
$temp = $temp -replace ("\`${0}" -f $_.name),$_.value
}
$temp
}
cls
Greet-User
Update
To avoid code injection this makes use of -whatif; that will only help where the injected code supports the whatif functionality, but hopefully better than nothing...
Also the code now doesn't require parameters to be declared; but just takes those variables which are available at the time of execution.
$WelcomeMessage = {"Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}
#...
# 100 lines of other functions and what not...
#...
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
write-output {param($UserName,$Date);"$WelcomeMessage";}
}
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Greet-User {
[cmdletbinding(SupportsShouldProcess=$True)]
param()
begin {$original = $WhatIfPreference; $WhatIfPreference = $true;}
process {
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
& $WelcomeMessage
}
end {$WhatIfPreference = $original;}
}
cls
Greet-User
I have a requirement like:
Have a text file containing the following in the following pattern
172.26.xxy.zxy:Administrator:Password
172.26.xxy.yyx:Administrator:Password
172.26.xxy.yyy:Administrator:Password
172.26.xxy.yxy:Administrator:Password
I need my powershell script to read each word and use that word whereever required. For example,
foreach(something)
{
I want the IP's(172.26.---.---) to read and store the value as a variable.
I want to store the two words after **:** in seperate variables.
}
How can this be done? I know to read an entire file or get some specific string. But I need the same to be done on each line.Any help would be really appreciated.
Something like this? You can just split on the : and then store your variables based on the index
$contents = Get-Content C:\your\file.txt
foreach($line in $contents) {
$s = $line -split ':'
$ip = $s[0]
$user = $s[1]
$pass = $s[2]
write-host $ip $user $pass
}
minor edit: "t" missing in content.
You can write a regular expression to replace to remove the parts you do not need
$ip_address= '172.26.xxy.zxy:Administrator:Password' -replace '^(.+):(.+):(.+)$','$1'
$user= '172.26.xxy.zxy:Administrator:Password' -replace '^(.+):(.+):(.+)$','$2'
$pwd= '172.26.xxy.zxy:Administrator:Password' -replace '^(.+):(.+):(.+)$','$3'
I think the more generic and pure Powershell way would be something like this:
Select-String "(.*):(.*):(.*)" c:\file.txt |
Select #{Name="IP"; Expression = {$_.Matches.Groups[1]}},
#{Name="User"; Expression = {$_.Matches.Groups[2]}},
#{Name="Password"; Expression = {$_.Matches.Groups[3]}}
The Output would be then an array of objects each having three properties IP, User and Password. So you can now use them for your purposes, or just add more commands at the end of the pipe.
I have developed a PowerShell function that performs a number of actions involving provisioning SharePoint Team sites. Ultimately, I want the function to return the URL of the provisioned site as a String so at the end of my function I have the following code:
$rs = $url.ToString();
return $rs;
The code that calls this function looks like:
$returnURL = MyFunction -param 1 ...
So I am expecting a String, however it's not. Instead, it is an object of type System.Management.Automation.PSMethod. Why is it returning that type instead of a String type?
PowerShell has really wacky return semantics - at least when viewed from a more traditional programming perspective. There are two main ideas to wrap your head around:
All output is captured, and returned
The return keyword really just indicates a logical exit point
Thus, the following two script blocks will do effectively the exact same thing:
$a = "Hello, World"
return $a
$a = "Hello, World"
$a
return
The $a variable in the second example is left as output on the pipeline and, as mentioned, all output is returned. In fact, in the second example you could omit the return entirely and you would get the same behavior (the return would be implied as the function naturally completes and exits).
Without more of your function definition I can't say why you are getting a PSMethod object. My guess is that you probably have something a few lines up that is not being captured and is being placed on the output pipeline.
It is also worth noting that you probably don't need those semicolons - unless you are nesting multiple expressions on a single line.
You can read more about the return semantics on the about_Return page on TechNet, or by invoking the help return command from PowerShell itself.
This part of PowerShell is probably the most stupid aspect. Any extraneous output generated during a function will pollute the result. Sometimes there isn't any output, and then under some conditions there is some other unplanned output, in addition to your planned return value.
So, I remove the assignment from the original function call, so the output ends up on the screen, and then step through until something I didn't plan for pops out in the debugger window (using the PowerShell ISE).
Even things like reserving variables in outer scopes cause output, like [boolean]$isEnabled which will annoyingly spit a False out unless you make it [boolean]$isEnabled = $false.
Another good one is $someCollection.Add("thing") which spits out the new collection count.
With PowerShell 5 we now have the ability to create classes. Change your function into a class, and return will only return the object immediately preceding it. Here is a real simple example.
class test_class {
[int]return_what() {
Write-Output "Hello, World!"
return 808979
}
}
$tc = New-Object -TypeName test_class
$tc.return_what()
If this was a function the expected output would be
Hello World
808979
but as a class the only thing returned is the integer 808979. A class is sort of like a guarantee that it will only return the type declared or void.
As a workaround I've been returning the last object in the array that you get back from the function... It is not a great solution, but it's better than nothing:
someFunction {
$a = "hello"
"Function is running"
return $a
}
$b = someFunction
$b = $b[($b.count - 1)] # Or
$b = $b[-1] # Simpler
All in all, a more one-lineish way of writing the same thing could be:
$b = (someFunction $someParameter $andAnotherOne)[-1]
I pass around a simple Hashtable object with a single result member to avoid the return craziness as I also want to output to the console. It acts through pass by reference.
function sample-loop($returnObj) {
for($i = 0; $i -lt 10; $i++) {
Write-Host "loop counter: $i"
$returnObj.result++
}
}
function main-sample() {
$countObj = #{ result = 0 }
sample-loop -returnObj $countObj
Write-Host "_____________"
Write-Host "Total = " ($countObj.result)
}
main-sample
You can see real example usage at my GitHub project unpackTunes.
The existing answers are correct, but sometimes you aren't actually returning something explicitly with a Write-Output or a return, yet there is some mystery value in the function results. This could be the output of a builtin function like New-Item
PS C:\temp> function ContrivedFolderMakerFunction {
>> $folderName = [DateTime]::Now.ToFileTime()
>> $folderPath = Join-Path -Path . -ChildPath $folderName
>> New-Item -Path $folderPath -ItemType Directory
>> return $true
>> }
PS C:\temp> $result = ContrivedFolderMakerFunction
PS C:\temp> $result
Directory: C:\temp
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2/9/2020 4:32 PM 132257575335253136
True
All that extra noise of the directory creation is being collected and emitted in the output. The easy way to mitigate this is to add | Out-Null to the end of the New-Item statement, or you can assign the result to a variable and just not use that variable. It would look like this...
PS C:\temp> function ContrivedFolderMakerFunction {
>> $folderName = [DateTime]::Now.ToFileTime()
>> $folderPath = Join-Path -Path . -ChildPath $folderName
>> New-Item -Path $folderPath -ItemType Directory | Out-Null
>> # -or-
>> $throwaway = New-Item -Path $folderPath -ItemType Directory
>> return $true
>> }
PS C:\temp> $result = ContrivedFolderMakerFunction
PS C:\temp> $result
True
New-Item is probably the more famous of these, but others include all of the StringBuilder.Append*() methods, as well as the SqlDataAdapter.Fill() method.
You need to clear output before returning. Try using Out-Null. That's how powershell return works. It returns not the variable you wanted, but output of your whole function. So your example would be:
function Return-Url
{
param([string] $url)
. {
$rs = $url.ToString();
return
} | Out-Null
return $rs
}
$result = Return-Url -url "https://stackoverflow.com/questions/10286164/function-return-value-in-powershell"
Write-Host $result
Write-Host $result.GetType()
And result is:
https://stackoverflow.com/questions/10286164/function-return-value-in-powershell
System.String
Credits to https://riptutorial.com/powershell/example/27037/how-to-work-with-functions-returns
It's hard to say without looking at at code. Make sure your function doesn't return more than one object and that you capture any results made from other calls. What do you get for:
#($returnURL).count
Anyway, two suggestions:
Cast the object to string:
...
return [string]$rs
Or just enclose it in double quotes, same as above but shorter to type:
...
return "$rs"
Luke's description of the function results in these scenarios seems to be right on. I only wish to understand the root cause and the PowerShell product team would do something about the behavior. It seems to be quite common and has cost me too much debugging time.
To get around this issue I've been using global variables rather than returning and using the value from the function call.
Here's another question on the use of global variables:
Setting a global PowerShell variable from a function where the global variable name is a variable passed to the function
The following simply returns 4 as an answer. When you replace the add expressions for strings it returns the first string.
Function StartingMain {
$a = 1 + 3
$b = 2 + 5
$c = 3 + 7
Return $a
}
Function StartingEnd($b) {
Write-Host $b
}
StartingEnd(StartingMain)
This can also be done for an array. The example below will return "Text 2"
Function StartingMain {
$a = ,#("Text 1","Text 2","Text 3")
Return $a
}
Function StartingEnd($b) {
Write-Host $b[1]
}
StartingEnd(StartingMain)
Note that you have to call the function below the function itself. Otherwise, the first time it runs it will return an error that it doesn't know what "StartingMain" is.