Hello, Guest! 👋 You're just a few clicks away from joining an exclusive space for tech enthusiasts, problem-solvers, and lifelong learners like you.
🔐Why Join? By becoming a member of CodeNameJessica, you’ll get access to: ✅ In-depth discussions on Linux, Security, Server Administration, Programming, and more ✅ Exclusive resources, tools, and scripts for IT professionals ✅ A supportive community of like-minded individuals to share ideas, solve problems, and learn together ✅Project showcases, guides, and tutorials from our members ✅Personalized profiles and direct messaging to collaborate with other techies
🌐Sign Up Now and Unlock Full Access! As a guest, you're seeing just a glimpse of what we offer. Don't miss out on the complete experience! Create a free account today and start exploring everything CodeNameJessica has to offer.
I told you about the AWK tutorial series in the previous newsletter. Well, it has an awkward start. I thought I would be able to finish, but I could only complete the first three chapters. Accept my apologies. I have the additional responsibilities of a month-old child now 😊
You already saw a few built-in variables in the first chapter. Let's have a look at some other built-in variables along with the ones you already saw. Repitition is good for reinforced learning.
Sample Data Files
Let me create some sample files for you to work with. Save these to follow along the tutorial on your system:
Create access.log:
192.168.1.100 - alice [29/Jun/2024:10:15:22] "GET /index.html" 200 1234
192.168.1.101 - bob [29/Jun/2024:10:16:45] "POST /api/login" 200 567
192.168.1.102 - charlie [29/Jun/2024:10:17:10] "GET /images/logo.png" 404 0
10.0.0.50 - admin [29/Jun/2024:10:18:33] "GET /admin/panel" 403 892
192.168.1.100 - alice [29/Jun/2024:10:19:55] "GET /profile" 200 2456
In this example, it uses comma as a separator to extract product type, manufacturer, and price from CSV.
laptop by Dell costs $1299.99
desktop by HP costs $899.50
tablet by Apple costs $599.00
monitor by Samsung costs $349.99
keyboard by Logitech costs $99.99
Of course, you can simply use awk '{print $3 "," $1 "," $7}' access.log to achieve the same output, but that's not the point here.
📋
BEGIN is a special block that ensures your formatting is set up correctly before any data processing begins, making it perfect for this type of data transformation task. You can also use it without BEGIN:
awk -v OFS="," '{print $3, $1, $7}' access.log
Similarly, let's change our inventory csv to a pipe-delimited report:
Note that the original files are not touched. You see the output on STDOUT. They are not written on the input file.
RS (Record Separator): How you define records
RS tells AWK where one record ends and another begins.
We'll use a new sample file multiline_records.txt:
Name: John Smith
Age: 35
Department: Engineering
Salary: 75000
Name: Mary Johnson
Age: 42
Department: Marketing
Salary: 68000
Name: Bob Wilson
Age: 28
Department: Sales
Salary: 55000
It is a bit complicated, but assuming that you are terating data files, it will be worth the effort. Here, awk treats empty lines as record separators and each line (\n) within a record as a field, then extracts the values after the colons.
Look at the formatted output now:
John Smith 35 Engineering 75000
Mary Johnson 42 Marketing 68000
Bob Wilson 28 Sales 55000
ORS (Output Record Separator): How you end records
ORS controls what goes at the end of each output record - think of it as choosing your punctuation mark.
For example, if you use this command with inventory.csv file:
NF tells you how many fields are in each record (row/line). This is excellent when you have to loop on data (discussed in later chapters) or have to get the last column/field for processing.
Server web01 has 2 fields: web01 active
Server db02 has 4 fields: db02 maintenance scheduled friday
Server cache01 has 2 fields: cache01 offline
Server backup01 has 4 fields: backup01 running full-backup nightly
Server api-server has 3 fields: api-server online load-balanced
Let's take another example where it always prints the last field irrespective of the number of fields:
awk '{print $1 ":", $NF}' variable_fields.txt
Works fine, right?
web01: active
db02: friday
cache01: offline
backup01: nightly
api-server: load-balanced
📋
There is no check on the number of columns. If a line has only 5 fields and you want to display the 6th field, it will show blank. There won't be any error.
FILENAME: Your file tracker
FILENAME shows which file is being processed. This is essential when you handle multiple files.
As you can see, it finds all ERROR lines and shows which file they came from.
server1.log: ERROR: Database connection failed
server2.log: ERROR: Network timeout
server2.log: ERROR: Disk space low
FNR (File Number of Records): Your per-file counter
Another in-built AWK variable that helps while dealing with multiple files. FNR resets to 1 for each new file.
Imagine a situation where you have two files to deal with AWK. If you use NR, it will count the number of rows from both files together. FNR on the other hand, will give you the number of records from each file.
It shows both the line number within each file (FNR) and the overall line number (NR) across all files.
server1.log line 1 (overall line 1): ERROR: Database connection failed
server1.log line 2 (overall line 2): WARN: High memory usage
server1.log line 3 (overall line 3): INFO: Backup completed
server2.log line 1 (overall line 4): ERROR: Network timeout
server2.log line 2 (overall line 5): INFO: Service restarted
server2.log line 3 (overall line 6): ERROR: Disk space low
2024-06-29::10:15:22::INFO::System started successfully
2024-06-29::10:16:45::ERROR::Database connection timeout
2024-06-29::10:17:10::WARN::Low disk space detected
2024-06-29::10:18:33::ERROR::Network unreachable
Think of AWK patterns like a security guard at a nightclub - they decide which lines get past the velvet rope and which actions get executed. Master pattern matching, and you control exactly what AWK processes.
Pattern matching fundamentals
AWK patterns work like filters: they test each line and execute actions only when conditions are met. No match = no action.
Here are some very basic examples of pattern matching:
Regular expressions in AWK use the same syntax as grep and sed. The pattern sits between forward slashes /pattern/.
You must have some basic understanding of regex to use the pattern matching.
Tip: Instead of multiple AWK calls:
awk '/ERROR/ file && awk '/WARNING/ file
Use one call with OR:
awk '/ERROR|WARNING/ {print}' file
💡
I advise creating the data files and trying all the commands on your system. This will give you a lot better understanding of concepts than just reading the text and mentioned outputs.
Conditional operations: making decisions with if-else
AWK's if-else statements work like traffic lights - they direct program flow based on conditions.
server01 cpu 75 memory 60 disk 45
server02 cpu 45 memory 30 disk 85
server03 cpu 95 memory 85 disk 70
server04 cpu 25 memory 40 disk 20
server05 cpu 65 memory 75 disk 90
And we shall see how you can use if-else to print output that matches a certain pattern.
Simple if statement: Binary decisions
Think of if like a bouncer - one condition, one action.
Let's use this command with the performance.txt we created previously:
server01 DISK OK
server02 HIGH DISK
server03 DISK OK
server04 DISK OK
server05 HIGH DISK
if-else structure
Every server gets classified - no line escapes without a label.
📋
The multi-line AWK command can be copy-pasted as it is in the terminal and it should run fine. While it all is just one line, it is easier to understand when written across lines. When you are using it inside bash scripts, always use multiple lines.
if-else if-else chain: Multi-tier classification
Think of nested conditions like a sorting machine - items flow through multiple gates until they find their category.
awk '{
if ($5 > 80)
status = "CRITICAL"
else if ($5 > 60)
status = "WARNING"
else if ($5 > 40)
status = "MODERATE"
else
status = "OK"
print $1, "disk:", $5"%", status
}' performance.txt
This tiered approach mimics real monitoring systems - critical issues trump warnings, combined load factors matter. You can combine it with proc data and cron to convert it into an actual system resource alert system.
Comparison operators
AWK comparison operators work like mathematical symbols - they return true and false for conditions. This gives you greater control to put login in place.
We will use the following data files for our testing in this section.
A server_stats.txt file that has the hostname, cpu_cores, memory_mb, cpu_usage, status fields.
And a network_ports.txt file that has ip, service, port, protocol and state fields.
192.168.1.10 ssh 22 tcp open
192.168.1.10 http 80 tcp open
192.168.1.15 ftp 21 tcp closed
192.168.1.20 mysql 3306 tcp open
192.168.1.25 custom 8080 tcp open
192.168.1.30 ssh 22 tcp open
Numeric comparisons
Numeric comparisons are simple. You use the regular <,>,= etc symbols for comparing numbers.
Greater than - Like checking if servers exceed CPU thresholds:
web01 is running
db01 is running
db02 is running
backup01 is running
Exact String Match
Pattern match (~)
Let's find ports that are running a database like sql:
awk '$2 ~ /sql/ {print $1, "database service on port", $3}' network_ports.txt
Output:
192.168.1.20 database service on port 3306
Pattern Match
Does NOT match (!~):
When you want to exclude the matches. For example,
echo -e "# This is a comment\nweb01 active\n# Another comment\ndb01 running" | awk '$1 !~ /^#/ {print "Valid config:", $0}'
The output will omit lines starting with #:
Valid config: web01 active
Valid config: db01 running
String Comparisons: Does not Match
💡
The ~ operator is like a smart search - it finds patterns within strings, not exact matches.
Logical operators: &&, || and !
Logical operators work like sentence connectors - they join multiple conditions into complex tests. You'll be using them as well to add complex logic to your scripts.
Here's the test file process_list.txt for this section:
It has process, pid, user, memory_mb, cpu_percent and status fields.
AND Operator (&&) - Both conditions must be true
Let's find processes that are both memory AND CPU hungry in our input file. Let's filter the lines that have RAM more than 100 and CPU usage greater than 5.
Now, let's see some real-world scenarios where you can use these operators. It will also have some elements from the previous chapters.
Example 1: Failed SSH login analysis
Find failed SSH attempts with IP addresses. Please note that this may not output anything if you are on a personal system that does not accept SSH connections.
It is more precise to search in fields than searching entire line, when it is suited. For example, if you are looking for username that starts with adm, use awk '$1 ~ /^adm/ {print}' /etc/passwd as you know only the first field consists of usernames.
🪧 Time to recall
In this chapter, you've learned:
Patterns filter which lines get processed
Comparison operators test numeric and string conditions
Logical operators combine multiple tests
Regular expressions provide flexible string matching
Field-specific patterns offer precision control
Practice Exercises
Find all users with UID greater than 1000 in /etc/passwd
Extract error and warning messages from a log file (if you have one)
Show processes using more than 50% CPU from ps aux output
Find /etc/ssh/sshd_config configuration lines that aren't comments
Identify network connections on non-standard ports (see one of the examples below for reference)
In the next chapter, learn about built-in variables and field manipulation - where AWK transforms from a simple filter into a data processing powerhouse.
If you're a Linux system administrator, you've probably encountered situations where you need to extract specific information from log files, process command output, or manipulate text data.
While tools like grep and sed are useful, there's another much more powerful tool in your arsenal that can handle complex text processing tasks with remarkable ease: AWK.
What is AWK and why should You care about it?
AWK is not just a UNIX command, it is a powerful programming language designed for pattern scanning and data extraction. Named after its creators (Aho, Weinberger, and Kernighan), AWK excels at processing structured text data, making it invaluable for system administrators who regularly work with log files, configuration files, and command output.
Here's why AWK should be in your and every sysadmin's toolkit:
Built for text processing: AWK automatically splits input into fields and records, making column-based data trivial to work with.
Pattern-action paradigm: You can easily define what to look for (pattern) and what to do when found (action).
No compilation needed: Unlike most other programming languages, AWK scripts do not need to be compiled first. AWK scripts run directly and thus making them perfect for quick one-liners and shell integration.
Handles complex logic: Unlike simple Linux commands, AWK supports variables, arrays, functions, and control structures.
Available everywhere: Available on virtually every Unix-like system and all Linux distros by default.
AWK vs sed vs grep: When to use which tool
grep, sed and awk all three deal with data processing and that may make you wonder whether you should use sed or grep or AWK.
In my opinion, you should
Use grep when:
You need to find lines matching a pattern
Simple text searching and filtering
Binary yes/no decisions about line content
# Find all SSH login attempts
grep "ssh" /var/log/auth.log
Use sed when:
You need to perform find-and-replace operations
Simple text transformations
Stream editing with minimal logic
# Replace all occurrences of "old" with "new"
sed 's/old/new/g' file.txt
Use AWK when:
You need to work with columnar data
Complex pattern matching with actions
Mathematical operations on data
Multiple conditions and logic branches
Generating reports or summaries
# Print username and home directory from /etc/passwd
awk -F: '{print $1, $6}' /etc/passwd
Now that you are a bit more clear about when to use AWK, let's see the basics of AWK command structure.
Basic AWK syntax and structure
AWK follows a simple but powerful syntax:
awk 'pattern { action }' file
Pattern: Defines when the action should be executed (optional)
Action: What to do when the pattern matches (optional)
File: Input file to process (can also read from stdin)
💡
If you omit the pattern, the action applies to every line. If you omit the action, matching lines are printed (like grep).
Let's get started with using AWK for some simple but interesting use cases.
Your first AWK Command: Printing specific columns
Let's start with a practical example. Suppose you want to see all users in Linux and their home directories from /etc/passwd file:
awk -F: '{print $1, $6}' /etc/passwd
The output should be something like this:
See all users
root /root
daemon /usr/sbin
bin /bin
sys /dev
sync /bin
games /usr/games
...
Let's break this doww:
-F: sets the field separator to colon (since /etc/passwd uses colons)
$1 refers to the first field (username)
$6 refers to the sixth field (home directory)
print outputs the specified fields
Understanding AWK's automatic field splitting and
AWK automatically splits each input line into fields based on whitespace (i.e. spaces and tabs) by default. Each field is accessible through variables:
$0 - The entire line
$1 - First field
$2 - Second field
$NF - Last field (NF = Number of Fields)
$(NF-1) - Second to last field
Let's see it in action by extracting process information from the ps command output.
ps aux | awk '{print $1, $2, $NF}'
This prints the user, process ID, and command for each running process.
Process ID and Command
NF is one of the several built-in variables
Know built-in variables: Your data processing toolkit
AWK provides several built-in variables that make text processing easier.
AWK built-in variables
FS (Field separator)
By default, AWK uses white space, i.e. tabs and spaces as field separator. With FS, you can define other field separators in your input.
For example, /etc/passwd file contains lines that have values separated by colon : so if you define file separator as : and extract the first column, you'll get the list of the users on the system.
awk -F: '{print $1}' /etc/passwd
Field Separator
You'll get the same result with the following command.
awk 'BEGIN {FS=":"} {print $1}' /etc/passwd
More about BEGIN in later part of this AWK series.
NR (Number of records/lines)
NR keeps track of the current line number. It is helpful when you have to take actions for certain lines in a file.
For example, the command below will output the content of the /etc/passwd file but with line numbers attached at the beginning.
awk '{print NR, $0}' /etc/passwd
Print with line number
NF (Number of fields)
NF contains the number of fields in the current record. Which is basically number of columns when separated by FS.
For example, the command below will print exactly 5 fields in each line.
awk -F: 'NF == 5 {print $0}' /etc/passwd
Number of Fields
Practical examples for system administrators
Let's see some practical use cases where you can utilize the power of AWK.
Example 1: Analyzing disk usage
The command below shows disk usage percentages and mount points, sorted by usage where as skipping the header line with NR > 1.
df -h | awk 'NR > 1 {print $5, $6}' | sort -nr
Analyze Disk usage
Example 2: Finding large files
I know that find command is more popular for this but you can also use AWK to print file sizes and names for files larger than 1MB.
Pattern matching in AWK works like a smart bouncer - it evaluates conditions and controls access to actions. Master these concepts:It is slightly complicated than the other examples, so let me break it down for you.
Typical free command output looks like this:
total used free shared buff/cache available
Mem: 7974 3052 723 321 4199 4280
Swap: 2048 10 2038
With NR==2 we only take the second line from the above output. $2 (second column) gives total memory and $3 gives used memory.
Next, in printf "Memory Usage: %.1f%%\n", $3/$2 * 100 , the printf command prints formatted output, %.1f%% shows one decimal place, and a % symbol and $3/$2 * 100 calculates memory used as a percentage.
So in the example above, you get the output as Memory Usage: 38.3% where 3052 is ~38.3% of 7974.
You'll learn more on arithmetical operation with AWK later in this series.
🪧 Time to recall
In this introduction, you've learned:
AWK is a pattern-action language perfect for structured text processing
It automatically splits input into fields, making columnar data easy to work with
Built-in variables like NR, NF, and FS provide powerful processing capabilities
AWK excels where grep and sed fall short: complex data extraction and manipulation
AWK's real power becomes apparent when you need to process data that requires logic, calculations, or complex pattern matching.
In the next part of this series, we'll dive deeper into pattern matching and conditional operations that will transform how you handle text processing tasks.
🏋️ Practice exercises
Try these exercises to reinforce what you've learned:
Display only usernames from /etc/passwd
Show the last field of each line in any file
Print line numbers along with lines containing "root" in /etc/passwd
Extract process names and their memory usage from ps aux output
Count the number of fields in each line of a file
The solutions involve combining the concepts we've covered: field variables, built-in variables, and basic pattern matching.
In the next tutorial, we'll explore these patterns in much more detail.
Is it too 'AWKward' to use AWK in the age of AI? I don't think so. AWK is so underrated despite being so powerful for creating useful automation scripts.
In this technologically rich era, businesses deploy servers in no time and also manage hundreds of devices on the cloud. All this is possible with the assistance of Ansible-like automation engines.
Ansible is an automation server that manages multiple remote hosts and can deploy applications, install packages, troubleshoot systems remotely, perform network automation, configuration management, and much more, all at once or one by one.
In today’s guide, we’ll elaborate on the steps to install, configure, and automate Linux in minutes. This guide is broadly divided into 2 categories:
Install and Configure Ansible → Practical demonstration of installing and configuring Ansible on Control Node.
Ansible Playbooks | Automate Linux in Minutes → Creating Ansible Playbooks and implementing playbooks on managed nodes.
Install and Configure the Ansible | Control Node and Host Nodes
As already discussed, Ansible is the automation server that has the control node and some managed nodes to manage the overall server. In this section, we’ll demonstrate how you can install and configure Ansible to work properly.
Prerequisites: Understanding the Basics | Control Node, Managed Nodes, Inventory File, Playbook
Before proceeding to the real-time automation, let’s have a look at the list of components that we need to understand before proceeding:
Control Node: The system where Ansible is installed. In this guide, the Ansible Server is set up on OpenSUSE Linux.
Managed Nodes: The servers that are being managed by the Ansible control node.
Inventory/Host File: The inventory file contains a list of host(s) IP(s) that a control node will manage.
Playbook: Playbook is an automated script based on YAML that Ansible utilizes to perform automated tasks on the Managed Nodes.
Let’s now start the initial configuration:
Step 1: Install and Configure Ansible on the Control Node
Let’s set up Ansible on the control node, i.e., installing Ansible on the Control node:
sudo zypper install ansible
The command will automatically select the required essentials (Python and its associated dependencies, especially):
Here are the commands to install Ansible on other Linux distros:
Step 2: Create an Inventory/Hosts File on the Control Node
The inventory file is by default located in the “/etc/ansible/hosts”. However, if it is not available, we can create it manually:
sudonano/etc/ansible/hosts
Here, the [main] is the group representing specific servers. Similarly, we can create multiple groups in the same pattern to access the servers and perform the required operation on the group as a whole.
Step 3: Install and Configure SSH on the Host Nodes
Ansible communicates with the host nodes via SSH. Now, we’ll set up SSH on the host nodes (managed nodes). The process in this “Step” is performed on all the “Managed Nodes”.
Let’s first install SSH on the system:
sudo apt install openssh-server
If you have managed nodes other than Ubuntu/Debian, you can use one of the following commands, as per your Linux distribution, to install SSH:
Since we have only one “Control Node”, for better security, we add a rule that only the SSH port can be accessed from the Control Node:
sudo ufw allow ssh
Note: If you have changed the SSH default port, then you have to mention the port name to open that specific port.
Allow Specific IP on SSH Port: When configuring the Firewall on the Managed Nodes, you can only allow a specific IP to interact with the managed node on SSH. For instance, the command below will only allow the IP “192.168.140.142” to interact over the SSH port.
sudo ufw allow from 192.168.140.142 to any port 22
Let’s reload the firewall:
sudo ufw reload
Confirming the firewall status:
sudo ufw status
Step 4: Create an Ansible User for Remote Connections
Let’s use the “adduser” command to create a new user for Ansible. The Control Node only communicates through the Ansible user:
sudo adduser <username>
Adding it to a sudo group:
sudo usermod -aGsudo<username>
Creating a no-password login for this user only. Open the “/etc/sudoers” file and add the following line at the end of the file:
Step 5: Set up SSH Key | Generate and Copy
Let’s generate the SSH keys on the control node:
ssh-keygen
Now, copy these keys to the remote hosts:
ssh-copy-id username@IP-address/hostname
Note: There are multiple ways of generating and copying the SSH keys. Read our dedicated guide on “How to Set up SSH Keys” to have a detailed overview of how SSH keys work.
Step 6: Test the Connection | Control Node to Host Nodes
Once every step is performed error-free, let’s test the connection from the control node to the managed hosts. There are two ways to test the connection, i.e., one-to-one connection and one-to-many.
The Ansible command below uses its “ping” module to test the connection from the Control Node to one of the hosts, i.e., linuxhint.
ansible linuxhint -mping-u<user-name>
Here, the following Ansible command pings all the hosts that a Control Node has to manage:
ansible all -mping-u ansible_admin
The success status paves the way to proceed further.
Ansible Playbooks | Automate Linux in Minutes
Ansible Playbook is an automated script that runs on the managed nodes (either all or the selected ones). Ansible Playbooks follow the YAML syntax that needs to be followed strictly to avoid any syntax errors. Let’s first have a quick overview of the YAML Syntax:
Prerequisites: Understanding the YAML Basics
YAML is the primary requirement for writing an Ansible playbook. Since it is a markup language thus its syntax must be followed properly to have an error-free playbook and execution. The main components of the YAML that need to be focused on at the moment to get started with Ansible playbooks are:
Indentation → Defines the hierarchy and the overall structure. Only 2 spaces. Don’t use Tab.
Key:Value Pairs → Defines the settings/parameters/states to assist the tasks in the playbook.
Lists → In YAML, a list contains a series of actions to be performed. The list may act as an independent or can assist with any task.
Variables → Just like other scripting/programming languages. The variables in YAML define dynamic values in a playbook for reusability.
Dictionaries → Groups relevant “key:value” pairs under a single key, often for module parameters.
Strings → Represents text values such as task names, messages, and optional quotes. The strings also have the same primary purpose, just like in other scripting/programming languages.
That’s what helps you write Ansible playbooks.
Variable File | To be used in the Ansible Playbook
Here, we will be using a variable file, which is used in the Playbook for variable calling/assignment. The content of the Vars.yml file is as follows:
There are three variables in this file, i.e., the package contains only one package, and the other two variables are “server_packages” and “other_utils,” which contain a group of packages.
Step 1: Create an Ansible Playbook
Let’s create a playbook file:
sudonano/etc/ansible/testplay.yml
Here, the variables file named “vars.yml” is linked to this Playbook. At our first run, we will use the first variable named “package”:
The “hosts: all” states that this playbook will be implemented on all the hosts listed in the hosts/inventory file.
“become: yes” elevates the permissions, i.e., useful when running the commands that require root privileges.
“Vars_file” calls the variable files.
The “tasks” contain the tasks to be implemented in this playbook. There is only one task in this playbook:
The task is named “Install package”, with the “apt” module, and the variable “name” to be used from the variable file.
Step 2: Automate the Tasks
Before implementing this playbook, we can have a dry run of the playbook on all the servers to check for its successful execution. Here’s the command to do so:
Note: We can also provide the hosts/inventory file location (if it is not at the default location, i.e., /etc/ansible/) here as well, i.e., using the “-i” option and providing the path of the inventory file.
Similarly, we can use other variable groups mentioned in the variable file as well.
For instance, the following playbook now calls the “server_packages” variable and installs the server as per their availability:
Here, the “become: yes” is used for the root permissions. This is used when the tasks require root privileges. The task in this playbook utilizes different variables from the variable file.
Let’s dry-run the playbook on the managed nodes using the below command:
All green states that the playbook will be successfully implemented. Remove the “–check” flag from the above command to implement the playbook.
That’s all about the main course of this article. Since Ansible is backed up by a list of commands, we have compiled a list of commands necessary for beginners to understand while using Ansible.
Bonus: Ansible 101 Commands
Ansible is an essential automation server with a long list of its own commands to manage the overall server operations. Here’s the list of Ansible commands that would be useful for all those using Ansible or aiming to use Ansible in the future:
Command(s)
Purpose
ansible -i <inventory/host-file> all -m ping
Test Ansible’s connectivity with all the hosts in the inventory/hosts file.
Executes the playbook with verbose output. Use -vv for more detailed options.
ansible-inventory -i <inventory_file> –list
Displays all hosts/groups in the inventory file to verify the configurations.
Note: If the inventory/hosts file is at the default location (/etc/ansible/), we can skip the “-i” flag used in the above commands.
For a complete demonstration of the Ansible CLI Cheat Sheet, please see the Ansible documentation – Ansible CLI Cheat Sheet.
Conclusion
To get started with Ansible, first, install Ansible on one system (Control Node), then install and configure SSH on the remote hosts (Managed Nodes). Now, generate the SSH keys on the Control Node and copy the key to the Managed Nodes.
Once the connectivity is resolved, configure the inventory file and write the playbook. That’s it. The Ansible will be configured and ready to run.
All these steps are practically demonstrated in this guide. Just go through the guide and let us know if you have any questions or anything that is difficult to understand. We would assist with Ansible’s installation and configuration.
Ever wondered how to make your bash scripts more robust and professional? The declare command in bash is your secret weapon for proper variable management!
Alright! So, variables in bash don't have any types and you can simply use them as name=value . That's not surprising.
What you might not know is that you can control variable types, scope, and behavior by using declare with your variables.
Interesting, right?
What is Declare in Bash?
The declare built-in command in bash allows you to explicitly declare variables with specific attributes. Think of it as giving your variables special properties that control how they behave.
The syntax for declare is simple:
declare [options] [variable[=value]]
If you use it without any options, it will be the same as regular variable assighnment.
# Simple variable assignment
name="John"
# Using declare (equivalent but more explicit)
declare name="John"
The magic happens with the options that define variable attributes, as they open up a world of possibilities! Stay with me to see the incredible power of this lesser known bash command.
Making variables read-only (-r)
Want to create constants that can't be accidentally modified? Use the -r flag:
declare -r var=value
Here's an example:
declare -r API_URL="https://api.example.com"
declare -r MAX_RETRIES=3
# This will fail with an error because API_URL is readonly variable
API_URL="https://malicious.com"
💡
Use read-only variables for configuration values that should never change during script execution!
You can use -g option to create global variables when used inside a shell function.
Use array variables (-a)
You can create indexed arrays explicitly with option -a:
declare -a array_var=("get" "LHB" "Pro" "Membership")
A better example that shows things in action:
declare -a fruits=("apple" "banana" "orange")
declare -a numbers
# Add elements
fruits[3]="grape"
numbers[0]=100
numbers[1]=200
echo ${fruits[@]} # Output: apple banana orange grape
echo ${#fruits[@]} # Output: 4 (array length)
⚠️ Beware of the index gap issues. In the example below, the element was added at 11th position but length of the array is still counted as 2. Basically, bash is not a high-level programming language. So, be careful of the pitfalls.
I hope you are familiar with the concept of associative arrays in bash. Basically, Associative arrays let you create structured data with key-value pairs, offering a more flexible way to handle data compared to indexed arrays.
declare -A user_info
user_info["name"]="Alice"
user_info["age"]=30
user_info["role"]="developer"
echo ${user_info["name"]} # Output: Alice
echo ${!user_info[@]} # Output: name age role (keys)
echo ${user_info[@]} # Output: Alice 30 developer (values)
💡
You can combine options. declare -r -x -i MAX_WORKERS=4 will create a read-only, exported, integer.
Create exported variables (-x)
By default, the variables you create are not available to the child
Make variables available to child processes with option -x.
In the screenshot below, you can see that the exported variable with declare was available in the subshell while the normal variable was not.
This is useful when you have scripts with the variables that need to be available beyond the current shell. An example with pseudocode:
declare -x DATABASE_URL="postgresql://localhost/mydb"
declare -x -r CONFIG_FILE="/etc/myapp.conf" # Read-only AND exported
# Now DATABASE_URL is available to any command you run
python my_script.py # Can access DATABASE_URL
🚧
Child processes won't see the array structure! Arrays can't be exported in bash.
Unsetting attributes
You can also remove specific attributes from variables by using + in the option.
In the example below, I have the variable set as an integer first and then I change it to a string.
declare -i number=42
declare +i number # Remove integer attribute
number="hello" # Now this works (was previously integer-only)
Just letting you know that this option is also there if the situation demands it.
💡
declare -p variable_name will show specific variable attributes declare -p will show all variables in the system with their attributes. Pretty huge output.
When and where should you use declare?
Use declare when you want to:
Create constants with -r
Work with arrays (-a or -A)
Ensure variables are integers (-i)
Make variables available to child processes (-x)
Create more readable, self-documenting code
Stick with simple assignment when:
Creating basic string variables
Working with temporary values
Writing quick one-liners
I can think of some practical, real-world use cases.
Let's say you are creating a script for system monitoring (pseudocode for example):
#!/bin/bash
# System thresholds (read-only)
declare -r -i CPU_THRESHOLD=80
declare -r -i MEMORY_THRESHOLD=85
declare -r -i DISK_THRESHOLD=90
# Current values (will be updated)
declare -i current_cpu
declare -i current_memory
declare -i current_disk
# Arrays for storing historical data
declare -a cpu_history
declare -a memory_history
# Function to check system resources
check_system() {
current_cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
current_memory=$(free | grep Mem | awk '{printf("%.0f", $3/$2 * 100.0)}')
# Add to history
cpu_history+=($current_cpu)
memory_history+=($current_memory)
# Alert if thresholds exceeded
if (( current_cpu > CPU_THRESHOLD )); then
echo "⚠️ CPU usage high: ${current_cpu}%"
fi
if (( current_memory > MEMORY_THRESHOLD )); then
echo "⚠️ Memory usage high: ${current_memory}%"
fi
}
The declare command transforms bash from a simple scripting language into a more robust programming environment. It's not just about creating variables - it's about creating reliable, maintainable scripts that handle data properly.
That being said, there are still a few pitfalls you should avoid. I mentioned a few of them in the article but it is always good to double test your scripts.
Let me know if you have questions on the usage of declare shell built-in.
Bash (Bourne Again Shell) is a free and open-source shell and scripting language. Its journey started in the late 80s, and since then, the Bash has been adopted by routine Linux users and Linux SysAdmins.
Bash has automated the daily tasks of a Linux System Administrator. A Linux SysAdmin has to spend hours running scripts and commands. Not only the SysAdmins, but the simplicity and easy-to-learn capability of Bash have automated the tasks of a normal Linux user as well.
Inspired by this, we have today demonstrated the 10 most useful Bash scripts for Linux SysAdmins. These are chosen based on the general working of any Linux System Administrator (from a small scale to a larger scale).
10 Bash Scripts to Automate Daily Linux SysAdmin Tasks
A System Administrator can create as many scripts as required. We have automated some of the most common and most used tasks through Bash scripts. Let’s go through the prerequisites first and then the Scripts:
Prerequisite 1: Running a Bash Script | To be Done Before Running Each Script in This Post
Before we get into the scripts, let’s quickly go through the process to run a bash script.
Step 1: Make the Script Executable
A bash script is useless until it is made executable. Here, the scripts refer to the Linux sys admin, so we use the “u+x” with “sudo” to make the scripts executable for admin only:
sudochmod u+x /path/to/script
Step 2: Execute the Script
Once the script is executable, it can now be run from the terminal using the command:
sudo/path/to/script
Click here to get more details on running a Bash script.
Prerequisite 2: Package Management Commands for Distros Other Than Debian/Ubuntu
To assist with Script 1, Script 2, and Script 3, we prepared a command cheat sheet for managing the packages on Linux distros other than Debian/Ubuntu and their derivatives. Here’s the table that lists the commands referring to each package manager of the Linux distro:
Package Manager
Update/Upgrade
Install
Remove
pacman (Arch-based)
sudo pacman -Syu
sudo pacman -S <package>
sudo pacman -R <package-name>
zypper (SUSE-based)
sudo zypper update/sudo zypper upgrade
sudo pacman install <package>
sudo zypper remove <package-name>
dnf (Fedora/RHEL-based)
sudo dnf update/sudo dnf upgrade
sudo dnf install <package>
sudo dnf remove <package>
apt (Debian/Ubuntu-based)
sudo apt update/upgrade
sudo apt install <package>
sudo apt remove <package>
Script 1: Update and Upgrade the System Repositories/Packages Index
“Update and upgrade” commands are the most used commands by any Linux SysAdmin or a routine user.
Here, the below script updates, upgrades, and autoremoves packages:
#! /bin/bash
#updating the system repositories
sudo apt update -y
#installing the updated packages from repositories
Permission Denied: Since the script belongs to the SysAdmin, we strictly kept the permissions to the sudo user only:
Here’s the update, upgrade, and autoremoval of packages:
Script 2: Install a Package on Linux
A Linux SysAdmin has to install and remove packages from the systems and keep an eye on this process. Each package installation requires a few commands to effectively install that package.
Update and upgrade package repositories, followed by installing a specific package ($1, specify the package name while running the script):
Here, we choose $1=ssh and run the script:
Script 3: Remove a Package
A complete removal of a package involves multiple commands. Let’s manage it through a single script:
Note: Go through the table (Prerequisites 2) for the commands of other Linux package managers:
#!/bin/bash
#remove the package with only a few dependencies
sudo apt remove $1
#remove package and its data
sudo apt purge $1
#remove unused dependencies
sudo apt autoremove $1
Let’s execute it, i.e.,”$1=ssh”:
sudo ./removepack.sh ssh
Script 4: Monitoring Systems Performance
A Linux sysadmin has to monitor and keep an eye on measurable components (CPU, RAM) of the system. These preferences vary from organization to organization.
Here’s the Bash script that checks the RAM status, Uptime, and CPU/memory stats, which are the primary components to monitor:
#!/bin/bash
echo"RAM Status"
# free: RAM status
free-h
echo"Uptime"
# uptime: how long the system has been running
uptime
echo"CPU/memory stats"
# vmstat: Live CPU/memory stats
vmstat2
free -h: RAM status in human-readable form.
uptime: how long the system has been running.
vmstat 2: Live CPU/memory stats, i.e., records every 2 seconds.
Once we run the command, the output shows the “Ram Status”, the “Uptime”, and the “CPU/Memory” status:
Script 5: Log Monitoring
A Linux SysAdmin has to go through different log files to effectively manage the system. For instance, the “/var/log/auth.log” file contains the user logins/logouts, SSH access, sudo commands, and other authentication mechanisms.
Here’s the Bash script that allows filtering these logs based on a search result.
#!/bin/bash
cat/var/log/auth.log |grep$1
The $1 positional parameter shows that this script would be run with one argument:
We use “UID=0” as the variable’s value for this script. Thus, only those records are shown that contain UID=0:
The log file can be changed in the script as per the requirement of the SysAdmin. Here are the log files associated with different types of logs in Linux:
Log File/Address
Purpose/Description
/var/log/
The main directory where most of the log files are placed.
/var/log//logapache2
Refers to the Apache server logs (access and error logs).
/var/log/dmesg
Messages relevant to the device drivers
/var/log/kern.log
Logs/messages related to the Kernel.
/var/log/syslog
These are general system logs and messages from different system services that are available here
There are a few more. Let’s open the “/var/log” directory and look at the logs that SysAdmin can use for fetching details inside each log file:
Script 6: User Management | Adding a New User, Adding a User to a Group
Adding a new user is one of the key activities in a Linux sysadmin’s daily tasks. There are numerous ways to add a new user with a Bash script. We have created the following Bash Script that demonstrates the user creation:
#!/bin/bash
USER=$1
GROUP=$2
#Creating a group
sudo groupadd $GROUP
#Creating a User
sudo adduser $USER
#Adding a user to a group
sudo usermod -aG$GROUP$USER
2 positional parameters are initialized, i.e., $1 for user and $2 for group.
First, the required group is created. Then, the user is created. Lastly, the newly created user is added to the group.
Since the script has positional parameters, let’s execute it with the required 2 arguments (one for username and the other for groupname):
Similarly, the system administrator can create scripts to delete users as well.
Script 7: Disk Management
Disk management involves multiple commands, such as listing and checking the number of block devices. We run the “lsblk” command. To “mount” or “unmount” any filesystem, we run the “mount” and “umount” commands.
Let’s incorporate a few commands in a Bash script to view some data about disks:
#!/bin/bash
#Disk space check
df-h
#Disk usage of a specific directory
echo"Disk Usage of:"$1
du-sh$1
$1 positional parameter refers to the address of the directory whose disk usage is to be checked:
Let’s run the script:
sudo ./dfdu.sh /home/adnan/Downloads
Also, remember to provide the argument value, i.e., here, the “$1=/home/adnan/Downloads”:
Script 8: Service Management
To manage any service, the SysAdmin has to run multiple commands for each service. Like, to start a service, the SysAdmin uses the “systemctl start” command and verifies its status through “systemctl status”. Let’s make this task easy for Linux SysAdmins:
Start a Service
The following Bash script refers to only one service, i.e., every time the script only manages the NGINX service:
#!/bin/bash
sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginx
For a more diverse use case, we declare a positional parameter to manage different services with each new run:
Now, pass the value of the positional parameter at the time of executing the script:
sudo ./sys.sh apache2
The “apache2” is the argument on which the script would run:
Stop a Service
In this case, we use the positional parameter to make it more convenient for the Linux SysAdmins or the regular users:
#!/bin/bash
sudo systemctl stop $1
sudo systemctl disable $1
sudo systemctl status $1
The $1 positional parameter refers to the specific service that is mentioned when executing a command:
Let’s execute the command:
sudo ./sys1.sh apache2
Script 9: Process Management
A Linux System Administrator has a keen eye on the processes and manages each category of process as per the requirement. A simple script can kill the specific processes. For instance, the script demonstrated here fetches the Zombie and Defunct processes, identifies the parent IDs of these processes:
#!/bin/bash
#Fetching the process ids of Zombie processes and defunct processes
ZOM=`ps aux |grep'Z'|awk'{print $2}'|grep[0-9]`
DEF=`ps aux |grep'Z'|awk'{print $2}'|grep[0-9]`
echo"Zombie and Defunct Process IDs are:"$ZOM"and"$DEF
#Getting parent process ids of Zombies and defunct
PPID1=`ps-oppid= $ZOM`
PPID2=`ps-oppid= $DEF`
echo"ZParent process IDs of Zombie and Defunct Processes are:"$PPID"and"$PPID2.
Zombie and Defunct process IDs are fetched and stored in a variable.
The parent process IDs of the Zombie and defunct processes are fetched.
Then, the parent processes can be killed
Let’s execute it:
sudo ./process.sh
Script 10: Allow or Deny Services Over the Firewall
A firewall is a virtual wall between your system and the systems connecting to your system. We can set the firewall rules to allow or deny what we want. Firewall has a significant role in managing the system. Let’s automate to allow or deny any service on your system:
Allow a Service Through the Firewall
The following script enables SSH through the firewall:
#!/bin/bash
sudo ufw allow ssh
sudo ufw enable
sudo ufw status
Let’s execute the script.
We can also include a positional parameter here to use the same script for multiple services to be allowed on the firewall. For instance, the script below has only one positional parameter. This parameter’s value is to be provided at the time of executing the script.
#!/bin/bash
sudo ufw allow $1
sudo ufw enable
sudo ufw status
While executing, just specify the name of the service as an argument:
sudo ./firewall.sh ssh
Deny a Service or Deny All:
We can either deny one service or deny all the services attempting to reach our system. The below script updates the default incoming policy to deny, disables the firewall as well.
Note: These kinds of denial scripts are run when the overall system is in trouble, and we just need to make sure there is no service trying to approach our system.
#!/bin/bash
sudo ufw default deny incoming
sudo ufw disable
sudo ufw status
sudo ufw default allow outgoing
Running the script:
Now that you have learned the 10 Bash scripts to automate daily SysAdmin tasks.
Let’s learn how we can schedule the scripts to run them as per our schedule.
Bonus: Automating the Scripts Using Cron
A cron job allows the SysAdmin to execute a specific script at a specific time, i.e., scheduling the execution of the script. It is managed through the crontab file.
First, use the “crontab -e” command to enter the edit mode of the crontab file:
crontab -e
To put a command on a schedule with the cron file, you have to use a specific syntax to put it in the cron file. The below script will run on the 1st minute of each hour.
There are a total of 5 parameters to be considered for each of the commands:
m: minute of a specific hour, i.e., choose between 1-59 minutes.
h: hour of the day, i.e., choose between 0-23.
dom: date of the month → Choose between 1-31.
mon: foreach month → Choose between 1- 12
dow: day of the week → Choose between 1-7
You can check the crontab listings using:
crontab -l
Important: Do you want a Linux Commands Cheat Sheet before you start using Bash? Click here to get a detailed commands cheat sheet.
Conclusion
Bash has eased the way of commanding in Linux. Usually, we can run a single command each session on a terminal. With Bash scripts, we can automate the command/s execution process to accomplish tasks with the least human involvement. We have to write it once and then keep on repeating the same for multiple tasks.
With this post, we have demonstrated the 10 Bash scripts to automate daily Linux System Administrator.
FAQs
How to run a Bash script as an Admin?
Use the “sudo /path/to/script” command to run the Bash script as an Admin. It is recommended to restrict the executable permissions of the Bash scripts to only authorized persons.
What is #!/bin/bash in Bash?
The “#!/bin/bash” is the Bash shebang. It tells the system to use the “bash” interpreter to run this script. If we don’t use this, our script is a simple shell script.
How do I give permissions to run a Bash script?
The “chmod” command is used to give permissions to run the Bash script. For a Linux sysadmin script, use the “sudo chmod u+x /path/of/script” command to give permissions.
What does $1 mean in Bash?
In Bash, $1 is a positional parameter. While running a Bash script, the first argument refers to $1, the second argument refers to $2, and so on.
Basic Workflow of Ansible | What components are necessary
sudo apt update
sudo apt install ansible
ansible --version
Ansible Control Node IP: 192.168.140.139 (Where Ansible is configured)
Ansible Host IPs: {
Server 1 [172.17.33.7]
Server2 [192.168.18.140]
}
Inventory File:
Default inventory file location: /etc/ansible/hosts. Usually, it is not available when we install Ansible from the default repositories of the distro, so we need to create it anywhere in the filesystem. If we create it in the default location, then no need to direct Ansible to the location of the file.
However, when we create the inventory file other than the default, we need to tell Ansible about the location of the inventory file.
Inventory listing (Verifying the Inventory Listing):
ansible-inventory --list-y
SSH (as it is the primary connection medium of Ansible with its hosts):
sudo apt installssh
Allow port 22 through the firewall on the client side:
sudo ufw allow 22
Let’s check the status of the firewall:
sudo ufw status
Step 2: Establish a No-Password Login on a Specific Username | At the Host End
Create a new dedicated user for the Ansible operations:
sudo adduser username
Adding the Ansible user to the sudo group:
sudo usermod -aGsudo ansible_root
Add the user to the sudo group (open the sudoers file):
sudonano/etc/sudoers
SSH Connection (From Ansible Control Node to one Ansible Host):
ssh username@host-ip-address
ansible all -mping-u ansible_root
SSH key generation and copying the public key to the remote host:
ssh-keygen
Note: Copy the public key to the user that you will be using to control the hosts on various machines.
ssh-copy-id username@host-ip-address
Test All the Servers Listed in the Inventory File:
Testing the Ansible Connection to the Ansible host (remember to use the username who is trusted at the host or has a passwordless login). I have the user “adnan” as the trusted user in the Ansible user list.
ansible all -mping-u username
Same with the different username configured on the host side:
We can ping a specific group, i.e., in our case, we have a group named [servers] in the inventory.
Another week, another chance to pretend you're fixing something important by typing furiously in the terminal. You do that, right? Or is it just me? 😉
This week's highlights are:
lsattr, chatter and grep commands
brace expansions
VERT converter
And your regular dose of news, memes and tips
❇️ Explore DigitalOcean with $100 free credit
DigitalOcean is my favorite alternative to the likes of AWS and Azure and Google Cloud. I use it to host Linux Handbook and pretty happy with their performance and ease of deployment. Try their servers and marketplace apps for free with $100 credit which is applicable to new accounts.
Note-taking has come a long way from crumpled sticky notes and scattered .txt files. Today, we want our notes to be searchable, linked, visualized, and ideally, available anywhere. That’s where Obsidian shines.
Built around plain-text Markdown files, Obsidian offers local-first knowledge management with powerful graph views, backlinks, and a thriving plugin ecosystem.
For many, it has become the go-to app for personal knowledge bases and second brains.
While Obsidian does offer Obsidian Sync, a proprietary syncing service that lets you keep your notes consistent across devices, it’s behind a paywall.
That’s fair for the convenience, but I wanted something different:
A central Obsidian server, running in my homelab, accessible via browser, no desktop clients, no mobile apps, just one self-hosted solution available from anywhere I go.
And yes, that’s entirely possible.
Thanks to LinuxServer.io, who maintain some of the most stable and well-documented Docker images out there, setting this up was a breeze.
I’ve been using their containers for various services in my homelab, and they’ve always been rock solid.
Let me walk you through how I deployed Obsidian this way.
If you prefer keeping your self-hosted apps neatly organized (like I do), it's a good idea to create separate folders for each container.
This not only helps with manageability, but also makes it easier to back up or migrate later.
1. Create a data directory for Obsidian
Let’s start by creating a folder for Obsidian data:
mkdir -p ~/docker/obsidian
cd ~/docker/obsidian
You can name it whatever you like, but I’m sticking with obsidian to keep things clear.
2. Create a docker-compose.yml File
Now, we’ll set up a Docker Compose file, this is the file that tells Docker how to run Obsidian, what image to use, which ports to open, and other important stuff.
You don’t need to write the whole thing from scratch. I’m using the official example from the LinuxServer.io image page, but with a few changes tailored to my system.
Just copy the following into a new file named docker-compose.yml:
image: We're using the latest Obsidian image provided by LinuxServer.io.
volumes: Maps a config folder in your current directory to Obsidian’s internal config directory, this is where all your Obsidian data and settings will live.
ports: The app will be available on port 3000 of your machine. You can change this if you prefer a different port.
shm_size: Allocates shared memory; useful for apps with a UI like Obsidian.
environment: This is where you set up your user, password, timezone, and file ownership.
Make sure you replace the following placeholders with your own values:
yourusername: The username you'll use to log in to Obsidian.
yourpassword: Choose a strong password.
TZ: Use your local timezone. (Example: Asia/Kolkata)
PUID and PGID: These should match your user’s UID and GID on the host system. To find them, run:
Once the docker-compose.yml file is ready and the values are customized, go ahead and start the container:
docker-compose up -d
This command tells Docker to:
Pull the Obsidian image (if it’s not already downloaded)
Create a container using the settings we defined
Run it in detached mode (-d), so it continues running in the background
Give it a minute or two, the first time you run this, Docker needs to download the entire image and set everything up. After that, it’ll be much faster on subsequent restarts.
Accessing Obsidian in your browser
Once it's done, you should be able to open Obsidian in your browser at:
http://localhost:3000
Or replace localhost with your server's IP if you’re not running it locally.
💡
Optional: If you plan to access this instance from outside your local network, we strongly recommend putting it behind a reverse proxy like Caddy or NGINX with HTTPS and authentication. You can even pair it with a secure tunneling solution (like Cloudflare Tunnel or Tailscale Funnel) if you're behind CGNAT.
Log in using the CUSTOM_USER and PASSWORD you set earlier.
Once inside, it will look like this:
Here you can:
Create a new vault.
Open an existing vault in the config volume.
Explore the graph view, plugins, and everything else, right from the browser.
Creating new vault
For this tutorial, we’ll keep things simple, I’m just going to create a new vault to get started.
Click on "Create", give your vault a name (anything you like - "secondbrain", "mynotes", "vault", etc.), and Obsidian will take care of the rest.
It’ll create a new folder inside the mounted config directory we set up in Docker earlier. This means all your notes and settings will be saved persistently on your machine, even if the container is stopped or restarted.
After you name and create the vault, Obsidian will drop you straight into the note-taking interface. And that’s it, you’re in!
You can now start writing notes, creating folders, and playing around with features like:
Graph view to visualize links between notes
Command palette to quickly access features
Themes and plugin settings to customize your environment
Everything is accessible from the left sidebar, just like in the desktop app. No extra setup needed, just start typing and let your ideas flow.
Final thoughts
Setting up Obsidian inside Docker was surprisingly easy, it didn’t take much time, and before I knew it, I had the full desktop-like experience running in my browser.
This setup is especially great for people on the go or students like me who love using Obsidian but can’t always afford the Sync feature just yet.
Now, I personally don’t mind paying for good software and I think Obsidian Sync is a solid service but those little costs start stacking up fast.
I’ve also seen quite a few Reddit threads where folks have built their own syncing setups using Syncthing to keep notes in sync across devices, and that seems like a solid workaround as well.
For me, this self-hosted browser version of Obsidian fits somewhere in the middle. It gives you the full experience without the limitations of a mobile app or the need to sync through someone else’s servers.
And if you're already in the self-hosting ecosystem, it’s just another powerful tool you can add to your stack.
Lesser known... that's the theme of this week's newsletter. Hope you like it 😄
Here are the highlights of this edition :
Lesser known mouse mode in Vim
Lesser known dir command in Linux
Lesser known special file permissions
And your regular dose of better known memes, tips and news ;)
🚀 Level up your coding skills and build your own bots
Harness the power of machine learning to create digital agents and more with hot courses like Learning LangChain, The Developer's Playbook for Large Language Model Security, Designing Large Language Model Applications, and more.
Part of the purchase goes to Code for America! Check out the ebook bundle here.
Think of Vim tabs like browser tabs for your code editor - each tab holds one or more windows, letting you organize multiple files into logical workspaces.
Unlike window splits that divide your screen, tabs stack contexts you can flip between instantly.
Three files opened in separate tabs in Vim
Let's see how you can use tabs in Vim.
Essential Vim tab commands at a glance
Here are the most common actions you can use while dealing with tabs in Vim.
Command
Action
Memory Hook
vim -p file1 file2
Opens files in tabs
Vim in pages
:tabnew filename
Open file in new tab
Tab new
:tabedit filename
Open file for editing in new tab
Tab edit
gt
Next tab
Go to next
gT
Previous tab
Go to previous
{n}gt
Jump to tab number n
Go to specific
:tabfirst
Jump to first tab
Self-explanatory
:tabclast
Jump to last tab
Self-explanatory
:tabclose
Close current tab
Self-explanatory
:tabonly
Close all other tabs
Keep only this
:tabs
List all tabs
Show tabs
Interesting, right? Let's see it in details.
Opening files in tabs in Vim
Let's start by opening files in tabs first.
Start Vim with multiple files opened in tabs
Launch Vim with multiple tabs instantly:
vim -p file1.py file2.py file3.py
0:00
/0:13
Open two existing files in tabs while starting Vim
How can you open just one file in tab? Well... if it's one file, what's the point of tab, right?
📋
Vim tabs aren't file containers - they're viewport organizers. Each tab can hold multiple split windows, making tabs perfect for grouping related files by project, feature, or context. It's like having separate desks for different projects.
Open a file in a new tab in the current Vim session
When you are already inside Vim and want to open a file in a new tab, switch to normal mode by pressing Esc key and use the command:
:tabnew filename
This will load the file in a new tab. If the file doesn't exist, it will create a new one.
Filename is optional. If you don't provide it, it will open a new file without any name:
:tabnew
0:00
/0:11
Opening existing or new files in tabs from existing Vim session
💡
If you use tabedit instead of tabnew, it open the file in Edit mode (insert mode) in the new tab.
Search for files and open them in tabs
Search the current directory for filename matching the given pattern and open it in a new tab:
:tabf filename*
This only works if the search results into a single file. If there are more than one file matched, it will throw an error:
E77: Too many file names
💡
While you can open as many tabs as you want, only 10 tabs are shown by default. You can change this by setting tabpagemax in your vimrc to something like set tabpagemax=12
Navigating between tabs
You can move between opened tabs using:
:tabn: for next tab
:tabp: for previous tab
Typing the commands could be tedious, so you can use the following key combinations in the nomral mode:
gt: To go to the next tab
gT (i.e. press g and shift and t keys together) To go to the previous tab
If there are too many tabs opened, you can use:
:tabfirst: Jump to first tab
:tablast: Jump to last tab
💡
You can enable mouse mode in Vim and that makes navigating between tabs easier with mouse clicks.
In many distributions these days, Vim is preconfigured to show the tab labels on the top. If that's not the case, add this to your vimrc:
set showtabline=2
You can list all the opened tabs with:
:tabs
💡
If you are particular about putting the opened tabs in specific order, you can move the current tab to Nth position with :tabm N. This tabm is short for tabmove. Note that Vim starts numbering at 0.
Closing tabs
How do you close a tab? If the tab has a single filed opened, the regular save/exit Vim commands work.
But it will be an issue if you have multiple split windows opened in a tab.
:tabclose: Close current tab
:tabonly: Only keep the current tab opened, close all others
0:00
/0:14
Tab closing operation in Vim
💡
Accidentally closed a tab? :tabnew | u creates a new tab and undoes in one motion - your file returns.
Bulk tab operations
With :tabdo command, you can run the same operations in all the tabs.
For example, :tabdo w will save file changes in all tabs, :tabdo normal! gg=G auto-indents every file.
Similarly, tabdo %s/oldvar/newvar/g executes search-replace across every tab simultaneously. Parallel processing for repetitive changes.
You get the idea. tabdo is the key here.
💡
You can save your meticulously crafted tab layout. :mksession project.vim then vim -S project.vim restores exact tab layout.
Conclusion
While it is good to enjoy tabs in Vim, don't create dozens of them - they become harder to navigate than helpful. Use buffers for file switching and tabs for context switching.
As you can see, with the tab feature, you get one inch closer to having the IDE like experience in Vim.