Skip to main content

Chapter 04 · The Vim Editor and Shell Scripts

Chapter Overview

In this chapter I first walk you through the Vim editor—how to write and modify text, and how to work fluently across its three modes. We’ll practice by editing real system files (hostname, network interfaces, and repository definitions) so you truly internalize the most-used commands, shortcuts, and modes.

Next, we revisit highlights from Chapters 2–3 and combine them with logical operators to write practical shell scripts. You’ll learn several ways to accept user input in scripts and how to validate and compare that input as files, integers, or strings. Building on the three logical operators (and, or, not), you’ll practice if, for, while, and case constructs through more than ten hands‑on examples—enough to use them flexibly at work.

Finally, I’ll demonstrate how to schedule one‑off tasks with at and recurring tasks with the crond service, automating jobs by minute, hour, day, month, and weekday so routine work gets done on time—and you get to clock out earlier.

4.1 The Vim Text Editor

Vim dates back to 1991; the name is short for Vi Improved (its logo appears in Figure 4‑1). It extends the classic vi editor; one especially helpful improvement is syntax highlighting, which helps you spot and correct mistakes quickly.

When I teach, there’s a line my students remember because I often raise my voice right before it: “On Linux, everything is a file; configuring a service means editing its configuration files.” In day‑to‑day work you’ll constantly write documents and change configs, and a text editor is the tool you reach for. My goal with Learn Linux the Right Way is to teach genuine Linux administration—not merely “how to operate a particular desktop.” That’s why I standardize on Vim. It’s installed by default on essentially every Linux system and it’s a superb editor.

Figure 4‑1 Vim logo

Vim succeeds in large part because it offers three modes—command, insert, and last‑line—each with its own shortcuts. Once those motions are in your fingers, your efficiency jumps, and editing configs feels natural. The key is to understand what each mode does and how to switch among them (Figure 4‑2).

Command mode: move the cursor; copy, paste, delete, and search.
Insert mode: normal text entry.
Last‑line mode: save or quit; change editor settings; run external commands; jump to a specific line.

Figure 4‑2 Switching among Vim modes

Each Vim session starts in command mode. Switch to insert mode before typing. When you’re done, return to command mode, enter last‑line mode, and save or quit. (You cannot jump directly from insert mode to last‑line mode.)

To help you get productive quickly, Table 4‑1 lists the command‑mode keys you’ll use most often.

Table 4‑1 Frequently used command‑mode keys

CommandAction
ddDelete (cut) the current line
5ddDelete (cut) 5 lines starting here
yyCopy the current line
5yyCopy 5 lines starting here
nJump to the next search match
NJump to the previous search match
uUndo the last change
pPaste the last deleted/yanked text after the cursor

Last‑line mode is where you save, quit, adjust options, execute external Linux commands, or jump to a specific line. Type a colon in command mode to enter it. Table 4‑2 lists the usual commands.

Table 4‑2 Common last‑line commands

CommandAction
:wSave
:qQuit
:q!Quit and discard changes
:wq!Save and quit (force)
:set nuShow line numbers
:set nonuHide line numbers
:commandRun that external command
:numberJump to that line
:s/one/twoReplace the first “one” on the current line with “two”
:s/one/two/gReplace all “one” on the current line
:%s/one/two/gReplace all “one” in the entire file
?textSearch upward for text
/textSearch downward for text

Practice Vim whenever you can. Once you’re fluent, config editing becomes fast and reliable. In 2011, a hacker named Aleksandr Levchuk even built a “Vim Clutch” (Figure 4‑3): two pedals wired over USB—left pedal to enter insert mode (i), right pedal to save & quit (wq!). That’s dedication!

Figure 4‑3 The Vim Clutch prototype

4.1.1 Write a Simple Document

You now have the theory—let’s write a tiny script‑like text file. I’ll label every step; if you forget what a key does, scroll back to the tables above.

First, name the file practice.txt. If it exists, Vim opens it; if not, Vim creates a new buffer (Figure 4‑4).

Figure 4‑4 Opening a text document

When practice.txt opens, you’re in command mode; you can’t type text freely yet. Switch to insert mode with one of a, i, or o (Figure 4‑5):

  • a inserts after the cursor
  • i inserts at the cursor
  • o opens a new line below the cursor

Figure 4‑5 Entering insert mode

Now enter your text (Figure 4‑6).

