I'm surprised that I didn't get the answer for this common scenario after Googling for while...
How can an environment variable in be set in PowerShell if it does not exist?
The following code defines environment variable FOO for the current process, if it doesn't exist yet.
if ($null -eq $env:FOO) { $env:FOO = 'bar' }
# If you want to treat a *nonexistent* variable the same as
# an existent one whose value is the *empty string*, you can simplify to:
if (-not $env:FOO) { $env:FOO = 'bar' }
# Alternatively:
if (-not (Test-Path env:FOO)) { $env:FOO = 'bar' }
# Or even (quietly fails if the variable already exists):
New-Item -ErrorAction Ignore env:FOO -Value bar
In PowerShell (Core) 7.1+, which has null-coalescing operators, you can simplify to:
$env:FOO ??= 'bar'
Note:
Environment variables are strings by definition. If a given environment variable is defined, but has no value, its value is the empty string ('') rather than $null. Thus, comparing to $null can be used to distinguish between an undefined environment variable and one that is defined, but has no value. However, note that assigning to environment variables in PowerShell / .NET makes no distinction between $null and '', and either value results in undefining (removing) the target environment variable; similarly, in cmd.exe set FOO= results in removal/non-definition of variable FOO, and the GUI dialog (accessible via sysdm.cpl) doesn't allow you to define a variable with an empty string either. However, the Windows API (SetEnvironmentVariable) does permit creating environment variables that contain the empty string.
On Unix-like platforms, empty-string values are allowed too, and the native, POSIX-compatible shells (e.g, bash and /bin/sh) - unlike PowerShell - also allow you to create them (e.g, export FOO=). Note that environment variable definitions and lookups are case-sensitive on Unix, unlike on Windows.
Note: If the environment variable is created on demand by the assignment above ($env:FOO = ...), it will exist for the current process and any child processes it creates only Thanks, PetSerAl.
The following was mostly contributed by Ansgar Wiechers, with a supplement by Mathias R. Jessen:
On Windows[*], if you want to define an environment variable persistently, you need to use the static SetEnvironmentVariable() method of the [System.Environment] class:
# user environment
[Environment]::SetEnvironmentVariable('FOO', 'bar', 'User')
# system environment (requires admin privileges)
[Environment]::SetEnvironmentVariable('FOO', 'bar', 'Machine')
Note that these definitions take effect in future sessions (processes), so in order to define the variable for the current process as well, run $env:FOO = 'bar' in addition, which is effectively the same as [Environment]::SetEnvironmentVariable('FOO', 'bar', 'Process').
When using [Environment]::SetEnvironmentVariable() with User or Machine, a WM_SETTINGCHANGE message is sent to other applications to notify them of the change (though few applications react to such notifications).
This doesn't apply when targeting Process (or when assigning to $env:FOO), because no other applications (processes) can see the variable anyway.
See also: Creating and Modifying Environment Variables (TechNet article).
[*] On Unix-like platforms, attempts to target the persistent scopes - User or Machine- are quietly ignored, as of .NET (Core) 7, and this non-support for defining persistent environment variables is unlikely to change, given the lack of a unified mechanism across Unix platforms.
Code
function Set-LocalEnvironmentVariable {
param (
[Parameter()]
[System.String]
$Name,
[Parameter()]
[System.String]
$Value,
[Parameter()]
[Switch]
$Append
)
if($Append.IsPresent)
{
if(Test-Path "env:$Name")
{
$Value = (Get-Item "env:$Name").Value + $Value
}
}
Set-Item env:$Name -Value "$value" | Out-Null
}
function Set-PersistentEnvironmentVariable {
param (
[Parameter()]
[System.String]
$Name,
[Parameter()]
[System.String]
$Value,
[Parameter()]
[Switch]
$Append
)
Set-LocalEnvironmentVariable -Name $Name -Value $Value -Append:$Append
if ($Append.IsPresent) {
$value = (Get-Item "env:$Name").Value
}
if ($IsWindows) {
setx "$Name" "$Value" | Out-Null
return
}
$pattern = "\s*export[ \t]+$Name=[\w]*[ \t]*>[ \t]*\/dev\/null[ \t]*;[ \t]*#[ \t]*$Name\s*"
if ($IsLinux) {
$file = "~/.bash_profile"
$content = (Get-Content "$file" -ErrorAction Ignore -Raw) + [System.String]::Empty
$content = [System.Text.RegularExpressions.Regex]::Replace($content, $pattern, [String]::Empty);
$content += [System.Environment]::NewLine + [System.Environment]::NewLine + "export $Name=$Value > /dev/null ; # $Name"
Set-Content "$file" -Value $content -Force
return
}
if ($IsMacOS) {
$file = "~/.zprofile"
$content = (Get-Content "$file" -ErrorAction Ignore -Raw) + [System.String]::Empty
$content = [System.Text.RegularExpressions.Regex]::Replace($content, $pattern, [String]::Empty);
$content += [System.Environment]::NewLine + [System.Environment]::NewLine + "export $Name=$Value > /dev/null ; # $Name"
Set-Content "$file" -Value $content -Force
return
}
throw "Invalid platform."
}
function Set-PersistentEnvironmentVariable
Set a variable/value in actual process and system. This function calls Set-LocalEnvironmentVariable function to set process scope variables and perform task for set variables in machine scope.
On Windows you can use:
[Environment]::SetEnvironmentVariable with machine scope, user or machine don't work on Linux or MacOS
setx command
On Linux we can add export VARIABLE_NAME=Variable value to file ~/.bash_profile. For a new bash terminal the process execute these instructions located in ~/.bash_profile.
On MacOS similiar to Linux but if you have zsh terminal the file is .zprofile, if the default terminal is bash, the file is .bash_profile. In my function code we need to add detection of default terminal if you wish. I assume that default terminal is zsh.
function Set-LocalEnvironmentVariable
Set a variable/value in actual process. Using Drive env:.
Examples
#Set "Jo" value to variable "NameX", this value is accesible in current process and subprocesses, this value is accessible in new opened terminal.
Set-PersistentEnvironmentVariable -Name "NameX" -Value "Jo"; Write-Host $env:NameX
#Append value "ma" to current value of variable "NameX", this value is accesible in current process and subprocesses, this value is accessible in new opened terminal.
Set-PersistentEnvironmentVariable -Name "NameX" -Value "ma" -Append; Write-Host $env:NameX
#Set ".JomaProfile" value to variable "ProfileX", this value is accesible in current process/subprocess.
Set-LocalEnvironmentVariable "ProfileX" ".JomaProfile"; Write-Host $env:ProfileX
Output
Windows 10
Ubuntu WSL
References
Check About Environment Variables
Shell initialization files
ZSH: .zprofile, .zshrc, .zlogin - What goes where?
You can use the following code to set an environment variable in PowerShell if it doesn't exist:
if (!(Test-Path -Path Env:VAR_NAME)) {
New-Item -Path Env:VAR_NAME -Value "VAR_VALUE"
}
Replace VAR_NAME with the name of the environment variable and VAR_VALUE with the desired value.
Related
Given:
PowerShell 5.1
Azure DevOps Server 2019
I'm trying to call my function directly from my Azure PowerShell Task Arguments, is this possible? I'm not getting any expected output.
param([String] $Name, [Int] $Age, [String] $Path)
Function Foo
{
Param(
[String]
$Name
,
[Int]
$Age
,
[string]
$Path
)
Process
{
write-host "Hello World"
If ("Tom","Dick","Jane" -NotContains $Name)
{
Throw "$($Name) is not a valid name! Please use Tom, Dick, Jane"
}
If ($age -lt 21 -OR $age -gt 65)
{
Throw "$($age) is not a between 21-65"
}
IF (-NOT (Test-Path $Path -PathType ‘Container’))
{
Throw "$($Path) is not a valid folder"
}
# All parameters are valid so New-stuff"
write-host "New-Foo"
}
}
Update 3
Update 2
Update 1
If you execute your script directly, it will simply define the Foo function, but never call it.
Place the following after the function definition in your script in order to invoke it with the arguments that the script itself received, using the automatic $args variable via splatting, which allows you to pass arguments via a variable containing an array or hashtable of parameter values, which needs to be referenced with # rather than $:
Foo #args
The alternative would be not to invoke a script file, but a piece of PowerShell code (in PowerShell CLI terms, this means using the -Command parameter rather than -File), which would allow you to use ., the dot-sourcing operator to first load the function definition into the caller's scope, which then allows it to be called:
. "$(System.DefaultWorkingDirectory)/_RodneyConsole1Repo/FunctionExample.ps1"
Foo -Name Rodney -Age 21 -Path ""
I have the following variable $Obj set to the following string value:
$Obj = '#{Version=1; Name=a;}'
How do I convert this value from a string into a custom psobject?
I would like to be able to call
$Obj.Version and get the value 1. Currently this call returns nothing.
Note: Due to how I am retrieving this variable, I can't initialize it without the single quotes.
Edit:
Here is the current code:
$Command = "script.ps1 -ExtraInfo $_"
Write-Host $Command
Invoke-Expression -Command $Command
where $_ is #{Version=1; Name=a;} (without the quote)
Originally this code was written as
. script.ps1 -ExtraInfo $_
and worked, but when I added unit tests I changed it to use Invoke-Expression so that it could be testable with Pester unit tests. Is there a better way?
Edit2:
Turns out this can be solved by putting a back tic ` before the expression and that solves the issue for me. Thank you everyone for your input.
$Command = "script.ps1 -ExtraInfo `$_"
Write-Host $Command
Invoke-Expression -Command $Command
The stringified version of a [pscustomobject] instance, which resembles a hashtable literal, is not suitable for programmatic processing, as the following example demonstrates:
# Stringify a [pscustomobject] instance.
PS> "$([pscsutomobject] #{ Version=1; Name='a value' })"
#{Version=1; Name=a value} # !! Quoting of the .Name value was lost
The problem gets worse for property values that are themselves complex objects.
Since you do appear to have access to the original [pscustomobject] instance, the solution is not to stringify.
For that, you simply need to avoid up-front string interpolation by using a verbatim (single-quoted) string literal ('...') and letting Invoke-Expression - which should generally be avoided - interpret the $_ variable as its original type:
# Use *single* quotes to prevent up-front expansion.
$Command = 'script.ps1 -ExtraInfo $_'
Write-Host $Command
Invoke-Expression -Command $Command
Note that the use of a verbatim (non-interpolating) string literal makes the use of Invoke-Expression safe here, though, as Santiago Squarzon points out, there may be a better alternatives in general, and particularly in the context of Pester.
A script-block-based solution ({ ... }) that receives the object as an argument:
$Command = { script.ps1 -ExtraInfo $args[0] }
Write-Host "Calling { $Command } with argument $_"
. $Command $_
This doesn't work with Name=a because a is not a known object (or at least not defined in my PS Session). But if this is a string, this can be done with the following script:
$Obj = '#{Version=1; Name="a";}'
$s= [System.Management.Automation.ScriptBlock]::Create("New-Object -TypeName PSObject -Property $Obj")
$o = Invoke-Command -ScriptBlock $s
$o.Version
As I stated in my comment, this is odd, and should be resolved earlier in the code base. However, if that is not possible, use Invoke-Expression
like so
$newObj = Invoke-Expression $Obj
Further reading on Invoke-Expression
I have created a script in the Azure PowerShell.
If I use the "echo" command, it displays output to the console.
However, if I use Write-Output and Write-Error, I don't see the output.
I have uploaded the script "change-to-static.ps1" to a storage account. Then I open the "Cloud Shell" with a button at the top bar. Then I type "./change-ip-to-static.ps1" in the PowerShell console.
Therefore, the script does not produce any output unless I replace "Write-Output" and "Write-Error" with "echo" or "print".
Please help me. What should I do to see the output?
The script is below.
There is a similar question at How to output something in PowerShell. I have read it, but there are no concrete examples on how to achieve my goal, i.e. how to modify my script to see the output. And in my case, it does not output even if I redirect to a text file. However, commands like "echo" and "print" in my case work but they are not covered in the above example. See the script below.
$IPs = Get-AzPublicIpAddress;
$Static = "Static";
foreach ($PublicIP in $IPs) {
$Method = $PublicIP.PublicIpAllocationMethod;
$Name = $PublicIP.Name;
if ($Method -eq $Static) {
$message = "The method of " + $Name + " is already " + $Static;
Write-Progress -Activity $message;
}
else {
Write-Progress -Activity "Changing the method of "+$Name+" from "+$Method+" to "+$Static+"...";
$PublicIP.PublicIpAllocationMethod = $Static;
Set-AzPublicIpAddress -PublicIpAddress $PublicIP;
Write-Progress -Activity "Querying the method of "+$Name+"...";
$ModifiedAddress = Get-AzPublicIpAddress -Name $Name -ResourceGroupName $PublicIP.ResourceGroupName -Location $PublicIP.Location
$NewMethod = $ModifiedAddress.PublicIpAllocationMethod;
if ($NewMethod -eq $Static) {
Write-Output "The method for "+$Name+" has successfully changed to "+$Static;
}
else {
Write-Error -Message "Cannot change the method for "+$Name+" to "+$Static+", it is still "+$NewMethod+"!!!";
}
}
}
P.S. I have updated the script (use this URL) according to the suggestions, but there is still no output. Only "echo" or "print" gives the output.
P.P.S. The Write-Progress does not even show a temporary message in the status line during Set-AzPublicIpAddress which takes a couple of seconds to complete, or if I add the Start-Sleep cmdlet. It does only set during Get-AzPublicIpAddress.
After reading your last edit to my answer, I believe you made a bit of confusion in using Write-* commandlets, and in your script logic, so I provided a more detailed answer with context.
echo in the Powershell Azure Cloud Shell is an alias of Write-Output, as executing echo without parameters clearly shows (docs here ).
PS /home/mikelangelo> echo
cmdlet Write-Output at command pipeline position 1
Supply values for the following parameters:
InputObject:
Moreover: the unix echo can also be run in the Powershell Azure Cloud Shell.
PS /home/mikelangelo> which echo
/usr/bin/echo
PS /home/mikelangelo> /usr/bin/echo ciao mondo
ciao mondo
print, on the other hand, is not a Powershell alias, so the unix counterpart is the one which always get executed when using the print keyword (presently a symlink to run-mailcap - but it's not clear to me how it comes into play into your use case.)
PS /home/mikelangelo> which print
/usr/bin/print
So, basically, echo and Write-Output will both work, because they call the same commandlet, unless you execute /usr/bin/echo directly, mixing up technologies and effectively impairing portability.
Back to the question:
Write-Output works as expected. The logic is faulty: You use = as a comparison operator, but you need to use -eq instead.
Write-Progress needs to be used differently, replace it with Write-Host or Write-Output. Refer to the docs for an explanation.
Note that Write-Output sends an object down the pipeline, which can eventually be represented as a console output.
Write-Progress and Write-Host, on the other hand, do not generate output - the latter sends an object to the host for displaying, though, so Write-Host is the recommended way to display something in the console. Refer to this question for more details on Write-Host, Write-Output and the Powershell pipeline.
Like other commenters before me I can also confirm that the code from your gist works just fine in Azure Cloud Shell.
I noticed that there is only an output if you have at least one dynamic public ip that the script can change to static. The reason is, that only in this case you use Write-Output to return a string to the console.
If there is no dynamic public ip left, your script only writes a progress message, but you never get to see it, as the script execution ends too quickly after you write the message and progress messages don't linger.
Put the command Start-Sleep -Seconds 2 under the line with Write-Progress and you will see what I mean:
$IPs = Get-AzPublicIpAddress;
$Static = "Static";
foreach ($PublicIP in $IPs) {
$Method = $PublicIP.PublicIpAllocationMethod;
$Name = $PublicIP.Name;
if ($Method -eq $Static) {
$message = "The method of $Name is already $Static";
Write-Progress -Activity $message;
Start-Sleep -Seconds 2 # This will keep the script running 2 seconds longer and the message visible.
}
else {
Write-Progress -Activity "Changing the method of $Name from $Method to $Static ...";
$PublicIP.PublicIpAllocationMethod = $Static;
Set-AzPublicIpAddress -PublicIpAddress $PublicIP;
Write-Progress -Activity "Querying the method of $Name ...";
$ModifiedAddress = Get-AzPublicIpAddress -Name $Name -ResourceGroupName $PublicIP.ResourceGroupName
$NewMethod = $ModifiedAddress.PublicIpAllocationMethod;
if ($NewMethod -eq $Static) {
Write-Output "The method for $Name has successfully changed to $Static";
}
else {
Write-Error -Message "Cannot change the method for $Name to $Static, it is still $NewMethod!!!";
}
}
}
Write-Progress is probably not the cmdlet that you want to use to write out the progress of your script (despite it's name). As I do not see how you would need to further process the output of your script, you might as well replace it with Write-Host.
i got some problem while preparing some useful script based on PS.
I try to make script which will collect data from Active Directory and Exchange (users/mailboxes), then this data will be processed in further part of script (in some function for example)
function toProcess($userObj, $mailboxObj)
{
echo $userObj.enabled #the output is null
}
$users = get-adusers -Filter * -properties *
foreach($user in $users)
{
$guid = $user.objectGuid.toString()
if($user.mail -ne $null)
{
$mailbox = get-mailbox $guid | select-object *
if($mailbox -ne $null)
{
toProcess($user, $mailbox)
}
}
}
When there is assigned only one parameter ($user) to the onProcess(), function executes correctly and display status of account. When i assign two objects, values becomes null.
What's wrong?
I use powershell 2.0
When calling PowerShell functions, arguments are separated by spaces, and parentheses are not needed.
Your function call should look like this.
toProcess $user $mailbox
By placing a comma between the variables, you were creating a single argument that is an array of objects.
What's the DOS FINDSTR equivalent for PowerShell? I need to search a bunch of log files for "ERROR".
Here's the quick answer
Get-ChildItem -Recurse -Include *.log | select-string ERROR
I found it here which has a great indepth answer!
For example, find all instances of "#include" in the c files in this directory and all sub-directories.
gci -r -i *.c | select-string "#include"
gci is an alias for get-childitem
Just to expand on Monroecheeseman's answer. gci is an alias for Get-ChildItem (which is the equivalent to dir or ls), the -r switch does a recursive search and -i means include.
Piping the result of that query to select-string has it read each file and look for lines matching a regular expression (the provided one in this case is ERROR, but it can be any .NET regular expression).
The result will be a collection of match objects, showing the line matching, the file, and and other related information.
if ($entry.EntryType -eq "Error")
Being Object Oriented, you want to test the property in question with one of the standard comparison operators you can find here.
I have a PS script watching logs remotely for me right now - some simple modification should make it work for you.
edit: I suppose I should also add that is a cmdlet built for this already if you don't want to unroll the way I did. Check out:
man Get-EventLog
Get-EventLog -newest 5 -logname System -EntryType Error
On a related note, here's a search that will list all the files containing a particular regex search or string. It could use some improvement so feel free to work on it. Also if someone wanted to encapsulate it in a function that would be welcome.
I'm new here so if this should go in it's own topic just let me know. I figured I'd put it her since this looks mostly related.
# Search in Files Script
# ---- Set these before you begin ----
$FolderToSearch="C:\" # UNC paths are ok, but remember you're mass reading file contents over the network
$Search="Looking For This" # accepts regex format
$IncludeSubfolders=$True #BUG: if this is set $False then $FileIncludeFilter must be "*" or you will always get 0 results
$AllMatches=$False
$FileIncludeFilter="*".split(",") # Restricting to specific file types is faster than excluding everything else
$FileExcludeFilter="*.exe,*.dll,*.wav,*.mp3,*.gif,*.jpg,*.png,*.ghs,*.rar,*.iso,*.zip,*.vmdk,*.dat,*.pst,*.gho".split(",")
# ---- Initialize ----
if ($AllMatches -eq $True) {$SelectParam=#{AllMatches=$True}}
else {$SelectParam=#{List=$True}}
if ($IncludeSubfolders -eq $True) {$RecurseParam=#{Recurse=$True}}
else {$RecurseParam=#{Recurse=$False}}
# ---- Build File List ----
#$Files=Get-Content -Path="$env:userprofile\Desktop\FileList.txt" # For searching a manual list of files
Write-Host "Building file list..." -NoNewline
$Files=Get-ChildItem -Include $FileIncludeFilter -Exclude $FileExcludeFilter -Path $FolderToSearch -ErrorAction silentlycontinue #RecurseParam|Where-Object{-not $_.psIsContainer} # #RecurseParam is basically -Recurse=[$True|$False]
#$Files=$Files|Out-GridView -PassThru -Title 'Select the Files to Search' # Manually choose files to search, requires powershell 3.0
Write-Host "Done"
# ---- Begin Search ----
Write-Host "Searching Files..."
$Files|
Select-String $Search #SelectParam| #The # instead of $ lets me pass the hastable as a list of parameters. #SelectParam is either -List or -AllMatches
Tee-Object -Variable Results|
Select-Object Path
Write-Host "Search Complete"
#$Results|Group-Object path|ForEach-Object{$path=$_.name; $matches=$_.group|%{[string]::join("`t", $_.Matches)}; "$path`t$matches"} # Show results including the matches separated by tabs (useful if using regex search)
<# Other Stuff
#-- Saving and restoring results
$Results|Export-Csv "$env:appdata\SearchResults.txt" # $env:appdata can be replaced with any UNC path, this just seemed like a logical place to default to
$Results=Import-Csv "$env:appdata\SearchResults.txt"
#-- alternate search patterns
$Search="(\d[-|]{0,}){15,19}" #Rough CC Match
#>
This is not the best way to do this:
gci <the_directory_path> -filter *.csv | where { $_.OpenText().ReadToEnd().Contains("|") -eq $true }
This helped me find all csv files which had the | character in them.
PowerShell has basically precluded the need for findstr.exe as the previous answers demonstrate. Any of these answers should work fine.
However, if you actually need to use findstr.exe (as was my case) here is a PowerShell wrapper for it:
Use the -Verbose option to output the findstr command line.
function Find-String
{
[CmdletBinding(DefaultParameterSetName='Path')]
param
(
[Parameter(Mandatory=$true, Position=0)]
[string]
$Pattern,
[Parameter(ParameterSetName='Path', Mandatory=$false, Position=1, ValueFromPipeline=$true)]
[string[]]
$Path,
[Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
$LiteralPath,
[Parameter(Mandatory=$false)]
[switch]
$IgnoreCase,
[Parameter(Mandatory=$false)]
[switch]
$UseLiteral,
[Parameter(Mandatory=$false)]
[switch]
$Recurse,
[Parameter(Mandatory=$false)]
[switch]
$Force,
[Parameter(Mandatory=$false)]
[switch]
$AsCustomObject
)
begin
{
$value = $Pattern.Replace('\', '\\\\').Replace('"', '\"')
$findStrArgs = #(
'/N'
'/O'
#('/R', '/L')[[bool]$UseLiteral]
"/c:$value"
)
if ($IgnoreCase)
{
$findStrArgs += '/I'
}
function GetCmdLine([array]$argList)
{
($argList | foreach { #($_, "`"$_`"")[($_.Trim() -match '\s')] }) -join ' '
}
}
process
{
$PSBoundParameters[$PSCmdlet.ParameterSetName] | foreach {
try
{
$_ | Get-ChildItem -Recurse:$Recurse -Force:$Force -ErrorAction Stop | foreach {
try
{
$file = $_
$argList = $findStrArgs + $file.FullName
Write-Verbose "findstr.exe $(GetCmdLine $argList)"
findstr.exe $argList | foreach {
if (-not $AsCustomObject)
{
return "${file}:$_"
}
$split = $_.Split(':', 3)
[pscustomobject] #{
File = $file
Line = $split[0]
Column = $split[1]
Value = $split[2]
}
}
}
catch
{
Write-Error -ErrorRecord $_
}
}
}
catch
{
Write-Error -ErrorRecord $_
}
}
}
}
FYI:
If you update to Powershell version 7 you can use grep...
I know egrep is in powershell on Azure CLI...
But SS is there!
An old article here: [https://devblogs.microsoft.com/powershell/select-string-and-grep/]