Creating CDXML Modules (Part 3)

Discover how you can manage environment variables via CDXML and learn how to add, modify and remove instances.

In our previous parts of this series you learned how CDXML can turn WMI classes into PowerShell cmdlets and how you can retrieve instances and call methods. We already covered a lot of ground, so in case you want to go back and refresh some concepts before reading on, here is a quick summary of the previous parts:

  • CIM Cmdlets: learn what WMI really is and how manual WMI queries work.
  • CDXML Intro: understand how a CDXML file can be used to auto-generate PowerShell cmdlets that are much easier to use than manual WMI queries.
  • Custom Types: turn cryptic numeric values into friendly text by defining custom types (enums) right in your CDXML definition.
  • Confirmation Support: add support for automatic confirmation dialogs so PowerShell automatically asks for confirmation when a user is about to call a method that you think might be dangerous.
  • Example Win32_OperatingSystem: test-drive a fully working example of a CDXML module that lets you access the WMI class Win32_OperatingSystem including calling methods to log off users, shut down or restart the system, and set the system date and time.
  • Advanced Queries: add sophisticated query parameters to your CDXML-generated cmdlets that use fast server-side filters to provide you with exactly the instances you need.
  • Return Information: understand how you can control the information returned by calls to WMI methods.
  • Example Win32_Service: test-drive a fully working example of a CDXML module that lets you leverage the WMI class Win32_Service. Thanks to sophisticated query parameters you can search for services, change service settings, and return the service security descriptors.

In this part, I’ll look at the WMI class Win32_Environment. Each instance represents one environment variable. Specifically, I’ll be looking at how you can create new environment variables and delete old ones.

I’ll also discuss Windows environment variables in general so there’s a lot to learn even if you’re not particularly interested in CDXML.

Environment Variables

Environment variables work like a bulletin board where anyone can post information. The operating system for example, among other things, “posts” its home path so everyone knows where the windows folder is located. It also posts the computer name and the name of the currently logged on user:

"Windows Folder: $env:windir"
"Computer Name: $env:computername"
$username = '{0}\{1}' -f $env:userdomain, $env:username
"You are: $username"

As you see, reading this “bulletin board” is simple because in PowerShell, all environment variables surface in drive env:. To discover the names of all environment variables, use Get-ChildItem:

Get-ChildItem -Path env:

WMI is not required when you just want to read environment variables. WMI and its class Win32_Environment become important once you want to manage environment variables, i.e. change them, or add or delete environment variables - locally or remote.

For reasons that are obscure to many, the drive env: behaves weird once you start and add or change things. So let’s quickly go over the architecture of environment variables.

# change process set environment variables
# adding drive Z: to list of locations in environment variable "path":
$env:path += ";z:\"
# adding new environment variable:
$env:newVar = "Hello"

All changes to drive env: only apply to your currently running PowerShell and all programs you launched from there.

Strings Stored In Registry

Environment variables are stored in the Windows Registry in two locations:

  • Generic variables (“machine”): they are stored in the registry key HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Sessionmanager\Environment and can be read from any user. To change these variables, you need Administrator privileges.
  • Private variables (“user”): they are stored in the registry key HKEY_USERS\Environment and are accessible only for a specific user. These variables can be changed without Administrator privileges.

Each environment variable is just a registry value of type REG_SZ which emphasizes that environment variables exclusively store text.

Software Uses Snapshot Copy

Software (including PowerShell) typically is not using these original environment variables directly. Instead, whenever Windows launches a program, it reads all environment variables from both locations and composes a third set, the process set. This set works like a snapshot of the original environment variables, and it is a copy.

Whenever you launch a program from within another program, i.e. you run a program from within a PowerShell console, something different happens: you pass on your current process set to the program you launch.

This explains everything:

  • Since drive env: is a snapshot copy of the original environment variables, all changes to them that occur after you launched PowerShell are invisible to drive env:.
  • For the same reason, any change to drive env: occurs only in your private snapshot copy and is invisible to programs started by someone else.
  • When you launch a new program from within PowerShell, it receives your current private process set so all changes you made are also visible in the launched software. Any change to env: that you apply after the launch remains invisible.

  • And once you close your PowerShell session, your process set is discarded, and so are all changes to env:.

