Разбор Crab Mysqldump2Git

Все неверные решения добавляем в комменты и потом уже правим

#!/bin/bash
 
echo "$0 $@ [$$] START" >&2
set -euE
 
if [ "${1:---help}" = "--help" ]; then
        echo 'Info: Создает бекап mysql бд и отправляет его в git origin, таблицы пофайлово'
        echo "Usage: $0 --user=root --password=mypass --host=localhost DB_NAME BACKUP_DIR"
        echo 'Example: restore: git log; git checkout;'
        echo 'Example: restore: for f in *.sql.*; do cat $f >>new.sql; done;'
        echo 'Example: restore: /usr/bin/mysql -uroot -p123  < new.sql'
        exit 0
fi
 
ARG_USER="${1/--user=/}"
ARG_PASSWORD="${2/--password=/}"
ARG_DB_HOST="${3/--host=/}"
ARG_DB_NAME="${4}"
ARG_BACKUP_DIR="${5}"
PREFIX="${ARG_DB_NAME}.sql"
 
prepare(){
        if [ ! -d .git ]; then
                git init .
                echo '*.sql' > .gitignore
                git add .gitignore
                git commit -m init
        fi
        if git remote show origin; then
                git pull origin master
        fi
        return 0
}
 
mysqldump_csplit_per_table(){
        set -o pipefail
        mysqldump --user="$ARG_USER" --host="$ARG_DB_HOST" --password="$ARG_PASSWORD" \
                --opt -c --databases "$ARG_DB_NAME" \
                --skip-quick \
                --add-drop-table --add-drop-database --skip-extended-insert \
                --create-options --single-transaction \
                | csplit -n 4  -s -f"$PREFIX.table." - '/-- Table structure for table/' '{*}'
        set +o pipefail
        return 0
}
 
table_split_per_size10MB(){
        local table=""
        for table in "$PREFIX.table."*; do
                if [ $(stat -c %s "$table") -gt $((10*1024*1024)) ]; then
                        split -a 4 -d -b 10M "$table" "$table.split."
                        rm -f "$table";
                fi
        done
        return 0
}
 
git_commit_push(){
        local ret
        git add .
        ret=$( git status )
        if [[ "$ret" != *'nothing to commit'* ]]; then
                git commit -am $(date +%Y-%m-%d_%H-%M)
        fi
        git gc
        if git remote show origin; then
                git push origin master
        fi
        return 0
}
 
main(){
        cd "$ARG_BACKUP_DIR"
        rm -f "$PREFIX.table."*
        mysqldump_csplit_per_table
        table_split_per_size10MB
        git_commit_push
        return 0
}
main
 
echo "$0 $@ [$$] SUCCESS" >&2
exit 0
#!/bin/bash
### Весь bash скрипт рассматриваем как инстанс объекта crab_mysqldump2git
### Глобальные переменные - это атрибуты(property) объекта
### функции - это методы объекта, либо иногда, это аналоги команд OS.
### если функция работает с переменной, которая не является атрибутом объекта,
### то она должна быть с входными параметрами
 
echo "$0 $@ [$$] START" >&2
### падаем при любых необработанных ошибках
set -euE
 
### Делаем --help самом верху скрипта тк он же является и инфо о скрипте и сразу понятно, что делает скрипт
if [ "${1:---help}" = "--help" ]; then
        echo 'Info: Создает бекап mysql бд и отправляет его в git origin, таблицы пофайлово'
        echo "Usage: $0 --user=root --password=mypass --host=localhost DB_NAME BACKUP_DIR"
        echo 'Example: restore: git log; git checkout;'
        echo 'Example: restore: for f in *.sql.*; do cat $f >>new.sql; done;'
        echo 'Example: restore: /usr/bin/mysql -uroot -p123  < new.sql'
        exit 0
fi
 
### Подготовительная работа
### Подготавливаем глобальные переменные, что тождественно приватным property объекта self
### по сути понимаем, что $ARG_USER - это self.user или crab_mysqldump2git.user
### все аргументы записываем в ARG_NAME=значение, если это опция ARG_OPTNAME=TRUE, если позиционная в массив ARGV[$ARGC]="$i"
 
