Hey, Scripting Guy! Can I Use Windows PowerShell to Read a Text File and Update an Environment Variable on Remote Computers?

Hey, Scripting Guy! Can I Use Windows PowerShell to Read a Text File and Update an Environment Variable on Remote Computers?

  • Comments 2
  • Likes

 

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I need to be able to read a text file, and connect to a number of different remote machines to update an environment variable. Is this something that can be done using Windows PowerShell?

-- ES

 

Hey, Scripting Guy! AnswerHello ES,

Microsoft Scripting Guy Ed Wilson here. We looked at that specific scenario during the 2010 Scripting Games. In fact, there are VBScript examples and Windows PowerShell examples of accomplishing this task written by our expert commentators already posted. The requirements for that event were posted earlier. Rather than re-do the solutions that were written by our expert commentators, I will instead take the opportunity to review one of the scripts submitted by a contestant. It is a good script and it is obvious the writer spent a decent amount of time and effort in its composition; however, it has a number of areas where it could be improved upon.

The complete ADV_EddySteenbergen.ps1 script is shown here.

ADV4_EddySteenbergen.ps1

# 2010 Scripting Games: Advanced Event 4--Creating an Environment Variable
# author: eddy steenbergen
# acknowledgements for environment variable operations:
# http://www.vistax64.com/powershell/156781-wmiclass.html
# http://www.vistax64.com/powershell/25088-wmi-instances-put-options.html
PARAM(
[Parameter(Mandatory=$false)] [string]$computerlist,
[Parameter(Mandatory=$false)] [switch]$silent = $false,
[Parameter(Mandatory=$false)] [string]$username = "<SYSTEM>",
[Parameter(Mandatory=$false)] [string]$varname = "ScriptingGuys",
[Parameter(Mandatory=$false)] [string]$vardata = "ScriptingGuys.com",
[Parameter(Mandatory=$false)] [string]$logfile,
[Parameter(Mandatory=$false)] [switch]$helpme = $false
)
#requires -version 2.0
$version = "1.3"
Function ShowHelp
{
$msg = "GENERAL SCRIPT HELP`n-------------------"
$msg = "$msg`nThis script checks computers on the network named in an input file."
$msg = "$msg`nIt will attempt to create or update an environment variable on each specified computer."
$msg = "$msg`n"
$msg = "$msg`nTo specify the computer(s) to check use:"
$msg = "$msg`n`t-computerlist <textfilecontaining list of computer names>"
$msg = "$msg`n"
$msg = "$msg`nTo specify the name of the environment variable, use `"-varname`". e.g. -varname myvariable"
$msg = "$msg`nDefault value for varname is `"$varname`"."
$msg = "$msg`n"
$msg = "$msg`nIf the variable owned by a user, enter `"-username <logon>`"."
$msg = "$msg`nTo enter a domain logon use : domain\\logon."
$msg = "$msg`nIf the -username parameter is omitted then the variable is assumed to be a system variable."
$msg = "$msg`n"
$msg = "$msg`nTo specify the value to be given to the variable use `"-vardata`". e.g. -vardata `"xyz`"."
$msg = "$msg`nDefault value is `"$vardata`""
$msg = "$msg`n"
$msg = "$msg`nThe script will append the results of its work to a file specified by `"-logfile`". e.g. -logfile d:\swdev\elog4.txt"
$msg = "$msg`nThis parameter is MANDATORY."
$msg = "$msg`n"
$msg = "$msg`nIf the specified variable already exists then the script will by default ask"
$msg = "$msg`nthe user whether to overwrite the current value."
$msg = "$msg`nTo force the script to overwrite without user intervention, use `"-silent`"."
$msg = "$msg`n"
$msg = "$msg`nUse the parameter"
$msg = "$msg`n`t-helpme"
$msg = "$msg`nto get this help. If present, overrides all other parameters."
WriteProgressText $msg
}
Function WriteProgressText([string] $msg) { Write-Host -ForegroundColor DarkGreen $msg }
Function WriteErrorText([string] $msg) { Write-Host -ForegroundColor Red $msg }
Function IsLocalAdmin([string] $computer)
# returns $true if current credentials have local admin rights on named computer, $false otherwise
{
Test-Path \\$computer\admin`$
}
Function LogComputerUpdate([string] $computer)
{
$timestamp = (get-date).Tostring("yyyy/MM/dd hh:mm:ss")
"$computer,$timestamp" | out-file $logfile -append
}
Function IsOnline([string] $computer)
# returns $true if named computer can be pinged, $false otherwise
{
(gwmi -class win32_pingstatus -filter ("address='$computer'") -computername .).statuscode -eq 0
}
Function UpdateEnvVar([string] $computer)
# returns $true if successful,$false otherwise
{
# check for prior existence
$myfilter = "(Name='$varname') and (UserName='$username')"
$myresults = $null
$myresults = gwmi -computer $computer -class Win32_Environment -filter "$myfilter" -ErrorAction SilentlyContinue
if ($myresults -ne $null) {
if ($silent -eq $false) {
Write-Host ("Var `"$varname`" already exists on computer `"$computer`" for user `"$username`"`n")
$response = Read-Host -Prompt ("To overwrite current value [`"" + ($myresults.VariableValue) + "`"] hit `"y`" then Enter (to skip just hit Enter) >")
if ($response -notmatch "^y") {
return $false
}
}
}
$myinstance = ([WMIClass]"\\$computer\root\cimv2:Win32_environment").createinstance()
$myinstance.Name = $varname
$myinstance.UserName = $username # SystemVariable automatically set according to UserName
$myinstance.VariableValue = $vardata
$myputoptions = New-Object System.Management.PutOptions
$myputoptions.Type = [int] [system.management.puttype]::updateorcreate
$myinstance.Put($myputoptions)
}
Function CheckOneComputer([string] $computer)
{
if (IsOnline $computer) {
#WriteProgressText "Computer $computer online."
if (IsLocalAdmin $computer) {
#WriteProgressText "Current logon has local admin rights on the computer $computer."
$result = UpdateEnvVar $computer
} else {
WriteErrorText "Current logon does not appear to have local admin rights on the computer $computer."
}
} else {
WriteErrorText "Computer $computer does not appear to be online."
}
}
#-------------------------------------------------------------------------------
# main line code starts here
#-------------------------------------------------------------------------------
WriteProgressText "`n`nVersion $version"
# process help param if any
if ($helpme) {
cls
ShowHelp
return
}
# process logfile param if any
if ($logfile) {
if (!(Test-Path $logfile)) {
Set-Content -Path $logfile -Value $null -Encoding Ascii
}
if (!(Test-Path $logfile)) {
WriteErrorText "No log file with the name `"$logfile`" could be found or created."
WriteErrorText "Script aborted."
ShowHelp
return
}
} else {
WriteErrorText "No log file was specified in the command line."
WriteErrorText "Script aborted."
ShowHelp
return
}
# now act on computer list if any
if ($computerlist) {
if (Test-Path $computerlist) {
gc $computerlist | % { CheckOneComputer $_ }
} else {
WriteErrorText "No input file with the name `"$computerlist`" could be found."
WriteErrorText "Script aborted."
ShowHelp
return
}
} else {
WriteErrorText "No input file was specified in the command line."
WriteErrorText "Script aborted."
ShowHelp
return
}