Most confusion occurs once users try and abuse the environment variables for purposes they weren’t intended for. As long as you use environment variables as a bulletin board that posts fundamental information that is not frequently changing, the limitations above won’t bite you.

As soon as you try and use environment variables as a way to communicate in real-time between processes, you are running in various issues.

Communicating Between Processes

To emphasize the architecture of environment variables once again, let’s look at what you could do if you wanted to communicate real-time information. For this, you’d have to bypass the process set and read and write the original environment variables directly.

Here are two functions that you can use to communicate between two different PowerShell sessions via environment variables: Set-Data writes text to the environment variable DataExchange, and Get-Datareads it.

The API functions SetEnvironmentVariable() and GetEnvironmentVariable() access the original environment variable and bypass the cached process set:

function Set-Data([Parameter(Mandatory)][string][AllowEmptyString()]$text)
{
	[Environment]::SetEnvironmentVariable('DataExchange', $text, 'user')
}

function Get-Data
{
	[Environment]::GetEnvironmentVariable('DataExchange', 'user')
}

Start two PowerShell instances, and run above code in both of them. Next, run this in the first session:

Set-Data "Hello World!"

Once you run Set-Data, you’ll get a taste of the issues you run into when abusing environment variables this way: the command has a considerable delay of a couple of seconds (more later).

To delete the environment variable DataExchange, submit an empty string:

Set-Data ''

Switch to the other session, and run this to retrieve the value:

Get-Data

Don’t Change Frequently

Environment variables are designed to be a simple information system where software can look up important paths and other information. Environment variables are not designed to be a storage system for information that frequently changes.

The delay you experienced with Set-Data was caused by a broadcast message sent to all open applications, informing them of a change to environment variables. This broadcast message can take considerable time, and it is crucial for environment variables but complete nonsense for your real-time communications.

Actually, even though theoretically any application could listen for the broadcast message, the Windows Explorer is the only one who cares. Whenever the explorer receives the message, it rebuilds its process set from scratch by reading the original environment variables again.

That’s super important. Remember? Whenever you launch a program, its process set is a copy of the process set of the launching process. So if the Windows Explorer would not update its process set, any program you launch would always receive exactly the environment variables as they were defined when the explorer started. Put differently: without this message, you’d have to restart your computer for changes in environment variables to take effect.

Environment variables really are just registry keys, so if you just want to exchange information between processes, then simply use your own registry value. This way, you skip the broadcast message, and there is no delay anymore:

function Set-Data([Parameter(Mandatory)][string][AllowEmptyString()]$text)
{
	if ($text)
	{
		Set-ItemProperty -Path 'HKCU:\Software' -Name MyDataExchange -Value $text -Type String
	}
	else
	{
		Remove-ItemProperty -Path 'HKCU:\Software' -Name MyDataExchange
	}
}

function Get-Data
{
	(Get-ItemProperty -Path 'HKCU:\Software').MyDataExchange
}

Managing Environment Variables With WMI

If all you need is reading a environment variable then don’t bother using WMI and use the drive env: instead. If however you need to manage environment variables, i.e. query remotely, or add, change, and remove variables, then the WMI class Win32_Environment exists to help you manage environment variables.

Since this article is about CDXML, I dive right into creating CDXML-based cmdlets. If you’d rather like to use CIM Cmdlets to manually run WMI queries, then you find plenty of examples here.

Implementing Get-WmiEnvironment

Let’s start with a simple CDXML module that auto-generates the cmdlet Get-WmiEnvironment. With this cmdlet, you can easily search for environment variables based on a number of search criteria, locally and remotely:

<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Environment" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiEnvironment</DefaultNoun>

    <InstanceCmdlets>
      <GetCmdletParameters />
      <!-- defining Get-WmiEnvironment -->
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters>
          <!-- defining query parameters -->
          <QueryableProperties>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="SystemVariable">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="UserName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="VariableValue">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>
    </InstanceCmdlets>
  </Class>
