Содержание

BackUp и синхронизация

За долгую деятельность в области системного администрирования было написано много bash-скриптов для различных «мелких» задач. Некоторяе скрипты запускались по cron-ну или incron-ну, а некоторые через алиасы.
Часть скриптов по сути выполняли одинаковую задачу, но с разными данными. Например, создание dd-образов разделов жесткого диска. Для объединения скриптов, запускаемых в ручном режиме, с одинаковыми задачами и были написаны данные bash-скрипты.
Все скрипты рабочие. Т.е. регулярно используются реально. Много раз переписывались. Есть задумки их «расширений» и т.д.

Скрипты объединены в две независимые группы. Условно группы названы «BackUpData» и «SyncData».
«BackUpData» и «SyncData» написаны по одинаковым принципам:

  1. Модульность. Подключение дополнительныз скриптов с минимальными изменениями кода.
  2. Построение динамического древовидного интерактивного меню в зависимости от подключаемых скриптов, текущего компьютера и других условий.
  3. Широкое использование функций «главного» скрипта в подключаемых скриптах.
  4. Хранение данных отдельно от кода. Правда, это относится к подключаемым скриптам. В них реализована очень гибкая возможность выполнения действий в зависимоти от разных условий (см. «.arrays»-файлы подключаемых скриптов).

Описывать буду на основе скрипта «BackUpData». Для «SyncData» опишу только принципиальные отличия.

BackUpData

Назначение этого «главного» скрипта:

Вот пример меню при вызове скрипта с параметром «BackUpData.bash debug_menu_main»:

Вот пример меню при вызове скрипта с параметром «BackUpData.bash debug_menu_main»:

##### menu_main ():
1:  Создание архивных копий файлов (всего файлов: 5)=show_menu
1.1: файл: "asus-x55c.tc" (8,1G)=show_menu
1.1.1: создать архив в "media/VtData/BackUp/Data/HD/Work/TC" (свободно: 344G) (последний: 2022-06-02)=backup_file_run asus-x55c.tc asus-x55c.tc:VtData
1.1.2: создать архив в "media/wd1t/BackUp/Data/HD/Work/TC" (свободно: 221G) (последний: 2022-06-02)=backup_file_run asus-x55c.tc asus-x55c.tc:wd1t
1.2: файл: "w7-64.qcow2" (12G)=show_menu
1.2.1: создать архив в "media/wd1t/BackUp/Virt-Storage/asus-x55c/AQEMU" (свободно: 221G) (последний: 2022-06-02)=backup_file_run w7-64.qcow2 w7-64.qcow2:wd1t
1.3: файл: "w7-RDPWrap.qcow2" (9,8G)=show_menu
1.3.1: создать архив в "media/wd1t/BackUp/Virt-Storage/asus-x55c/AQEMU" (свободно: 221G) (последний: 2022-01-18)=backup_file_run w7-RDPWrap.qcow2 w7-RDPWrap.qcow2:wd1t
2: Создание образов внутренних дисков=show_menu
2.1: Диск "backup" не подключен=empty
2.2: Создание образов внутренних дисков на "media/wd1t" (свободно: 220,9G)=show_menu
2.2.1: создать образ "sda1" (300M) на "media/wd1t" (последний: 2022-06-02)/}=dd_disk_run wd1t wd1t:sda1
2.2.2: создать образ "sda3" (20G) на "media/wd1t" (последний: 2022-06-02)/}=dd_disk_run wd1t wd1t:sda3
2.2.3: создать образ "sda4" (18,6G) на "media/wd1t" (последний: 2022-06-02)/}=dd_disk_run wd1t wd1t:sda4
3: Создание образов флешек (всего флешек: 2)=show_menu
3.1: Создание ".dd_gzip"-образа флешки "Debian_11_64_live" (28,5G)=show_menu
3.1.1: создать образ (последний: 2022-06-03) на "media/VtData" (свободно: 343,4G)=dd_flash_run Debian_11_64_live Debian_11_64_live:VtData VtData
3.1.2: создать образ (последний: 2022-06-03) на "media/wd1t" (свободно: 220,9G)=dd_flash_run Debian_11_64_live Debian_11_64_live:wd1t wd1t
3.2: Флешка: "Nik-rp" не подключена=empty
"debug_menu_main" "Enter" - продолжение, "Ctrl+c" - прервать


А так это выводится на консоль (примеры):

Главное меню

Главное меню

 Главное меню)

Создание архивных копий файлов (всего файлов: 5)

Создание архивных копий файлов (всего файлов: 5)

 Создание архивных копий файлов

файл: «asus-x55c.tc» (8,1G)

файл: «asus-x55c.tc» (8,1G)

 файл: "asus-x55c.tc" (8,1G)

Создание образов внутренних дисков

Создание образов внутренних дисков

 Создание образов внутренних дисков

Создание образов внутренних дисков на «media/wd1t» (свободно: 220,9G)

Создание образов внутренних дисков на «media/wd1t» (свободно: 220,9G)

 Создание образов внутренних дисков на "media/wd1t" (свободно: 220,9G)

Создание образов флешек (всего флешек: 2)

Создание образов флешек (всего флешек: 2)

 Создание образов флешек (всего флешек: 2)

Создание «.dd_gzip»-образа флешки «Debian_11_64_live» (28,5G)

Создание «.dd_gzip»-образа флешки «Debian_11_64_live» (28,5G)

 Создание ".dd_gzip"-образа флешки "Debian_11_64_live" (28,5G)


Скрипт BackUpData

BackUpData.bash

BackUpData.bash

#!/bin/bash
# set -e
# set -x

################
# Главный скрипт
################
# Обрабатывает параметры:
# debug_menu_main - показ массива главного меню
# lsblk_show - показ блочных устройств

clear

#===================
# Начальные проверки
#===================
if [ -z "$(command -v whiptail)" ]; then
    echo "Не установлен пакет \"whiptail\"!"
    exit 0
fi