Let’s begin our critique from the beginning of the script. There is no reason to mark every parameter Mandatory = $false because this is the default behavior anyway. In addition, if none of the parameters are marked as mandatory, why clutter the script? Incidentally, it might have been a good idea to make the environment variable and its value stored in the $varname, and the $vardata variables mandatory parameters. You absolutely should make the input file a mandatory parameter because the script displays an error if it is not supplied, which is shown in the following image.

Image of error displayed when input file is not mandatory parameter

In addition, there is no reason to assign the value of $false to a switched parameter—all you need to do is to check to see if the switched parameter exists. In addition, I tend to group my switched parameters together because they should be exceptions to the normal operation of the script.

There is no reason to write your own help function. It is simply a lot of extra work for no added value. Instead, you should use the Windows PowerShell help tags that were introduced for Windows PowerShell 2.0. They are much nicer, and they will make your script work like a cmdlet.

If I were going to build up a string on a series of lines, I would use a Here-String instead of the concatenation shown here:

$msg = "GENERAL SCRIPT HELP`n-------------------"
$msg = "$msg`nThis script checks computers on the network named in an input file."
$msg = "$msg`nIt will attempt to create or update an environment variable on each specified computer."

$msg

When the code above is run, the output seen here is displayed.

GENERAL SCRIPT HELP

-------------------

This script checks computers on the network named in an input file.

