Thursday, August 24, 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 sort of per-process basis, enabling the creation of child processes with distinct characteristics. This is particularly useful when you want to modify environment variables within a confined scope.

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 (back to original)
echo "PATH after subshell: $PATH"
echo " "
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 "This path only affects the current subshell":

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

Printing out to the console before, inside, and after the subshell. Only inside does our command run and have access to the particular path we've provided, which points to our do_stuff.sh script.

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 %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 attribute to create a New-Object called $x, 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, disable UseShellExecute so our new object doesn't use the shell's default variables and will start the process directly from our executable, and finally create a new object $p, which we'll hook to System.Diagnostics.Process, then set the StartInfo for our new $p object to $x, and launch it. 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. We've created a new subshell with custom environment variables, removed its regular system Path, and reconfigured it's %PATH% to our custom directory. And after our program runs, we can confirm we didn't affect any of the global variables in our main shell.

Windows console screenshot

No comments:

Post a Comment