Linux Basics: Limit Temporary Shell Changes with Bash Subshells
Parentheses run a group of commands in a subshell. They are useful when a short operation needs a different directory, shell option, or variable value without leaving that shell state changed afterward.
These examples were tested on Linux with GNU Bash 5.2.37. Use Bash rather than sh. No administrator access is needed.
Prepare a scratch directory
You need Bash, a text editor, and the mkdir, mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file in this scratch directory before running its command. Calling bash explicitly avoids changing executable permissions.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Change directory for one operation
Save this as subshell-directory.sh:
#!/usr/bin/env bash
original=$PWD
mkdir -- scoped-work || exit 1
(
cd -- scoped-work || exit 1
printf 'Inside: %s\n' "${PWD##*/}"
) || exit 1
if [[ $PWD == "$original" ]]; then
printf 'Original directory preserved\n'
fi
rmdir -- scoped-workRun it:
bash subshell-directory.shOutput:
Inside: scoped-work
Original directory preservedThe directory change ends with the subshell. The guards prevent continuing after a failed directory operation. The directory itself is shared filesystem state, so it still needs explicit removal. Subshell isolation is not a filesystem sandbox or an undo feature.
2. Enable an option for a single filename expansion
Save this as subshell-options.sh:
#!/usr/bin/env bash
shopt -u nullglob
set -- missing-*.txt
printf 'Before: %d argument\n' "$#"
(
shopt -s nullglob
set -- missing-*.txt
printf 'Inside: %d arguments\n' "$#"
)
set -- missing-*.txt
printf 'After: %d argument\n' "$#"Run it:
bash subshell-options.shOutput:
Before: 1 argument
Inside: 0 arguments
After: 1 argumentThe scratch directory contains no matching filenames. With nullglob off, the unmatched pattern stays one literal argument. With it on, the pattern disappears. The option change is confined to the subshell. The script explicitly starts with nullglob off so the demonstration is predictable.
3. Keep a child’s variables and exit separate
Save this as subshell-state.sh:
#!/usr/bin/env bash
stage=planned
(
stage=testing
printf 'Inside: %s\n' "$stage"
exit 7
)
status=$?
printf 'Parent: %s; child status: %d\n' "$stage" "$status"
{
stage=ready
}
printf 'After braces: %s\n' "$stage"Run it:
bash subshell-state.shOutput:
Inside: testing
Parent: planned; child status: 7
After braces: readyThe child’s exit 7 ends that subshell, and its status is available to the parent. Its variable assignment does not change the parent’s value. A brace group runs in the current shell, so the later assignment does persist. This script intentionally handles the child’s nonzero status without enabling set -e.
Clean up
In the same terminal, remove only the files created for these examples, return to your original directory, and remove the empty scratch directory:
rm -- subshell-directory.sh subshell-options.sh subshell-state.sh
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirReferences
- GNU Bash manual: Command Grouping
- GNU Bash manual: Command Execution Environment
- GNU Bash manual: The Shopt Builtin
Acknowledgements
This post was created with assistance from AI.