You’ve added users with useradd. Maybe deleted one with userdel. But have you built a tool that does this safely, with validation, confirmations, and proper error handling?

That’s the difference between running commands and being a sysadmin.

Today, we’re building a User Management Tool - an interactive menu-driven utility for managing Linux users and groups. The kind of tool that prevents the “oops I deleted the wrong user” disasters.

The Problem: Manual User Management is Risky

Here’s how most people manage users:

useradd john           # Hope I spelled it right
passwd john            # Set a password
usermod -aG sudo john  # Add to sudo group
userdel john           # Wait, was that the right John?

No confirmation. No validation. No safety checks. One typo and you’ve got problems.

Our tool will:

  • Validate usernames before creating
  • Prevent deleting root or system users
  • Show what’s about to happen before doing it
  • Provide a clean, menu-driven interface
  • Handle all the edge cases

The Toolkit: User Management Commands

Group 1: Information Gathering

CommandPurpose
id usernameCheck if user exists, show UID/GID
getent passwdQuery user database
groups usernameList user’s groups
last usernameUser’s login history
who / wCurrently logged-in users

Group 2: User Modification

CommandPurpose
useraddCreate new user
usermodModify existing user
userdelDelete user
passwdSet/change password
chagePassword expiry settings

Group 3: Group Management

CommandPurpose
groupaddCreate new group
groupdelDelete group
usermod -aGAdd user to group
gpasswd -dRemove user from group

Important Files

FilePurpose
/etc/passwdUser account information
/etc/shadowEncrypted passwords
/etc/groupGroup definitions
/etc/sudoersSudo configuration

The Walkthrough: Building Step by Step

Step 1: Check for Root Privileges

Most user operations require root. Let’s make that check reusable:

check_root() {
    if [[ $EUID -ne 0 ]]; then
        echo -e "${RED}Error: This operation requires root privileges${NC}"
        return 1
    fi
    return 0
}

# Usage:
create_user() {
    check_root || return 1
    # ... rest of function
}

What’s $EUID? It’s the Effective User ID. Root is always 0.

Step 2: Validate Username Format

Before creating a user, validate the input:

validate_username() {
    local username="$1"

    # Check if empty
    if [[ -z "$username" ]]; then
        echo "Username cannot be empty"
        return 1
    fi

    # Check format: must start with letter/underscore, contain only valid chars
    if ! [[ "$username" =~ ^[a-z_][a-z0-9_-]*$ ]]; then
        echo "Invalid username format"
        echo "Must start with lowercase letter or underscore"
        echo "Can contain: lowercase letters, numbers, underscore, hyphen"
        return 1
    fi

    # Check length
    if [[ ${#username} -gt 32 ]]; then
        echo "Username too long (max 32 characters)"
        return 1
    fi

    return 0
}

Regex breakdown:

  • ^[a-z_] - Must start with lowercase letter or underscore
  • [a-z0-9_-]*$ - Rest can be letters, numbers, underscore, hyphen

Step 3: Build the Interactive Menu

print_header() {
    clear
    echo -e "${CYAN}"
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║              USER MANAGEMENT TOOL v1.0                     ║"
    echo "╚════════════════════════════════════════════════════════════╝"
    echo -e "${NC}"
}

print_menu() {
    echo -e "${BLUE}Select an option:${NC}"
    echo ""
    echo "  1) List all users"
    echo "  2) Show user details"
    echo "  3) Create new user"
    echo "  4) Delete user"
    echo "  5) Modify user"
    echo "  6) Lock/Unlock user"
    echo "  7) List groups"
    echo "  8) Add user to group"
    echo "  9) Show logged-in users"
    echo "  0) Exit"
    echo ""
}

main() {
    while true; do
        print_header
        print_menu

        read -p "Enter choice [0-9]: " choice

        case $choice in
            1) list_users ;;
            2) show_user_details ;;
            3) create_user ;;
            4) delete_user ;;
            5) modify_user ;;
            6) lock_unlock_user ;;
            7) list_groups ;;
            8) add_user_to_group ;;
            9) show_logged_users ;;
            0) echo "Goodbye!"; exit 0 ;;
            *) echo "Invalid option" ;;
        esac

        read -p "Press Enter to continue..."
    done
}