Figure 4‑6 Typing in the editor

When finished, press Esc to return to command mode (Figure 4‑7), then type :wq! to enter last‑line mode and force a save & quit (Figure 4‑8).

Figure 4‑7 Back to command mode

Figure 4‑8 Save and quit

Tips:
Watch the lower‑left indicator—Vim shows different hints in different modes (compare Figures 4‑6 through 4‑8).

After saving with :wq!, check the content with cat (Figure 4‑9).

Figure 4‑9 Viewing the document

Let’s edit the file again. Because we want to append a new line, press o from command mode to enter insert mode on a new line (Figures 4‑10 to 4‑12).

Figure 4‑10 Editing again in Vim

Figure 4‑11 Back in insert mode

Figure 4‑12 Appending one line of text

Because the buffer has been modified, Vim won’t let you quit without saving (Figures 4‑13 and 4‑14). To exit and discard the changes, use :q! (Figure 4‑15).

Figure 4‑13 Attempting to quit

Figure 4‑14 Refusing to quit due to unsaved edits

Figure 4‑15 Forced quit

Check the file again (Figure 4‑16): the appended line wasn’t saved—as expected after :q!.

Figure 4‑16 Final content after discarding changes

With the basics in hand, here are three small tasks. Do them one by one; don’t skip. If a step fails, that’s fine—focus on getting comfortable with Vim.

Tips:
It’s okay if the following exercises don’t work the first time. Concentrate on Vim; being able to edit configs correctly is the real goal.

4.1.2 Set the Hostname

To make hosts easier to find and distinguish on a LAN, each has an IP and a hostname—much like a short domain. Linux stores the hostname in /etc/hostname. Change it to linuxprobe.com:

Step 1: Open /etc/hostname in Vim.
Step 2: Delete the original text and type linuxprobe.com, then save with :wq!.
Step 3: Verify with the hostname command.

root@linuxprobe:~# vim /etc/hostname
linuxprobe.com

If the change hasn’t taken effect (some systems cache the old name), reboot the VM and check again:

root@linuxprobe:~# hostname
linuxprobe.com

4.1.3 Configure Network Interfaces

Whether two hosts can talk depends first on correct IP configuration. In Linux, everything is a file, so configuring networking means editing the interface’s configuration file. This not only gives you Vim practice—it lays the groundwork for all service configuration that follows. By the time you finish this book, you’ll really feel the payoff: a solid foundation up front, then nearly identical IP and environment across later chapters so you can focus on services, not setup.

If you’ve used older Linux releases, you’ll see naming differences. In RHEL 5/6, interface names were eth0, eth1, and so on. RHEL 7 used names like eno16777736. In RHEL 8/9/10, common names include ens160. Aside from filenames, the parameters are similar.

Assume a device named ens160. We’ll enable autostart and set static IPv4 settings. First, interface profiles live under /etc/NetworkManager/system-connections:

Step 1: Go to /etc/NetworkManager/system-connections.
Step 2: Edit ens160.nmconnection in Vim and fill in the following (the Chinese annotations are for explanation—do not type them). Because hardware varies, confirm your adapter name with ifconfig.

root@linuxprobe:~# cd /etc/NetworkManager/system-connections
root@linuxprobe:/etc/NetworkManager/system-connections# vim ens160.nmconnection
[connection]
id=ens160
type=ethernet
interface-name=ens160
autoconnect=true
[ipv4]
address1=192.168.10.10/24
method=manual
gateway=192.168.10.10
dns=192.168.10.10

Step 3: Restart networking and test connectivity. Remember ping runs until you stop it; press Ctrl+C to end.

root@linuxprobe:/etc/NetworkManager/system-connections# nmcli connection up ens160
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/8)
root@linuxprobe:/etc/NetworkManager/system-connections# nmcli connection reload
root@linuxprobe:/etc/NetworkManager/system-connections# ping 192.168.10.10
PING 192.168.10.10 (192.168.10.10) 56(84) bytes of data.
64 bytes from 192.168.10.10: icmp_seq=1 ttl=64 time=0.079 ms
64 bytes from 192.168.10.10: icmp_seq=2 ttl=64 time=0.050 ms
64 bytes from 192.168.10.10: icmp_seq=3 ttl=64 time=0.083 ms
64 bytes from 192.168.10.10: icmp_seq=4 ttl=64 time=0.038 ms
^C
--- 192.168.10.10 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3092ms
rtt min/avg/max/mdev = 0.038/0.062/0.083/0.019 ms