</PowerShellMetadata>

Run this to write the CDXML definition to disk and import the module using Import-Module:

$testfolder = "$env:temp\cdxmlTest"

#region create module win32_environment (needs to be done only once):
$cdxml = @'
<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Environment" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiEnvironment</DefaultNoun>

    <InstanceCmdlets>
      <GetCmdletParameters />
      <!-- defining Get-WmiEnvironment -->
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters>
          <!-- defining query parameters -->
          <QueryableProperties>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="SystemVariable">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="UserName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="VariableValue">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>
    </InstanceCmdlets>
  </Class>
</PowerShellMetadata>
'@

# create a test folder if it does not exist
$exists = Test-Path -Path $testfolder -PathType Container
if (!$exists) { $null = New-Item -Path $testfolder -ItemType Directory }

# write cdxml file
$path = Join-Path -Path $testfolder -ChildPath 'win32_environment.cdxml'
$cdxml | Set-Content -Path $path -Encoding UTF8
#endregion create module win32_environment

# import the module
Import-Module -Name $path -Verbose -Force

It generates Get-WmiEnvironment. With this cmdlet, you can now query environment variables in a number of ways:

# dump all environment variables
Get-WmiEnvironment

# dump only system variables (machine)
Get-WmiEnvironment -SystemVariable

# dump only user variables (user)
Get-WmiEnvironment -SystemVariable:$false

# list all environment variables starting with "T"
Get-WmiEnvironment -Name t*

# find all variables that contain "temp"
Get-WmiEnvironment -VariableValue *temp*

# find all variables that apply to users containing "tobi"
Get-WmiEnvironment -UserName *tobi*"

Remote Access

Since Get-WmiEnvironment is CDXML-generated, it comes with built-in remoting capabilities, and you can use the parameter -CimSession to access one or many remote systems. This parameter accepts either a CIMSession object generated by New-CimSession, or a computer name.

For more information about remoting and its prerequisites, here you can find details.

Implementing New-WmiEnvironment

There is no method anywhere in Win32_Environment that would allow you to create new environment variables. To create new environment variables, you need to add new instances of Win32_Environment. This can be achieved via the internal method cim:CreateInstance.

Since there are no class instances “to begin with”, the method cim:CreateInstance is an example of a so-called static method. It does not require any instances and works directly on the WMI class. In the CDXML definition, there is a separate node StaticCmdlets that can be added for this.

Let’s update the CDXML and add the cmdlet: New-WmiEnvironment.

<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Environment" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiEnvironment</DefaultNoun>

    <InstanceCmdlets>
      <GetCmdletParameters/>
      <!-- defining Get-WmiEnvironment -->
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters>
          <!-- defining query parameters -->
          <QueryableProperties>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="SystemVariable">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="UserName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="VariableValue">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>
    </InstanceCmdlets>

    <StaticCmdlets>
      <Cmdlet>
        <CmdletMetadata Verb="New" Noun="WmiEnvironment" ConfirmImpact="Low" />
        <Method MethodName="cim:CreateInstance">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
          <Parameters>
            <Parameter ParameterName="Name">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="0" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="UserName">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="1" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="2" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>
    </StaticCmdlets>
  </Class>
</PowerShellMetadata>

Here is the code to update the module and import it again:

$testfolder = "$env:temp\cdxmlTest"

#region create module win32_environment (needs to be done only once):
$cdxml = @'
<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Environment" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiEnvironment</DefaultNoun>

    <InstanceCmdlets>
      <GetCmdletParameters/>
      <!-- defining Get-WmiEnvironment -->
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters>
          <!-- defining query parameters -->
          <QueryableProperties>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="SystemVariable">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="UserName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="VariableValue">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>
    </InstanceCmdlets>

    <StaticCmdlets>
      <Cmdlet>
        <CmdletMetadata Verb="New" Noun="WmiEnvironment" ConfirmImpact="Low" />
        <Method MethodName="cim:CreateInstance">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
          <Parameters>
            <Parameter ParameterName="Name">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="0" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="UserName">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="1" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="2" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>
    </StaticCmdlets>
  </Class>
</PowerShellMetadata>
'@

# create a test folder if it does not exist
$exists = Test-Path -Path $testfolder -PathType Container
if (!$exists) { $null = New-Item -Path $testfolder -ItemType Directory }

# write cdxml file
$path = Join-Path -Path $testfolder -ChildPath 'win32_environment.cdxml'
$cdxml | Set-Content -Path $path -Encoding UTF8
#endregion create module win32_environment

# import the module
Import-Module -Name $path -Verbose -Force

Once you run it, you now get another cmdlet: New-WmiEnvironment. Use it to create new environment variables:

$username = '{0}\{1}' -f $env:userdomain, $env:username
New-WmiEnvironment -Name testvar -UserName $username -VariableValue 1234

Please see the section about important limitations and workarounds at the end of this article.

Implementing Set-WmiEnvironment

To update and change the value of an existing environment variable, simply update the property VariableValue - it is the only property that is writeable. You can use the internal method cim:ModifyInstance to change the value of this property.

Let’s update the CDXML and add another cmdlet: Set-WmiEnvironment. Let’s add a query parameter for it, too:

<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Environment" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiEnvironment</DefaultNoun>

    <InstanceCmdlets>
      <!-- NEW: query parameter for method calls -->
      <GetCmdletParameters>
        <QueryableProperties>
          <Property PropertyName="Name">
            <Type PSType="system.string" />
            <RegularQuery AllowGlobbing="false">
              <CmdletParameterMetadata IsMandatory="false" />
            </RegularQuery>
          </Property>
        </QueryableProperties>
      </GetCmdletParameters>

      <!-- defining Get-WmiEnvironment -->
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters>
          <!-- defining query parameters -->
          <QueryableProperties>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="SystemVariable">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="UserName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="VariableValue">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>

      <!--Set-Environment: modifying instance properties-->
      <Cmdlet>
        <CmdletMetadata Verb="Set" ConfirmImpact="Low" />
        <!--using internal method to modify instance:-->
        <Method MethodName="cim:ModifyInstance">
          <Parameters>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>
    </InstanceCmdlets>
      
    <StaticCmdlets>
      <Cmdlet>
        <CmdletMetadata Verb="New" Noun="WmiEnvironment" ConfirmImpact="Low" />
        <Method MethodName="cim:CreateInstance">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
          <Parameters>
            <Parameter ParameterName="Name">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="0" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="UserName">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="1" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="2" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>
    </StaticCmdlets>
      
  </Class>
</PowerShellMetadata>

Here is the code to update the module and import it again:

$testfolder = "$env:temp\cdxmlTest"

#region create module win32_environment (needs to be done only once):
$cdxml = @'
<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Environment" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiEnvironment</DefaultNoun>

    <InstanceCmdlets>
      <!-- NEW: query parameter for method calls -->
      <GetCmdletParameters>
        <QueryableProperties>
          <Property PropertyName="Name">
            <Type PSType="system.string" />
            <RegularQuery AllowGlobbing="false">
              <CmdletParameterMetadata IsMandatory="false" />
            </RegularQuery>
          </Property>
        </QueryableProperties>
      </GetCmdletParameters>

      <!-- defining Get-WmiEnvironment -->
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters>
          <!-- defining query parameters -->
          <QueryableProperties>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="SystemVariable">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="UserName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="VariableValue">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>

      <!--Set-Environment: modifying instance properties-->
      <Cmdlet>
        <CmdletMetadata Verb="Set" ConfirmImpact="Low" />
        <!--using internal method to modify instance:-->
        <Method MethodName="cim:ModifyInstance">
          <Parameters>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>
    </InstanceCmdlets>
      
    <StaticCmdlets>
      <Cmdlet>
        <CmdletMetadata Verb="New" Noun="WmiEnvironment" ConfirmImpact="Low" />
        <Method MethodName="cim:CreateInstance">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
          <Parameters>
            <Parameter ParameterName="Name">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="0" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="UserName">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="1" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="2" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>
    </StaticCmdlets>
      
  </Class>