Step 4: List Users with Highlighting

list_users() {
    echo -e "${CYAN}=== System Users ===${NC}"
    echo ""
    printf "%-20s %-8s %-8s %-30s %s\n" "USERNAME" "UID" "GID" "HOME" "SHELL"
    echo "─────────────────────────────────────────────────────────────────────────────"

    while IFS=: read -r username _ uid gid _ home shell; do
        # Highlight regular users (UID >= 1000)
        if [[ $uid -ge 1000 && $uid -lt 65534 ]]; then
            printf "${GREEN}%-20s${NC} %-8s %-8s %-30s %s\n" "$username" "$uid" "$gid" "$home" "$shell"
        else
            printf "%-20s %-8s %-8s %-30s %s\n" "$username" "$uid" "$gid" "$home" "$shell"
        fi
    done < /etc/passwd

    echo ""
    echo -e "${GREEN}Green${NC} = Regular users (UID >= 1000)"
}

What’s happening:

  • IFS=: sets the field separator to colon (passwd format)
  • read -r reads each line into variables
  • We skip unused fields with _
  • UIDs 1000+ are typically regular users

Step 5: Create User with Safety Checks

create_user() {
    check_root || return 1

    echo -e "${CYAN}=== Create New User ===${NC}"

    read -p "Enter username: " username
    validate_username "$username" || return 1

    # Check if user already exists
    if id "$username" &>/dev/null; then
        echo -e "${RED}Error: User '$username' already exists${NC}"
        return 1
    fi

    read -p "Enter full name (optional): " fullname
    read -p "Enter shell [/bin/bash]: " shell
    shell="${shell:-/bin/bash}"  # Default to bash

    # Ask about home directory
    local create_home=""
    read -p "Create home directory? [y/N]: " response
    if [[ "${response,,}" == "y" ]]; then
        create_home="-m"
    fi

    echo ""
    echo "Creating user '$username'..."

    if useradd $create_home -s "$shell" ${fullname:+-c "$fullname"} "$username"; then
        echo -e "${GREEN}User '$username' created successfully${NC}"

        read -p "Set password now? [y/N]: " set_pwd
        if [[ "${set_pwd,,}" == "y" ]]; then
            passwd "$username"
        fi
    else
        echo -e "${RED}Failed to create user${NC}"
        return 1
    fi
}

Parameter expansion tricks:

  • ${shell:-/bin/bash} - Use /bin/bash if shell is empty
  • ${fullname:+-c "$fullname"} - Only add -c flag if fullname is set
  • ${response,,} - Convert to lowercase for comparison

Step 6: Delete User with Safety Rails

This is where things can go wrong. Let’s add multiple safety checks:

delete_user() {
    check_root || return 1

    echo -e "${CYAN}=== Delete User ===${NC}"

    read -p "Enter username to delete: " username

    # Check if user exists
    if ! id "$username" &>/dev/null; then
        echo -e "${RED}Error: User '$username' does not exist${NC}"
        return 1
    fi

    # NEVER delete root
    if [[ "$username" == "root" ]]; then
        echo -e "${RED}ERROR: Cannot delete root user!${NC}"
        return 1
    fi

    # Show what we're about to delete
    echo ""
    echo "User to be deleted:"
    id "$username"
    echo ""

    # Ask about home directory
    local remove_home=""
    read -p "Remove home directory and mail spool? [y/N]: " response
    if [[ "${response,,}" == "y" ]]; then
        remove_home="-r"
    fi

    # Final confirmation
    read -p "Are you SURE you want to delete user '$username'? [y/N]: " confirm
    if [[ "${confirm,,}" != "y" ]]; then
        echo "Deletion cancelled"
        return 0
    fi

    # Kill any running processes
    pkill -u "$username" 2>/dev/null || true

    # Delete the user
    if userdel $remove_home "$username"; then
        echo -e "${GREEN}User '$username' deleted successfully${NC}"
    else
        echo -e "${RED}Failed to delete user${NC}"
        return 1
    fi
}

