Читаем INI-файл: что может быть проще?

  • BASH
Как ни странно, INI-файлы читаются на BASH не сложнее, чем на любом другом языке, а по мне — так даже элегантнее.

Вот вам пример, который можно сделать ещё короче, если немного покумекать:


#!/bin/bash
read_ini () {
  local file="$1"
  local arr_name="$2"
  local -A ini_conf
  while read l; do
    if [[ $l =~ ^\[([^]]+)\] ]]; then
            section=${BASH_REMATCH[1]}
    elif [[ $l =~ ^([^=[:space:]]+)[[:space:]]*=[[:space:]]*(.+)$ ]]; then
            source <(cat <<CODE
            ini_conf["${section:+${section}.}${BASH_REMATCH[1]}"]=${BASH_REMATCH[2]}
CODE
)   
    fi
  done < <(sed -r -e '/^\s*([#;].*)$/d' -e 's%^\s+%%; s%\s+$%%' "$file")
  local t=$(declare -p ini_conf)
  [[ $arr_name ]] && echo ${t/declare -A ini_conf=/declare -A ${arr_name}=} || echo "$t"
}
 
read_ini "$@"


Дерзайте и дерзновенны будете! К чему бы это я? А к тому, что source в BASH — обалденно мощная и удобная штука, намного лучше eval, вынуждающего экранировать кавычки и далеко не всегда дающего адекватные результаты. Не стесняйтесь чаще и больше пользоваться source'ом!

Собственно, именно source'ом и нужно «подобрать» вывод функции read_ini, дабы получить ассоциативный массив, имя которого передаётся вторым параметром.

Возникли вопросы? Пишите в комментариях — буду рад ответить.
За сим до новых встреч!

Если пробел встал у вас на пути. Как правильно перейти в каталог (рас|место)положения скрипта?

  • BASH
Вы всё ещё делаете это так?


cd $(dirname $(readlink -e $0))


Это ленивый вариант, который работает «в большинстве случаев». Однако же он не является верным, поскольку пути (пусть это не покажется вам странным) в общем случае могут содержать пробельные символы.

Надо бы вот так:

cd "$(eval "readlink -e '$0'" | sed -r -e 's%(^|/)[^/]+$%%' -e 's%([^/])/+$%\1%')"


Попробуйте — это просто работает как надо, сколько бы ни встретилось пробелов на тернистом пути вашего скрипта.

Идеальный PS4

  • BASH
Нашёл я как-то на просторах интернет интереснейший вариант наполнения префикса PS4 полезным содержимым. Считаю необходимым сделать эту информацию достоянием широкой общественности, а посему спешу поделиться ею со своими читателями, дабы и они тоже смогли проникнуться всей силой и мощью set -x:


export PS4='+(${BASH_SOURCE}: #${LINENO}): ${FUNCNAME[0]:-main}(): '


Напомню: PS4 — это префиксный кусок текста, добавляемый в каждую интерполированную строку, которую BASH выводит в режиме отладки/трассировки, включаемый по команде set -x и отключаемый по команде set +x соответственно.

Быстрой вам отладки и доброго продакшна!

Как собрать в архив все файлы rpm-пакета?

  • BASH
В продолжение аналогичного поста о dpkg предлагаю использовать следующий «наивный» вариант скрипта, собирающего те файлы, которые были скопированы в систему при установке rpm-пакета. Файлы, созданные автоматически INSTALL-скриптом пакета при этом «подобраны» не будут.

Для запуска скрипта нужно иметь возможность выполнять sudo под суперпользователем: это связано с тем, что немалая часть пакетов устанавливает файлы с эксклюзивным доступом к ним root'а: в таких случаях попытка создать tar-архив провалиться с треском.

Собственно код:

#!/bin/bash
[[ $1 == '-x' ]] && { shift; export TRACE=1; set -x; }
[[ $1 ]] || { echo 'You must specify package name or path to some file installed from the package' >&2; exit 1; }

if [[ ${1:0:1} == '/' ]]; then
        FILE=$1
        if ! PACKAGE=$(rpm -qf $(readlink -e $FILE)) 2>/dev/null || ! [[ $PACKAGE ]]; then
                echo 'Cant determine package name by file name' >&2
                exit 2
        fi
