Como ler do sistema de arquivos de um jeito organizado em C
Pré-requisitos
- Ambiente Linux com GCC
- Conhecimento de ponteiros e strings em C
Aprendizados:
getcwd
strcat
strstr
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:
- Se por acaso eu precisar mudar o repositório de lugar no meu computador, esse caminho absoluto já não valeria mais
- Se outra pessoa baixar esse repositório no computador dela, esse caminho absoluto
faria menos sentido ainda. Ela teria que ter um username igual a
yudi
e baixar o repositório em~/uni/
- Essa solução polui o projeto com informações do sistema de arquivos que estão fora do repositório e não importam pra ele.
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.