### стараемся глобальные переменные устанавливать в начале скрипта так удобней для чтения
# self.ARG_USER="${1/--user=/}"
ARG_USER="${1/--user=/}"
# self.ARG_PASSWORD="${2/--password=/}" и т.д.
ARG_PASSWORD="${2/--password=/}"
ARG_DB_HOST="${3/--host=/}"
ARG_DB_NAME="${4}"
ARG_BACKUP_DIR="${5}"
PREFIX="${ARG_DB_NAME}.sql"
 
### подготовительная работа действия, инициализация каталога при необходимости и тп
prepare(){
        ### Присвоение глобальных переменных и ARG_NAME здесь не делаем, делаем в начале скрипта
        if [ ! -d .git ]; then
                git init .
                echo '*.sql' > .gitignore
                git add .gitignore
                git commit -m init
        fi
        if git remote show origin; then
                git pull origin master
        fi
        return 0
}
 
### Что такое объектный подход в stronbash
### Описание пользовательских команд(методов) и функций
### метод может работать с глобальными переменным без входящих значений
### если эти переменные это атрибут объекта self-script или его ARG_NAME CONF_NAME переменные
### но если это иные переменные, то обязательно делать через входные значения
### например ТОЛЬКО аргументом если мы обрабатываем файлы циклом,
### через for file in /tmp/mysql/*; do funct_name "$file"; done
 
# self.mysqldump_csplit_per_table()
mysqldump_csplit_per_table(){
        set -o pipefail
        mysqldump --user="$ARG_USER" --host="$ARG_DB_HOST" --password="$ARG_PASSWORD" \
                --opt -c --databases "$ARG_DB_NAME" \
                --skip-quick \
                --add-drop-table --add-drop-database --skip-extended-insert \
                --create-options --single-transaction \
                | csplit -n 4  -s -f"$PREFIX.table." - '/-- Table structure for table/' '{*}'
        set +o pipefail
        return 0
}
 
# self.table_split_per_size10MB()
table_split_per_size10MB(){
        local table=""
        for table in "$PREFIX.table."*; do
                if [ $(stat -c %s "$table") -gt $((10*1024*1024)) ]; then
                        split -a 4 -d -b 10M "$table" "$table.split."
                        rm -f "$table";
                fi
        done
### return 0 всегда обязательно, иначе вызов функции может упасть тк вернется не ноль после цикла или if,
### а код возврата от последней команды даже если мы его обработали и он не приведет к локальному падению
        return 0
}
 
git_commit_push(){
        local ret
        git add .
        ret=$( git status )
        if [[ "$ret" != *'nothing to commit'* ]]; then
                git commit -am $(date +%Y-%m-%d_%H-%M)
        fi
        git gc
        if git remote show origin; then
                git push origin master
        fi
        return 0
}
 
### основной алгоритм скрипта, это последовательность команд и методов
### простейшие команды не выносим в отдельные методы
main(){
### КОМАНДА №1
        cd "$ARG_BACKUP_DIR"
### КОМАНДА №2
        rm -f "$PREFIX.table."*
### КОМАНДА №3 - это Команда-Метод объекта self-script, он использует глобальные переменные, которые по сути атрибуты объекта self-script и это нормально, что мы не передаем параметры
        mysqldump_csplit_per_table
### КОМАНДА №4 - это Команда-Метод
        table_split_per_size10MB
### КОМАНДА №5 - это Команда-Метод        
        git_commit_push
        return 0
}
main
 
echo "$0 $@ [$$] SUCCESS" >&2
### exit 0 обязателен потому что, если в конце будет условие или выход из цикла и забудешь написать exit 0, то вернется не ноль и все упадет. Всегда надо делать exit 0 и return 0 и не думать каждый раз - это позволит избежать кучу проблем и освобождает мозг.
### Если нужно вернуть не 0 делаем явно [ $ret != 0 ] && exit $ret
exit 0
правила разработки как надо делать разбор crab mysqldump2git
Yes(10) No(1) Clear

Yes:
Олег Стрижеченко, Krat Nikolay, , admin, Anton Klinskih, Сергей Трошин, Николай, Александр Ефим, Дмитрий Пономарь, Сергей Трошин,

No:
Николай Глазов,

~~OWNERAPPROVE~~

Олег СтрижеченкоОлег Стрижеченко, 16.02.2018 07:14

Сугубо моя точка зрения:

