Командный интерпретатор Bash и нестандартное

Автор: © Уильям Парк (William Park)
Перевод: © Иван Песин

О данной серии статей

В этой серии ежемесячных статей, я попробую продемонстрировать мощь командного интерпретатора Bash. В частности, читатели познакомятся с проектом Bash.Diff, который представляет собой коллекцию моих патчей, реализующих различные идеи из Ksh, Zsh, Awk, Python и других языков.

Каждая статья будет посвящена одной теме или возможности, которая обычно не рассматривается в контексте командного интерпретатора. Также я предоставляю в свободное использование приведённые функции командного интерпретатора в виде, удобном для использования и поддержки.

string.sh

В языке C, <string.h> определяет функции strcat(3), strcpy(3), strlen(3) и strcmp(3) для конкатенации, копирования, определения размера и сравнения строк соответственно. Такие базовые операции постоянно нужны при программировании на любом языке и скрипты командного интерпретатора не исключение.

strcat() и strcpy()

Для копирования и конкатенации строк в командном интерпретаторе, вам нужно будет сделать что-то вроде:
    a=abc
a=${a}'123' # a=abc123
Это простое присвоение переменной. Однако, вы не можете использовать в левой части присвоения переменную, как ссылку на другую переменную. Вам придется либо указывать непосредственно имя переменной, либо использовать команду eval, например следующим образом:
    x=a
eval "$x=abc"
eval "$x=\${$x}'123'"
Однако, такие конструкции использовать быстро надоедает, особенно когда имена переменных читаются из файла или какой-либо строки.

Всё что требуется -- это аналог функций strcat(3) и strcpy(3) языка C, которые позволят выполнять описанные задачи гораздо проще. Вот реализация указанных функций:

    strcat ()		# var+=string
{
local _VAR=$1 _STRING=$2 _a _b

case $#.$3 in
2.) ;;
3.*:*) _a=${3%:*} _b=${3#*:}
set -- `python_to_shell_range "$_a" "$_b" ${#_STRING}`
_STRING=${_STRING:$1:$2}
;;
*) echo "Usage: strcat var string [a:b]"
return 2
;;
esac
eval "$_VAR=\${$_VAR}\$_STRING"
}

strcpy () # var=string
{
local _VAR=$1 _STRING=$2 _a _b

case $#.$3 in
2.) ;;
3.*:*) _a=${3%:*} _b=${3#*:}
set -- `python_to_shell_range "$_a" "$_b" ${#_STRING}`
_STRING=${_STRING:$1:$2}
;;
*) echo "Usage: strcpy var string [a:b]"
return 2
;;
esac
eval "$_VAR=\$_STRING"
}
где 'var' -- это имя переменной, в которой нужно сохранить результат. Теперь, вышепреведённый пример можно реализовать следующим образом:
    x=a
strcpy $x abc # a=abc
strcat $x 123 # a+=123

strlen()

В языке C, функция strlen(3) возвращает размер строки. В командном интерпретаторе для этого используется конструкция вида ${#var}:

    a=abc123
echo ${#a} # 6
Вот реализация функции strlen(3) :
    strlen ()		# echo ${#string} ...
{
for i in "$@"; do
echo ${#i}
done
}
Эта фунция позволяет передавать более одного аргумента:
    strlen abc123 0123456789		# 6 10

strcmp()

Для проверки равенства двух строк в языке С используется функция strcmp(3). В скриптах, конструкция вида

    [ $a = abc123 ]
Ниже приведена версия strcmp(3):
    strcmp ()		# string == string
{
local _STRING1=$1 _STRING2=$2 _a _b

case $#.$3 in
2.) ;;
3.*:*) _a=${3%:*} _b=${3#*:}
set -- `python_to_shell_range "$_a" "$_b" ${#_STRING1}`
_STRING1=${_STRING1:$1:$2}
set -- `python_to_shell_range "$_a" "$_b" ${#_STRING2}`
_STRING2=${_STRING2:$1:$2}
;;
*) echo "Usage: strcmp string1 string2 [a:b]"
return 2
;;
esac
[ "$_STRING1" == "$_STRING2" ]
}
Теперь можно использовать вызов
    strcmp $a abc123

Работа с подстрокой [a:b] в стиле Python

Извлечение подстроки -- ещё одна распространённая операция. В шелле для этого используется конструкция ${var:a:n}, где 'a' -- начальная позиция, а 'n' -- количество извлекаемых символов. Таким образом,

    b=0123456789
echo ${b:0:3} ${b: -3} ${b:1:${#b}-2}
напечатает первые 3 символа, последние 3 символа и все символы, кроме первого и последнего.

Основной проблемой является то, что 'n' -- это относительное число символов начиная с позиции 'a'. Обычно, абсолютный индекс более удобен и не только потому, что это более естественно, но и потому, что так сделано в языке C. В Python используется синтаксис var[a:b], где 'a' и 'b' -- индексы, которые могут быть положительными, отрицательными либо вообще опущены. И хотя это приблизительно эквивалентно шелловскому ${var:a:b-a}, отсутствующие 'a' и 'b' означают начало и конец строки, а отрицательные значения -- смещение от конца строки.

Вышепреведённые функции strcat(), strcpy() и strcmp() поддерживают формат [a:b] в стиле Python, используя функцию

    # string[a:b] --> ${string:a:n}
#
# Convert Python-style string[a:b] range into Shell-style ${string:a:n} range,
# where
# 0 <= a <= b <= size and a + n = b
#
python_to_shell_range ()
{
local -i size=$3
local -i b=${2:-$size}
local -i a=${1:-0}

if [ $# -ne 3 ]; then
echo "Usage: python_to_shell_range a b size"
return 2
fi

[[ a -lt 0 ]] && a=$((a+size))
[[ a -lt 0 ]] && a=0
[[ a -gt size ]] && a=$size

[[ b -lt 0 ]] && b=$((b+size))
[[ b -lt 0 ]] && b=0
[[ b -gt size ]] && b=$size
[[ b -lt a ]] && b=$a

echo $a $((b-a))
}
для конвертации диапазона в стиле Python, в представление шелла. Она не очень удобна для постоянного прямого использования, но можете попробовать:
    python_to_shell_range '' 3 10		# 0 3
python_to_shell_range -3 '' 10 # 7 3
python_to_shell_range 1 -1 10 # 1 8
Теперь, вы можете третьим параметром указать функциям strcat(), strcpy() и strcmp() диапазон подстроки в стиле Python [a:b], например:
    b=0123456789
strcpy x $b :3 # x=012
strcpy y $b -3: # y=789
strcpy z $b 1:-1 # z=12345678
echo $x $y $z

Цепочки тестов

Функция strcmp() проверяет на равенство две строки. Когда же необходимо выполнить последовательность из двух или более двоичных тестов, например 'a < c > b' или '1 -lt 3 -gt 2', приходится их разбивать на части и проверять каждую пару:

    [[ a < c ]] && [[ c > b ]]
[ 1 -lt 3 ] && [ 3 -gt 2 ]
Это разрушает структуру вашего кода и часто вносит ошибки. Вот функция, которая позволяет просто записывать последовательные сравнения в одной строке:
    testchain ()		# string OP string OP string ...
{
if [ $# -lt 3 ]; then
echo "Usage: testchain string OP string [OP string ...]"
return 2
fi
while [ $# -ge 3 ]; do
test "$1" "$2" "$3" || return 1
shift 2
done
}
где 'OP' -- любой двоичный оператор, понимаемый командой test. Используется функция наподобие команды test:
    testchain a '<' c '>' b
testchain 1 -lt 3 -gt 2

Заключение

Исходный код шести функций командного интерпретатора, приведённых в этой статье, доступен здесь. Чтобы использовать их, просто укажите в начале скрипта:
    . string.sh
В следующей статье, мы посмотрим как можно написать функции strcat(), strcpy(), strlen() и strcmp() на языке C и скомпилировать как встроенные команды шелла. И это будет введением в мой улучшенный командный интерпретатор Bash. :-)


[BIO] Я изучал Unix, используя Bourne shell. И, после моего путешествия сквозь множество языков, я сделал круг, вернувшись к шеллу. С недавних пор я занимаюсь разработкой новой функциональности Bash, которая делает его еще более опасным конкурентом других скриптовых языков. С самого начала я использовал дистрибутив Slackware, так как там я могу всё делать руками. Мои рабочие инструменты -- это Vim, Bash, Mutt, Tin, TeX/LaTeX, Python, Awk, Sed. Даже командная строка у меня находится в режиме Vi.

Copyright © 2004, William Park. Released under the Open Publication license. Linux Gazette is not produced, sponsored, or endorsed by its prior host, SSC, Inc.

Published in Issue 108 of Linux Gazette, November 2004