Мы не могли утаить от вас эти сокровища! Работая над вторым изданием книги, мы написали несколько дополнительных сценариев на всякий случай. В итоге они нам не понадобились, но нам не хотелось хранить их в секрете от наших читателей.
Первые два дополнительных сценария предназначены для системных администраторов, которым приходится управлять перемещением или обработкой большого количества файлов. Последний сценарий предназначен для пользователей Интернета, ищущих веб-службы, которые так и просят преобразовать их в сценарий командной оболочки. На этот раз мы использовали сайт, помогающий следить за фазами Луны!
Системным администраторам часто приходится перемещать большое количество файлов из одной системы в другую, и вполне обычное дело, когда файлы в новой системе требуют совершенно иной схемы именования. Когда файлов немного, это легко можно сделать вручную, но, когда требуется переименовать сотни и тысячи файлов, решение задачи лучше поручить сценарию командной оболочки.
Простой сценарий в листинге Б.1 принимает два аргумента с текстом для поиска и замены и список файлов, которые требуется переименовать (может также включать шаблонные символы).
Листинг Б.1. Сценарий bulkrename
#!/bin/bash
# bulkrename -- переименовывает указанные файлы, замещая текст в их именах.
printHelp()
{
echo "Usage: $0 -f find -r replace FILES_TO_RENAME*"
echo -e "\t-f The text to find in the filename"
echo -e "\t-r The replacement text for the new filename"
exit 1
}
while getopts "f:r:" opt
do
case "$opt" in
r ) replace="$OPTARG" ;;
f ) match="$OPTARG" ;;
? ) printHelp ;;
esac
done
shift $(( $OPTIND - 1 ))
if [ -z $replace ] || [ -z $match ]
then
echo "You need to supply a string to find and a string to replace";
printHelp
fi
for i in $@
do
newname=$(echo $i | sed "s/$match/$replace/")
mv $i $newname
&& echo "Renamed file $i to $newname"
done
Первой идет функция printHelp() , которая выводит список требуемых аргументов и назначение сценария, после чего завершает сценарий. Код, следующий за определением функции, выполняет обход аргументов, переданных сценарию с помощью getopts , и, используя прием, демонстрировавшийся в предыдущих сценариях, присваивает переменным replace и match значения соответствующих аргументов командной строки.
Затем сценарий проверяет наличие значений для дальнейшего использования. Если переменная replace или match имеет нулевую длину, сценарий выводит сообщение об ошибке, извещая пользователя, что тот должен передать строку для поиска и строку замены, а затем вызывает функцию printHelp и завершается.
Убедившись в наличии значений в переменных match и replace, сценарий приступает к обработке остальных аргументов , в которых должны передаваться имена файлов для переименования. Мы использовали sed для замены строки match строкой replace в именах файлов и сохраняем новое имя файла в переменной. После сохранения нового имени файла сценарий вызывает команду mv для перемещения файла с новым именем и затем выводит сообщение, извещающее пользователя, что файл переименован.
Сценарий bulkrename принимает два строковых аргумента и имена файлов для переименования (которые могут содержать шаблонные символы). Если указаны недопустимые аргументы, выводится дружественное сообщение со справкой, как показано в листинге Б.2.
Листинг Б.2. Запуск сценария bulkrename
$ ls ~/tmp/bulk
1_dave 2_dave 3_dave 4_dave
$ bulkrename
You need to supply a string to find and a string to replace
Usage: bulkrename -f find -r replace FILES_TO_RENAME*
-f The text to find in the filename
-r The replacement text for the new filename
$ bulkrename -f dave -r brandon ~/tmp/bulk/*
Renamed file /Users/bperry/tmp/bulk/1_dave to /Users/bperry/tmp/bulk/1_brandon
Renamed file /Users/bperry/tmp/bulk/2_dave to /Users/bperry/tmp/bulk/2_brandon
Renamed file /Users/bperry/tmp/bulk/3_dave to /Users/bperry/tmp/bulk/3_brandon
Renamed file /Users/bperry/tmp/bulk/4_dave to /Users/bperry/tmp/bulk/4_brandon
$ ls ~/tmp/bulk
1_brandon 2_brandon 3_brandon 4_brandon
Файлы для переименования можно перечислить по отдельности или использовать шаблонный символ звездочки (*) в пути, как в примере команды . После перемещения имя каждого файла выводится на экран вместе с новым именем, чтобы пользователь мог убедиться, что все выполнено верно.
Иногда возникает необходимость заменить текст в имени файла специальной строкой, например текущей датой или отметкой времени (timestamp), чтобы обозначить, когда они были переименованы, не указывая дату в аргументе -r. Для этого достаточно добавить в сценарий поддержку специальных лексем для замены при переименовании файлов. Например, лексемы %d или %t в строке замены можно было бы заменять текущей датой или отметкой времени, соответственно, в момент переименования файлов.
Специальные лексемы, подобные этим, могут упростить перемещение файлов с целью резервного копирования. Если вы добавите в cron задание, перемещающее определенные файлы с использованием динамических лексем, для которых подстановка значений будет выполняться автоматически, вам не придется обновлять задание cron, когда понадобится изменить дату в именах файлов.
Во времена, когда вышло первое издание этой книги, многоядерные или многопроцессорные системы были редкостью. В основном к таким системам относились серверы и большие ЭВМ. В наши дни большинство ноутбуков и настольных компьютеров оснащается многоядерными процессорами, позволяющими компьютерам решать несколько задач одновременно. Но некоторые программы не способны использовать дополнительные вычислительные ресурсы и выполняются только на одном ядре; чтобы задействовать несколько ядер, нужно запустить сразу несколько экземпляров программы.
Допустим, что у вас есть программа для преобразования изображений из одного формата в другой и вам требуется обработать большое количество файлов. Последовательное (поочередное, а не параллельное) преобразование всех файлов единственным процессом может занять много времени. Работа завершится намного быстрее, если разбить файлы между несколькими процессами, работающими параллельно.
Сценарий в листинге Б.3 демонстрирует, как распараллелить заданную команду по нескольким процессам, выполняющимся одновременно.
ПРИМЕЧАНИЕ
Если ваш компьютер оснащен одноядерным процессором или выбранная программа работает медленно по другим причинам, например из-за невысокой скорости доступа к жесткому диску, запуск сразу нескольких экземпляров программы только ухудшит производительность. Будьте осторожны, не запускайте слишком много процессов, так как это легко может застопорить маломощную систему. К счастью, даже Raspberry Pi имеет несколько ядер!
Листинг Б.3. Сценарий bulkrun
#!/bin/bash
# bulkrun -- выполняет обход файлов в каталоге и запускает несколько
# процессов для их одновременной обработки.
printHelp()
{
echo "Usage: $0 -p 3 -i inputDirectory/ -x \"command -to run/\""
echo -e "\t-p The maximum number of processes to start concurrently"
echo -e "\t-i The directory containing the files to run the command on"
echo -e "\t-x The command to run on the chosen files"
exit 1
}
while getopts "p:x:i:" opt
do
case "$opt" in
p ) procs="$OPTARG" ;;
x ) command="$OPTARG" ;;
i ) inputdir="$OPTARG" ;;
? ) printHelp ;;
esac
done
if [[ -z $procs || -z $command || -z $inputdir ]]
then
echo "Invalid arguments"
printHelp
fi
total=$(ls $inputdir | wc -l)
files="$(ls -Sr $inputdir)"
for k in $(seq 1 $procs $total)
do
for i in $(seq 0 $procs)
do
if [[ $((i+k)) -gt $total ]]
then
wait
exit 0
fi
file=$(echo "$files" | sed $(expr $i + $k)"q;d")
echo "Running $command $inputdir/$file"
$command "$inputdir/$file"&
done
wait
done
Сценарий bulkrun принимает три аргумента: максимальное количество процессов, действующих в каждый конкретный момент времени , каталог с файлами для обработки и команду для запуска (в конец которой добавляется имя файла для обработки) . После чтения пользовательских аргументов с помощью getopts сценарий убеждается в наличии всех трех обязательных аргументов. Если любая из переменных — procs, command или inputdir — осталась неопределенной, сценарий выводит сообщение об ошибке , текст справки и завершает выполнение.
После проверки переменных, необходимых для управления запуском параллельных процессов, сценарий приступает к выполнению фактической работы. Для начала определяется количество обрабатываемых файлов , и их список сохраняется для дальнейшего использования. Затем сценарий входит в цикл for, который следит за количеством файлов, обработанных к текущему моменту. Команда seq используется для организации итераций по всем файлам с шагом, равным максимальному числу процессов, которые могут выполняться параллельно.
Внутри находится еще один цикл for , который следит за количеством процессов, запущенных к данному моменту. Этот внутренний цикл также использует команду seq для организации итераций от 0 до максимального числа процессов с шагом, по умолчанию равным 1. В каждой итерации внутренний цикл for извлекает новый файл из списка , вызывая sed, чтобы вывести только требуемое имя файла из списка, сохраненного в начале сценария, и запускает указанную команду с этим файлом в фоновом режиме с применением знака &.
После запуска максимального количества процессов сценарий вызывает команду wait , которая приостанавливает его выполнение до момента, когда завершатся все команды, запущенные в фоновом режиме. После того как wait завершится, вновь начинается процедура запуска процессов для обработки остальных файлов. Она похожа на процедуру выбора лучшей программы сжатия в сценарии bestcompress (сценарий № 34 в главе 4).
Пользоваться сценарием bulkrun очень просто. Ему нужно передать три аргумента: максимальное количество одновременно действующих процессов, каталог с файлами для обработки и запускаемую команду. Например, в листинге Б.4 показано, как изменить размеры изображений в каталоге, запустив одновременно несколько экземпляров утилиты mogrify из пакета ImageMagick.
Листинг Б.4. Запуск команды bulkrun для параллельной обработки файлов изображений утилитой mogrify из пакета ImageMagick
$ bulkrun -p 3 -i tmp/ -x "mogrify -resize 50%"
Running mogrify -resize 50% tmp//1024-2006_1011_093752.jpg
Running mogrify -resize 50% tmp//069750a6-660e-11e6-80d1-001c42daa3a7.jpg
Running mogrify -resize 50% tmp//06970ce0-660e-11e6-8a4a-001c42daa3a7.jpg
Running mogrify -resize 50% tmp//0696cf00-660e-11e6-8d38-001c42daa3a7.jpg
Running mogrify -resize 50% tmp//0696cf00-660e-11e6-8d38-001c42daa3a7.jpg
--опущено--
Часто бывает полезно иметь возможность указать имя файла внутри команды или использовать лексемы, подобные тем, что упоминались в описании сценария bulkrename (сценарий № 102 выше): специальные строки, динамически заменяемые во время выполнения фактическими значениями (например, %d можно было бы заменять текущей датой, или %t отметкой текущего времени). Реализация поддержки подобных специальных лексем в команде или в именах файлов стала бы ценным усовершенствованием сценария.
Другим полезным усовершенствованием было бы использование утилиты time для определения продолжительности обработки всех файлов. Вывод статистической информации о количестве обработанных файлов или файлов, ожидающих обработки, тоже мог бы пригодиться в случае выполнения массивных заданий.
Если вы оборотень, ведьма или просто интересуетесь лунным календарем, вам наверняка пригодится полезная и познавательная возможность определять фазы Луны.
Вся сложность в том, что Луна совершает полный оборот вокруг Земли за 27,32 дня. И ее фаза фактически зависит от вашего местоположения на Земле. Однако эта зависимость невелика, так что фазы Луны можно определять только по заданной дате.
Но зачем разбираться со всеми этими сложностями, если в Интернете полно сайтов, которые вычисляют фазу Луны для любой даты в прошлом, в настоящем или в будущем? Для сценария в листинге Б.5 мы используем для этого тот же сайт, что использует Google: http://www.moongiant.com/.
Листинг Б.5. Сценарий moonphase
#!/bin/bash
# moonphase -- сообщает фазу Луны (в действительности процент
# освещенности) на текущую или указанную дату.
# Формат запроса к Moongiant.com:
# http://www.moongiant.com/phase/MM/DD/YYYY
# Если дата не указана, использовать текущую дату.
if [ $# -eq 0 ] ; then
thedate="today"
else
# Дата указана. Проверить правильность ее формата.
mon="$(echo $1 | cut -d/ -f1)"
day="$(echo $1 | cut -d/ -f2)"
year="$(echo $1 | cut -d/ -f3)"
if [ -z "$year" -o -z "$day" ] ; then # Нулевая длина?
echo "Error: valid date format is MM/DD/YYYY"
exit 1
fi
thedate="$1" # Отсутствие проверки ошибок = опасность.
fi
url="http://www.moongiant.com/phase/$thedate"
pattern="Illumination:"
phase="$( curl -s "$url" | grep "$pattern" | tr ',' '\
' | grep "$pattern" | sed 's/[^0-9]//g')"
# Сайт возвращает ответ в формате: "Illumination: <span>NN%\n<\/span>"
if [ "$thedate" = "today" ] ; then
echo "Today the moon is ${phase}% illuminated."
else
echo "On $thedate the moon = ${phase}% illuminated."
fi
exit 0
По аналогии с другими сценариями, извлекающими информацию из веб-запросов, основу сценария moonphase составляет код конструирования URL запроса и извлечения требуемого ответа из потока данных в формате HTML.
Анализ сайта показал, что поддерживается два вида адресов URL: один определяет текущую дату и имеет вид «phase/today», а другой — дату в прошлом или в будущем в формате MM/DD/YYYY, например: «phase/08/03/2017».
Задайте дату в правильном формате, и вы получите фазу Луны в указанный день. Но мы не можем просто добавить дату к доменному имени сайта, не выполнив некоторые проверки. С этой целью сценарий разбивает ввод пользователя на три поля — месяц, день и год — и затем проверяет строковые значения дня и года на ненулевую длину . В сценарий можно было бы добавить еще несколько проверок на ошибки, о которых рассказывается в разделе «Усовершенствование сценария».
Самая хитрая часть любого сценария, извлекающего информацию из Интернета — правильное определение шаблона, позволяющего получить желаемые данные. В сценарии moonphase этот шаблон определяется в строке . Самой длинной и сложной строкой () сценарий получает страницу с сайта moongiant.com и затем, с помощью последовательности команд grep и sed, извлекает строку, соответствующую заданному шаблону.
После этого остается только вывести освещенности для текущей или для указанной даты с использованием заключительной инструкции if/then/else.
При вызове без аргумента сценарий moonphase выводит освещенности для текущей даты. Попробуйте указать любой день в прошлом или в будущем в формате MM/DD/YYYY, как показано в листинге Б.6.
Листинг Б.6. Запуск сценария moonphase
$ moonphase 08/03/2121
On 08/03/2121 the moon = 74% illuminated.
$ moonphase
Today the moon is 100% illuminated.
$ moonphase 12/12/1941
On 12/12/1941 the moon = 43% illuminated.
ПРИМЕЧАНИЕ
12 декабря 1941 года кинокомпания Universal выпустила на экраны кинотеатров классический фильм ужасов «The Wolf Man» («Человек-волк»). И в этот день Луна не была полной. Проверьте!
С точки зрения внутренней реализации, сценарий можно было бы существенно усовершенствовать, добавив дополнительные проверки на ошибки или даже просто задействовав сценарий № 3 из главы 1, чтобы дать пользователям возможность вводить даты в разных форматах. Также инструкцию if/then/else в конце неплохо было бы заменить функцией, преобразующей значение процента освещенности в более общепринятые названия фаз луны, такие как «убывающая», «растущая» или номер четверти. На сайте NASA имеется веб-страница, которую можно использовать для определения разных фаз Луны: http://starchild.gsfc.nasa.gov/docs/StarChild/solar_system_level2/moonlight.html.