← Back to Blogs

How I Learned Bash Scripting Faster with GitHub Copilot

A practical, step-by-step learning journey using Bash, Vagrant virtual machines, and AI-assisted scripting.

Instead of only reading commands and syntax, I used virtual machines, real scripts, and GitHub Copilot to understand how Bash is actually used in a DevOps-style workflow. What made this learning experience useful was not just writing scripts, but improving them, testing them, and understanding why certain Bash practices matter.

In this blog, I want to walk through what I learned step by step, from basic scripts to better practices like using functions, handling variables safely, validating files, using arrays, and even deploying scripts to multiple remote machines.

Why This Learning Style Helped Me

Bash can feel simple at first, but once you start automating real setup tasks, you quickly realize that structure and safety matter a lot. A script that works once is not enough. It should also be readable, reusable, and safe to run on real machines.

My biggest takeaway: AI can help generate code quickly, but the real value comes from reviewing, understanding, and testing everything yourself.

Step 1: Start with a Basic Script

The first scripts were simple system information scripts. They included commands to print uptime, memory usage, and disk usage. This was useful because it showed how Bash scripts are just a sequence of shell commands written in a reusable format.

#!/bin/bash

echo "System Information"
uptime
free -m
df -h

Even from a simple script like this, GitHub Copilot started suggesting additional sections such as CPU usage, network information, and running processes. That was the first time I saw how AI can speed up scripting when the structure is already clear.

Step 2: Use a Shared Folder with Virtual Machines

I used Vagrant virtual machines for testing. Instead of manually copying scripts every time, I placed them in the synced folder. Inside the VM, the scripts became available through the /vagrant directory.

cd /vagrant
ls

This made the workflow much easier. I could edit the script in VS Code, save it, and then test it directly from the VM. It felt much closer to a real automation workflow than just writing shell commands in one local terminal.

Step 3: Improve the Script with Copilot

One of the most useful features was selecting the entire script and asking Copilot to improve it according to development best practices. This was not only about getting a better script, but also about learning what a better script looks like.

A common improvement it suggested was this line:

set -euo pipefail

At first this looked small, but it is actually very important in Bash scripting.

  • -e makes the script exit immediately if a command fails.
  • -u stops the script when an undefined variable is used.
  • pipefail makes a pipeline fail if any command inside it fails.

This taught me that production-style scripts should fail early instead of silently continuing in a broken state.

Step 4: Learn Functions to Make Scripts Cleaner

Another big improvement was using functions. Instead of repeating the same echo lines and setup blocks, I learned to group reusable tasks into functions.

log() {
  echo "########################"
  echo "$1"
  echo "########################"
}

install_dependencies() {
  log "Installing packages"
  yum install -y httpd wget unzip
}

With functions, the script became much easier to read. Each part had a clear purpose, and the main execution section simply called those functions in order.

main() {
  install_dependencies
  setup_webfiles
  start_service
}

main

This was one of the most useful structural lessons for me. Instead of writing one long script from top to bottom, I learned to think in blocks of responsibility.

Step 5: Handle Variables Safely

I also learned that variable usage in Bash can be risky if you are not careful. For example, writing this is not ideal:

yum install $PACKAGE

A better version is:

yum install "$PACKAGE"

The double quotes help prevent word splitting and globbing issues. This may seem minor, but it can prevent weird bugs, especially when variables come from user input or contain spaces.

Step 6: Validate Directories and Files Before Running Commands

One great lesson was that scripts should not assume everything is valid. For example, a command like this can be dangerous:

cd $TEMP_DIR

If the variable is empty or the path is wrong, the script may continue running in the wrong location. The safer approach is:

cd "$TEMP_DIR" || exit 1

This means the script will stop immediately if it cannot change to the expected directory.

The same idea applies to checking files before using them:

if [ ! -f "$HOSTS_FILE" ]; then
  echo "Hosts file not found"
  exit 1
fi

Step 7: Use Modern Bash Syntax

Some older Bash syntax still works, but there are cleaner and more recommended ways to write things today. A good example is command substitution.

Older style:

DATE=`date`

Recommended style:

DATE=$(date)

The newer syntax is easier to read and better when commands become more complex.

Step 8: Handle User Input More Carefully

When reading user input, I learned that this is not the safest form:

read name

A better version is:

read -r name

Using -r helps prevent backslashes from being interpreted in unexpected ways. This is one of those small improvements that makes input handling safer.

Step 9: Move from Single-Machine Scripts to Multi-Host Automation

One of the most interesting parts of this learning journey was working with a remote setup script that could deploy changes to multiple machines. This was much closer to real DevOps work.

The script first read a list of hosts from a file and stored them in an array:

mapfile -t hosts < hosts.txt

Then it looped over each host:

for host in "${hosts[@]}"; do
  echo "Working on $host"
done

This was especially useful because I learned why arrays and loops matter in automation. Instead of repeating the same steps manually, the script can apply them server by server.

Step 10: Copy and Execute Scripts on Remote Machines

The remote deployment workflow used tools like scp and ssh. The basic idea was simple:

scp setup.sh user@$host:/tmp/
ssh user@$host "bash /tmp/setup.sh"

This means the automation script can copy another script to a remote machine and then run it there. That felt like an important shift from basic Bash practice to actual environment setup automation.

Step 11: Add OS-Based Logic

Another useful lesson was that not all Linux systems are the same. Package names and package managers may differ between Ubuntu and RPM-based systems.

if grep -qi ubuntu /etc/os-release; then
  PACKAGE="apache2"
else
  PACKAGE="httpd"
fi

This kind of operating system check makes scripts more flexible and reusable across environments.

Step 12: Use AI to Scaffold Bigger Projects, But Think Critically

One of the most impressive parts of the learning process was asking Copilot to generate a complete Tomcat setup project. The request included two setup scripts, one for Ubuntu and one for RPM-based systems, plus a deployment script that reads hosts from a file.

AI was able to generate a full project structure quickly, including a README and deployment flow. But the biggest lesson was not that AI can generate code. The real lesson was that I still needed to review it, question it, and test it in my VMs before trusting it.

Important reminder: Never blindly apply AI suggestions. Review the code, test it in a safe environment, and make sure it does exactly what you expect.

What I Learned Overall

This experience helped me understand Bash scripting in a much more practical way. I did not just learn commands. I learned how to make scripts safer, more structured, and more reusable. I also learned that GitHub Copilot is most useful when I already understand the problem I am trying to solve.

In short, these were my biggest takeaways:

  • Use Bash for real automation tasks, not just command practice.
  • Structure scripts with functions and a clear main execution flow.
  • Quote variables and validate files or directories before acting.
  • Use safer Bash practices like set -euo pipefail and read -r.
  • Use arrays and loops when working with multiple hosts.
  • Let AI help, but always verify and test everything yourself.

Final Thoughts

This was more than a Bash lesson for me. It was a lesson in how modern engineers work: using tools like AI to move faster, while still relying on their own judgment, testing, and debugging skills.

As I continue building my DevOps and backend skills, this kind of practical learning is exactly what I want more of: write, improve, test, break, fix, and understand.