Skip to main content

Command Palette

Search for a command to run...

From Zero to DevOps: Git, Shell Scripting & Bash Fundamentals You Must Know

Updated
11 min read
From Zero to DevOps: Git, Shell Scripting & Bash Fundamentals You Must Know

A structured deep-dive into version control systems, shell scripting concepts, variables, conditions, functions, and redirections — built for DevOps engineers starting their journey.


Table of Contents

  1. Why You Shouldn't Store Code Without VCS

  2. Centralized vs Distributed Version Control

  3. Git: The Distributed VCS

  4. Shell Scripting Fundamentals

  5. Variables and Data Types

  6. Special Shell Variables

  7. Exit Codes

  8. Conditions in Shell

  9. Functions

  10. Redirections


Why You Shouldn't Store Code Without VCS {#why-not-without-vcs}

Storing code without a Version Control System (VCS) is a recipe for disaster in any team or production environment. Here's what you lose:

What You Lose Impact
Backup & Security No recovery if your local machine crashes or files get deleted
Version History Can't roll back to a working state after a bad change
Collaboration Multiple developers overwriting each other's work
Change Tracking No audit trail of who changed what and why
Branching Every "branch" becomes a separate physical copy of the codebase — chaotic and unmanageable

Bottom line: Without VCS, you're one mistake away from losing everything.