# Это должно быть в самом начале!!!
name_scr=${0##*/}
name_scr=${name_scr%.*}
readonly name_scr
readonly title_scr="BackUp данных на \"${HOSTNAME}\" (${name_scr})"
readonly IFS_OLD=$IFS

# pgrep не понимает имен более 15 символов!
if [ $(pgrep -c "${name_scr:0:15}") -gt 1 ]; then
    whiptail --title "${name_scr}" --msgbox  "Скрипт уже запущен!" 7 50
    exit
fi

# Должна быть первой функцией
function whiptail_msgbox {
    [[ -z "$@" ]] && return

    local strings="$@"

    local lines=$(echo -e "${strings}" | wc -l)
    local height=7
    local height_max=12
    [[ "${lines}" -gt "1" ]] && height=$((height + ${lines}))

    local width=60
    local width_max=80

    IFS=$'\n'
    for string in $(echo -e ${strings}); do
        [[ "${#string}" -gt "${width}" ]] && width=${#string}
    done
    IFS=${IFS_OLD}

    [[ "${width}" -gt "${width_max}" ]] && width=${width_max}

    if [ "${height}" -gt "${height_max}" ]; then
        whiptail --scrolltext --title "${title_scr}" --msgbox "${strings}" ${height_max} ${width}
    else
        whiptail --title "${title_scr}" --msgbox "${strings}" ${height} ${width}
    fi
}

#================
# Общие константы
#================

# pv
readonly PvInstalled="$(command -v pv)"

# netcat (nc)
readonly NetcatInstalled="$(command -v netcat)"

# Каталог откуда запущен скрипт
readonly ScriptDir="$(cd -- "$(dirname -- "${0}")" &> /dev/null && pwd)"

# Каталог дополнительных подключаемых скриптов
readonly Inclusions="${ScriptDir}/Inclusions"
if [ ! -d ${Inclusions} ]; then
    whiptail_msgbox "Не найден каталог:\n${Inclusions}"
    exit
fi

# Каталог дополнительных файлов для подключаемых скриптов
readonly ScriptCfg="${ScriptDir}/${name_scr}Cfg"
if [ ! -d ${ScriptCfg} ]; then
    whiptail_msgbox "Не найден каталог:\n${ScriptCfg}"
    exit
fi

# Шаблон
readonly template='????-??-??'

#==============
# Общие функции
#==============
function press_enter {
    # Вывод сообщений в консоль и ожидание нажатия Enter
    # использовать echo здесь не получится
    # из-за возврата результата через echo в некоторых вызывающих функциях!

    set +x
    if [ -z "$@" ]; then
        read -p "\"Enter\" - продолжение, \"Ctrl+c\" - прервать"
    else
        read -p "\"$@\" \"Enter\" - продолжение, \"Ctrl+c\" - прервать"
    fi
}

function arr_keys_sort {
    # Возвращает отсортированную строку ключей массива
    # "$1" - имя массива

    [[ -z "$1" ]] && return

    declare -n arr_keys_declare="$1"
    if [[ ${#arr_keys_declare[@]} == 0 ]]; then
        press_enter "$FUNCNAME: Массив \"$1\" не существует или пуст!"
        return
    fi

    local sorted_arr_keys

    IFS=$'\n'
    sorted_arr_keys=($(sort <<< "${!arr_keys_declare[*]}"))
    IFS=${IFS_OLD}

    echo "${sorted_arr_keys[@]}"
}

function debug_menu_main {
    # Показ массива главного меню для отладки
    # Можно вставлять для отладки в разные места и передавать параметр $@
    # Например передавать $FUNCNAME для отображения имени вызывающей функции

    set +x
    clear
    echo -e "\n##### menu_main ($@):"

    for key in $(arr_keys_sort "main_menu"); do
        echo "${key}: ${main_menu[${key}]}"
    done

    press_enter "$FUNCNAME"
}

function debug_menu_current {
    # Показ массива текущего меню для отладки
    # Можно вставлять для отладки в разные места и передавать параметр $@
    # Например передавать $FUNCNAME для отображения имени вызывающей функции

    set +x
    echo -e "\n##### menu_current ($@):"

    for element in "${menu_current[@]}"; do
        echo "${element}"
    done

    press_enter "$FUNCNAME"
}

function replacement {
    [[ -z "$1" ]] && return

    local txt="$1"
    txt="${txt//${HOME}/'$HOME'}"
    txt="${txt//\/media\/${USER}/media}"

    # Обрезание строки
    local max_length=40
    [[ -n "$2" ]] && max_length="$2"

    if [ ${#txt} -gt $((max_length + 4)) ]; then
        txt="${txt:0:$((max_length / 2))}...${txt:(-$((max_length / 2)))}"
    fi

    echo "${txt}"
}

function vars_declare_local {
    # Для создания локальных переменных чеsрез echo в вызывающей функции
    # "$@" - имена массивов откуда брать переменные

    [[ -z "$@" ]] && return

    local result=''
    for name_vars_declare in "$@"; do
        declare -n arr_vars_declare="${name_vars_declare}"
        if [[ ${#arr_vars_declare[@]} == 0 ]]; then
            press_enter "$FUNCNAME: Массив \"${name_vars_declare}\" не существует или пуст!"
            continue
        fi

        for var_vars_declare in ${arr_vars_declare[@]}; do
            [[ -n "${result}" ]] && result+=' '
            result+='local '${var_vars_declare}
        done
    done

    echo -e "${result}"
}

function vars_init {
    # Установка значений поумолчанию записанных в массивах
    # Меняет переменные вызывающей функции!!!
    # Применять после vars_declare_local или другого объявления локальных переменных в вызывающей функции!
    # "$@" - имена массивов откуда брать переменные

    [[ -z "$@" ]] && return

    for name_vars_init in "$@"; do
        declare -n arr_vars_init="${name_vars_init}"
        if [[ ${#arr_vars_init[@]} == 0 ]]; then
            echo -e "\nМассив \"${name_vars_init}\" не существует или пуст!"
            press_enter "$FUNCNAME"
            continue
        fi

        for var_vars_init in ${arr_vars_init[@]}; do
            if [[ ${var_vars_init} == *'='* ]]; then eval ${var_vars_init}; else eval ${var_vars_init}=''; fi
        done
    done
}

function vars_set {
    # Устанавливает актуальные значения переменных
    # Меняет значения переменных в вызывающей функции!!!
    # "$@" - элементы массивов откуда брать значения переменных

    [[ -z "$@" ]] && return

    local element_vars_set
    for name_vars_set in "$@"; do
        element_vars_set="${!name_vars_set}"
        if [[ ${#element_vars_set[@]} == 0 ]]; then
            echo -e "\nМассив \"${name_vars_set}\" не существует или пуст!"
            press_enter "$FUNCNAME"
            continue
        fi

        for var in "${element_vars_set[@]}"; do [[ ${var} == *'='* ]] && eval ${var}; done
    done
}

function vars_test {
    # Вывод пременных и их значений для тестирования
    # Не применять если вызывающая функция возвращает что-то через echo!!!
    # "$@" - имена массивов откуда брать переменные

    [[ -z "$@" ]] && return

    for name_vars_test in "$@"; do
        echo -e "\nмассив переменных: ${name_vars_test}"
        declare -n arr_vars_test="${name_vars_test}"
        if [[ ${#arr_vars_test[@]} == 0 ]]; then
            echo -e "\nМассив \"${name_vars_test}\" не существует или пуст!"
            press_enter "$FUNCNAME"
            continue
        fi

        for var_vars_test in ${arr_vars_test[@]}; do
            [[ ${var_vars_test} == *'='* ]] && var_vars_test=${var_vars_test//'='*/''}
            echo "${var_vars_test}:${!var_vars_test}"
        done
    done

    press_enter "$FUNCNAME"
}

function archiver_file_ext {
    # Возвращает дополнительное расширение для файла
    # $@ - имя архиватора
    [[ -z "$@" ]] && return

    local extension
    if [[ -n "$(command -v $@)" ]]; then
        case "$@" in
            gzip)
                extension=".gz"
            ;;
            tar)
                extension="tar.gz"
            ;;
            7z)
                extension="7z"
            ;;
        esac
    fi

    echo "${extension}"
}

function archiver_dd_ext {
    # Возвращает дополнительное расширение для dd-файла
    # $@ - имя архиватора

    local return=".dd"
    [[ -n "$@" ]] && [[ -n "$(command -v $@)" ]] && return=".dd_$@"
    echo "${return}"
}

function archiver_params {
    # Возвращает архиватор с дополнительными параметрами (если нужны)
    # $@ - имя архиватора
    [[ -z "$@" ]] && return
    [[ -z "$(command -v $@)" ]] && return

    local params="$@"

    case "$@" in
        gzip)
            params="gzip"
        ;;
        tar)
            params='tar --exclude=lost+found --exclude=.Trash-* -zcvf'
        ;;
        7z)
            params='7z -xr!lost+found -xr!.Trash-* a'
        ;;
    esac

    echo "${params}"
}

function lsblk_show {
# дописать!
    local heade=''
    echo -e "$(lsblk -o NAME,PTUUID,PARTUUID,UUID,PTTYPE,TYPE,LABEL,SIZE,MODEL,VENDOR,SERIAL,HOTPLUG)"
    # for string in "$(lsblk -o NAME,PTUUID,PARTUUID,UUID,PTTYPE,TYPE,SIZE,MOUNTPOINT,HOTPLUG)"; do
    #     # if [ -z "${heade}" ]; then
    #     #     heade=$(echo ${string})
    #     #     echo "${heade}"
    #     #     continue
    #     # fi
    #     echo -e "${string}"
    # done
    press_enter
}

function disk_name {
    # Возвращает имя диска
    # "$@" - id или uuid диска
    # переписать через lsblk ?

    [[ -z "$@" ]] && return

    local disk_name="$(ls -l /dev/disk/by-uuid/ | grep -w "$@" | awk '{print $11}')"
    [[ -z "${disk_name}" ]] && disk_name="$(ls -l /dev/disk/by-id/ | grep -w "$@" | head -n 1 | awk '{print $11}')"
    disk_name=${disk_name//..\//''}

    echo "${disk_name}"
}

function disk_parent_name {
    # Возвращает имя "родительского" диска
    # "$@" - имя диска

    [[ -z "$@" ]] && return

    local disk_name="$@"
    local var="$(lsblk -n -o TYPE /dev/${disk_name} 2> /dev/null)"

    if [ -z "${var}" ]; then
        echo -e "Ошибка параметра: \"$@\"!"
        press_enter "$FUNCNAME"
        return
    fi

    if [ "${var}" == 'part' ]; then
        disk_name=''
        var=$(lsblk -n -o PTUUID /dev/$@)
        [[ -n "${var}" ]] && disk_name=$(lsblk -n -o NAME,TYPE,PTUUID | grep -w ${var} | grep -m1 'disk' | awk '{print $1}')
    fi

    if [ -z "${disk_name}" ]; then
        echo -e "Ошибка определения имени диска: \"$@\"!"
        press_enter "$FUNCNAME"
    fi

    echo "${disk_name}"
}

function disk_mounted {
    # Возвращает точку монтирования
    # "$@" - uuid

    [[ -z "$@" ]] && return

    local disk_name="$(disk_name $@)"
    [[ -z "${disk_name}" ]] && return

    echo "$(lsblk -n -l -o MOUNTPOINT /dev/${disk_name})"
}

function disk_hotplug {
    # Проверка что диск "извлекаемый"
    # $@ - имя диска

    [[ -z "$@" ]] && return

    local disk_hotplug="$(lsblk -n -d -l -o HOTPLUG /dev/$@ 2> /dev/null)"

    if [ -z "${disk_hotplug}" ]; then
        echo -e "Ошибка параметра: \"$@\"!"
        press_enter "$FUNCNAME"
        return
    fi

    disk_hotplug=${disk_hotplug// /''}
    echo "${disk_hotplug}"
}

function disk_info {
    # Создает файлы с разной информацией по диску
    # использую sudo !!!
    # "$1" - имя диска
    # "$2" - куда писать файл с информацией
    # "$3" - префикс файла

    local disk_name="$(disk_parent_name "$1")"
    local full_path="$2"
    local prefix="$3"
    local ptuid

    [[ -z "${disk_name}" ]] && return
    [[ -z "${full_path}" ]] && return

    if [[ ! -d "${full_path}" ]]; then
        echo -e "Не найден каталог: \"${full_path}\"!"
        press_enter "$FUNCNAME"
        return
    fi

    [[ -z "${prefix}" ]] && prefix="Info_"
    local name_file="${prefix}$(date +%F).txt"

    echo "sfdisk ${disk_name}:" > ${full_path}/${name_file}
    echo '-----------' >> ${full_path}/${name_file}
    sudo sfdisk -d /dev/${disk_name} >> ${full_path}/${name_file}

    for var in "by-id"  "by-uuid" "by-label" "by-partuuid"; do
        echo -e "\nls -l ${var}:" >> ${full_path}/${name_file}
        echo '------' >> ${full_path}/${name_file}
        ls -l /dev/disk/${var}/ | grep "${disk_name}" | awk '{print $9, $10, $11}' >> ${full_path}/${name_file}
    done

    echo -e "\nlsblk:" >> ${full_path}/${name_file}
    echo '------' >> ${full_path}/${name_file}
    lsblk -o NAME,PTUUID,PARTUUID,UUID,PTTYPE,TYPE,LABEL,SIZE,MODEL,VENDOR,SERIAL /dev/${disk_name} >> ${full_path}/${name_file}

    if [[ "${disk_name}" == mmc* ]]; then
        if [ -n "$()command -v mmc" ]; then
            name_file="${prefix}$(date +%F).info"
            sudo mmc extcsd read /dev/${disk_name} > ${full_path}/${name_file}
        fi
    else
        if [ -n "$(whereis smartctl | awk '{print $2}')" ]; then
            name_file="${prefix}$(date +%F).smart"
            sudo smartctl -a /dev/${disk_name} > ${full_path}/${name_file}
            [[ -n $(grep "Unknown USB bridge" ${full_path}/${name_file}) ]] && rm ${full_path}/${name_file}
        fi
    fi
}

function disk_size {
    # SIZE  размер файловой системы
    # FSAVAIL  доступный размер файловой системы
    # FSUSED  использованный размер файловой системы
    # FSUSE%  использование файловой системы в процентах
    # "$@" - имя диска

    [[ -z "$@" ]] && return

    local column
    case "$2" in
        FSAVAIL)
            column="FSAVAIL"
        ;;
        FSUSED)
            column="FSUSED"
        ;;
        *)
            column="SIZE"
        ;;
    esac

    local human=''
    [[ -z "$3" ]] && human='-b'

    local disk_size
    if [ -z $(ls -1 /dev/ | grep -Fx "$1") ]; then
        press_enter "$FUNCNAME: Не найден диск \"$1\"!"
        return
    else
        disk_size=$(lsblk -dn ${human} -o ${column} /dev/$1)
    fi

    echo "${disk_size//' '/''}"
}

function del_old_template_dirs {
    # Удаляет каталоги с шаблоном в имени ${template}
    # "$@" - строчка с параметрами

    local full_path
    # count - количество оставляемых файлов
    local count
    local prefix
    local suffix

    for var in "$@"; do [[ ${var} == *'='* ]] && eval ${var}; done

    [[ -d "${full_path}" ]] || return
    [[ -z "${count}" ]] || [[ "${count}" -lt "1" ]] && return

    local dirs="$(ls -1rd ${full_path}/${prefix}${template}${suffix} 2> /dev/null)"
    if [ -n "${dirs}" ]; then
        for dir in ${dirs[@]}; do
            [[ -d "${dir}" ]] || continue

            let "count -= 1"
            if [ "${count}" -lt "0" ]; then
                rm -R ${dir}
                if [ $? -ne 0 ]; then
                    whiptail_msgbox "Ошибка удаления каталога:\n${dir}"
                fi
            fi
        done
    fi
}

function del_old_template_files {
    # Удаляет файлы с шаблоном в имени ${template}
    # "$@" - строчка с параметрами

    local full_path
    # count - количество оставляемых файлов
    local count
    local prefix
    local suffix

    for var in "$@"; do [[ ${var} == *'='* ]] && eval ${var}; done

    [[ -d "${full_path}" ]] || return
    [[ -z "${count}" ]] || [[ "${count}" -lt "1" ]] && return

    local files=$(ls -1r ${full_path}/${prefix}${template}${suffix} 2> /dev/null)
    if [ -n "${files}" ]; then
        for file in ${files[@]}; do
            [[ -f "${file}" ]] || continue

            let "count -= 1"
            if [ "${count}" -lt "0" ]; then
                rm ${file}
                if [ $? -ne 0 ]; then
                    whiptail_msgbox "Ошибка удаления файла:\n${file}"
                fi
            fi
        done
    fi
}

# Функции для меню
function next_menu_index {
    # Возвращает следующий индекс для пункта меню
    # "$@" - предыдущий или "родительчкий" индекс

    local index="$@"
    local next_index=0
    local length="${#index}"
    local var=''
    local next_menu_index=''

    if [ ${#main_menu[@]} -eq 0 ]; then
        next_menu_index="1"
    elif [[ -z ${index} ]]; then
        # индекс первого уровня
        for key in ${!main_menu[@]}; do
            (( ${key:0:1} > next_index )) && next_index=${key:0:1}
        done
        next_menu_index="$((next_index + 1))"
    else
        for key in ${!main_menu[@]}; do
            if [[ ${key} == ${index}* ]]; then
                var=${key:0:$((length + 2))}
                if [ ${#var} -gt ${length} ]; then
                    (( ${var: -1} > next_index )) && next_index=${var: -1}
                fi
            fi
        done

        if [ -z "${var}" ]; then
            # "родительский" индекс не найден!
            for key in ${!main_menu[@]}; do
                (( ${key:0:1} > next_index )) && next_index=${key:0:1}
            done
            next_menu_index="$((next_index + 1))"
        else
            next_menu_index="${index}.$((next_index + 1))"
        fi
    fi

    echo "${next_menu_index}"
}

function menu_name {
    # Возвращает имя пункта меню

    [[ -z "$@" ]] && return
    echo "$(echo "$@" | awk -F'=' '{print $1}')"
}

function menu_function {
    # Возвращает имя функции для вызова, что назначена пункту меню

    [[ -z "$@" ]] && return
    echo "$(echo "$@" | awk -F'=' '{print $2}')"
}

function menu_rebuilding {
    # Для частичного перестроения главного меню (main_menu)
    # Вызывается команда ("$2") и после завершения работы команды
    # вызывается функция создания меню в дополнительных скриптах
    # с передачей индекса в эту функцию
    # Использует ${breadcrumbs[-1]} массива скрипта BackUpData.bash!!!
    # "$1" - имя функции меню для пересоздания
    # "$2" - команда перед вызовом функции меню
    # "$3" - каталог где запускать команду

    [[ -z "$1" ]] && return
    if [[ ! $(declare -F "$1") ]]; then
        echo -e "\nНе найдена функция \"$1\"!"
        press_enter "$FUNCNAME"
        return
    fi

    if [ -n "$3" ] && ! [ -d "$3" ]; then
        echo -e "\nНе найден каталог \"$3\"!"
        press_enter "$FUNCNAME"
        return
    fi

    if [ -n "$2" ]; then
        if [[ -z "$(command -v "$2")" ]]; then
            cd "$3"
            bash
        else
            case "$2" in
                mc)
                    mc "$3"
                ;;
                ls)
                    ls -l "$3"
                    press_enter
                ;;
                bash)
                    cd "$3"
                    bash
                ;;
            esac
        fi
    fi

    [[ -n "${breadcrumbs[-1]}" ]] && "$1" "${breadcrumbs[-1]:0:1}"
}

#=================================
# Загрузка дополнительных скриптов
#=================================
[[ -f "${Inclusions}/BackUpDir.bash" ]] && . ${Inclusions}/BackUpDir.bash
[[ -f "${Inclusions}/BackUpFile.bash" ]] && . ${Inclusions}/BackUpFile.bash
[[ -f "${Inclusions}/DdDisk.bash" ]] && . ${Inclusions}/DdDisk.bash
[[ -f "${Inclusions}/DdFlash.bash" ]] && . ${Inclusions}/DdFlash.bash
# [[ -f "${Inclusions}/RestoreDisk.bash" ]] && . ${Inclusions}/RestoreDisk.bash
# [[ -f "${Inclusions}/RestoreFlash.bash" ]] && . ${Inclusions}/RestoreFlash.bash
# [[ -f "${Inclusions}/Config.bash" ]] && . ${Inclusions}/Config.bash
# [[ -f "${Inclusions}/Help.bash" ]] && . ${Inclusions}/Help.bash

#===============================================================
# Главный ассоциативный массив имен меню и их вызываемых функций
#===============================================================
# Главное меню
declare -A main_menu

[[ $(declare -F backup_dir_menu) ]] && backup_dir_menu
[[ $(declare -F backup_file_menu) ]] && backup_file_menu
[[ $(declare -F dd_disk_menu) ]] && dd_disk_menu
[[ $(declare -F dd_flash_menu) ]] && dd_flash_menu
# [[ $(declare -F restore_disk_menu) ]] && restore_disk_menu
# [[ $(declare -F restore_flash_menu) ]] && restore_flash_menu

# для отладки
# -----------
[[ "$@" == *'lsblk_show'* ]] && [[ $(declare -F lsblk_show) ]] && lsblk_show
[[ "$@" == *'debug_menu_main'* ]] && debug_menu_main

# Текущий массив меню
declare -a menu_current

# Массив "хлебные крошки"
declare -a breadcrumbs

# для возвращения к выбранному пункту меню
default_item=''

#==============
# Основной цикл
#==============
while true; do
    clear

    main_item=''
    unset menu_current
    height=1
    width=${#title_scr}
    width_max=60

    while [ ${#breadcrumbs[@]} -gt 0 ]; do
        if [ -z "${main_menu[${breadcrumbs[-1]}]}" ]; then
            unset breadcrumbs[-1]
        else
            break
        fi
    done

    if [ ${#breadcrumbs[@]} = 0 ]; then
        prefix=''
        header="Главное меню"
    else
        prefix="${breadcrumbs[-1]}."
        header="$(menu_name ${main_menu[${breadcrumbs[-1]}]})"
    fi

    (( ${#header} > width )) && width=${#header}

    for ((i=1; i <= ${#main_menu[@]}; i++)); do
        if [ -n "${main_menu["${prefix}${i}"]}" ]; then
            main_item="$(menu_name ${main_menu["${prefix}${i}"]})"
            menu_current=("${menu_current[@]}" "${i}" "${main_item}")
            (( height++ ))
            (( ${#main_item} > width )) && width=${#main_item}
        fi
    done

    if [ ${#menu_current[@]} -eq 0 ]; then
        if [ ${#main_menu[@]} -eq 0 ]; then
            whiptail_msgbox "Главный массив меню пуст!"
            break
        else
            whiptail_msgbox "Массив меню:\n\"${header}\"\nпуст!"
        fi
    fi

    tag=$(whiptail --title "${title_scr}" --menu "${header}" --default-item "${default_item}" $(( height + 8 )) $(( width + 16 )) ${height} "${menu_current[@]}" 3>&1 1>&2 2>&3)

    if [ $? -eq 0 ];  then
        default_item=${tag}
        menu_function="$(menu_function ${main_menu["${prefix}${tag}"]})"
        if [ -z "${menu_function}" ]; then
            whiptail_msgbox "Нет функции для меню:\n$(menu_name ${main_menu["${prefix}${i}"]})"
            continue
        elif [ "${menu_function}" == 'show_menu' ]; then
            default_item=''
            breadcrumbs+=("${prefix}${tag}")
        else
            # запуск дочернего процесса
            # [[ "${menu_function}" != 'empty' ]] && (${menu_function})

            # без запуска дочернего процесса?
            [[ "${menu_function}" != 'empty' ]] && ${menu_function}
        fi
    else
        if [ ${#breadcrumbs[@]} == 0 ]; then
            break
        else
            default_item="${breadcrumbs[-1]: -1}"
            unset breadcrumbs[-1]
        fi
    fi
done

exit 0

В принципе в самом скрипте есть пояснения.
Но есть существенные моменты:

  1. Скрипт НЕ создает меню! Пункты и подпункты меню создают подключаемые скрипты через заполнение массива главного меню (см. пример выше вызова BackUpData с параметром). Однако индексы массива главного меню формируются в этом скрипте и выдаются запросившему!
  2. Скрипт НЕ меняет массив главного меню! Он обеспечивает визуализацию меню/подменю («хлебные крошки») и запуск нужных функций или команд.
  3. Изменения массива главного меню могут делать только подключаемые скрипты! И только «своих» пунктов/подпунктов (если правильно написан подключаемый скрипт)!

Подключаемый скрипт BackUpDir

BackUpDir.bash

BackUpDir.bash

# =======
# Массивы
# =======
[[ -f "${ScriptCfg}/BackUpDir.arrays" ]] || return
. ${ScriptCfg}/BackUpDir.arrays

# локальные переменные для функций в этом скрипте
backup_dir_vars=("label" "dir_size")

# ======================
# Дополнительные функции
# ======================
function backup_dir_menu {
    [[ ${#backup_dir_to_vars[@]} == 0 ]] && return
    [[ ${#backup_dir_from[@]} == 0 ]] && return
    [[ ${#backup_dir_to[@]} == 0 ]] && return

    clear
    echo "Подготовка меню (модуль $FUNCNAME)..."

    local menu_name=" Создание архивных копий каталогов (всего каталогов: ${#backup_dir_from[@]})=show_menu"
    local menu_index

    if [ -z "$@" ]; then
        menu_index="$(next_menu_index)"
        main_menu["${menu_index}"]="${menu_name}"
    else
        menu_index="$@"
        for key in ${!main_menu[@]}; do
            [[ "${key}" == "${menu_index}."* ]] && unset main_menu[${key}]
        done
    fi

    local submenu_index
    local submenu_name
    local last_backup

    $(vars_declare_local "backup_dir_to_vars" "backup_dir_from" backup_dir_to)

    for key_dir_from in $(arr_keys_sort "backup_dir_from"); do
        vars_init "backup_dir_to_vars"
        vars_set "backup_dir_from["${key_dir_from}"]"
        # vars_test "backup_dir_to_vars"

        submenu_index="$(next_menu_index ${menu_index})"

        if [ ! -d "${from}/${key_dir_from}" ]; then
            submenu_name="Не найден каталог \"$(replacement "${from}")/${key_dir_from}\""
            main_menu["${submenu_index}"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
            continue
        fi

        dir_size=$(/usr/bin/du --exclude='*lost+found' --exclude='*.Trash-*' -sh ${from}/${key_dir_from}/ | awk '{print $1}')
        submenu_name="Каталог \"$(replacement "${from}")/${key_dir_from}\" (${dir_size})"

        for key_dir_to in $(arr_keys_sort "backup_dir_to"); do
            [[ "${key_dir_to}" == ${key_dir_from}:* ]] || continue

            vars_init "backup_dir_to_vars"
            vars_set "backup_dir_to["${key_dir_to}"]"

            [[ -z "${main_menu["${submenu_index}"]}" ]] && main_menu["${submenu_index}"]="${submenu_name}=show_menu"

            to_for_menu="${to:0:14}...$(echo "${to}" | sed 's/.*\('${key_dir_to#*:*}'.*\).*/\1/')"
            if [ ! -d "${to}" ]; then
                submenu_name="Не найден каталог \"$(replacement "${to_for_menu}")\""
                main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                continue
            fi

            if [ -z "${archivator}" ]; then
                if [ -d "${to}/${prefix}_$(date +%F)" ]; then
                    submenu_name="Показать каталог с архивом за сегодня (\"$(replacement "${to_for_menu}")\")"
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" "mc" "${to}""
                else
                    last_backup="$(find "${to}" -type d -iname "${prefix}_${template}" | sort | tail -1)"
                    if [[ -n  "${last_backup}" ]]; then
                        last_backup=" (последний: ${last_backup##*/})"
                        last_backup=${last_backup//${prefix}_/''}
                    fi

                    submenu_name="Создать архив в \"$(replacement "${to_for_menu}")\" (свободно: "$(/usr/bin/df -h ${to} | tail -n1 | awk '{print $4}')")"${last_backup}""
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=backup_dir_run "${key_dir_from}" "${key_dir_to}""
                fi
            else
                if [ -f "${to}/${prefix}_$(date +%F).$(archiver_file_ext ${archivator})" ]; then
                    submenu_name="Показать каталог с ${archivator}-архивом за сегодня (\"$(replacement "${to_for_menu}")\")"
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" "mc" "${to}""
                else
                    last_backup="$(find "${to}" -type f -iname "${prefix}_${template}.$(archiver_file_ext ${archivator})" | sort | tail -1)"
                    if [[ -n  "${last_backup}" ]]; then
                        last_backup=" (последний: ${last_backup##*/})"
                        last_backup=${last_backup//${prefix}_/''}
                        last_backup=${last_backup//.$(archiver_file_ext ${archivator})/''}
                    fi

                    submenu_name="Создать \"${archivator}\"-архив в \"$(replacement "${to_for_menu}")\" (свободно: "$(/usr/bin/df -h ${to} | tail -n1 | awk '{print $4}')")"${last_backup}""
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=backup_dir_run "${key_dir_from}" "${key_dir_to}""
                fi
            fi
        done
    done
}

function backup_dir_run {
    # "$1" - ключ массива backup_dir_from
    # "$2" - ключ массива backup_dir_to
    clear

    $(vars_declare_local "backup_dir_vars" "backup_dir_to_vars" backup_dir_to_vars)
    vars_set "backup_dir_from["$1"]" "backup_dir_to["$2"]"

    local name="${prefix}_$(date +%F)"

    local dir_size="$(/usr/bin/du --exclude='*lost+found' --exclude='*.Trash-*' -sh ${from}/$1/ | awk '{print $1}')"
    local archiver_params="$(archiver_params ${archivator})"

    if [ -z "${archiver_params}" ]; then
        echo -e "Копирование \"$(replacement "${from}")/$1\" (${dir_size})\n в \"$(replacement "${to}" 60)/${prefix}_$(date +%F)\"\n"

        mkdir -p ${to}/${prefix}_$(date +%F)
        if [ -d ${to}/${prefix}_$(date +%F) ]; then
            cp -rT -P "${from}/$1" "{to}/${prefix}_$(date +%F)"
        else
            whiptail_msgbox "Ошибка создания каталога \"$(replacement "${to}" 60)/${prefix}_$(date +%F)\""
        fi
    else
        echo -e "Архивирование \"$(replacement "${from}")/$1\" (${dir_size})\n в \"$(replacement "${to}" 60)/${prefix}_$(date +%F).$(archiver_file_ext ${archivator})\"\n"
        cd ${from}
        ${archiver_params} ${to}/${prefix}_$(date +%F).$(archiver_file_ext ${archivator}) "$1"
    fi

    if [ $? -eq 0 ];  then
        clear
        if [ -z "${archiver_params}" ]; then
            del_old_template_dirs "full_path=${to}" "count=${save_copies}" "prefix=${prefix}_"
        else
            del_old_template_files "full_path=${to}" "count=${save_copies}" "prefix=${prefix}_" "suffix=.$(archiver_file_ext ${archivator})"
        fi
        menu_rebuilding "backup_dir_menu"
    else
        # whiptail_msgbox "Ошибка архивирования каталога \"${prefix}_$(date +%F)\""
        press_enter "Ошибка \"$?\" архивирования каталога \"${prefix}_$(date +%F)\" в \"$(replacement "${to}" 60)\""
        menu_rebuilding "backup_dir_menu" "mc" "${to}"
    fi
}

BackUpDir.arrays

BackUpDir.arrays

# допустимые значения "archivator": gzip, tar, 7z
backup_dir_to_vars=("prefix=${HOSTNAME}" "to" "save_copies=3" "archivator=7z")
declare -A backup_dir_from
declare -A backup_dir_to

media_rp4='Network/Home/WorkStations/nik-rp4/media'

BackUp="${HOME}/Network/Home/WorkStations/nik-rp4/media/BackUp"
[[ -d "/media/${USER}/BackUp" ]] && BackUp="/media/${USER}/BackUp"

case "${HOSTNAME}" in
    asus-x55c)
        backup_dir_from["SoftForNik"]="from=${HOME}/Work/HD"
        backup_dir_to["SoftForNik:BackUp"]="to=${BackUp}/Data/HD/SoftForNik"

        backup_dir_from["TC"]="from=${HOME}/Work"
        backup_dir_to["TC:BackUp"]="to=${BackUp}/Data/TC save_copies=10"
    ;;
esac


Подключаемый скрипт BackUpFile

BackUpFile.bash

BackUpFile.bash

# =======
# Массивы
# =======
[[ -f "${ScriptCfg}/BackUpFile.arrays" ]] || return
. ${ScriptCfg}/BackUpFile.arrays

# локальные переменные для функций в этом скрипте
backup_file_vars=("label" "file_size")

# ======================
# Дополнительные функции
# ======================
function backup_file_menu {
    [[ ${#backup_file_files_vars[@]} == 0 ]] && return
    [[ ${#backup_file_files[@]} == 0 ]] && return

    [[ ${#backup_file_to_vars[@]} == 0 ]] && return
    [[ ${#backup_file_to[@]} == 0 ]] && return

    clear
    echo "Подготовка меню (модуль $FUNCNAME)..."

    local menu_name=" Создание архивных копий файлов (всего файлов: ${#backup_file_files[@]})=show_menu"
    local menu_index

    if [ -z "$@" ]; then
        menu_index="$(next_menu_index)"
        main_menu["${menu_index}"]="${menu_name}"
    else
        menu_index="$@"
        for key in ${!main_menu[@]}; do
            [[ "${key}" == "${menu_index}."* ]] && unset main_menu[${key}]
        done
    fi

    local submenu_index
    local submenu_name

    local last_backup

    $(vars_declare_local "backup_file_vars" "backup_file_files_vars" backup_file_to_vars)

    for key_file_files in $(arr_keys_sort "backup_file_files"); do
        vars_init "backup_file_files_vars"
        vars_set "backup_file_files["${key_file_files}"]"
        # vars_test "backup_file_files_vars"

        if [ -f "${from}/${name}" ]; then
            file_size="$(ls -sh ${from}/${name} | awk '{print $1}')"
        fi

        submenu_index="$(next_menu_index ${menu_index})"
        submenu_name="Файл \"${name}\" (${file_size})"

        for key_file_to in $(arr_keys_sort "backup_file_to"); do
            [[ "${key_file_to}" == ${key_file_files}:* ]] || continue

            vars_init "backup_file_to_vars"
            vars_set "backup_file_to["${key_file_to}"]"

            if [[ ! -d "${from}" ]] && [[ "${from}" == "/media/${USER}"* ]]; then
                label="${from//\/media\/${USER}\/''}"
                label=${label//\/*/''}

                [[ ! -d "/media/${USER}/${label}" ]] && continue 2
            fi

            [[ -z "${main_menu["${submenu_index}"]}" ]] && main_menu["${submenu_index}"]="${submenu_name}=show_menu"

            if [[ ! -d "${from}" ]]; then
                if [[ "${from}" == "/media/${USER}"* ]]; then
                    label="${from//\/media\/${USER}\/''}"
                    label=${label//\/*/''}

                    submenu_name="Не найден каталог \"$(replacement "${from}")\/${label}\//''}\" на \"${label}\""
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" mc "/media/${USER}/${label}""
                    continue 2
                else
                    submenu_name="Не найден каталог \"$(replacement "${from}")\""
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                    continue 2
                fi
            fi

            if [ ! -f "${from}/${name}" ]; then
                submenu_name="Не найден файл в каталоге \"$(replacement "${from}")\""
                main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                continue 2
            fi

            to_for_menu="${to:0:14}...$(echo "${to}" | sed 's/.*\('${key_file_to#*:*}'.*\).*/\1/')"
            if [[ ! -d "${to}" ]]; then
                if [[ "${to}" == "/media/${USER}"* ]]; then
                    label="${to//\/media\/${USER}\/''}"
                    label=${label//\/*/''}

                    if [ -d "/media/${USER}/${label}" ]; then
                        submenu_name="Не найден каталог \"$(replacement "${to_for_menu}")\/${label}\//''}\" на \"${label}\""
                        main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" mc "/media/${USER}/${label}""
                    else
                        submenu_name="Диск \"${label}\" не подключен"
                        main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                    fi
                    continue
                else
                    submenu_name="Не найден каталог \"$(replacement "${to_for_menu}")\""
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                    continue
                fi
            fi

            last_backup="$(find "${to}/" -type f -iname "${name%.*}_${template}.${name#*.}$(archiver_file_ext ${archivator})" | sort | tail -1)"

            if [[ -n  "${last_backup}" ]]; then
                last_backup=" (последний: ${last_backup##*/})"
                last_backup=${last_backup//${name%.*}_/''}
                last_backup=${last_backup//$(archiver_file_ext ${archivator})/''}
                last_backup=${last_backup//.${name#*.}/''}
            fi

            if [ -f "${to}/${name%.*}_$(date +%F).${name#*.}$(archiver_file_ext ${archivator})" ]; then
                submenu_name="Показать каталог с архивом за сегодня в \"$(replacement "${to_for_menu}")\""
                main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" "mc" "${to}""
            else
                submenu_name="Создать архив в \"$(replacement "${to_for_menu}")\" (свободно: "$(/usr/bin/df -h ${to} | tail -n1 | awk '{print $4}')")"${last_backup}""
                main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=backup_file_run "${key_file_files}" "${key_file_to}""
            fi
        done
    done
}

function backup_file_run {
    # "$1" - ключ массива backup_file_files
    # "$2" - ключ массива backup_file_to
    clear

    $(vars_declare_local "backup_file_vars" "backup_file_files_vars" backup_file_to_vars)
    vars_set "backup_file_files["$1"]" "backup_file_to["$2"]"

    file_size="$(ls -sh ${from}/${name} | awk '{print $1}')"
    local archiver_params="$(archiver_params ${archivator})"

    if [ -z "${archiver_params}" ]; then
        echo -e "Копирование \"${name}\" (${file_size})\n в \"$(replacement "${to}" 60)\"\n"
        if [ -n "${PvInstalled}" ]; then
            pv ${from}/${name} > ${to}/${name%.*}_$(date +%F).${name#*.}
        else
            cp ${from}/${name} ${to}/${name%.*}_$(date +%F).${name#*.}
        fi
    else
        echo -e "Архивирование \"${name}\" (${file_size})\n в \"$(replacement "${to}" 60)\"\n"
        if [ -n "${PvInstalled}" ]; then
            pv ${from}/${name} | ${archiver_params} > ${to}/${name%.*}_$(date +%F).${name#*.}$(archiver_file_ext ${archivator})
        else
            cat ${from}/${name} | ${archiver_params} > ${to}/${name%.*}_$(date +%F).${name#*.}$(archiver_file_ext ${archivator})
        fi
    fi

    if [ $? -eq 0 ];  then
        clear
        del_old_template_files "full_path=${to}" "count=${save_files}" "prefix=${name%.*}_" "suffix=.${name#*.}$(archiver_file_ext ${archivator})"
        menu_rebuilding "backup_file_menu" "mc" "${to}"
    else
        whiptail_msgbox "Ошибка \"$?\" архивирования файла \"${name}\" в \"$(replacement "${to}")\""
        menu_rebuilding "backup_file_menu" "mc" "${to}"
    fi
}

BackUpFile.arrays

BackUpFile.arrays

# допустимые значения "archivator": gzip, tar, 7z
backup_file_files_vars=("name" "from" "archivator=gzip")
declare -A backup_file_files

backup_file_to_vars=("to" "save_files=3")
declare -A backup_file_to

BackUp="${HOME}/Network/Home/WorkStations/nik-rp4/media/BackUp"
[[ -d "/media/${USER}/BackUp" ]] && BackUp="/media/${USER}/BackUp"

case "${HOSTNAME}" in
    asus-x55c)
        backup_file_files["w7-64.qcow2"]="name=w7-64.qcow2 from=${HOME}/Work/HD/Virt-Storage/AQEMU"
        backup_file_to["w7-64.qcow2:BackUp"]="to=${BackUp}/Virt-Storage/${HOSTNAME}/AQEMU"
    ;;
esac


Подключаемый скрипт DdDisk

DdDisk.bash

DdDisk.bash

############################
# Dd-образы диска (под sudo)
############################

if [ -z "$(sudo -l | grep "NOPASSWD: ALL")" ]; then
    echo "Для модуля \"Создание образов локальных дисков\""
    echo "необходимы повышенные права пользователя \"$USER\"!"
    echo "Для этого можно добавить в файл \"/etc/sudoers\" или"
    echo "создать в каталоге \"/etc/sudoers.d\" любой файл"
    echo "и добавить в него строчку ниже:"
    echo "$USER $HOSTNAME=NOPASSWD: ALL"
    press_enter
    return
fi

# =======
# Массивы
# =======
[[ -f "${ScriptCfg}/DdDisk.arrays" ]] || return
. ${ScriptCfg}/DdDisk.arrays

# локальные переменные для функций в этом скрипте
dd_disks_vars=("dir_free" "disk_size" "disk_name" "dd_ext")

# ======================
# Дополнительные функции
# ======================
function dd_disk_menu {
    [[ ${#dd_disks_init[@]} == 0 ]] && return
    [[ ${#dd_disks_to[@]} == 0 ]] && return
    [[ ${#dd_disks_from[@]} == 0 ]] && return

    clear
    echo "Подготовка меню (модуль $FUNCNAME)..."

    local menu_name="Создание образов внутренних дисков=show_menu"
    local menu_index
    local submenu_index
    local submenu_name

    local last_backup

    if [ -z "$@" ]; then
        menu_index="$(next_menu_index)"
        main_menu["${menu_index}"]="${menu_name}"
    else
        menu_index="$@"
        for key in ${!main_menu[@]}; do
            [[ "${key}" == "${menu_index}."* ]] && unset main_menu[${key}]
        done
    fi

    $(vars_declare_local "dd_disks_vars" "dd_disks_init")

    for key_disk_to in $(arr_keys_sort "dd_disks_to"); do
        vars_set "dd_disks_to["${key_disk_to}"]"
        # vars_test "dd_disks_vars" "dd_disks_init"

        [[ -z  "${main_menu["${menu_index}"]}" ]] && main_menu["${menu_index}"]="${menu_name}"

        if [ ! -d "${to}" ]; then
            submenu_index="$(next_menu_index ${menu_index})"
            submenu_name="Не найден каталог \"$(replacement "${to}")\""
            main_menu["${submenu_index}"]="${submenu_name}=empty"
            continue
        else
            submenu_index="$(next_menu_index ${menu_index})"
            dir_free="$(/usr/bin/df -h ${to} | tail -n1 | awk '{print $4}')"
            submenu_name="Создание образов в \"$(replacement "${to}")\" (свободно: "${dir_free}")"
            main_menu["${submenu_index}"]="${submenu_name}=show_menu"

            for key_disk_from in $(arr_keys_sort "dd_disks_from"); do
                if [[ ${key_disk_from} == ${key_disk_to}:* ]]; then
                    vars_init "dd_disks_init"
                    vars_set "dd_disks_from[${key_disk_from}]"
                    # vars_test "dd_disks_init"

                    disk_name="$(disk_name "${id_or_uuid}")"
                    if [ -z "${disk_name}" ]; then
                        submenu_name="Не найден диск \"${key_disk_from//${key_disk_to}':'/''}\""
                        main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}==empty"
                        continue
                    else
                        # на "asus-e410ma" это не сработало!
                        # if [ "$(disk_hotplug ${disk_name})" -ne "0" ]; then
                        #     submenu_name="диск \"${disk_name}\" не является внутренним"
                        #     main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=empty"
                        #     continue
                        # fi

                        if [ -n "disk_mounted ${id_or_uuid}" ]; then
                            if [ "$(/usr/bin/df ${to} | tail -n1)" == "$(/usr/bin/df /dev/$disk_name | tail -n1)" ]; then
                                submenu_name="Попытка создания образа \"${disk_name}\" в \"$(replacement "${to}")\" на диске \"${disk_name}\""
                                main_menu["${submenu_index}"]="${submenu_name}=empty"
                                continue
                            fi
                        fi

                        if [ -d "${to}/${save_dir}" ]; then
                            disk_size="$(disk_size ${disk_name} 'SIZE' 'human')"
                            dd_ext="$(archiver_dd_ext ${archivator})"

                            last_backup="$(find "${to}/${save_dir}/" -type f -iname "${disk_name}_${template}${dd_ext}" | sort | tail -1)"
                            if [[ -n  "${last_backup}" ]]; then
                                last_backup=" (последний: ${last_backup##*/})"
                                last_backup=${last_backup//${disk_name}_/''}
                                last_backup=${last_backup//${dd_ext}/''}
                            fi

                            if [ -f "${to}/${save_dir}/${disk_name}_$(date +%F)${dd_ext}" ]; then
                                submenu_name="Показать каталог с образом \"${disk_name}\" за сегодня в \"$(replacement "${to}")\""
                                main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" "mc" "${to}/${save_dir}""
                            else
                                submenu_name="Создать образ \"${disk_name}\" (${disk_size}) в \"$(replacement "${to}")\""${last_backup}""
                                main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=dd_disk_run "${key_disk_to}" "${key_disk_from}""
                            fi
                        else
                            submenu_name="Каталог \"${save_dir}\" не найден в \"$(replacement "${to}")\""
                            main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" "mc" "${to}""
                        fi
                    fi
                fi
            done
        fi
    done
}

function dd_disk_run {
    clear

    local key_disk_to="$1"
    local key_disk_from="$2"

    $(vars_declare_local "dd_disks_vars" "dd_disks_init")
    vars_set "dd_disks_to["${key_disk_to}"]" "dd_disks_from[${key_disk_from}]"
    # vars_test "dd_disks_init"

    disk_name="$(disk_name "${id_or_uuid}")"
    [[ -z "${disk_name}" ]] && return

    local prefix="$(disk_parent_name ${disk_name})_"
    disk_info ${disk_name} "${to}/${save_dir}" "${prefix}"

    dd_ext="$(archiver_dd_ext ${archivator})"
    local dd_if="/dev/${disk_name}"
    local dd_of="${to}/${save_dir}/${disk_name}_$(date +%F)${dd_ext}"

    disk_size="$(disk_size ${disk_name} 'SIZE' 'human')"
    echo -e "Создание \"${dd_ext}\"-образа диска \"${disk_name}\" (${disk_size})\n в \"$(replacement "${to}" 60)\"\n"
    disk_size=$(disk_size ${disk_name})
    dir_free="$(/usr/bin/df -B1 ${to} | tail -n1 | awk '{print $4}')"

    if [ "$(( disk_size * 12 / 10 ))" -gt "${dir_free}" ]; then
        echo "Мало свободного места на \"$(replacement "${to}" 60)\"!"
        press_enter "$FUNCNAME"
        return
    fi

    local archiver_params="$(archiver_params ${archivator})"
    if [ -z "${archiver_params}" ]; then
        if [ -n "${PvInstalled}" ] && [ -n "${disk_size}" ]; then
            sudo dd bs=1M if=${dd_if} | pv -s ${disk_size} > "${dd_of}"
        else
            sudo dd bs=1M status=progress if=${dd_if} of="${dd_of}"
        fi
    else
        if [ -n "${PvInstalled}" ] && [ -n "${disk_size}" ]; then
            sudo dd bs=1M if=${dd_if} | pv -s ${disk_size} | ${archiver_params} > "${dd_of}"
        else
            sudo dd bs=1M status=progress if=${dd_if} | ${archiver_params} > "${dd_of}"
        fi
    fi

    if [ $? -eq 0 ];  then
        clear
        sudo chown $USER:$USER ${to}/${save_dir}/*
        del_old_template_files "full_path=${to}/${save_dir}" "count=${save_files}" "prefix=${disk_name}_" "suffix=${dd_ext}"
        del_old_template_files "full_path=${to}/${save_dir}" "count=${save_files}" "prefix=${prefix}" "suffix=.txt"
        del_old_template_files "full_path=${to}/${save_dir}" "count=${save_files}" "prefix=${prefix}" "suffix=.smart"
        del_old_template_files "full_path=${to}/${save_dir}" "count=${save_files}" "prefix=${prefix}" "suffix=.info"
        menu_rebuilding "dd_disk_menu"
    else
        sudo chown $USER:$USER ${to}/${save_dir}/*
        whiptail_msgbox "Ошибка \"$?\" создания образа \"${disk_name}\" в \"$(replacement "${to}" 60)\""
        menu_rebuilding "dd_disk_menu" mc "${to}/${save_dir}"
    fi
}

DdDisk.arrays

DdDisk.arrays

declare -A dd_disks_to

# допустимые значения "archivator": gzip, tar, 7z
dd_disks_init=("id_or_uuid" "save_dir" "save_files=5" "archivator=gzip")
declare -A dd_disks_from

VtData="${HOME}/Network/Home/WorkStations/nik-rp4/media/VtData"
[[ -d "/media/${USER}/VtData" ]] && VtData="/media/${USER}/VtData"

Archive="${HOME}/Network/Home/WorkStations/nik-rp4/media/Archive"
[[ -d "/media/${USER}/Archive" ]] && Archive="/media/${USER}/Archive"

case "${HOSTNAME}" in
    asus-x55c)
        dd_disks_to["VtData"]="to=${VtData}"
        dd_disks_from["VtData:sda1"]="id_or_uuid=DA05-40F1 save_dir=Notebook/Asus-x55c save_files=20"
        dd_disks_from["VtData:sda3"]="id_or_uuid=1800b4fa-5228-4698-949f-dbdc7e431b3e save_dir=Notebook/Asus-x55c save_files=20"
    ;;
esac


Подключаемый скрипт DdFlash

DdFlash.bash

DdFlash.bash

##########################
# Dd usb-флешек (под sudo)
##########################

if [ -z "$(sudo -l | grep "NOPASSWD: ALL")" ]; then
    echo "Для модуля \"Создание образов флешек\""
    echo "необходимы повышенные права пользователя \"$USER\"!"
    echo "Для этого можно добавить в файл \"/etc/sudoers\" или"
    echo "создать в каталоге \"/etc/sudoers.d\" любой файл"
    echo "и добавить в него строчку ниже:"
    echo "$USER $HOSTNAME=NOPASSWD: ALL"
    press_enter
    return
fi

# =======
# Массивы
# =======
[[ -f "${ScriptCfg}/DdFlash.arrays" ]] || return
. ${ScriptCfg}/DdFlash.arrays

# локальные переменные для функций в этом скрипте
dd_flash_vars=("flash_name" "flash_size" "dir_free" "dd_ext")

# ======================
# Дополнительные функции
# ======================
function flash_name {
    # lsblk -n -l -o NAME,PTUUID,PARTUUID,UUID,TYPE,MOUNTPOINT
    # "$@" - PTUUID флешки
    [[ -z "$@" ]] && return

    echo "$(lsblk -n -l -o NAME,PTUUID,TYPE | grep "$@" | grep 'disk' | awk '{print $1}')"
}

function dd_flash_menu {
    [[ ${#dd_flash_init[@]} == 0 ]] && return
    [[ ${#dd_flash_ptuuids[@]} == 0 ]] && return
    [[ ${#dd_flash_dirs[@]} == 0 ]] && return

    clear
    echo "Подготовка меню (модуль $FUNCNAME)..."

    local menu_index
    local menu_name="Создание образов флешек (всего флешек: ${#dd_flash_ptuuids[@]})=show_menu"

    local submenu_index
    local submenu_name

    if [ -z "$@" ]; then
        menu_index="$(next_menu_index)"
        main_menu["${menu_index}"]="${menu_name}"
    else
        menu_index="$@"
        for key in ${!main_menu[@]}; do
            [[ "${key}" == "${menu_index}."* ]] && unset main_menu[${key}]
        done
    fi

    local last_backup

    $(vars_declare_local "dd_flash_vars" "dd_flash_init")
    # vars_test "dd_flash_vars" "dd_flash_init" "dd_flash_ptuuids"

    # перебор флешек
    for key_flash_ptuuid in $(arr_keys_sort "dd_flash_ptuuids"); do
        vars_init "dd_flash_init"
        vars_set "dd_flash_ptuuids[${key_flash_ptuuid}]"
        # vars_test "dd_flash_init"

        flash_name="$(flash_name "${flash_ptuuid}")"
        if [[ -z "${flash_name}" ]]; then
            submenu_index="$(next_menu_index ${menu_index})"
            main_menu["${submenu_index}"]="Флешка: \"${key_flash_ptuuid}\" не подключена=empty"
            continue
        fi

        if [ "$(disk_hotplug ${flash_name})" -ne "1" ]; then
            submenu_index="$(next_menu_index ${menu_index})"
            main_menu["${submenu_index}"]="\"${key_flash_ptuuid}\" не является флешкой=empty"
            continue
        fi

        flash_size="$(lsblk -n -d -l -o SIZE /dev/${flash_name})"
        flash_size="${flash_size//' '/''}"
        submenu_index="$(next_menu_index ${menu_index})"
        main_menu["${submenu_index}"]="Создание образа флешки \"${key_flash_ptuuid}\" (${flash_size})=show_menu"

        # перебор каталогов
        for key_flash_dir in $(arr_keys_sort "dd_flash_dirs"); do
            if [[ ${key_flash_dir} == ${key_flash_ptuuid}:* ]]; then
                vars_init "dd_flash_init"
                vars_set "dd_flash_dirs[${key_flash_dir}]"
                # vars_test "dd_flash_init"

                dd_ext="$(archiver_dd_ext ${archivator})"
                save_dir_for_menu="${save_dir:0:14}...$(echo "${save_dir}" | sed 's/.*\('${key_flash_dir#*:*}'.*\).*/\1/')"

                if [[ -d "${save_dir}" ]]; then
                    last_backup="$(find "${save_dir}/" -type f -iname "${key_flash_ptuuid}_${template}${dd_ext}" | sort | tail -1)"
                    if [[ -n  "${last_backup}" ]]; then
                        last_backup=" (последний: ${last_backup##*/})"
                        last_backup=${last_backup//${dd_ext}/''}
                    fi

                    if [ -f "${save_dir}/${key_flash_ptuuid}_$(date +%F)${dd_ext}" ]; then
                        submenu_name="Показать каталог с \"${dd_ext}\"-образом за сегодня в \"$(replacement "${save_dir_for_menu}")\""
                        main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" "mc" "${save_dir}""
                    else
                        dir_free="$(/usr/bin/df -h ${save_dir} | tail -n1 | awk '{print $4}')"
                        submenu_name="Создать \"${dd_ext}\"-образ в \"$(replacement "${save_dir_for_menu}")\" (свободно: ${dir_free})"${last_backup}""
                        main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=dd_flash_run "${key_flash_ptuuid}" "${key_flash_dir}""
                    fi
                else
                    submenu_name="Не найден каталог \"$(replacement "${save_dir_for_menu}")\""
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" "mc" "${save_dir}""
                fi
            fi
        done
    done
}

function dd_flash_run {
    clear

    local key_flash_ptuuid="$1"
    local key_flash_dir="$2"

    $(vars_declare_local "dd_flash_vars" "dd_flash_init")
    vars_set "dd_flash_ptuuids[${key_flash_ptuuid}]" "dd_flash_dirs[${key_flash_dir}]"
    # vars_test "dd_flash_init"

    flash_name="$(flash_name "${flash_ptuuid}")"
    dd_ext="$(archiver_dd_ext ${archivator})"
    local dd_if="/dev/$(flash_name "${flash_name}")"
    local dd_of="${save_dir}/${key_flash_ptuuid}_$(date +%F)${dd_ext}"

    disk_info "${flash_name}" "${save_dir}" "${key_flash_ptuuid}_"

    [[ -z "${dd_if}" ]] && return

    flash_size="$(disk_size ${flash_name} "SIZE" "human")"
    echo -e "Создание \"${dd_ext}\"-образа флешки \"${key_flash_ptuuid}\" (${flash_size})\n в \"$(replacement "${save_dir}" 60)\"\n"

    dir_free="$(/usr/bin/df -B1 ${save_dir} | tail -n1 | awk '{print $4}')"
    flash_size=$(disk_size ${flash_name})

    if [ "$(( flash_size * 15 / 10 ))" -gt "${dir_free}" ]; then
        echo "Мало свободного места на \"$(replacement "${save_dir}" 60)\"!"
        press_enter "$FUNCNAME"
        return
    fi

    local archiver_params="$(archiver_params ${archivator})"
    if [ -z "${archiver_params}" ]; then
        if [ -n "${PvInstalled}" ] && [ -n "${flash_size}" ]; then
            sudo dd bs=1M if=${dd_if} | pv -s ${flash_size} > "${dd_of}"
        else
            sudo dd bs=1M status=progress if=${dd_if} of="${dd_of}"
        fi
    else
        if [ -n "${PvInstalled}" ] && [ -n "${flash_size}" ]; then
            sudo dd bs=1M if=${dd_if} | pv -s ${flash_size} | ${archiver_params} > "${dd_of}"
        else
            sudo dd bs=1M status=progress if=${dd_if} | ${archiver_params} > "${dd_of}"
        fi
    fi

    if [ $? -eq 0 ];  then
        clear
        sudo chown $USER:$USER ${save_dir}/*
        del_old_template_files "full_path=${save_dir}" "count=${save_files}" "prefix=${key_flash_ptuuid}_" "suffix=${dd_ext}"
        del_old_template_files "full_path=${save_dir}" "count=${save_files}" "prefix=${key_flash_ptuuid}_" "suffix=".txt""
        menu_rebuilding "dd_flash_menu" "mc" "${save_dir}"
    else
        sudo chown $USER:$USER ${save_dir}/*
        whiptail_msgbox "Ошибка \"$?\" создания образа флешки \"${key_flash_ptuuid}\"\n в \"$(replacement "${save_dir}" 60)\""
        menu_rebuilding "dd_flash_menu" "mc" "${save_dir}"
    fi
}

DdFlash.arrays

DdFlash.arrays

# Внимание! Для флешек используется PTUUID - идентификатор таблицы разделов (не зависит от карт-ридера)!
# Не путать с ID и UUID самих разделов!
# Для поиска можно использовать:
# lsblk -o NAME,PTUUID,PARTUUID,UUID,PTTYPE,TYPE,SIZE,MOUNTPOINT,HOTPLUG
# Или запустить скрипт с ключем lsblk_show

# допустимые значения "archivator": gzip, tar, 7z
dd_flash_init=("save_dir" "save_files=3" "archivator=gzip")

# индексы массива "dd_flash_ptuuids" условные и являются именами "конечного" каталога хранения образов флешки!
declare -A dd_flash_ptuuids
declare -A dd_flash_dirs

BackUp="${HOME}/Network/Home/WorkStations/nik-rp4/media/BackUp"
[[ -d /media/${USER}/BackUp ]] && BackUp="/media/${USER}/BackUp"

Archive="${HOME}/Network/Home/WorkStations/nik-rp4/media/Archive"
[[ -d /media/${USER}/Archive ]] && BackUp="/media/${USER}/Archive"

case "${HOSTNAME}" in
    asus-x55c)
        dd_flash_ptuuids["debian-12-64"]="flash_ptuuid=3678146b-1757-4fd3-a029-163f393382c8"
        dd_flash_dirs["debian-12-64:BackUp"]="save_dir=${BackUp}/USB-flash/DebianLive/Debian-12"
        if [[ -d ${Archive} ]]; then
            dd_flash_dirs["debian-12-64:Archive"]="save_dir=${Archive}/USB-flash/DebianLive/Debian-12 save_files=2"
        fi

        dd_flash_ptuuids["kali-rp"]="flash_ptuuid=8f2fac4f"
        dd_flash_dirs["kali-rp:BackUp"]="save_dir=${BackUp}/USB-flash/RaspberryPi/Kali-pi"
        if [[ -d ${Archive} ]]; then
            dd_flash_dirs["kali-rp:Archive"]="save_dir=${Archive}/USB-flash/RaspberryPi/Kali-pi save_files=2"
        fi
    ;;
esac


Синхронизация (черновик)

Скрины для asus-x55

Скрины для asus-x55

 Главное меню  Синхронизация по сети (unison))  Локальная синхронизация (unison))

Скрины для nik-vm

Скрины для nik-vm

 Главное меню  Синхронизация по сети (rsync))


Скрипт SyncData

SyncData.bash

SyncData.bash

#!/bin/bash
# set -e
# set -x

# Обрабатывает параметры: debug_menu_main

################
# Главный скрипт
################
clear

#===================
# Начальные проверки
#===================
if [ -z "$(command -v whiptail)" ]; then
    echo "Не установлен пакет \"whiptail\"!"
    exit 0
fi

# Это должно быть в самом начале!!!
name_scr=${0##*/}
name_scr=${name_scr%.*}
readonly name_scr
readonly title_scr="Синхронизация данных на \"${HOSTNAME}\" (${name_scr})"
readonly IFS_OLD=$IFS

# pgrep не понимает имен более 15 символов!
if [ $(pgrep -c "${name_scr:0:15}") -gt 1 ]; then
    whiptail --title "${name_scr}" --msgbox  "Скрипт уже запущен!" 7 50
    exit
fi

# Должна быть первой функцией
function whiptail_msgbox {
    [[ -z "$@" ]] && return

    local strings="$@"

    local lines=$(echo -e "${strings}" | wc -l)
    local height=7
    local height_max=12
    [[ "${lines}" -gt "1" ]] && height=$((height + ${lines}))

    local width=60
    local width_max=80

    IFS=$'\n'
    for string in $(echo -e ${strings}); do
        [[ "${#string}" -gt "${width}" ]] && width=${#string}
    done
    IFS=${IFS_OLD}

    [[ "${width}" -gt "${width_max}" ]] && width=${width_max}

    if [ "${height}" -gt "${height_max}" ]; then
        whiptail --scrolltext --title "${title_scr}" --msgbox "${strings}" ${height_max} ${width}
    else
        whiptail --title "${title_scr}" --msgbox "${strings}" ${height} ${width}
    fi
}

#================
# Общие константы
#================
# mc
readonly McInstalled="$(command -v mc)"

# meld
readonly MeldInstalled="$(command -v meld)"

# netcat (nc)
readonly NetcatInstalled="$(command -v netcat)"

# unison
readonly UnisonGtkInstalled="$(command -v unison-gtk)"
[[ -n "${UnisonGtkInstalled}" ]] && readonly UnisonVersion="$(unison -version | awk '{print $3}')"

# rsync
readonly RsyncInstalled="$(command -v rsync)"
[[ -n "${RsyncInstalled}" ]] && readonly RsyncVersion="$(rsync --version | awk '{print $3}')"

# Каталоги и файлы
readonly ScriptDir="$(cd -- "$(dirname -- "${0}")" &> /dev/null && pwd)"
readonly ScriptCfg="${ScriptDir}/${name_scr}Cfg"
if [ ! -d ${ScriptCfg} ]; then
    whiptail_msgbox "Не найден каталог:\n${ScriptCfg}"
    exit
fi

readonly Inclusions="${ScriptDir}/Inclusions"
readonly UnisonCfg="${ScriptDir}/UnisonCfg"
readonly UnisonIgnoreFile="${UnisonCfg}/IgnoreFiles.cfg"

#==============
# Общие функции
#==============
function press_enter {
    # Вывод сообщений в консоль и ожидание нажатия Enter
    # использовать echo здесь не получится
    # из-за возврата результата через echo в некоторых вызывающих функциях!

    set +x
    if [ -z "$@" ]; then
        read -p "\"Enter\" - продолжение, \"Ctrl+c\" - прервать"
    else
        read -p "\"$@\" \"Enter\" - продолжение, \"Ctrl+c\" - прервать"
    fi
}

function arr_keys_sort {
    # Возвращает отсортированную строку ключей массива
    # "$1" - имя массива

    [[ -z "$1" ]] && return

    declare -n arr_keys_declare="$1"
    if [[ ${#arr_keys_declare[@]} == 0 ]]; then
        press_enter "$FUNCNAME: Массив \"$1\" не существует или пуст!"
        return
    fi

    local sorted_arr_keys

    IFS=$'\n'
    sorted_arr_keys=($(sort <<< "${!arr_keys_declare[*]}"))
    IFS=${IFS_OLD}

    echo "${sorted_arr_keys[@]}"
}

function debug_menu_main {
    # Показ массива главного меню для отладки
    # Можно вставлять для отладки в разные места и передавать параметр $@
    # Например передавать $FUNCNAME для отображения имени вызывающей функции

    set +x
    echo -e "\n##### menu_main ($@):"

    for key in $(arr_keys_sort "main_menu"); do
        echo "${key}: ${main_menu[${key}]}"
    done

    press_enter "$FUNCNAME"
}

function debug_menu_current {
    set +x
    echo -e "\n##### menu_current ($@):"

    for element in "${menu_current[@]}"; do
        echo "${element}"
    done

    press_enter "$FUNCNAME"
}

function replacement {
    [[ -z "$1" ]] && return

    local txt="$1"
    txt="${txt//${HOME}/'$HOME'}"
    txt="${txt//\/media\/${USER}/media}"

    # Обрезание строки
    local max_length=40
    [[ -n "$2" ]] && max_length="$2"

    if [ ${#txt} -gt $((max_length + 4)) ]; then
        txt="${txt:0:$((max_length / 2))}...${txt:(-$((max_length / 2)))}"
    fi

    echo "${txt}"
}

function is_unison_run {
    if [ -z "$1" ]; then
        echo "$(pgrep -a unison-gtk)"
    else
        echo "$(pgrep -a unison-gtk | grep -w $1)"
    fi
}

function vars_declare_local {
    # Для создания локальных переменных чеsрез echo в вызывающей функции
    # "$@" - имена массивов откуда брать переменные

    [[ -z "$@" ]] && return

    local result=''
    for name_vars_declare in "$@"; do
        declare -n arr_vars_declare="${name_vars_declare}"
        if [[ ${#arr_vars_declare[@]} == 0 ]]; then
            press_enter "$FUNCNAME: Массив \"${name_vars_declare}\" не существует или пуст!"
            continue
        fi

        for var_vars_declare in ${arr_vars_declare[@]}; do
            [[ -n "${result}" ]] && result+=' '
            result+='local '${var_vars_declare}
        done
    done

    echo -e "${result}"
}

function vars_init {
    # Установка значений поумолчанию записанных в массивах
    # Меняет переменные вызывающей функции!!!
    # Применять после vars_declare_local или другого объявления локальных переменных в вызывающей функции!
    # "$@" - имена массивов откуда брать переменные

    [[ -z "$@" ]] && return

    for name_vars_init in "$@"; do
        declare -n arr_vars_init="${name_vars_init}"
        if [[ ${#arr_vars_init[@]} == 0 ]]; then
            echo -e "\nМассив \"${name_vars_init}\" не существует или пуст!"
            press_enter "$FUNCNAME"
            continue
        fi

        for var_vars_init in ${arr_vars_init[@]}; do
            if [[ ${var_vars_init} == *'='* ]]; then eval ${var_vars_init}; else eval ${var_vars_init}=''; fi
        done
    done
}

function vars_set {
    # Устанавливает актуальные значения переменных
    # Меняет значения переменных в вызывающей функции!!!
    # "$@" - элементы массивов откуда брать значения переменных

    [[ -z "$@" ]] && return

    local element_vars_set
    for name_vars_set in "$@"; do
        element_vars_set="${!name_vars_set}"
        if [[ ${#element_vars_set[@]} == 0 ]]; then
            echo -e "\nМассив \"${name_vars_set}\" не существует или пуст!"
            press_enter "$FUNCNAME"
            continue
        fi

        for var in "${element_vars_set[@]}"; do [[ ${var} == *'='* ]] && eval ${var}; done
    done
}

function vars_test {
    # Вывод пременных и их значений для тестирования
    # Не применять если вызывающая функция возвращает что-то через echo!!!
    # "$@" - имена массивов откуда брать переменные

    [[ -z "$@" ]] && return

    for name_vars_test in "$@"; do
        echo -e "\nмассив переменных: ${name_vars_test}"
        declare -n arr_vars_test="${name_vars_test}"
        if [[ ${#arr_vars_test[@]} == 0 ]]; then
            echo -e "\nМассив \"${name_vars_test}\" не существует или пуст!"
            press_enter "$FUNCNAME"
            continue
        fi

        for var_vars_test in ${arr_vars_test[@]}; do
            [[ ${var_vars_test} == *'='* ]] && var_vars_test=${var_vars_test//'='*/''}
            echo "${var_vars_test}:${!var_vars_test}"
        done
    done

    press_enter "$FUNCNAME"
}

# Функции для меню
function next_menu_index {
    local index="$@"
    local next_index=0
    local length="${#index}"
    local var=''
    local next_menu_index=''

    if [ ${#main_menu[@]} -eq 0 ]; then
        next_menu_index="1"
    elif [[ -z ${index} ]]; then
        # индекс первого уровня
        for key in ${!main_menu[@]}; do
            (( ${key:0:1} > next_index )) && next_index=${key:0:1}
        done
        next_menu_index="$((next_index + 1))"
    else
        for key in ${!main_menu[@]}; do
            if [[ ${key} == ${index}* ]]; then
                var=${key:0:$((length + 2))}
                if [ ${#var} -gt ${length} ]; then
                    (( ${var: -1} > next_index )) && next_index=${var: -1}
                fi
            fi
        done

        if [ -z "${var}" ]; then
            # "родительский" индекс не найден!
            for key in ${!main_menu[@]}; do
                (( ${key:0:1} > next_index )) && next_index=${key:0:1}
            done
            next_menu_index="$((next_index + 1))"
        else
            next_menu_index="${index}.$((next_index + 1))"
        fi
    fi

    echo "${next_menu_index}"
}

function menu_name {
    [[ -z "$@" ]] && return
    echo "$(echo "$@" | awk -F'=' '{print $1}')"
}

function menu_function {
    [[ -z "$@" ]] && return
    echo "$(echo "$@" | awk -F'=' '{print $2}')"
}

function connection_check {
    # "$@" - элемент массива откуда брать его значения
    local error=''

    local host=''
    local port='22'

    # whiptail_msgbox ниже не показывает окно, причину не понял!
    if [ -z "$@" ]; then
        error="Передан пустой параметр в connection_check!"
        # whiptail_msgbox "${error}"
        press_enter "${error}"
    else
        vars_set "$@"
        if [ -z "${NetcatInstalled}" ]; then
            error="Не установлен пакет \"netcat\"!"
            # whiptail_msgbox "${error}"
            press_enter "${error}"
        elif [ -z "${host}" ] || [ -z "${port}" ]; then
            error="Ошибка параметров в connection_check host: ${host} port: ${port}"
            # $(whiptail_msgbox "${error}")
            press_enter "${error}"
        else
            (nc -zw3 ${host} ${port})
            if [ $? -gt 0 ]; then
                error="Порт ${port} хоста ${host} недоступен!\n"
                error+="Проверка: nc -zvw3 ${host} ${port}"
            fi
        fi
    fi

    echo "${error}"
}

function menu_rebuilding {
    # для перестроения main_menu
    # используется ${breadcrumbs[-1]} массива скрипта BackUpData.bash!!!
    # "$1" - имя функции меню для пересоздания
    # "$2" - команда перед вызовом функции меню
    # "$3" - каталог где запускать команду
    [[ -z "$1" ]] && return
    if [[ ! $(declare -F "$1") ]]; then
        echo -e "\nНе найдена функция \"$1\"!"
        press_enter "$FUNCNAME"
        return
    fi

    if [ -n "$3" ] && ! [ -d "$3" ]; then
        echo -e "\nНе найден каталог \"$3\"!"
        press_enter "$FUNCNAME"
        return
    fi

    if [ -n "$2" ]; then
        if [[ -z "$(command -v "$2")" ]]; then
            cd "$3"
            bash
        else
            case "$2" in
                mc)
                    mc "$3"
                ;;
                ls)
                    ls -l "$3"
                    press_enter
                ;;
                bash)
                    cd "$3"
                    bash
                ;;
            esac
        fi
    fi

    [[ -n "${breadcrumbs[-1]}" ]] && "$1" "${breadcrumbs[-1]:0:1}"
}

#=================================
# Загрузка дополнительных скриптов
#=================================
[[ -f "${Inclusions}/UnisonRemote.bash" ]] && . ${Inclusions}/UnisonRemote.bash
[[ -f "${Inclusions}/UnisonLocal.bash" ]] && . ${Inclusions}/UnisonLocal.bash
[[ -n "${RsyncInstalled}" ]] && [[ -f "${Inclusions}/RsyncRemote.bash" ]] && . ${Inclusions}/RsyncRemote.bash

#===============================================================
# Главный ассоциативный массив имен меню и их вызываемых функций
#===============================================================
# Главное меню
declare -A main_menu

[[ $(declare -F unison_remote_menu) ]] && unison_remote_menu
[[ $(declare -F unison_local_menu) ]] && unison_local_menu
[[ $(declare -F rsync_remote_menu) ]] && rsync_remote_menu

# для отладки
# -----------
[[ "$@" == *'debug_menu_main'* ]] && debug_menu_main

# Текущий массив меню
declare -a menu_current

# Массив "хлебные крошки"
declare -a breadcrumbs

# для возвращения к выбранному пункту меню
default_item=''

#==============
# Основной цикл
#==============
while true; do
    clear

    main_item=''
    unset menu_current
    height=1
    width=${#title_scr}
    width_max=60

    while [ ${#breadcrumbs[@]} -gt 0 ]; do
        if [ -z "${main_menu[${breadcrumbs[-1]}]}" ]; then
            # press_enter "breadcrumbs:${breadcrumbs[-1]}"
            unset breadcrumbs[-1]
        else
            break
        fi
    done

    if [ ${#breadcrumbs[@]} = 0 ]; then
        prefix=''
        header="Главное меню"
    else
        prefix="${breadcrumbs[-1]}."
        header="$(menu_name ${main_menu[${breadcrumbs[-1]}]})"
    fi

    (( ${#header} > width )) && width=${#header}

    for ((i=1; i <= ${#main_menu[@]}; i++)); do
        if [ -n "${main_menu["${prefix}${i}"]}" ]; then
            main_item="$(menu_name ${main_menu["${prefix}${i}"]})"
            menu_current=("${menu_current[@]}" "${i}" "${main_item}")
            (( height++ ))
            (( ${#main_item} > width )) && width=${#main_item}
        fi
    done

    if [ ${#menu_current[@]} -eq 0 ]; then
        if [ ${#main_menu[@]} -eq 0 ]; then
            whiptail_msgbox "Список главного меню пуст!"
            break
        else
            whiptail_msgbox "Список меню:\n\"${header}\" пуст!"
        fi
    fi

    tag=$(whiptail --title "${title_scr}" --menu "${header}" --default-item "${default_item}" $(( height + 8 )) $(( width + 16 )) ${height} "${menu_current[@]}" 3>&1 1>&2 2>&3)

    if [ $? -eq 0 ];  then
        default_item=${tag}
        menu_function="$(menu_function ${main_menu["${prefix}${tag}"]})"
        if [ -z "${menu_function}" ]; then
            whiptail_msgbox "Нет функции для меню:\n$(menu_name ${main_menu["${prefix}${i}"]})"
            continue
        elif [ "${menu_function}" == 'show_menu' ]; then
            default_item=''
            breadcrumbs+=("${prefix}${tag}")
        else
            # запуск дочернего процесса
            # [[ "${menu_function}" != 'empty' ]] && (${menu_function})

            # без запуска дочернего процесса?
            [[ "${menu_function}" != 'empty' ]] && ${menu_function}
        fi
    else
        if [ ${#breadcrumbs[@]} == 0 ]; then
            if [ -z "$(is_unison_run)" ]; then
                break
            else
                whiptail_msgbox "Unison-gtk еще работает!"
            fi
        else
            default_item="${breadcrumbs[-1]: -1}"
            unset breadcrumbs[-1]
        fi
    fi
done

exit 0


Подключаемый скрипт RsyncRemote

RsyncRemote.bash

RsyncRemote.bash

##########################################
# Удаленная синхронизация rsync
# Аккуратно!!!
# Можно случайно удалить целые каталоги!!!
##########################################

# =======
# Массивы
# =======
[[ -f "${ScriptCfg}/RsyncRemote.arrays" ]] || return
. ${ScriptCfg}/RsyncRemote.arrays

rsync_remote_vars=("disk")

# ======================
# Дополнительные функции
# ======================
function rsync_remote_base_dirs_check {
    # "$1" - индеск массива rsync_remote_connections
    # "$2" - индеск массива rsync_remote_base_dirs
    # возвращает через echo! vars_test и подобное не применять!

    $(vars_declare_local "rsync_remote_connections_arr" "rsync_remote_base_dirs_arr")
    vars_set "rsync_remote_connections["$1"]" "rsync_remote_base_dirs["$2"]"

    local error_dir_remote=''

    ssh -p ${port} ${user}@${host} [[ -d ${base_remote} ]] > /dev/null 2>&1
    if [[ $? -ne 0 ]]; then
        error_dir_remote="нет доступа к удаленному каталогу: \"${base_remote}\""
    fi

    echo "${error_dir_remote}"
}

function rsync_remote_menu {
    [[ ${#rsync_remote_connections_arr[@]} == 0 ]] && return
    [[ ${#rsync_remote_connections[@]} == 0 ]] && return

    [[ ${#rsync_remote_base_dirs_arr[@]} == 0 ]] && return
    [[ ${#rsync_remote_base_dirs[@]} == 0 ]] && return

    [[ ${#rsync_remote_dirs_arr[@]} == 0 ]] && return
    [[ ${#rsync_remote_dirs[@]} == 0 ]] && return

    local menu_name="Синхронизация по сети (rsync)=show_menu"
    echo "Подготовка меню: \"Синхронизация по сети (rsync)\"..."
    sleep 1

    if [ -z "$@" ]; then
        menu_index="$(next_menu_index)"
        main_menu["${menu_index}"]="${menu_name}"
    else
        menu_index="$@"
        for key in ${!main_menu[@]}; do
            [[ "${key}" == "${menu_index}."* ]] && unset main_menu[${key}]
        done
    fi

    local submenu_index
    local submenu_name

    $(vars_declare_local "rsync_remote_vars" "rsync_remote_base_dirs_arr")
    # vars_test "rsync_remote_vars" "rsync_remote_base_dirs_arr"

    for key_connection in $(arr_keys_sort "rsync_remote_connections"); do
        [[ -z "$(connection_check rsync_remote_connections["${key_connection}"])" ]] || continue

        submenu_index="$(next_menu_index ${menu_index})"
        main_menu["${submenu_index}"]="Синхронизация по сети с \"${key_connection}\" (rsync)=show_menu"

        for key_base_dir in $(arr_keys_sort "rsync_remote_base_dirs"); do
            if [[ ${key_base_dir} == ${key_connection}:* ]]; then
                vars_init "rsync_remote_base_dirs_arr"
                vars_set "rsync_remote_base_dirs["${key_base_dir}"]"
                # vars_test "rsync_remote_vars" "rsync_remote_base_dirs_arr"

                if [ ! -d ${base_locally} ]; then
                    if [[ "${base_locally}" == *"/media/${USER}"* ]]; then
                        disk="${base_locally//\/media\/${USER}\/''}"
                        disk=${disk//\/*/''}

                        if [ -d "/media/${USER}/${disk}" ]; then
                            submenu_name="не найден каталог \"$(replacement ${base_locally})\" на \"${disk}\""
                            main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" mc "/media/${USER}/${disk}""
                        else
                            submenu_name="диск \"${disk}\" не подключен"
                            main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                        fi
                        continue
                    else
                        submenu_name="не найден каталог \"$(replacement ${base_locally})\""
                        main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                        continue
                    fi
                fi

                submenu_name="$(rsync_remote_base_dirs_check "${key_connection}" "${key_base_dir}")"
                if [ -z "${submenu_name}" ]; then
                    submenu_name="удаленный:\"$(replacement ${base_remote})\" и локальный:\"$(replacement ${base_locally})\""
                    submenu_name=${submenu_name//\/media\/${USER}/media}
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=rsync_remote_sync "${key_connection}" "${key_base_dir}""
                else
                    submenu_name=${submenu_name//\/media\/${USER}/media}
                    main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=empty"
                fi
            fi
        done
    done
}

function rsync_remote_sync {
    # "$1" - индеск массива rsync_remote_connections
    # "$2" - индеск массива rsync_remote_dirs
    clear

    local key_connection="$1"
    local key_base_dir="$2"

    $(vars_declare_local "rsync_remote_connections_arr" "rsync_remote_base_dirs_arr" "rsync_remote_dirs_arr")
    vars_set "rsync_remote_connections["${key_connection}"]" "rsync_remote_base_dirs["${key_base_dir}"]"
    local version="$(ssh -p ${port} ${user}@${host} command -v rsync)"
    if [ -z "${version}" ]; then
        echo -e "\nНа хосте \"${host}\" не установлен \"rsync\"!"
        press_enter
        return
    fi
    local version="$(ssh -p ${port} ${user}@${host} rsync --version | head -n 1 | awk '{print $3}')"

    local progress
    local size

    echo "Сихронизация данных с сервером ${host}..."

    progress=''
    size=0

    for key_dir in ${!rsync_remote_dirs[@]}; do
        if [[ ${key_dir} == "${key_base_dir}:"* ]]; then
            vars_init "rsync_remote_dirs_arr"
            vars_set "rsync_remote_dirs["${key_dir}"]"

            if [ ! -d ${base_locally}/${locally} ]; then
                echo -e "\nНа локальном компьютере нет каталога: ${base_locally}/${locally}"
                press_enter "$FUNCNAME"
                continue
            fi

            # написать проверку ошибки подключения!!!
            ssh -p ${port} ${user}@${host} [[ -d ${base_remote}/${remote} ]] > /dev/null 2>&1
            if [ $? -ne 0 ]; then
                echo -e "\nНа ${host} нет каталога: ${base_remote}/${remote}"
                press_enter "$FUNCNAME"
                continue
            fi

            size=$(ssh -p ${port} ${user}@${host} du --exclude='*lost+found' --exclude='*.Trash-*' -sh --exclude='lost+found' ${base_remote}/${remote}/ | awk '{print $1}')
            echo -e "\nКаталог: ${remote} (${size})"
            echo "--------"

            # progress='--progress'
            rsync --exclude='lost+found' ${params} ${delete} ${progress} -e "ssh -p ${port}" "${user}@${host}:${base_remote}/${remote}/" "${base_locally}/${locally}"

            if [ $? -ne 0 ]; then
                echo -e "\nОшибка: $?"
                press_enter "$FUNCNAME"
            fi

            if [[ -n "${pause}" ]]; then
                echo -e "\nЗакончил с каталогом: ${remote}"
                press_enter
            fi
        fi
    done

    echo -e "\nЗакончилась синхронизаця данных с сервером ${host}"

    case "${execute_after}" in
        mc)
            [[ -n "${McInstalled}" ]] && mc "${base_locally}/${locally}" "${base_locally}"
        ;;
        *)
            press_enter
        ;;
    esac
}

RsyncRemote.arrays

RsyncRemote.arrays

# ключи массива rsync_remote_connections условные, но участвуют в других массивах
rsync_remote_connections_arr=("host" "port=22" "user=${USER}")
declare -A rsync_remote_connections

# ключи массива rsync_remote_base_dirs условные, но участвуют в других массивах
rsync_remote_base_dirs_arr=("base_remote" "base_locally" "execute_after")
declare -A rsync_remote_base_dirs

# в "params" можно добавить долполнительно --progress и/или --stats
rsync_remote_dirs_arr=("remote" "locally" "params='-avhL'" "delete" "pause")
declare -A rsync_remote_dirs

case "${HOSTNAME}" in
    "nik-vm")
        media_rp4='rp4/media'

        # smb
        rsync_remote_connections["smb"]="host=smb.mh.loc user=root"
        rsync_remote_base_dirs["smb:1"]="base_remote=/mnt/data base_locally=${HOME}/${media_rp4}/BackUp/Servers/smb/data"
        rsync_remote_dirs["smb:1:1"]="remote=Distributiv locally=Distributiv params='-avhL' delete='--delete-during'"
        rsync_remote_dirs["smb:1:4"]="remote=Work locally=Work params='-avhL'"
    ;;
esac


Подключаемый скрипт UnisonLocal

UnisonLocal.bash

UnisonLocal.bash

#########################
# Локальная синхронизация
#########################

if [ -z "$(command -v unison-gtk)" ]; then
    echo "Для работы модуля \"Локальная синхронизация (unison)\""
    echo "необходим установленный пакет \"unison-gtk\"!"
    echo "Либо установите пакет, либо отключите загрузку этот модуля."
    echo "Отключить модуль можно переименовав файл \"${Inclusions}/UnisonLocal.script\"."
    press_enter
    return
fi

# =======
# Массивы
# =======
[[ -f "${ScriptCfg}/UnisonLocal.arrays" ]] || return
. ${ScriptCfg}/UnisonLocal.arrays

unison_local_vars=("disk")

# ======================
# Дополнительные функции
# ======================
function unison_local_menu {
    [[ ${#unison_local_dirs_arr[@]} == 0 ]] && return
    [[ ${#unison_local_dirs[@]} == 0 ]] && return

    local menu_index
    local menu_name="Локальная синхронизация (unison)=show_menu"

    if [ -z "$@" ]; then
        menu_index="$(next_menu_index)"
        main_menu["${menu_index}"]="${menu_name}"
    else
        menu_index="$@"
        for key in ${!main_menu[@]}; do
            [[ "${key}" == "${menu_index}."* ]] && unset main_menu[${key}]
        done
    fi

    local submenu_name

    # echo "Подготовка меню: \"${menu_name}\""

    $(vars_declare_local "unison_local_vars" "unison_local_dirs_arr")

    for key_dir in $(arr_keys_sort "unison_local_dirs"); do
        # получаем переменные ${dir1} и ${dir2}
        vars_set "unison_local_dirs["${key_dir}"]"

        [[ -z "${dir1}" ]] && continue
        [[ -z "${dir2}" ]] && continue

        if [ ! -d ${dir1} ]; then
            if [[ "${dir1}" == *"/media/${USER}"* ]]; then
                disk="${dir1//\/media\/${USER}\/''}"
                disk=${disk//\/*/''}

                if [ -d "/media/${USER}/${disk}" ]; then
                    submenu_name="не найден каталог \"$(replacement ${dir1})\" на \"${disk}\""
                    main_menu["$(next_menu_index ${menu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" mc "/media/${USER}/${disk}""
                else
                    submenu_name="диск \"${disk}\" не подключен"
                    main_menu["$(next_menu_index ${menu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                fi
                continue
            else
                submenu_name="не найден каталог \"$(replacement ${dir1})\""
                main_menu["$(next_menu_index ${menu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                continue
            fi
        fi

        if [ ! -d ${dir2} ]; then
            if [[ "${dir2}" == *"/media/${USER}"* ]]; then
                disk="${dir2//\/media\/${USER}\/''}"
                disk=${disk//\/*/''}

                if [ -d "/media/${USER}/${disk}" ]; then
                    submenu_name="не найден каталог \"$(replacement ${dir2})\" на \"${disk}\""
                    main_menu["$(next_menu_index ${menu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME" mc "/media/${USER}/${disk}""
                else
                    submenu_name="диск \"${disk}\" не подключен"
                    main_menu["$(next_menu_index ${menu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                fi
                continue
            else
                submenu_name="не найден каталог \"$(replacement ${dir2})\""
                main_menu["$(next_menu_index ${menu_index})"]="${submenu_name}=menu_rebuilding "$FUNCNAME""
                continue
            fi
        fi

        submenu_name="\"$(replacement ${dir1})\" и \"$(replacement ${dir2})\""
        # submenu_name=${submenu_name//\/${USER}/}
        main_menu["$(next_menu_index ${menu_index})"]="${submenu_name}=unison_local_sync ${key_dir}"
    done
}

function unison_local_sync {
    # параметр $@ - ключ массива unison_local_dirs
    $(vars_declare_local "unison_local_dirs_arr")
    vars_set "unison_local_dirs["$@"]"

    local unison_cfg="${HOSTNAME}/${@}.cfg"
    local UnisonProfile=${name_scr}_${HOSTNAME}_${@}.cfg

    if [ ! -f "${UnisonCfg}/${unison_cfg}" ]; then
        whiptail_msgbox "Нет конфиг-файла: \"${unison_cfg}\"\nдля индекса: \"$@\""
        [[ -n "$(command -v mc)" ]] && mc ${UnisonCfg}
        return
    fi

    if [ -n "$(is_unison_run ${UnisonProfile})" ]; then
        whiptail_msgbox "Синхронизация\n\"${UnisonProfile}\"\nуже запущена!"
        return
    fi

    echo "perms = 0" > $HOME/.unison/${UnisonProfile}
    cat "${UnisonCfg}/${unison_cfg}" >> "$HOME/.unison/${UnisonProfile}"
    cat "${UnisonIgnoreFile}" >> "$HOME/.unison/${UnisonProfile}"

    if [ ! -f "$HOME/.unison/${UnisonProfile}" ]; then
        whiptail_msgbox "Нет unison-профиля:\n$HOME/.unison/${UnisonProfile}"
        return
    fi

    # label_str="${unison_cfg}"
    # unison-gtk "${UnisonProfile}" "${dir1}" "${dir2}" -label "${label_str}" > /dev/null 2>&1 &
    unison-gtk "${UnisonProfile}" "${dir1}" "${dir2}" > /dev/null 2>&1 &
}

UnisonLocal.arrays

UnisonLocal.arrays

# ключи массива unison_local_dirs условные,
# на их основе формируются имена файлов ${unison_cfg} в функции unison_local_sync
unison_local_dirs_arr=("dir1" "dir2")
declare -A unison_local_dirs

case "${HOSTNAME}" in
    asus-x55c)
        unison_local_dirs["home_-_projects_${HOSTNAME}_home"]="dir1=${HOME} dir2=${HOME}/Work/TC/Projects/VSCodium/Home/Data/WorkStations/Linux/${HOSTNAME}/${HOME}"
    ;;
esac


Подключаемый скрипт UnisonRemote

UnisonRemote.bash

UnisonRemote.bash

################################
# Удаленная синхронизация unison
################################

if [ -z "$(command -v unison-gtk)" ]; then
    echo "Для работы модуля \"Синхронизация по сети (unison)\""
    echo "необходим установленный пакет \"unison-gtk\"!"
    echo "Либо установите пакет, либо отключите загрузку этот модуля."
    echo "Отключить модуль можно переименовав файл \"${Inclusions}/UnisonRemote.script\"."
    press_enter
    return
fi

# =======
# Массивы
# =======
[[ -f "${ScriptCfg}/UnisonRemote.arrays" ]] || return
. ${ScriptCfg}/UnisonRemote.arrays

unison_remote_vars=("time")

# ======================
# Дополнительные функции
# ======================
function unison_remote_dirs_check {
    # "$1" - элемент массива unison_remote_connections
    # "$2" - элемент массива unison_remote_dirs
    local error=''

    $(vars_declare_local "unison_remote_dirs_arr" "unison_remote_connections_arr")
    vars_set "$2"

    [[ -z "${remote}" ]] && remote="${locally}"

    if [ -z "$1" ] || [ -z "$2" ]; then
        error="Не хватает параметров в \"unison_remote_dirs_check\" 1: $1, 2: $2!"
        press_enter "${error}"
    else
        if [ ! -d "${locally}" ]; then
            error="На локальном компьютере нет каталога: ${locally}"
        else
            vars_set "$1"

            ssh -p ${port} ${user}@${host} [[ -d ${remote} ]] > /dev/null 2>&1
            if [ $? -ne 0 ]; then
                error="На удаленном компьютере нет каталога: ${remote}"
            fi
        fi
    fi

    echo "${error}"
}

function unison_remote_menu {
    [[ ${#unison_remote_connections_arr[@]} == 0 ]] && return
    [[ ${#unison_remote_connections[@]} == 0 ]] && return

    [[ ${#unison_remote_dirs_arr[@]} == 0 ]] && return
    [[ ${#unison_remote_dirs[@]} == 0 ]] && return

    local menu_name="Синхронизация по сети (unison)=show_menu"
    local menu_index="$(next_menu_index $@)"
    main_menu["${menu_index}"]="${menu_name}"

    local error=''
    local submenu_index=''
    local submenu_name
    local menu_added=''

    $(vars_declare_local "unison_remote_dirs_arr")

    # echo "Подготовка меню: \"${menu_name}\""

    for key_connection in $(arr_keys_sort "unison_remote_connections"); do
        error="$(connection_check unison_remote_connections["${key_connection}"])"
        if [ -z "${error}" ]; then
            # есть соединение с удаленным компом
            menu_added=''
            submenu_index="$(next_menu_index ${menu_index})"
            main_menu["${submenu_index}"]="Синхронизация по сети с \"${key_connection}\" (unison):=show_menu"

            for key_dir in $(arr_keys_sort "unison_remote_dirs"); do
                if [[ ${key_dir} == ${key_connection}:* ]]; then
                    error="$(unison_remote_dirs_check "unison_remote_connections["${key_connection}"]" "unison_remote_dirs["${key_dir}"]")"
                    if [ -z "${error}" ]; then
                        # все проверки успешны
                        vars_init "unison_remote_dirs_arr"
                        vars_set "unison_remote_dirs[${key_dir}]"
                        # vars_test "unison_remote_dirs_arr"

                        if [[ -z "${remote}" ]]; then
                            submenu_name="локальный и удаленный: \"$(replacement ${locally})\""
                        else
                            submenu_name="локальный: \"$(replacement ${locally})\" и удаленный: \"$(replacement ${remote})\""
                        fi

                        main_menu["$(next_menu_index ${submenu_index})"]="${submenu_name}=unison_remote_sync "unison_remote_connections["${key_connection}"]" "unison_remote_dirs["${key_dir}"]""

                        menu_added='yes'
                    fi
                fi
            done

            if [ -z "${menu_added}" ]; then
                main_menu["$(next_menu_index ${submenu_index})"]="Массив меню пуст!=empty"
            fi
        fi
    done
}

function unison_remote_sync {
    # $1 - это элемент массива "unison_remote_connections"
    # $2 - это элемент массива "unison_remote_dirs"
    if [ -z "${UnisonGtkInstalled}" ]; then
        whiptail_msgbox "Не установлен пакет \"unison-gtk\"!"
        return
    fi

    $(vars_declare_local "unison_remote_connections_arr" "unison_remote_dirs_arr")
    vars_set "$1" "$2"

    [[ -z "${remote}" ]] && remote="${locally}"

    local version="$(ssh -p ${port} ${user}@${host} unison -version | awk '{print $3}')"
    # version='2.48.4.2'
    if [ "${version}" != "${UnisonVersion}" ]; then
        whiptail_msgbox "Не совпадают версии unison:\nлокальная ${UnisonVersion}\nудаленная ${version}"
        return
    fi

    local unison_cfg="${locally//${HOME}/home}_-_${host}_${remote//${HOME}/home}"
    unison_cfg=${unison_cfg//\//'_'}
    unison_cfg="${unison_cfg}.cfg"
    local UnisonProfile="${name_scr}_${HOSTNAME}_${unison_cfg}"

    if [ -n "$(is_unison_run ${UnisonProfile})" ]; then
        whiptail_msgbox "Синхронизация\n${UnisonProfile}\nуже запущена!"
        return
    fi

    if [ ! -f "${UnisonCfg}/${HOSTNAME}/${unison_cfg}" ]; then
        whiptail_msgbox "Нет конфиг-файла:\n ${HOSTNAME}/${unison_cfg}"
        return
    fi

    echo "root = ${locally}" > "${HOME}/.unison/${UnisonProfile}"
    echo "root = ssh://${user}@${host}/${remote}" >> "${HOME}/.unison/${UnisonProfile}"
    echo "sshargs = -C -p ${port}" >> "${HOME}/.unison/${UnisonProfile}"
    # echo "label = Sync local \"${param}\" with ${host}" >> "${HOME}/.unison/${UnisonProfile}"
    [[ -n "${MeldInstalled}" ]] && echo "diff = meld -u"  >> "${HOME}/.unison/${UnisonProfile}"
    echo "perms = 0"  >> "${HOME}/.unison/${UnisonProfile}"

    cat "${UnisonCfg}/${HOSTNAME}/${unison_cfg}" >> "${HOME}/.unison/${UnisonProfile}"
    cat "${UnisonIgnoreFile}" >> "${HOME}/.unison/${UnisonProfile}"

    if [ ! -f "${HOME}/.unison/${UnisonProfile}" ]; then
        whiptail_msgbox "Нет unison-профиля:\n${HOME}/.unison/${UnisonProfile}"
        return
    fi

    (unison-gtk "${UnisonProfile}") > /dev/null 2>&1 &
}

UnisonRemote.arrays

UnisonRemote.arrays

# ключи массива unison_remote_connections условные, но участвуют в других массивах
unison_remote_connections_arr=("host" "port=22" "user=${USER}")
declare -A unison_remote_connections

# профиль unison-а формируется из значений "locally" и "remote"
unison_remote_dirs_arr=("locally" "remote")
declare -A unison_remote_dirs

case "${HOSTNAME}" in
    asus-x55c)
        unison_remote_connections["nik-vm"]="host=nik-vm.mh.loc port=4444"
        unison_remote_dirs["nik-vm:1"]="locally=${HOME}"
    ;;
esac