else
        PACKAGE=$1
        if ! PACKAGE=$(rpm -q $PACKAGE); then
                echo "This package seems to be not installed" >&2
                exit 3
        fi
fi

sudo bash <<EOSCRIPT
        tar -cjf "/tmp/${PACKAGE}.tbz2" -T <(
          while read f; do
                  [[ -d \$f ]] || echo "\$f"
          done < <(rpm -ql '${PACKAGE}')
        )
        chown "$(whoami)":"$(id -gn)" "/tmp/${PACKAGE}.tbz2"
        echo "/tmp/${PACKAGE}.tbz2 created" >&2 
EOSCRIPT


UPD: Поправил код скрипта, избавив его от 2-х вызовов sudo и пофиксив ошибки отсутствия присвавивания FILE=$1, а также ошибку сохранения короткого имени пакета, если было передано именно таковое.

Do You Know That? Как раскрывается "${array[@]}" и "${array[*]}"

  • BASH
Оказывается, в зависимости от режима интерполяции, внутри двойных кавычек массивы раскрываются принципиально по разному:

  • В случае с "${array[*]}" — массив интерполируется в один аргумент (одну строку), представляющий собой результат простой конкатенации всех элементов массива через пробел
  • В случае "${array[@]}" — каждый элемент массива становится отдельным аргументом так, словно каждый элемент взял себе внешние двойные кавычки, что уберегло его от дробления по пробельному символу (точнее, по IFS)

Тестовый пример:

Читать дальше →

Вдогонку к Tips&Tricks'у #10h

  • BASH
Конечно же, для функции join2 должна быть и комплементарная split.

Учитывая то, что просто split — это «split a file into pieces», а также принимая во внимание тот факт, что split-функция всё-таки будет писать в переменную-массив, а не просто на STDOUT, я добавил к привычному имени циферку 2, дабы получилось у нас «SplitTo (the) arrayName».

Ну а теперь код:

