parent
8cbe5d026a
commit
d93858c12d
@ -0,0 +1 @@
|
||||
|
@ -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,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…
Reference in new issue