It will attempt to create or update an environment variable on each specified computer.

To use a Here-String to accomplish the same thing, you begin with an @” symbol. You next type the text the way you would like it to appear. On the last line you reverse the sequence “@. This is shown here:

$msg = @"

GENERAL SCRIPT HELP

-------------------

This script checks computers on the network named in an input file.

It will attempt to create or update an environment variable on each specified computer.

"@

$msg

The IsLocalAdmin function is pretty interesting because it relies upon being able to access the admin$ share. Note that the dollar sign needs to be escaped because it is a special character. Therefore, you see the `$ in use in the function. One problem with this function is that it does not work on a local computer—a nonadministrator will be able to access the path. The IsLocalAdmin function is shown here:

Function IsLocalAdmin([string] $computer)
# returns $true if current credentials have local admin rights on named computer, $false otherwise
{
Test-Path \\$computer\admin`$
}

A better way to test for administrative rights is seen here.

function Test-IsAdministrator

{

<#

.Synopsis

Tests if the user is an administrator

.Description

Returns true if a user is an administrator, false if the user is not an administrator

.Example

Test-IsAdministrator

#>

param()

$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()

(New-Object Security.Principal.WindowsPrincipal $currentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)

} #end function Test-IsAdministrator

The function IsOnline is used to send a ping to a remote computer. It uses the Win32_PingStatus WMI class to do this, and it looks for a status code of 0, which means that the ping completed successfully. This is shown here:

Function IsOnline([string] $computer)
# returns $true if named computer can be pinged, $false otherwise
{
(gwmi -class win32_pingstatus -filter ("address='$computer'") -computername .).statuscode -eq 0
}

In Windows PowerShell 2.0, there is no reason to write your own ping function because there is the Test-Connection cmdlet. To send one ping to a remote computer named hyperv, use the syntax shown here:

Test-Connection -ComputerName hyperv -Count 1

To see if the environment variable exists, the script uses WMI to query for the value. If it does not exist, an error is returned. Because the script uses the –erroractionpreference silentlycontinue for the Get-WmiObject cmdlet, no error is displayed in the Windows PowerShell console. If the value does exist, it prompts you to change the value. This is shown here:

$myresults = $null
$myresults = gwmi -computer $computer -class Win32_Environment -filter "$myfilter" -ErrorAction SilentlyContinue
if ($myresults -ne $null) {
if ($silent -eq $false) {
Write-Host ("Var `"$varname`" already exists on computer `"$computer`" for user `"$username`"`n")
$response = Read-Host -Prompt ("To overwrite current value [`"" + ($myresults.VariableValue) + "`"] hit `"y`" then Enter (to skip just hit Enter) >")
if ($response -notmatch "^y") {
return $false
}
}
}

One last thing about the script: It has a number of lines that are commented out. These would allow you to set a different level of tracing for calling the writeprogresstext function. Here is an example of one such line:

#WriteProgressText "Computer $computer online."

The function itself is simple, and uses Write-Host to display text in red. This is shown here:

Function WriteErrorText([string] $msg) { Write-Host -ForegroundColor Red $msg }

Windows PowerShell has a Write-Debug cmdlet and a Write-Verbose cmdlet that can be used to provide additional tracing or debugging information. These cmdlets do not require you to comment and uncomment lines of text in your script to provide this type of information. The use of the Write-Debug cmdlet is detailed in How Can I Use WMI to Set the Screen Saver Timeout on Workstations.

ES, that is all there is to using Windows PowerShell to work with environment variables. The 2010 Scripting Games Wrap-Up Week will continue tomorrow.

 

If you want to know exactly what we will be looking at tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys


Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • <p>Like regular strings, variables are replaced by their values in double-quoted here-strings. In single-quoted here-strings, variables are not replaced by their values. In this particular case, we don&#39;t have a variable in a here-string, so I would suggest using single-quoted here-string.</p> <p>$msg = @&#39;</p> <p>GENERAL SCRIPT HELP</p> <p>-------------------</p> <p>This script checks computers on the network named in an input file.</p> <p>It will attempt to create or update an environment variable on each specified computer.</p> <p>&#39;@</p>

  • <p>If you really want to mimic a behaviour of a contestant&#39;s IsOnline function, and return $true if a computer can be pinged, and $false otherwise, you should consider using Quiet parameter: </p> <p>Test-Connection -ComputerName hyperv -Count 1 -Quiet</p>