26 июня 2010

Файлы конфигурации

Привет. Многие игры позволяют настроить себя, так, как удобно игроку. В основном настройки затрагивают разрешение экрана, включение или выключение эффектов, настройки управления и звука.

Как вы наверное поняли, мы поговорим о мартышках. Шутка. Не смешно? Да, зато неожиданно. Мы поговорим о конфигурационных файлах, и а том, как можно их реализовать.

Начнем с простого. Напишем программу на языке Python которая будет читать конфигурационный файл.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

if __name__ == '__main__':
    print("Find config file...")

    width = 320
    height = 240

    try:
        f = open("config")
        print("\topen config file.")
    except:
        print("\tfile not fond.")
        print("Create default config file...")
        f = open("config","w")
        f.write("width={0}\n".format(width))
        f.write("height={0}\n".format(height))
        f.flush()
        f.close()
        print("\tfinish.")
    else:
        for line in f:
            sp = line.strip("\n").split('=')
            if sp[0] == "width":
                if sp[1].isdigit():
                    width = int(sp[1])
            elif sp[0] == "height":
                if sp[1].isdigit():
                    height = int(sp[1])
        f.close()

    print("")
    print("width\tis {0}".format(width))
    print("height\tis {0}".format(height))

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

Стоит заметить что создавать конфигурационный файл где попало нельзя. Для этого есть директория $HOME, и еще есть директория $XDG_CONFIG_HOME, это что касается GNU/Linux, в Windows и Mac OS есть похожие переменные окружения, но о них мы говорить не будем.

Что же использовать, $XDG_CONFIG_HOME или $HOME? Если вы попробуете выполнить следующую команду в консоле:

echo "XDG_CONFIG_HOME = $XDG_CONFIG_HOME\nHOME = $HOME"

То увидите, что $XDG_CONFIG_HOME это ~/.config, а $HOME это ~/. В случае если переменная $XDG_CONFIG_HOME не определенна, то считается что она равна ~/.config. Стоит заметить, что $XDG_CONFIG_HOME является стандартом, и все конфигурационные файлы должны хорониться там. А теперь на берите в консоле ls -a ~/. Как видите, не все соблюдают это стандарт. Я не знаю почему они так делают, но они так делают.

Соблюдение стандартов это хорошо. Мы будем использовать переменную $XDG_CONFIG_HOME, по мимо этой переменной есть $XDG_DATA_HOME, $XDG_DATA_DIRS, $XDG_CONFIG_DIRS, $XDG_CACHE_HOME. Мы рассмотрим только $XDG_CONFIG_HOME, о других переменных окружения можно посмотреть в стандарте.

Давайте теперь узнаем, как же нам работать с переменными окружения. Это просто. Я приведу два простых примера на Python и C.

#if 0
gcc -o bin $0
exit
#endif
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  printf("%s = %s\n%s = %s\n",
         "HOME", getenv("HOME"),
         "XDG_CONFIG_HOME", getenv("XDG_CONFIG_HOME"));
  return 0;
}

Пристальное внимание на функцию getenv. Думаю если вы скомпилируете данную программу вы увидите тоже, что видели, запустив команду на shell выше.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os

if __name__ == '__main__':
    print("HOME = "+os.environ["HOME"])
    print("XDG_CONFIG_HOME = "+os.environ["XDG_CONFIG_HOME"])

А тут внимание на словарь os.environ. Догадываетесь что делает этот код? Если нет, то вы наверняка уже его запустили и все посмотрели.

Предположим что вы решили хранить файл конфигурации в одном из директорий, $HOME или $XDG_CONFIG_HOME, не так уж и важно. Хранить наш файл как .my_config_file не стоит. Лучше все же послушать стандарт, и создать свой каталог, по имени программы. Для $XDG_CONFIG_HOME это будет выглядеть так: "$XDG_CONFIG_HOME/my_game/config". Для $HOME будет немного иначе: "$HOME/.my_game/config". Так-то.

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

#if 0
gcc -o bin $0 `pkg-config --cflags --libs lua`
exit
#endif
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <stdbool.h>

int get_int(lua_State *lua, const char *name, int by_default);
bool is_there_file(const char *name);

int main()
{
  const char *str;
  int width  = 320;
  int height = 240;
  int bpp    = 32;
  /* Строим путь к файлу конфигурации используя переменную $HOME. */
  char config_name[255] = { 0 };
  strcat(config_name, getenv("HOME"));
  strcat(config_name, "/config");

  lua_State *config = luaL_newstate();
  if (!config)
    {
      fprintf(stderr,
              "%s (%s : %d)\n",
              strerror(errno), __FILE__, __LINE__);
      exit(EXIT_FAILURE);
    }

  /* Проверяем есть ли файл. */
  if (is_there_file(config_name))
    {
      if (luaL_dofile(config, config_name))
        {
          str = lua_tostring(config, 1);
          fprintf(stderr,
                  "Error: %s (%s : %d)\n",
                  str, __FILE__, __LINE__);
          exit(EXIT_FAILURE);
        }
    }

  width  = get_int(config, "width",  width);
  height = get_int(config, "height", height);
  bpp    = get_int(config, "bpp",    bpp);
  lua_close(config);

  printf("%d\n", width);
  printf("%d\n", height);
  printf("%d\n", bpp);

  /* Записываем файл конфигурации. */
  FILE *file = fopen(config_name, "w");
  if (file == NULL)
    exit(errno);
  fprintf(file, "%s = %d\n", "width",  width);
  fprintf(file, "%s = %d\n", "height", height);
  fprintf(file, "%s = %d\n", "bpp",    bpp);
  fclose(file);

  return 0;
}

/*
  Пытается найти переменную из конфигурационного файла, содержащую
  целочисленное значение. Если нет то вернет значение by_default.
 */
int get_int(lua_State *lua, const char *name, int by_default)
{
  int ret = by_default;

  lua_getglobal(lua, name);
  if (lua_isnumber(lua, -1))
    ret = lua_tonumber(lua, -1);
  lua_pop(lua, 1);

  return ret;
}

/*
  Простая проверка на существование файла.
 */
bool is_there_file(const char *name)
{
  FILE *file = fopen(name, "r");
  if (file == NULL)
    return false;
  fclose(file);
  return true;
}

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

На сегодня все. Код может содержать ошибки, как и текст.