5 Useful Linux Commands Every Sysadmin Should Use
Building a directory structure one `mkdir` at a time or hunting for PIDs with `ps` and `grep` wastes time on every server you manage. Here’s how `mkdir -p`, heredocs, `tar -C`, `pkill`, and `pidwait` can simplify these tasks with fewer commands.
Most of us learn a way to perform a **Linux** task and stick with it for years because it works, and there’s rarely a good time to look for a better approach. You might create a directory, change into it, create another directory, and repeat the process. Or you might copy a tarball into `/opt` just to extract it there.
These habits aren’t wrong, but they add unnecessary steps. Across multiple servers, those extra commands add up. They also make shell scripts longer and introduce more opportunities for typos.
In this guide, we’ll explore five practical techniques that replace repetitive, multi-step routines with simpler commands. We’ve tested the examples on `Ubuntu 26.04 LTS` and **Rocky Linux 10**. They should also work on other Linux distributions, unless a section notes otherwise.
## Difference Between Shell Features vs. Command Options
Before diving into the examples, it’s worth understanding that these techniques fall into two categories: **shell features** and **command options**. Knowing the difference helps explain how they behave, especially in scripts.
The first category includes **shell features** , such as **brace expansion** and **heredocs** , Bash handles these before executing a command.
For example, when you use brace expansion with `mkdir`, Bash expands the braces into a list of paths before passing them to `mkdir`. The command itself never sees the braces. Because these features depend on the shell, their behavior can vary when you use a different shell or run a script with a different interpreter.
The second category includes **command options** , such as `tar -C` and the process-matching options available in `pgrep` and `pkill`. These are implemented by the respective utilities rather than by the shell, so their behavior doesn’t depend on which shell launches them.
## 1. Create a Nested Directory Tree with mkdir -p and Brace Expansion
Setting up a directory structure is a common task when deploying a new application. Suppose you’re deploying an app called **myapp** in the tecmint user’s home directory and need separate directories for binaries, configuration files, logs, data, and scripts.
The traditional approach is to run multiple mkdir commands, one for each directory. A quicker approach combines `mkdir -p` with Bash brace expansion to create the entire directory tree in a single command:
mkdir -pv ~/myapp/{bin,config/{nginx,systemd},logs,data/{uploads,cache,backups},scripts}
The `-p` option creates missing parent directories as needed, while `-v` (verbose) prints a message for each directory created. Together, they let you build the structure and verify the results in one step.
**Output** :
mkdir: created directory '/home/tecmint/myapp'
mkdir: created directory '/home/tecmint/myapp/bin'
mkdir: created directory '/home/tecmint/myapp/config'
mkdir: created directory '/home/tecmint/myapp/config/nginx'
mkdir: created directory '/home/tecmint/myapp/config/systemd'
mkdir: created directory '/home/tecmint/myapp/logs'
mkdir: created directory '/home/tecmint/myapp/data'
mkdir: created directory '/home/tecmint/myapp/data/uploads'
mkdir: created directory '/home/tecmint/myapp/data/cache'
mkdir: created directory '/home/tecmint/myapp/data/backups'
mkdir: created directory '/home/tecmint/myapp/scripts'
The exact wording and ordering of verbose messages may vary by implementation, but the intended directory structure remains the same.
Here’s how Bash expands the braces in this command:
* **{a,b,c}** expands into separate paths for each item. For example, `{logs,scripts}` becomes logs and scripts.
* **Nested braces** , such as `config/{nginx,systemd}`, generate multiple paths under the same parent directory: config/nginx and config/systemd.
* **Avoid spaces after commas**. Writing `{bin, logs}` prevents Bash from recognizing the expression as intended, leaving the braces in the resulting argument.
### Display the Directory Tree
To verify the resulting structure, use the tree command. It may not be installed on minimal **Linux** systems, so install it first if necessary.
On **Ubuntu** and **Debian** , run:
sudo apt install -y tree
On **Rocky Linux** , **AlmaLinux** , and **RHEL** , use:
sudo dnf install -y tree
Once installed, display the directory structure:
tree ~/myapp
**Output** :
/home/tecmint/myapp
├── bin
├── config
│ ├── nginx
│ └── systemd
├── data
│ ├── backups
│ ├── cache
│ └── uploads
├── logs
└── scripts
10 directories, 0 files
### Preview Brace Expansion Before Creating Directories
Long brace expressions are easy to mistype. A misplaced comma can produce an unexpected path, so it’s useful to check the expansion before creating anything, because Bash expands braces before executing a command, you can use echo command to preview the resulting paths without modifying the filesystem:
echo ~/myapp/{bin,config/{nginx,systemd},logs}
**Output** :
/home/tecmint/myapp/bin /home/tecmint/myapp/config/nginx /home/tecmint/myapp/config/systemd /home/tecmint/myapp/logs
### Brace Expansion Doesn’t Work in /bin/sh Scripts
One important limitation is that brace expansion is a Bash feature, not part of the POSIX shell standard. On **Ubuntu** and **Debian** , `/bin/sh` typically points to dash, which does not support Bash-style brace expansion.
As a result, a script beginning with `#!/bin/sh` may pass an expression such as `{bin,config}` to `mkdir` literally instead of expanding it into separate directory names.
If your script relies on brace expansion, use a Bash shebang:
#!/bin/bash
This ensures the script runs under Bash, where the expansion works as expected. If you need POSIX shell compatibility, use separate path arguments or a loop instead.
If this saved you from typing a dozen `mkdir` commands, share it with someone who still builds project folders one directory at a time.
## 2. Create a File with Content Using a cat Heredoc
Once the directories are in place, the next step is usually to create a configuration file. The traditional approach involves creating the file, opening it in an editor, entering the content, and saving it. That works well for occasional tasks, but it becomes tedious when you’re configuring multiple servers or automating deployments.
A **heredoc** (short for **here document**) lets you pass multiple lines of text directly to a command using a designated closing marker. Combined with `cat` and output redirection, it creates a file and writes its contents in one step.
For example, let’s create an environment file in the `config` directory from the previous example:
cat << 'EOF' > ~/myapp/config/app.env
APP_NAME=myapp
APP_PORT=8000
LOG_DIR=/home/tecmint/myapp/logs
EOF
To verify the contents, run:
cat ~/myapp/config/app.env
**Output** :
APP_NAME=myapp
APP_PORT=8000
LOG_DIR=/home/tecmint/myapp/logs
Here’s how the command works:
* `<< 'EOF'` starts the heredoc. `EOF` is a delimiter you choose; any marker works as long as the closing line matches it exactly.
* **The quotes around** `EOF` prevent Bash from expanding variables and command substitutions in the heredoc body. For example, `PATH=$HOME/bin` is written literally rather than expanded. This is a useful default when creating configuration files.
* `> ~/myapp/config/app.env` redirects the text into the file, replacing any existing contents. Use `>>` instead to append the text.
* **The closing** `EOF` must appear on a line by itself, without leading or trailing spaces, unless you use a heredoc variant that explicitly permits indentation.
### Expand Variables Inside a Heredoc
Sometimes you want Bash to insert dynamic values into the file, such as the current username, hostname, or deployment date. In that case, leave the delimiter unquoted so Bash expands variables and executes command substitutions before passing the text to `cat`.
For example:
cat << EOF > ~/myapp/DEPLOYED.txt
Deployed by: $USER
Host: $(hostname)
Date: $(date +%F)
EOF
Read the file to see the expanded values:
cat ~/myapp/DEPLOYED.txt
**Example output** :
Deployed by: tecmint
Host: server1
Date: 2026-10-27
The output will vary depending on the user, hostname, and date when you run the command.
### Write to Root-Owned Files with sudo tee
Heredocs are also useful for creating system configuration files under directories such as `/etc`. However, a common mistake is to assume that prefixing a command with `sudo` also grants elevated permissions to its output redirection.
For example, this command fails for a regular user who lacks permission to write to the destination:
sudo cat << 'EOF' > /etc/sysctl.d/90-tecmint.conf
vm.swappiness = 10
EOF
You may see an error similar to:
bash: /etc/sysctl.d/90-tecmint.conf: Permission denied
The reason is that your shell processes the `>` redirection before running `sudo cat`. The shell attempts to open the file using your regular user’s permissions, so the operation fails before `cat` can run with elevated privileges.
The solution is to use `sudo tee`, which opens and writes the destination file with root privileges:
sudo tee /etc/sysctl.d/90-tecmint.conf > /dev/null << 'EOF'
vm.swappiness = 10
EOF
The `> /dev/null` redirection suppresses tee’s normal output, keeping the configuration text from being printed to the terminal. To append content instead of replacing the file, use `sudo tee -a`.
## 3. Extract a Tarball into Another Directory with tar -C
Once the application directories and configuration files are ready, the next step is often installing software distributed as a tarball. A common approach is to move the archive to its destination, change into that directory, and extract it. Alternatively, you might extract it in `~/Downloads` and move the resulting directory afterward. Both approaches involve unnecessary steps.
The `-C` option tells `tar` to change to a specified directory before extracting the archive. This lets you leave the downloaded archive where it is and extract its contents directly into the intended location.
The official Go installation method provides a practical example. Go’s Linux archive can be extracted directly into `/usr/local`:
wget https://go.dev/dl/go1.25.3.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.25.3.linux-amd64.tar.gz
**Note** : Replace `go1.25.3` with the appropriate version listed on the official website site.
The `rm -rf /usr/local/go` command removes an existing Go installation before extraction. The Go project recommends this approach because extracting a new version over an existing installation can leave obsolete files behind.
After extraction, the Go binaries are available in `/usr/local/go/bin`, even though you never changed your working directory:
ls /usr/local/go/bin
**Output** :
go gofmt
### Let tar Detect the Compression Type
Traditionally, you might use different `tar` options for different compression formats, such as `-z` for **gzip** , `-j` for **bzip2** , and `-J` for **xz**. When extracting archives, GNU `tar` can automatically detect common compression formats, so you generally don’t need to remember the corresponding option.
For example, the following command extracts an xz-compressed archive directly into `/opt`:
sudo tar -xf archive.tar.xz -C /opt
The same basic approach works with **gzip** , **bzip2** , **xz** , and Zstandard-compressed tar archives, provided the required decompression support is available. For `.tar.zst` archives, install the `zstd` package if it’s missing:
On **Ubuntu** and **Debian** :
sudo apt install zstd
On **Rocky Linux** , **AlmaLinux** , and **RHEL** :
sudo dnf install zstd
### Common Mistakes with tar -C
The destination directory must already exist because `tar -C` changes into that directory but doesn’t create it and if the directory is missing, extraction fails with an error similar to:
tar: /opt/node: Cannot open: No such file or directory
tar: Error is not recoverable: exiting now
You can avoid this problem by combining `mkdir -p` with `tar -C`. Another common issue is that archives often contain a versioned top-level directory, such as `node-v24.11.0-linux-x64/`. If you want the files inside that directory to land directly in your destination, use `--strip-components=1` to remove the first path component:
sudo mkdir -p /opt/node
sudo tar -xf node-v24.11.0-linux-x64.tar.xz -C /opt/node --strip-components=1
**Note** : The archive filename is an example. Choose the appropriate Node.js release and architecture from the official Node.js downloads page.
Before extracting an unfamiliar archive, inspect its contents to understand its directory structure:
tar -tf archive.tar.xz | head
This displays the first few paths in the archive, helping you determine whether `--strip-components=1`` is appropriate. Removing too many components can flatten the directory structure incorrectly or discard useful path levels.
If you’ve been moving tarballs into `/opt` just to extract them, share this with a teammate who still does the same.
## 4. Find and Kill Processes by Name with pgrep and pkill
Eventually, you’ll need to stop a running process, whether it’s a test server, a stuck script, or an application that’s no longer responding. The traditional approach is to run `ps -A | grep -i apache2`, identify the relevant PIDs, and pass them to `kill` manually. Although this works, it takes extra steps and can produce confusing results when `grep` matches its own command line.
A simpler approach is to use `pgrep` to find processes and `pkill` to signal them by name or command-line pattern.
Let’s start a small test process so you can practise safely. Python’s built-in HTTP server is available on both **Ubuntu** and **Rocky Linux** , and the `&` operator runs it in the background:
python3 -m http.server 8000 &
Use `pgrep` to find the running process. The `-a` option displays each matching PID alongside its full command line, making it easier to identify the correct process:
pgrep -a python3
**Example output** :
4821 python3 -m http.server 8000
Your PID will differ. Once you’ve confirmed which process you want to stop, use `pkill` to send it a signal without copying its PID:
pkill -e -f "http.server"
**Example output** :
python3 killed (pid 4821)
The `-f` option matches the full command line, allowing `pkill` to find the Python server even though its process name is simply `python3`. The `-e` option reports the process name and PID for each process signaled.
Here are some useful `pgrep` and `pkill` options to remember:
* `-f` matches against the full command line instead of just the process name. Use it when multiple programs share the same executable name, but make sure your pattern identifies only the intended process.
* `-x` requires an exact match for the process name. For example, `pkill -x ssh` matches a process named `ssh`, not one named `sshd`.
* `-u tecmint` restricts the search to processes owned by the `tecmint` user, helping avoid unintended matches on shared servers.
* `-HUP` or `--signal HUP` sends **SIGHUP** instead of the default **SIGTERM**. **SIGTERM** requests a graceful shutdown, whereas **SIGKILL** (`-9`) forcibly terminates a process without giving it an opportunity to clean up.
Before using `pkill` with a broad pattern, preview the matches with `pgrep -a` using the same pattern and relevant filters. This helps you identify the processes that could be affected before sending a signal.
### Stop System Services with systemctl Instead of pkill
You may see older examples that use `pkill apache2` to stop a web server. For services managed by systemd, however, the recommended approach is to use `systemctl stop`.
Killing a service process directly can cause problems. If its systemd unit has a restart policy configured, systemd may start it again. Otherwise, the service may enter a failed state or leave systemd’s recorded service status inconsistent with your intentions.
On **Ubuntu** and **Debian** , the Apache service is named `apache2`:
sudo systemctl stop apache2
On **Rocky Linux** , **AlmaLinux** , and **RHEL** , it is named `httpd`:
sudo systemctl stop httpd
Reserve `pkill` for processes you intend to manage directly, such as your own test servers, stuck scripts, or unwanted commands running in another terminal. For systemd-managed services, use `systemctl` to stop the service cleanly and let the service manager handle its lifecycle.
## 5. Wait for a Process to Finish with pidwait
The final tip builds on `pgrep` and `pkill` but solves a different problem: waiting for a process to finish before running the next command. For example, you might want to start a package upgrade only after a long-running backup has completed.
A common workaround is a `while` loop that repeatedly checks for the process with `pgrep` and pauses with `sleep`. The `pidwait` command, provided by the procps-ng package, offers a simpler alternative by waiting for matching processes to terminate.
For example, to wait for an `rsync` process to finish before printing a completion message, run:
pidwait -x rsync && echo "Backup finished at $(date +%T)"
The `-x` option matches the process name exactly, while `&&` ensures that the completion message is printed only if `pidwait` exits successfully. The message includes the current time.
One important detail is that `pidwait` waits for processes it successfully identifies when it starts. If no matching process exists, it returns immediately rather than waiting for a future `rsync` process to appear. Also, matching by process name alone can be ambiguous if multiple `rsync` processes are running.
`pidwait` availability depends on the installed procps-ng version and distribution packaging. Check whether it is available on your system with:
command -v pidwait
If it isn’t installed, you can use a polling loop as a fallback:
while pgrep -x rsync > /dev/null; do
sleep 5
done
echo "Backup finished at $(date +%T)"
This loop checks every five seconds until no process named `rsync` remains. Like the `pidwait` example, it returns immediately if no matching process is running when the loop begins.
It also checks only for the process name, not whether a particular backup job completed successfully, so use a more specific process check or an explicit job-status mechanism when that distinction matters.
If you’ve ever written a sleep loop just to wait for a backup, share this with someone who’s still babysitting long-running jobs.
## Pro Tips for Using These Commands Safely in Scripts
Now that you have all five tricks, a few habits make them safer when you move them from the terminal into scripts that run on many servers.
The biggest risk with `pkill -f` is a pattern that’s too broad. A pattern like `pkill -f python` stops every Python program running as your user, including tools you didn’t know were Python-based. Always test the pattern with `pgrep -a` first, and add `-u` to limit it to the user who owns the job.
For heredocs in scripts, quote the marker (`<< 'EOF'`) unless you specifically need variable expansion. An unquoted heredoc that contains a `$` meant for the config file, such as a password or a Nginx variable like `$host`, gets silently replaced with an empty string, and the result is a config file that looks fine but doesn’t work.
Finally, remember that `mkdir -p` only applies the `-m` permission option to the last directory it creates, while the parents get your default permissions.
If you need a whole path created with specific permissions and ownership, install `-d -m 750 -o tecmint -g tecmint /path/to/dir` is the more predictable choice.
##### Conclusion
You now have five commands that each replace a multi-step routine: `mkdir -p` with brace expansion for directory trees, heredocs with `cat` or `sudo tee` for files, `tar -C` for extracting archives anywhere, `pgrep` and `pkill` for finding and stopping processes by name, and `pidwait` for running a command once a process finishes. Each one makes your scripts shorter and removes chances for a typo along the way.
If you’d like to build more habits like these, our 100+ Essential Linux Commands course on Pro TecMint covers the everyday commands sysadmins rely on with practical examples.
When you’re ready to turn tricks like heredocs and brace expansion into full scripts, Bash Scripting for Beginners walks through scripting from the first line to error handling.
We’d love to hear which of these tips you already use and which one is new to you. Do you have your own one-liner that replaced a long routine on your servers?
Have you ever been caught by a `pkill -f` pattern that matched more than you expected? Share your favourite command-line shortcuts in the comments below.