Skip over navigation

How to access environment variables

Contents

Why this article?

Sometimes we need to access some system configuration information that is stored in environment variables – such as the search path. These are stored in environment variables.

On other occasions we might want to provide a spawned application with some global information using environment variables.

The purpose of this article is to explain how to read existing environment variables and how to set up new environment variables to pass to other processes.

A little background information

Before we jump into writing some code to access and update environment variables, let us take a brief look at how Windows handles them.

When Windows starts a program it provides the program with a copy of its environment variables. The API functions that work with environment variables operate on this copy, not on the original environment variables. This means that any changes you make to environment variables within the program only apply to the program's copy and do not update the underlying Window's environment variables – such changes are lost when your program terminates.

Any child process started by your application gets, by default, a copy of your program's current environment, not that of Windows. This means that any changes you make to the environment before starting the child process are reflected in the child's own environment block. This default behaviour can be overridden by defining a custom block of environment variables to be passed to the child process. We discuss how to do this below.

One final point to note is that the block of memory allocated for environment variables is a fixed size. This means that you can't keep adding new environment variables to the block without limit. At some point the attempt to add a variable, or to store more data in an existing one, will fail. The size of the block created for a process depends on the number of environment variables and the size of the data to be passed to it. Back in the days of Windows 98 the block size was small (about 16Kb) and was incremented in 4Kb chunks. On modern Windows systems this size is massively increased.

How it's done

Now we have an understanding of how Windows handles environment variables, let us move on to look at how we work with them. Windows provides four API functions for accessing and updating environment variables:

  1. GetEnvironmentVariable
    Returns the value of a given environment variable.
  2. SetEnvironmentVariable
    Sets an environment variable's value, creating a new variable if necessary. This routine can also be used to delete an environment variable.
  3. GetEnvironmentStrings
    Gets a list of all the environment variables available to a process.
  4. ExpandEnvironmentStrings
    Replaces environment variables delimited by "%" characters in a string with the variable's value.

We will develop six Delphi routines: five that wrap these API calls plus one that can be used to create a new environment block for passing to a child process. They are:

  1. GetEnvVarValue
    Returns the value of a given environment variable.
  2. SetEnvVarValue
    Sets the value of the given environment variable.
  3. DeleteEnvVar
    Deletes the given environment variable.
  4. GetAllEnvVars
    Fills a string list with the names and values of all the program's environment variables.
  5. ExpandEnvVars
    Replaces all "%" delimited environment variables in a string with their values.
  6. CreateEnvBlock
    Creates a new environment block suitable for passing to a child process.

And so to the code:

GetEnvVarValue

This routine returns the value of a given environment variable (or returns the empty string if there is no variable with that name). Here's the definition:

  1function GetEnvVarValue(const VarName: string): string;
  2var
  3  BufSize: Integer;  // buffer size required for value
  4begin
  5  // Get required buffer size (inc. terminal #0)
  6  BufSize := GetEnvironmentVariable(
  7    PChar(VarName), nil, 0);
  8  if BufSize > 0 then
  9  begin
 10    // Read env var value into result string
 11    SetLength(Result, BufSize - 1);
 12    GetEnvironmentVariable(PChar(VarName),
 13      PChar(Result), BufSize);
 14  end
 15  else
 16    // No such environment variable
 17    Result := '';
 18end;
Listing 1

You'll notice that GetEnvironmentVariable is called twice: once to get the size of the required buffer and the second time to actually read the variable. This is a common Windows idiom.

Back to list

SetEnvVarValue

This routine sets the value of an environment variable. If the variable doesn't already exist it is created. Zero is returned if all goes well, otherwise a Windows error code is returned. An error may occur if there is no room in the environment block for the new value. The implementation is very simple:

  1function SetEnvVarValue(const VarName,
  2  VarValue: string): Integer;
  3begin
  4  // Simply call API function
  5  if SetEnvironmentVariable(PChar(VarName),
  6    PChar(VarValue)) then
  7    Result := 0
  8  else
  9    Result := GetLastError;
 10end;
