24 August 2023

Subshells in Linux (and Windows)

Or rather, subshells in Bash and Powershell. A subshell functions as a sort of isolated environment for executing commands, creating a subprocess or child process within the parent shell. It lets a user define specific environment variables on a per-process basis, enabling the creation of child processes with distinct characteristics.

In Bash

Imagine you have a Bash script that could alter certain exports, but you don't want these changes to affect the global system values. Enter subshells. Here's a simple example. Subshells in Bash are broken into and out of using parentheses:

#!/bin/bash

echo "PATH before subshell: $PATH"
echo " "
(
  subshell_path="/Users/hexagr/subshell"
  export PATH="$subshell_path"
  echo "PATH within subshell: $PATH"
  
  echo " "
  # Execute the command using the full path
  subshell_cmd="do_stuff.sh"
  $subshell_cmd
  echo " "
)

# Print the PATH after the subshell
echo "PATH after subshell: $PATH"
echo " "
# Try the subshell command script in the regular shell,
# but it fails because we don't have the path!
echo "Executing do_stuff.sh after subshell:"
$subshell_cmd
do_stuff.sh 

The bash script's output, in combination with our other shell script do_stuff.sh which just echo's a simple message. Access to the custom path only happens in the subshell:

$ ./test.sh
PATH before subshell: /usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
 
PATH within subshell: /Users/hexagr/subshell
 
This path only affects the current subshell.
 
PATH after subshell: /usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
 
Executing do_stuff.sh after subshell:
./test.sh: line 22: do_stuff.sh: command not found

A Windows Analog in Powershell

While Microsoft Windows doesn't officially specify this as a "subshell" as far as I can tell, the following strategy provides functionally similar behavior for Windows operating systems. We can utilize the .NET API to manipulate the environment variables on a per-process basis in Powershell. But before we do so, let's print our regular system shell %PATH% like so, with cmd.exe /c echo %PATH%:

Windows regular system %PATH%

Now let's use the System.Diagnostics capabilities provided by .NET's ProcessStartInfo class to create a New-Object called $x.

We'll set the filename to cmd.exe along with our argument. Then we'll remove the original system Path and replace it with our own custom path. We shall also disable UseShellExecute so our new object doesn't use the shell's default variables and will instead start the process directly from our process.

Finally, we'll assign $p to a System.Diagnostics.Process object. And then set the StartInfo for our new $p object to our $x ProcessStartInfo object. Then launch it with $p.Start().

Thank you Microsoft Documentation and StackOverflow ;)

$x = New-Object System.Diagnostics.ProcessStartInfo
$x.FileName = "cmd.exe"
$x.Arguments = "/c echo %PATH%"
$x.EnvironmentVariables.Remove("Path")
$x.EnvironmentVariables.Add("PATH", "C:\custom\path")
$x.UseShellExecute = $false
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $x
$p.Start()
PS C:\Users\User> $p.Start()
True
PS C:\Users\User> C:\custom\path

We can see our new subprocess is effectively confined to the C:\custom\path now since we created a new subshell with custom environment variables, removed its regular system Path, and set it's %PATH% to be our custom directory. And after our cmd.exe subprocess runs and we're back in the regular shell, we can print the default system path to confirm we didn't affect any of the global environment variables in the main shell.

Windows console screenshot

No comments:

Post a Comment