Автор: © Уильям Парк (William
Park)
Перевод: © Иван Песин
В этой серии ежемесячных статей, я попробую продемонстрировать мощь командного интерпретатора Bash. В частности, читатели познакомятся с проектом Bash.Diff, который представляет собой коллекцию моих патчей, реализующих различные идеи из Ksh, Zsh, Awk, Python и других языков.
Каждая статья будет посвящена одной теме или возможности, которая обычно не рассматривается в контексте командного интерпретатора. Также я предоставляю в свободное использование приведённые функции командного интерпретатора в виде, удобном для использования и поддержки.
В языке C, <string.h> определяет функции strcat(3), strcpy(3), strlen(3) и strcmp(3) для конкатенации, копирования, определения размера и сравнения строк соответственно. Такие базовые операции постоянно нужны при программировании на любом языке и скрипты командного интерпретатора не исключение.
a=abcЭто простое присвоение переменной. Однако, вы не можете использовать в левой части присвоения переменную, как ссылку на другую переменную. Вам придется либо указывать непосредственно имя переменной, либо использовать команду eval, например следующим образом:
a=${a}'123' # a=abc123
x=aОднако, такие конструкции использовать быстро надоедает, особенно когда имена переменных читаются из файла или какой-либо строки.
eval "$x=abc"
eval "$x=\${$x}'123'"
Всё что требуется -- это аналог функций strcat(3) и strcpy(3) языка C, которые позволят выполнять описанные задачи гораздо проще. Вот реализация указанных функций:
strcat () # var+=stringгде 'var' -- это имя переменной, в которой нужно сохранить результат. Теперь, вышепреведённый пример можно реализовать следующим образом:
{
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"
}
x=a
strcpy $x abc # a=abc
strcat $x 123 # a+=123
В языке C, функция strlen(3) возвращает размер строки. В командном интерпретаторе
для этого используется конструкция вида ${#var}
:
a=abc123Вот реализация функции strlen(3) :
echo ${#a} # 6
strlen () # echo ${#string} ...Эта фунция позволяет передавать более одного аргумента:
{
for i in "$@"; do
echo ${#i}
done
}
strlen abc123 0123456789 # 6 10
Для проверки равенства двух строк в языке С используется функция 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
Извлечение подстроки -- ещё одна распространённая операция. В шелле для этого
используется конструкция ${var:a:n}
, где 'a' -- начальная позиция,
а 'n' -- количество извлекаемых символов. Таким образом,
b=0123456789напечатает первые 3 символа, последние 3 символа и все символы, кроме первого и последнего.
echo ${b:0:3} ${b: -3} ${b:1:${#b}-2}
Основной проблемой является то, что '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}для конвертации диапазона в стиле Python, в представление шелла. Она не очень удобна для постоянного прямого использования, но можете попробовать:
#
# 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_to_shell_range '' 3 10 # 0 3Теперь, вы можете третьим параметром указать функциям strcat(), strcpy() и strcmp() диапазон подстроки в стиле Python [a:b], например:
python_to_shell_range -3 '' 10 # 7 3
python_to_shell_range 1 -1 10 # 1 8
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 ...где 'OP' -- любой двоичный оператор, понимаемый командой test. Используется функция наподобие команды test:
{
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
}
testchain a '<' c '>' b
testchain 1 -lt 3 -gt 2
. string.shВ следующей статье, мы посмотрим как можно написать функции strcat(), strcpy(), strlen() и strcmp() на языке C и скомпилировать как встроенные команды шелла. И это будет введением в мой улучшенный командный интерпретатор Bash. :-)
Я изучал Unix, используя Bourne shell. И, после моего путешествия сквозь
множество языков, я сделал круг, вернувшись к шеллу. С недавних пор я занимаюсь
разработкой новой функциональности Bash, которая делает его еще более опасным
конкурентом других скриптовых языков. С самого начала я использовал дистрибутив
Slackware, так как там я могу всё делать руками. Мои рабочие инструменты --
это Vim, Bash, Mutt, Tin, TeX/LaTeX, Python, Awk, Sed. Даже командная строка
у меня находится в режиме Vi.