If it still doesn’t work, don’t worry—later chapters cover networking in depth. For now, keep your attention on Vim.

4.1.4 Configure a Software Repository

Earlier we learned that repositories make RPM package management far easier: DNF/Yum automatically analyze dependencies and install what’s needed. Think of a repository as a giant warehouse: tell it the package name, and it handles the rest. In RHEL 10 you can use both dnf and yum; I recommend dnf for better performance and multi‑threaded installs.

Step 1: Go to /etc/yum.repos.d.
Step 2: Create a new repo file named rhel10.repo (the filename is arbitrary, but must end with .repo) and fill in the parameters below, then save and exit.

Repository ID: a unique identifier that must not collide with other repos.
Description (name): a human‑readable label for the repo.
Location (baseurl): where packages are fetched—FTP, HTTP, or a local path (prefix local paths with file://).
Enabled (enabled): set to 1 to enable, 0 to disable.
GPG check (gpgcheck): 1 to verify signatures, 0 to skip verification.
GPG key (gpgkey): the public‑key file location if gpgcheck=1; otherwise omit.

Step 3: Mount the installation media at the location you referenced, and add it to /etc/fstab for persistent mounts.
Step 4: Test with dnf install httpd -y.

root@linuxprobe:~# cd /etc/yum.repos.d
root@linuxprobe:/etc/yum.repos.d# vim rhel10.repo
[BaseOS]
name=BaseOS
baseurl=file:///media/cdrom/BaseOS
enabled=1
gpgcheck=0
[AppStream]
name=AppStream
baseurl=file:///media/cdrom/AppStream
enabled=1
gpgcheck=0

Create a mount point, mount the disc, and configure automatic mounting (see Chapter 6):

root@linuxprobe:/etc/yum.repos.d# mkdir -p /media/cdrom 
root@linuxprobe:/etc/yum.repos.d# mount /dev/cdrom /media/cdrom
mount: /media/cdrom: WARNING: device write-protected, mounted read-only.
root@linuxprobe:/etc/yum.repos.d# vim /etc/fstab
………………(output truncated in book for brevity)………………
/dev/cdrom /media/cdrom iso9660 defaults 0 0

Try installing the Apache Web server package (httpd). Seeing Complete! indicates success:

root@linuxprobe:/etc/yum.repos.d# dnf install httpd -y
Updating Subscription Management repositories.
BaseOS 18 MB/s | 1.7 MB 00:00
AppStream 80 MB/s | 6.3 MB 00:00
Dependencies resolved.
………………(output truncated)………………
Installed:
apr-1.7.0-11.el9.x86_64 apr-util-1.6.1-20.el9.x86_64
apr-util-bdb-1.6.1-20.el9.x86_64 apr-util-openssl-1.6.1-20.el9.x86_64
httpd-2.4.53-11.el9_2.4.x86_64 httpd-core-2.4.53-11.el9_2.4.x86_64
httpd-filesystem-2.4.53-11.el9_2.4.noarch httpd-tools-2.4.53-11.el9_2.4.x86_64
mod_http2-1.15.19-4.el9_2.4.x86_64 mod_lua-2.4.53-11.el9_2.4.x86_64
redhat-logos-httpd-90.4-1.el9.noarch

Complete!

If you’re used to yum, that still works—RHEL 10 keeps the commands compatible.

4.2 Write Shell Scripts

You can think of the shell as the “translator” between people and hardware. Beyond variables and parameters, Bash offers the same control structures you see in higher‑level languages, such as loops and branches. In practice, scripts run in two styles:

Interactive: you type a command and the system executes it immediately.
Batch: you write a complete script and the shell executes all of its commands at once.

You’ll use many commands you learned earlier plus regexes, pipelines, and redirection; we’ll group related pieces into modules and tie them together with logic to form the scripts you see in daily work.

Confirm the default interpreter is Bash:

root@linuxprobe:~# echo $SHELL
/bin/bash

4.2.1 A First Script

After reading the formal definition you might be thinking, “this sounds complicated.” But that description applies to advanced scripting. A simple script is just a text file where you write ordinary Linux commands in order.

For example, to print the current working directory and then list all files with attributes:

root@linuxprobe:~# vim example.sh
#!/bin/bash
#For Example by linuxprobe.com
pwd
ls -al

Script filenames are arbitrary, but I recommend using the .sh suffix so you and others recognize them at a glance.

This tiny script actually contains three kinds of content:

The shebang on the first line (#!) tells the OS which interpreter to use.
Lines beginning with # are comments that explain the script’s purpose or warn about side effects—useful to you or anyone else who reads it later.
The remaining lines are executable commands—the same Linux commands you run interactively.

Try it:

root@linuxprobe:~# bash example.sh
/root
total 44
dr-xr-x---. 14 root root 4096 Mar 12 23:53 .
dr-xr-xr-x. 18 root root 235 Mar 10 02:32 ..
-rw-------. 1 root root 1019 Mar 10 02:37 anaconda-ks.cfg
-rw-------. 1 root root 52 Mar 10 10:51 .bash_history
-rw-r--r--. 1 root root 18 Jun 24 2024 .bash_logout
-rw-r--r--. 1 root root 141 Jun 24 2024 .bash_profile
-rw-r--r--. 1 root root 429 Jun 24 2024 .bashrc
………………(output truncated)………………

Another way to run a script is by path. By default, you’ll get a permissions error; grant execute permission (Chapter 5 covers this in detail) and try again:

root@linuxprobe:~# ./example.sh
bash: ./example.sh: Permission denied
root@linuxprobe:~# chmod u+x example.sh
root@linuxprobe:~# ./example.sh
/root
total 44
dr-xr-x---. 14 root root 4096 Mar 12 23:53 .
dr-xr-xr-x. 18 root root 235 Mar 10 02:32 ..
-rw-------. 1 root root 1019 Mar 10 02:37 anaconda-ks.cfg
-rw-------. 1 root root 52 Mar 10 10:51 .bash_history
-rw-r--r--. 1 root root 18 Jun 24 2024 .bash_logout
-rw-r--r--. 1 root root 141 Jun 24 2024 .bash_profile
-rw-r--r--. 1 root root 429 Jun 24 2024 .bashrc
………………(output truncated)………………

4.2.2 Receive User Parameters

The simple script above can only perform predefined work—too rigid. To meet real‑time needs, scripts must accept parameters, just like commands do.

For example, different parameters produce different wc outputs:

root@linuxprobe:~# wc -l anaconda-ks.cfg 
40 anaconda-ks.cfg
root@linuxprobe:~# wc -c anaconda-ks.cfg
1019 anaconda-ks.cfg
root@linuxprobe:~# wc -w anaconda-ks.cfg
92 anaconda-ks.cfg

Bash already provides positional parameters, separated by spaces: $0 is the script name, $# the number of parameters, $* all parameters, $? the previous command’s exit code, and $1, $2, $3 … are the first, second, third, and so on (Figure 4‑17).

Figure 4‑17 Positional parameters in a shell script

Let’s see them in action:

root@linuxprobe:~# vim Example.sh
#!/bin/bash
echo "Script name: $0"
echo "There are $# parameters: $*."
echo "The 1st is $1; the 5th is $5."
root@linuxprobe:~# bash Example.sh one two three four five six
Script name: Example.sh
There are 6 parameters: one two three four five six.
The 1st is one; the 5th is five.

4.2.3 Make Decisions on Parameters

Learning is a climb from basics to mastery. After Linux commands, script syntax, and parameter handling, the next step is processing those parameters.

Earlier we saw that when mkdir runs, it checks your input: if the directory already exists, it prints an error; otherwise it creates it. Shell test expressions check whether conditions are true. A true test returns 0; false returns a non‑zero value. Figure 4‑18 shows the general form—remember to include spaces around the expression.

Figure 4‑18 Test expression syntax

There are four common families: file tests, logical operators, integer comparisons, and string comparisons.

  1. File tests

Use file‑test operators to check whether files exist and whether permissions are satisfied (Table 4‑3).

Table 4‑3 File‑test operators

OperatorMeaning
-dIs a directory
-eExists
-fIs a regular file
-rCurrent user has read permission
-wCurrent user has write permission
-xCurrent user has execute permission

Examples:

root@linuxprobe:~# [ -d /etc/fstab ]
root@linuxprobe:~# echo $?
1
root@linuxprobe:~# [ -f /etc/fstab ]
root@linuxprobe:~# echo $?
0
  1. Logical operators

Chain tests with logical “and,” “or,” and “not.”

root@linuxprobe:~# [ -e /dev/cdrom ] && echo "Exist"
Exist
root@linuxprobe:~# echo $USER
root
root@linuxprobe:~# [ $USER = root ] || echo "user"
root@linuxprobe:~# su - linuxprobe
linuxprobe@linuxprobe:~$ [ $USER = root ] || echo "user"
user
linuxprobe@linuxprobe:~$ exit
root@linuxprobe:~# [ ! $USER = root ] || echo "administrator"
administrator

Tips:
&& runs the right‑hand command only if the left succeeded.
|| runs the right‑hand command only if the left failed.
! inverts the test result (true becomes false; false becomes true).

A compact example that prints user for non‑root and root otherwise:

root@linuxprobe:~# [ ! $USER = root ] && echo "user" || echo "root"
root

In shell logic, && and || short‑circuit. Only when the && condition fails does the || clause run.

  1. Integer comparisons

Use the integer comparison operators in Table 4‑4. These work only with numbers; don’t mix in strings or files. Also avoid the everyday =, <, > symbols here: = conflicts with assignment; > and < are redirection operators.

Table 4‑4 Integer comparison operators

OperatorMeaning
-eqEqual to
-neNot equal to
-gtGreater than
-ltLess than
-leLess than or equal to
-geGreater than or equal to

Examples:

root@linuxprobe:~# [ 10 -gt 10 ]
root@linuxprobe:~# echo $?
1
root@linuxprobe:~# [ 10 -eq 10 ]
root@linuxprobe:~# echo $?
0

A practical check: warn if free memory is below a threshold. First examine memory usage, then isolate the “free” column, and finally assign it to a variable:

root@linuxprobe:~# free -m
total used free shared buff/cache available
Mem: 3879 1370 2008 14 746 2509
Swap: 2047 0 2047
root@linuxprobe:~# free -m | grep Mem:
Mem: 3879 1371 2006 14 746 2508
root@linuxprobe:~# free -m | grep Mem: | awk '{print $4}'
2006
root@linuxprobe:~# FreeMem=`free -m | grep Mem: | awk '{print $4}'`
root@linuxprobe:~# echo $FreeMem
2006
root@linuxprobe:~# [ $FreeMem -lt 2024 ] && echo "Insufficient Memory"
Insufficient Memory
  1. String comparisons

String tests check for emptiness or equality (Table 4‑5).

Table 4‑5 Common string operators

OperatorMeaning
=Strings are equal
!=Strings differ
-zString is empty

Examples:

root@linuxprobe:~# [ -z $String ]
root@linuxprobe:~# echo $?
0
root@linuxprobe:~# echo $LANG
en_US.UTF-8
root@linuxprobe:~# [ ! $LANG = "en.US" ] && echo "Not en.US"
Not en.US

4.3 Control‑Flow Statements

Although you can glue commands together with pipelines and &&/||, production scripts need adaptability: they must branch when conditions change and repeat tasks as needed. By default the shell executes top‑to‑bottom, but if one step fails, blindly continuing often makes things worse. Real work calls for conditionals and loops.

To make this concrete, imagine the “plan in your head” when you meet someone you like (Figure 4‑19). If the chat doesn’t go well, you won’t proceed to ask for a number, then dinner, then a movie—you’ll change course. Scripts need the same flexibility.

Figure 4‑19 A mental flow

We’ll start with if, then practice for, while, and case. I’ll keep the examples realistic and self‑contained so you learn by doing rather than patching a single script repeatedly.

4.3.1 if Conditionals

A single‑branch if runs commands only when the test is true (Figure 4‑20).

Figure 4‑20 Single‑branch if

Create /media/cdrom if it doesn’t exist:

root@linuxprobe:~# vim mkcdrom.sh
#!/bin/bash
DIR="/media/cdrom"
if [ ! -d "$DIR" ]; then
mkdir -p "$DIR"
fi
root@linuxprobe:~# bash mkcdrom.sh
root@linuxprobe:~# ls -ld /media/cdrom
drwxr-xr-x. 2 root root 6 Mar 13 00:00 /media/cdrom

A two‑branch if … else … selects one of two paths (Figure 4‑21).

Figure 4‑21 Two‑branch if

Ping a host and print Online/Offline based on the exit code:

root@linuxprobe:~# vim chkhost.sh
#!/bin/bash
ping -c 3 -i 0.2 -W 3 "$1" &> /dev/null
if [ $? -eq 0 ]; then
echo "Host $1 is On-line."
else
echo "Host $1 is Off-line."
fi
root@linuxprobe:~# bash chkhost.sh 192.168.10.10
Host 192.168.10.10 is On-line.
root@linuxprobe:~# bash chkhost.sh 192.168.10.20
Host 192.168.10.20 is Off-line.

A multi‑branch if … elif … else … handles ranges (Figure 4‑22).

Figure 4‑22 Multi‑branch if

Classify a score:

root@linuxprobe:~# vim chkscore.sh
#!/bin/bash
read -p "Enter your score (0-100): " GRADE
if [ "$GRADE" -ge 85 ] && [ "$GRADE" -le 100 ]; then
echo "$GRADE is Excellent"
elif [ "$GRADE" -ge 70 ] && [ "$GRADE" -le 84 ]; then
echo "$GRADE is Pass"
else
echo "$GRADE is Fail"
fi
root@linuxprobe:~# bash chkscore.sh
Enter your score (0-100): 88
88 is Excellent

root@linuxprobe:~# bash chkscore.sh
Enter your score (0-100): 80
80 is Pass

4.3.2 for Loops

for loops iterate over lists or ranges (Figure 4‑23). They shine when you already know the items to process.

Figure 4‑23 for loop

Create several users from a text file:

root@linuxprobe:~# vim users.txt
andy
barry
carl
duke
eric
george
winston
root@linuxprobe:~# vim addusers.sh
#!/bin/bash
read -p "Enter The Users Password : " PASSWD
for UNAME in $(cat users.txt); do
id "$UNAME" &> /dev/null
if [ $? -eq 0 ]; then
echo "$UNAME , Already exists"
else
useradd "$UNAME"
echo "$PASSWD" | passwd --stdin "$UNAME" &> /dev/null
echo "$UNAME , Create success"
fi
done

Tips:
/dev/null is Linux’s black hole—redirect noisy output there to keep the screen clean.

Run it, then confirm in /etc/passwd:

root@linuxprobe:~# bash addusers.sh
Enter The Users Password : linuxprobe
andy , Create success

winston , Create success

root@linuxprobe:~# tail -6 /etc/passwd
andy:x:1001:1001::/home/andy:/bin/bash
barry:x:1002:1002::/home/barry:/bin/bash
carl:x:1003:1003::/home/carl:/bin/bash
duke:x:1004:1004::/home/duke:/bin/bash
eric:x:1005:1005::/home/eric:/bin/bash
george:x:1006:1006::/home/george:/bin/bash
winston:x:1007:1007::/home/winston:/bin/bash

Process a list of hosts:

root@linuxprobe:~# vim ipaddrs.txt
192.168.10.10
192.168.10.11
192.168.10.12
root@linuxprobe:~# vim CheckHosts.sh
#!/bin/bash
HLIST=$(cat ~/ipaddrs.txt)
for IP in $HLIST; do
ping -c 3 -i 0.2 -W 3 "$IP" &> /dev/null
if [ $? -eq 0 ]; then
echo "Host $IP is On-line."
else
echo "Host $IP is Off-line."
fi
done
root@linuxprobe:~# bash CheckHosts.sh
Host 192.168.10.10 is On-line.
Host 192.168.10.11 is On-line.
Host 192.168.10.12 is Off-line.

4.3.3 while Loops

while repeats as long as a condition is true (Figure 4‑24). It’s ideal when you don’t know in advance how many times you’ll iterate.

Figure 4‑24 while loop

Count from 1 to 10:

root@linuxprobe:~# vim count.sh
#!/bin/bash
N=1
while [ $N -le 10 ]; do
echo $N
N=$((N+1))
done
root@linuxprobe:~# bash count.sh
1
2
3
4
5
6
7
8
9
10

Monitor free memory every 2 seconds, stop when it dips below a threshold:

root@linuxprobe:~# vim watchmem.sh
#!/bin/bash
THRESH=2024
while true; do
FreeMem=$(free -m | awk '/^Mem:/{print $4}')
echo "Free: $FreeMem MB"
[ "$FreeMem" -lt "$THRESH" ] && echo "Insufficient Memory" && break
sleep 2
done

4.3.4 case Statements

case selects behavior based on patterns—often simpler than a forest of if/elif. (Figure 4‑25)

Figure 4‑25 case statement

Start/stop/restart a service by name:

root@linuxprobe:~# vim svc.sh
#!/bin/bash
ACTION="$1"
SERVICE="$2"
case "$ACTION" in
start) systemctl start "$SERVICE" ;;
stop) systemctl stop "$SERVICE" ;;
restart) systemctl restart "$SERVICE" ;;
status) systemctl status "$SERVICE" ;;
*) echo "Usage: $0 {start|stop|restart|status} <service>" ;;
esac
root@linuxprobe:~# bash svc.sh status sshd | head -5
● sshd.service - OpenSSH server daemon
Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: enabled)
Active: active (running) ...