</PowerShellMetadata>
'@

# create a test folder if it does not exist
$exists = Test-Path -Path $testfolder -PathType Container
if (!$exists) { $null = New-Item -Path $testfolder -ItemType Directory }

# write cdxml file
$path = Join-Path -Path $testfolder -ChildPath 'win32_environment.cdxml'
$cdxml | Set-Content -Path $path -Encoding UTF8
#endregion create module win32_environment

# import the module
Import-Module -Name $path -Verbose -Force

Once you run this, the module now also exports Set-WmiEnvironment:

# create new environment variable
$username = '{0}\{1}' -f $env:userdomain, $env:username
New-WmiEnvironment -Name testvar -UserName $username -VariableValue 1234

# reading environment variable
Get-WmiEnvironment -Name testvar

# changing value
Set-WmiEnvironment -Name testvar -VariableValue 'NewValue'

# reading environment variable again
Get-WmiEnvironment -Name testvar

Please see the section about important limitations and workarounds at the end of this article.

Implementing Remove-WmiEnvironment

Now the only thing missing is a way to remove environment variables again. This is done by removing the unwanted instances using the internal method cim:RemoveInstance. Since this method is acting on an existing instance, it belongs to the node InstanceCmdlets and not StaticCmdlets:

<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Environment" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiEnvironment</DefaultNoun>

    <InstanceCmdlets>
      <!-- NEW: query parameter for method calls -->
      <GetCmdletParameters>
        <QueryableProperties>
          <Property PropertyName="Name">
            <Type PSType="system.string" />
            <RegularQuery AllowGlobbing="false">
              <CmdletParameterMetadata IsMandatory="false" />
            </RegularQuery>
          </Property>
        </QueryableProperties>
      </GetCmdletParameters>

      <!-- defining Get-WmiEnvironment -->
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters>
          <!-- defining query parameters -->
          <QueryableProperties>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="SystemVariable">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="UserName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="VariableValue">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>

      <!--Set-Environment: modifying instance properties-->
      <Cmdlet>
        <CmdletMetadata Verb="Set" ConfirmImpact="Low" />
        <!--using internal method to modify instance:-->
        <Method MethodName="cim:ModifyInstance">
          <Parameters>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>

      <!--Remove-Environment: invoking method cim:DeleteInstance():-->
      <Cmdlet>
        <!--defining the ConfirmImpact which indicates how severe the changes are that this cmdlet performs-->
        <CmdletMetadata Verb="Remove" Noun="WmiEnvironment" ConfirmImpact="Medium" />
        <!--defining the WMI instance method used by this cmdlet:-->
        <Method MethodName="cim:DeleteInstance">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
        </Method>
      </Cmdlet>
    </InstanceCmdlets>
      
    <StaticCmdlets>
      <Cmdlet>
        <CmdletMetadata Verb="New" Noun="WmiEnvironment" ConfirmImpact="Low" />
        <Method MethodName="cim:CreateInstance">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
          <Parameters>
            <Parameter ParameterName="Name">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="0" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="UserName">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="1" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="2" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>
    </StaticCmdlets>
      
  </Class>
</PowerShellMetadata>

Removing instances does not require any parameters which is why the newly added Cmdlet node is short.

Final Implementation

Here is the final implementation of this CDXML-defined PowerShell module:

$testfolder = "$env:temp\cdxmlTest"

#region create module win32_environment (needs to be done only once):
$cdxml = @'
<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Environment" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiEnvironment</DefaultNoun>

    <InstanceCmdlets>
      <!-- NEW: query parameter for method calls -->
      <GetCmdletParameters>
        <QueryableProperties>
          <Property PropertyName="Name">
            <Type PSType="system.string" />
            <RegularQuery AllowGlobbing="false">
              <CmdletParameterMetadata IsMandatory="false" />
            </RegularQuery>
          </Property>
        </QueryableProperties>
      </GetCmdletParameters>

      <!-- defining Get-WmiEnvironment -->
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters>
          <!-- defining query parameters -->
          <QueryableProperties>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="SystemVariable">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="UserName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="VariableValue">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>

      <!--Set-Environment: modifying instance properties-->
      <Cmdlet>
        <CmdletMetadata Verb="Set" ConfirmImpact="Low" />
        <!--using internal method to modify instance:-->
        <Method MethodName="cim:ModifyInstance">
          <Parameters>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>

      <!--Remove-Environment: invoking method cim:DeleteInstance():-->
      <Cmdlet>
        <!--defining the ConfirmImpact which indicates how severe the changes are that this cmdlet performs-->
        <CmdletMetadata Verb="Remove" Noun="WmiEnvironment" ConfirmImpact="Medium" />
        <!--defining the WMI instance method used by this cmdlet:-->
        <Method MethodName="cim:DeleteInstance">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
        </Method>
      </Cmdlet>
    </InstanceCmdlets>
      
    <StaticCmdlets>
      <Cmdlet>
        <CmdletMetadata Verb="New" Noun="WmiEnvironment" ConfirmImpact="Low" />
        <Method MethodName="cim:CreateInstance">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
          <Parameters>
            <Parameter ParameterName="Name">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="0" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="UserName">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="1" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
            <Parameter ParameterName="VariableValue">
              <Type PSType="system.string" />
              <CmdletParameterMetadata Position="2" IsMandatory="false">
                <ValidateNotNull />
                <ValidateNotNullOrEmpty />
              </CmdletParameterMetadata>
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>
    </StaticCmdlets>
      
  </Class>
</PowerShellMetadata>
'@

# create a test folder if it does not exist
$exists = Test-Path -Path $testfolder -PathType Container
if (!$exists) { $null = New-Item -Path $testfolder -ItemType Directory }

# write cdxml file
$path = Join-Path -Path $testfolder -ChildPath 'win32_environment.cdxml'
$cdxml | Set-Content -Path $path -Encoding UTF8
#endregion create module win32_environment

Run it once to create the module file. Once this file exists, all you need to do is import the module:

# import the module
Import-Module -Name $path -Verbose -Force

It provides you with four simple-to-use new cmdlets to manage environment variables locally and remotely:

VERBOSE: Loading module from path 'C:\Users\tobia\AppData\Local\Temp\cdxmlTest\win32_environment.cdxml'.
VERBOSE: Importing function 'Get-WmiEnvironment'.
VERBOSE: Importing function 'New-WmiEnvironment'.
VERBOSE: Importing function 'Remove-WmiEnvironment'.
VERBOSE: Importing function 'Set-WmiEnvironment'.

Important Limitation

Please keep in mind that Win32_Environment was invented to help you manage environment variables. With this class, you can do exactly three things: create, change, and remove environment variables. No less, but also no more.

Actually, Win32_Environment simply focuses on changing the registry keys that store the environment variables.

One thing WMI won’t do is sending a broadcast message and advertising environment variable changes to Windows Explorer. That’s why Windows and applications you launch won’t notice changes to environment variables you have done via WMI until the system is rebooted.

If you want to let the changes take effect immediately, you must send the broadcast:

  • Either use a way that you know does send the broadcast message. I.e. run [Environment]::SetEnvironmentVariable('zumselIstDoof','','user') once. This line would try and delete the user environment variable zumselIstDoof assuming that it does not exist anyway.
  • Or let PowerShell send the broadcast message:
function Send-ChangeNotification
{
  $pInvoke =  '[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
              public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);'
  
  Add-Type -Namespace Win32 -Name Native -MemberDefinition $pInvoke

  $HWND_BROADCAST = [IntPtr]0xffff
  $WM_SETTINGCHANGE = 0x1a
  $result = [UIntPtr]::Zero

  [Win32.Native]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", 0, 1000, [ref] $result)
}