Пишем многопоточный скрипт на bash

















Скажу сразу, что топик посвящён только пользователям *nix систем, в которых есть bash и присутствует утилита GNU parallel. Пользователи Windows могут тоже найти здесь что-то интересное, но они должны учитывать отличия bash скриптов от bat файлов. Статья рассчитана на начинающих, но не на полных новичков, потому элементарные вещи, такие как получение имён и путей файлов подробно рассматриваться не будут. Я лишь кратко напомню о них. Для примера напишем скрипт, оптимизирующий изображения для интернет страничек и удаляющий из них всю лишнюю информацию, такую, как EXIF. Возможно есть более удобные способы написания такого скрипта, но мне они не известны, если Вы знаете, как написать скрипт лучше (сохранив при этом совместимость с Linux), выложите вашу версию в комментариях, я обязательно посмотрю её. Скрипт проверялся только на трёх Linux дистрибутивах: Ubuntu 13.04, Ubuntu 13.10 и Gentoo. Сжатие изображений без потери качества — дело медленное, потому разумно запустить конвертирование в несколько потоков, что даёт значительный прирост (кратный количеству ядер процессора) на многопроцессорных и многоядерных системах. Сразу оговорюсь, что инструменты, которыми я буду пользоваться однопоточные, потому мы будем запускать одновременно несколько процессов обработки (по одному на каждый файл, но в то же время так, чтоб их не было больше, чем доступных машине ядер процессора, GNU Parallel без нас «знает» сколько ядер процессора у нашего ПК и в этом примере нагрузит их все). Вот собственно код скрипта:

#!/bin/bash

opt_optipng ()
{
input="$1"
echo "Обрабатывается: (optipng) $input"
optipng -o3 "$input" >/dev/null 2>&1
}

opt_advpng ()
{
input="$1"
echo "Обрабатывается (advpng): $input"
advpng -z -4 "$input">/dev/null 2>&1
}

opt_advdef ()
{
input="$1"
echo "Обрабатывается (advdef): $input"
advdef -z -4 "$input">/dev/null 2>&1
}

opt_jpegtran ()
{
input="$1"
echo "Обрабатывается: $input"
jpegtran -optimize -progressive -copy none -outfile "$input" "$input" 2>&1
}

opt_gifsicle ()
{
input="$1"
echo "Обрабатывается (gifsicle): $input"
gifsicle --batch --optimize=3 "$input" >/dev/null 2>&1
}

SIZEPNG=$(find . -type f \( -iname '*.png' -o -iname '*.PNG' \) -printf '%s\n' | awk '{SUM+=$1} END {print SUM}');
SIZEGIF=$(find . -type f \( -iname '*.gif' -o -iname '*.GIF' \) -printf '%s\n' | awk '{SUM+=$1} END {print SUM}');
SIZEJPG=$(find . -type f \( -iname '*.JPG' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.JPEG' \) -printf '%s\n' | awk '{SUM+=$1} END {print SUM}')
SIZE=$[SIZEPNG+SIZEGIF+SIZEJPG]

export -f opt_optipng
find -type f | cut -c 3- | grep -ie '.png$' | sort | parallel opt_optipng '{}'
export -f opt_advpng
find -type f | cut -c 3- | grep -ie '.png$' | sort | parallel opt_advpng '{}'
export -f opt_advdef
find -type f | cut -c 3- | grep -ie '.png$' | sort | parallel opt_advdef '{}'
export -f opt_jpegtran
find -type f | cut -c 3- | grep -e '.JPG$' -e '.jpg$' -e '.jpeg$' -e '.JPEG$' | sort | parallel opt_jpegtran '{}'
export -f opt_gifsicle
find -type f | cut -c 3- | grep -ie '.gif$' | parallel opt_gifsicle '{}'

echo "Готово!"
SIZEPNG2=$(find . -type f \( -iname '*.png' -o -iname '*.PNG' \) -printf '%s\n' | awk '{SUM+=$1} END {print SUM}');
SIZEGIF2=$(find . -type f \( -iname '*.gif' -o -iname '*.GIF' \) -printf '%s\n' | awk '{SUM+=$1} END {print SUM}');
SIZEJPG2=$(find . -type f \( -iname '*.JPG' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.JPEG' \) -printf '%s\n' | awk '{SUM+=$1} END {print SUM}')
SIZE2=$[SIZEPNG2+SIZEGIF2+SIZEJPG2]
if [ $SIZEPNG ]; then
PERCENTPNG=$[100-SIZEPNG2*100/SIZEPNG]
fi
if [ $SIZEGIF ]; then
PERCENTGIF=$[100-SIZEGIF2*100/SIZEGIF]
fi
if [ $SIZEJPG ]; then
PERCENTJPG=$[100-SIZEJPG2*100/SIZEJPG]
fi
if [ $SIZE ]; then
PERCENT=$[100-SIZE2*100/SIZE]
echo "Исходный размер всех обрабатываемых файлов был $SIZE байт а после обработки составил $SIZE2 байт. Среднее сжатие на $PERCENT%
Из них:"
fi
if [ $SIZEPNG ]; then
echo "PNG: было: $SIZEPNG байт, стало $SIZEPNG2 байт, сжалось на $PERCENTPNG%"
fi
if [ $SIZEGIF ]; then
echo "GIF: было: $SIZEGIF байт, стало $SIZEGIF2 байт, сжалось на $PERCENTGIF%"
fi
if [ $SIZEJPG ]; then
echo "JPG: было: $SIZEJPG байт, стало $SIZEPJPG байт, сжалось на $PERCENTJPG%"
fi


Теперь более детально разберём структуру скрипта и его работу:


Скрипт состоит из 5 функций, выполняющих различные оптимизации изображений при помощи программ optipng, AdvanceCOMP, jpegtran и gifsicle. Подробно разбирать их параметры мы не будем, для этого есть man-ы. Скажу, что на текущий момент такое их использование даёт наиболее сильное сжатие, какого мне удалось добиться. Поиск файлов в директориях осуществляется при помощи команды find, find. -type f означает, что искать будем в директории, откуда запущен скрипт и только файлы, а не каталоги и ссылки. Командой sort выполняется сортировка, можно обойтись и без неё, но так вывод скрипта во время работы получается более удобным и информативным. Про echo, grep, cut и awk можете почитать самостоятельно, благо информации по их использованию на русском языке в сети достаточно много и ничего сложного в них нет. Теперь пару слов о функциях:

opt_optipng ()

{
input="$1"
echo "Обрабатывается: (optipng) $input"
optipng -o3 "$input" >/dev/null 2>&1
}


Это функция, код её очень прост. В качестве переменной $input она принимает первый аргумент, с которым её запустили, в нашем скрипте за это отвечает следующий код:

find  -type f | cut -c 3- | grep -ie '.png$' | sort | parallel opt_optipng '{}'


фигурные скобки {} требуются для GNU Parallel, именно там происходит «вставка» аргументов со стандартного ввода, а в нашем случае поступивших через пайп. Осталось обратить ваше внимание на последний, но очень важный момент. Перед запуском функции через GNU Parallel мы должны сделать её видимой для скрипта, как внешнюю команду. (GNU Parallel разрабатывался именно для запуска внешних программ в несколько потоков) Для этого служит строка

export -f opt_optipng


Названия для функции в таком случае надо выбирать такое, чтоб оно не совпадало со стандартными командами, такими как cat, grep, sed и т.д., во избежание недоразумений. После завершения скрипта, экспортированные командой export -f функции будут недоступны и не вызовут никаких проблем (новые команды терминала не появятся).



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