Published 3 minutes ago
Graeme Peacock is a seasoned Linux expert with more than 15 years of hands-on experience. He has worked extensively with Ubuntu, Gentoo, Arch Linux, Qubes, and Fedora, gaining deep proficiency in everything from routine terminal operations to highly customized system builds.
Graeme began his journey with Ubuntu, quickly mastering the command line and essential system administration skills. A year later, he moved to Arch Linux, where he spent nearly a decade refining his expertise through the installation and configuration of multiple minimalist systems. After some time, he moved to Gentoo, where he configured and compiled both server and desktop environments using normal and hardened profiles and frequently compiled custom kernels. Graeme moved to Qubes in 20…
Published 3 minutes ago
Graeme Peacock is a seasoned Linux expert with more than 15 years of hands-on experience. He has worked extensively with Ubuntu, Gentoo, Arch Linux, Qubes, and Fedora, gaining deep proficiency in everything from routine terminal operations to highly customized system builds.
Graeme began his journey with Ubuntu, quickly mastering the command line and essential system administration skills. A year later, he moved to Arch Linux, where he spent nearly a decade refining his expertise through the installation and configuration of multiple minimalist systems. After some time, he moved to Gentoo, where he configured and compiled both server and desktop environments using normal and hardened profiles and frequently compiled custom kernels. Graeme moved to Qubes in 2016, where he has remained ever since.
Graeme has extensive experience with highly configurable tools such as Vim, Neovim, and Emacs, and he maintains his own complex configurations. He is also highly proficient with Bash, Zsh, and dozens of utilities.
Graeme holds a B.S. in software engineering and has a strong passion for programming and web development. He is proficient in Golang, Python, Bash, JavaScript, TypeScript, HTML, and CSS. He also has considerable experience with Docker and is currently working on learning Kubernetes.
Are you currently learning Bash? Have you seen things like $0 and $EUID and wondered what they mean? Or what the difference is between $UID and $EUID? I’ll cover these and more: what they do and why they’re important.
HTG Wrapped 2025: 24 days of tech
24 days of our favorite hardware, gadgets, and tech
Special parameters
Special parameters are variables provided by Bash for specific purposes. There are less than 10 of them, and the following ones I find the most useful.
Get the script path
Sometimes when you’re writing a script, you want to know its path. For example, when creating a help menu, it’s customary to include the name of the script at the top. Here’s the help menu for the ls command:
ls --help
You can access the (relative) path using the $0 special parameter. It’s a POSIX standard, so you can use it in most shells:
echo "$0"
Then you can get the script name using $(basename $0):
However, sometimes $0 isn’t reliable—for example, source /path/to/script.sh returns "bash" and not the script path. Instead of that, use the $BASH_SOURCE variable, which works almost identically, except without surprises. If you use Bash only, then it’s the better choice, but it’s not portable to other shells.
Determine the exit status of a process
Every program returns a number upon exit. It’s called the exit code, and it’s used to make decisions when a command returns an error. For example, if you try to ls a non-existent directory, it returns a non-zero value.
A zero exit status means success; non-zero indicates a problem.
When you encounter a problem, you may want to halt execution, and the $? special parameter provides the necessary exit code:
ls /system32
echo $?
There are several ways to detect and handle an exit code. The most obvious one is to evaluate "$?" with a conditional statement:
if [[ $? -eq 0 ]]; then
# If the script executed successfully.
fi
The exit code usually means something, and it’s program specific, but you can handle distinct values with a case statement:
case $? in
0) echo "OK";;
1) echo "Err";;
*) echo "Unhandled error";;
esac
A favorite approach of mine is to use a conditional statement without brackets:
if ls /system32; then
# Upon zero exit code.
echo "/system32 exists"
fi
But the most elegant solution is to use logical operators:
ls /system32 && echo "/system32 exist" # && executes on zero exit status.
ls /system32 || echo "/system32 does not exist" # || executes on non-zero exit status.
Access arguments
If you’re writing a Bash script, you will invariably need to pass values to it—aka arguments or positional parameters. Bash’s approach is a little clunky, but it works. Examples are the easiest way to understand them:
foo() {
echo "$1" # First argument.
echo "$2" # Second argument.
}
foo "first" "second"
I’ve passed arguments into a function, but they work exactly the same at the top level of your script. For example:
#!/usr/bin/env bash
# This is the top-level of your script.
echo "The first argument: $1"
echo "The second argument: $2"
You can then pass arguments into your script via the CLI: script.sh "first" "second".
Moving on, the "$@" special parameter represents all arguments as an array:
foo() {
for arg in "$@"; do
echo "$arg"
done
}
foo "first" "second"
The "$*" special parameter is the same as "$@", except it puts all arguments into one string (if "$*" has double quotes):
at() {
printf '@: [%s]\n' "$@"
}
star() {
printf '*: [%s]\n' "$*"
}
at "one two" "three"
star "one two" "three"
You can see that "$@" prints the two arguments across two lines, but "$*" prints them on one line.
To get the number of arguments, you can use the ${#@} or ${#*} syntax:
foo() {
echo ${#@}
echo ${#*}
}
foo "one" "two"
Environment variables
Environment variables are values provided to running programs by the shell. For example, $HOME provides the path to the current user’s home directory.
Get the user ID
Occasionally, I need to know the current user ID in a script. For example, recently I was interacting with a socket in the /run/user/1000 directory. The "1000" is the user ID, and if we want a robust script, we shouldn’t hard-code this value, because another user executing it may have a different user ID.
To address that, we can use the $UID and $EUID variables. "$UID" displays the ID of the user executing a binary. "$EUID" (effective user ID) is the user ID of the process executing a binary. Normally, these equal each other, but for setuid binaries they can differ. For example, when executing something with sudo, they vary as forking progresses through different stages.
ps --forest -eo cmd,euid,ruid | grep -C 1 '[s]udo'
I have "sudo sleep 600" running in the background, and the above prompt displays the command, EUID ("1"), and UID ("2") columns for any process running sudo. You can see that the EUID becomes "0" immediately, but the UID doesn’t change until the final forked process (which is "sleep" running as root).
Related
8 Linux shell tricks that completely change how commands work
The shell does far more than run commands. Here’s how Bash expands your input behind the scenes so you can write cleaner, more reliable commands.
What can we take from this? Well, it doesn’t really matter which one you use. Some say use "$EUID" to check if you have root privileges, because it’s the effective permissions. However, you can see from the image that the final "sleep" process has both UID and EUID set to "0" (root), and the shell process has both set to "1000" (user). These represent either end of the forking process—where you initiated the sudo request and also the final executing process. Therefore, there’s no useful distinction between them when it counts, but remember that they differ for setuid binaries during the forking process.
Get common user paths
In almost every script I write, I access the home directory. Less frequently, I need access to ~/.config or ~/.local/share. Accessing typical locations in the file system is common, but hard-coding paths like this is bad practice because they could change. The recommended approach is to use the XDG Directory Specification, which is a set of standard variables provided by freedesktop.org.
env | grep XDG | sort -u
Not all variables are set, and some are not paths.
When using XDG variables, be sure to always set a sensible default:
export "${XDG_CACHE_HOME:=$HOME/.cache}"
That will also set the variable (if unset) and make it available to any binaries or scripts you execute.
The most common variables I find myself using:
export "${XDG_CACHE_HOME:=$HOME/.cache}" # Semi-temporary data.
export "${XDG_CONFIG_HOME:=$HOME/.config}" # Configuration files.
export "${XDG_DATA_HOME:=$HOME/.local/share}" # Downloaded data, etc.
There are several more, and I encourage you to learn them. You should use these locations instead of writing everything to /tmp or a custom directory in ~/.
Related
On top of these variables, many useful environment variables are available via the shell. The covered variables represent those I use most often. I couldn’t write Bash scripts without them. Conversely, hard coding values will lead to breakages eventually. When you have hundreds of scripts, such breakages become a maintenance nightmare. Therefore, it’s best to infer as much as you can from somewhere else, preferably from standard variables, and the variables presented here today fit the bill.
In closing, learn and use as many standard variables as you can.
Related