Safety features:

  • Can’t delete root
  • Shows user info before deletion
  • Requires explicit confirmation
  • Kills running processes first (prevents “user is logged in” errors)
  • || true prevents script exit if no processes found

Step 7: Lock/Unlock Users

lock_unlock_user() {
    check_root || return 1

    read -p "Enter username: " username

    if ! id "$username" &>/dev/null; then
        echo -e "${RED}User '$username' does not exist${NC}"
        return 1
    fi

    # Check current status
    local status=$(passwd -S "$username" 2>/dev/null | awk '{print $2}')

    if [[ "$status" == "L" || "$status" == "LK" ]]; then
        echo "User '$username' is currently LOCKED"
        read -p "Unlock user? [y/N]: " response
        if [[ "${response,,}" == "y" ]]; then
            usermod -U "$username" && echo "User unlocked"
        fi
    else
        echo "User '$username' is currently ACTIVE"
        read -p "Lock user? [y/N]: " response
        if [[ "${response,,}" == "y" ]]; then
            usermod -L "$username" && echo "User locked"
        fi
    fi
}

Lock status codes:

  • L or LK = Locked
  • P = Has password (active)
  • NP = No password

Step 8: Group Management

add_user_to_group() {
    check_root || return 1

    read -p "Enter username: " username

    if ! id "$username" &>/dev/null; then
        echo -e "${RED}User '$username' does not exist${NC}"
        return 1
    fi

    echo "Current groups for $username:"
    groups "$username"
    echo ""

    read -p "Enter group name to add: " groupname

    # Check if group exists, offer to create
    if ! getent group "$groupname" &>/dev/null; then
        echo "Group '$groupname' does not exist"
        read -p "Create group? [y/N]: " create
        if [[ "${create,,}" == "y" ]]; then
            groupadd "$groupname" && echo "Group created"
        else
            return 1
        fi
    fi

    # Add user to group
    if usermod -aG "$groupname" "$username"; then
        echo -e "${GREEN}User '$username' added to group '$groupname'${NC}"
        echo "New groups:"
        groups "$username"
    else
        echo -e "${RED}Failed to add user to group${NC}"
    fi
}

Key command: usermod -aG

  • -a = Append (don’t replace existing groups)
  • -G = Supplementary groups

Without -a, you’d replace ALL the user’s groups!

The Complete Script Structure

#!/bin/bash
set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'

# Helper functions
check_root() { ... }
validate_username() { ... }
confirm() { ... }

# User operations
list_users() { ... }
show_user_details() { ... }
create_user() { ... }
delete_user() { ... }
modify_user() { ... }
lock_unlock_user() { ... }

# Group operations
list_groups() { ... }
add_user_to_group() { ... }

# Menu
print_header() { ... }
print_menu() { ... }

main() {
    while true; do
        print_header
        print_menu
        read -p "Enter choice: " choice
        case $choice in
            # ... handle options
        esac
        pause
    done
}

main

What You Just Learned

You built a professional user management tool. Along the way, you mastered:

  • User management commands - useradd, usermod, userdel, passwd
  • System files - /etc/passwd, /etc/shadow, /etc/group
  • Input validation - Regex, length checks, existence checks
  • Safety patterns - Confirmation dialogs, root protection
  • Interactive menus - case statements, read loops
  • Error handling - Return codes, graceful failures

Your Challenge

The tool is functional. Now make it production-ready:

  1. Add bulk user creation - Read usernames from CSV
  2. Add password expiry management - Use chage command
  3. Export user list - Generate CSV or JSON report
  4. Add audit logging - Log all changes to a file
  5. Add backup before delete - Archive home directory first

Remember: With great power comes great responsibility. Test these scripts on a VM first.


Next in the series: Building a Todo CLI with JSON Persistence