4.4 Schedule Tasks with at and crond

The last step in this chapter is automation. For one‑time jobs, use at; for recurring jobs, use crond.

4.4.1 One‑Time Jobs with at

Enable the atd service and schedule a job for 5 minutes from now:

root@linuxprobe:~# systemctl enable --now atd
root@linuxprobe:~# at now + 5 minutes
at> echo "Hello from at at $(date)" >> /root/at.log
at> <Ctrl+D>
job 2 at Tue Mar 18 10:35:00 2025

List and remove pending at jobs:

root@linuxprobe:~# atq
2 Tue Mar 18 10:35:00 2025 a root
root@linuxprobe:~# atrm 2

4.4.2 Recurring Jobs with crond

crond runs commands at fixed times/dates, defined in crontab files. Each line has five time fields plus the command:

# ┌─ minute (0 - 59)
# │ ┌─ hour (0 - 23)
# │ │ ┌─ day of month (1 - 31)
# │ │ │ ┌─ month (1 - 12)
# │ │ │ │ ┌─ day of week (0 - 7) (Sunday = 0 or 7)
# │ │ │ │ │
# * * * * * command-to-execute

Common patterns:

*/5 * * * *   /usr/local/bin/backup.sh          # every 5 minutes
0 2 * * * /usr/local/bin/rotate-logs.sh # daily at 02:00
0 3 * * 0 /usr/local/bin/full-backup.sh # Sundays at 03:00