Listing 2

Back to list

DeleteEnvVar

This routine deletes the given environment variable. Note that SetEnvVarValue('') has the same effect. Zero is returned on success and a Windows error code on error. The implementation is again simple:

  1function DeleteEnvVar(const VarName: string): Integer;
  2begin
  3  if SetEnvironmentVariable(PChar(VarName), nil) then
  4    Result := 0
  5  else
  6    Result := GetLastError;
  7end;
Listing 3

Back to list

GetAllEnvVars

This routine returns all of a program's environment variables in a string list. Each entry in the list is of the form Name=Value. You can use the TStrings Names[] and Values[] properties to extract the variable names and value from the string. The function returns the amount of space taken by the strings in the environment block. If you just want to know the size of the environment variables, pass a nil parameter. Here's the definition:

  1function GetAllEnvVars(const Vars: TStrings): Integer;
  2var
  3  PEnvVars: PChar;    // pointer to start of environment block
  4  PEnvEntry: PChar;   // pointer to an env string in block
  5begin
  6  // Clear the list
  7  if Assigned(Vars) then
  8    Vars.Clear;
  9  // Get reference to environment block for this process
 10  PEnvVars := GetEnvironmentStrings;
 11  if PEnvVars <> nil then
 12  begin
 13    // We have a block: extract strings from it
 14    // Env strings are #0 separated and list ends with #0#0
 15    PEnvEntry := PEnvVars;
 16    try
 17      while PEnvEntry^ <> #0 do
 18      begin
 19        if Assigned(Vars) then
 20          Vars.Add(PEnvEntry);
 21        Inc(PEnvEntry, StrLen(PEnvEntry) + 1);
 22      end;
 23      // Calculate length of block
 24      Result := (PEnvEntry - PEnvVars) + 1;
 25    finally
 26      // Dispose of the memory block
 27      Windows.FreeEnvironmentStrings(PEnvVars);
 28    end;
 29  end
 30  else
 31    // No block => zero length
 32    Result := 0;
 33end;
Listing 4

Back to list

ExpandEnvVars

This function takes as a parameter a string of text containing one or more environment variables, delimited by "%" characters, and returns the string with each environment variable replaced by its value.

For example, if we have two environment variables ME and YOU set to "Peter" and "Gloria" respectively.

ExpandEnvVars('%ME% say hello to %YOU%');

returns

Peter say hello to Gloria
  1function ExpandEnvVars(const Str: string): string;
  2var
  3  BufSize: Integer; // size of expanded string
  4begin
  5  // Get required buffer size 
  6  BufSize := ExpandEnvironmentStrings(
  7    PChar(Str), nil, 0);
  8  if BufSize > 0 then
  9  begin
 10    // Read expanded string into result string
 11    SetLength(Result, BufSize - 1);
 12    ExpandEnvironmentStrings(PChar(Str),
 13      PChar(Result), BufSize);
 14  end
 15  else
 16    // Trying to expand empty string
 17    Result := '';
 18end;
Listing 5

Back to list

CreateEnvBlock

This final function creates an environment block that can be passed to a child process.

