I'm quite new to Powershell scripting and I have hit a bump where I obviously don't know how to ask Google the right question.
I am writing a script to be called from a system that only allows me to add a single parameter to the command line, but I in fact need more values to execute the script.
My idea is to build a variable for each possible parameter, and then use the variable going forward (Simplified):
$name1= "value1","value2","value3"
$name2= "value4","value5","value6"
$name3= "value7","vlaue8","value9"
foreach ($value in $nameX) { }
and then call the script like: script.ps1 nameX
But how to convert the parameter into the name of the corresponding variable?
Or are there easier ways...?
You should be able to solve your problem with the Get-Variable cmdlet:
# The one and only argument passed: the name of a variable defined inside
# the script; e.g., 'name1'
$variableName = $args[0]
# Define the variables that the argument can refer to:
$name1= "value1","value2","value3"
$name2= "value4","value5","value6"
$name3= "value7","vlaue8","value9"
# Use Get-Variable to get a variable's value by name.
# (Error handling omitted for brevity.)
foreach ($value in (Get-Variable $variableName -ValueOnly)) {
# ...
}
Related
issue
the called powershell script will accept parameters but not all of them:
Current Set-Up and code:
I have a common folder where two .ps1 scripts are located:
DoWork.ps1
Workmanager.ps1
Workmanager.ps1 calls the Dowork.ps1:
$targetPath="M:\target"
echo "target path: $targetPath"
start powershell {.\DoWork.ps1 -target $targetPath -tempdrive D:\}
output (as expected):
target path: M:\target
DoWork.ps1 contains some start code:
param
(
[string]$tempdrive,
[string]$target,
[int] $threads = 8,
[int] $queuelength = -1
)
echo "variables:"
echo "temp drive: $tempdrive"
echo "target path: $target"
Unexpectedly, the $target is not beeing assigned. Previously I had the variable named $targetpath, which did not work either.
variables:
temp drive: D:\
target path:
Findings
It appears that the issue relies in Workmanager.ps1. Spcifying the parameter as fixed string rather than as variable will load the parameter. Any solution for this?
start powershell {.\DoWork.ps1 -target "foo" -tempdrive D:\}
When you use a ScriptBlock as an argument to powershell.exe, variables aren't going to be evaluated until after the new session starts. $targetPath has not been set in the child PowerShell process called by Workmanager.ps1 and so it has no value. This is actually an expected behavior of a ScriptBlock in general and behaves this way in other contexts too.
The solution is mentioned in the help text for powershell -?:
[-Command { - | <script-block> [-args <arg-array>] <========== THIS GUY
| <string> [<CommandParameters>] } ]
You must provide the -args parameter which will be passed to the ScriptBlock on execution (separate multiple arguments with a ,). Passed arguments are passed positionally, and must be referenced as though you were processing the arguments to a function manually using the $args array. For example:
$name = 'Bender'
& powershell { Write-Output "Hello, $($args[0])" } -args $name
However, especially with more complicated ScriptBlock bodies, having to remember which index of $args[i] contains the value you want at a given time is a pain in the butt. Luckily, we can use a little trick with defining parameters within the ScriptBlock to help:
$name = 'Bender'
& powershell { param($name) Write-Output "Hello, $name" } -args $name
This will print Hello, Bender as expected.
Some additional pointers:
The ScriptBlock can be multiline as though you were defining a function. way. The examples above are single line due to their simplicity.
A ScriptBlock is just an unnamed function, which is why defining parameters and referencing arguments within one works the same way.
To exemplify this behavior outside of powershell.exe -Command, Invoke-Command requires you to pass variables to its ScriptBlock in a similar fashion. Note however that answer uses an already-defined function body as the ScriptBlock (which is totally valid to do)
You don't need to use Start-Process here (start is its alias), at least as demonstrated in your example. You can simply use the call operator & unless you need to do something more complex than "run the program and wait for it to finish". See this answer of mine for more information.
If you opt to pass a string to powershell.exe instead, you don't need to provide arguments and your variables will get rendered in the current PowerShell process. However, so will any other unescaped variables that might be intended to set within the child process, so be careful with this approach. Personally, I prefer using ScriptBlock regardless, and just deal with the extra parameter definition and arguments.
Using the call & operator is optional when you are not executing a path rendered as a string. It can be omitted in the examples above, but is more useful like so:
& "C:\The\Program Path\Contains\spaces.exe"
& $programPathAsAVariable
I have come across the strangest behaviour that has been driving me nuts when writing scripts. It is impossible sometimes to remove the value of a variable in Powershell. I have tried:
Remove-Variable -Force
Also tried making it equal to an empty string or making it $null but the variable value and type remains.
Anyone have an idea how this can happen?
I am using Powershell version 5 on Windows Server 2016.
Here some screenshots:
To remove a variable, pass its name without the $ sigil to the Remove-Variable cmdlet's
-Name parameter (which is positionally implied); using the example of a variable $date:
Using an argument:
# Note the required absence of $ in the name; quoting the var. name is
# optional in this case.
Remove-Variable -Force -Name date
Using the pipeline would require you to specify objects whose .Name property contains the name of the variable to delete, because these property values implicitly bind to Remove-Variable's -Name parameter; the simplest way to achieve that is to use the Get-Variable cmdlet, which too requires specifying the name without the $:
# Works, but is inefficient.
Get-Variable -Name date | Remove-Variable -Force
However, this is both more verbose and less efficient than directly passing the name(s) as an argument.
As for what you tried:
You variable-removal command is conceptually flawed:
$date | Remove-Variable -Force
Except as the LHS of an assignment ($date = ...), referring to a variable with the $ sigil returns its value, not the variable itself.
That is, since your $date variable contains a [datetime] instance, it is that instance that is sent through the pipeline, and since only strings are supported as input - that is, variable names - the command fails.
In effect, your call is equivalent to the following, which predictably fails:
PS> Get-Date | Remove-Variable -Force
Remove-Variable : The input object cannot be bound to any parameters for the command
either because the command does not take pipeline input
or the input and its properties do not match any of the parameters that take pipeline input.
What the somewhat verbose, general error message is implying in this case is that the input object was of the wrong type (because only objects with a .Name property are accepted, which [datetime] doesn't have).
Contexts in which you need refer to a variable itself rather than to its value:
What these contexts have in common is that you need to specify the variable name without the $ sigil.
Two notable examples:
All *-Variable cmdlets expect the names of variables to operate on, such as the Get-Variable cmdlet that returns objects representing variables, of type System.Management.Automation.PSVariable; these objects include the name, value, and other attributes of a PowerShell variable.
# Gets an object describing variable $date
$varObject = Get-Variable date # -Name parameter implied
When you pass the name of an output variable to a -*Variable common parameter
# Prints Get-Date's output while also capturing the output
# in variable $date.
Get-Date -OutVariable date
As implied, above, assigning to a variable with = is the only exception: there you do use the $ sigil, e.g. $date = Get-Date.
Note that this differs from POSIX-compatible shells such as bash, where you do not use $ in assignments (and must not have whitespace around =); e.g., date=$(date).
Experienced C# developer learning PowerShell here so I am sure I am just missing something silly. What I am trying to do is write a function that simply writes it's input to a temporary file in JSON format. I have code that works fine if I run it 'inline', but that same code writes an empty file when called in a function.
Here is the code:
function Dump-File {
param (
[Parameter(Mandatory=$true)]
$Input
)
$tmp = New-TemporaryFile
$Input | ConvertTo-Json | Out-File $tmp.FullName
Write-Output "Dump file written: $($tmp.FullName)"
}
$args = #{}
$args.Add('a', 1)
$args.Add('b', 2)
$args.Add('c', 3)
$args.Add('d', 4)
# results in json written to temp file
$tmp = New-TemporaryFile
$args | ConvertTo-Json | Out-File $tmp.FullName
Write-Output "args dumped: $($tmp.FullName)"
# results in empty temp file
Dump-File $args
Can anyone help me understand why the code called inline works but the same code does not work when I wrap it up as a function?
$Input is an automatic variable.
Changing the name of your Dump-File parameter to $somethingelse will resolve your problem. Never use $input as a parameter or variable name.
Automatic variables are to be considered read-only.
About Automatic Variables
SHORT DESCRIPTION
Describes variables that store state information for
PowerShell. These variables are created and maintained by PowerShell.
LONG DESCRIPTION
Conceptually, these variables are considered to be
read-only. Even though they can be written to, for backward
compatibility they should not be written to.
Here is a list of the automatic variables in PowerShell:
...
$INPUT
Contains an enumerator that enumerates all input that is passed to a function. The $input variable is available only to functions and script blocks (which are unnamed functions). In the Process block of a function, the $input variable enumerates the object that is currently in the pipeline. When the Process block completes, there are no objects left in the pipeline, so the $input variable enumerates an empty collection. If the function does not have a Process block, then in the End block, the $input variable enumerates the collection of all input to the function.
Source: About_Automatic_Variables
This information is also avaible through Get-help command
Get-Help about_Automatic_Variables
I have a powershell script that with the following hashtable
[HashTable]$folder_and_prefix = #{}
After a certain point in my script I no longer need that hashtable, I tried doing:
Remove-Variable $folder_and_prefix
but I get the following error:
Remove-Variable : Cannot find a variable with the name
'System.Collections.Hashtable'
Is it possible to remove a hastable that I no longer need?
This is a common mistake. Remove-Variable is taking the name of the variable (a [String]), and by referencing the variable itself (with a dollar sign $) you are passing the value. Remove the dollar sign and that's all you need:
Remove-Variable folder_and_prefix
Further, it takes an array of names, so you can do:
$var1 = 5
$var2 = 'Hello'
$var3 = #{}
Remove-Variable var1,var2,var3
And it accepts wildcards:
Remove-Variable var*
The wildcard acceptance is also true for Set-Variable, Get-Variable, and Clear-Variable (New-Variable is the exception).
As mentioned by others, don't use the dollar sign ($) when referring to the variable itself. Additionally, I always check to see if variable exists (before attempting to delete it) using conditional statement as below:
if (Test-Path variable:myVar) {Remove-Variable myVar} #Delete only if local variable exists.
[int]$myVar = 5;
You can also check your variable's existence in a specific Scopes, such as, for Global scope:
if (Test-Path variable:global:myVar) {Remove-Variable myVar –Scope Global}
You can even check Environment variables as below:
if (Test-Path env:ComputerName) {"do something with $env:ComputerName."}
HTH
I have two PowerShell scripts.
The first script has the following code:
$var = "abc"
$DIR = "C:\"
$SCRIPT_NAME = "abc.ps1"
&"${DIR}\${SCRIPT_NAME}" #execute the second script
If I want to pass the variable $var to the second script, how do I achieve that? What code do I need to put in both the first and the second script?
Parameters (Recommended): Use parameters to pass values to the second script.
Step2.ps1:
param ($myparameter)
write-host $myparameter
Step1.ps1:
$var = "abc"
$DIR = "C:\"
$SCRIPT_NAME = "step2.ps1"
&"${DIR}\${SCRIPT_NAME}" -myparameter $var
Alternative: You could also have used arguments $args (extra values not linked to a parameter). You can specify the first argument using $args[0]. I would however always recommend parameters as arguments needs to be in a specific order (if multiple arguments are passed) etc.
Step2.ps1:
write-host $args[0]
Step1.ps1:
$var = "abc"
$DIR = "C:\"
$SCRIPT_NAME = "step2.ps1"
&"${DIR}\${SCRIPT_NAME}" $var
There are several ways to do what you want, two of which have already been suggested by #FrodeF..
Pass the variable as a (named) parameter:
# script1.ps1
$var = 'foo'
$dir = 'C:\some\folder'
$scriptname = "script2.ps1"
& "${dir}\${scriptname}" -Foo $var
# script2.ps1
Param($foo)
Write-Output $foo
This is the cleanest solution. You have a well-defined interface and pass the variable in a clear-cut way from one script to another.
Parameter definitions will also allow you to make a parameter mandatory (so that the script will ask the user to provide input if the parameter was omitted), require a particular data type, easily incorporate validation routines, or add comment-based help.
# script2.ps1
<#
.SYNOPSIS
Short description of the script or function.
.DESCRIPTION
Longer description of what the script or function actually does.
.PARAMETER Foo
Description of the parameter Foo.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
[ValidateRange(2,42)]
[int]$foo
)
Write-Output $foo
See Get-Help about_Function_Advanced_Parameters for more information.
Pass the variable as an unnamed argument:
# script1.ps1
$var = 'foo'
$dir = 'C:\some\folder'
$scriptname = "script2.ps1"
& "${dir}\${scriptname}" $var
# script2.ps1
Write-Output $args[0]
This is the second best approach, because you still pass the variable in a clear-cut way, but the interface isn't as well defined as before.
Define the variable as an environment variable:
# script1.ps1
$env:var = 'foo'
$dir = 'C:\some\folder'
$scriptname = "script2.ps1"
& "${dir}\${scriptname}"
# script2.ps1
Write-Output $env:var
This is a less clean approach than the argument-based ones, as the variable is passed using a "side-channel" (the process environment, which is inherited by child processes).
Just define the variable in the first script and use it in the second one:
# script1.ps1
$var = 'foo'
$dir = 'C:\some\folder'
$scriptname = "script2.ps1"
& "${dir}\${scriptname}"
# script2.ps1
Write-Output $var
This will work as well, because by using the call operator (&) the second script is run in the same context as the first script and thus has access to the same variables. However, "passing" a variable like this will easily break if someone runs the second script in a different context/scope or modies it without being aware of the implicit dependency.
If you want to go this route it's usually better to use the first script for variable (and function) definitions only, and dot-source it in the second script, so that the definitions are imported into the scope of the second script:
# script1.ps1
$var = 'foo'
# script2.ps1
. 'C:\path\to\script1.ps1'
Write-Output $var
Technically, passing values via a file would be another option. However, I would recommend against using this approach for several reasons:
it's prone to errors due to improper permissions (could be mitigated by creating the file in the $env:TEMP folder),
it's prone to littering the filesystem if you don't clean up the file afterwards,
it needlessly generates disk I/O when simple in-memory operations provided by the language would suffice.