Первое: соотношение размера скрипта с приносимой пользой, 8 команд - 100 строк (хотя там комменты).

Второе: переносы длинных строк не делают строку короче, концептуально это всё та же длинная строка. Лучше заменить на более читаемое (практически как английский язык):

if ! error=$( git push origin master 2>&1 ); then
    if ! echo "$error" | grep -qm1 'does not appear to be a git repositor'; then
        echo "$error"
        exit 1
    fi
fi

Или:

rc=0
error=$( git push origin master 2>&1 ) || rc=$?
if grep -qm1 'does not appear to be a git repositor' <<< "$error"; then
    echo "$error"
    exit 1
fi

Для более удобного управления потоком исполнения я бы вынес даже в функцию это, в итоге будет отделена логика скрипта от деталей реализации и обработок ошибок:

commit() {
    local error
    error=$( git push origin master 2>&1 ) && return 0
    # тут бы я кстати пояснил _почему_ мы игнорируем эту ошибку
    grep -qm1 'does not appear to be a git repositor' <<< "$error" && return 0
    echo "$error"
    return 1
}

Третье: из-за размера теряется суть - что это вообще за скрипт и зачем он нужен и что делает.

Можно прояснить это с помощью оглавления-main():

main() {
    prepare
    dump_db
    dump_split
    commit_and_push
}

Четвёртое: моя вкусовщина - команды-методы зло по причине того, что их менее удобно отлаживать по кирпичикам. Вставить в консоль код и выполнить:

mysql_dumpsplit "$PREFIX"

проще, чем вставить в консоль код, выполнить:

mysql_dumpsplit

подумать "тысяча чертей, забыл из начала скрипта вставить переменные", вставить их, запустить заново, заметить что из-за пустого prefix потенциально что-то стёр, т.к. set -eu в консоль вставить забыл. Ну, с аргументом та же проблема, однако.

В общем:

  • я бы переписал на питон, из-за парсинга опций, но здесь половина действий - вызов команд, так что это нерационально.
  • прятал бы сложность куда-нибудь в функции, например вызов mysqldump с csplit спрятал бы в Поясняющую своим названием функцию-враппер.

Насчёт exit 0 - я плохо понимаю зачем делать exit 0 если я хочу возвращать код возврата последней команды, я не хочу его обрабатывать, пусть оно падает и рушит вместе с собой остальной скрипт.

adminadmin, 03.09.2018 07:08 (03.09.2018 07:38)
Первое: соотношение размера скрипта с приносимой пользой, 8 команд - 100 строк (хотя там комменты).

Это скрипт для разбора, поэтому оч.много комментов

Второе: переносы длинных строк не делают строку короче, концептуально это всё та же длинная строка.

Задача не короче, а читабельней с учетом особенностей мозга человека.

Вот это
error=$( git push origin master 2>&1 )\
        || echo "$error" | grep -qm1 'does not appear to be a git repositor' \
        || { echo  "$error"; exit 1; }
Лучше заменить на более читаемое (практически как английский язык):
if ! error=$( git push origin master 2>&1 ); then
    if ! echo "$error" | grep -qm1 'does not appear to be a git repositor'; then
        echo "$error"
        exit 1
    fi
fi
Или:
 
rc=0
error=$( git push origin master 2>&1 ) || rc=$?
if grep -qm1 'does not appear to be a git repositor' <<< "$error"; then
    echo "$error"
    exit 1
fi

Это был пример проверенного, понятного, разрешенного Сахара для короткой записи, не нравится не юзай)

Для более удобного управления потоком исполнения я бы вынес даже в функцию это, в итоге будет >отделена логика скрипта от деталей реализации и обработок ошибок:
commit() {
    local error
    error=$( git push origin master 2>&1 ) && return 0
    # тут бы я кстати пояснил _почему_ мы игнорируем эту ошибку
    grep -qm1 'does not appear to be a git repositor' <<< "$error" && return 0
    echo "$error"
    return 1
}

По сравнению с предложенным Сахаром сильное переусложнение, и имеет смысл скорей при многократном использовании.

>Третье: из-за размера теряется суть - что это вообще за скрипт и зачем он нужен и что делает.

Можно прояснить это с помощью оглавления-main():
main() {
    prepare
    dump_db
    dump_split
    commit_and_push
}