Edit your user’s crontab and add a simple test job:

root@linuxprobe:~# systemctl enable --now crond
root@linuxprobe:~# crontab -e
# add:
*/1 * * * * echo "Cron tick at $(date)" >> /root/cron.log

Check whether crond is running and tail the log after a minute:

root@linuxprobe:~# systemctl status crond | sed -n '1,5p'
root@linuxprobe:~# tail -3 /root/cron.log
Cron tick at Tue Mar 18 10:36:00 CST 2025
Cron tick at Tue Mar 18 10:37:00 CST 2025
Cron tick at Tue Mar 18 10:38:00 CST 2025

Tips:
System‑wide cron jobs live under /etc/crontab and /etc/cron.*. For safety, prefer per‑user crontabs (crontab -e) unless you specifically need system‑wide behavior.


Review Questions

  1. Which Vim mode do you use to save and quit a file?
    Answer: Last‑line mode (:wq, :q!, etc.).

  2. Where does Linux store the hostname? What command verifies it?
    Answer: /etc/hostname; verify with hostname.

  3. What’s the difference between interactive and batch shell usage?
    Answer: Interactive executes each command immediately; batch runs a script of commands.

  4. What exit code represents “true” for a test expression in Bash?
    Answer: 0 means true/success; non‑zero means false/failure.

  5. Write a one‑liner that echoes “Exist” only if /dev/cdrom exists.
    Answer: [ -e /dev/cdrom ] && echo "Exist"

  6. Which integer comparison operators mean “less than” and “greater than or equal to”?
    Answer: -lt and -ge.

  7. Show a compact command that prints user when the current user is not root, otherwise prints root.
    Answer: [ ! "$USER" = root ] && echo "user" || echo "root"

  8. In for vs. while, when would you prefer each?
    Answer: Use for when you already know the items; use while when you loop until a condition changes.

  9. What is the basic crontab time‑field layout?
    Answer: minute hour day-of-month month day-of-week command

  10. Which tool schedules a one‑time command for, say, “now + 10 minutes”?
    Answer: at (with the atd service running).