- •§2 Unix Shell – лабораторные работы
- •Лабораторная работа №2: Написание скриптов. Практика использования Unix Shell. Обработка журнальных файлов.
- •Некоторые базовые элементы и средства языка Unix Shell
- •Конструкции языка Shell
- •Написание скриптов Unix Shell
- •Exit – завершение скрипта и выход. В качестве необязательного параметра можно указать код возврата. Подробнее см. Man exit.
- •Задание для выполнения лаб. Работы №2
- •Контрольные вопросы
операционные системы. UNIX Shell
§2 Unix Shell – лабораторные работы
Для выполнения лабораторной работы из любого раздела необходимо: 1) изучить соответствующий теоретический материал; 2) выполнить задание; 3) подготовить ответы на контрольные вопросы; 4) защитить работу, продемонстрировав ее преподавателю и ответив на его вопросы.
Лабораторная работа №2: Написание скриптов. Практика использования Unix Shell. Обработка журнальных файлов.
Некоторые базовые элементы и средства языка Unix Shell
Переменные
Как и большинство интерпретируемых языков программирования, Shell не поддерживает типы данных. Это позволяет программисту по-разному интерпретировать значение переменной в зависимости от ситуации. Все необходимые преобразования по приведению типов будут выполнены интерпретатором Shell автоматически. Такой подход существенно упрощает скрипты и повышает скорость их разработки. Минусом являются накладные расходы при неявном преобразовании типов.
Именем переменной может быть любая комбинация символов, не являющаяся зарезервированным словом (например if, for и т.д.).
В качестве оператора присваивания используется знак = (пробелы до и после знака не допускаются)
A=5; B=”test”;
Для получения значения переменной используется знак $.
A=4; B=$A; С=${B}; echo $A;
(В переменную А помещаем значение 4, в переменную B помещаем значение переменной А, в переменную C помещаем значение переменной B, выводим на консоль значение переменной А – будет выдано: 4)
Можно заключать имя переменной в фигурные скобки. Полезно в тех случаях, когда необходимо отделить имя переменной от следующего за ним текста.
A=10; echo “${A}ka”
(На экран будет выведено: 10ka. Если опустить фигурные скобки, то интерпретатор будет считать, что знак $ относится к строчке Aka, т.е. мы работаем со значением переменной “Aka”, а не “A”)
Длину переменной в символах можно получить при помощи ${#NAME}
A=”this is a long sequence”; echo “length of A = ${#A}”
(На экран будет выведено: length of A = 23)
Переменная создается автоматически при присваивании ей значения. Можно (но не обязательно) предварительно объявить переменную при помощи встроенной команды Shell declare.
declare A
declare –a B
A=5; B[0]=”test”
(создаем переменную А и массив B, заносим значение 5 в А и строчку test в нулевой элемент массива B)
Переменные окружения
Существует набор заранее определенных служебных переменных, которые называются «окружением» или «переменными окружения». Эти переменные содержат множество полезной для программ служебной информации, например: имя машины (HOSTNAME), версия ОС (OSTYPE), имя пользователя (USER), пути поиска исполняемых программ (PATH), настройки локализации (LANG), параметры консоли (COLUMNS, LINES) и т.д.
Список всех переменных окружения вместе с их значениями можно получить при помощи команды set.
Переменные окружения являются одним из способов организации передачи информации от родительского процесса к дочернему.
Программы (в том числе и скрипты на Shell) могут изменять значения переменных окружения, создавать новые переменные со своими значениями, и т.д. Все переменные по умолчанию «видны» только внутри того скрипта, где они были созданы и использованы. Для того, чтобы переменная была доступна дочернему процессу, необходимо предварительно выполнить экспортирование переменной, используя встроенную команду export, после чего можно запускать какой-либо дочерний процесс (например, другую программу или скрипт) – ему будет доступна переменная со своим значением. В противном случае (без предварительного экспортирования) дочерний процесс не увидит переменной.
export NAME=”Vasily Pupkin”
NAME2=”Ivan Ivanov”; export NAME2
./child_script.sh
(Переменным NAME и NAME2 присваиваются значения, после чего они экспортируются и запускается программа child_script.sh, для которой эти переменные будут доступны)
Некоторые специальные переменные
Shell поддерживает множество специальных переменных. Некоторые из них:
$? – код возврата. Хранит значение кода возврата предыдущей выполненной команды (программы). Часто используется для контроля за возникновением ошибок. Если код = 0, считается, что программа выполнилась успешно, это соглашение обычно выполняется для всех программ. Ненулевое значение говорит об ошибке. Разные ненулевые коды возврата могут означать различные ошибки в ходе выполнения программы и обычно описываются автором в документации. Один и тот же ненулевой код возврата в разных программах может означать совершенно разные события, выбор целиком зависит от фантазии и желания авторов.
$@ – переменная, хранящая одной строкой все параметры командной строки, переданные скрипту.
$0, $1, $2, $3, … – каждая из переменных хранит соответствующий ее номеру параметр командной строки, переданный скрипту. Счет параметров начинается с единицы. $0 содержит имя исполняемого файла самого скрипта.
$# – содержит количество параметров, переданных скрипту. Часто используется при разборе параметров: зная количество параметров можно для считывания и обработки параметров запустить цикл for, который выполнится $# раз
$_ – содержит имя последней выполненной команды (программы)
$RANDOM – специальная переменная, которая возвращает случайное число при каждом обращении
Массивы
Массив может быть определен с помощью declare –a <ИМЯ ПЕРЕМЕННОЙ> или при непосредственном присваивании значения элементам массива.
Индекс массива указывается в квадратных скобках. Элементы могут быть разного типа.
A[0]=5; A[1]=10; echo “A = ${A[0]} ${A[1]}”
(На экран будет выведено A = 5 10)
B[0]=5; B[1]=”test word”; echo “B = ${B[0]} ${B[1]}”
(На экран будет выведено B = 5 test word)
Специальный индекс * позволяет получить значение всех элементов массива одной строкой:
B[0]=5; B[1]=”test word”; echo “B = ${B[*]}”
(На экран будет выведено B = 5 test word)
Специальное обозначение ${#NAME[*]} позволяет получить количество элементов массива:
B[0]=5; B[1]=”test word”; echo “Array B has ${#B[*]} elements”
(На экран будет выведено Array B has 2 elements)
Арифметические выражения
Оболочка bash может производить арифметические вычисления в целых числах (не работает с числами с плавающей точкой). Для вычисления выражения его необходимо поместить внутрь конструкции $[ ].
A=2; B=3; C=$[${A} + 2*${B}]; echo ${C}
(В переменную С помещается результат сложения значения переменной А и удвоенного значения переменной B. На экран будет выведено: 8)
Кавычки
Знаки кавычек играют важную роль в Shell! Существует 3 типа кавычек:
Двойные кавычки: “”. Используются для запрета интерпретации специальных символов (см. Лаб. №1). Знак $ будет действовать внутри строки, заключенной в двойные кавычки.
[mike@bublik ~]$ a=5; echo "a=$a"
a=5
[mike@bublik ~]$
Одинарные кавычки: ‘’ Действуют «еще сильнее» двойных кавычек. Внутри одинарных кавычек не интерпретируются никакие символы или последовательности, строчка целиком воспринимается «как есть».
[mike@bublik ~]$ a=5; echo ‘a=$a’
a=$a
[mike@bublik ~]$
Обратные кавычки: `` В то место, где находится строчка, заключенная в обратные кавычки, подставляется результат ее выполнения (содержимое строки рассматривается Shell-ом как команда и исполняется)
[mike@bublik ~]$ date "+%d/%m/%Y %H:%M:%S"
03/03/2009 07:39:41
[mike@bublik ~]$ echo "Today is: `date "+%d/%m/%Y %H:%M:%S"`"
Today is: 03/03/2009 07:40:03
[mike@bublik ~]$
(Замечание: строка-параметр "+%d/%m/%Y %H:%M:%S" задает формат вывода данных командой date, что видно из примера. Подробнее см. man date)
Перенаправление ввода-вывода
Перенаправление и конвейер позволяют сохранять вывод программ в файлы, использовать файлы как источник для ввода данных из них в программы, организовать взаимодействие двух и более программ, передачу данных от предыдущей к следующей по цепочке и т.д. Реальное практическое применение Unix Shell невозможно без использования методов перенаправления потоков.
Любая программа (даже не работающая с файлами) всегда связана с тремя файлами (потоками):
Стандартный поток ввода. Имеет файловый дескриптор = 0. Часто называется stdin
Стандартный поток вывода. Имеет файловый дескриптор = 1. Часто называется stdout
Стандартный поток ошибок. Имеет файловый дескриптор = 2. Часто называется stderr
По умолчанию, все три дескриптора консольной программы связаны с терминалом: ввод данных на stdin производится с клавиатуры, при выводе данных на stdout и/или stderr информация попадает в терминал и отображается в терминальном окне.
Средства перенаправления Shell – это гибкий, прозрачный и незаметный для самой программы механизм, при помощи которого возможно изменение привязки дескрипторов, что позволяет получать входные данные на stdin из других источников (например, из файлов или от другой программы) и отправлять информацию, выдаваемую программой на stdout и/или stderr в альтернативный приемник (например, в файл, на другие дескрипторы или на stdin другой программы).
Перенаправление вывода:
[m]>name – перенаправить информацию, выводимую программой в поток с дескриптором №m, в файл с именем name. Если файл уже существовал до этого, то его содержимое будет перезаписано новыми данными. Если файл не существовал, то он будет создан, и данные будут записаны в него. Если m опущено ( >name), то полагается m=1, т.е. в файл перенаправляется информация, выводимая на stdout.
[m]>>name – перенаправить информацию, выводимую программой в поток с дескриптором №m, в файл с именем name. Если файл уже существовал до этого, то его содержимое будет дополнено новыми данными. Если файл не существовал, то он будет создан, и данные будут записаны в него. Если m опущено ( >>name), то полагается m=1, т.е. в файл перенаправляется информация, выводимая на stdout.
[m]>&n – перенаправить («перенести», а не «скопировать») информацию, выводимую программой на дескриптор №m, в поток с дескриптором №n. Если m опущено ( >&n), то полагается m=1, т.е. в поток с дескриптором №n перенаправляется информация, выводимая программой на stdout.
Серия примеров, демонстрирующих перенаправление вывода (примеры связаны, необходимо смотреть все по порядку сверху вниз):
(Создаем файл /tmp/test.txt)
[mike@bublik ~]$ touch /tmp/test.txt
(Проверяем, что файл создался. Программа ls выдает сообщение на stdout! Вывод идет на консоль, пока невозможно визуально определить, на какой именно дескриптор были выведены данные)
[mike@bublik ~]$ ls -1 /tmp/test.txt
/tmp/test.txt
(Проверяем существование файла /tmp/test2.txt. Файл не существует, сообщение об этом программа ls выдает на stderr! Вывод идет на консоль, пока невозможно визуально определить, на какой именно дескриптор были выведены данные)
[mike@bublik ~]$ ls -1 /tmp/test2.txt
ls: /tmp/test2.txt: No such file or directory
Выполняем те же действия, поочередно перенаправляя stdout и stderr в файлы:
(Перенаправляем stdout в файл /tmp/res1_1.txt, на консоль ничего не выдается, весь вывод перенаправился в файл)
[mike@bublik ~]$ ls -1 /tmp/test.txt >/tmp/res1_1.txt
(Перенаправляем stderr в файл /tmp/res1_2.txt, информация выдается на консоль, т.к. программа ls выводит ее в stdout, а не stderr)
[mike@bublik ~]$ ls -1 /tmp/test.txt 2>/tmp/res1_2.txt
/tmp/test.txt
(Перенаправляем stdout в файл /tmp/res2_1.txt, информация выдается на консоль, т.к. программа ls выводит ее в stderr, а не stdout)
[mike@bublik ~]$ ls -1 /tmp/test2.txt >/tmp/res2_1.txt
ls: /tmp/test2.txt: No such file or directory
(Перенаправляем stderr в файл /tmp/res2_2.txt, на консоль ничего не попадает, весь вывод ушел в файл)
[mike@bublik ~]$ ls -1 /tmp/test2.txt 2>/tmp/res2_2.txt
(Проверяем результаты: одни из созданных файлов оказались пустыми, т.к. в них не была перенаправлено ничего, в других сохранились перенаправленные данные)
[mike@bublik ~]$ cat /tmp/res1_1.txt
/tmp/test.txt
[mike@bublik ~]$ cat /tmp/res1_2.txt
[mike@bublik ~]$ cat /tmp/res2_1.txt
[mike@bublik ~]$ cat /tmp/res2_2.txt
ls: /tmp/test2.txt: No such file or directory
(Пробуем прочитать информацию о несуществующем файле /tmp/test2.txt, при этом ls выдаст сообщение на stderr, после чего информация с дескриптора №2 (stderr) переносится в дескриптор №1 (stdout), затем весь stdout перенаправляется в файл /tmp/res3.txt. Как видно, в результате в файле /tmp/res3.txt сохраняется сообщение, выданное ls)
[mike@bublik ~]$ ls -1 /tmp/test2.txt >/tmp/res3.txt 2>&1
[mike@bublik ~]$ cat /tmp/res3.txt
ls: /tmp/test2.txt: No such file or directory
[mike@bublik ~]$
Перенаправление ввода:
[m]<name – перенаправить содержимое файла с именем name в поток программы с дескриптором №m. Если m опущено ( <name), то полагается m=0, т.е. содержимое файла name попадает в stdin программы.
[m]<<word – перенаправлять текст, поступающий на stdin программы в поток программы с дескриптором №m до тех пор, пока в тексте не встретится слово word. Если m опущено ( <<word), то подразумевается m=0, т.е. текст перенаправляется с stdin в stdin программы (при m=0 метод может показаться глупым и ненужным, однако на самом деле он представляет собой весьма удобный прием, часто применяемый в скриптах для ввода большого количества текста на stdin программы, см. примеры)
Примеры, демонстрирующие перенаправление ввода (необходимо смотреть все и по порядку сверху вниз):
(Для начала запишем строчку в файл xx.txt)
[mike@bublik ~]$ echo "test string" >xx.txt
(Способ №1: применяем конвейер (про конвейер см. далее) для передачи содержимого файла xx.txt программе wc. Программа wc считает количество байт, поступивших на stdin и выводит число)
[mike@bublik ~]$ cat xx.txt | wc -c
12
(Способ №2: вместо запуска cat и передачи информации через конвейер выполним перенаправление ввода для передачи содержимого файла программе wc. Программа wc считает количество байт, поступивших на stdin и выводит число)
[mike@bublik ~]$ wc -c < xx.txt
12
(Программа cat получает данные с stdin до тех пор, пока не встретится слово END. Далее вывод программы cat перенаправляется в файл newfile.txt. В этом примере сами данные вводятся с клавиатуры)
[mike@bublik ~]$ cat >newfile.txt <<END
> test!
> END
(Проверка: вывод содержимого файла newfile.txt)
[mike@bublik ~]$ cat newfile.txt
test!
Другой вариант применения “<<” – в тексте внутри скрипта:
Способ №1 (без “<<”, при помощи многочисленных команд echo):
echo “Перенаправление и конвейер позволяют сохранять вывод программ”
echo “в файлы, использовать файлы как источник для ввода данных “
echo “из них в программы, организовать взаимодействие двух и “
echo “более программ, передачу данных от предыдущей к следующей “
echo “по цепочке и т.д.”
недостаток – необходимость писать команду echo для каждой строки (строк может быть много!!!), заключать каждую строчку в кавычки.
Способ №2 (с применением перенаправления ввода “<<”):
cat <<END
Перенаправление и конвейер позволяют сохранять вывод программ
в файлы, использовать файлы как источник для ввода данных
из них в программы, организовать взаимодействие двух и
более программ, передачу данных от предыдущей к следующей
по цепочке и т.д.
END
Очевидно, способ №2 более удобный. Текст пишется «как есть» внутри скрипта, после него дописывается признак завершения (в данном случае - строка “END”), все данные поступают на stdin команды cat, которая выводит их на экран.
Результат выполнения скриптов, описанных в обоих способах будет одним и тем же – текст выводится на экран.
Конвейер:
Символ конвейера “|” предназначен для связывания двух программ путем перенаправления потока stdout предыдущей команды в поток stdin следующей команды. Эта цепочка может быть продолжена и далее, т.о. конвейер позволяет выполнять достаточно сложные действия, связывая множество простых программ-компонентов путем передачи результатов работы предыдущей программы следующей программе и т.д. (МОДУЛЬНОСТЬ, ОДИН ИЗ ОСНОВОПОЛАГАЮЩИХ ПРИНЦИПОВ ОРГАНИЗАЦИИ UNIX-подобных систем)
Задача подсчета статистики входов пользователей в систему – несколько примеров, демонстрирующих использование конвейера:
Обратите внимание, в примерах постепенно шаг за шагом собирается цепочка из нескольких команд, связанных конвейерами, в каждом следующем примере к предыдущей цепочке добавляется новое звено и демонстрируется результат работы:
(Шаг №1. Команда last выводит информацию о входах пользователей в систему в виде множества строк, каждая строка содержит информацию об одном событии регистрации пользователя в системе)
[mike@bublik ~]$ last
mike ttyp1 10.10.65.11 вт 3 мар 02:34 still logged in
p73_13 ttyp2 89.251.107.24 вт 3 мар 01:38 - 01:45 (00:06)
mike ttyp1 10.10.65.11 вт 3 мар 01:00 - 02:34 (01:33)
mike ttyp3 10.10.65.11 вт 3 мар 00:12 - 00:56 (00:44)
p74_03 ttyp2 91.196.246.2 пн 2 мар 23:26 - 00:50 (01:24)
p74_03 ttyp2 91.196.246.2 пн 2 мар 23:19 - 23:22 (00:03)
mike ttyp1 10.10.65.11 пн 2 мар 21:51 - 00:12 (02:21)
p73_25 ttyp1 91.196.247.14 пн 2 мар 20:23 - 20:51 (00:27)
p73_25 ttyp1 91.196.247.14 пн 2 мар 20:15 - 20:22 (00:07)
mike ttyp0 10.10.65.11 пн 2 мар 19:23 still logged in
p74_03 ttyp0 91.196.246.2 пн 2 мар 16:30 - 17:08 (00:38)
p72_15 ttyp0 pi46-34-33.cn.ru пн 2 мар 01:33 - 02:03 (00:30)
p72_15 ttyp0 pi46-34-33.cn.ru вс 1 мар 22:32 - 22:40 (00:08)
p72_15 ttyp0 pi46-34-33.cn.ru вс 1 мар 22:27 - 22:29 (00:01)
p72_16 ttyp0 92.127.178.51 вс 1 мар 16:27 - 16:58 (00:30)
wtmp begins воскресенье, 1 марта 2009 г. 16:27:44 (NOVT)
(Шаг №2. При помощи grep убираем из предыдущего вывода ненужное: пустые строки и строки, содержащие подстрочку “wtmp begins”, таким образом остается только информация о пользователях)
[mike@bublik ~]$ last | grep -v -E "(^$|wtmp begins)"
mike ttyp1 10.10.65.11 вт 3 мар 02:34 still logged in
p73_13 ttyp2 89.251.107.24 вт 3 мар 01:38 - 01:45 (00:06)
mike ttyp1 10.10.65.11 вт 3 мар 01:00 - 02:34 (01:33)
mike ttyp3 10.10.65.11 вт 3 мар 00:12 - 00:56 (00:44)
p74_03 ttyp2 91.196.246.2 пн 2 мар 23:26 - 00:50 (01:24)
p74_03 ttyp2 91.196.246.2 пн 2 мар 23:19 - 23:22 (00:03)
mike ttyp1 10.10.65.11 пн 2 мар 21:51 - 00:12 (02:21)
p73_25 ttyp1 91.196.247.14 пн 2 мар 20:23 - 20:51 (00:27)
p73_25 ttyp1 91.196.247.14 пн 2 мар 20:15 - 20:22 (00:07)
mike ttyp0 10.10.65.11 пн 2 мар 19:23 still logged in
p74_03 ttyp0 91.196.246.2 пн 2 мар 16:30 - 17:08 (00:38)
p72_15 ttyp0 pi46-34-33.cn.ru пн 2 мар 01:33 - 02:03 (00:30)
p72_15 ttyp0 pi46-34-33.cn.ru вс 1 мар 22:32 - 22:40 (00:08)
p72_15 ttyp0 pi46-34-33.cn.ru вс 1 мар 22:27 - 22:29 (00:01)
p72_16 ttyp0 92.127.178.51 вс 1 мар 16:27 - 16:58 (00:30)
(Шаг №3. При помощи программы tr убираем дубликаты пробелов – это понадобится для корректной работы программы cut на следующем шаге, т.к. она не считает несколько последовательных разделителей одним разделителем)
[mike@bublik ~]$ last | grep -v -E "(^$|wtmp begins)" | tr -s " "
mike ttyp1 10.10.65.11 вт 3 мар 02:34 still logged in
p73_13 ttyp2 89.251.107.24 вт 3 мар 01:38 - 01:45 (00:06)
mike ttyp1 10.10.65.11 вт 3 мар 01:00 - 02:34 (01:33)
mike ttyp3 10.10.65.11 вт 3 мар 00:12 - 00:56 (00:44)
p74_03 ttyp2 91.196.246.2 пн 2 мар 23:26 - 00:50 (01:24)
p74_03 ttyp2 91.196.246.2 пн 2 мар 23:19 - 23:22 (00:03)
mike ttyp1 10.10.65.11 пн 2 мар 21:51 - 00:12 (02:21)
p73_25 ttyp1 91.196.247.14 пн 2 мар 20:23 - 20:51 (00:27)
p73_25 ttyp1 91.196.247.14 пн 2 мар 20:15 - 20:22 (00:07)
mike ttyp0 10.10.65.11 пн 2 мар 19:23 still logged in
p74_03 ttyp0 91.196.246.2 пн 2 мар 16:30 - 17:08 (00:38)
p72_15 ttyp0 pi46-34-33.cn.ru пн 2 мар 01:33 - 02:03 (00:30)
p72_15 ttyp0 pi46-34-33.cn.ru вс 1 мар 22:32 - 22:40 (00:08)
p72_15 ttyp0 pi46-34-33.cn.ru вс 1 мар 22:27 - 22:29 (00:01)
p72_16 ttyp0 92.127.178.51 вс 1 мар 16:27 - 16:58 (00:30)
(Шаг №4. Используем программу cut для того, чтобы из всего вывода взять только столбец №1, в качестве разделителя столбцов считать пробел)
[mike@bublik ~]$ last | grep -v -E "(^$|wtmp begins)" | tr -s " " | cut -f 1 -d " "
mike
p73_13
mike
mike
p74_03
p74_03
mike
p73_25
p73_25
mike
p74_03
p72_15
p72_15
p72_15
p72_16
(Шаг №5. Полученный на предыдущем этапе текст отправляем на вход программы sort, которая меняет порядок строк и выдает все в лексико-графическом порядке)
[mike@bublik ~]$ last | grep -v -E "(^$|wtmp begins)" | tr -s " " | cut -f 1 -d " " | sort
mike
mike
mike
mike
mike
p72_15
p72_15
p72_15
p72_16
p73_13
p73_25
p73_25
p74_03
p74_03
p74_03
(Шаг №6. Предыдущий вывод отправить программе uniq, которая выкидывает дубликаты строк, при этом для корректной работы необходимо, чтобы повторяющиеся строки шли подряд, а не были раскиданы в тексте – это обеспечивается на предыдущем шаге с помощью sort. Параметр –c у программы uniq заставляет посчитать и вывести перед каждой строкой количество повторений, встреченных в тексте)
[mike@bublik ~]$ last | grep -v -E "(^$|wtmp begins)" | tr -s " " | cut -f 1 -d " " | sort | uniq -c
5 mike
3 p72_15
1 p72_16
1 p73_13
2 p73_25
3 p74_03
(Шаг №7. Предыдущий вывод отсортируем при помощи sort по первому столбцу (-k1), по убыванию (-r), предполагая числовые данные (-n) в том столбце, по которому будем сортировать)
[mike@bublik ~]$ last | grep -v -E "(^$|wtmp begins)" | tr -s " " | cut -f 1 -d " " | sort | uniq -c | sort -rnk1
5 mike
3 p74_03
3 p72_15
2 p73_25
1 p73_13
1 p72_16
[mike@bublik ~]$
Итог: несложная цепочка команд легко выполняет последовательность операций по обработке текста, в результате получаем необходимую статистику входов пользователей в систему.
На создание, отладку и доведение до работоспособного состояния такого скрипта опытному системному администратору требуется не более 1-2 минут.
Заметим, что применение Shell – далеко не единственный способ решения задач. Всегда возможно создание цельной программы с аналогичным функционалом «под конкретную задачу», написанной на каком-либо другом языке, например, на C/C++. Вы можете самостоятельно оценить количество времени, которое нужно будет затратить на разработку в этом случае.
