Разработка приложений в среде Linux. Второе издание

Джонсон Майкл К.

Троан Эрик В.

Часть IV

Библиотеки для разработки

 

 

Глава 23

Сопоставление строк

 

Осуществлять сравнение строк можно не только с помощью функции strcmp() или даже strncmp(). Linux предлагает несколько общих функций сопоставления строк, использование которых позволяет упростить решение задач программирования. Мы рассмотрим сначала самые простые примеры, а затем перейдем к более сложным.

 

23.1. Универсализация произвольных строк

В главе 14 мы говорили о том, как с помощью функции glob() производится универсализация имен файлов, однако пользователи, знакомые с возможностями универсализации, нередко пытаются применить их и к другим разновидностям строк. Функция fnmatch() позволяет применять правила универсализации в отношении произвольных строк:

#include

int fnmatch(const char * pattern, const char * string, int flags);

Предложенный шаблон является стандартным выражением универсализации с четырьмя специальными символами, за которые отвечает аргумент flags.

* Соответствует любой строке, включая пустую.
? Соответствует любому одиночному символу.
[ Начинает список символов для сопоставления или, если следующим символом является ^ , то список символов для несовпадения. Весь список может совпадать, или не совпадать с одним символом. Список заканчивается знаком ] .
\ Следующий символ будет интерпретироваться как литерал, а не как специальный символ.

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

FNM_NOESCAPE Обработка символа \ как обычного, а не специального символа.
FNM_PATHNAME Символы / в строке string не сопоставляются с последовательностью * , ? , или даже [/] в шаблоне pattern ; сопоставление производится только с литералом, а не специальным символом / .
FNM_NOESCAPE Первый символ . в шаблоне pattern соответствует символу . в строке string только в том случае, если он является первым символом в строке string или если задано значение FNM_PATHNAME , а символ . в string непосредственно следует за символом \ .

Функция fnmatch() возвращает нулевое значение, если шаблон соответствует строке, FNM_NOMATCH, если шаблон не соответствует строке, или другое неопределенное значение в случае возникновения ошибки.

Пример использования функции fnmatch() вы можете посмотреть в программе, приведенной в разделе 14.7.3 главы 14, в которой эта функция используется как часть простой реализации команды find.

 

23.2. Регулярные выражения

 

Регулярные выражения, используемые в программах sed, awk, grep, vi, а также во множестве других программ Unix, со временем приобрели большое значение в среде программирования Unix. Регулярные выражения можно применять и при написании программ на языке С. В этом разделе будет рассказано об их использовании и будет предложен пример простой программы синтаксического анализа файла, построенной на этих функциях.

 

23.2.1. Регулярные выражения в Linux

Существуют две разновидности регулярных выражений: базовые регулярные выражения (basic regular expression — BRE) и расширенные регулярные выражения (extended regular expression — ERE). Они соответствуют (в первом приближении) командам grep и egrep. Описание каждой разновидности регулярных выражений можно найти на man-странице grep, в стандарте POSIX.2 (IEEE, 1993), в [32], а также в других источниках, поэтому здесь мы не станем описывать их синтаксис, а рассмотрим только интерфейс функции, с помощью которой вы сможете применять регулярные выражения в своих программах.

 

23.2.2. Сопоставление с регулярными выражениями

Стандарт POSIX определяет четыре функции обработки регулярных выражений.

#include

int regcomp(regex_t *preg, const char * regex, int cflags);

int regexec(const regex_t *preg, const char * string, size_t nmatch,

 regmatch_t pmatch[], int eflags);

void regfree(regex_t *preg);

size_t regerror(int errcode, const regex_t *preg, char * errbuf,

 size_t errbuf_size);

Прежде чем сравнивать строку с регулярным выражением, нужно выполнить ее компиляцию с помощью функции regcomp(). Аргумент regex_t *preg указывает на область хранения регулярного выражения. Чтобы каждое регулярное выражение было доступно одновременно, для него потребуется отдельный аргумент regex_t. Структура regex_t включает только один важный член, re_nsub, который определяет количество подвыражений в регулярном выражении, заключенных в скобки. Рассмотрим оставшуюся часть непрозрачной структуры.

Аргумент сflags определяет варианты интерпретации регулярного выражения regex. Он может иметь нулевое значение или быть любой комбинацией перечисленных ниже значений, объединенных битовым "ИЛИ".

REG_EXTENDED Вместо синтаксической структуры BRE будет использоваться структура ERE.
REG_ICASE Не будет учитываться регистр.
REG_NOSUB Не будут выделяться подстроки. Функция regexec() будет игнорировать аргументы nmatch и pmatch .
REG_NEWLINE Если значение REG_NEWLINE не будет задано, то символ новой строки будет обрабатываться точно так же, как и любой другой символ. Символы ^ и $ соответствуют только началу и концу всей строки, а не соседним символам новой строки. Если значение REG_NEWLINE будет задано, то результат будет таким же, как и в случае использования grep , sed и других стандартных системных инструментальных средств; символ ^ осуществляет привязку к началу строки и символу, следующему после символа новой строки (фактически он соответствует строке нулевой длины, следующей за символом новой строки); $ осуществляет привязку к концу строки и символу, следующему после символа новой строки (фактически, он соответствует строке нулевой длины, предшествующей символу новой строки); символ . не соответствует символу новой строки.

Ниже представлен пример типичного вызова функции.

if ((rerr = regcomp(&p, "(^(.*[^\\])#.*$)|(^[^#]+$)",

 REG_EXTENDED|REG_NEWLINE))) {

 if (rerr == REG_NOMATCH) {

  /* строка просто не совпадает с регулярным выражением */

 } else {

  /* какая-то другая ошибка, например, неправильно сформированное регулярное выражение */

 }

}

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

Даже если вы компилируете выражение, которое, по вашему мнению, является нормальным, вам все равно необходимо проверить его на наличие ошибок. Функция regcomp() возвращает нулевое значение при успешном выполнении компиляции и ненулевой код ошибки — в противном случае. Большинство ошибок может быть связано с разного рода ошибками в регулярных выражениях, но не исключено, что ошибка может быть связана с переполнением памяти. Далее в этой главе дается описание функции regerror().

#include

int regexec(const regex_t *preg, const chat *string, size_t nmatch,

 regmatch_t pmatch[], int eflags);

Функция regexec() сравнивает строку с предварительно компилированным регулярным выражением. Аргумент eflags может иметь нулевое значение или быть любой комбинацией перечисленных ниже значений, объединенных битовым "ИЛИ".

REG_NOTBOL Первый символ строки не будет соответствовать символу ^ . Любой символ, следующий за символом новой строки, будет соответствовать при том условии, что в вызове функции regcomp() будет задано значение REG_NEWLINE .
REG_NOTEOL Последний символ строки не будет соответствовать символу $ . Любой символ, предшествующий символу новой строки, будет соответствовать символу $ при том условии, что в вызове функции regcomp() будет задано значение REG_NEWLINE .

Массив структур regmatch_t используется для представления местоположения подвыражений в регулярном выражении.

#include

typedef struct {

 regoff_t rm_so; /* индекс байта в строке в начале сопоставления*/

 regoff_t rm_eo; /* индекс байта в строке в конце сопоставления*/

} regmatch_t;

Первый элемент regmatch_t описывает всю совпавшую строку; обратите внимание, что вся эта строка содержит любой символ начала строки, включая хвостовой символ новой строки, независимо от того, задано ли значение REG_NEWLINE.

Следующие элементы массива хранят подвыражения, заключенные в скобки, в том порядке, в котором они присутствуют в регулярном выражении, в порядке расположения открывающих скобок. (В коде на языке С элемент i эквивалентен выражению замены \ i в программах sed или awk.) В несовпадающих подвыражениях член regmatch_t.rm_so имеет значение -1.

В следующем коде производится сопоставление строки с регулярным выражением, содержащим подвыражения. После сопоставления на экран выводятся все совпавшие подвыражения.

 1: /* match.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8: #include

 9:

10: void do_regerror(int errcode, const regex_t *preg) {

11:  char *errbuf;

12:  size_t errbuf_size;

13:

14:  errbuf_size = regerror(errcode, preg, NULL, 0);

15:  errbuf = alloca(errbuf_size);

16:  if (!errbuf) {

17:   perror("alloca");

18:   return;

19:  }

20:

21:  regerror(errcode, preg, errbuf, errbuf_size);

22:  fprintf(stderr, "%s\n", errbuf);

23: }

24:

25: int main() {

26:

27:  regex_t p;

28:  regmatch_t *pmatch;

29:  int rerr;

30:  char *regex = "(^(.*[^\\])#.*$)|(^[^#]+$)";

31:  char string[BUFSIZ+1];

32:  int i;

33:

34:  if ((rerr = regcomp(&p, regex, REG_EXTENDED | REG_NEWLINE))) {

35:   do_regerror(rerr, &p);

36:  }

37:

38:  pmatch = alloca(sizeof(regmatch_t) * (p.re_nsub+1));

39:  if (!pmatch) {

40:   perror("alloca");

41:  }

42:

43:  printf("Введите строку: ");

44:  fgets(string, sizeof(string), stdin);

45:

46:  if ((rerr = regexec(&p, string, p.re_nsub+1, pmatch, 0))) {

47:   if (rerr == REG_NOMATCH) {

48:    /* эту ситуацию может обработать regerror,

49:     * но зачастую она обрабатывается особым образом

50:     */

51:    printf("Строка не совпадает с %s\n", regex);

52:   } else {

53:    do_regerror(rerr, &p);

54:   }

55:  } else {

56:   /* сопоставление закончено */

57:   printf("Строка совпадает с регулярным выражением %s\n", regex);

58:   for (i = 0; i <= p.re_nsub; i++) {

59:    /* вывод на экран совпавшей части (частей) строки */

60:    if (pmatch[i].rm_so != -1) {

61:     char *submatch;

62:     size_t matchlen = pmatch[i].rm_eo - pmatch[i].rm_so;

63:     submatch = malloc(matchlen+1);

64:     strncpy(submatch, string+pmatch[i].rm_so,

65:      matchlen);

66:     submatch[matchlen] = '\0';

67:     printf("совпавшее подвыражение %d: %s\n", i,

68:      submatch);

69:     free(submatch);

70:    } else {

71:     printf ("нет совпадения с подвыражением %d\n", i);

72:    }

73:   }

74:  }

75:  exit(0);

76: }

В примере регулярного выражения из программы match.с имеется три подвыражения. Первое из них представляет собой всю строку, содержащую текст, за которым следует символ комментария, вторым является текст в строке, предшествующей символу комментария, а третье представляет всю строку без символа комментария. Для строки, в начале которой содержится комментарий, элементу rm_so во втором и третьем элементе из массива pmatch[] присвоено значение -1. Для строки, в начале которой содержится комментарий, значение -1 присваивается первому и второму элементу; для строки, не содержащей символы комментария, второму и третьему элементу присваивается значение -1.

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

#include

void regfree(regex_t *preg);

В стандарте POSIX четко не сказано, следует ли использовать функцию regfree() каждый раз при вызове функции regcomp(), или же только после того, как вы в последний раз вызывали функцию regcomp() в одной структуре regex_t. Таким образом, чтобы избежать утечек памяти, в промежутках между использованием структур regex_t необходимо вызывать функцию regfree().

Всякий раз когда функция regcomp() или regex() возвращает ненулевой результат, функция regerror() может предоставить подробное сообщение, в котором будет указано, в чем состоит ошибка. Она записывает по возможности все сообщение об ошибке в буфер и возвращает размер всего сообщения. Поскольку вы заранее не знаете, какой размер будет иметь сообщение, то сначала вам необходимо узнать его размер, а затем распределить и использовать буфер, как показано в следующем далее примере кода. Поскольку этот вариант обработки ошибок быстро становится устаревшим, и вам придется включать его как минимум дважды (один раз после функции regcomp() и один раз после функции regex()), мы советуем вам написать код собственной оболочки функции regerror(), как показано в строке 10 из листинга math.с.

 

23.2.3. Простая утилита

grep

grep является популярной утилитой, определенной в стандарте POSIX, которая предлагает возможности поиска регулярного выражения в текстовых файлах. Ниже показана простая (не соответствующая стандарту POSIX) версия утилиты grep, реализованная с помощью функций стандартного регулярного выражения.

  1: /* grep.с */

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10:

 11: #define MODE_REGEXP 1

 12: #define MODE_EXTENDED 2

 13: #define MODE_FIXED 3

 14:

 15: void do_regerror(int errcode, const regex_t *preg) {

 16:  char *errbuf;

 17:  size_t errbuf_size;

 18:

 19:  errbuf_size = regerror(errcode, preg, NULL, 0);

 20:  errbuf = alloca(errbuf_size);

 21:  if (!errbuf) {

 22:   perror("alloca");

 23:   return;

 24:  }

 25:

 26:  regerror(errcode, preg, errbuf, errbuf_size);

 27:  fprintf(stderr, "%s\n", errbuf);

 28: }

 29:

 30: int scanFile(FILE * f, int mode, const void * pattern,

 31:  int ignoreCase, const char * fileName,

 32:  int * maxCountPtr) {

 33:  long lineLength;

 34:  char * line;

 35:  int match;

 36:  int rc;

 37:  char * chptr;

 38:  char * prefix = "";

 39:

 40:  if (fileName) {

 41:   prefix = alloca(strlen(fileName) + 4);

 42:   sprintf(prefix, "%s: ", fileName);

 43:  }

 44:

 45:  lineLength = sysconf(_SC_LINE_MAX);

 46:  line = alloca(lineLength);

 47:

 48:  while (fgets(line, lineLength, f) && (*maxCountPtr)) {

 49:   /* если у нас не будет завершающего символа '\n'

 50:      то мы не сможем получить всю строку целиком */

 51:   if (line [strlen (line) -1] != '\n') {

 52:    fprintf(stderr, " %s line слишком длинная\n", prefix);

 53:    return 1;

 54:   }

 55:

 56:   if (mode == MODE_FIXED) {

 57:    if (ignoreCase) {

 58:     for (chptr = line; *chptr; chptr++) {

 59:      if (isalpha(*chptr)) *chptr = tolower(*chptr);

 60:     }

 61:    }

 62:    match = (strstr(line, pattern) != NULL);

 63:   } else {

 64:    match = 0;

 65:    rc = regexec (pattern, line, 0, NULL, 0);

 66:    if (!rc)

 67:     match = 1;

 68:    else if (rc != REG_NOMATCH)

 69:    do_regerror(match, pattern);

 70:   }

 71:

 72:   if (match) {

 73:    printf("%s%s", prefix, line);

 74:    if (*maxCountPtr > 0)

 75:     (*maxCountPtr)--;

 76:   }

 77:  }

 78:

 79:  return 0;

 80: }

 81:

 82: int main(int argc, const char ** argv) {

 83:  const char * pattern = NULL;

 84:  regex_t regPattern;

 85:  const void * finalPattern;

 86:  int mode = MODE_REGEXP;

 87:  int ignoreCase = 0;

 88:  int maxCount = -1;

 89:  int rc;

 90:  int regFlags;

 91:  const char ** files;

 92:  poptContext optCon;

 93:  FILE * f;

 94:  char * chptr;

 95:  struct poptOption optionsTable[] = {

 96:   { "extended-regexp", 'E', POPT_ARG_VAL,

 97:     &mode, MODE_EXTENDED,

 98:     "шаблоном для соответствия является расширенное регулярное "

 99:     "выражение"},

100:   { "fixed-strings", 'F', POPT_ARG_VAL,

101:     &mode, MODE_FIXED,

102:     "шаблоном для соответствия является базовая строка (не "

103:     "регулярное выражение)", NULL },

104:   { "basic-regexp", 'G', POPT_ARG_VAL,

105:     &mode, MODE_REGEXP,

106:     "шаблоном для соответствия является базовое регулярное выражение" },

107:   { "ignore-case", 'i', POPT_ARG_NONE, &ignoreCase, 0,

108:     "выполнять поиск, чувствительный к регистру", NULL },

109:   { "max-count", 'm', POPT_ARG_INT, &maxCount, 0,

110:     "завершить после получения N. совпадений", "N" },

111:   { "regexp", 'e', POPT_ARG_STRING, &pattern, 0,

112:     "регулярное выражение для поиска", "pattern" },

113:     POPT_AUTOHELP

114:   { NULL, '\0', POPT_ARG_NONE, NULL, 0, NULL, NULL }

115:  };

116:

117:  optCon = poptGetContext("grep", argc, argv, optionsTable, 0);

118:  poptSetOtherOptionHelp(optCon, "<шаблон> <список файлов>");

119:

120:  if ((rc = poptGetNextOpt(optCon)) < -1) {

121:   /* во время обработки параметра возникла ошибка */

122:   fprintf(stderr, "%s: %s\n",

123:    poptBadOption(optCon, POPT_BADOPTION_NOALIAS),

124:   poptStrerror(rc));

125:   return 1;

126:  }

127:

128:  files = poptGetArgs(optCon);

129:  /* если мы не получили шаблон, то он должен быть первым

130:     из оставшихся */

131:  if (!files && !pattern) {

132:   poptPrintUsage(optCon, stdout, 0);

133:   return 1;

134:  }

135:

136:  if (!pattern) {

137:   pattern = files[0];

138:   files++;

139:  }

140:

141:  regFlags = REG_NEWLINE | REG_NOSUB;

142:  if (ignoreCase) {

143:   regFlags |= REG_ICASE;

144:   /* преобразование шаблона в нижний регистр; этого можно не делать,

145:      если мы игнорируем регистр в регулярном выражении, однако позволяет

146:      функции strstr() правильно обработать -i */

147:   chptr = alloca(strlen(pattern) + 1);

148:   strcpy(chptr, pattern);

149:   pattern = chptr;

150:

151:   while (*chptr) {

152:    if (isalpha(*chptr)) *chptr = tolower(*chptr);

153:    chptr++;

154:   }

155:  }

156:

157:

158:  switch (mode) {

159:  case MODE_EXTENDED:

160:   regFlags |= REG_EXTENDED;

161:  case MODE_REGEXP:

162:   if ((rc = regcomp(®Pattern, pattern, regFlags))) {

163:    do_regerror(rc, ®Pattern);

164:    return 1;

165:   }

166:   finalPattern = ®Pattern;

167:   break;

168:

169:  case MODE_FIXED:

170:   finalPattern = pattern;

171:   break;

172:  }

173:

174:  if (!*files) {

175:   rc = scanFile(stdin, mode, finalPattern, ignoreCase, NULL,

176:    &maxCount);

177:  } else if (!files[1]) {

178:   /* эта часть обрабатывается отдельно, поскольку имя файла

179:      выводить не нужно */

180:   if (!(f = fopen(*files, "r"))) {

181:    perror(*files);

182:    rc = 1;

183:   } else {

184:    rc = scanFile(f, mode, finalPattern, ignoreCase, NULL,

185:     &maxCount);

186:    fclose(f);

187:   }

188:  } else {

189:   rc = 0;

190:

191:   while (*files) {

192:    if (!(f = fopen(*files, "r"))) {

193:     perror(*files);

194:     rc = 1;

195:    } else {

196:     rc |= scanFile(f, mode, finalPattern, ignoreCase,

197:      *files, &maxCount);

198:     fclose(f);

199:    }

200:    files++;

201:    if (!maxCount) break;

202:   }

203:  }

204:

205:  return rc;

206: }

 

Глава 24

Управление терминалами с помощью библиотеки S-Lang

 

С помощью библиотеки S-Lang, написанной Джоном Дэвисом (John Е. Davis), можно осуществлять доступ к терминалам на среднем уровне. Все действия, связанные с управлением терминалами на низком уровне, осуществляются посредством набора подпрограмм, предлагающих прямой доступ к видеотерминалам и автоматически управляющих прокруткой и цветами. Несмотря на незначительную прямую поддержку окон и отсутствие в S-Lang каких-либо элементов управления, для таких задач эта библиотека предлагает удобную основу.

Библиотеку S-Lang можно использовать и для работы в DOS, что делает ее привлекательной для создания приложений, которые будут выполняться на платформах Unix и DOS.

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

 

24.1. Обработка ввода

 

Подсистема управления вводом на терминалах является одной из наименее доступных подсистем в мире Unix.

Широко распространенными подсистемами являются BSD sgtty, System termio, a также POSIX termios. За работу по управлению входными данными в библиотеке S-Lang отвечают несколько функций, предназначенных специально для того, чтобы сделать обработку данных, поступающих с клавиатуры, более простой и доступной.

Чтобы написать программу для посимвольного чтения из терминала и вывода каждого символа в отдельной строке потребуется несложный код.

 1: /* slecho.c */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: int main(void) {

 8:  char ch = '\0';

 9:

10:  /*

11:   Начать обработку SLANG tty со следующими параметрами:

12:   -1 символ прерывания по умолчанию (обычно Ctrl-C)

13:   0 управление потоком не производится; все символы (кроме

14:   символа прерывания) будут переданы в программу 1 разрешение

15:   обработки выходных данных OPOST управляющих последовательностей

16:  */

17:  SLang_init_tty(-1, 0, 1);

18:

19:  while (ch != 'q') {

20:   ch = SLang_getkey();

21:   printf("чтение: %c 0x%x\n", isprint(ch) ? ch : ' ', ch);

22:  }

23:

24:  SLang_reset_tty();

25:

26:  return 0;

27: }

Эта программа предполагает, что все заголовочные файлы S-Lang содержатся в каталоге /usr/include/slang. Если в вашей системе они находятся в другом каталоге, то тогда следует изменить соответствующим образом код (это касается всех примеров в этой главе). Для компилирования и компоновки этой программы в команду компоновки потребуется добавить -lslang, чтобы компоновщик мог найти функции S-Lang.

 

24.1.1. Инициализация обработки ввода в S-Lang

Прежде чем какая-либо функция обработки входных данных сможет работать, с помощью функции Slang_init_tty() нужно перевести терминал в состояние, которое ожидается S-Lang:

int SLang_init_tty(int abort_char, int flow_ctrl, int opost);

Первый параметр функции Slang_init_tty() определяет символ, который будет использован для прекращения работы. При передаче значения -1 будет сохранен текущий символ прерывания tty (обычно, ); в противном случае символу прерывания будет назначено переданное значение. Каждый раз при вводе на терминале символа прекращения работы ядро посылает сигнал SIGINT тому процессу, который обычно завершает работу приложения. В главе 12 мы рассказывали о том, как производится обработка сигналов, подобных SIGINT.

Следующий параметр отвечает за включение и отключение управления потоком. Управляя потоком на уровне терминала, пользователь может приостанавливать процесс вывода данных на терминал, не допуская прокрутки, а затем возобновлять его. Обычно для приостановления процесса вывода данных на экран используется и — для возобновления этого процесса. Хотя эта особенность удобна для некоторых утилит, ориентированных на работу со строками, программы, работающие с библиотекой S-Lang, обычно ориентированы на работу с экраном, поэтому она может оказаться излишней. S-Lang позволяет приложениям отключать эту функциональность, в результате чего программа сможет назначить нажатия клавиш Stop (Стоп) и Start (Пуск) для других команд. Чтобы включить управление потоками, функции SLang_init_tty() необходимо передать ненулевое значение в качестве второго параметра.

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

 

24.1.2. Восстановление состояния терминала

После того как состояние терминала было изменено с помощью функции SLang_init_tty(), программа, прежде чем завершить свою работу, должна явным образом восстановить первоначальное состояние терминала. Если этого не сделать, то вряд ли можно будет работать с терминалом после завершения программы. Функция SLang_init_tty() не принимает и не возвращает никаких аргументов.

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

Не исключено, что в процессе разработки программ с помощью библиотеки S-Lang в них будут неоднократно происходить сбои, после которых терминал будет находиться в нестандартном состоянии. С этой проблемой можно справиться, если выполнить команду stty sane.

 

24.1.3. Чтение символов с терминала

После правильной инициализации терминала чтение одиночных нажатий клавиш не составит труда. Функция SLang_getkey() возвращает одиночный символ из терминала. Однако это не означает, что функция возвращает одиночное нажатие клавиши, ведь в системе Unix после многих нажатий клавиш может быть возвращено несколько символов. Например, на терминале VT100 (а также на многих других терминалах, включая консоль Linux) при нажатии клавиши на экран посылается четыре символа — ESC [ [ А (попробуйте запустить slecho, нажать клавишу и посмотрите, что получится). Чтобы установить соответствие между подобными многосимвольными последовательностями и нажатиями клавиш, можно использовать базу данных terminfo [37].

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

 

24.1.4. Проверка ожидающего ввода

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

int SLang_input_pending(int timeout);

Функция SLang_input_pending() возвращает true, если символы стали доступными в течение n десятых долей секунды. Она возвращает результат, как только символы становятся доступными, и false, если ни один из символов не окажется доступным в течение определенного периода времени. Если задать нулевой период времени, то функция SLang_input_pending() сообщит о доступных в данный момент символах.

Это поведение легко пронаблюдать. Для этого в программе slecho.с достаточно изменить проверку в цикле while:

while (ch != 'q' && SLang_input_pending(20))

Теперь программа будет ожидать ввода дополнительных данных не более двух секунд. По истечении двух секунд, если никакие данные не будут введены, работа программы будет завершена.

 

24.2. Обработка вывода

 

Функции библиотеки S-Lang, предназначенные для вывода данных на терминал, бывают двух разновидностей: функции управления терминалом (семейство SLtt) и функции высокого уровня для управления экраном (семейство SLsmg).

Функции, принадлежащие семейству SLtt, работают напрямую с терминалом; к ним принадлежат функции, осуществляющие отображение данных в строгом соответствии с возможностями, определенными в терминальной базе данных. Это семейство также включает набор подпрограмм для определения пар цветов переднего плана и фона, а также включения и выключения курсора. Разработчики приложений обычно используют только некоторые из этих функций, а остальные вызываются внутри самой библиотеки S-Lang.

Семейство SLsmg предлагает высокий уровень абстракции терминала. Хотя эти функции используют функции семейства SLtt для управления терминалом, они предлагают разработчикам приложений интерфейс с более широкими возможностями.

Эти функции отвечают за вывод строк, рисование линий и отправку запросов к экрану. Чтобы не допустить снижения производительности, эти подпрограммы осуществляют запись во внутренний буфер, а не напрямую на терминал. Когда приложение посылает библиотеке S-Lang запрос на обновление физического терминала, она сравнивает новое содержимое с исходным и соответствующим образом оптимизирует последовательность выходных данных.

 

24.2.1. Инициализация управления экраном

Прежде чем использовать функции библиотеки S-Lang для вывода данных на терминал, программа должна послать S-Lang запрос на поиск текущего терминала (как это определено в переменной окружения TERM) в терминальной базе данных. Это осуществляется следующим образом:

void SLtt_get_terminfo(void);

Одной из главных задач функции SLtt_get_terminfo() является установка физического размера экрана в соответствии с размером, указанным в базе данных терминала. Информация о количестве строк и колонок в терминале хранится, соответственно, в SLtt_Screen_Rows и SLtt_Screen_Cols. Хотя данные в терминальной базе данных обычно корректны, в настоящее время широкую популярность приобрели терминалы с изменяемыми размерами (например, xterms). После того как размер такого терминала будет изменен по отношению к размеру, принятому по умолчанию, терминальная база данных не будет содержать корректной информации о размерах терминала. Для компенсации этой неточности библиотека S-Lang позволяет программам восстанавливать исходные значения SLtt_Screen_Rows и SLtt_Screen_Cols после вызова функции SLtt_get_terminfo(). В системе Unix текущие размеры терминала всегда можно узнать с помощью команды TIOCGWINSZ управления вводом-выводом, которая подробно описана в главе 16.

Инициализировать уровень управления экраном в S-Lang можно очень просто:

void SLsmg_init_smg(void);

SLsmg_init_smg()

 

24.2.2. Обновление экрана

Прежде чем результаты выполнения последовательности подпрограмм SLsmg смогут быть отражены на физическом терминале, необходимо вызвать функцию SLsmg_refresh(). Эта функция не принимает аргументы и не возвращает значения, а обновляет физический терминал по результатам рисования чего-либо на экране, которое было выполнено со времени ее последнего вызова.

 

24.2.3. Перемещение курсора

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

extern void SLsmg_gotorc(int row, int column);

Имейте в виду, что верхний левый угол экрана определяется координатами (0, 0), а нижний правый угол — (SLtt_Screen_Rows - 1, SLtt_Screen_Cols - 1).

 

24.2.4. Завершение управления экраном

Когда программа, использующая SLsmg, завершает свою работу, она должна послать библиотеке S-Lang соответствующее сообщение об этом, после чего библиотека освободит буферы и восстановит состояние терминала. Прежде чем сделать это, будет правильным переместить курсор вниз экрана и обновить дисплей, чтобы пользователь смог увидеть все выводимые данные.

 

24.2.5. Скелет программы управления экраном

Ниже приведен пример программы, которая сначала инициализирует возможности библиотеки S-Lang для управления экраном, а затем закрывает их. Хотя эта программа выполняет лишь некоторые действия, она иллюстрирует основы использования функциональных возможностей SLsmg библиотеки S-Lang.

 1: /* slinit.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: int main(void) {

 9:  struct winsize ws;

10:

11:  /* получение размеров терминала, подключенного к stdout */

12:  if (ioctl(1, TIOCGWINSZ, &ws)) {

13:   perror("сбой при получении размеров окна");

14:   return 1;

15:  }

16:

17:  SLtt_get_terminfo();

18:

19:  SLtt_Screen_Rows = ws.ws_row;

20:  SLtt_Screen_Cols = ws.ws_col;

21:

22:  SLsmg_init_smg();

23:

24:  /* здесь находится ядро программы */

25:

26:  SLsmg_gotorc(SLtt_Screen_Rows - 1, 0);

27:  SLsmg_refresh();

28:  SLsmg_reset_smg();

29:  SLang_reset_tty();

30:

31:  return 0;

32: }

 

24.2.6. Переключение наборов символов

Большинство современных терминалов (включая VT100, который достаточно точно эмулирует консоль Linux) поддерживают как минимум два набора символов. Основным набором обычно является ISO-8859-1 или ему подобный; другой набор используется главным образом для линейных символов. Библиотека S-Lang позволяет выбирать набор тех символов, которые будут применяться для вычерчивания символов.

void SLsmg_set_char_set(int useAlternate)

Если функцию SLsmg_set_char_set() вызвать с ненулевым аргументом, на экране будут выводиться новые символы, отображаемые с применением альтернативного набора символов. Если функцию SLsmg_set_char_set() вызвать с нулевым аргументом, то это отображение использоваться не будет, вследствие чего на экране будут появляться обычные символы.

S-Lang определяет набор символических имен для наиболее часто используемых линейных символов, входящих в альтернативный набор. В табл. 24.1 перечислены доступные линейные символы и имена S-Lang для каждого из них.

Таблица 24.1. Линейные символы

Глиф Символическая константа
SLSMG_HLINE_CHAR
SLSMG_VLINE_CHAR
SLSMG_ULCORN_CHAR
SLSMG_URCORN_CHAR
SLSMG_LLCORN_CHAR
SLSMG_LRCORN_CHAR
SLSMG_RTEE_CHAR
SLSMG_LTEE_CHAR
SLSMG_UTEE_CHAR
SLSMG_DTEE_CHAR
SLSMG_PLUS_CHAR

 

24.2.7. Запись на экран

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

void SLsmg_write_char(char ch);

void SLsmg_write_string(char * str);

void SLsmg_write_nchars(char * chars, int length);

void SLsmg_write_nstring(char * str, int length);

void SLsmg_printf(char * format, ...);

void SLsmg_vprintf(char * format, va_list args);

void SLsmg_write_wrapped_string(char * str, int row, int column, int height,

int width, int fill);

Каждая из этих функций, за исключением SLsmg_write_wrapped_string(), записывает требуемую строку в буфер экрана в текущую позицию курсора, используя текущий цвет и набор символов. Однако все они по-разному определяют, какую строку необходимо записать. После того как информация будет записана, курсор переместится в конец выделенной для этого области, как и на обычном терминале. Любая строка, выходящая за пределы правого края экрана, усекается, а не переносится на другую строку. Хотя этот способ отличается от обычного вывода на терминал, он подходит для большинства полноэкранных приложений, в которых текст, переводимый на новую строку, искажает содержимое экрана.

SLsmg_write_char() Среди всех функций вывода данных на экран это самая простая функция. Она записывает передаваемый символ в текущую позицию курсора и перемещает курсор.
SLsmg_write_string() Выводит на экран передаваемую ей строку.
SLsmg_write_nchars() Выводит на экран символы length , на которые указывает chars . Символ конца строки NULL игнорируется — если он будет найден, выводится комбинация '\0' и подпрограмма продолжает работу после окончания строки.
SLsmg_write_nstring() Выводит на экран не более length символов из str . Если str содержит менее length символов, оставшееся пространство заполняется пробелами.
SLsmg_printf() Как можно судить из имени функции, она работает подобно стандартной функции printf() , форматируя первый аргумент, а остальные аргументы используются в качестве параметров для форматирования. После этого на экран выводится сформатированная строка.
SLsmg_vprintf() Подобно функции vfprintf() из библиотеки С. Эта функция ожидает получение аргумента va_arg , который она использует для форматирования первого параметра. Затем на экран выводится сформатированная строка.
SLsmg_write_wrapped_string() Хотя S-Lang отсекает строки, а не переносит их на следующие строки, она предлагает простую функцию для записи строк, перенесенных в произвольную прямоугольную область экрана. Функция SLsmg_write_wrapped_string() записывает строку str в прямоугольную область, которая начинается в row и column и имеет размеры height и width . Несмотря на то что эта подпрограмма осуществляет перенос границ слов, последовательность \n указывает на необходимость перехода на следующую строку. Если последний параметр fill имеет ненулевое значение, то каждая строка будет заполнена по всей ширине прямоугольной области, а при необходимости будут добавляться пробелы.

 

24.2.8. Рисование линий и прямоугольников

Хотя функция SLsmg_set_char_set() предлагает весь спектр функциональных возможностей, необходимых для рисования простой линейной графики на терминале, в библиотеке S-Lang для этого предусмотрено несколько простых функций.

void SLsmg_draw_hline(int row);

void SLsmg_draw_vline(int column);

void SLsmg_draw_box(int row, int column, int height, int width);

Функция SLsmg_draw_hline() рисует одну горизонтальную линию в строке row, а функция SLsmg_draw_vline() — одну вертикальную линию в колонке col.

Функция SLsmg_draw_box() рисует прямоугольник, начиная с row и col, который простирается на height строк и width колонок. Функция SLsmg_draw_box() подобна комбинации функций SLsmg_draw_hline() и SLsmg_draw_vline(), однако вдобавок она получает информацию о вершинах.

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

 1: /* slcharset.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: /* отображает таблицу, содержащую 256 символов из одного набора символов,

 9:    начиная со столбца col. Поверх таблицы отображается метка 'label',

10:    а альтернативный набор символов будет отображаться в том случае,

11:    если isAlternate будет иметь ненулевое значение */

12: static void drawCharSet(int col, int isAlternate, char * label) {

13:  int i, j;

14:  int n = 0;

15:

16:  /* нарисовать прямоугольник */

17:  SLsmg_draw_box(0, col, 20, 38);

18:

19:  /* центрировать надпись */

20:  SLsmg_gotorc(0, col + 2);

21:  SLsmg_write_string(label);

22:

23:

24:  /* нарисовать горизонтальную линию */

25:  SLsmg_gotorc(2, col + 4);

26:  SLsmg_write_string("0123456789ABCDEF");

27:

28:  /* задать используемый набор символов */

29:  SLsmg_set_char_set(isAlternate);

30:

31:  /* итерация по 4 самым старшим битам */

32:  for (i = 0; i < 16; i++) {

33:   SLsmg_gotorc(3 + i, 2 + col);

34:   SLsmg_write_char(i < 10 ? i + '0' : (i - 10) + 'A');

35:

36:   /* итерация по 4 самым младшим битам */

37:   for (j = 0; j < 16; j++) {

38:    SLsmg_gotorc(3 + i, col + 4 + (j * 2));

39:    SLsmg_write_char(n++);

40:   }

41:  }

42:

43:  SLsmg_set_char_set(0);

44: }

45:

46: int main (void) {

47:  struct winsize ws;

48:

49:  /* получить размеры терминала, подключенного к stdout */

50:  if (ioctl(1, TIOCGWINSZ, &ws)) {

51:   perror("сбой при получении размеров окна");

52:   return 1;

53:  }

54:

55:  SLtt_get_terminfо();

56:

57:  SLtt_Screen_Rows = ws.ws_row;

58:  SLtt_Screen_Cols = ws.ws_col;

59:

60:  SLsmg_init_smg();

61:  SLang_init_tty(-1, 0, 1);

62:

63:  drawCharSet(0, 0, "Normal Character Set");

64:  drawCharSet(40, 1, "Alternate Character Set");

65:

66:  SLsmg_refresh();

67:  SLang_getkey();

68:

69:  SLsmg_gotorc(SLtt_Screen_Rows - 1, 0);

70:  SLsmg_refresh();

71:  SLsmg_reset_smg();

72:  SLang_reset_tty();

73:

74:  return 0;

75: }

 

24.2.9. Использование цвета

Библиотека S-Lang упрощает процесс добавления цветов в приложения. Она позволяет использовать палитру, состоящую из 256 элементов, каждый из которых определяет цвет переднего плана и фона. В большинстве приложений используется элемент палитры для одного визуализируемого объекта, например, рамки окна или пункта списка. Настроить цвета палитры можно с помощью функции SLtt_set_color().

void SLtt_set_color(int entry, char * name, char * fg, char * bg);

Первый параметр определяет модифицируемый элемент палитры. Параметр name в настоящий момент игнорируется и должен быть равен NULL. Два последних элемента задают новые цвета переднего плана и фона для данного элемента палитры. В табл. 24.2 приведен список цветов, которые поддерживает библиотека S-Lang; fg и bg должны представлять строки, содержащие имя используемого цвета. Все цвета в левой колонке таблицы могут использоваться как для переднего плана, так и для фона. Цвета в правой колонке таблицы могут служить только в качестве цветов переднего плана. Попытка использования этих цветов для фона может привести к непредсказуемым результатам.

Таблица 24.2. Цвета в S-Lang

Передний план и фон Передний план
black gray
red brightred
green brightgreen
brown yellow
blue brightblue
magenta brightmagenta
cyan brightcyan
lightgray white

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

void SLsmg_set_color(int entry);

Эта функция задает текущий элемент палитры по определенному элементу. Цвета, определяемые этим элементом, будут использоваться при последующих записях на экран.

Хотя приложение может вызывать функции для работы с цветом на терминале любого типа, возможность отображения того или иного цвета будет определяться некоторыми факторами. Глобальная переменная SLtt_Use_Ansi_Colors контролирует отображение цветов. Если эта переменная будет иметь нулевое значение, цвета не используются, а если любое другое значение — то используются.

Функция SLtt_get_terminfo() пытается предположить, будет ли цвет доступен на текущем терминале. К сожалению, многие базы данных termcap и terminfo в этом отношении несовершенны. Если будет задана переменная среды COLORTERM, то S-Lang установит переменную SLtt_Use_Ansi_Colors независимо от того, что отражено в базе данных терминала.

Большинство приложений, обеспечивающих поддержку цветов, также предлагают опцию командной строки, позволяя избирательно разрешать поддержку цветов. Указание этой опции приводит к явной установке переменной SLtt_Use_Ansi_Colors в приложении.

 

Глава 25

Библиотека хешированных баз данных

 

Приложениям часто необходимо хранить некоторую разновидность бинарных данных в файлах. Хранение таких данных, когда во главу угла ставится задача эффективного их извлечения, отличается сложностью и слабой устойчивостью к ошибкам. Существует несколько библиотек, которые предлагают простые API-интерфейсы для хранения информации в файлах. В системах семейства Unix одной из первых использовалась библиотека dbm (впоследствии она была повторно реализована как ndbm), что привело затем к появлению библиотек Berkley db и gdbm проекта GNU. Все эти библиотеки обеспечивали простой доступ к файлам, организованным в виде хеш-таблиц, с двоичным ключом, который обеспечивал доступ к области бинарных данных.

Несмотря на то что gdbm и Berkley db широко доступны в системах Linux, лицензии, сопровождаемые их, снижают удобство их коммерческого использования. Библиотека gdbm во многом похожа на другие библиотеки, но подпадает под действие лицензии LGPL, что делает ее более привлекательной для большинства разработчиков. Базовый API-интерфейс каждой из этих библиотек похож на остальные, поэтому переносить код между библиотеками несложно.

Полный исходный код и документацию по библиотеке gdbm можно найти на Web-сайте по адресу http://qdbm.sourceforge.net. В этой главе будут описаны все функции, которые большинство приложений должны использовать для qdbm (каждая из них имеет близкие аналоги в Berkley db, adbm и ndbm). Доступны также и другие функции API, описание которых можно найти на Web-сайте qdbm.

 

25.1. Обзор

qdbm предлагает несколько различных API-интерфейсов. Самый основной из них, Depot, является низкоуровневым API, который мы и рассмотрим в этой главе. Интерфейс Curia позволяет разбивать базу данных на несколько файлов (для повышения масштабируемости или с целью работы в файловой системе с ограничениями), а функции Villa предлагают две модели: модель B-деревьев и модель транзакций. API-интерфейс Odeon позволяет работать с инвертированными индексами. Два последних API, Relic и Hovel, предлагают реализацию таких интерфейсов, как ndbm и qdbm.

Функции Depot обеспечивают выполнение основных операций по схеме "ключ-значение", при этом ключ используется для извлечения значения. Ключ и значение представляют собой произвольные бинарные потоки, размер которых передается отдельно от данных; библиотеке ничего не нужно знать об их структуре. Однако у интерфейса Depot имеется пара функциональных средств, благодаря которым применение строк в качестве ключей и элементов данных становится более удобным. Во-первых, всякий раз при передаче размера ключа или элемента данных в библиотеку вместо них может быть передано значение -1, на основании которого Depot будет использовать функцию strlen() для вычисления используемого размера. Во-вторых, большинство функций чтения ключей и элементов данных автоматически добавляют к возвращаемому значению байт 0. Этот дополнительный символ не включается в возвращаемый размер, поэтому его можно проигнорировать, если значение не является строкой. Если же это строка, то возвращаемое значение может быть обработано как строка, и приложению не нужно будет завершать ее с помощью NULL.

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

#include

const char * dperrmsg(int ecode);

Подобно strerror(), функция dperrmsg() принимает код ошибки (обычно из dpecode) и возвращает строку, в которой приводится описание возникшей ошибки.

Функции в API-интерфейсе Depot используют указатель на структуру DEPOT. Это непрозрачная структура (программы, использующие Depot, не могут самостоятельно проверять значения в структуре), однако в ней содержится вся информация, используемая библиотекой для обслуживания файла, хранящегося на диске.

 

25.2. Основные операции

 

25.2.1. Открытие файла

qdbm

Библиотечная функция dpopen() используется для открытия файлов базы данных.

#include

DB * dpopen(const char * filename, int omode, int bnum);

Первый аргумент представляет имя файла, который будет использоваться для базы данных. Аргумент omode определяет способ доступа к файлу, и должен иметь одно из двух значений: DP_OREADER и DP_OWRITER, в зависимости от того, какой вид доступа к базе данных необходим программе — для чтения или для записи. По умолчанию база данных блокируется, чтобы разрешить нескольким программам доступ для чтения или одной программе доступ для записи. Если приложению не нужна блокировка, производимая qdbm, то DP_ONOLCK может быть объединен с omode битовым "ИЛИ".

Когда приложения создают новые базы данных, они должны также использовать битовое "ИЛИ" с DP_CREAT для отправки qdbm запроса на создание нового файла, если он еще не был создан. Флаг DP_OTRUNC сигнализирует о том, что первоначальное содержимое filename будет удалено и заменено пустой базой данных.

Последний параметр функции dpopen(), bnum, сообщает qdbm о том, сколько сегментов памяти нужно задействовать в хеш-массиве. Чем меньшим будет значение этого параметра, тем меньший размер будет иметь база данных; чем больше будет его значение, тем быстрее она будет работать благодаря сокращению количества конфликтных ситуаций в хеш-памяти. В документации к qdbm рекомендуется, чтобы это значение составляло от половины до величины, в четыре раза большей от того количества элементов, которые, предположительно, будет иметь база данных. Если вы не уверены, какое следует использовать значение, можно присвоить нулевое значение, которое является значением по умолчанию.

Функция dpopen() возвращает указатель на структуру DEPOT, который передается остальным функциям Depot. В случае возникновения ошибки функция dpopen() возвращает NULL и устанавливает dpecode.

 

25.2.2. Закрытие базы данных

Чтобы закрыть файлы базы данных, используйте функцию dpclose().

int dpclose(DEPOT * depot);

Функция dpclose() возвращает нулевое значение после успешного закрытия файлов и ненулевое — при сбое, который может произойти из-за невозможности очистки данных из буферов базы данных. Ниже показан пример программы, которая открывает файл базы данных в текущем каталоге и сразу же закрывает его.

 1: /* qdbmsimple.c */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: int main(void) {

 9:  DEPOT * dp;

10:

11:  dp = dpopen("test.db", DP_OWRITER | DP_OCREAT, 0);

12:  if (!dp) {

13:   printf("ошибка: %s\n", dperrmsg(dpecode));

14:   return 1;

15:  }

16:

17:  dpclose(dp);

18:

19:  return 0;

20: }

 

25.2.3. Получение файлового дескриптора

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

int dpfdesc(DEPOT * depot);

Эта функция возвращает файловый дескриптор базы данных depqt.

 

25.2.4. Синхронизация базы данных

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

int dpsync(DEPOT * depot);

 

25.3. Чтение записей

 

Прочитать записи в базе данных можно двумя способами: посредством поиска записи по ее ключу и путем чтения последовательных пар "ключ-значение".

 

25.3.1. Чтение определенной записи

Функции dpget() и dpgetwb() производят поиск записей в базе данных по ключу.

int dpget(DEPOT * depot, const char * key, int keySize, int start,

 int max, int * dataSize);

key является элементом (ключом), с помощью которого производится поиск по базе данных, a keySize определяет длину ключа (или значение -1, при котором Depot использует функцию strlen(key) для определения длины ключа). С помощью следующих двух параметров, start и max, можно производить частичное чтение записей; параметр start задает смещение в данных, с которого начнется операция чтения, а max — максимальное количество байтов для чтения. Например, если область данных представляла бы собой массив из четырехбайтовых целочисленных значений int, то в результате присвоения параметру start значения 12 и параметру max значения 8 производилось бы чтение четвертого и пятого элементов массива. Если для чтения доступно менее start байтов, функция dpget() вернет NULL. Чтобы прочитать все байты из данных, параметру max следует присвоить значение -1.

Если последний параметр, dataSize, не будет равен NULL, то целое число, на которое он указывает, будет соответствовать количеству прочитанных байтов.

В случае сбоя эта функция возвращает NULL, а в случае успешного завершения она возвращает указатель на прочитанные данные. В случае сбоя dpcode сообщает о том, что стало причиной сбоя. В частности, если элемент не существует или имеет менее start байтов данных, dpcode будет присвоено DP_ENOITEM.

Когда функция dpget() возвращает данные, к ним добавляется байт 0, позволяя работать с ними как со строкой. Размещение указателя производится с помощью функции malloc(), и приложение отвечает за освобождение памяти после завершения своей работы. Если приложениям необходимо поместить данные в буфер, вместо того чтобы Depot размещала его с помощью функции malloc(), то они должны использовать функцию dpgetwb().

int dpgetwb(DEPOT * depot, const char * key, int keySize, int start,

int max, const char * data);

Функции dpgetwb() и dpget() отличаются друг от друга только двумя параметрами: max (который интерпретируется по-разному) и data (который заменяет параметр dataSize из функции dpgetwb()). Параметр data должен указывать на буфер из max байтов, в который функция dpgetwb() будет помещать данные, прочитанные из базы данных. В функции dpgetwb() параметр max не должен иметь значение -1, и буфер не будет иметь байт 0, автоматически добавляемый в него этой функцией. Функция dpgetwb() возвращает количество байтов, хранящихся в data, и -1, если запись не была найдена, если данных оказалось меньше start байтов или если возникла ошибка.

 

25.3.2. Последовательное чтение записей

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

int dpiterinit(DEPOT * depot);

char * dpiternext(DEPOT * depot, int * keySize);

В результате вызова функции dpiterinit() qdbm вернет первый ключ в базе данных во время следующего вызова функции dpiternext().

Функция dpiternext() возвращает указатель либо на первый ключ в базе данных (если только что была вызвана функция dpiterinit()), либо ключ в базе данных, который следует за ключом, возвращенным в последний раз. Если же в базе данных больше не окажется ключей, будет возвращено NULL. Если keySize не равен NULL, то целочисленное значение, на которое указывает этот параметр, будет задано в качестве размера возвращаемого ключа.

Функция dpiternext() буфера возвращает указатель на размещение, выполненное функцией malloc(); после того как приложение завершит работу с ключом, указатель необходимо освободить функцией free(). Буфер также завершается NULL, поэтому при необходимости его можно трактовать как строку.

 

25.4. Модификация базы данных

 

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

 

25.4.1. Добавление записей

Новые и обновленные записи заносятся в базу данных с использованием функции dpput().

int dpput(DEPOT * dfepot, const char * key, int keySize, const char * data,

 int dataSize, int dmode);

key представляет собой значение индекса, который впоследствии может использоваться для получения информации, на которую указывает data. Параметры keySize и dataSize могут иметь значение -1, при котором функция dpput() будет использовать функцию strlen() для получения размера данного поля. Проверка параметра dmode производится только в том случае, если параметр key в базе данных уже связан с элементом данных. Параметр dmode может иметь одно из перечисленных ниже значений.

DP_DCAT Новые данные добавляются в конец данных, которые уже находятся в базе данных.
DP_DKEEP База данных не модифицируется; функция dpput() возвращает сбой, а параметру dpecode присваивается значение DP_EKEEP .
DP_DOVER Вместо существующего значения записывается новое.

Функция dpput() возвращает нулевое значение в случае возникновения ошибки (или если ключ уже существует, и было определено значение DP_DKEEP), и ненулевое значение, если данные для ключа были успешно обновлены.

 

25.4.2. Удаление записей

Удаление записей из базы данных осуществляется путем вызова функции dpout() и передачи ей ключа, данные которого необходимо удалить.

int dpout(DEPOT * depot, const char * key, int keySize);

Заданный ключ и связанные с ним данные удаляются из базы, после чего возвращается ненулевое значение. Если для заданного ключа данные не существовали, возвращается нулевое значение. Как и для всех остальных функций, принимающих ключ, если параметр keySize равен -1, то функция dpout() использует strlen() для определения длины ключа.

 

25.5. Пример

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

Флаг -а добавляет запись в базу данных. Если будет указан флаг -f, то любой существующий элемент будет заменен новыми данными. Следующий параметр представляет собой значение ключа, которое необходимо использовать, а последний параметр — собственно данные (номер телефона).

Флаг -q запрашивает в базе данных определенный ключ, который должен быть представлен другим указанным параметром. Записи удаляются из базы данных с помощью флага -d, который принимает значение ключа для удаления в другом параметре.

Если задать флаг -l, то будут перечислены все пары "ключ-значение", имеющиеся в базе данных.

Вот как выглядят пример использования phones.

$ ./phones -a Erik 374-5876

$ ./phones -a Michael 642-4235

$ ./phones -a Larry 527-7976

$ ./phones -a Barbara 227-2272

$ ./phones -q Larry

Larry 527-7976

$ ./phones -l

Larry 527-7976

Erik 374-5876

Michael 642-4235

Barbara 227-2272

$ ./phones -d Michael

$ ./phones -l

Larry 527-7976

Erik 374-5876

Barbara 227-2272

Эта программа выполняет определенные полезные действия, состоит менее чем из 200 строк исходного кода, и с успехом может применяться для работы с большим количеством пар "ключ-значение", четко раскрывая назначение библиотеки qdbm.

  1: /* phones.с */

  2:

  3: /* Программа реализует очень простую базу данных телефонных номеров.

  4: Всю необходимую информацию по ее использованию можно найти в тексте. */

  5:

  6: #include

  7: #include

  8: #include

  9: #include

 10: #include

 11: #include

 12: #include

 13: #include

 14:

 15: void usage(void) {

 16:  fprintf(stderr, "использование: phones -a [-f] <имя> <телефон>\n");

 17:  fprintf(stderr, " -d <имя>\n");

 18:  fprintf(stderr, " -q <имя>\n");

 19:  fprintf(stderr, " -l\n");

 20:  exit(1);

 21: }

 22:

 23: /* Открыть базу данных $НОМЕ/.phonedb. Если writeable имеет ненулевое

 24:    значение, база данных открывается для обновления. Если writeable

 25:    равен 0, база данных открывается только для чтения. */

 26: DEPOT * openDatabase(int writeable) {

 27:  DEPOT * dp;

 28:  char * filename;

 29:  int flags;

 30:

 31:  /* Установить режим открытия */

 32:  if (writeable) {

 33:   flags = DP_OWRITER | DP_OCREAT;

 34:  } else {

 35:   flags = DP_OREADER;

 36:  }

 37:

 38:  filename = alloca(strlen(getenv("HOME")) + 20);

 39:  strcpy(filename, getenv("HOME"));

 40:  strcat(filename, "/.phonedb");

 41:

 42:  dp = dpopen(filename, flags, 0);

 43:  if (!dp) {

 44:   fprintf(stderr, "сбой при открытии %s: %s\n", filename,

 45:    dperrmsg(dpecode));

 46:   return NULL;

 47:  }

 48:

 49:  return dp;

 50: }

 51:

 52: /* добавить новую запись в базу данных; произвести

 53:    прямой разбор аргументов командной строки */

 54: int addRecord(int argc, char ** argv) {

 55:  DEPOT * dp;

 56:  char * name, * phone;

 57:  int rc = 0;

 58:  int overwrite = 0;

 59:  int flag;

 60:

 61:  /* проверить параметры; -f означает перезапись

 62:     существующего элемента, а имя и номер телефона

 63:     должны оставаться неизмененными */

 64:  if (!argc) usage();

 65:  if (!strcmp(argv[0], " -f")) {

 66:   overwrite = 1;

 67:   argc--, argv++;

 68:  }

 69:

 70:  if (argc! = 2) usage();

 71:

 72:  name = argv[0];

 73:  phone = argv[1];

 74:

 75:  /* открыть базу данных для записи */

 76:  if (!(dp = openDatabase(1))) return 1;

 77:

 78:  /* если не перезаписывается существующий элемент,

 79:     проверить, не используется ли уже это имя */

 80:  if (!overwrite) {

 81:   flag = DP_DKEEP;

 82:  } else {

 83:   flag = DP_DOVER;

 84:  }

 85:

 86:  if (!dpput(dp, name, -1, phone, -1, flag)) {

 87:   if (dpecode == DP_EKEEP) {

 88:    fprintf(stderr, "%s уже существует\n", name);

 89:   } else {

 90:    fprintf(stderr, "сбой записи: %s\n", dperrmsg(dpecode));

 91:   }

 92:

 93:   rc = 1;

 94:  }

 95:

 96:  dpclose(dp);

 97:

 98:  return rc;

 99: }

100:

101: /* найти имя и вывести номер телефона, с которым оно связано;

102:    напрямую разобрать командную строку */

103: int queryRecord(int argc, char ** argv) {

104:  DEPOT * dp;

105:  int rc;

106:  char * phone;

107:

108:  /* ожидается только один аргумент, имя для поиска */

109:  if (argc != 1) usage();

110:

111:  /* открыть базу данных для чтения */

112:  if (!(dp = openDatabase(0))) return 1;

113:

114:  phone = dpget(dp, argv[0], -1, 0, -1, NULL);

115:  if (!phone) {

116:   if (dpecode == DP_ENOITEM)

117:    fprintf(stderr, "%s не существует\n", argv[0]);

118:   else

119:    fprintf(stderr, "ошибка чтения базы данных: %s\n"

120:     dperrmsg(dpecode));

121:

122:   rc = 1;

123:  } else {

124:   printf("%s %s\n", argv[0], (char *) phone);

125:   rc = 0;

126:  }

127:

128:  dpclose(dp);

129:

130:  return rc;

131: }

132:

133: /* удалить определенную запись; имя передается в качестве

134:    аргумента командной строки */

135: int delRecord(int argc, char ** argv) {

136:  DEPOT * dp;

137:  int rc;

138:

139:  /* ожидается только один аргумент */

140:  if (argc != 1) usage();

141:

142:  /* открыть базу данных для обновления */

143:  if (!(dp = openDatabase(1))) return 1;

144:

145:  if (!(rc = dpout(dp, argv[0], -1))) {

146:   if (dpecode == DP_ENOITEM)

147:    fprintf(stderr, "%s не существует\n", argv[0]);

148:   else

149:    fprintf(stderr, "ошибка удаления элемента: %s\n",

150:     dperrmsg(dpecode));

151:

152:   rc = 1;

153:  }

154:

155:  dpclose(dp);

156:

157:  return rc;

158: }

159:

160: /* вывести список всех записей, имеющихся в базе данных */

161: int listRecords(void) {

162:  DEPOT * dp;

163:  char * key, * value;

164:

165:  /* открыть базу данных только для чтения */

166:  if (!(dp = openDatabase(0))) return 1;

167:

168:  dpiterinit(dp);

169:

170:  /* итерация по всем записям */

171:  while ((key = dpiternext(dp, NULL))) {

172:   value = dpget(dp, key, -1, 0, -1, NULL);

173:   printf("%s %s\n", key, value);

174:  }

175:

176:  dpclose(dp);

177:

178:  return 0;

179: }

180:

181: int main(int argc, char ** argv) {

182:  if (argc == 1) usage();

183:

184:  /* найти флаг режима и вызвать соответствующую функцию

185:     с остальными аргументами */

186:  if (!strcmp(argv[1], "-а"))

187:   return addRecord(argc - 2, argv + 2);

188:  else if (!strcmp(argv[1], "-q"))

189:   return queryRecord(argc - 2, argv + 2);

190:  else if (!strcmp(argv[1], "-d"))

191:   return delRecord(argc - 2, argv + 2);

192:  else if (!strcmp(argv[1], "-l")) {

193:   if (argc != 2) usage();

194:   return listRecords();

195:  }

196:

197:  usage(); /* не обнаружено никаких параметров */

198:  return 0; /* возврат */

199: }

 

Глава 26

Синтаксический анализ параметров командной строки

 

Многие Linux-программы позволяют задавать параметры командной строки. Эти параметры выполняют самые разнообразные функции, однако имеют практически одинаковую синтаксическую структуру. Короткие параметры состоят из символа -, за которым следует один алфавитно-цифровой символ. Длинные параметры, обычные для утилит GNU, состоят из пары символов --, за которыми следует строка, состоящая из букв, цифр и дефисов. После любого из этих параметров может стоять аргумент. Пробел отделяет короткий параметр от его аргументов, а пробел или знак равенства отделяют длинный параметр от аргумента.

Проверить синтаксис параметров командной строки можно многими способами. Наиболее популярным методом является проверка синтаксиса массива argv, выполняемая вручную. Помочь в проверке синтаксиса параметров могут библиотечные функции getopt() и getoptlong(). Функция getopt() присутствует во многих реализациях Unix, однако она поддерживает только короткие параметры. Функция getoptlong() доступна в Linux и позволяет автоматически анализировать синтаксис коротких и длинных параметров.

Библиотека popt предназначена специально для синтаксического анализа параметров. По сравнению с функциями getopt() она обладает некоторыми преимуществами.

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

• Она может анализировать синтаксис произвольного массива, состоящего из элементов в стиле argv. Поэтому библиотеку popt можно применять для синтаксического анализа текстовых строк, представленных в стиле командной строки, из любого источника.

• Библиотека может анализировать синтаксис аргументов многих типов, не требуя для этого дополнительного кода в приложении.

• Она предлагает стандартный метод использования псевдонимов параметров. Программы, использующие библиотеку popt, могут позволить пользователям добавлять новые параметры командной строки, которые будут определяться как комбинации уже существующих параметров. Благодаря этому пользователь может определять новое сложное поведение или изменять поведение существующих параметров, принятое по умолчанию.

• Благодаря особому механизму библиотеки могут анализировать синтаксис одних параметров в тот момент, когда главное приложение анализирует синтаксис других параметров.

• Она может автоматически генерировать сообщение об использовании, в котором будут перечислены параметры, воспринимаемые программой, а также более подробное справочное сообщение.

• Библиотека может генерировать обычные сообщения об ошибках.

Подобно функции getoptlong(), библиотека popt поддерживает короткие и длинные параметры.

Библиотека popt является в высшей степени доступной и может работать на любой POSIX-платформе. Самую последнюю версию библиотеки можно найти по адресу ftp://ftp.rpm.org/pub/rpm. Библиотека popt обладает целым рядом функциональных возможностей, не упоминаемых в этой главе; их описание можно найти на man-странице для popt.

Библиотека popt может распространяться либо под лицензией General Public License GNU, либо под лицензией Library General Public License GNU.

 

26.1. Таблица параметров

 

26.1.1. Определение параметров

Приложения передают библиотеке popt информацию о своих параметрах командной строки через массив структур struct poptOption.

#include

struct poptOption {

 const char * longName; /* может иметь значение NULL */

 char shortName;        /* может иметь значение '\0' */

 int argInfo;

 void * arg;            /* зависит от argInfo */

 int val;               /*0 означает не возвращаться, а просто обновить флаг*/

 char * descrip;        /* необязательное описание параметра */

 char * argDescrip;     /* необязательное описание аргумента */

};

Каждый элемент таблицы определяет один параметр, который может быть передан программе. Длинные и короткие параметры рассматриваются как один параметр, который может встречаться в двух различных формах. Первые два элемента, longName и shortName, определяют имена параметров; первый соответствует длинному имени, а второй — одиночный символ.

Элемент argInfo сообщает библиотеке popt о том, какой тип аргумента ожидается после параметра. Если не ожидается никакого параметра, будет использоваться значение РОPT_ARG_NONE. Остальные допустимые значения перечислены в табл. 26.1.

Таблица 26.1. Типы аргументов popt

Значение Описание Тип arg
POPT_ARG_NONE He ожидается ни одного аргумента. int
POPT_ARG_STRING Не должна выполняться проверка соответствия типов. char *
POPT_ARG_INT Ожидается целочисленный аргумент. int
POPT_ARG_LONG Ожидается длинный целочисленный тип. long
POPT_ARG_FLOAT Ожидается тип с плавающей точкой. float
POPT_ARG_DOUBLE Ожидается тип с плавающей точкой двойной точности. double
POPT_ARG_VAL Не ожидается ни одного аргумента (см. текст). int

Следующий элемент, arg, позволяет библиотеке popt обновлять переменные в программе автоматически в случае использования параметра. Если arg имеет значение NULL, то он будет проигнорирован, и popt не будет выполнять никаких действий. В противном случае он будет указывать на переменную, тип которой задан в правой колонке табл. 26.1.

Если параметр не принимает аргументов (argInfo имеет значение POPT_ARG_NONE), то переменная, на которую указывает arg, получает единичное значение при использовании параметра. Если параметр принимает аргумент, то значение переменной, на которую указывает arg, обновляется до значения аргумента. Аргументы POPT_ARG_STRING могут принимать любую строку, а аргументы POPT_ARG_INT, POPT_ARG_LONG, POPT_ARG_FLOAT и POPT_ARG_DOUBLE преобразуются в соответствующий тип, при этом, если преобразование не удастся выполнить, будет сгенерирована ошибка.

Если используется значение POPT_ARG_VAL, то никаких аргументов не ожидается. Вместо этого popt скопирует целочисленное значение val в адрес, на который указывает arg. Это будет полезно в том случае, когда в программе имеется набор взаимно исключающих аргументов, и выбор падает на последний указанный аргумент. Определяя различные значения val для каждого параметра, когда член arg каждого параметра будет указывать на одно и то же целочисленное значение, и, определяя для каждого из них значение POPT_ARG_VAL, можно легко узнать, какой из этих параметров был определен последним. Если будет задано более одного параметра, то сгенерировать ошибку не удастся.

Член val устанавливает значение, возвращаемое функцией проверки синтаксиса popt при обнаружении параметра, если только не используется значение POPT_ARG_VAL. Если значение будет равно нулю, функция проверки синтаксиса продолжит проверку следующего аргумента командной строки, и не будет возвращать результат.

Два последних члена являются необязательными, и должны иметь значение NULL, если они не нужны. Первый из них, descrip, представляет строку, описывающую параметр. Он используется библиотекой popt во время генерации справочного сообщения, в котором описываются все доступные параметры. Член descrip предлагает эталонный аргумент для параметра, который также используется для отображения справочной информации. Генерация справочных сообщений рассматривается далее в этой главе.

В последней структуре таблицы все значения указателей должны быть равны NULL, а все арифметические значения должны быть нулевыми, отмечая конец таблицы.

Давайте посмотрим, как можно было бы определить таблицу параметров для обычного приложения. Ниже показана таблица параметров для простой версии утилиты grep.

const char * pattern = NULL;

int mode = MODE_REGEXP;

int ignoreCase = 0;

int maxCount = -1;

struct poptOption optionsTable[] = {

 { "extended-regexp", 'E', POPT_ARG_VAL, &mode, MODE_EXTENDED,

   "шаблоном для соответствия является расширенное регулярное выражение",

   NULL },

 { "fixed-strings", 'F', POPT_ARG_VAL, &mode, MODE_FIXED,

   "шаблоном для соответствия является базовая строка (не "

   "регулярное выражение)", NULL } ,

 { "basic-regexp", 'G', POPT_ARG_VAL, &mode, MODE_REGEXP,

   "шаблоном для соответствия является базовое регулярное выражение" },

 { "ignore-case", 'i', POPT_ARG_NONE, &ignoreCase, 0,

   "выполнять поиск, чувствительный к регистру", NULL },

 { "max-count", 'm', POPT_ARG_INT, &maxCount, 0,

   "завершить после получения N совпадений", "N" },

 { "regexp", 'e', POPT_ARG_STRING, &pattern, 0,

   "регулярное выражение для поиска", "pattern" },

 { NULL, '\0', POPT_ARG_NONE, NULL, 0, NULL, NULL }

};

Параметр retry не принимает аргумента, поэтому popt присваивает переменной retry единицу, если определен --retry. Параметры bytes и lines принимают целочисленные аргументы, которые хранятся в переменных с идентичными именами. Последний параметр, follow, может быть либо литеральным name, либо descriptor. Переменная followType задается таким образом, чтобы она указывала на каждое значение, которое будет введено в командной строке, и требует проверки на корректность. Если первоначально она будет указывать на "descriptor", то будет предоставлено полезное значение по умолчанию.

 

26.1.2. Вложенные таблицы параметров

Некоторые библиотеки предлагают реализацию набора общих параметров командной строки. Например, одно из первых инструментальных средств X Window обрабатывало параметры -geometry и -display для приложений, предоставляя большинству программ X Window стандартный набор параметров командной строки для управления обычным поведением. К сожалению, сделать это далеко не просто. Если массивы argc и argv передать функции инициализации в библиотеке, то библиотека сможет обрабатывать соответствующие параметры, однако приложение должно знать, какие параметры необходимо проигнорировать во время синтаксического анализа argv.

Чтобы не допустить возникновения этой проблемы, функция XtAppInitialize() принимала массивы argc и argv в качестве параметров и возвращала новые значения для каждого из них с параметрами, обработанными удаленной библиотекой. Несмотря на то что такой подход мог работать, с ростом количества библиотек он стал излишне громоздким.

Чтобы выйти из этой ситуации, popt позволяет формировать вложенные таблицы параметров. Благодаря этому подходу библиотеки определяют те параметры, которые им нужны для обработки (для этого может потребоваться еще одна вложенная таблица), а главная программа может предоставить эти параметры путем вложения таблиц с параметрами библиотек внутри самих себя.

Таблица параметров, которая будет представлена в форме вложенной таблицы, определяется подобно любой другой таблице. Чтобы включить ее в другую таблицу, необходимо создать новый параметр с пустыми параметрами longName и shortName. В поле argInfo должна быть назначена переменная POPT_ARG_INCLUDE_TABLE, а член arg должен указывать на таблицу, представляемую в форме вложенной таблицы. Ниже показан пример таблицы параметров, включающей другую таблицу.

struct poptOption nestedArgs[] = {

 { "option1", 'a', POPT_ARG_NONE, NULL, 'a' },

 { "option2", 'b', POPT_ARG_NONE, NULL, 'b' },

 { NULL, '\0', POPT_ARG_NONE, NULL, 0 }

};

struct poptOption mainArgs[] = {

 { "anoption", 'о', POPT_ARG_NONE, NULL, 'o' },

 { NULL, '\0', POPT_ARG_INCLUDE_TABLE, nestedArgs, 0 },

 { NULL, '\0', POPT_ARG_NONE, NULL, 0 }

};

В этом примере приложение заканчивается тремя параметрами, --option1, --option2 и --anoption. Более сложный пример с вложенными таблицами параметров рассматривается далее в главе.

 

26.2. Использование таблиц параметров

 

26.2.1. Создание содержимого

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

Новое содержимое popt формируется с помощью функции poptGetContext().

#include

poptContext poptGetContext(char * name, int argc, const char ** argv,

 struct poptOption * options, int flags);

Первый параметр, name, используется для работы с псевдонимами и в справочных сообщениях, и должен представлять имя того приложения, параметры которого будут проходить проверку синтаксиса. Следующие два параметра определяют те аргументы командной строки, которые будут проходить проверку синтаксиса. Как правило, они передаются функции poptGetContext(), точно так, как если бы они передавались функции main() программы. Параметр options указывает на таблицу параметров командной строки, которая была определена в предыдущем разделе. Последний параметр, flags, определяет способ синтаксического анализа параметров и включает перечисленные ниже флаги (которые могут быть объединены битовым "ИЛИ").

POPT_CONTEXT_KEEP_FIRST Как правило, popt игнорирует значение в argv[0] , которое обычно представляет имя выполняемой программы, а не аргумент командной строки. Если определить этот флаг, то popt будет обрабатывать argv[0] как параметр.
POPT_CONTEXT_POSIXMEHADER Стандарт POSIX гласит, что все параметры должны стоять перед дополнительными параметрами командной строки. Например, в соответствии с POSIX, rm -f file1 file2 приведет к удалению файлов file1 и file2 , тогда как rm file1 file2 -f приведет к обычному удалению трех файлов: file1 , file2 и -f . В большинстве Linux-программы это частное условие игнорируется, поэтому popt не придерживается этого правила по умолчанию. Этот флаг сообщает библиотеке popt о необходимости анализировать синтаксис параметров в соответствии с этим условием.

Помимо всего прочего, poptContext следит за тем, какие параметры прошли проверку синтаксиса, а какие нет. Если программе необходимо перезапустить обработку параметров в наборе аргументов, она может восстановить исходное состояние poptContext, передавая функции poptResetContext() содержимое в качестве единственного аргумента.

После завершения обработки аргумента процесс должен освободить структуру poptContext, поскольку в ней содержатся динамически размещаемые компоненты. Функция poptFreeContext() принимает poptContext в качестве своего единственного аргумента и освобождает ресурсы, занятые в содержимом.

Ниже представлены прототипы функций poptResetContext() и poptFreeContext().

#include

void poptFreeContext(poptContext con);

void poptResetContext(poptContext con);

 

26.2.2. Синтаксический анализ командной строки

После того как приложение создаст poptContext, оно может приступить к синтаксическому анализу аргументов. Функция poptGetNextContext() выполняет синтаксический анализ аргумента.

#include

int poptGetNextOpt(poptContext con);

Принимая содержимое в качестве своего единственного аргумента, эта функция анализирует синтаксис следующего обнаруженного аргумента командной строки. После того как следующий аргумент будет обнаружен в таблице параметров, функция заполняет объект, на который указывает указатель arg элемента таблицы параметров, если только он не равен NULL. Если элемент val для параметра имеет ненулевое значение, функция возвращает это значение. В противном случае функция poptGetNextOpt() переходит к следующему аргументу.

Функция poptGetNextOpt() возвращает значение -1, если был проанализирован синтаксис последнего аргумента, и другие отрицательные значения в случае возникновения ошибки. Поэтому лучше всего присваивать элементам val в таблице параметров значения больше нуля.

Если все параметры командной строки обрабатываются через указатели arg, то синтаксический анализ командной строки сокращается до следующей строки кода:

rc = poptGetNextOpt(poptcon);

Тем не менее, для многих приложений требуется более сложный синтаксический анализ командной строки, нежели этот, и применяется показанная ниже структура.

while ((rc = poptGetNextOpt(poptcon)) > 0) {

 switch (rc) {

  /* здесь обрабатываются специфические аргументы */

 }

}

Во время обработки возвращенных параметров приложению необходимо знать значение каждого аргумента, который был определен после параметра. Это можно сделать двумя способами. Один из них заключается в том, чтобы popt присваивала переменной значение параметра из элементов arg таблицы параметров. Другой способ предусматривает применение функции poptGetOptArg().

#include

char * poptGetOptArg(poptContext con);

Эта функция возвращает аргумент, заданный для последнего параметра, возвращенного функцией poptGetNextOpt(), или возвращает значение NULL, если ни один из аргументов не был определен.

 

26.2.3. Остаточные аргументы

Многие приложения принимают произвольное количество аргументов командной строки, например, список имен файлов. Когда popt встречает аргумент, перед которым отсутствует дефис -, она считает его таким аргументом и добавляет его в список остаточных аргументов. Доступ к этим аргументам в приложениях можно реализовать с помощью описанных далее трех функций.

char * poptGetArg(poptContext con);

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

char * poptPeekArg(poptContext con);

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

char ** poptGetArgs(poptContext con);

Эта функция возвращает все остаточные аргументы в виде argv. Последний элемент в возвращаемом массиве указывает на NULL, подтверждая конец аргументов.

 

26.2.4. Автоматические справочные сообщения

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

#include

void poptPrintHelp(poptContext con, FILE * f, int flags);

void poptPrintUsage(poptContext con, FILE * f, int flags);

Обе эти функции ведут себя практически одинаково, записывая соответствующий тип сообщения в файл f. Аргумент flags на данный момент не используется ни одной из этих функций, и должен быть равен нулю для совместимости с будущими версиями библиотеки popt.

Поскольку за справочное сообщение отвечает параметр --help, а за сообщение об использовании — параметр --usage, библиотека popt предлагает простой способ добавления этих двух параметров в программу. Чтобы добавить эти параметры в таблицу параметров, можно использовать макрос POPT_AUTOHELP, который выводит соответствующие сообщения в STDOUT и закрывается после возвращения кода 0. В следующем примере показана таблица параметров в файле grep.с; мы должны добавить одну строку в таблицу параметров для grep, чтобы активизировать автоматическое генерирование справочных сообщений.

 95: struct poptOption optionsTable[] = {

 96:  { "extended-regexp", 'E', POPT_ARG_VAL,

 97:    &mode, MODE_EXTENDED,

 98:    "шаблоном для соответствия является расширенное регулярное "

 99:    "выражение" },

100:  { "fixed-strings", 'F', POPT_ARG_VAL,

101:    &mode, MODE_FIXED,

102:    "шаблоном для соответствия является базовая строка, (не "

103:    "регулярное выражение)", NULL },

104:  { "basic-regexp", 'G', POPT_ARG_VAL,

105:    &mode, MODE_REGEXP,

106:    "шаблоном для соответствия является базовое регулярное выражение" },

107:  { "ignore-case", 'i', POPT_ARG_NONE, &ignoreCase, 0,

108:    "выполнять поиск, чувствительный к регистру", NULL },

109:  { "max-count", 'm', POPT_ARG_INT, &maxCount, 0,

110:    "завершить после получения N совпадений", "N" },

111:  { "regexp", 'е', POPT_ARG_STRING, &pattern, 0,

112:    "регулярное выражение для поиска", "pattern" },

113:  POPT_AUTOHELP

114:  { NULL, ' \0', POPT_ARG_NONE, NULL, 0, NULL, NULL }

115: };

Ниже показан пример того, как выглядит справочное сообщение, сгенерированное данной таблицей параметров.

Usage: grep [OPTION...]

Использование: grep [ПАРАМЕТРЫ...]

 -Е, --extended-regexp  шаблоном для соответствия является

                        расширенное регулярное выражение

 -F, --fixed-strings    шаблоном для соответствия является

                        базовая строка (не регулярное выражение)

 -G, --basic-regexp     шаблоном для соответствия является базовое

                        регулярное выражение

 -i, --ignore-case      выполнять поиск, чувствительный к регистру

 -m, --max-count=N      завершить после получения N совпадений

 -е, --regexp=pattern   регулярное выражение для поиска

Help options:

 -?, --help  Show this help message

 --usage     Display brief usage message

Параметры справки:

 -?, --help  Показать это сообщение

 --usage     Отобразить краткое сообщение об использовании

Хотя эта информация и имеет привлекательный вид, она требует некоторых уточнений. В первой строке не сказано, что команда ожидает имена файлов в командной строке. Показанный здесь текст [OPTION...] принят в popt по умолчанию, и с помощью функции poptSetOtherOptionHelp() может быть изменен для получения более детального описания.

#include

poptSetOtherOptionHelp(poptContext con, const char * text);

Первым параметром является содержимое, а второй параметр определяет текст, который должен появиться после имени программы. Если добавить следующий вызов

poptSetOtherOptionHelp(optCon, "<шаблон> <список файлов>");

то первая строка в справочном сообщении будет изменена на

Usage: grep <шаблон> <список файлов>

Использование: grep <шаблон> <список файлов>

что является более точным.

Последнее, что требуется уточнить в отношении справочных сообщений, это способ обработки вложенных таблиц. Давайте снова обратимся к справочному сообщению для нашей программы grep; для параметров справки выделяется отдельный раздел справочного сообщения. Если элемент POPT_ARG_INCLUDE_TABLE таблицы параметров содержит член descrip, то строка будет использоваться в качестве описания для всех параметров во вложенной таблице, и эти параметры будут отображаться в своем собственном разделе справочного сообщения (подобно параметрам справки для tail). Если descrip будет иметь значение NULL, то параметры для вложенной таблицы будут отображаться вместе с параметрами из главной таблицы, а не в своем собственном разделе.

Иногда программы предлагают параметры, которые, возможно, не должны использоваться; они могут быть включены для поддержки унаследованных приложений или приложений, разработанных только для тестирования. Автоматическая генерация справочных сообщений для такого параметра можно подавить с помощью битового "ИЛИ" POPT_ARGFLAG_DOC_HIDDEN и члена arg структуры struct poptOption, описывающей данный параметр.

 

26.3. Использование обратных вызовов

Мы показали два способа обработки параметров с помощью библиотеки popt: с помощью возврата параметра функцией poptGetNextOpt() и путем автоматического изменения переменных во время представления параметров. К сожалению, ни один из этих способов не подходит для вложенных таблиц. Очевидно, что возвращение параметров, определяемых во вложенной таблице для обработки в приложении, не будет работать, поскольку вложенные таблицы предназначены для того, чтобы приложению не нужно было знать, какие параметры предлагает библиотека. Присвоение переменным значений тоже не подходит, поскольку в этом случае не ясно, каким переменным нужно присваивать значения. Использование глобальных переменных часто тоже является неподходящим, а библиотека не имеет доступных для использования локальных переменных, поскольку синтаксический анализ выполняется из главного приложения, а не из библиотеки. Чтобы обеспечить гибкую обработку параметров во вложенных таблицах, библиотека popt предлагает использовать обратные вызовы (callback).

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

Обратные вызовы можно определять только в первом элементе таблицы параметров. Если этот элемент определяет обратный вызов, член argInfo будет иметь значение POPT_ARG_CALLBACK, a arg будет указывать на функцию обратного вызова. Член descrip может представлять любое значение указателя, и передается в обратный вызов каждый раз во время его инициирования, открывая доступ к любым произвольным данным. Все остальные члены структуры struct poptOption должны иметь нулевое значение или NULL.

Во время обработки параметров обратный вызов можно инициировать в трех точках: до начала обработки, при нахождении параметра в таблице для данного обратного вызова и после завершения обработки. Это дает библиотекам возможность инициализировать любые необходимые им структуры (включая данные, определяемые членом descrip), и выполнять любые служебные действия, которые могут понадобиться после завершения обработки (например, очищать динамическую память, выделенную для члена descrip). Они всегда вызываются при нахождении параметра, однако в таблице параметров необходимо указать, что их нужно вызывать в двух других местах. Чтобы сделать это, значения POPT_CBFLAG_PRE или POPT_CBFLAG_POST (или оба) должны объединяться битовым "ИЛИ" со значением POPT_ARG_CALLBACK, присвоенным члену arg структуры, которая определяет обратный вызов.

Далее показан прототип, который следует использовать для определения функции обратного вызова:

void callback(poptContext con, enum poptCallbackReason reason,

 const struct poptOption * opt, const char * arg,

 const void * data);

Первый параметр представляет содержимое, синтаксический анализ которого будет выполнен во время инициирования обратного вызова. Следующим параметров является POPT_CALLBACK_REASON_PRE, если обработка параметра еще не началась, POPT_CALLBACK_REASON_POST, если обработка параметров завершена, или POPT_CALLBACK_REASON_OPTION, если в таблице для данного обратного вызова был обнаружен параметр. Если этот параметр является последним, то аргумент opt будет указывать на элемент таблицы параметров для обнаруженного параметра, а аргумент arg будет указывать на строку, определяющую аргумент для данного параметра. Если ожидается аргумент, не представленный в виде строки, обратный вызов будет отвечать за проверку типа и преобразование аргумента. Последний параметр для обратного вызова, data, представляет собой значение поля descrip в элементе таблицы параметров, который задает обратный вызов.

Ниже показан пример библиотеки, которая использует вложенную таблицу popt и обратные вызовы для синтаксического анализа некоторых параметров командной строки. Структура данных инициализируется до начала синтаксического анализа командной строки, а затем отображаются последние значения.

 1: /* popt-lib.с */

 2:

 3: #include

 4: #include

 5:

 6: struct params {

 7:  int height, width;

 8:  char*fg,*bg;

 9: };

10:

11: static void callback(poptContext con,

12:  enum poptCallbackReason reason,

13:  const struct poptOption * opt,

14:  const char * arg,

15:  const void * data);

16:

17: /* Здесь сохраняются переменные, которые прошли синтаксический анализ. Обычно

18:    глобальные переменные использовать не рекомендуется, зато работать с ними проще.*/

19: struct params ourParam;

20:

21: struct poptOption libTable[] = {

22:  { NULL, '\0',

23:    POPT_ARG_CALLBACK | POPT_CBFLAG_PRE | POPT_CBFLAG_POST,

24:    callback, '\0', (void *) &ourParam, NULL },

25:  { "height", 'h', POPT_ARG_STRING, NULL, '\0', NULL, NULL },

26:  { "width", 'w', POPT_ARG_STRING, NULL, '\0', NULL, NULL },

27:  { "fg", 'f', POPT_ARG_STRING, NULL, '\0', NULL, NULL },

28:  { "bg", 'b', POPT_ARG_STRING, NULL, '\0', NULL, NULL },

29:  { NULL, '\0', POPT_ARG_NONE, NULL, '\0', NULL, NULL }

30: };

31:

32: static void callback(poptContext con,

33:  enum poptCallbackReason reason,

34:  const struct poptOption * opt,

35:  const char * arg,

36:  const void * data) {

37:  struct params * p = (void *) data;

38:  char * chptr = NULL;

39:

40:  if (reason == POPT_CALLBACK_REASON_PRE) {

41:   p->height = 640;

42:   p->width = 480;

43:   p->fg = "white";

44:   p->bg = "black";

45:  } else if (reason == POPT_CALLBACK_REASON_POST) {

46:   printf("используется высота %d ширина %d передний план %s фон %s\n",

47:   p->height, p->width, p->fg, p->bg);

48:

49:  } else {

50:   switch (opt->shortName) {

51:   case 'h': p->height = strtol(arg, &chptr, 10); break;

52:   case 'w': p->width = strtol(arg, &chptr, 10); break;

53:   case 'f' : p->fg = (char *) arg; break;

54:   case 'b': p->bg = (char *) arg; break;

55:   }

56:

57:   if (chptr && *chptr) {

58:    fprintf(stderr, "для %s ожидался числовой аргумент\n",

59:     opt->longName);

60:    exit(1);

61:   }

62:  }

63: }

64:

Программа, для которой необходимо обеспечить эти аргументы командной строки, должна включать одну дополнительную строку в своей таблице popt. Обычно этой строкой является макрос, задаваемый в заголовочном файле (подобно тому, как реализуется POPT_AUTOHELP), но в целях упрощения в данном примере мы просто явным образом покажем эту строку.

 1: /* popt-nest.c */

 2:

 3: #include

 4:

 5: /* Обычно это объявление осуществляется в заголовочном файле */

 6: extern struct poptOption libTable[];

 7:

 8: int main(int argc, const char * argv[]) {

 9:  poptContext optCon;

10:  int rc;

11:  struct poptOption options[] = {

12:   { "app1", '\0', POPT_ARG_NONE, NULL, '\0' },

13:   { NULL, '\0', POPT_ARG_INCLUDE_TABLE, libTable,

14:     '\0', "Nested:", }

15:     POPT_AUTOHELP

16:   { NULL, '\0', POPT_ARG_NONE, NULL, '\0' }

17:  };

18:

19:  optCon = poptGetContext("popt-nest", argc, argv, options, 0);

20:

21:  if ((rc = poptGetNextOpt (optCon)) < -1) {

22:   fprintf(stderr, "%s: %s\n",

23:    poptBadOption(optCon, POPT_BADOPTION_NOALIAS),

24:   poptStrerror(rc));

25:   return 1;

26:  }

27:

28:  return 0;

29: }

 

26.4. Обработка ошибок

Каждая из функций popt, которая может возвращать ошибки, возвращает целочисленные значения. В случае возникновения ошибки возвращается отрицательный код. В табл. 26.2 перечислены коды возможных ошибок. После таблицы дается подробное обсуждение каждой ошибки.

Таблица 26.2. Коды ошибок popt

Код ошибки Описание
POPT_ERROR_NOARG Отсутствует аргумент для данного параметра.
POPT_ERROR_BADOPT Невозможно проанализировать синтаксис аргумента параметра.
POPT_ERROR_OPTSTOODEEP Слишком глубокое вложение замещений имени параметра.
POPT_ERROR_BADQUOTE Несоответствие кавычек.
POPT_ERROR_BADNUMBER Невозможно преобразовать параметр в число.
POPT_ERROR_OVERFLOW Данное число слишком большое или слишком маленькое.
POPT_ERROR_NOARG Параметр, для которого требуется аргумент, был определен в командной строке, однако аргумент не был предоставлен. Эта ошибка может быть возвращена только функцией poptGetNextOpt() .
POPT_ERROR_BADOPT Параметр был определен в массиве argv , однако его нет в таблице параметров. Эта ошибка может быть возвращена только функцией poptGetNextOpt() .
POPT_ERROR_OPTSTOODEEP Совокупность замещений имени параметра имеет большую глубину вложений. На данный момент popt отслеживает параметры только до 10 уровня, чтобы избежать возникновения бесконечной рекурсии. Эту ошибку возвращает только функция poptGetNextOpt() .
POPT_ERROR_BADQUOTE В строке, прошедшей синтаксический анализ, было обнаружено несоответствие кавычек (например, была обнаружена только одна одинарная кавычка). Эту ошибку могут возвращать функции poptParseArgvString() , poptReadConfigFile() и poptReadDefaultConfig() .
POPT_ERROR_BADNUMBER Преобразование строки в число ( int или long ) не было выполнено вследствие того, что строка содержит нецифровые символы. Эта ошибка возникает в том случае, когда функция poptGetNextOpt() обрабатывает аргумент типа РOРТ_ARG_INT или POPT_ARG_LONG .
POPT_ERROR_OVERFLOW Преобразование из строки в число не было выполнено вследствие того, что число было слишком большим или слишком маленьким. Подобно ошибке POPT_ERROR_BADNUMBER , эта ошибка может возникнуть только в том случае, если функция poptGetNextOpt() обрабатывает аргумент типа РОРТ_ARG_INT или POPT_ARG_LONG .
POPT_ERROR_ERRNO Системный вызов был возвращен вместе с ошибкой, а errno до сих пор содержит ошибку из системного вызова. Эту ошибку могут возвращать функции poptReadConfigFile() и poptReadDefaultConfig() .

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

const char * poptStrerror(const int error);

Эта функция принимает код ошибки popt и возвращает строку с описанием ошибки, как и стандартная функция strerror().

char * poptBadOption(poptContext con, int flags);

Если во время выполнения функции poptGetNextOpt() возникла ошибка, эта функция возвращает параметр, вызвавший ошибку. Если аргументу flags присвоено значение POPT_BADOPTION_NOALIAS, возвращается самый внешний параметр. В противном случае аргумент flags должен иметь нулевое значение, а возвращаемый параметр может быть определен посредством псевдонима.

Для большинства приложений эти две функции существенно упрощают обработку ошибок popt. Если ошибка возникает во время выполнения большинства функций, то выводится сообщение об ошибке, а функция poptStrerror() возвращает строку с описанием ошибки. Если ошибка возникла во время синтаксического анализа аргумента, то код, подобный представленному ниже, отобразит информативное сообщение об ошибке.

fprintf(stderr, "%s: %s\n",

 poptBadOption(optCon, POPT_BADOPTION_NOALIAS),

 poptStrerror(rc));

 

26.5. Псевдонимы параметров

 

Одним из основных преимуществ использования библиотеки popt по сравнению с функцией getopt() является возможность использования псевдонимов параметров. Благодаря ним пользователь может определить параметры, которые popt будет расширять их на другие параметры по мере их определения. Если стандартная программа grep использовала popt, то пользователи могли добавлять параметр --text, который расширялся до -i -n -Е -2, облегчая поиск информации в текстовых файлах.

 

26.5.1. Определение псевдонимов

Псевдонимы обычно определяются в двух местах: в /etc/popt и в файле .popt, хранящемся в домашнем каталоге пользователя (его можно найти через переменную окружения HOME). Оба файла имеют одинаковую форму в виде произвольного количества строк, форматированных следующим образом:

appname alias newoption expansion

appname представляет имя приложения, которое должно быть таким же именем, как и имя в параметре name, переданное функции poptGetContext(). Благодаря этому в каждом файле можно определять псевдонимы для нескольких программ. Ключевое слово alias указывает на то, что определяется псевдоним; на данный момент конфигурационные файлы popt поддерживают только псевдонимы, однако в будущем появятся новые возможности. Следующим параметром является параметр, для которого необходимо задать псевдоним; это может быть как короткий, так и длинный параметр. Остальная часть строки определяет расширение псевдонима. Синтаксический анализ строки выполняется по аналогии с командой оболочки, в которой в качестве кавычек можно использовать символы \, " и '. Если последним символом строки будет обратная косая черта, то следующая строка в файле трактуется как логическое продолжение строки, содержащей этот символ, как и в оболочке.

Следующий элемент добавляет параметр --text в команду grep, как было предложено в начале этого раздела.

grep alias --text -i -n -E -2

 

26.5.2. Разрешение псевдонимов

Приложение должно разрешать разворачивание псевдонимов для popContext перед первым вызовом функции poptGetNextOpt(). Псевдонимы для содержимого определяются с помощью трех функций.

int poptReadDefaultConfig(poptContext con, int flags);

Эта функция считывает псевдонимы из /etc/popt и файла .popt в домашнем каталоге пользователя. На данный момент flags должен иметь нулевое значение, поскольку он зарезервирован только для будущего использования.

int poptReadConfigFile(poptContext con, char * fn);

Файл, определяемый посредством fn, открывается и анализируется как конфигурационный файл popt. Это позволяет программам использовать конфигурационные файлы конкретных программ.

int poptAddAlias(poptContext con, struct poptAlias alias, int flags);

В некоторых случаях в программах необходимо определять псевдонимы, не читая их из конфигурационного файла. Эта функция добавляет новый псевдоним в содержимое. Аргумент flags должен иметь нулевое значение, и в настоящий момент он зарезервирован только для будущего использования. Новый псевдоним определяется как struct poptAlias следующим образом:

struct poptAlias {

 char * longName; /* может быть NULL */

 char shortName;  /* может быть '\0' */

 int argc;

 char ** argv;    /*должна быть возможность освобождения с помощью free()*/

};

Первые два элемента, longName и shortName, определяют параметр, для которого вводится псевдоним. Два последних аргумента, argc и argv, определяют разворачивание, которое будет использовано при обнаружении псевдонима параметра.

 

26.6. Синтаксический анализ строк аргументов

Хотя библиотека popt обычно используется для синтаксического анализа аргументов, уже разделенных на массив вида argv, в некоторых программах необходимо анализировать синтаксис строк, формат которых идентичен командным строкам. Для этой цели popt предлагает функцию, которая анализирует синтаксис строки в виде массива строки, руководствуясь правилами, подобными обычному синтаксическому анализу в оболочке.

#include

int poptParseArgvString(char * s, int * argcPtr, char *** argvPtr);

Строка s разбирается в массив argv. Целочисленное значение, на которое указывает второй параметр, argcPtr, содержит количество проанализированных элементов, а указатель, на который ссылается последний параметр, указывает на вновь созданный массив. Размещение массива осуществляется динамически; после того как приложение завершит работу с массивом, необходимо вызвать функцию free().

Массив argvPtr, созданный функцией poptParseArgvString(), подходит для прямой передачи функции poptGetContext().

 

26.7. Обработка дополнительных аргументов

Некоторые приложения реализуют эквивалент псевдонимов параметров, однако для этого им необходима специальная логика. Функция poptStuffArgs() позволяет приложению вставлять новые аргументы в текущую структуру poptContext.

#include

int poptStuffArgs(poptContext con, char ** argv);

Передаваемый массив argv должен иметь указатель NULL в качестве своего последнего элемента. Когда функция poptGetNextContext() будет вызвана в следующий раз, то анализироваться будут сначала "заполненные" аргументы. Библиотека popt возвращает обычные аргументы после того, как закончатся все "заполненные".

 

26.8. Пример приложения

Библиотека popt используется для обработки параметров во многих примерах из других глав книги. Простая реализация grep представлена в главе 23, a robin — в главе 16. Обе реализации предлагают хорошие примеры использования библиотеки popt в большинстве приложений.

RPM, популярная программа для управления пакетами Linux, интенсивно использует функциональные возможности библиотеки popt. Многие из ее аргументов командной строки реализованы через псевдонимы popt, что делает RPM превосходным примером применения преимуществ popt. Более подробную информацию о RPM доступна по адресу http://www.rpm.org.

Программа Logrotate помогает в управлении системными файлами-журналами. Подобно RPM, она являет собой простой пример использования библиотеки popt и входит в состав большинства дистрибутивов Linux.

 

Глава 27

Динамическая загрузка во время выполнения

 

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

Многие Unix-приложения, в частности крупные приложения, в основном реализуются в виде отдельных блоков кода, часто называемых подключаемыми модулями или просто модулями. В некоторых случаях они реализуются в виде полностью независимых программ, которые связываются с основным кодом приложения через каналы или с помощью какой-то другой разновидности межпроцессного взаимодействия (interprocess communication — IPC). В остальных случаях они реализуются в виде разделяемых объектов.

Разделяемые объекты обычно создаются подобно стандартным разделяемым библиотекам (см. главу 8), однако используются они совершенно иначе. Компоновщику никогда не сообщается о разделяемых объектах, и во время компоновки приложения они даже не нужны. Их не нужно устанавливать в системе таким же способом, как и разделяемые библиотеки.

Подобно обычным разделяемым библиотекам, разделяемые объекты должны компоноваться явным образом с каждой библиотекой, которая их вызывает. Это позволит гарантировать, что динамический загрузчик корректно разрешит работу всех внешних ссылок при загрузке разделяемого объекта. Если этого не сделать, то внешние ссылки будут разрешены только в контексте того приложения, которое в данном случае будет загружать разделяемый объект. Теоретически разделяемые объекты могут быть стандартными объектными файлами. Однако так поступать не рекомендуется, поскольку внешние зависимости разделяемой библиотеки не будут разрешены должным образом, как и разделяемой библиотеки, явным образом не скомпонованной относительно всех библиотек, от которых она зависит.

Символьные имена, используемые в разделяемых объектах, не обязательно должны быть уникальными среди различных разделяемых объектов, загружаемых в одну и ту же программу; обычно они таковыми и не являются. Различные разделяемые объекты, написанные для одного и того же интерфейса, обычно используют точки входа с одинаковыми именами. Для обычных разделяемых библиотек такая практика будет истинным бедствием, а для разделяемых объектов, динамически загружаемых во время выполнения, именно так и поступают.

Пожалуй, чаще всего разделяемые объекты, загружаемые во время выполнения, применяются при создании интерфейса для некоторого общего средства, которое может иметь множество различных реализаций. Рассмотрим, к примеру, процедуру сохранения графического файла. Приложение может иметь один внутренний формат для управления его графикой, однако существует много других форматов файлов, в которых приложению понадобится сохранить графические данные, и еще больше форматов создано для разнообразных частных ситуаций [21]. Обобщенный интерфейс для сохранения графического файла, который экспортируется разделяемыми объектами, загружаемыми во время выполнения, позволяет программистам добавлять новые форматы графических файлов в приложение без повторной его компиляции. Если интерфейс хорошо документирован, то даже независимые разработчики, не имеющие исходного кода приложения, смогут включать новые форматы графических файлов.

Точно так же используется и код каркаса (framework), который предлагает только интерфейс, а не реализацию. Например, каркас РАМ (Pluggable Authentication Modules — подключаемые модули аутентификации) предлагает обобщенный интерфейс для методов аутентификации с запросом и подтверждением, например, с участием имен пользователей и паролей. Сам процесс аутентификации осуществляется посредством модулей, а решение о выборе модуля аутентификации для отдельно взятого приложения принимается во время выполнения (а не во время компиляции) за счет обращения к конфигурационным файлам. Этот интерфейс имеет хорошее описание и является стабильным, а новые модули можно внедрять и использовать в любой момент без повторной компиляции каркаса или приложения. Каркас загружается в виде разделяемой библиотеки, а код в этой разделяемой библиотеке загружает и выгружает модули, обеспечивающие методы аутентификации.

 

27.1. Интерфейс

dl

 

Процесс динамической загрузки заключается в открытии библиотеки, поиске любого количества символов, обработке любых возникающих ошибок и закрытии библиотеки. Все функции динамической загрузки объявляются в одном заголовочном файле, , и определяются в libdl (чтобы воспользоваться функциями динамической загрузки скомпонуйте приложение с -ldl).

Функция dlerror() возвращает строку, описывающую самую последнюю ошибку, которая возникла в одной из трех других функций динамической загрузки:

const char * dlerror(void);

Каждый раз при возврате значения она очищает состояние ошибки. Если не будет создано другое состояние ошибки, она продолжит выполнение, чтобы вернуть NULL вместо строки. Объяснение этого необычного поведения можно найти в описании функции dlsym().

Функция dlopen() открывает библиотеку. Этот процесс включает поиск библиотечного файла, открытие файла и выполнение некоторой предварительной обработки. Переменные окружения и параметры, переданные функции dlopen(), определяют детали этого процесса.

void * dlopen(const char * filename, int flag);

Если filename является абсолютным путем (то есть начинается с символа /), то функции dlopen() не нужно производить поиск библиотеки. Это обычный способ применения функции dlopen() в коде приложения. Если filename является простым именем файла, то функция dlopen() произведет поиск библиотеки filename в перечисленных ниже местах.

• Набор каталогов, разделенных двоеточием, который определен в переменной окружения LD_ELF_LIBRARY_PATH, или, если ее не существует, в переменной LD_LIBRARY_PATH.

• Библиотеки, определенные в файле /etc/ld.so.cache. Этот файл генерируется программой ldcoding, регистрирующей каждую библиотеку, которую она находит в каталоге, указанном в /etc/ld.so.conf, во время ее выполнения.

• /usr/lib

• /lib

Если filename равен NULL, то функция dlopen() открывает экземпляр текущего исполняемого файла. Это полезно только в редких случаях. В случае сбоя функция dlopen() возвращает NULL.

Поиск файлов является простой частью работы функции dlopen(); разрешение символов является более сложной задачей. Существует два фундаментально разных типа разрешения символов: немедленный (immediate) и отложенный (lazy). При немедленном разрешении функция dlopen() разрешает все неразрешенные символы до возвращения результата; под отложенным разрешением подразумевается, что разрешение символов будет происходить по требованию.

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

Во время разработки и отладки вы практически во всех случаях будете использовать немедленное разрешение. Если ваши разделяемые объекты имеют неразрешенные символы, вам нужно будет знать об этом немедленно, а не тогда, когда в программе произойдет сбой во время выполнения кода, который на первый взгляд не будет иметь к этому отношения. Отложенное разрешение станет причиной сложно воспроизводимых ошибок, если вы не проверите свои разделяемые объекты сначала с немедленным разрешением.

Это особенно относится к тем случаям, когда вам необходимо, чтобы разделяемые объекты, зависящие от других разделяемых объектов, могли передавать некоторые свои символы. Если разделяемый объект А зависит от символа b в разделяемом объекте В, а В загружается после А, то отложенное разрешение b сможет быть выполнено только после загрузки объекта В, а до его загрузки — нет. Если написать код с немедленным разрешением, то вы сможете перехватить эту ошибку еще до того, как она сможет стать причиной возникновения проблем.

Здесь подразумевается, что загружать модули нужно всегда в обратном порядке по отношению к их зависимостям: если объект А зависит от объекта В в некоторых его символах, вы должны загрузить объект В до загрузки объекта А, и должны выгрузить объект А до выгрузки объекта B. К счастью, многие приложения с динамически загружаемыми разделяемыми объектами не имеют подобных взаимозависимостей.

По умолчанию символы в разделяемом объекте не экспортируются и потому не используются для разрешения символов в остальных разделяемых объектах. Они будут доступны только для их поиска и использования, о чем будет сказано в следующем разделе. Однако вы можете экспортировать все символы из одного разделяемого объекта во все остальные разделяемые объекты; эти символы будут доступны всем разделяемым объектам, которые будут загружены позже.

Управление всеми этими действиями осуществляется через аргумент flags. Он должен иметь значение RTLD_LAZY для отложенного разрешения и RTLD_NOW для немедленного разрешения. Любое из этих значений может быть объединено битовым "ИЛИ" с RTLD_GLOBAL, чтобы разрешить экспортирование символов в остальные модули.

Если разделяемый объект экспортирует программу _init, то она будет выполняться до того, как функция dlopen() вернет результат.

Функция dlopen() возвращает дескриптор (handle) того разделяемого объекта, который она открыла. Это непрозрачный объектный дескриптор, который следует использовать только как аргумент для последующих вызовов функций dlsym() и dlclose(). Если разделяемый объект открывается несколько раз, функция dlopen() каждый раз будет возвращать один и тот же дескриптор, и с каждым новым вызовом счетчик ссылок будет увеличиваться на единицу.

Функция dlsym() производит поиск символа в библиотеке:

void * dlsym(void * handle, char * symbol);

handle должен представлять собой дескриптор, возвращенный функцией dlopen(), a symbol должен содержать строку с завершающим NULL, которая именует искомый символ. Функция dlsym() возвращает адрес определенного вами символа или NULL в случае возникновения неустранимой ошибки. Если вы будете знать, что NULL не является правильным адресом символа (например, при поиске адреса функции), можно выполнить проверку на наличие ошибок, посмотрев, возвращает ли она NULL. Однако в общем случае некоторые символы могут иметь нулевые значения и быть равными NULL. Тогда вам нужно будет узнать, не возвращает ли функция dlerror() ошибку. Поскольку функция dlerror() возвращает ошибку только один раз, возвращая после этого NULL, вы должны организовать свой код следующим образом.

/* удалить любое состояние ошибки, которое еще не было прочитано */

dlerror();

p = dlsym(handle, "this_symbol");

if ((error = dlerror()) != NULL) {

 /* обработка ошибки */

}

Так как функция dlsym() возвращает void *, вам необходимо использовать приведение типов, чтобы компилятор С не выдавал сообщений об ошибках. Если вы сохраняете указатель, возвращенный функцией dlsym(), сохраните его в переменной того типа, который вы хотите использовать, и выполните приведение типа во время вызова функции dlsym(). Не сохраняйте результат в переменной void *; вам придется выполнять приведение типов каждый раз во время ее использования.

Функция dlclose() закрывает библиотеку.

void * dlclose(void * handle);

Функция dlclose() проверяет счетчик обращений, который увеличивался на единицу при каждом повторном вызове функции dlopen(), и если он равен нулю, она закрывает библиотеку. Этот счетчик обращений позволяет библиотекам применять функции dlopen() и dlclose() для произвольных объектов, не беспокоясь о том, что код, в котором производится вызов, уже открыл какие-либо из этих объектов.

 

27.1.1. Пример

В главе 8 был представлен пример использования обычной разделяемой библиотеки. Библиотеку libhello.so, которую нам удалось создать, можно загружать во время выполнения. Программа loadhello загружает libhello.so динамически и вызывает функцию print_hello, которая находится в библиотеке.

Ниже показан код loadhello.с.

 1: /* loadhello.с */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: typedef void (*hello_function) (void);

 8:

 9: int main(void) {

10:  void * library;

11:  hello_function hello;

12:  const char * error;

13:

14:  library = dlopen("libhello.so", RTLD_LAZY);

15:  if (library == NULL) {

16:   fprintf (stderr, "He удается открыть libhello.so: %s\n",

17:    dlerror());

18:   exit(1);

19:  }

20:

21:  /* Хотя в данном случае мы знаем, что символ print_hello никогда

22:   * не должен быть равен NULL, при поиске произвольных символов

23:   * все происходит иначе. Поэтому вместо проверки результата функции dlsym()

24:   * мы показываем пример проверки кода, возвращаемого функцией dlerror().

25:   */

26:  dlerror();

27:  hello = dlsym(library, "print_hello");

28:  error = dlerror();

29:  if (error) {

30:   fprintf(stderr, "He удается найти print_hello: %s\n", error);

31:   exit(1);

32:  }

33:

34:  (*hello)();

35:  dlclose(library);

36:  return 0;

37: }

 

Глава 28

Идентификация и аутентификация пользователей

 

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

 

28.1. Преобразование идентификатора в имя

 

В результате выполнения команды ls -l для вывода списка содержимого текущего каталога в третьей и четвертой колонках указываются идентификаторы (ID) пользователя и группы, к которой принадлежит каждый файл. Этот список выглядит примерно следующим образом.

drwxrwxr-x  5 christid christid 1024 Aug 15 02:30 christid

drwxr-xr-x 73 johnsonm root     4096 Jan 18 12:48 johnsonm

drwxr-xr-x 25 kim      root     2048 Jan 12 21:13 kim

drwxrwsr-x  2 tytso    tytso    1024 Jan 30  1996 tytso

Однако нигде в ядре не хранится строка christid. Программа ls осуществляет преобразование номеров, предоставленных ядром, в имена. Она получает номера из системного вызова stat() и производит поиск имен в двух системных базах данных. Обычно эти базы данных хранятся в файлах /etc/passwd и /etc/group, хотя в некоторых системах информация может располагаться где-нибудь в сети или в каком-то другом нестандартном месте. Программистам не нужно беспокоиться о том, где хранится эта информация; библиотека С предлагает обобщённые функции, которые считывают конфигурационные файлы для определения места хранения этой информации, производят выборку информации и возвращают ее незаметно для вас.

Чтобы продемонстрировать, что программа ls получает из ядра, выполним команду ls -ln.

drwxrwxr-x  5 500  500  1024 Aug 15 02:30 christid

drwxr-xr-x 73 100  0    4096 Jan 18 12:48 johnsonm

drwxr-xr-x 25 101  0    2048 Jan 12 21:13 kim

drwxrwsr-x  2 1008 1008 1024 Jan 30 1996  tytso

Структура, представляющая элементы в /etc/passwd (или эквивалентной базы данных системы), определена в .

struct passwd {

char * pw_name;    /* Имя пользователя */

 char * pw_passwd; /* Пароль */

 __uid_t pw_uid;   /* Идентификатор пользователя */

 __gid_t pw_gid;   /* Идентификатор группы */

 char * pw_gecos;  /* Настоящее имя */

 char * pw_dir;    /* Домашний каталог */

 char * pw_shell;  /* Программа shell */

};

• pw_name представляет уникальное имя пользователя.

• pw_passwd может представлять зашифрованный пароль или нечто подобное, связанное с процедурой аутентификации. Зависит от системы.

• pw_uid представляет номер (обычно уникальный), который используется в ядре для идентификации пользователя.

• pw_gid представляет главную группу, которую ядро связывает с пользователем.

• pw_gecos представляет член, зависящий от системы, который хранит информацию о пользователе. Обычно сюда включается настоящее имя пользователя; во многих системах здесь приводится список членов, разделенных запятыми, который включает номера домашних и рабочих телефонов.

• pw_dir представляет домашний каталог, связанный с пользователем. Обычные сеансы регистрации начинают работать с этим каталогом в качестве текущего каталога.

• pw_shell представляет имя командной оболочки, которая запускается в случае успешной регистрации пользователя. Сюда обычно относятся /bin/bash, /bin/tcsh, bin/zsh и так далее. Однако элементы, используемые для других целей, могут иметь другие оболочки, /bin/false применяется для элементов passwd, которые не используются для регистрации пользователей. Специализированные оболочки часто служат для целей, рассмотрение которых выходит за рамки настоящей книги.

Структура, которая представляет элементы в /etc/group (или в эквивалентных базах данных), определена в .

struct group {

 char * gr_name;   /* Имя группы */

 char * gr_passwd; /* Пароль */

 __gid_t gr_gid;   /* Идентификатор группы */

 char ** gr_mem;   /* Список членов */

};

• gr_name представляет уникальное имя группы.

• gr_passwd представляет пароль (обычно неиспользуемый). К нему применимы те же требования, что и к pw_passwd, только в еще большей степени.

• gr_gid представляет номер (обычно неуникальный), который ядро использует для идентификации группы.

• gr_mem представляет список членов группы, разделенных запятыми. Это список имен пользователей, которые присваиваются этой группе на вторичной основе (см. главу 10).

Существуют две общих причины, по которым производится доступ к системным идентификационным базам данных: если ядро получает номер, а вам необходимо имя, или если какой-то пользователь или какая-то программа предоставляют вам имя, а вы должны сообщить ядру номер. Предусмотрены две функции поиска числовых идентификаторов, getpwuid() и getgrgid(), которые принимают целочисленный идентификатор и возвращают указатель на структуру, содержащую информацию из соответствующей системной базы данных. Точно так же имеются две функции, которые производят поиск имен, getpwnam() и getgrnam(), и они возвращают те же две структуры.

База данных пользователей База данных групп
Номер getpwuid() getgrgid()
Имя getpwnam() getgrnam()

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

Четыре вышеупомянутых функции являются, по сути, сокращениями, предлагающими наиболее часто используемые функции для доступа к системным базам данных. Функции низкого уровня, getpwent() и getgrent(), производят итерации по строкам в базе данных вместо поиска конкретной записи. Каждый раз при вызове одной из этих функций она будет считывать другой элемент из соответствующей системной базы данных, и возвращать его. После того как вы завершите чтение элементов, вызовите функцию endpwent() или endgrent(), чтобы закрыть файл.

В качестве примера далее приводится функция getpwuid(), записанная в отношении функции getpwent().

struct passwd * getpwuid(uid_t uid) {

 struct passwd * pw;

 while (pw = getpwent()) {

  if (!pw)

   /* обнаружена ошибка; * сквозной проход для обработки ошибки */

   break;

  if (pw->pw_uid == uid) {

   endpwent();

   return(pw);

  }

 }

 endpwent();

 return NULL;

}

 

28.1.1. Пример: команда

id

Команда id использует многие из этих функций и предлагает несколько хороших примеров работы с ними. Она также использует некоторые функциональные возможности ядра, описанные в главе 10.

  1: /* id.с */

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10:

 11: void usage (int die, char *error) {

 12:  fprintf(stderr, "Использование: id [<имя_пользователя>]\n") ;

 13:  if (error) fprintf(stderr, "%s\n", error);

 14:  if (die) exit(die);

 15: }

 16:

 17: void die(char *error) {

 18:  if (error) fprintf(stderr, "%s\n", error);

 19:  exit(3);

 20: }

 21:

 22: int main(int argc, const char *argv[]) {

 23:  struct passwd *pw;

 24:  struct group *gp;

 25:  int current_user = 0;

 26:  uid_t id;

 27:  int i;

 28:

 29:  if (argc > 2)

 30:   usage(1, NULL);

 31:

 32:  if (argc == 1) {

 33:   id = getuid();

 34:   current_user = 1;

 35:   if (!(pw = getpwuid(id)))

 36:    usage(1, "Имя пользователя не существует");

 37:  } else {

 38:   if (!(pw = getpwnam(argv[1])))

 39:    usage(1, "Имя пользователя не существует");

 40:   id = pw->pw_uid;

 41:  }

 42:

 43:  printf("uid=%d(%s)", id, pw->pw_name);

 44:  if ((gp = getgrgid(pw->pw_gid)))

 45:   printf(" gid=%d(%s)", pw->pw_gid, gp->gr_name);

 46:

 47:  if (current_user) {

 48:   gid_t *gid_list;

 49:   int gid_size;

 50:

 51:   if (getuid() != geteuid()) {

 52:    id = geteuid();

 53:    if (!(pw = getpwuid(id)))

 54:     usage(1, "Имя пользователя не существует");

 55:    printf(" euid=%d(%s)", id, pw->pw_name);

 56:   }

 57:

 58:   if (getgid() != getegid()) {

 59:    id = getegid();

 60:    if (!(gp = getgrgid(id)))

 61:     usage(1, "Группа не существует");

 62:    printf(" egid=%d(%s)", id, gp->gr_name);

 63:   }

 64:

 65:   /* использование интерфейса getgroups для получения текущих групп */

 66:   gid_size = getgroups(0, NULL);

 67:   if (gid_size) {

 68:    gid_list = malloc(gid_size * sizeof(gid_t));

 69:    getgroups(gid_size, gid_list);

 70:

 71:    for (i = 0; i < gid_size; i++) {

 72:     if (!(gp = getgrgid(gid_list[i])))

 73:      die("Группа не существует");

 74:     printf("%s%d(%s)", (i == 0) ? " groups=" : ",",

 75:      gp->gr_gid, gp->gr_name);

 76:    }

 77:

 78:    free(gid_list);

 79:   }

 80:  } else {

 81:   /* получение списка групп из базы данных групп */

 82:   i = 0;

 83:   while ((gp = getgrent())) {

 84:    char *c = * (gp->gr_mem);

 85:

 86:    while (c && *c) {

 87:     if (!strncmp(c, pw->pw_name, 16)) {

 88:      printf("%s%d(%s)",

 89:       (i++ == 0) ? " groups=" : ",",

 90:       gp->gr_gid, gp->gr_name);

 91:      с = NULL;

 92:     } else {

 93:      c++;

 94:     }

 95:    }

 96:   }

 97:   endgrent();

 98:  }

 99:

100:  printf("\n");

101:  exit(0);

102: }

Код обработки аргументов, который начинается в строке 29, обращается к нескольким важным функциям. При вызове без аргументов командной строки id производит поиск информации, основанной на том, какую программу запустил пользователь, и сообщает об этом. Описание функции getuid() можно найти в главе 10; она возвращает идентификатор пользователя процесса, который вызвал его. Затем функция getpwuid() производит поиск элемента в файле паролей для данного идентификатора пользователя. Если программе id в качестве аргумента командной строки будет задано имя пользователя, то вместо этого она будет искать элемент, основанный на заданном имени, независимо от идентификатора пользователя, запустившего его.

Вначале программа id выводит имя и числовой идентификатор пользователя. Файл паролей содержит имя главной группы пользователя. Если эта группа существует в файле групп, id выводит его номер и имя.

В главе 10 описаны все различные формы идентификаторов, используемых в ядре. Программа id должна применять функции geteuid() и getegid() для проверки uid и gid и выводить их, если они отличаются от эффективных uid и gid. И снова, структуры паролей и групп просматриваются по числовому идентификатору.

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

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

Как говорилось в главе 10, функция getgroups() может использоваться необычным (но удобным) образом: ее можно вызвать один раз с нулевым размером и проигнорировать указатель (который, как в данном случае, может быть равен NULL), и она вернет то количество элементов данных, которые ей нужно вернуть. Таким образом, после этого программа id выделит список точного размера и вызовет функцию getgroups() еще раз, но теперь уже с точным размером, и список сможет хранить всю необходимую информацию.

Далее программа id производит итерации по всему списку, получая все необходимые ей элементы из базы данных групп. Обратите внимание, что этот процесс отличается от использования базы данных групп для получения списка групп, к которым принадлежит пользователь. В данном случае id использует базу данных групп только для установления соответствия между членами группы и именами группы. Более эффективный интерфейс мог бы использовать функцию getgrent() для производства итераций по базе данных групп и поиска элементов в списке, а не наоборот. По окончания работы не забывайте вызывать функцию endgrent(). Если этого не сделать, то индекс файла останется открытым, что впоследствии может привести к сбою в коде, если этот код предполагает (что он и должен делать), что функция getgrent() начнет работу с первого элемента.

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

Если пользователь ввел имя пользователя в качестве аргумента командной строки, то программа id выполнит итерацию по файлу групп, производя поиск групп, в которых будет определено введенное имя пользователя. Не забывайте, что после всех действий необходимо вызывать функцию очистки endgrent()!

 

28.2. Подключаемые модули аутентификации (РАМ)

 

Интерфейс библиотеки С удобен для поиска информации о пользователе, однако он не позволяет администратору системы в достаточной мере управлять процессом выполнения аутентификации.

РАМ (Pluggable Authentication Modules — подключаемые модули аутентификации) является спецификацией и библиотекой, предназначенной для конфигурирования процесса аутентификации в системе. Библиотека предлагает стандартный и относительно простой интерфейс для аутентификации пользователей и изменения информации об аутентификации (например, пароля пользователя). Реализация РАМ в Linux (http://www.kernel.org/pub/linux/libs/pam) содержит полную документацию по программированию интерфейса РАМ, включая документацию по написанию новых модулей РАМ (The Module Writer's Manual), а также по написанию приложений, которые могут использовать РАМ (The Application Developer's Manual). Здесь мы только покажем пример простого использования РАМ в приложении, с помощью которого необходимо выполнять проверку паролей.

РАМ является стандартным интерфейсом, определяемым DCE, X/Open и The Open Group. Он включается как часть в некоторые версий Unix и входит в состав практически всех версий Linux. Этот интерфейс является переносимым, поэтому мы рекомендуем осуществлять аутентификацию пользователей именно с помощью РАМ. Если вам понадобится перенести код, написанный в соответствии со стандартом РАМ, в операционную систему, не поддерживающую РАМ, то сделать это можно будет очень просто. Однако поскольку РАМ является несколько жестким стандартом, то он может оказаться более сложным для переноса приложений, не поддерживающих РАМ, в систему, в которой РАМ используется.

Помимо служб, связанных с аутентификацией (определяющих, действительно ли пользователь является тем, за кого себя выдает, изменяющих информацию об аутентификации, например, паролей), РАМ также может справиться с управлением учетных записей (определяя, может ли пользователь зарегистрироваться в данный момент и на этом терминале) и управлением сертификатами (обычно, признаки аутентификации, которые используются для X и Kerberos — но определенно не uid- и gid-идентификаторы).

Реализация РАМ в Linux предлагает как стандартную библиотеку libpam, так и набор нестандартных вспомогательных функций в библиотеке libpam_misc. Главным заголовочным файлом для написания приложений, которые смогут работать с РАМ, является .

Мы займемся рассмотрением программ яра, используемых для создания приложения, зависящего от РАМ в смысле его аутентификации пользователей, а затем предложим простое приложение pamexample, которое будет использовать библиотеки libpam и libpam_misc.

 

28.2.1. Диалоги РАМ

РАМ проводит различие между политикой и механизмом; модули РАМ, которые реализуют политику, не взаимодействуют с пользователем напрямую, а приложение не определяет политику. Политику определяет администратор системы в файле выработки политики, и реализуется она с помощью модулей, вызываемых файлом определения политики. На основании структуры диалога, struct pam_conv, модули определяют способ, посредством которого приложениям будет посылаться запрос на получение информации от пользователя.

#include

struct pam_conv {

 int (*conv) (int num_msg, const struct pam_message ** msg,

 struct pam_response ** resp, void * appdata_ptr);

 void * appdata_ptr;

};

Член conv() является указателем на функцию диалога, которая принимает сообщения для передачи пользователю в структуру struct pam_message и возвращает введенную пользователем информацию в структуру struct pam_response. Библиотека libpam_misc предлагает функцию диалога misc_conv, которая отвечает за работу с текстовыми консольными приложениями. Чтобы использовать ее (мы рекомендуем вам делать это по мере возможности), вам нужно будет включить заголовочный файл и присвоить члену conv значение misc_conv. Этот простой механизм использован в программе pamexample, представленной в конце этой главы.

Как вариант, вам будет необходимо реализовать свою собственную функцию диалога. Для этого вы должны разобраться еще с двумя структурами данных.

struct pam_message {

 int msg_style;

 const char * msg;

};

struct pam_response {

 char * resp;

 int resp_retcode; /*пока что не используется, ожидается нулевое значение*/

};

Функции диалога передается массив указателей на структуры pam_message и массив указателей на структуры pam_response, каждый из которых имеет длину num_msg. При получении отклика его необходимо передавать каждой структуре pam_message в структуре pam_response с одним и тем же индексом массива, msg_style может принимать одно из перечисленных ниже значений.

PAM_PROMPT_ECHO_OFF Выводит текст, определенный в msg как информационный (например, в стандартном дескрипторе выходного файла), просит пользователя об отклике, не отображая введенные символы (например, пароль), и возвращает текст в новую сформированную строку символов, хранящуюся в соответствующей структуре resp pam_response .
PAM_PROMPT_ECHO_ON Выводит текст, определенный в msg как информационный (например, в стандартном дескрипторе выходного файла), просит пользователя об отклике, отображая введенные символы (например, имя пользователя), и возвращает текст в новую сформированную строку символов, хранящуюся в соответствующей структуре resp pam_response .
PAM_ERROR_MSG Выводит текст, определенный в msg как текст ошибки (например, стандартный дескриптор файла ошибок), присваивает соответствующей структуре resp pam_response значение NULL .
PAM_TEXT_INFO Выводит текст, определенный в msg как информационный (например, стандартный дескриптор выходного файла), присваивает структуре resp pam_response значение NULL .

Остальные значения могут быть определены как расширения стандарта; ваша функция диалога должна игнорировать их, если они не записываются для последующей их обработки, и должна просто передавать им NULL-отклик. РАМ (вернее, модуль РАМ, производящий запрос) отвечает за освобождение каждой строки resp, не содержащей NULL, а также массивов структур pam_message и pam_response.

Член appdate_ptr, который задается в структуре диалога, передается функции диалога. Это будет полезно в тех случаях, когда вы используете одну функцию диалога в нескольких контекстах, или если вы хотите передать контекстную информацию функции диалога. Эта информация может включать спецификацию дисплея X, внутреннюю структуру данных, в которой хранятся описатели файлов для соединения, или любые другие данные, которые могут быть полезны для вашего приложения. В любом случае, эта информация не интерпретируется библиотекой РАМ.

Функция диалога должна вернуть PAM_CONVERR, если во время выполнения возникнет ошибка, в противном случае — PAM_SUCCESS.

 

28.2.2. Действия

РАМ

РАМ не хранит никакой статической информации в библиотеке между вызовами, а хранит всю постоянную информацию с использованием непрозрачной структуры данных, передаваемой всем вызовам. Этот непрозрачный объект, pam_handle, управляется РАМ, однако хранится в вызывающем приложении. Он инициализируется с помощью функции pam_start() и освобождается функцией pam_end().

#include

int pam_start (const char * service_name, const char * user,

 const struct pam_conv * pam_conversation,

 pam_handle_t ** pamh);

int pam_end(pam_handle_t * pamh, int pam_status);

Аргумент service_name должен представлять уникальное имя для вашего приложения. Это уникальное имя позволяет администратору системы конфигурировать защиту применительно к вашему приложению; два приложения, использующие одно и то же имя service name, разделяют одну и ту же конфигурацию. Аргумент user представляет имя аутентифицированного пользователя. Аргумент pam_conversation представляет структуру диалога, о которой мы уже говорили. Аргумент pamh представляет непрозрачный объект, который следит за внутренним состоянием. Функция pam_start() показана в строке 97 кода.

Функция pam_end(), показанная в строке 137 кода pamexample.с, очищает каждое; состояние, хранящееся в непрозрачном объекте pamh, и информирует модули, на которые он ссылается, о конечном состоянии действий. Если приложение успешно использовало РАМ, оно должно присвоить pam_status значение PAM_SUCCESS; в противном случае оно должно предоставить самую последнюю ошибку, возвращенную РАМ.

Бывают ситуации, когда модули РАМ могут использовать дополнительную информацию при принятии решения об аутентификации пользователя; эту информацию предоставляет система, а не пользователь. Кроме того, в некоторых случаях модули РАМ должны посылать приложению предупреждение об изменениях. Механизмом передачи этой дополнительной информации является элемент РАМ. Значение элемента задается с помощью функции pam_set_item(), а его запрос осуществляется функцией pam_get_item().

#include

extern int pam_set_item(pam_handle_t * pamh, int item_type,

 const void * item);

extern int pam_get_item(const pam_handle_t * pamh, int item_type,

 const void ** item);

Аргумент item_type определяет идентичность и семантику элемента РАМ item. Мы рассмотрим только наиболее часто используемые значения item_type.

PAM_TTY item представляет указатель на строку, содержащую имя устройства TTY, с которым связан запрос аутентификации. Это может быть tty59 для первого последовательного порта в стандартной системе или pts/0 для первого псевдотерминала, или tty1 для первой виртуальной консоли.
PAM_USER Функция pam_start() автоматически присваивает это значение аргументу user , переданному функции pam_start() . Важно отметить, что это имя может изменяться! Если вашему приложению нужно имя пользователя, то оно должно проверять значение посредством функции pam_get_item() после попадания в стек РАМ и перед производством изменения имени в другом коде.
PAM_RUSER Для сетевых протоколов (например, rsh и ssh) этот элемент должен применяться для передачи имени пользователя удаленной системы любым модулям РАМ, которые используют его. Благодаря этому администратор системы сможет определить, разрешена ли аутентификация типа rhost .
PAM_RHOST Подобно PAM_RUSER , PAM_RHOST необходимо задавать для сетевых протоколов, в которых имя удаленного хоста может использоваться как компонент аутентификации, или при управлении учетной записью.

Все остальные функции принимают по два аргумента: непрозрачный объект pamh и целое число, которое может быть либо нулевым, либо флагом РАМ_SILENT. Флаг PAM_SILENT требует, чтобы каркас и модули РАМ не генерировали информационных сообщений, однако при этом будет производиться запрос на ввод пароля. В обычных приложениях флаг РАМ_SILENT не задается.

Функция pam_authenticate(), показанная в строке 100 кода pamexample.с, выполняет все, что было сконфигурировано администратором системы в данном приложении (что определяется аргументом service_name функции pam_start()) для аутентификации пользователя. Сюда может быть включено следующее: запрос на ввод одного или нескольких паролей; проверка, что пользователь с текущим именем пользователя (что определяется по элементу РАМ РАМ_USER, а не по текущему uid; модули РАМ не рассматривают uid, поскольку приложения, вызывающие РАМ, обычно явным образом не выполняются после аутентификации пользователя) является текущим пользователем консоли; проверка, что текущий пользователь (снова по имени пользователя) недавно прошел аутентификацию для эквивалентного уровня обслуживания; проверка элементов РАМ PAM_RUSER и PAM_RHOST в отношении локальных таблиц эквивалентных удаленных пользователей и хостов (например, то, что выполняет демон rsh), или что-либо подобное. (Обратите внимание, что в большинстве систем задействована система "теневых паролей", при котором с целью защиты пароля только процессы с полномочиями root могут проверять пароли произвольных пользователей; процесс, который не выполняется как root, может проверять только собственный пароль uid. Это единственное исключение из правила, когда модули РАМ принимают во внимание uid.)

Функция pam_acct_mgmt(), показанная в строке 107, предполагает, что функция pam_authenticate() уже была вызвана и продолжает свою работу, а затем проверяет (также по имени), разрешено ли пользователю осуществлять запрошенный доступ. Она может рассматривать элементы РАМ PAM_USER, PAM_TTY, PAM_RUSER и PAM_RHOST, определяя разрешение на доступ. Например, какому-то одному пользователю могут быть разрешены некоторые tty только в течение определенных часов, или каждому пользователю в некотором классе может быть разрешено только некоторое количество параллельных регистраций, или некоторым совокупностям удаленных пользователей и хостов могут быть даны разрешения только на несколько часов.

Функция pam_setcred(), показанная в строке 118, предполагает, что аутентификация была пройдена, и затем устанавливает сертификаты для пользователя. Хотя uid, gid и дополнительные группы технически являются сертификатами, они не находятся под управлением функции pam_setcred(), поскольку во многих приложениях это не будет соответствовать модели безопасности. Наоборот, она устанавливает дополнительные сертификаты. Вероятным кандидатом для использования в качестве сертификата является мандат Kerberos — файл, содержащий зашифрованные данные, которые предоставляют пользователю разрешение на доступ к некоторым ресурсам.

Функция pam_open_session(), показанная в строке 113, открывает новый сеанс. Если в процессе есть ветвления, то функцию pam_open_session() необходимо вызывать после ветвления, поскольку при этом могут происходить такие действия, как установка rlimits (см. стр. 120–121). Если ваш процесс запускается как root, а затем изменяется на uid аутентифицированного пользователя, функцию pam_open_session() нужно вызывать перед сбросом привилегий root, поскольку модули сеанса могут попытаться выполнить системные операции (например, монтирование домашних каталогов), которые зарезервированы только для root.

Функция pam_close_session(), показанная в строке 128, закрывает существующий сеанс. Ее можно вызывать из другого процесса, а не только из того, который вызвал функцию pam_open_session(), при том условии, что РАМ будут доступны одни и те же данные — те же аргументы, заданные для РАМ, и те же элементы РАМ, которые используются для открытия сеанса. Обратите внимание, что поскольку элемент PAM_USER может быть изменен во время аутентификации и управления учетной записью, вы должны убедиться, что производится учет любых изменений, производимых с PAM_USER, если вы вызываете ее из отдельного процесса, установившего сеанс. Для работы функции pam_close_session() могут потребоваться привилегии root.

  1: /* pamexample.с */

  2:

  3: /* Программа pamexample демонстрирует вариант простой обработки РАМ.

  4:  * Вам нужно будет либо использовать параметр командной строки —service

  5:  * для выбора имени уже установленной службы (может работать "system-auth",

  6:  * проверьте наличие /etc/pam.d/system-auth в своей системе), либо

  7:  * установки системного файла */etc/pam.d/pamexample со следующими

  8:  * четырьмя строками (игнорируя ведущие символы "*") :

  9:  * #%РАМ-1.0

 10:  * auth required /lib/security/pam_unix.so

 11:  * account required /lib/security/pam_unix.so

 12:  * session required /lib/security/pam_limits.so

 13:  *

 14:  * Обратите внимание, что если вы запустите эту программу не как root, то

 15:  * можете столкнуться с ограничениями системы; при управлении учетными

 16:  * записями может произойти сбой, вам может быть не разрешено проверять

 17:  * другие пароли пользователей, в управлении сеансом может произойти

 18:  * сбой - все будет зависеть от того, как сконфигурирована служба.

 19:  */

 20:

 21: #include

 22: #include

 23: #include

 24: #include

 25: #include

 26: #include

 27: #include

 28: #include

 29:

 30: /* Эта структура может быть автоматической, но она не должна выходить

 31:  * за пределы между функциями pam_start() и pam_end(), поэтому в простых

 32:  * программах легче всего сделать ее статической.

 33:  */

 34:  static struct pam_conv my_conv = {

 35:   misc_conv, /* использование функции диалога TTY из libpam_misc */

 36:   NULL /* у нас нет специальных данных для передачи в misc_conf */

 37:  };

 38:

 39:  void check_success(pam_handle_t * pamh, int return_code) {

 40:   if (return_code != PAM_SUCCESS) {

 41:    fprintf (stderr, '"%s\n", pam_strerror(pamh, return_code));

 42:    exit(1);

 43:   }

 44:  }

 45:

 46:  int main(int argc, const char ** argv) {

 47:   pam_handle_t * pamh;

 48:   struct passwd * pw;

 49:   char * username=NULL, * service=NULL;

 50:   int account = 1, session = 0;

 51:   int c;

 52:   poptContext optCon;

 53:   struct poptOption optionsTable[] = {

 54:    { "username", 'u', POPT_ARG_STRING, &username, 0,

 55:      "Имя пользователя для аутентификации", "<имя_пользователя>" },

 56:    { "service", 'S', РОPT_ARG_STRING, &service, 0,

 57:      "Имя службы для инициализации как (pamsample)",

 58:      "<служба>" },

 59:    { "account", 'a', POPT_ARG_NONE|POPT_ARGFLAG_XOR,

 60:      &account, 0,

 61:      "включение/выключение управления учетными записями (включено)", "" },

 62:    { "session", 's', POPT_ARG_NONE|POPT_ARGFLAG_XOR,

 63:      &session, 0,

 64:      "включение/выключение запуска сеанса (выключено)", "" },

 65:    POPT_AUTOHELP

 66:    POPT_TABLEEND

 67:   };

 68:

 69:   optCon = poptGetContext("pamexample", argc, argv,

 70:    optionsTable, 0);

 71:   if ((c = poptGetNextOpt(optCon)) < -1) {

 72:    fprintf(stderr, "%s: %s\n",

 73:    poptBadOption(optCon, POPT_BADOPTION_NOALIAS),

 74:    poptStrerror(c));

 75:    return 1;

 76:   }

 77:   poptFreeContext(optCon);

 78:

 79:   if (!service) {

 80:    /* Обратите внимание, что обычное приложение не должно предоставлять

 81:     * этот параметр пользователю; он присутствует здесь, чтобы можно было

 82:     * проверить это приложение, не производя изменений в системе,

 83:     * требующих доступа root.

 84:     */

 85:   service = "pamexample";

 86:  }

 87:

 88:  if (!username) {

 89:   /* по умолчанию для текущего пользователя */

 90:   if (!(pw = getpwuid (getuid())) ) {

 91:    fprintf(stderr, "Имя пользователя не существует");

 92:    exit(1);

 93:   }

 94:   username = strdup(pw->pw_name);

 95:  }

 96:

 97:  с = pam_start(service, username, &my_conv, &pamh);

 98:  check_success(pamh, c);

 99:

100:  с = pam_authenticate(pamh, 0);

101:  check_success(pamh, c);

102:

103:  if (account) {

104:   /* если аутентификация не была закончена, управление

105:    * учетной записью не определено

106:    */

107:   с = pam_acct_mgmt(pamh, 0);

108:   check_success(pamh, с);

109:  }

110:

111:  if (session) {

112:   /* В случае необходимости мы могли бы организовывать здесь ветвление */

113:   с = pam_open_session(pamh, 0);

114:   check_success(pamh, с);

115:

116:   /* Обратите внимание, что здесь не устанавливается uid, gid

117:      или дополнительные группы */

118:   с = pam_setcred(pamh, 0);

119:

120:   /* В случае необходимости мы могли бы закрыть здесь полномочия */

121:

122:   /* Вызов оболочки, которая была "аутентифицирована" */

123:   printf("Запуск оболочки...\n");

124:   system("exec bash -");

125:

126:   /* Здесь мы могли бы использовать wait4(), если бы организовывали

127:      ветвление вместо вызова system() */

128:   с = pam_close_session(pamh, 0);

129:   check_success(pamh, с);

130:  }

131:

132:  /* Реальные приложения могли бы сообщать о сбое вместо

133:   * выхода, что мы и делали в check_success на каждой стадии,

134:   * поэтому в таких случаях с может иметь значения, отличные

135:   * от PAM_SUCCESS.

136:   */

137:  с = pam_end(pamh, с);

138:  check_success(pamh, с);

139:

140:  return 0;

141: }