Книга: Основы программирования в Linux

Проектирование

Проектирование

Поиск и отображение данных — заставляют предполагать, что адекватным решением будет простое меню. Все данные, которые следует хранить, — текстовые, и, полагая, что ваша коллекция компакт-дисков не слишком велика, для управления ею не потребуется сложная база данных, подойдут простые текстовые файлы. Сохранение информации в текстовых файлах позволит сделать приложение простым, и если ваши требования изменятся, всегда легче манипулировать текстовыми файлами, чем какими бы то ни было еще. И, в крайнем случае, вы сможете применить редактор для ввода и удаления данных вручную, а не писать для этого специальную программу.

Необходимо принять важное проектное решение, касающееся способа хранения данных. Достаточно одного файла? Если да, то какой у него должен быть формат? Большая часть информации, которую вы собираетесь хранить, за исключением данных о дорожках, вводится однократно для каждого компакт-диска (мы пока оставим в стороне вариант наличия на одном CD произведений разных композиторов и исполнителей). И практически на всех компакт-дисках много дорожек.

Нужно ли ограничить количество дорожек на одном компакт-диске, которые можно хранить? Это ограничение кажется лишним, поэтому сразу отбросим его.

Если вы допускаете, что на компакт-диске может быть разное количество дорожек, у вас есть три варианта:

? использовать один файл с одной строкой для "заголовочной" типовой информации и n строк для сведений о дорожках на каждом компакт-диске;

? поместить всю информацию о каждом компакт-диске в одну строку, разрешая ей продолжаться то тех пор, пока вся информация о дорожках диска не будет сохранена;

? отделить заголовочную информацию от данных о дорожках и для каждого типа информации использовать отдельный файл.

Только третий вариант позволит нам легко изменять формат файлов, что потребуется, если вы когда-либо захотите превратить вашу базу данных в реляционную (более подробную информацию об этом см. в главе 7), поэтому выберем этот вариант.

Далее нужно решить, какие данные помещать в файлы.

Сначала вы выбираете для заголовка каждого компакт-диска хранение следующей информации:

? номер компакт-диска в каталоге;

? название;

? музыкальный стиль (классика, рок, поп, джаз и т.д.);

? композитор или исполнитель.

О дорожках вы будете хранить две характеристики:

? номер дорожки;

? ее название.

Для объединения двух файлов вы должны сопоставить данные о дорожках с остальной информацией о компакт-диске. Для этого будет использоваться номер компакт-диска в каталоге. Поскольку он уникален для каждого диска, он будет появляться однократно в файле с заголовками и один раз для каждой дорожки в файле с данными о дорожках.

В табл. 2.22 показан пример файла с заголовочными данными, а соответствующий ему файл с данными о дорожках может выглядеть так, как представлено в табл. 2.23.

Таблица 2.22

Catalog Title Type Composer
CD123 Cool sax Jazz Bix
CD234 Classic violin Classical Bach
CD345 Hits99 Pop Various

Таблица 2.23

Catalog Track No. Title
CD123 1 Some jazz
CD123 2 More jazz
CD234 1 Sonata in D minor
CD345 1 Dizzy

Два файла объединены общим полем Catalog (Каталог). Следует помнить о том, что обычно на одну строку файла с заголовочной информацией приходится много строк в файле с данными о дорожках.

Последнее, что мы должны решить, — способ разделения элементов данных. Поля фиксированной ширины, обычные в реляционных базах, — не всегда самый удобный вариант. Другой распространенный способ, применяемый в данном примере, — запятая (т. е. файл со значениями, разделенными запятыми, или CSV-файл).

В упражнении 2.23 только для того, чтобы вы окончательно не запутались, применяются следующие функции:

? get_return();

? get_confirm();

? set_menu_choice();

? insert_title();

? insert_track();

? add_record_tracks();

? add_records();

? find_cd();

? update_cd();

? count_cds();

? remove_records();

? list_tracks().

Упражнение 2.23. Приложение для работы с коллекцией компакт-дисков

1. Сначала в примере сценария как всегда стоит строка, обеспечивающая его выполнение как сценария командной оболочки, за которой следует некоторая информация об авторских правах:

