Rewirtten backup tool

master
Kynsight 2 weeks ago
parent 8cbe5d026a
commit d93858c12d

@ -0,0 +1,227 @@
#!/bin/bash
# =============================================================================
# Backup Tool — Argument-Driven rsync Wrapper
# =============================================================================
# Description:
# A modular backup script using rsync with smart CLI parsing
# Supports: local/system/user backups, remote SSH push/pull, link-dest snapshots
# =============================================================================
# Load CLI parser (must be in same or relative path)
source ./lib/cli/arg_parser.sh
# Flag check utility
get_flag() {
local name="$1"
[[ -n "${FLAG_GROUPS[$name]}" && "${FLAG_GROUPS[$name]}" == "true" ]]
}
# ----------------------------------------------------------------------------
# Define valid flags
# ----------------------------------------------------------------------------
VALID_FLAGS=(
"source" "target" "dry-run" "verbose" "compress" "system"
"ssh-send" "ssh-receive" "link" "ntfs" "name" "exclude"
)
# ----------------------------------------------------------------------------
# Default values and constants
# ----------------------------------------------------------------------------
SYSTEM_EXCLUDES=(
"/dev/*" "/proc/*" "/sys/*" "/tmp/*" "/run/*" "/mnt/*" "/media/*"
"swapfile" "lost+found" ".cache" "Downloads" ".ecryptfs"
)
# ----------------------------------------------------------------------------
# Parse CLI args
# ----------------------------------------------------------------------------
parse_grouped_args "$@"
# ----------------------------------------------------------------------------
# Validate flags
# ----------------------------------------------------------------------------
for key in "${!FLAG_GROUPS[@]}"; do
found=false
for valid in "${VALID_FLAGS[@]}"; do
[[ "$key" == "$valid" ]] && found=true && break
done
if [[ "$found" == false ]]; then
echo "Error: Unknown flag '--$key'"
exit 1
fi
done
# ----------------------------------------------------------------------------
# Extract values from flags
# ----------------------------------------------------------------------------
IFS=' ' read -r -a SOURCE <<<"${FLAG_GROUPS["source"]}"
IFS=' ' read -r -a TARGET <<<"${FLAG_GROUPS["target"]}"
IFS=' ' read -r -a EXCLUDES <<<"${FLAG_GROUPS["exclude"]}"
BACKUP_NAME=""
[[ -n "${FLAG_GROUPS["name"]}" ]] && BACKUP_NAME="${FLAG_GROUPS["name"]}"
DRY_RUN=false
VERBOSE=false
COMPRESS=false
SYSTEM=false
NTFS=false
get_flag "dry-run" && DRY_RUN=true
get_flag "verbose" && VERBOSE=true
get_flag "compress" && COMPRESS=true
get_flag "system" && SYSTEM=true
get_flag "ntfs" && NTFS=true
# ----------------------------------------------------------------------------
# Required argument checks
# ----------------------------------------------------------------------------
if [[ -z "${SOURCE[0]}" || -z "${TARGET[0]}" ]]; then
echo "Error: --source and --target are required."
exit 1
elif [[ ${#SOURCE[@]} -gt 1 || ${#TARGET[@]} -gt 1 ]]; then
echo "Error: --source and --target must be single paths only."
exit 1
fi
# ----------------------------------------------------------------------------
# Backup naming (target path suffix)
# ----------------------------------------------------------------------------
if [[ -n "$BACKUP_NAME" ]]; then
TARGET_PATH="${TARGET[0]%/}/$BACKUP_NAME"
else
TARGET_PATH="${TARGET[0]}"
fi
# ----------------------------------------------------------------------------
# SSH configuration (push or pull)
# ----------------------------------------------------------------------------
SSH_SEND="${FLAG_GROUPS["ssh-send"]}"
SSH_RECEIVE="${FLAG_GROUPS["ssh-receive"]}"
if [[ -n "$SSH_SEND" && -n "$SSH_RECEIVE" ]]; then
echo "Error: Only one of --ssh-send or --ssh-receive can be set."
exit 1
fi
if [[ -n "$SSH_SEND" && ! "$SSH_SEND" =~ ^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+$ ]]; then
echo "Error: --ssh-send must be in user@host format."
exit 1
fi
if [[ -n "$SSH_RECEIVE" && ! "$SSH_RECEIVE" =~ ^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+$ ]]; then
echo "Error: --ssh-receive must be in user@host format."
exit 1
fi
if [[ -n "$SSH_SEND" ]]; then
TARGET_PATH="$SSH_SEND:$TARGET_PATH"
elif [[ -n "$SSH_RECEIVE" ]]; then
SOURCE[0]="$SSH_RECEIVE:${SOURCE[0]}"
fi
# ----------------------------------------------------------------------------
# Link-dest configuration (incremental snapshots)
# ----------------------------------------------------------------------------
LINK_DIR=""
if [[ -n "${FLAG_GROUPS["link"]}" ]]; then
LINK_DIR="${FLAG_GROUPS["link"]}"
if [[ ! "$LINK_DIR" =~ ^/.* ]]; then
echo "Error: --link must be an absolute path."
exit 1
fi
fi
# ----------------------------------------------------------------------------
# System-specific excludes
# ----------------------------------------------------------------------------
if [[ "$SYSTEM" == true ]]; then
EXCLUDES+=("${SYSTEM_EXCLUDES[@]}")
fi
# ----------------------------------------------------------------------------
# Build rsync command
# ----------------------------------------------------------------------------
CMD=("rsync" "-a" "--delete")
if [[ "$NTFS" == false ]]; then
CMD+=("-A" "-X")
fi
[[ "$SYSTEM" == true ]] && CMD+=("-H")
CMD+=("-P")
CMD+=("--delete-excluded")
$VERBOSE && CMD+=("-v")
$DRY_RUN && CMD+=("--dry-run")
$COMPRESS && CMD+=("-z")
[[ -n "$LINK_DIR" ]] && CMD+=("--link-dest=$LINK_DIR")
# Add excludes here
if [[ ${#EXCLUDES[@]} -gt 0 ]]; then
for ex in "${EXCLUDES[@]}"; do
CMD+=("--exclude=$ex")
done
fi
CMD+=("${SOURCE[0]}" "$TARGET_PATH")
# ----------------------------------------------------------------------------
# Summary Output
# ----------------------------------------------------------------------------
echo "┌──────────────────────────────────────────┐"
echo "│ Starting Backup Process │"
echo "└──────────────────────────────────────────┘"
echo
echo "Directories:"
echo " ├─ Source : ${SOURCE[0]}"
echo " └─ Target : $TARGET_PATH"
echo
echo "Mode:"
if [[ "$SYSTEM" == true ]]; then
echo " └─ System Backup : Default excludes applied"
else
echo " └─ User Backup : No system-specific excludes"
fi
echo
echo "Configuration:"
flag_printed=false
[[ "$DRY_RUN" == true ]] && echo " ├─ Dry-run : enabled" && flag_printed=true
[[ "$VERBOSE" == true ]] && echo "${flag_printed:+ ├─ }Verbose : enabled" && flag_printed=true
[[ "$COMPRESS" == true ]] && echo " └─ Compression : enabled"
[[ "$flag_printed" == false ]] && echo " └─ No Configuration set"
echo
echo "Snapshot:"
echo " ├─ Backup Name : ${BACKUP_NAME:-none}"
echo " └─ Link From : ${LINK_DIR:-none}"
echo
echo "Excludes:"
if [[ ${#EXCLUDES[@]} -gt 0 ]]; then
last_index=$((${#EXCLUDES[@]} - 1))
for i in "${!EXCLUDES[@]}"; do
prefix=$([[ "$i" -eq "$last_index" ]] && echo " └─" || echo " ├─")
echo "$prefix ${EXCLUDES[$i]}"
done
else
echo " └─ No Excludes Set"
fi
echo
echo "┌──────────────────────────────────────────┐"
echo "│ Executing Rsync Process │"
echo "└──────────────────────────────────────────┘"
echo "Command : ${CMD[*]}"
echo
# Uncomment to execute
"${CMD[@]}"
echo
echo "┌──────────────────────────────────────────┐"
echo "│ Backup Process Completed │"
echo "└──────────────────────────────────────────┘"

@ -0,0 +1,184 @@
#!/bin/bash
# =============================================================================
# Bash CLI Argument Parser — Greedy Grouping, Flag-Agnostic
# =============================================================================
#
# Overview:
# Parses command-line arguments into key/value groups.
# Every flag (long or short) collects values until the next flag.
# You do NOT need to define which flags expect values.
# Short flags can be grouped (-abc) and are treated as booleans.
#
# Supported input formats:
# --key value1 value2 → key = [value1, value2]
# --key=value → key = value
# -k value → k = value
# -k=value → k = value
# -abc → a=true, b=true, c=true
#
# Not supported:
# -kvalue → this is parsed as combined short flags (k, v, a…)
# Global positionals not linked to any flag (can be added separately if needed)
#
# Example usage:
# ./arg_analyse.sh --mode fast --files input1.txt input2.txt -vx --output=result.txt
#
# Output:
# --mode:
# [0]: fast
# --files:
# [0]: input1.txt
# [1]: input2.txt
# --v:
# [boolean flag]
# --x:
# [boolean flag]
# --output:
# [0]: result.txt
#
# =============================================================================
# STORAGE: key = flag name, value = string or boolean
declare -A FLAG_GROUPS
# -----------------------------------------------------------------------------
# FUNCTION: parse_grouped_args
# Description:
# Parses input arguments and fills the FLAG_GROUPS associative array.
# -----------------------------------------------------------------------------
parse_grouped_args() {
local current_flag=""
local -a current_values=()
while [[ $# -gt 0 ]]; do
local token="$1"
# -------------------------------------------------------------------------
# Case 1: --key=value or -k=value (explicit assignment form)
# This handles both long and short flags passed as --flag=value or -f=value
# -------------------------------------------------------------------------
# Breakdown of regex:
# ^ → start of the string
# --? → match one or two dashes (e.g. "-" or "--")
# ([^=]+) → capture group 1: one or more characters that are NOT "="
# → this becomes the flag name (e.g. "output")
# = → literal equals sign
# (.*) → capture group 2: any number of characters (can be empty)
# → this becomes the first value for the flag
#
# Examples:
# --file=abc.txt → key = "file", val = "abc.txt"
# -x=1 → key = "x", val = "1"
if [[ "$token" =~ ^--?([^=]+)=(.*)$ ]]; then
# Before processing this --flag=value, we must flush any previous group
if [[ -n "$current_flag" ]]; then
if [[ ${#current_values[@]} -eq 0 ]]; then
FLAG_GROUPS["$current_flag"]="true"
else
FLAG_GROUPS["$current_flag"]="${current_values[*]}"
fi
fi
# Capture the key and value from the current --key=value or -k=value
local key="${BASH_REMATCH[1]}" # group 1 from regex (flag name)
local val="${BASH_REMATCH[2]}" # group 2 from regex (value after =)
# Set current flag context to continue collecting values if any follow
current_flag="$key"
current_values=("$val") # start a new value group with first value from assignment
# -------------------------------------------------------------------------
# Case 2: --long flag (starts a new group)
# -------------------------------------------------------------------------
elif [[ "$token" == --* ]]; then
# Save previous flag (if any)
if [[ -n "$current_flag" ]]; then
if [[ ${#current_values[@]} -eq 0 ]]; then
FLAG_GROUPS["$current_flag"]="true"
else
FLAG_GROUPS["$current_flag"]="${current_values[*]}"
fi
fi
current_flag="${token#--}" # Remove leading "--"
current_values=()
# -------------------------------------------------------------------------
# Case 3: -short or -grouped flags (-x or -abc)
# -------------------------------------------------------------------------
elif [[ "$token" == -* && "$token" != "--" ]]; then
# Save previous group
if [[ -n "$current_flag" ]]; then
if [[ ${#current_values[@]} -eq 0 ]]; then
FLAG_GROUPS["$current_flag"]="true"
else
FLAG_GROUPS["$current_flag"]="${current_values[*]}"
fi
current_flag=""
current_values=()
fi
local chars="${token#-}"
if [[ "${#chars}" -gt 1 ]]; then
# -abc → treat each letter as a boolean flag
for ((i = 0; i < ${#chars}; i++)); do
FLAG_GROUPS["${chars:i:1}"]="true"
done
else
# Single short flag (-x) may collect values
current_flag="$chars"
current_values=()
fi
# -------------------------------------------------------------------------
# Case 4: Value (non-flag)
# -------------------------------------------------------------------------
else
current_values+=("$token")
fi
shift
done
# Final flush (for last group)
if [[ -n "$current_flag" ]]; then
if [[ ${#current_values[@]} -eq 0 ]]; then
FLAG_GROUPS["$current_flag"]="true"
else
FLAG_GROUPS["$current_flag"]="${current_values[*]}"
fi
fi
}
# -----------------------------------------------------------------------------
# FUNCTION: print_grouped_args
# Description:
# Pretty-prints the FLAG_GROUPS key/value structure.
# -----------------------------------------------------------------------------
print_grouped_args() {
echo "Parsed Arguments:"
for key in "${!FLAG_GROUPS[@]}"; do
IFS=' ' read -r -a values <<<"${FLAG_GROUPS[$key]}"
echo "--$key:"
if [[ "${FLAG_GROUPS[$key]}" == "true" ]]; then
echo " [boolean flag]"
elif [[ ${#values[@]} -eq 0 ]]; then
echo " [empty]"
else
for i in "${!values[@]}"; do
echo " [$i]: ${values[$i]}"
done
fi
done
}
# -----------------------------------------------------------------------------
# TEST CASE (for development)
# Comment this out to use real CLI args
# -----------------------------------------------------------------------------
#set -- --name Kerem yollu siki --age=33 --flag -vx --output result.txt -t alpha beta
# Run parser and print results
#parse_grouped_args "$@"
#print_grouped_args

@ -1,6 +1,6 @@
#!/bin/bash
COLORS_SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
COLORS_SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
black='30'
red='31'
@ -35,14 +35,12 @@ themeInstall="$yellow"
themeSkip="$lightGreen"
themeComment="$lightGreen"
color_start()
{
local -n ref1=$1
printf $colorDelimiterBegin$ref1$colorDelimiterEnd
color_start() {
local -n ref1=$1
printf $colorDelimiterBegin$ref1$colorDelimiterEnd
}
color_stop()
{
printf $colorDelimiterBegin$fontNormal$colorDelimiterEnd
printf $colorStop
color_stop() {
printf $colorDelimiterBegin$fontNormal$colorDelimiterEnd
printf $colorStop
}

@ -1,99 +0,0 @@
#!/bin/bash
#sudo apt-get install -y rsync
OK=0
response="na"
echo "#######################################"
echo "What kind of backup do you want to do:"
echo "Create new from ssh -> ns "
echo "Create new from local -> nl "
echo "Create itteration from ssh -> is"
echo "Create itteration from local -> il"
echo "Restore from local to local -> rl"
echo "#######################################"
# loop until we get a good answer and break out
while [ "$OK" = 0 ]
do
read -p "Please choose : ns OR nl OR is OR il OR rl OR quit: " input
# Test the input
if [ "$input" = "ns" ] || [ "$input" = "nS" ] || [ "$input" = "Ns" ] || [ "$input" = "NS" ]
then
response="ns"
OK=1
elif [ "$input" = "nl" ] || [ "$input" = "nL" ] || [ "$input" = "Nl" ] || [ "$input" = "NL" ]
then
response="nl"
OK=1
elif [ "$input" = "is" ] || [ "$input" = "iS" ] || [ "$input" = "iS" ] || [ "$input" = "IS" ]
then
response="is"
OK=1
elif [ "$input" = "il" ] || [ "$input" = "iL" ] || [ "$input" = "Il" ] || [ "$input" = "IL" ]
then
response="il"
OK=1
elif [ "$input" = "rl" ] || [ "$input" = "rL" ] || [ "$input" = "Rl" ] || [ "$input" = "RL" ]
then
response="rl"
OK=1
elif [ "$input" = "help" ] || [ "$input" = "h" ]
then
echo "What kinf of backup do you want to do:"
echo "Create new from ssh -> ns "
echo "Create new from local -> nl "
echo "Create itteration from ssh -> is"
echo "Create itteration from local -> il"
echo "Restore from local to local -> rl"
elif [ "$input" = "q" ] || [ "$input" = "quit" ]
then
exit
else
# Invalid input
echo "INPUT ERROR: Must be ns OR nl OR is OR il OR rl OR quit: Please try again."
fi
done
if [ "$response" = "ns" ];
then
read -p "Source ip address: " -i -e sourceIp
read -p "Source user name: " -i -e sourceUser
read -p "Source directory: " -i "/" -e sourceDir
read -p "Target directory: " -i "/" -e targetDir
sudo rsync -aAXP -e ssh --delete --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","swapfile","lost+found",".cache","Downloads",".ecryptfs"} $sourceUser@$sourceIp:$sourceDir $targetDir
elif [ "$response" = "nl" ];
then
read -p "Source directory: " -i "/" -e sourceDir
read -p "Target directory: " -i "/" -e targetDir
sudo rsync -aAXP --delete --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","swapfile","lost+found",".cache","Downloads",".ecryptfs"} $sourceDir $targetDir
elif [ "$response" = "is" ];
then
read -p "Source ip address: " -i -e sourceIp
read -p "Source user name: " -i -e sourceUser
read -p "Source directory: " -i "/" -e sourceDir
read -p "Target current directory: " -i "/" -e linkDir
read -p "Target itteration directory: " -i "/" -e targetDir
sudo rsync -aAXPH -e ssh --delete --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","swapfile","lost+found",".cache","Downloads",".ecryptfs"} --link-dest=$linkDir $sourceUser@$sourceIp:$sourceDir $targetDir
elif [ "$response" = "il" ];
then
read -p "Source directory: " -i "/" -e sourceDir
read -p "Target current directory: " -i "/" -e linkDir
read -p "Target itteration directory: " -i "/" -e targetDir
sudo rsync -aAXPH --delete --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","swapfile","lost+found",".cache","Downloads",".ecryptfs"} --link-dest=$linkDir $sourceDir $targetDir
elif [ "$response" = "rl" ];
then
lsblk
read -p "Target Device to be mounted: " -i "/dev/" -e deviceName
read -p "Target device mount direcotry: " -i "/mnt/" -e targetDir
read -p "Backup directory: " -i "/" -e sourceDir
sudo mount $deviceName $targetDir
ls $targetDir
sudo rsync -aAXP --delete --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","swapfile","lost+found",".cache","Downloads",".ecryptfs"} $sourceDir $targetDir
fi
exit
Loading…
Cancel
Save