Я всегда тремя руками за такой подход. Fixed.

Четвёртое: моя вкусовщина - команды-методы зло по причине того, что их менее удобно отлаживать по кирпичикам. Вставить в консоль код и выполнить:
mysql_dumpsplit «$PREFIX»
проще, чем вставить в консоль код, выполнить:
mysql_dumpsplit
подумать «тысяча чертей, забыл из начала скрипта вставить переменные», вставить их, запустить >заново, заметить что из-за пустого prefix потенциально что-то стёр, т.к. set -eu в консоль >вставить забыл. Ну, с аргументом та же проблема, однако.

1. mysql_dumpsplit без импорта все равно не будет такой команды.
2. Передавать каждый раз глобальный проперти в метод это не удобно, тут надо понять объектный подход в скрипте или процедурный. Если объектный тот не передавать и рассматривать глобальные переменные как свойства объекта self-script. Если процедурный, то передавать. Смешивать не желательно, без необходимости. Лично я за объектный подход и этот скрипт пример этого подхода.

В общем: я бы переписал на питон, из-за парсинга опций, но здесь половина действий - вызов команд, так что это нерационально.

Писать такой скрипт на питоне нерационально и дольше в разы, авторазбор параметров есть в в либе carbon.sys, но здесь его не стал использовать, что было явнее.

прятал бы сложность куда-нибудь в функции, например вызов mysqldump с csplit спрятал бы в Поясняющую своим названием функцию-враппер.

В целом согласен. Fixed.

Насчёт exit 0 - я плохо понимаю зачем делать exit 0 если я хочу возвращать код возврата последней команды, я не хочу его обрабатывать, пусть оно падает и рушит вместе с собой остальной скрипт.

1. Скрипт и так упадет по set -e и даст код возврата команды падения.
2. exit 0 обязателен потому что, если в конце будет условие или выход из цикла и забудешь написать exit 0, то вернется не ноль и все упадет.
Всегда надо делать exit 0 и return 0 и не думать каждый раз - это позволит избежать кучу проблем и освобождает мозг.

Олег СтрижеченкоОлег Стрижеченко, 08.11.2018 06:48
тут надо понять объектный подход в скрипте или процедурный.

Понял почему мне не нравится объектный подход в bash-скриптах:

  1. При развитии скрипта будет неудобно экспортировать «методы» в отдельные скрипты, т.к. придётся всё таки организовывать передачу глобальных проперти через аргументы и менять в многих местах, вместо простой замены вызова метода на вызов утилиты (возможно даже одноимённой и находящейся в PATH, так вообще не надо будет ничего менять).
  2. Если скрипт не будет развиваться - не совсем понятно зачем туда вообще ООП притащили.
adminadmin, 13.11.2018 11:48

Олег, в старых правилах все так и было как ты описываешь, хочешь процедурно - делай никто не запрещает.

При развитии скрипта

Ноль один и бесконечность, у 99% скриптов нет развития на подскрипты при продуманной архитектуре.

глобальных проперти через аргументы и менять в многих местах

Менять только в одном месте убрать тело функции, сделать там вызов внешний проги да и все делов то.

зачем туда вообще ООП притащили

Потому что это удобно, я же пытаюсь объяснить когда глобальные переменные это не зло, тк они не глобальные переменные, а тождественны проперти.

Anton KlinskihAnton Klinskih, 11.07.2018 04:32 (03.09.2018 06:12)

set -euEo pipefail Вроде как так уже нельзя писать.

adminadmin, 03.09.2018 06:13

Fixed

set -euE

adminadmin, 03.09.2018 06:08 (03.09.2018 06:12)
Этот подход здесь не имеет смысла
error=$( git commit -am $(date +%Y-%m-%d_%H-%M) 2>&1 ) \
        || [[ "$error" == *'nothing to commit'* ]] || { echo  "$error"; exit 1; }
 
error=$( git push origin master 2>&1 )\
        || echo "$error" | grep -qm1 'does not appear to be a git repositor' \
        || { echo  "$error"; exit 1; }

тк можно сделать

git remote show origin && git push origin master
git status || git commit -m 123
adminadmin, 03.09.2018 06:12 (03.09.2018 06:12)

fixed

Ваш комментарий. Вики-синтаксис разрешён: