Pourquoi le langage C? §

Je n'ai jamais eu de formation en programmation/développement/code. Enfin presque, j'ai eu droit à un module d'algorithmique en première année de fac de sciences. Mais j'ai toujours aimé programmer. Pour moi, c'est comme une partie d'Age of empire/Rimworld/échecs/ Smallworld/go/... : il faut anticiper, s'adapter et chercher le meilleur moyen d'arriver à ses fins. Quand je dis "meilleur moyen", ça signifie à la fois rapide et élégant.

J'ai appris énormément grâce à l'esprit du logiciel libre, en lisant ce que d'autres ont bien voulu partager sur le web. Aussi j'ai pu découvrir des scripts shell (si on peut parler de programmation), puis le python, le PHP, le javascript et le go. En passant, j'ai appris le C via le site du zéro (pour ceux qui s'en souviennent). Cependant je n'ai pas approfondi : grave erreur.

Pendant la période de confinement de 2020, j'en ai profité pour m'y remettre et réécrire certains projets en C. J'ai donc de nouveau lu du code, notamment celui de suckless.org mais aussi celui d'OpenBSD (en partie). Cette expérience a été une aventure exaltante qui me fait dire aujourd'hui : j'adore le C.

suckless.org

OpenBSD

Certes, je ne suis pas un expert, loin de là. Cependant, je suis convaincu qu'il s'agit d'un des meilleurs langages existants. Voilà pourquoi...

Pas de compromis §

La syntaxe en C demande de la rigueur. Certains la désapprouve. Je trouve au contraire qu'elle permet d'écrire du code plus lisible et moins souvent source d'erreur.

Une erreur, et le compilateur ne voudra pas vous créer un binaire. Pas de pitié. Oui, mais en même temps, ça évite de mauvaises surprises. Au passage, clang sous OpenBSD donne de bons conseils :)

C'est léger §

La bibliothèque standard est à la fois complète et restreinte. Autrement dit, il y a tout ce qu'il faut dedans, mais il faut parfois assembler quelques pièces entre elles pour parvenir à ses fins.

Ça force à mieux programmer §

Les exigences du langage poussent à se poser davantage de questions, à bien réfléchir à son projet et comment y parvenir. Ça force à mieux programmer.

C'est sur-documenté §

C'est un langage qui date. Il y a énormément de ressources et de conseils sur le web pour apprendre. (merci StackOverflow).

C'est rapide §

Comparé aux autres langage que j'ai pu appréhender, la différence est flagrante.

C'est beau §

Argument complètement subjectif : je trouve le code C beau.

Tout ce que j'aurais aimé qu'on me dise la première fois que j'ai appris le C §

Je code comme certains colorient un carreau sur 2, gribouillent des petits dessins rigolos ou font des arabesques pour s'occuper pendant les réunions. Je ne pense pas être bon à ça, mais j'aime beaucoup m'occuper l'esprit en écrivant du C. C'est un peu comme jouer à résoudre une énigme en s'amusant avec les règles du jeu.

J'ai appris le C il y a longtemps, en lisant le tutoriel du "site du zero" (j'ai dit, c'était il y a longtemps). Je m'y suis remis récemment, et avec du recul, sans être un expert, je m'aperçois que j'aurais bien aimé savoir certaines choses à l'époque. Voici donc tout ce que j'aurais aimé lire la première fois que j'ai appris le C.

Macros utiles §

Quand on déclare une chaîne de caractères, on ne sait jamais trop quelle doit être sa longueur. Il existe en fait des limites déjà bien définies pour nous :

PATH_MAX : La taille maximale d'un chemin vers un fichier (#include limits.h)

BUFSIZ : Taille d'un buffer, souvent utiliser pour traiter du texte. (#include stdio.h)

Lecture §

Aller regarder dans les fichiers /usr/include peut parfois aider à mieux comprendre. Mais par dessus tout, il faut lire les pages man. J'ignore si c'est spécifique à OpenBSD, mais les sections (3) regorgent d'explication, d'exemples, et de suggestions de fonctions associées dont on ignore peut-être l'existence. Par exemple, man strncat propose de lire des infos sur strlcpy pour la compléter/remplacer. Cette dernière nous montre carrément des exemples.

http://man.openbsd.org/strncat

http://man.openbsd.org/strlcpy

ternaire §

Bon, c'est pas original, mais je n'avais jamais vraiment compris avant:

(condition) ? expression_si_vrai : expression_si_faux ;

Les chaînes de caractères §

En C, il y a le type "char". Les chaînes de caractères ne sont pas un type. En C, il n'y a que des tableaux de "char", terminés par un NUL : '\0'. C'est idiot, mais en fait très important à garder en tête pour travailler sur du texte. La confusion est facile car on mélange les expressions avec des " ou des '. En bref :

'a' : c'est le char a

"a" : c'est la "chaîne" constituée de 'a' puis '\0'.

On ne peut pas "additionner" ou modifier un tableau de char aussi "facilement" que dans d'autres langages. Pourtant, dans string.h, on peut trouver tout ce qu'il faut pour gérer les chaînes de caractères.

Il vaut mieux se garder une bibliothèque personnelle de fonctions gérant les chaînes, et les réemployer au besoin.

Si une manipulation sur des chaînes de caractères nécessite d'allouer de la mémoire avec malloc, alors il existe certainement une solution plus simple. Ce n'est pas toujours vrai ceci dit ^^.

Utiliser la fonction strdup() permet de copier une chaîne de caractères. Il faut penser à appeler "free()" ensuite avec cette fonction.

La fonction strsep() modifie la chaîne qui lui est donnée. Il faut donc parfois utiliser strdup() avant, car il faut donner une chaîne modifiable et non constante. Autrement dit:

char *str = "coucou"; /* n'est pas modifiable */
char *str = strdup("coucou"); /* modifiable */

strchr() et strstr() sont très utiles pour récupérer l'emplacement d'un caractère ou d'une chaîne de caractères. On peut directement intervenir à partir de ce point ensuite.

Par exemple

char *pos = NULL;
char *str = "Respirer de la compote fait tousser";

pos = strstr(str, "compote");
puts(str); /* affiche "compote fait tousser /*

Toujours initialiser une chaîne avec des 0 : ainsi, on est sûr qu'elle est toujours terminée correctement (par un '\0').

char str[BUFSIZ] = {'\0'};
char *str2       = calloc(BUFSIZ, sizeof(char));

Pour "vider" (remettre à zéro) une chaine de caractère (reset) :

bzero(s, sizeof(s));

Pour modifier une chaîne de caratères à l'aide d'une fonction, cette dernière doit prendre en argument la taille de la chaîne. En effet, elle ne reçoit en réalité qu'un pointeur vers un tableau de char, et un sizeof() ne retournerait que la taille du pointeur. Exemple :

void
modif_str(char *str, ssize_t strsiz)
{
    strlcpy(str, "coucou", strsiz);
}

char    test[256] = {'\0'};

modif_str(test, sizeof(test));

Fichiers §

Il vaut mieux utiliser les fonctions fread()/fwrite() plutôt que read()/write() car elles sont plus efficaces (utilisation d'un cache).

size_t nread = 0;
FILE *fd = NULL;
char           *buffer[BUFSIZ];

if ((fd = fopen(fp, "r")) == NULL) { goto err; }

while ((nread = fread(buffer, 1, sizeof(buffer), fd)) != 0)
    fwrite(buffer, 1, nread, stdout);
fclose(fd);
if (ferror(fd)) {
    err(1,"closefile");
}

Pour lister le contenu répertoire: scandir()

#include <sys/types.h>
#include <dirent.h>
int n = 0;
struct dirent **namelist;

if ((n = scandir(path, &namelist, NULL, alphasort)) < 0) {
    err(1, "Can't scan %s", path);
} else {
    for(int j = 0; j < n; j++) {
        if (!strcmp(namelist[j]->d_name, ".")) {
            continue;
        }
        if (namelist[j]->d_type == DT_DIR) {
            printf("%s\n", namelist[j]->d_name);
        }
        free(namelist[j]);
}
free(namelist);
}

Gestion des erreurs §

#include <err.h>
#include <errno.h>

Pour afficher une erreur et quitter : err(1, "erreur :%s", blabla);

Pour afficher un avertissement et quitter : warn("attention :%s", blabla);

Compiler §

Dans la plupart des cours en C, on n'apprend pas à compiler. Pour un simple fichier "main.c", il suffit d'un:

make main

Qui est en fait un raccourci pour:

cc -o main main.c

Pour compiler en utilisant une bibliothèque, il faut utiliser les options "-l" "-L", et "-I". Le "-I" permet de dire où se trouvent les entêtes (.h), "-L" indique où se trouvent les bibliothèques et "-l" indique quelle bibilothèque utiliser.

C'est à partir de ce moment qu'un fichier Makefile devient intéressant. Voici le modèle que j'utilise. Il est assez strict au niveau de la gestion des erreurs, ça me force à écrire du code moins sensible:

NAME = thename
VERSION = 0.1

PREFIX?=/usr/local/
INCS     = -I/usr/local/include
LIBS     = -L/usr/local/lib -lCHANGEME -lOTHERLIB
LDFLAGS  = ${LIBS}
CPPFLAGS = -DVERSION=\"${VERSION}\"

CFLAGS  += -pedantic -Wall -Wextra -Wmissing-prototypes \
           -Werror -Wshadow -Wstrict-overflow -fno-strict-aliasing \
           -Wstrict-prototypes -Wwrite-strings \
           ${CPPFLAGS} \
            ${INCS} \
           -Os 

.SUFFIXES: .c .o

SRC != find . -type f -name \*.c
H != find . -type f -name \*.h
OBJ = ${SRC:.c=.o}

.c.o:
    ${CC} ${CFLAGS} -c $<

all: ${NAME}

${OBJ}: ${H}

clean:
    rm -f ${NAME} *.core *.o

${NAME}: ${OBJ}
    ${CC} -o $@ ${OBJ} ${CFLAGS} ${LDFLAGS}

Bien sûr, il faudra changer "NAME", les "CHANGEME" après les "-l" et pourquoi pas lire "man make" :)

Si on veut compiler un binaire pour un chroot, il faut ajouter l'option "-static".

Listes chaînées §

Les listes chaînées sont une méthode pour enregistrer des données. Un peu comme un dictionnaire en python. Et si c'est trop lent, vous pouvez regarder du côté des hashtables. Mais je trouve les premières largement suffisantes (on parle du C hein, c'est hyper rapide! :)).

Modestie §

Parfois, on lit du code très difficile à comprendre, avec des opérations sur pointeurs pour gérer des chaînes de caractère par exemple.

Il faut rester parfois modeste, et arrêter de vouloir un code surpuissant meilleur que les autres. Il n'y a pas de code parfait.

Par exemple, c'est parfois tout aussi efficace de lire un fichier caractère après caractères plutôt que de chercher à le découper en morceaux à la recherche d'un motif particulier. Le C est très rapide :)

Lire un fichier ligne après lignes §

getline(), et hop :) Le man présente même un exemple.

Ceci dit, c'est souvent aussi rapide d'utiliser fgets() ou fgetc().

Initialiser une structure §

memset(&structure, 0, sizeof(struc structure_type));

Utiliser fork() et éviter les zombies §

En appelant un signal qui matche tous les processus enfants. Attention, cela ne fait pas bon ménage avec kqueue().

#include <signal.h>

static void sigchld(int unused);

void
sigchld(int unused)
{
    (void)unused; // avoid unused variable warning
    while (waitpid(WAIT_ANY, NULL, WNOHANG) > 0);
}

int
main(int argc, char *argv[])
    /* ... */
    if (signal(SIGCHLD, sigchld) == SIG_ERR)
        err(1, "can't install SIGCHLD handler:");
    /* ... */

Ou avec un wait/waitpid bien placé :

const char *cmd[] = { "/usr/bin/top", "-s", "0.5", NULL };
pid_t pid = fork();
if (pid == -1) {
    err(1, "fork");
} else if (pid == 0) {
    /* child */
    execv(cmd[0], (char **)cmd);
    err(1, "execv");
} else {
    /* parent */
    wait(NULL);  // blocking
    /* or : 
    waitpid(pid, NULL, WNOHANG); // non-blocking
    */
}

Ressources §

Excellent site pour apprendre les bases du C

Learn C the Hard Way de Zed A. Shaw

Des remarques intéressantes pour écrire du C récent

Notions pour présenter du code propre

Idem, à la OpenBSD

Des exemples de code très instructifs

https://c-for-dummies.com/

https://beej.us/guide/bgnet/html/

Snippets C §

Cette partie sera alimentée de fonctions et extraits de code en C en guise d'aide mémoire. Ne prenez rien pour parfait, ces extraits peuvent certainement être améliorés.

Modifier une chaîne de caratères dans une fonction §

Il est nécessaire de connaître la taille de la chaîne pour que tout fonctionne correctement :

void
modif_str(char *str, ssize_t strsiz)
{
    strlcpy(str, "coucou", strsiz);
}

char    test[256] = {'\0'};

modif_str(test, sizeof(test));

ecalloc, erealloc : allocation avec gestion d'erreur §

/* same thing that calloc and reallocarray but check for
 * error and exit if necessary
 */

void *
ecalloc(size_t nmemb, size_t size)
{
    void *p;

    if ((p = calloc(nmemb, size)) == NULL)
        err(1, "calloc");
    return p;
}

void *
ereallocarray(void *ptr, size_t nmemb, size_t size)
{
    if ((ptr = reallocarray(ptr, nmemb, size)) == NULL)
        err(1, "reallocarray");

    return ptr;
}

get_stream_txt: Récupérer le contenu d'un fichier ouvert §

/* return text in fp
 * the fp must be closed properly after
 * the returned string is mallocated and must be freed.
 */
char *
get_stream_txt(FILE *fp)
{
    char *buf = NULL;
    size_t s;
    size_t len = 0;
    size_t bsize = 2 * BUFSIZ;

    buf = ecalloc(bsize, sizeof(char));
    while((s = fread(buf + len, 1, BUFSIZ, fp))) {
        len += s;
        if(BUFSIZ + len + 1 > bsize) {
            bsize += BUFSIZ;
            buf = ereallocarray(buf, bsize, sizeof(char));
        }
    }
    buf[len] = '\0';

    return(buf);
}

lire un fichier §

FILE *fd = NULL;
char buf[BUFSIZ] = {0};
size_t nread = 0;

if ((fd = fopen(path, "rb")) == NULL) {
        err(1, "%s: %s", "Fail to open", path);
}

while ((nread = fread(buf, 1, BUFSIZ, fd)) != 0 ) {
        fwrite(buf, 1, nread, stdout);
}
fclose(fd);
if (ferror(fd)) {
        err(1, "%s: %s", "close", path);
}

get_cmd_output: Récupérer la sortie d'une commande §

/* return the output of cmd in an allocated
 * string created by get_stream_txt().
 * It is the caller responsibility to free it later
 */
char *
get_cmd_output(const char *cmd)
{
    FILE *fp = NULL;
    char *res = NULL;

    if ((fp = popen(cmd, "r")) == NULL)
        err(1, "%s: %s", "Error opening pipe for cmd", cmd);

    res = get_stream_txt(fp);
    if (pclose(fp) != 0)
        err(1, "%s %s", cmd, "not found or exited with error status");

    return res;
}

la fonction popen peut poser des soucis dans le cas où ce n'est pas le développeur qui précise la commande à entrer : les variables d'environnement sont conservées. Voici donc une autre façon de faire :

Créer un "pipe" pour lequel "0" est la sortie et "1" l'entrée.

Indiquer que "stdout" correspond à la sortie du pipe avec la fonction dup2(). Ceci permet de relier la sortie de la commande qu'on veut lancer au pipe pour pouvoir le lire ensuite comme un fichier normal.

Lancer la commande avec execlp() aprèq avoir fork()

Lecture du retour de la commande dans le processus parent. On utilise fdopen() puisque tout ce qu'on connaît, c'est le "file descriptor", pas un chemin vers un fichier.

<!-- -->
/* use pipe and fork to avoid shell with popen */
int fildes[2] = {0};
int status = 0;
pid_t pid = 0;
char *str = "blabla";
char *cmd = "hello";

if (pipe(fildes) != 0) {
    err(1, "pipe failed");
}

pid = fork();

if (pid < 0) {
    close(fildes[0]);
    close(fildes[1]);
    err(1, "fork failed");
}

if (pid > 0) { /* parent */
    char buf[3];
    size_t nread = 0;
    FILE *output = NULL;

    close(fildes[1]); /* make sure entry is closed so fread() gets EOF */

    /* use fread/fwrite because are buffered */
    output = fdopen(fildes[0], "r");
    if (output == NULL) {
        err(1, "fdopen failed");
    }

    /* read pipe output */
    while ((nread = fread(buf, 1, sizeof(buf), output)) != 0) {
        fwrite(buf, 1, nread, stdout);
    }
    close(fildes[0]);
    fclose(output);

    /* wait for child to terminate */
    waitpid(pid, &status, 0);

    exit(0);

} else if (pid == 0) { /* child */
    dup2(fildes[1], STDOUT_FILENO); /* set pipe output equal to stdout */
    dup2(fildes[1], STDERR_FILENO);
    close(fildes[1]); /* no need this file descriptor : it is now stdout of the command below */
    execlp(cmd, cmd, NULL);
    /* if execlp is ok, this will never be reached */
    err(1, "execlp failed to run: %s", "ls");
}

startswith §

int
startswith(const char *str, const char *start)
{
        size_t str_len = strlen(str);
        size_t start_len = strlen(start);


        int ret = 0;
        if ((str_len >= start_len) &&
                (strncmp(str, start, start_len) == 0)) {
                ret = 1;
        }

        return ret;
}

endswith §

int
endswith(const char *str, const char *end)
{
    size_t str_len = strlen(str);
    size_t end_len = strlen(end);

    int ret = 0;
    if ((str_len >= end_len) &&
        (strcmp(str + (str_len -end_len), end) == 0)) {
        ret = 1;
    }

    return ret;
}

(v)estrlcat §

/* strlcat with error handling */
size_t
estrlcat(char *dst, const char *src, size_t dstsize)
{
    size_t size;
    if ((size = strlcat(dst, src, dstsize)) >= dstsize)
        err(1, "strlcat");

    return size;
}

/* variadic strlcat
 * a call must end with NULL
 * to append "bar", "zoo", yo" to foo, call:
 * vestrlcat(foo, sizeof(foo), "bar", "zoo", "yo", NULL);
 */
size_t
vestrlcat(char *dst, size_t dstsize, ...)
{
    size_t size = 0;
    char *s;
    va_list ap;

    va_start(ap, dstsize);

    while ((s = va_arg(ap, char *)) != NULL) {
        size += estrlcat(dst, s, dstsize);
    }
    va_end(ap);

    return size;
}

stralloc §

Et ma favorite qui permet de fabriquer n'importe quelle chaîne de caractères. On y passe un pointeur (qui peut être NULL au départ. Tous les arguments sont assemblés pour former une longue chaîne, cette dernière étant allouée dynamiquement au besoin.

/* 
 * Make any string 
 * add in str every argument and realloc str if necessary
 * if str is NULL, it is allocated
 * argument list **must** end with NULL
 * return str
 * str should be dynamically allocated and freed later
 * example: stralloc(&foo, "hello", " there.", NULL);
 */

char *
stralloc(char **str, ...)
{
    size_t len = 0;    /* length of str at last*/
    char *s = NULL;
    va_list ap;
    va_list apcopy;

    va_copy(apcopy, ap);

    /* count required length */
    va_start(ap, str);
    while ((s = va_arg(ap, char *)) != NULL) {
        len += strlen(s);
    }
    va_end(ap);
    len++; /* \0 */

    if (*str != NULL) {
        len += strlen(*str);
        *str = ereallocarray(*str, len, sizeof(char));
    } else {
        *str = ecalloc(len, sizeof(char));
    }

    /* copy */
    va_start(apcopy, str);

    while ((s = va_arg(apcopy, char *)) != NULL) {
        estrlcat(*str, s, len);
    }
    va_end(apcopy);
    return(*str);
}

Je lui préfère une version qui ne nécessite aucune allocation, et en profite pour vous présenter "estrlcpy" et "estrlcat" qui font comme strlcpy et strlcat mais en gérant les erreurs :

size_t
estrlcpy(char *dst, const char *src, size_t dstsize)
{
    size_t n = 0;

    n = strlcpy(dst, src, dstsize);
    if (n >= dstsize) {
        err(1, "strlcpy failed for %s = %s", dst, src);
    }

    return n;
}

size_t
estrlcat(char *dst, const char *src, size_t dstsize)
{
    size_t size;
    if ((size = strlcat(dst, src, dstsize)) >= dstsize)
        err(1, "strlcat on %s + %s", dst, src);

    return size;
}

/* usage:
 * mkstr(str, sizeof(str), optarg, "/INBOX/", NULL);
 */
size_t
mkstr(char *str, size_t dstsize, ...)
{
    char *s = NULL;
    size_t size = 0;
    va_list ap;

    va_start(ap, dstsize);

    while ((s = va_arg(ap, char *)) != NULL) {
        if (size == 0) {
            size += estrlcpy(str, s, dstsize);
        } else {
            size += estrlcat(str, s, dstsize);
        }
    }
    va_end(ap);
    return(size);
}

trim §

Comme son nom l'indique, elle vire les espaces et sauts de ligne en début et fin de chaîne. Il faut passer un pointeur vers la chaîne à modifier (et donc allouée dynamiquement).

/* return a trimmed copy of str */
/* trim(&str); */
void
trim(char **str)
{
    size_t begin = 0;
    size_t end = strlen(*str);


    while (isspace((*str)[begin])) {
        begin++;
    }

    while (isspace((*str)[end-1])) {
        end--;
    }
    for (size_t i = begin, j=0; i < end; i++, j++) {
        (*str)[j] = (*str)[i];
    }
    (*str)[end - begin] = '\0';
}

(v)esnprintf §

Cette fonction fait comme snprintf avec gestion d'erreur. Il faut avoir un tableau de char déjà prêt, par exemple char s[256]. Ce code est en partie tiré de celui utilisé souvent par suckless.org. Elle est très pratique car permet de définir le format des variables qu'une chaîne doit contenir. Par exemple :

esnprintf(s, sizeof(s), "%s%d%c", "blabla", 42, 'a');

static int
evsnprintf(char *str, size_t size, const char *format, va_list ap)
{
    int ret = vsnprintf(str, size, format, ap);

    if (ret < 0 || (size_t)ret >= size) {
        warn("%s", "vsnprintf: Output truncated");
    }

    return ret;
}

int
esnprintf(char *str, size_t size, const char *format, ...)
{
    va_list ap;
    int ret = 0;

    va_start(ap, format);
    ret = evsnprintf(str, size, format, ap);
    va_end(ap);

    return ret;
}

strrstr : rechercher le dernier morceau §