It creates a new environment block containing the strings from the NewEnv string list. If IncludeCurrent is true then the variables defined in the current process' environment block are included. The new block is stored in the memory pointed to by Buffer, which must be at least BufSize characters. The size of the block is returned. If the provided buffer is nil or is too small then no block is created. The return value gives the required buffer size in characters.

  1function CreateEnvBlock(const NewEnv: TStrings;
  2  const IncludeCurrent: Boolean;
  3  const Buffer: Pointer;
  4  const BufSize: Integer): Integer;
  5var
  6  EnvVars: TStringList; // env vars in new block
  7  Idx: Integer;         // loops thru env vars
  8  PBuf: PChar;          // start env var entry in block
  9begin
 10  // String list for new environment vars
 11  EnvVars := TStringList.Create;
 12  try
 13    // include current block if required
 14    if IncludeCurrent then
 15      GetAllEnvVars(EnvVars);
 16    // store given environment vars in list
 17    if Assigned(NewEnv) then
 18      EnvVars.AddStrings(NewEnv);
 19    // Calculate size of new environment block
 20    Result := 0;
 21    for Idx := 0 to Pred(EnvVars.Count) do
 22      Inc(Result, Length(EnvVars[Idx]) + 1);
 23    Inc(Result);
 24    // Create block if buffer large enough
 25    if (Buffer <> nil) and (BufSize >= Result) then
 26    begin
 27      // new environment blocks are always sorted
 28      EnvVars.Sorted := True;
 29      // do the copying
 30      PBuf := Buffer;
 31      for Idx := 0 to Pred(EnvVars.Count) do
 32      begin
 33        StrPCopy(PBuf, EnvVars[Idx]);
 34        Inc(PBuf, Length(EnvVars[Idx]) + 1);
 35      end;
 36      // terminate block with additional #0
 37      PBuf^ := #0;
 38    end;
 39  finally
 40    EnvVars.Free;
 41  end;
 42end;
Listing 6

The way we use this function is similar to the idiom used by many Windows API functions (such as GetEnvironmentVariable). We first call the function with a nil buffer to find the required buffer size, then call it again with a buffer of correct size to receive the data.

This routine can be used along with the Windows CreateProcess API function to spawn a new process with only one environment variable (FOO=Bar) as follows:

  1function ExecProg(const ProgName: string; EnvBlock: Pointer): Boolean;
  2  {Creates new process for given program passing any given environment block}
  3var
  4  SI: TStartupInfo;         // start up info
  5  PI: TProcessInformation;  // process info
  6  CreateFlags: DWORD;       // process creation flags
  7  SafeProgName: string;     // program name: safe for CreateProcessW
  8begin
  9  // Make ProgName parameter safe for passing to CreateProcessW
 10  SafeProgName := ProgName;
 11  UniqueString(SafeProgName);
 12  // Set up startup info record: all default values
 13  FillChar(SI, SizeOf(SI), 0);
 14  SI.cb := SizeOf(SI);
 15  // Set up creation flags: special flag required for unicode
 16  // environments, which is want when unicode support is enabled.
 17  // NOTE: we are assuming that the environment block is in Unicode
 18  // on Delphi 2009 or later. CreateProcessW does permits it to be
 19  // ANSI, but we don't support that
 20  {$IFDEF UNICODE}
 21  CreateFlags := CREATE_UNICODE_ENVIRONMENT;  // passing a unicode env
 22  {$ELSE}
 23  CreateFlags := 0;
 24  {$ENDIF}
 25  // Execute the program
 26  Result := CreateProcess(
 27    nil, PChar(SafeProgName), nil, nil, True,
 28	CreateFlags, EnvBlock, nil, SI, PI
 29  );
 30end;
Listing 7

Back to list

Demo Code

A demo program to accompany this article can be found in the delphidabbler/article-demos Git repository on GitHub.

You can view the code in the article-06 sub-directory. Alternatively download a zip file containing all the demos by going to the repository's landing page and clicking the Clone or download button and selecting Download ZIP.

See the demo's README.md file for details.

This source code is merely a proof of concept and is intended only to illustrate this article. It is not designed for use in its current form in finished applications. The code is provided on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.

The demo is open source. See the demo's LICENSE.md file for licensing details.

Going Further

My Environment Variables Unit provides a set of routines and a component that use the methods discussed here to access environment variables.

Feedback

I hope you found this article useful.

If you have any observations, comments, or have found any errors there are two places you can report them.

  1. For anything to do with the article content, but not the downloadable demo code, please use this website's Issues page on GitHub. Make sure you mention that the issue relates to "article #6".
  2. For bugs in the demo code see the article-demo project's README.md file for details of how to report them.