split2 () {
        local arrName=$1
        [[ $arrName =~ ^[0-9_A-Za-z]+$ ]] || return 1
        local delim=$2
        shift 2
        local args
        while (( $# )); do
         args+=${args:+$delim}$1
         shift
        done  
        readarray -t $arrName < <(echo -n "${args//$delim/$'\x0a'}")
        return $?
}


Пример использования:

split2 dir '/' '/usr/share/doc/LaTeX' 'a/b' 'c/d/e f g/h'
declare -p dir
# OUTPUT:
# declare -a dir='([0]="" [1]="usr" [2]="share" [3]="doc" [4]="LaTeX" [5]="a" [6]="b" [7]="c" [8]="d" [9]="e f g" [10]="h")'


Enjoy! :)

BASH Tips&Tricks #0010: И на BASH'е будут join'иться массивы!

  • BASH
Когда я пишу на Perl, у меня буквально всё под рукой. Кое-что даже мешается и заставляет постоянно проверять кусочки кода в командной строке (perl -e или perl -E), дабы сделать полёт моей фантазии более комфортным и безопасным для здоровья разрабатываемой софтины.

Когда я пишу на BASH, мне, конечно же, многого не хватает: необъятные возможности работы с текстом в Perl'е накладывают свой отпечаток на отношение к другим языкам. Кое-что приходится терпеть, но многое вполне можно исправить, предоставив самому себе привычное окружение, пусть и несколько… задумчивое («бытует мнение», что в случае с BASH скоростью работы обычно можно пренебречь).

Так случилось и с функцией join, которой мне всегда так не хватало в BASH. Я, конечно же, говорю не о той join, которая «join lines of two files on a common field», а об одноимённой функции Perl, «склеивающей» элементы массива (если быть точным, то всё-таки «вектора») в строку.

Я долго терпел сие неудобство и всячески его игнорировал, но… в какой-то момент терпение моё лопнуло, и я решился на сотворение мира в атомарных масштаба. По итогам краткого ознакомления с бесплодными исканиями на StackOverflow и захватывающе познавательного обсуждения join-вопросов с умными людьми © на linux.org.ru, мною был исторгнут приведённый ниже код:

join2 () {
 (( $# == 2 || $# == 3 )) || return 1
 local delim=$1
 local arrName=$2
 local join2var=$3
 local v
 for v in ${join2var:+join2var} arrName; do
  [[ ${!v} =~ ^[_0-9A-Za-z]+$ ]] || return 2
 done
 [[ $join2var ]] || join2var='v'
 source <(
        cat <<SOURCE
         printf -v $join2var "%s${delim}" "\${$arrName[@]}" || return \$?
         [[ \$delim ]] && $join2var=\${$join2var:0:-${#delim}}
SOURCE
 )
 (( $# == 2 )) && echo "$v"
 return 0
}


Бесспорно, он (код) как всегда великолепен и безупречен… пока мною же не будет доказано обратное :)

Квинтэссенция сути функции join2 (звучит как «Join To») заключается в том, что встроенная команда BASH printf понимает конструкцию ${arr[@]} не как уже интерполированную строку из элементов массива, объединённых абы чем (пробелами), но как именно набор элементов. Что эта белиберда значит? А то, что если какой-либо элемент массива уже содержит пробелы, а его объединят пробелом с элементом соседним, printf всё равно «увидит» каждый элемент в отдельности.

Пользоваться join2 можно двояко:
  1. 
    declare -a arr=('a b c' 'd e f' 'g h i')
    join2 '///' arr
    

    — и в этом случае результат конкатенации элементов массива будет «выведен на экран», то есть отправлен на STDOUT, где его легко подобрать так:
    str=$(join2  '///' arr)


    ИЛИ… (барабанная дробь, фанфары!)
  2. 
    declare -a arr=('a b c' 'd e f' 'g h i')
    join2 '///' arr str
    echo "$str"
    

    — а в этом случае название функции окажется весьма неслучайным, поскольку результат join'а запишется сразу в переменную str.

Cакральное знание об особенностях работы printf с массивами в BASH нагло сп скромно почёрпнуто из Yet Another BASH FAQ'а.

Пример преобразования форматов на BASH 4

  • BASH
Если кто-нибудь скажет Вам, что BASH — это какой-то недостойный внимания недоязык для написания циклов из командочек, киньте ему ссылку на данный пример.

Пример совсем несложный, я только что написал его для StackOverflow (где, к сожалению, люди всё ещё живут в криогенных камерах, не ведая о существовании BASH 4-й версии):

#!/bin/bash
inFile=$1
outFile=$2

join () {
 local del=$1
 shift
 IFS="$del"
 source <(
        cat <<SOURCE
 echo "\${$1[*]}"
SOURCE
 ) 
 unset IFS
}

declare -a CSV=('"Module Name","Module Group","Module Version"')
declare -a keysAccepted=('Name' 'Group' 'Version')

declare -i nMandatoryKeys=${#keysAccepted[@]}
declare -A KeyFilled
rxKeysAccepted='('$(join '|' keysAccepted)')'
while read line; do
        [[ $line =~ \<strong\>Module\ $rxKeysAccepted:\</strong\>[[:space:]]*([^<]+)\</p\> ]] || continue
        key=${BASH_REMATCH[1]}
        val=${BASH_REMATCH[2]}
        KeyFilled[$key]=$val
        if (( ${#KeyFilled[@]} == nMandatoryKeys )); then
                unset csvLine
                for k in ${keysAccepted[@]}; do
                        csvLine+=${csvLine:+,}${KeyFilled[$k]}
                done
                KeyFilled=()
                CSV+=($csvLine)
        fi
done <"$inFile"

(( ${#CSV[@]} > 1 )) || exit 1

join $'\x0a' CSV >"$outFile"


BASH-скрипт состоит из sed'ов, awk и grep'ов?
Давайте подсчитаем количество вхождений того, другого и третьего в данном примере!

Не спорю, тот же самый код на Perl, лучшем языке для работы с текстами, получился бы и короче намного, и быстрее.

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

Получение информации о процессах в стиле BASH 4

  • BASH
При написании BASH-скриптов распространённой практикой является считывание любой информации о процессах с помощью утилиты ps в сочетании с awk. В большинстве случаев это делается не ради кроссплатформенности (ps весьма ограниченно кроссплатформенна), а просто в силу непонимания возможностей BASH и принципиального отсутствия желания писать на нём более-менее «традиционный» код вместо нагромождения pipe'ов.

В данном посте я покажу, что код на BASH может оперировать вполне «традиционными» собственными структурами данных — и при этом с минимальным использованием внешних утилит.

Читать дальше →

Как собрать все файлы deb-пакета в tar-архив?

  • BASH
Если у вас есть устанволенный deb-пакет и хочется аккуратно собрать все его файлы в архив TAR, то сделать это можно вот так:


PACKAGE='libmysqlclient-dev'
tar -cjf /tmp/$PACKAGE.tbz2 -T <(while read f; do [[ -d $f ]] || echo "$f"; done < <(dpkg -L $PACKAGE))


Бывает, что права root'а недоступны, пакеты deb вы нормальным образом установить не сможете (в том числе и любые инструменты сборки из исходников) и тогда подобный «перенос в tar-архивах» становится единственной возможностью для работы в сильно стеснённых условиях. При этом если вы оставите все пересённые tar-архивы на целевой системе, то сможете с помощью опции -t получить список файлов архива и, соответственно, при необходимости вычистить ненужное.

Этакая эмуляция «пакетной системы» Slackware получается :)

P.S.
Думаю, несложно догадаться, что приведённый ниже скрипт соберёт в архив все файлы, установленные из rpm-пакета, в котором был /usr/sbin/zabbix_proxy:


FILE=/usr/sbin/zabbix_proxy
PACKAGE=$(rpm -qf $(readlink -e $FILE))
[[ $? == 0 && $PACKAGE ]] && \
    sudo bash -c 'tar -cjf /tmp/'$PACKAGE'.tbz2 -T <(while read f; do [[ -d $f ]] || echo "$f"; done < <(rpm -ql '$PACKAGE')); echo "/tmp/'$PACKAGE'.tbz2 created"'

Генерация случайного числа в диапазоне от min до max

  • BASH

min=121
max=257
v=$(( min + ($RANDOM*(max-min))>>15 ))


Работает по принципу:

v=min+rnd/32768*(max-min)

Но вместо деления используются побитовые сдвиги.
Недостаток неочевиден, но он есть: v никогда не сможет быть равным max, поскольку в действительности $RANDOM принимает значения от 0 до 32767, а v=max только если бы $RANDOM=32768. Причём если зазор между max и min значительный, то кроме самого max не смогут быть «достигнуты» и зачения меньше max в ближайшей окрестности.
Тем не менее, это самый простой и самый эффективный генератор случайных чисел в заданном диапазоне, недостатки которого, как водится, напрямую вытекают из его достоинств, а уж вам выбирать, что важнее. Для диапазонов, по размаху значительно меньше 32768, погрешность не должна быть критична.

Скажи мне "да" или "нет" и дай мне свой ответ! :)

  • BASH
Поскольку люди, которые на BASH'е пишут многословно и ужасно, уже откровенно (достали), предлагаю свой вариант элементарной функции чтения односложного ответа пользователя.

read_yn () {
 local yn dflt='Y'  
 if [[ ${#1} == 1 && ${1^} =~ ^[YN]$ ]]; then   
  shift           
  dflt=${1^}
 fi
 while :; do
  echo -en "$@"
  read yn; yn=${yn:0:1}; yn=${yn^}
  if [[ $yn =~ ^[NY]$ || ! $yn ]]; then
   yn=${yn:-$dflt}
   break
  fi
 done
 echo -n $yn
 [[ $yn == 'Y' ]]
 return $?
}


Большинство людей почему-то думают, что BASH-это какой-то примитивный язык, из которого можно только «командочки запускать», нечто вроде продвинутого языка командных сценариев DOS. Безусловно, право каждого думать как ему угодно и каждый сам себе злой Буратино, поскольку как правило хреново написанный код на BASH не переживает просмотра первым же вменяемым Perl-программистом, поскольку у того просто ломается мозг и он всё переписывает по-своему. Но хотя бы ради мира на Земле — не ленитесь, читайте man bash перед сном и во время ритуальной чистки зубов. Это очень помогает не писать какую-то «ребячливую» чушь вместо кода на добротном, пусть и специализированном, языке программирования.