#!/bin/bash
# Очень простой пример сценария командной оболочки для управления
# коллекцией компакт-дисков.
# Copyright (С) 1996-2007 Wiley Publishing Inc.
# Это свободно распространяемое программное обеспечение;
# вы можете распространять эту программу и/или изменять ее
# в соответствии с положениями GNU General Public License,
# документа, опубликованного фондом Free Software Foundation;
# либо версии 2 этой лицензии или (по вашему выбору)
# любой более свежей версии.
# Эта программа распространяется в надежде на ее полезность,
# но WITHOUT ANY WARRANTY, (без каких-либо гарантий);
# даже без предполагаемой гарантии MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE (годности
# ее для продажи или применения для определенной цели).
# Более подробную информацию см. в GNU General Public License.
# Вы должны были получить копию GNU General Public License
# вместе с этой программой;
# если нет, пишите в организацию Free Software Foundation,
# Inc. no адресу: 675 Mass Ave, Cambridge, MA 02139, USA.

2. Теперь убедитесь, что установлены некоторые глобальные переменные, которые будут использоваться во всем сценарии. Задайте заголовочный файл, файл с данными о дорожках и временный файл и перехватите нажатие комбинации клавиш <Ctrl>+<C> для того, чтобы удалить временный файл, если пользователь прервет выполнение сценария.

menu_choice=""
current cd=""
title_file="title.cdb"
tracks_file="tracks.cdb"
temp_file=/tmp/cdb.$$
trap 'rm -f $temp_file' EXIT

3. Определите ваши функции так, чтобы сценарий, выполняясь с первой строки, мог найти все определения функций до того, как вы попытаетесь вызвать любую из них в первый раз. Для того чтобы не повторять один и тот же программный код в нескольких местах, сначала вставьте две функции, служащие простыми утилитами:

get_return() (
 echo -е "Press return с"
 read x
 return 0
}
get_confirm() (
 echo -e "Are you sure? c"
 while true do
  read x
  case "$x" in
   y | yes | Y | Yes | YES )
    return 0;;
   n | no | N | No | NO )
    echo
    echo "Cancelled"
    return 1;;
   *)
    echo "Please enter yes or no" ;;
  esac
 done
}

4. Теперь вы дошли до основной, формирующей меню функции set_menu_choice. Содержимое меню изменяется динамически, добавляя дополнительные пункты при выборе компакт-диска.

set_menu_choice() {
 clear
 echo "Options :-"
 echo
 echo " a) Add new CD"
 echo " f) Find CD"
 echo " c) Count the CDs and tracks in the catalog"
 if [ "$cdcatnum" != "" ]; then
  echo " 1) List tracks on $cdtitle"
  echo " r) Remove $cdtitle"
  echo " u) Update track information for $cdtitle"
 fi
 echo " q) Quit" echo
 echo -e "Please enter choice then press return c"
 read menu_choice
 return
}

Примечание

Имейте в виду, что команда echo -е не переносится в некоторые командные оболочки.

5. Далее идут две очень короткие функции, insert_title и insert_track, для пополнения файлов базы данных. Несмотря на то, что некоторые программисты ненавидят однострочные функции вроде этих, они помогают сделать понятнее другие функции.

За ними следует более длинная функция add_record_track, использующая эти функции. В данной функции применяется проверка на соответствие шаблону, чтобы исключить ввод запятых (поскольку мы. используем запятые для разделения полей), и арифметические операции для увеличения номера текущей дорожки на 1, когда вводятся данные о новой дорожке.

insert_title() {
 echo $* >> $title_file
 return
}
insert_track() {
 echo $* >> $tracks_file
 return
}
add_record_tracks() {
 echo "Enter track information for this CD"
 echo "When no more tracks enter q"
 cdtrack=1
 cdttitle=""
 while [ "$cdttitle" != "q" ]
 do
  echo -e "Track $cdtrack, track title? c"
  read tmp
  cdttitle=${tmp%%, *}
  if [ "$tmp" != "$cdttitle" ]; then
   echo "Sorry, no commas allowed"
   continue
  fi
  if [ -n "$cdttitle" ] ; then
   if [ "$cdttitle" ! = "q" ]; then
    insert_track $cdcatnum, $cdtrack, $cdttitle
   fi
  else
   cdtrack=$((cdtrack-1))
  fi
  cdtrack=$((cdtrack+1))
 done
}

6. Функция add_records позволяет вводить основную информацию о новом компакт-диске.

add_records() {
 # Подсказка для начала ввода информации
 echo -е "Enter catalog name с"
 read tmp
 cdcatnum=${tmp%%, *}
 echo -e "Enter title c"
 read tmp
 cdtitle=${tmp%%, *}
 echo -e "Enter type c"
 read tmp
 cdtype=${tmp%%, *}
 echo -e "Enter artist/composer c"
 read tmp
 cdac=${tmp%%, *}
 # Проверяет, хочет ли пользователь ввести информацию
 echo About to add new entry
 echo "$cdcatnum $cdtitle $cdtype $cdac"
 # Если получено подтверждение, добавляет данные в конец файла.
 # с заголовками
 if get_confirm ; then
  insert_title $cdcatnum, $cdtitle, $cdtype, $cdac
  add_record_tracks
 else
  remove_records
 fi
 return
}

7. Функция find_cd с помощью команды grep ищет текст с названием компакт-диска в файле с заголовочной информацией. Вам нужно знать, сколько раз была найдена строка, а команда grep только вернет значение, указывающее на то, что строка не была найдена или была найдена многократно. Для решения этой проблемы сохраните вывод в файл, отводящий по одной строке на каждое найденное совпадение, а затем сосчитайте количество строк в файле.

У команды счетчика слов, wc, в выводе есть пробельный символ, разделяющий количества строк, слов и символов в файле. Используйте синтаксическую запись $(wc -l $temp_file) для извлечения первого параметра в выводе и переноса его в переменную linesfound. Если бы вам был нужен другой следующий далее параметр, нужно было бы воспользоваться командой set для установки значений переменных-параметров оболочки из вывода команды.

Изменив значение переменной IFS (Internal Field Separator, внутренний разделитель полей) на запятую, вы сможете разделить поля, разграниченные запятыми. Альтернативный вариант — применить команду cut.

find_сd() {
 if [ "$1" = "n" ]; then
  asklist=n
 else
  asklist=y
 fi
 cdcatnum=""
 echo -e "Enter a string to search for in the CD titles c"
 read searchstr
 if [ "$searchstr" = "" ]; then
  return 0
 fi
 grep "$searchstr" $title_file > $temp_file
 set $(wc -l $temp_file)
 linesfound=$1
 case "$linesfound" in
  0)
   echo "Sorry, nothing found"
   get_return
   return 0 ;;
  1) ;;
  2)
   echo "Sorry, not unique."
   echo "Found the following"
   cat $temp_file
   get_return
   return 0
 esac
 IFS=", "
 read cdcatnum cdtitle cdtype cdac < $temp_file
 IFS=" "
 if [ -z "$cdcatnum" ]; then
  echo "Sorry, could not extract catalog field from $temp_file"
  get_return
  return 0
 fi
 echo
 echo Catalog number: $cdcatnum echo Title: $cdtitle
 echo Type: $cdtype
 echo Artist/Composer: $cdac
 echo
 get_return
 if [ "$asklist" = "y" ]; then
  echo -e "View tracks for this CD? c"
  read x
  if [ "$x" = "y" ]; then
   echo
   list_tracks
   echo
  fi
 fi
 return 1
}

8. Функция update_cd позволит вам повторно ввести сведения о компакт-диске. Учтите, что вы ищите (с помощью команды grep) строки, начинающиеся (^) с подстроки $cdcatnum, за которой следует ", " и должны заключить подстановку значения $cdcatnum в {}. Таким образом, вы сможете найти запятую без специального пробельного символа между ней и номером в каталоге. Эта функция также использует {} для образования блока из нескольких операторов, которые должны выполняться, если функция get_confirm вернет значение true.

update_cd() {
 if [ -z "$cdcatnum" ]; then
  echo "You must select a CD first"
  find_cd n
 fi
 if [ -n "$cdcatnum" ]; then
  echo "Current tracks are :-"
  list_tracks
  echo
  echo "This will re-enter the tracks for $cdtitle"
  get_confirm && {
   grep -v "^${cdcatnum}, " $tracks_file > $temp_file
   mv $temp_file $tracks_file
   echo
   add_record_tracks
  }
 fi
 return
}