strrstr() fait comme strstr, mais pointe vers la dernière occurence, pas la première :

char *   strrstr(const char *, const char *);

char *
strrstr(const char *big, const char *little)
{
    char *p = NULL;
    char *ret = NULL;

    if ((p = strstr(big, little)) == NULL) {
        return NULL;
    }

    /* loop until last occurence */
    while ((p = strstr(p, little)) != NULL) {
        ret = p;
        p++;
    }

    return ret;
}

efopen, efclose, evfprintf : gestion des fichiers §

On ouvre et on ferme les fichiers en gérant les erreurs

FILE *
efopen(const char *path, const char *mode)
{
    FILE *f = NULL;
    if ((f = fopen(path, mode)) == NULL)
        err(1, "%s: %s", "Fail to open", path);

    return f;
}

int
efclose(FILE *stream)
{
    int ret = 0;
    ret = fclose(stream);
    if (ferror(stream)) {
        err(1,"closefile");
    }
    return ret;
}

On écrit dans un fichier autant de choses que l'on veut en une fois. C'est comme fprint mais avec une liste d'arguments variable.

int
evfprintf(FILE *stream, const char *format, va_list ap)
{
    int ret;

    ret = vfprintf(stream, format, ap);
    if (ret < 0)
        err(1,"evfprintf:");

    return ret;
}

int
efprintf(FILE *stream, const char *format, ...)
{
    va_list ap;
    int ret;

    va_start(ap, format);
    ret = evfprintf(stream, format, ap);
    va_end(ap);

    return ret;
}

OpenBSD unveil et pledge §

Deux macros pratiques et un exemple pour éviter des erreurs de compilation sur un autre système:

#define PLEDGEORDIE(prom) if (pledge(prom, NULL) == -1) { err(1, "pledge"); }
#define UNVEILORDIE(path,perms) if (unveil(path, perms) == -1) { err(1, "unveil"); }

Exemple:

#ifdef __OpenBSD__
    UNVEILORDIE(argv[1], "rwc");
    PLEDGEORDIE("stdio rpath cpath wpath exec proc");
#endif

Retirer le "\n" de fin de ligne §

line[strcspn(line, "\r\n")] = '\0'; /* remove newline */

de la bonne façon d'utiliser strsep §

strsep modifie la chaîne, et donc fait perdre la référence du pointeur correspondant. Il faut donc en garder une trace, ce qui donnerait (merci stacko) :

char *r = strdup(str);

char *tofree = r;
char *tok;
while ((tok = strsep(&r, " ")) != NULL) {
    puts(tok);
}

free(tofree);

récupérer les substrings d'une regex (regex.h) §

#include <regex.h>
#include <sys/types.h>

#define LEN(X)               (sizeof X / sizeof X[0])

#define SE_MAX 4 /* max subexpression, in nmatch. This is the number of () + 1 */

int 
main(int argc, char *argv[])
{
    regex_t greg;   /* compiled gemini regex */
    size_t nmatch = SE_MAX;
    regmatch_t match[SE_MAX];
    int ret = 0;

    char buf[BUFSIZ] = {'\0'};

    char *gemini_regex = "^gemini://([^/]*)/?([^\?]*)[\?]?(.*)?$";
    char *lines[] = {
        "gemini://si3t.ch",
        "gemini://si3t.ch/subdir/index.gmi?query?weird",
        "gemini://si3t.ch/subdir?query",
        "gemini://si3t.ch?query",
        "gemini://si3t.ch//?query",
    };

    if ((ret = regcomp(&greg, gemini_regex, REG_EXTENDED)) != 0) {
        regerror(ret, &greg, buf, sizeof(buf));
        regfree(&greg);
        err(1, "%s", buf);
    }

    for (size_t l = 0; l < LEN(lines); l++) {
        printf("[%s]\n", lines[l]);

        if ((ret = regexec(&greg, lines[l], nmatch, match, 0)) != 0) {
            regerror(ret, &greg, buf, sizeof(buf));
            if (ret == REG_NOMATCH)
                warnx("No match :(");
            else {
                regfree(&greg);
                errx(1, "%s", buf);
            }
        }

        for (int i = 1; i <= greg.re_nsub; i++) {
            if (match[i].rm_so != match[i].rm_eo)
                size_t len = match[i].rm_eo - match[i].rm_so;
                memcpy(buf, lines[l] + match[i].rm_so, len);
                puts(buf);
            }
        }
    }

    regfree(&greg);
    return 0;
}

Dictionnaire (liste chaînée) §

#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct Dict {
    char *key;
    char *val;
    struct Dict *next;
} Dict;


void * emalloc(size_t);
Dict * dict_new(void);
void dict_free(Dict *);
char * dict_get_key(Dict *, const char *);
void dict_print(Dict *);
void dict_append(Dict *, const char *, const char *);
void dict_push(Dict **, const char *, const char *);



void *
emalloc(size_t size)
{
    void *p;

    p = malloc(size);
    if (p == NULL)
        err(1, "malloc");

    return p;
}

void
dict_free(Dict *d)
{
    Dict *nextitm;


    while (d != NULL) {
        nextitm = d->next;
        free(d->key);
        free(d->val);
        free(d);
        d = nextitm;
    }
    free(d);
}

void
dict_print(Dict *d)
{
    Dict *cur = d;

    while (cur->next != NULL) {
        printf("%s: %s\n", cur->key, cur->val);
        cur = cur->next;
    }
}

char *
dict_get_key(Dict *d, const char *key)
{
    Dict *cur = d;

    while (cur->next != NULL) {
        if ((strcmp(cur->key, key)) == 0) {
            return cur->val;
        }
        cur = cur->next;
    }


    return NULL;
}

void
dict_append(Dict *d, const char *key, const char *val)
{

    Dict *cur = d;

    while (cur->next != NULL) {
        cur = cur->next;
    }

    cur->next = dict_new();
    cur->key = strdup(key);
    cur->val = strdup(val);
}

void
dict_push(Dict **d, const char *key, const char *val)
{

    Dict *new = dict_new();

    new->key = strdup(key);
    new->val = strdup(val);
    new->next = *d;

    *d = new;
}

Dict *
dict_new(void)
{
    Dict *d = emalloc(sizeof(Dict));
    d->key = NULL;
    d->val = NULL;
    d->next = NULL;
    return d;
}

int 
main(void)
{
    Dict *d = dict_new();
    dict_append(d, "batman", "un super héros");
    dict_append(d, "chat", "envahisseur d'internet");
    dict_push(&d, "abricot", "fruit placé au début");
    dict_print(d);
    dict_free(d);
    return 0;
}

Listes et queues (queue.h) §

Beaucoup plus simple que de réimplémenter des listes chaînées.

Voici un exemple. Le reste, c'est dans ''man queue''. Il n'y a pas de code de vérification d'erreur :!:.

J'utilise les conventions suivantes :

Les structures qui seront dans les listes ont un nom commençant pas une majuscule

Les entête de liste portent le nom de la structure suivi de "_head".

Le nom des entrées porte le nom de la structure en minuscule.

<!-- -->
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>

struct Item {
    int id;
    char path[PATH_MAX];
    char title[256];    /* 256 is more than enough */
    SIMPLEQ_ENTRY(Item) item;
};
SIMPLEQ_HEAD(Item_head, Item);

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

    struct Item_head itemh;
    struct Item *i1, *ip;

    SIMPLEQ_INIT(&itemh);

    i1 = malloc(sizeof(struct Item));

    strlcpy(i1->title, "coucou", sizeof(i1->title));

    SIMPLEQ_INSERT_TAIL(&itemh, i1, item);
    SIMPLEQ_FOREACH(ip, &itemh, item) {
        puts(ip->title);
    }

    free(i1);


    return 0;
}