Centralized vs Distributed Version Control {#centralized-vs-distributed}

Centralized VCS — SVN (Subversion)

In a centralized model, a single central server holds the entire history of the project — all commits, branches, and tags.

Developer A ──┐
Developer B ──┼──► Central SVN Server (Single Source of Truth)
Developer C ──┘

Drawbacks:

  • Single point of failure — if the central server goes down, no one can commit, branch, or retrieve old versions

  • No offline work — you need network access for almost every operation

  • Entire history lives on one machine — disaster recovery is risky


Distributed VCS — Git

In a distributed model, every developer has a full copy of the repository — including complete history, all branches, and all commits.

Developer A (full repo) ──┐
Developer B (full repo) ──┼──► Remote GitHub/GitLab/Bitbucket
Developer C (full repo) ──┘

Advantages:

  • No single point of failure — if the central server crashes, any developer's laptop can restore the entire repo

  • Work offline — commit, branch, and view history without a network connection

  • Faster operations — most operations are local

Git is the concept. GitHub, GitLab, and Bitbucket are companies that built platforms on top of Git with their own UIs. The underlying Git commands and features remain the same across all of them.


Git: The Distributed VCS {#git-the-distributed-vcs}

Core Git Workflow

Every change you make travels through three stages before reaching the remote repository:

Working Directory  ──►  Staging Area  ──►  Local Repo  ──►  Remote Repo
   (edit files)         (git add)        (git commit)      (git push)

Essential Git Commands

# Clone a remote repository to your local machine
git clone https://github.com/daws-90s/shell-practice.git

# Stage a specific file
git add <file-name>

# Stage ALL changed files
git add .

# Commit staged files to your local repo
git commit -m "meaningful commit message"

# Push committed changes to the remote main branch
git push origin main

The main Branch

  • main (or master) always points to production-ready code

  • Never push broken or untested code directly to main

  • Use feature branches for development, merge into main only when stable

Chaining Commands (Quick Push)

git add . ; git commit -m "hello world script created" ; git push origin main

Pro Tip: Spend 90% of your time thinking through the logic, and 10% actually writing code. Good thinking = fewer bugs.


Shell Scripting Fundamentals {#shell-scripting-fundamentals}

What is a Shell Script?

A shell script (also called a bash script) is a file containing a sequence of shell commands that are executed in order. It automates repetitive tasks on Linux/Unix systems.

The Shebang Line

#!/bin/bash
  • Must be the very first line of every shell script

  • Tells the OS which interpreter to use to execute the script (/bin/bash in this case)

  • Without it, the OS may not know how to execute the file

Thinking Before Coding — Pseudocode

Before writing a single line of bash, write your logic in plain English. This is called pseudocode.

Example — Check if today is a holiday:

today = thursday

if (today != sunday) {
    print "go to school"
} else {
    print "take holiday, enjoy"
}

Example — Check if a number is even or odd:

number = 11
reminder = 11 % 2     # remainder = 1

if (reminder == 0) {
    print "number is even"
} else {
    print "number is odd"
}

Think generally. Don't think about syntax first. Think about what needs to happen, then translate it to code.


Variables and Data Types {#variables-and-data-types}

Variable Assignment

VAR_NAME=VALUE    # No spaces around the = sign

Important: Shell does not allow spaces around =. VAR_NAME = VALUE will throw an error.

DRY Principle — Don't Repeat Yourself

Store repeated values in variables. Change in one place, reflects everywhere.

# Bad — hardcoded everywhere
echo "Deploying to prod-server-01"
scp app.tar prod-server-01:/opt/

# Good — use a variable
SERVER="prod-server-01"
echo "Deploying to $SERVER"
scp app.tar $SERVER:/opt/

Ways to Assign Variables

# 1. Direct assignment
NAME="DevOps"

# 2. From command-line arguments
# sh script.sh Trump Iran
# \(1 = Trump, \)2 = Iran

# 3. Interactive input from user
read VAR_NAME

# 4. Capture output of a command
TIMESTAMP=$(date)
VAR_NAME=$(command)

Data Types in Shell

Shell treats everything as a string by default. This is unlike strongly-typed languages (Java, C++) where you must declare the type.

# Shell — no type declaration needed
NAME="siva123"
NUMBER=42

# Compare with Java
# String name = "siva123";
# int number = 42;

Real-world lesson: The Ariane 4 rocket used integer speed (9 km/s). The Ariane 5 rocket went faster (8.2 km/s as a float). When Ariane 5's float was stored in an integer variable, it caused an overflow, leading to the rocket's self-destruction 37 seconds after launch. Data types matter.


Special Shell Variables {#special-shell-variables}

Shell automatically creates and populates several variables that are extremely useful in scripting:

echo "All arguments passed: $@"
echo "Number of arguments: $#"
echo "First argument: $1"
echo "Script name: $0"
echo "Current user: $USER"
echo "Current directory: $PWD"
echo "Home directory: $HOME"
echo "PID of current script: $$"

# For background processes
sleep 5 &
echo "PID of last background process: $!"
wait $!

echo "Current line number: $LINENO"
echo "Script ran for: $SECONDS seconds"
echo "Random number: $RANDOM"
Variable Description
\(1, \)2, ... Positional arguments passed to the script
$@ All arguments as separate words
$# Count of arguments passed
$0 Name of the script itself
$$ PID of the current script
$! PID of the last background process
$USER Currently logged-in user
$PWD Present working directory
$HOME Home directory of the current user
$LINENO Current line number in the script
$SECONDS Seconds elapsed since script started
$RANDOM A random number between 0–32767

Exit Codes {#exit-codes}

Every command in Linux returns an exit code when it finishes. This tells you whether it succeeded or failed.

$?    # Holds the exit code of the last executed command
Exit Code Meaning
0 Success — command ran without errors
1–127 Failure — various error conditions
# Example usage
apt install mysql-server
if [ $? -eq 0 ]; then
    echo "MySQL installed successfully"
else
    echo "Installation failed"
fi

Best practice: Always check $? after critical commands in your scripts. Never assume a command succeeded.


Conditions in Shell {#conditions-in-shell}

Basic Syntax

# Simple if
if [ expression ]; then
    # commands if true
fi

# if-else
if [ expression ]; then
    # commands if true
else
    # commands if false
fi

# if-elif-else
if [ expression1 ]; then
    # commands
elif [ expression2 ]; then
    # commands
elif [ expression3 ]; then
    # commands
else
    # commands if none matched
fi

Comparison Operators

# Numeric comparisons
-eq    # equal to
-ne    # not equal to
-gt    # greater than
-lt    # less than
-ge    # greater than or equal to
-le    # less than or equal to

# String comparisons
==     # equal
!=     # not equal
-z     # string is empty
-n     # string is not empty

# File checks
-f     # is a regular file
-d     # is a directory
-e     # file/directory exists

Example — Number Comparison

NUMBER=20
if [ $NUMBER -gt 10 ]; then
    echo "Number $NUMBER is greater than 10"
else
    echo "Number $NUMBER is less than 10"
fi

Real-World Example — MySQL Install Script

#!/bin/bash

# 1. Check if running as root
if [ "$USER" != "root" ]; then
    echo "ERROR: This script must be run as root. Exiting."
    exit 1
fi

# 2. Check if MySQL is already installed
if command -v mysql &>/dev/null; then
    echo "MySQL is already installed. Skipping."
    exit 0
fi

# 3. Install MySQL
echo "Installing MySQL..."
apt install mysql-server -y

# 4. Check if installation succeeded
if [ $? -eq 0 ]; then
    echo "SUCCESS: MySQL installed successfully."
else
    echo "FAILURE: MySQL installation failed."
    exit 1
fi

Functions {#functions}

Functions let you package reusable logic, avoid code duplication, and keep your scripts clean.

Syntax

FUNC_NAME() {
    # \(1, \)2 are arguments passed to the function
    echo "First arg: $1"
    echo "Second arg: $2"
}

# Call the function
FUNC_NAME arg-1 arg-2

Benefits of Functions

  • Less code — write once, use many times (DRY principle)

  • Less time — modify logic in one place

  • Fewer bugs — fewer places for errors to hide

Example — Reusable Logger Function

#!/bin/bash

log() {
    local LEVEL=$1
    local MESSAGE=$2
    echo "[\((date '+%Y-%m-%d %H:%M:%S')] [\)LEVEL] $MESSAGE"
}

install_package() {
    local PACKAGE=$1
    log "INFO" "Installing $PACKAGE..."
    apt install "$PACKAGE" -y
    if [ $? -eq 0 ]; then
        log "SUCCESS" "$PACKAGE installed."
    else
        log "ERROR" "$PACKAGE installation failed."
    fi
}

install_package mysql-server
install_package nginx

Redirections {#redirections}

Shell redirections let you control where a command's input comes from and where its output goes.

Redirection Operators

<     # Redirect INPUT to a command
>     # Redirect OUTPUT (overwrites file)
>>    # Redirect OUTPUT (appends to file)

File Descriptors

Descriptor Meaning
0 Standard Input (stdin)
1 Standard Output (stdout) — success
2 Standard Error (stderr) — failure
& Both stdout and stderr

Common Examples

# Redirect only success output to a file
ls -l 1> output.log

# Redirect only error output to a file
ls /nonexistent 2> error.log

# Redirect both success and error to the same file
ls -l &> all-output.log

# Append output (don't overwrite)
echo "New log entry" >> output.log

# Discard errors (send to /dev/null)
command 2>/dev/null

# Redirect both stdout and stderr, append
command >> all.log 2>&1

Practical Script with Timing

#!/bin/bash

TIMESTAMP_START=$(date +%s)
echo "Script started at: $(date)"

# ... your script logic here ...

TIMESTAMP_END=$(date +%s)
echo "Script completed at: $(date)"
echo "Time taken: $((TIMESTAMP_END - TIMESTAMP_START)) seconds"

Summary

Git Workflow:    Edit → git add → git commit → git push
Script Flow:     Shebang → Variables → Conditions → Functions → Redirections
Debug Habit:     Always check $? after critical commands
Code Hygiene:    Follow DRY — variables over hardcoded values
Think First:     Write pseudocode before bash code

Happy scripting! If you found this helpful, drop a ❤️ and share it with someone starting their DevOps journey.