Yudi Yamane

Como ler do sistema de arquivos de um jeito organizado em C

Pré-requisitos

Aprendizados:

Poblema

Para a disciplina de Estrutura de Dados 2 (EDA2), eu tenho um repositório com uma estrutura relativamente complexa para um “projeto” em C. Esse repositório vive no meu computador na pasta /home/yudi/<vários diretórios>/eda2/, mais ou menos com o seguinte conteúdo:

.
├── ... outros arquivos como .gitignore, README.md, etc
├── 00-revisao/
├── 01-sorting/
├── 02-hash/
├── 04-trees/
├── 05-heap/
├── 06-graph
│   ├── _exercises/
│   └── list-und-graph
│       ├── listundgraph.h
│       └── _tests
│           ├── listundgraph.test.cpp
│           └── ...
└── resources
    └── algs4-data
        ├── tinyG.txt
        ├── tinyCG.txt
        └── ... outros vários arquivos com amostras de dados

Em um dos exercícios do livro, eu tinha que criar um “construtor” que recebe o caminho de um arquivo para criar e preencher o grafo, com uma assinatura assim:

1ListUndGraph *ListUndGraph_create_from_file(const char *filepath)

Que é chamada nos arquivos em 06-graph/list-und-graph/_tests/ ou em 06-graph/_exercises/.

Primeiras soluções

A primeira solução que pensei foi a de passar o caminho relativo para o construtor, algo como "../../../../resources/algs4-data/tinyG.txt". A quantidade de ../ varia de acordo com o lugar de onde ListUndGraph_create_from_file é chamado. Essa é uma solução viável, mas um tanto incoveniente. O código cliente fica feio com muitos ../ e é chato para o programador ficar “calculando” essa quantidade.

Outra solução seria passar o caminho absoluto para o construtor. No meu computador, por exemplo, esse caminho poderia ser "/home/yudi/uni/eda2/resources/algs4-data/tinyG.txt". Essa solução, entretanto, traz mais restrições:

Uma solução melhor

Ao meu ver pelo menos.

Para o cliente, seria interessante se a chamada do construtor fosse algo assim:

1ListUndGraph *g = ListUndGraph_create_from_file("algs4-data/tinyG.txt");

Dessa maneira, o construtor recebe apenas o caminho relativo para o arquivo a partir de resources/ e ele se encarrega de resolver o caminho absoluto até a pasta resources/ ou de chamar alguém que sabe fazê-lo.

Vamos optar por criar a função void get_res_dir(char *res_dir) que preenche res_dir com o caminho absoluto até resources/ e é usada pelo construtor. Como bônus, ela ainda pode ser usada por outras funções de qualquer lugar do repositório e sua lógica pode ser reaproveitada em outros projetos.

Como vamos colocar get_res_dir em seu próprio arquivo de cabeçalho (.h), ele precisa ser incluído por quem quer usá-lo. Infelizmente, o problema da quantidade ../ volta aqui na hora de usar #include "../../../get_res_dir.h". Isso pode ser resolvido com um Makefile ou com um gerador de build system. Configurando essas ferramentas apropriadamente, a compilação por debaixo dos panos aconteceria mais ou menos assim:

1# essa quantidade de "../" é apenas para ilustração.
2gcc -I"../../../../get_res_dir.h" listundgraph.test.cpp -o listundgraph.test.out

Assim seria possível incluir a função apenas com

1#include "get_res_dir.h"

Eu teria que pesquisar mais pra configurar essas ferramentas apropriadamente, mas durante o curso de EDA2 eu não achei necessário fazer o setup de Make ou CMake para esses exercícios simples por preguiça ou falta de paciência pra aprender. Ainda assim, vale ressaltar que essa estratégia seria mais adequada em um projeto C/C++ de verdade .

Super pseudocódigo

1void get_res_dir(char* res_dir) {
2  // pegar caminho absoluto do diretório onde o programa está sendo executado
3  //    exemplo: "/home/yudi/uni/eda2/06-graph/list-und-graph/_tests/"
4  // pegar a substring do caminho absoluto até "eda2"
5  //   substr: "/home/yudi/uni/eda2/"
6  // concatenar a substring com "resources/"
7  //   resultado: "/home/yudi/uni/eda2/resources/"
8  // (esse resultado deve estar em res_dir no final da função)
9}

Implementação de get_res_dir

Para pegar o caminho absoluto do diretório eu pesquisei extamente isso no Google. Como estou programando em ambiente Linux, a função que foi recomendada foi char* getcwd(char* buf, size_t size) (get current working directory) do cabeçalho unistd.h. De acordo com as páginas do manual, ela retorna o diretório em buf, que deve apontar para um espaço previamente alocado.

Executando a essa função no meu computador no diretório /home/yudi/uni/eda2/06-graph/list-und-graph/_tests/, ela retorna exatamente isso. Por prevenção, vamos usar um buffer com o tamanho máximo.

1getcwd(res_dir, PATH_MAX);

Agora precisamos apenas de uma substring disso tudo: "/home/yudi/uni/eda2". Como diretório raíz do repositório, podemos usar a string "eda2" para extrair a substring de interesse. É uma suposição razoável porque é o nome do repositório e dificilmente vai mudar. Uma solução mais robusta, talvez, seria procurar o próximo diretório pai com a pasta .git, mas não vamos por esse caminho nesse post.

Para extrair a substring, vamos usar a função

1char* strstr(const char *haystack, const char *needle)

Que encontra a primeira ocorrência de needle em haystack e retorna o ponteiro que aponta para onde needle foi encontrado.

1const char *root_dir_name = "eda2";
2char *root_dir_ptr = strstr(res_dir, root_dir_name);

Agora root_dir_ptr aponta para a string "eda2/06-graph/list-und-graph/_tests". Vale ressaltar que não há cópias de strings, root_dir_ptr apenas aponta para uma região de memória alguns bytes à frente da região de memória apontada por res_dir, conforme a ilustração abaixo:

/home/yudi/uni/eda2/06-graph/list-und-graph/_tests/
↑              ↑
res_dir        root_dir_ptr

Como não precisamos de nada que vem depois de "eda2/", podemos desconsiderar essa parte colocando o terminador de string '\0'.

1*(root_dir_ptr + strlen(root_dir_name)) = '\0';

Agora res_dir aponta para

                   Note o '\0', antes era um '/'
                   ↓
/home/yudi/uni/eda2\006-graph/list-und-graph/_tests/
↑              ↑
res_dir        root_dir_ptr

Que é o mesmo que:


/home/yudi/uni/eda2
↑              ↑
res_dir        root_dir_ptr

Agora só precisamos concatenar "/resources" ao final de res_dir:

1const char *res = "/resources";
2strcat(res_dir, res);

Juntando tudo, a função get_res_dir fica assim:

 1#include <unistd.h>       // getcwd
 2#include <linux/limits.h> // PATH_MAX
 3#include <string.h>       // strstr, strcat, strlen
 4
 5// Preenche RES_DIR com "/.../eda2/resources".
 6// "..." é o caminho absoluto até "eda2/".
 7//
 8// RES_DIR deve ser um buffer com PATH_MAX bytes.
 9void get_res_dir(char *res_dir)
10{
11  getcwd(res_dir, PATH_MAX);
12  const char *root_dir_name = "eda2";
13  char *root_dir_ptr = strstr(res_dir, root_dir_name);
14  *(root_dir_ptr + strlen(root_dir_name)) = '\0';
15  const char *res = "/resources";
16  strcat(res_dir, res);
17}

Implementação de ListUndGraph_create_from_file

Agora só falta implementar o construtor de grafos. Vamos alocar um espaço para o caminho absoluto.

1char full_filepath[PATH_MAX]; // com lixo de memória por enquanto

Executando a função get_res_dir com full_filepath obtemos o seguinte:

1get_res_dir(full_filepath);
2// full_filepath: "/home/yudi/uni/eda2/resources"

Para terminar, precisamos acrescentar uma "/" e o caminho relativo do arquivo que foi passado a ListUndGraph_create_from_file. Vamos supor que esse caminho relativo é "algs4-data/tinyG.txt".

1// full_filepath: "/home/yudi/uni/eda2/resources"
2strcat(full_filepath, "/");
3// full_filepath: "/home/yudi/uni/eda2/resources/"
4strcat(full_filepath, filepath);
5// full_filepath: "/home/yudi/uni/eda2/resources/algs4-data/tinyG.txt"

O código do construtor fica assim:

1ListUndGraph *ListUndGraph_create_from_file(const char *filepath)
2{
3  char full_filepath[PATH_MAX];
4  get_res_dir(full_filepath);
5  strcat(full_filepath, "/");
6  strcat(full_filepath, filepath);
7
8  // restante do código para preencher o grafo
9}

Possíveis melhorias

Ainda seria possível deixar ListUndGraph_create_from_file mais interessante, criando uma função que faz o equivalente às últimas três linhas do trecho anterior.

Outra melhoria seria deixar esse código um pouco mais cross-platform, ou seja, que funcione melhor independentemente do sistema operacional. O cabeçalho unistd.h não existe em Windows, o que geraria um erro de compilação. Uma maneira de resolver isso seria com macros condicionais e usando a função GetCurrentDirectory do cabeçalho winbase.h.

Essas possíveis melhorias ficam como exercícios para leitor.

#c #sistema-de-arquivos

Responda a este post por email ↪