Практическое написание сценариев командной оболочки Bash/Код-сниппеты: различия между версиями
Практическое написание сценариев командной оболочки Bash/Код-сниппеты (править)
Версия от 18:55, 11 октября 2021
, 1 год назад→Своя реализация утилиты column
==== Своя реализация утилиты column ====
Код следующей функции написан на языке интерпретатора и позволяет рисовать таблицы. Код функции написан по меркам Bash очень сложно; местами автор просто продублировал некоторые фрагменты. Вероятно этот код не будет работать корректно во всех версиях Bash, но по крайней мере в Bash 4.4.19(1)-release он работает правильно.
Функция получилась такой большой из-за стремления наделить ее множеством опций для рисования таблиц в разных стилях. В отличие от команды <code>column</code>, этой функции нужно знать заранее сколько заголовков будет в таблице. Пользователь передает настройки каждого заголовка с помощью следующих опций:
* <code>--add-column <текст заголовка></code>. Объявляет новую колонку. Некоторые опции могут вызываться только следом за этой (обычно из контекста понятно, какие это опции). У этой опции есть обязательный аргумент в виде текста для заголовка столбца. Строка заголовка в принципе может быть пустой и содержать пробелы.
* <code>--column-alignment-title <left|center|right></code>. Задает выравнивание текста в заголовке столбца. По умолчанию текст выравнивается по левому краю.
* <code>--column-alignment <left|center|right></code>. Задает выравнивание содержимого столбца. По умолчанию текст выравнивается по левому краю.
* <code>--column-width <число></code>. Задает максимальную ширину столбца в символах. По умолчанию это значение равно 20. В функции не реализовано вычисление оптимальной ширины, поэтому вы должны брать ширину с некоторым запасом. Если данные будут превышать максимальную ширину, то это будет приводить к перекосам.
Обратите внимание на то, что опции с настройками колонки применяются к последней объявленной колонке.
Следующие опции настраивают таблицу в целом и могут быть в любом месте:
* <code>--underline-titles <yes|no></code>. Включает подчеркивание заголовков символом. По умолчанию значение этой опции <code>no</code>.
* <code>--underline-titles-char <символ></code>. Позволяет указать каким символом подчеркивать заголовки. По умолчанию это символ тире (<code>-</code>). Эта опция будет иметь эффект, если режим подчеркивания заголовков включен.
* <code>--sorting-column <число></code>. Позволяет указать колонку (начиная с 1), по которой нужно сортировать данные. По умолчанию сортировка делается по первой колонке. Эта опция имеет эффект, если опция сортировки включена. Если вы укажите номер колонки, которой нет в таблице, то это будет интерпретироваться как 1.
* <code>--default-width-between-columns <число></code>. Позволяет определить ширину между колонками. По умолчанию это значение равно 2.
* <code>--default-column-width <число></code>. Позволяет задать максимальную ширину колонки по умолчанию. Это значение будет использоваться, если вы не указали ширину некоторой колонки явно. По умолчанию это значение равно 20.
* <code>--default-left-margin <число></code>. Позволяет задать отступ всей таблицы от левого края экрана. По умолчанию это значение равно 2.
* <code>--sorting-options <строка></code>. Функция использует утилиту <code>sort</code> для сортировки. Введенная строка передается утилите транзитом. Таким образом, вы можете управлять сортировкой. По умолчанию утилите передается только опция <code>--dictionary-order</code>.
* <code>--default-data-delim <строка></code>. Позволяет задать другой разделитель полей. По умолчанию это <code>IFS</code>.
* <code>--hide-header</code>. Переключатель. Позволяет отключить вывод шапки таблицы. По умолчанию вывод шапки таблицы включен.
* <code>--crop-to-width</code>. Переключатель. Позволяет обрезать содержимое колонок по их максимальной ширине. По умолчанию данные выводятся в колонку как они есть, но без начальных и завершающих пробелов.
* <code>--disable-sort</code>. Переключатель. Позволяет отключить сортировку. По умолчанию сортировка включена. Вы можете заметить, что даже с небольшими данными таблица будет рисоваться с небольшой паузой в начале — это работает сортировка. Если она вам не требуется, то отключите ее этой опцией.
Данные функции следует передавать через стандартный дескриптор ввода. Если не указать дескриптор, то функция зависнет на вызове <code>read</code>, поэтому будьте внимательны с этим. Функция работает следующим образом: она читает строку и разбивает ее по разделителю на части, затем каждую часть она пытается положить в свою колонку. Если входящих данных больше чем колонок, то функция будет переносить их на следующую строку таблицы, не нарушая общее количество колонок. По этой причине функцией можно пользоваться для разбивки текста по указанному числу колонок.
Напротив, если данных окажется меньше, чем колонок, то функция оставит оставшиеся колонки на строке незаполненными.
;Код
<source lang=bash line=1>
table() {
local -A __settings=(
['underline-titles']='no'
['underline-titles-char']='-'
['sorting-column']='1'
['sorting-options']='--dictionary-order'
['default-column-width']='20'
['default-width-between-columns']='2'
['default-left-margin']='2'
['hide-header']='no'
['crop-to-width']='no'
[col_set]=''
)
local -a __titles=()
local -i cur_col=-1
local key
while [[ -n "$1" ]]; do
key=${1#--}
case "$1" in
# Creating columns
--add-column)
: $((++cur_col))
shift
[[ $1 =~ (^--.*|^-.*) ]] && {
echo "Wrong value for --${key}. Expected: <string> for the column's title."
return 1
}
__titles+=("$1")
__settings[col_set]="${__settings[col_set]};"
;;
--column-alignment-title)
[[ $cur_col -ge 0 ]] || {
echo "Error: You didn't create any columns. Use '--add-column' first."
return 1
}
shift
[[ $1 =~ (^--.*|^-.*) || ! $1 =~ (left|right|center) || ${#1} -eq 0 ]] && {
echo "Wrong value for --${key}. Expected: left|right|center"
return 1
}
__settings[col_set]="${__settings[col_set]}t:${1:0:1},"
;;
--column-alignment)
[[ $cur_col -ge 0 ]] || {
echo "Error: You didn't create any columns. Use '--add-column' first."
return 1
}
shift
[[ $1 =~ (^--.*|^-.*) || ! $1 =~ (left|right|center) || ${#1} -eq 0 ]] && {
echo "Wrong value for --${key}. Expected: left|right|center"
return 1
}
__settings[col_set]="${__settings[col_set]}a:${1:0:1},"
;;
--column-width)
[[ $cur_col -ge 0 ]] || {
echo "Error: You didn't create any columns. Use '--add-column' first."
return 1
}
shift
[[ $1 =~ (^--.*|^-.*) || ! $1 =~ ^[0-9]*$ || ${#1} -eq 0 ]] && {
echo "Wrong value for --${key}. Expected: <number>"
return 1
}
__settings[col_set]="${__settings[col_set]}w:${1},"
;;
# Common management
--underline-titles)
shift
[[ $1 =~ (^--.*|^-.*) || ! $1 =~ (yes|no) || ${#1} -eq 0 ]] && {
echo "Wrong value for --${key}. Expected: yes|no"
}
__settings[$key]=$1
;;
--underline-titles-char)
shift
[[ $1 =~ (^--.*|^-.*) || ${#1} -eq 0 ]] && {
echo "Wrong value for --${key}. Expected: <character>"
return 1
}
__settings[$key]=${1:0:1}
;;
--sorting-column | --default-width-between-columns | --default-column-width | --default-left-margin)
shift
[[ $1 =~ (^--.*|^-.*) || ! $1 =~ ^[0-9]*$ || ${#1} -eq 0 ]] && {
echo "Wrong value for --${key}. Expected: <number>"
return 1
}
__settings[$key]=${1}
;;
--sorting-options)
shift
[[ $1 =~ (^--.*|^-.*) || ${#1} -eq 0 ]] && {
echo "Wrong value for --${key}. Expected: <string>"
return 1
}
__settings[$key]=${1}
;;
--default-data-delim)
shift
[[ $1 =~ (^--.*|^-.*) ]] && {
echo "Wrong value for --${key}. Expected: <string>"
return 1
}
__settings[$key]=${1}
;;
--hide-header | --crop-to-width)
[[ ${__settings[$key]} == 'yes' ]] && __settings[$key]='no' || __settings[$key]='yes'
;;
--disable-sort)
__settings['sorting-options']=''
;;
esac
shift
done
## Helpers
__helper_field_drawer() {
[[ $# -ge 2 ]] || return 1
local align=${3}
if [[ ${#align} == 0 ]]; then align='--right'; fi
local length="$1"
local f
case "$align" in
--right)
f='%*s'
printf $f $length "$2"
;;
--left)
f='%-*s'
printf $f $length "$2"
;;
--center)
f='%*s'
local col=$((($length + ${#2}) / 2))
printf $f $col "$2"
printf $f $(($length - $col))
;;
esac
return 0
}
__helper_line_drawer() {
local c=${1}
local l=${2}
local line
printf -v line '%*s' $l
line=${line// /$c}
echo "$line"
}
## Drawing
local counter=0
local wdth align
if [[ ${#__titles[@]} -eq 0 ]]; then
echo "Error: Bad table: the table has not any columns."
return 1
fi
__settings[col_set]=${__settings[col_set]#?}
if [[ ${__settings['hide-header']} == 'no' ]]; then
while read -d ';' || [[ -n $REPLY ]]; do
wdth=$(echo -n "$REPLY" | grep -oE "w:[[:digit:]]+" | grep -oE "[[:digit:]]+")
wdth=${wdth:-${__settings['default-column-width']}}
[[ $wdth -ge ${#__titles[$counter]} ]] || wdth=${#__titles[$counter]}
align=$(echo -n "$REPLY" | grep -oE "t:[rlc]" | grep -oE "[rlc]")
if [[ $align == r ]]; then
align='--right'
elif [[ $align == c ]]; then
align='--center'
else
align='--left'
fi
if [[ $counter -eq 0 ]]; then
printf "%s%s" "$(__helper_line_drawer ' ' ${__settings['default-left-margin']})" "$(__helper_field_drawer $wdth "${__titles[$counter]}" ${align})"
else
printf "%s%s" "$(__helper_line_drawer ' ' ${__settings['default-width-between-columns']})" "$(__helper_field_drawer $wdth "${__titles[$counter]}" ${align})"
fi
: $((counter++))
done <<<"${__settings[col_set]}"
if [[ ${__settings['underline-titles']} != 'no' && -n ${__settings['underline-titles-char']} ]]; then
counter=0
echo
while read -d ';' || [[ -n $REPLY ]]; do
wdth=$(echo -n "$REPLY" | grep -oE "w:[[:digit:]]+" | grep -oE "[[:digit:]]+")
wdth=${wdth:-${__settings['default-column-width']}}
[[ $wdth -ge ${#__titles[$counter]} ]] || wdth=${#__titles[$counter]}
align='--left'
if [[ $((counter++)) -eq 0 ]]; then
printf "%s%s" "$(__helper_line_drawer ' ' ${__settings['default-left-margin']})" "$(__helper_line_drawer "${__settings['underline-titles-char']}" $wdth)"
else
printf "%s%s" "$(__helper_line_drawer ' ' ${__settings['default-width-between-columns']})" "$(__helper_line_drawer "${__settings['underline-titles-char']}" $wdth)"
fi
done <<<"${__settings[col_set]}"
fi
fi
local line
local -a arr_col_set
counter=0
while read -d ';' || [[ -n $REPLY ]]; do
arr_col_set[$((counter++))]=$REPLY
done <<<"${__settings[col_set]}"
counter=0
if [[ -n ${__settings['sorting-options']} ]]; then
[[ ${__settings['sorting-column']} -gt ${#__titles[@]} ]] && __settings['sorting-column']=1
local sorter="sort ${__settings['sorting-options']} -k ${__settings['sorting-column']}"
fi
if [[ -n $sorter ]]; then
while IFS= read -r line <&4 || [[ -n $line ]]; do
counter=0
local tail="$line"
while
IFS="${__settings['default-data-delim']:-$IFS}"
read -r line tail <<<"$tail"
[[ -n $line ]]
do
(($counter != 0)) || echo
wdth=$(echo -n "${arr_col_set[$counter]}" | grep -oE "w:[[:digit:]]+" | grep -oE "[[:digit:]]+")
wdth=${wdth:-${__settings['default-column-width']}}
align=$(echo -n "${arr_col_set[$counter]}" | grep -oE "a:[rlc]" | grep -oE "[rlc]")
if [[ $align == r ]]; then
align='--right'
elif [[ $align == c ]]; then
align='--center'
else
align='--left'
fi
line=${line#"${line%%[![:space:]]*}"}
line=${line%"${line##*[![:space:]]}"}
if [[ ${__settings['crop-to-width']} == 'yes' ]]; then
line=${line:0:$wdth}
fi
if [[ $counter -eq 0 ]]; then
printf "%s%s" "$(__helper_line_drawer ' ' ${__settings['default-left-margin']})" "$(__helper_field_drawer $wdth "${line}" ${align})"
else
printf "%s%s" "$(__helper_line_drawer ' ' ${__settings['default-width-between-columns']})" "$(__helper_field_drawer $wdth "${line}" ${align})"
fi
: $((counter++))
(($counter > ${#__titles[@]} - 1)) && {
counter=0
}
done
done 4<&0 | $sorter
else
while IFS= read -r line <&4 || [[ -n $line ]]; do
counter=0
local tail="$line"
while
IFS="${__settings['default-data-delim']:-$IFS}"
read -r line tail <<<"$tail"
[[ -n $line ]]
do
(($counter != 0)) || echo
wdth=$(echo -n "${arr_col_set[$counter]}" | grep -oE "w:[[:digit:]]+" | grep -oE "[[:digit:]]+")
wdth=${wdth:-${__settings['default-column-width']}}
align=$(echo -n "${arr_col_set[$counter]}" | grep -oE "a:[rlc]" | grep -oE "[rlc]")
if [[ $align == r ]]; then
align='--right'
elif [[ $align == c ]]; then
align='--center'
else
align='--left'
fi
line=${line#"${line%%[![:space:]]*}"}
line=${line%"${line##*[![:space:]]}"}
if [[ ${__settings['crop-to-width']} == 'yes' ]]; then
line=${line:0:$wdth}
fi
if [[ $counter -eq 0 ]]; then
printf "%s%s" "$(__helper_line_drawer ' ' ${__settings['default-left-margin']})" "$(__helper_field_drawer $wdth "${line}" ${align})"
else
printf "%s%s" "$(__helper_line_drawer ' ' ${__settings['default-width-between-columns']})" "$(__helper_field_drawer $wdth "${line}" ${align})"
fi
: $((counter++))
(($counter > ${#__titles[@]} - 1)) && {
counter=0
}
done
done 4<&0
echo
fi
}
</source>
;Примеры
<source lang=bash>
sed '1d' "contacts.txt" | table \
--disable-sort \
--add-column "NAME" \
--column-alignment-title center \
--column-width 40 \
--column-alignment right \
--add-column "DATE OF BIRTH" \
--column-width 14 \
--column-alignment center \
--add-column "POSITION" \
--column-alignment-title center \
--add-column "PHONE" \
--column-alignment-title center \
--column-width 8 \
--default-data-delim ':' \
--underline-titles yes
</source>
<source lang=bash>
# Без сортировки
NAME DATE OF BIRTH POSITION PHONE
---------------------------------------- -------------- -------------------- --------
Alice Brown 1989/04/03 Accountant 555-1268
Samanta Smith 1995/12/01 Copywriter 555-1233
John Berkley 1969/06/12 Boss 555-1201
Matthew Tucker 1988/11/01 Technician 555-1230
# Уберем опцию --disable-sort
NAME DATE OF BIRTH POSITION PHONE
---------------------------------------- -------------- -------------------- --------
Alice Brown 1989/04/03 Accountant 555-1268
John Berkley 1969/06/12 Boss 555-1201
Samanta Smith 1995/12/01 Copywriter 555-1233
Matthew Tucker 1988/11/01 Technician 555-1230
# Попробуем отсортировать по дням рождений --sorting-column 2
NAME DATE OF BIRTH POSITION PHONE
---------------------------------------- -------------- -------------------- --------
John Berkley 1969/06/12 Boss 555-1201
Alice Brown 1989/04/03 Accountant 555-1268
Samanta Smith 1995/12/01 Copywriter 555-1233
Matthew Tucker 1988/11/01 Technician 555-1230
# Попробуем сдвинуть таблицу немного вправо --default-left-margin 16 и поменяем символ заголовка --underline-titles-char '*'
NAME DATE OF BIRTH POSITION PHONE
**************************************** ************** ******************** ********
John Berkley 1969/06/12 Boss 555-1201
Alice Brown 1989/04/03 Accountant 555-1268
Samanta Smith 1995/12/01 Copywriter 555-1233
Matthew Tucker 1988/11/01 Technician 555-1230
# Наконец отключим заголовок --hide-header
John Berkley 1969/06/12 Boss 555-1201
Alice Brown 1989/04/03 Accountant 555-1268
Samanta Smith 1995/12/01 Copywriter 555-1233
Matthew Tucker 1988/11/01 Technician 555-1230
</source>
=== Анимации ===
|