9. Функция count_cds дает возможность быстро пересчитать содержимое базы данных.

count_cds() {
 set $(wc -l $title_file)
 num_titles=$1
 set $(wc -l $tracks_file)
 num_tracks=$1
 echo found $num_titles CDs, with a total of $num_tracks tracks
 get_return
 return
}

10. Функция remove_records удаляет элементы из файлов базы данных с помощью команды grep -v, удаляющей все совпадающие строки. Учтите, что нужно применять временный файл.

Если вы попытаетесь применить команду:

grep -v "^$cdcatnum" > $title_file

файл $title_file станет пустым благодаря перенаправлению вывода > до того, как команда grep выполнится, поэтому она будет читать уже пустой файл.

remove_records() {
 if [ -z "$cdcatnum" ]; then
  echo You must select a CD first find_cd n
 fi
 if [ -n "$cdcatnum" ]; then
  echo "You are about to delete $cdtitle"
  get_confirm && {
   grep -v "^${cdcatnum}, " $title_file > $temp_file
   mv $temp_file $title_file
   grep -v "^${cdcatnum}, " $tracks_file > $temp_file
   mv $temp_file $tracks_file
   cdcatnum=""
   echo Entry removed
  }
  get_return
 fi
 return
}

11. Функция list_tracks снова использует команду grep для извлечения нужных вам строк, команду cut для доступа к отдельным полям и затем команду more для постраничного вывода. Если вы посмотрите, сколько строк на языке С займет повторная реализация этих 20 необычных строк кода, то поймете, каким мощным средством может быть командная оболочка.

list_tracks() {
 if [ "$cdcatnum" = "" ]; then
  echo no CD selected yet
  return
 else
  grep "^${cdcatnum}, " $tracks_file > $temp_file
  num_tracks=${wc -l $temp_file}
  if [ "$num_tracks" = "0" ]; then
   echo no tracks found for $cdtitle
  else
   {
    echo
    echo "$cdtitle :-"
    echo
    cut -f 2- -d , $temp_file
    echo
   } | ${PAGER:-more}
  fi
 fi
 get_return
 return
}

12. Теперь, когда все функции определены, можно вводить основную процедуру. Первые несколько строк просто приводят файлы в известное состояние; затем вы вызываете функцию формирования меню set_menu_choice и действуете в соответствии с ее выводом.

Если выбран вариант quit (завершение), вы удаляете временный файл, выводите сообщение и завершаете сценарий с успешным кодом завершения.

rm -f $temp_file
if [ ! -f $title_file ]; then
 touch $title_file
fi
if [ ! -f $tracks_file ]; then
 touch $tracks_file
fi
# Теперь непосредственно приложение
clear
echo
echo
echo "Mini CD manager" sleep 1
quit=n
while [ "$quit" != "y" ]; do
 set_menu_choice
 case "$menu_choice" in
  a) add_records;;
  r) remove records;;
  f) find_cd y;;
  u) update_cd;;
  c) count_cds;;
  l) list_tracks;;
  b)
   echo
   more $title_file
   echo
   get return;;
  q | Q ) quit=y;;
  *) echo "Sorry, choice not recognized";;
 esac
done
# Убираем и покидаем
rm -f $temp_file echo "Finished"
exit 0

Замечания, касающиеся приложения

Команда trap в начале сценария предназначена для перехвата нажатия пользователем комбинации клавиш <Ctrt>+<C>. Им может быть сигнал EXIT или INT, в зависимости от настроек терминала.

Существуют другие способы реализации выбора пункта меню, особенно конструкция select в оболочках bash и ksh (которая, тем не менее, не определена в стандарте X/Open). Она представляет собой специализированный селектор пунктов меню. Проверьте ее на практике, если ваш сценарий может позволить себе быть немного менее переносимым. Для передачи пользователям многострочной информации можно также воспользоваться встроенными документами.

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

1 First CD Track 1
2 First CD Track 2
1 Another CD
2 With the same CD key

Мы оставляем это и другие усовершенствования в расчете на ваше воображение и творческие способности, которые проявятся при корректировке вами программного кода в соответствии с требованиями GPL.

Оглавление книги

Оглавление статьи/книги

Генерация: 0.074. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз