7 Bash variables I use in almost every script

7 Bash variables I use in almost every script


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.

Special parameters

Quick actions that are worth memorizing

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
7 Bash variables I use in almost every script

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"
A terminal window displays a relative path to a script.

Then you can get the script name using $(basename $0):

A terminal window displays the name of a script: script.sh.

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 $?
A terminal window displays an error message from the ls command. It states that the queried directory does not exist. It also displays the number 2.-1

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"
A terminal window displays the words first and second across two lines.

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"
A terminal window displays the results of the 'at' and 'star' special variables. The 'at' variable has its contents printed across multiple lines, and the 'star' variable on a single line.

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"
A terminal window displays the number 2 twice, across two lines.

Environment variables

Don’t lock down your scripts

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'
A terminal window shows a tree of forked processes. It shows that executing set UID binaries causes the UID and EUID to change from 1000 to 0.

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).

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
A terminal window displays a list of nine XDG variables, which consists of paths and non-path values.-2

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 ~/.


Use variables to keep your scripts functional and portable

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.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *