Partage
  • Partager sur Facebook
  • Partager sur Twitter

C/Linux, chemins de fichiers invalides...

...selon la façon dont je lance le programme

Sujet résolu
29 août 2011 à 13:57:35

Bonjour à tous.

Tout d'abord, inscris depuis peu, je tiens à remercier tous les contributeurs de ce site pour leur travail fabuleux.

J'espère pouvoir bientôt contribuer à la noble initiative qu'est la formation des Zeros (quand j'aurais atteinds les 0.5).

Quant à mon problème, il est le suivant.

J'ai suivi le tutoriel de M@teo (Sokoban en C), et jusque là tout va bien.

Plusieurs fichiers externes sont nécessaires au fonctionnement du programme (sons, images...).
Ayant achevé le programme, j'ai ajouté le répertoire du programme à la variable PATH :
export PATH=$PATH:/home/thanatos/sokoban
afin de pouvoir le lancer dans la console dans avoir à entrer le chemin complet de l’exécutable. Mais lorsque je le lance, les chemins des fichiers sont erronés.
J'ai défini les chemins ainsi :
#define CHEMIN_MUSIQUE  "./sons/musique.mp3"

et il semblerait que le répertoire considéré comme la racine ne soit pas celui de l’exécutable mais celui depuis lequel j'appelle le lien symbolique.

J'ai donc essayé les solutions suivantes :

- Récupérer le chemin de l’exécutable dans le premier argument du main (argv[0]) afin de construire le chemin absolu des fichier, mais le problème est identique.
-J'ai tenté d'utiliser la fonction getcwd (qui nécessite unistd.h) mais le problème est toujours le même. J'obtiens ceci à l'exécution :
thanatos@thanatos-VirtualBox:~$ kurkkoban
The current working directory is /home/thanatos
Initisalisation du son : 0
Cannot load sound file sons/caisseok.wav :
 Error code :  23
thanatos@thanatos-VirtualBox:~$


Ma question est la suivante :
Suis-je obligé de trouver un moyen de reconstruire le chemin absolu des fichiers ressources ou y a-t-il un moyen de "définir" les chemins par rapport au chemin de l'exécutable.

J'espère que les explications sont claires et merci par avance à tout ceux qui pourraient m'apporter leur aide.

Bonne journée à tous.

  • Partager sur Facebook
  • Partager sur Twitter
29 août 2011 à 14:05:08

argv[0] ne contient que le nom de ton programme, tu n'as aucune certitude d'avoir le chemin complet. :)
Normalement, le chemin pris en compte lors de l'exécution de ton programme est le répertoire courant (celui où est lancé l'exécutable). Tu ne devrais rien avoir à faire, à moins de vouloir changer radicalement le dossier. :)
  • Partager sur Facebook
  • Partager sur Twitter
29 août 2011 à 14:29:19

Bonjour,

c'est ce que je me suis dis aussi mais les faits sont là :( .

Mon executable des dans /home/monprofil/sokoban.
Si je le lance depuis ici, tout va bien.

J'ai ensuite ajoutée "/home/monprofil/sokoban" à PATH.
Par exemple, dans le console, le répertoire courant est "/home", je tape le nom de mon executable et là, les chemins des fichiers sont invalides.
L'application se lance mais au chargement de la première ressources externe-> erreur.

Comme je le disais, j'ai par exemple déclaré le chemin de fichier ... "./sons/musique.mp3" et le chemin absolu du fichier est :
"/home/monprofil/sokoban/sons/musique.mp3".
Dans l'exemple ci-dessus qui provoque l'erreur, il calcule "/home/sons/musique.mp3".

Moyenapacomprendre :'(
  • Partager sur Facebook
  • Partager sur Twitter
29 août 2011 à 14:31:43

Bonjour,

Citation : Pouet_forever

Normalement, le chemin pris en compte lors de l'exécution de ton programme est le répertoire courant (celui où est lancé l'exécutable). Tu ne devrais rien avoir à faire, à moins de vouloir changer radicalement le dossier. :)


Le problème, c'est que les chemins pour ouvrir ses ressources sont faux si on appelle le programme depuis un autre dossier que le dossier d'installation.

@ thanke : Je pense que tu peux retrouver le chemin du dossier où se trouve ton exécutable, et donc tes autres fichiers. Pour cela, (je ne l'ai jamais fait mais j'ai réfléchi à la question) :
  • tu regardes argv[0] ; si c'est un chemin absolu (test hyper-facile à effectuer, sous Linux, le 1er caractère est '/'), alors ce chemin absolu est le dossier de ton exécutable ;
  • sinon, tu regardes si c'est un chemin relatif (sous Linux, commence par "./"), si c'est le cas, tu concatènes le chemin d'exécution et argv[0] (sans oublier un éventuel '/' entre les 2) ;
  • sinon, c'est sans doute que ton exécutable a été appelé depuis le PATH. Il faut donc que tu fasses comme l'interpréteur de commande : tu parcours la variable PATH en ouvrant chacun des chemins qu'elle contient, jusqu'à trouver un fichier qui porte le nom de ton exécutable (argv[0]) dans un des dossiers. Ce dossier est donc le dossier où se trouve ton exécutable.
  • J'ai oublié les alias : apparemment, l'interpréteur de commande remplace bien l'alias par son contenu (je viens de tester), donc pas de problèmes.


ÉDIT: (j'avais oublié la fin :p )
Une fois que tu a retrouvé le chemin absolu de ton exécutable, tu n'as plus qu'à remplacer le nom de ton fichier exécutable par le chemin relatif de tes autres fichiers. ;)
Comme tu vois, c'est beaucoup de manipulations de chaînes de caractères (les chemins). Il y a aussi des histoires de parcours de dossier (si tu dois lire le PATH).
  • Partager sur Facebook
  • Partager sur Twitter
29 août 2011 à 15:01:30

Mais ce qui m'étonne c'est que quand le tape une partie du nom du programme, l'auto complétion se fait mais quand je tape "wereis monprogramme", il ne trouve rien.
J'ai bien regardé $PATH et le chemin du dossier ou se trouve l'exécutable est bien là.
En fait, si quelqu'un peut me renseigner, quelle méthode "standard" doit-on appliquer pour installer un programme perso.
Par exemple dans windows, je déploie mes fichiers dans "program file"s et mon script d'installation me fait un lien sur le bureau et dans le menu démarrer.
C'est plutôt intuitif.
La j'essaie d'appliquer la même logique mais ça ne semble pas être la bonne.
  • Partager sur Facebook
  • Partager sur Twitter
29 août 2011 à 16:17:58

Citation : thanke

Mais ce qui m'étonne c'est que quand le tape une partie du nom du programme, l'auto complétion se fait mais quand je tape "wereis monprogramme", il ne trouve rien.
J'ai bien regardé $PATH et le chemin du dossier ou se trouve l'exécutable est bien là.
En fait, si quelqu'un peut me renseigner, quelle méthode "standard" doit-on appliquer pour installer un programme perso.
Par exemple dans windows, je déploie mes fichiers dans "program file"s et mon script d'installation me fait un lien sur le bureau et dans le menu démarrer.
C'est plutôt intuitif.
La j'essaie d'appliquer la même logique mais ça ne semble pas être la bonne.


La logique est grosso modo la même sous Windows et sous Linux.

Tu as deux chemins importants : le dossier d'installation, qui est celui où se trouve ton fichier exécutable et tes autres fichiers et dossiers, et le dossier d'exécution, qui est celui dans lequel s'exécute ton programme. Les chemins relatifs employés dans ton programme sont donc relatifs au dossier d'exécution, et non à celui d'installation. Or, ces deux dossiers peuvent être différents, d'où les problèmes que tu peux avoir.

Pour avoir un programme qui marche quel que soit l'endroit où il se trouve et le dossier d'exécution, il faut que tu retrouves son dossier d'installation afin de pouvoir convertir les chemins relatifs utilisés à l'intérieur du programme en chemins absolus toujours valides (ou alors, pour éviter des manipulations, tu peux changer le dossier d'exécution avec chdir, et choisir le dossier d'installation une fois que tu l'as déterminé). Malheureusement, il n'existe à ma connaissance pas de fonctions standards permettant de le faire directement. :(

Considérons les cas possibles.
  • Sous Windows comme sous Linux (je pense), lorsque tu lances un programme depuis une interface graphique (un raccourci), argv[0] contient le chemin absolu de ton fichier exécutable (incluant le nom du fichier lui-même), et le dossier d'exécution est le dossier où il se trouve (ça j'en suis moins sûr, mais quand même). Tu n'as donc aucun problème dans ce cas-là.
     
  • Lorsque tu lances le programmes depuis une console, c'est une autre histoire. En effet, le dossier d'exécution est celui dans lequel on se trouvait lorsqu'on a appelé le programme.

    Considérons une commande, par exemple : CHEMINPROG arg1 arg2 …. Dans ce cas, argv[0] (argv étant le 2è argument de main, de type char**) contient "CHEMINPROG", quoi que tu aies tapé (dans le cas d'un alias, celui-ci est remplacé par sa valeur par la console, tu n'as donc pas à t'en soucier). Or, tu peux avoir tapé :
    • le chemin absolu ;
    • le chemin relatif ;
    • le nom du fichier tout court ; dans ce cas l'interpréteur de commande va consulter la variable PATH (oui, c'est pareil sous Windows, elle existe aussi) jusqu'à trouver un fichier qui porte le bon nom dans les dossiers qu'elle contient.
      NB: Sous Windows, tu n'as pas moyen de différencier un chemin relatif sans dossiers intermédiaires d'un nom de fichier simple. C'est pourquoi l'interpréteur regarde d'abord dans le dossier courant si un fichier avec un tel nom existe, avant de chercher dans les chemins du PATH.


Donc pour retrouver le chemin d'installation, on doit suivre le même raisonnement que l'interpréteur de commande. Commençons par analyser argv[0] :
  • Si c'est un chemin absolu (commence par "X:\\" sous Windows, "/" sous Linux), c'est bon, on l'a directement :) ;
     
  • Si c'est un chemin relatif (pas de début particulier sous Windows, commence par "./" ou "../" sous Linux), tu dois le concaténer au chemin d'exécution obtenu avec getcwd ;
     
  • Si c'est le nom du fichier tout court, tu regardes la variable PATH comme dit précédemment. Pour chaque chemin de dossier (noté CHEMIN) contenu dans PATH dans l'ordre, tu testes l'existence du fichier CHEMIN/FICHIER (ou FICHIER est le nom du fichier exécutable, contenu dans argv[0]) ; dès que tu trouves un fichier existant, tu arrêtes les recherches. Le chemin correspondant peut être relatif (mais sans commencer par "./" ou "../" cette fois), il faudra alors le traiter comme ci-dessus.
    Ce dernier cas n'est pas fiable à 100%, car on peut imaginer qu'un nouveau fichier portant le même nom que notre exécutable ait été créé dans un dossier contenu dans le PATH depuis le lancement du programme, mais étant donné que notre fonction ne serait destinée qu'à être appelée une seule fois au tout début du programme, ça ne devrait pas poser trop de problèmes (il faudrait vraiment ne pas avoir de chance). Remarque, ça ne constituerait pas une faille de sécurité exploitable ?
    Il y a une complication sous Windows, c'est que l'invite de commande accepte les extensions implicites. Par exemple, pour lancer test.exe, taper test suffit. Lors de la recherche du fichier, il faudra donc essayer sans rajouter d'extension, puis avec les différentes extensions implicites (contenues dans une autre variable d'environnement dont je ne me souviens plus du nom) ; ou, plus simple, étant donné que les programmes sous Windows portent l'extension .exe, il suffit de regarder si argv[0] se termine par ".exe", et le rajouter si ce n'est pas le cas, avant de faire les tests normalement.
Après ça, tu n'as plus qu'à enlever le nom du fichier exécutable qui se trouve à la fin du chemin absolu obtenu, pour retrouver le chemin du dossier d'installation.

En espérant t'avoir éclairé. ;)
  • Partager sur Facebook
  • Partager sur Twitter
29 août 2011 à 16:47:42

Yepee!
Merci beaucoup.
J'ai ajouté une ptite fonction qui check le chemin dans PATH (avec un expression régulière) puis qui fait un "chdir" au debut du programme.
Ca roule maintenant.

  • Partager sur Facebook
  • Partager sur Twitter
29 août 2011 à 18:18:30

Je serais curieux de voir ça, si ça ne te dérange pas de montrer ton code. :)

(Je me suis promis, un jour, de le faire, et puis je n'y ai plus pensé).
  • Partager sur Facebook
  • Partager sur Twitter
29 août 2011 à 23:47:42

Re.

Désolé, je sors de la sieste. Apprendre le C écourte sérieusement mes nuits biologiques.

Voici la méthode mais je te préviens, c'est du lard pur porc.

1- Je récupère la valeur de $Path var avec un
system( "echo `printenv PATH` >> /home/thanatos/test.txt");

2- Je récupère la chaîne dans le fichier

3-je récupère la sous chaine qui m'intéresse avec ça (mes suis servis de ce tuto : http://nicolasj.developpez.com/articles/regex/#LIV-B)
J'ai isolé la fonction pour l'exemple et changé le nom de quelques variables pour que ça me parle plus (récupérer un groupe correspondant à un patern rgx est bien plus évident en C# :p ):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>

char* getAppPath (char* fullString)
{
   int res;
   regex_t objRegex;
   const char *str_request = fullString;
   const char *s_Patern = "(:+[/+[a-z]*]*+sokoban)";
   res = regcomp (&objRegex, s_Patern, REG_EXTENDED);
   if (res == 0)
   {
      int match;
      size_t nb_patern_found = 0;
      regmatch_t *pmatch = NULL;

      nb_patern_found = objRegex.re_nsub;
      pmatch = malloc (sizeof (*pmatch) * nb_patern_found);
      if (pmatch)
      {
         match = regexec (&objRegex, str_request, nb_patern_found, pmatch, 0);

         regfree (&objRegex);

         if (match == 0)
         {
            char *group = NULL;
            int start = pmatch[0].rm_so;
            int end = pmatch[0].rm_eo;
            size_t size = end - start;

            group = malloc (sizeof (*group) * (size + 1));
            if (group)
            {
               strncpy (group, &str_request[start], size);
               group[size] = '\0';
             return group;

            }
         }

         else if (match == REG_NOMATCH)
         {
            printf ("%s Pas de chaione correspondante trouvée\n", str_request);
         }

         else
         {
            char *text;
            size_t size;


            size = regerror (res, &objRegex, NULL, 0);
            text = malloc (sizeof (*text) * size);
            if (text)
            {

               regerror (res, &objRegex, text, size);
               fprintf (stderr, "%s\n", text);
               free (text);
            }

         }
      }

   }


   return (NULL);
}


int main ()


{
  printf("%s", getAppPath("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/thanatos/sokoban"));
}



4- Ensuite, toujours en mode pur lard, je récupère le résultat et soustrait le ":" que j'ai inévitablement récupéré puis pour finir, un ptit "chdir"...
puis v'la.

Si quelqu'un à une idée plus sexy je suis preneur, j'ai trop tendance à bricoler mais j'essaie de m'améliorer à ce niveau.
  • Partager sur Facebook
  • Partager sur Twitter
30 août 2011 à 1:49:46

Pour récupérer la variable d'environnement, comme tu fais un truc non portable, autant utiliser unistd.h. Tu inclues unistd.h et tu utilises 'getenv("PATH");' et le tour est joué.

Si je suis ce qu'a dit maelan, ça pourrait donner un truc comme ça (sans regex) :

#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>

char * pathCpy(char * src, char * dst) {
  
  if (src == NULL || dst == NULL)
    return NULL;
  if (*src == '\0')
    return NULL;
  
  while (*src && *src != ':')
    *dst++ = *src++;
  *dst = '\0';
  src++;
  
  return src;
}

int fileExist(char * file) {
  int fd;
  
  if (file == NULL)
    return 0;
  
  fd = open(file, O_RDONLY);
  if (fd < 0)
    return 0;
  close(fd);
  
  return 1;
}

char * getPath(char * argv) {
  
  if (argv == NULL)
    return argv;
  /* Chemin absolu */
  if (argv[0] == '/')
    return strdup(argv);
  /* Chemin relatif */
  else if (argv[0] == '.' && argv[1] == '/') {
    char * s;
    char * t = getcwd(NULL, 0);
    
    if (t == NULL) {
      perror("getcwd : ");
      exit(EXIT_FAILURE);
    }
    
    s = malloc(strlen(t) + strlen(argv) - 1);
    if (s == NULL) {
      perror("malloc : ");
      exit(EXIT_FAILURE);
    }
    
    sprintf(s, "%s/%s", t, argv + 2);
    free(t);
    return s;
  }
  /* Nom de fichier */
  else {
    char * s;
    char * t = getenv("PATH");
    
    s = malloc(strlen(t));
    if (s == NULL) {
      perror("malloc : ");
      exit(EXIT_FAILURE);
    }
    
    while ((t = pathCpy(t, s))) {
      char * u = malloc(strlen(s) + strlen(argv) + 1);
      
      if (u == NULL) {
        perror("malloc : ");
        exit(EXIT_FAILURE);
      }
      sprintf(u, "%s/%s", s, argv);
      
      if (fileExist(u)) {
        free(u);
        return s;
      }
      free(u);
    }
  }
  
  return NULL;
}

J'ai pas fait de tests poussés, mais ça a l'air de fonctionner. :)
  • Partager sur Facebook
  • Partager sur Twitter
30 août 2011 à 2:30:41

En effet ç'est plus pro.
Cependant ça me retourne le chemin avec l'exécutable.
Niveau portabilité, ceci devrait aller non?

/*Je récupère le chemin avec 'getenv' et je lui passe un coup de regex : je récupère donc ":/home/thanatos/sokoban"*/
char* path = getAppPath(getenv("PATH"));
/*Je vire les ':'*/
path++;
/*Je modifie le répertoire d'exécution*/
   chdir(path);
  • Partager sur Facebook
  • Partager sur Twitter
30 août 2011 à 18:31:01

Ah, oui effectivement. :)
Ca devrait donner ce que tu souhaites. :)

#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>

char * pathCpy(char * src, char * dst) {
  
  if (src == NULL || dst == NULL)
    return NULL;
  if (*src == '\0')
    return NULL;
  
  while (*src && *src != ':')
    *dst++ = *src++;
  *dst = '\0';
  src++;
  
  return src;
}

int fileExist(char * file) {
  int fd;
  
  if (file == NULL)
    return 0;
  
  fd = open(file, O_RDONLY);
  if (fd < 0)
    return 0;
  close(fd);
  
  return 1;
}

char * delName(char * s) {
  int i;
  
  if (s == NULL)
    return s;
  i = strlen(s) - 1;
  if (i <= 1)
    return s;
  while (i > 0 && s[i] != '/')
    i--;
  if (i == 0 && s[i] == '/')
    s[i+1] = '\0';
  else
    s[i] = '\0';
  return s;
}

char * getPath(char * argv) {
  char * s;
  char * t;
  
  if (argv == NULL)
    return argv;
  /* Chemin absolu */
  if (argv[0] == '/') {
    s = strdup(argv);
    return delName(s);
  }
  /* Chemin relatif */
  else if (argv[0] == '.' && argv[1] == '/') {
    t = getcwd(NULL, 0);
    
    if (t == NULL) {
      perror("getcwd : ");
      exit(EXIT_FAILURE);
    }
    
    s = malloc(strlen(t) + strlen(argv) - 1);
    if (s == NULL) {
      perror("malloc : ");
      exit(EXIT_FAILURE);
    }
    
    sprintf(s, "%s/%s", t, argv + 2);
    free(t);
    return delName(s);
  }
  /* Nom de fichier */
  else {
    t = getenv("PATH");
    
    s = malloc(strlen(t));
    if (s == NULL) {
      perror("malloc : ");
      exit(EXIT_FAILURE);
    }
    
    while ((t = pathCpy(t, s))) {
      char * u = malloc(strlen(s) + strlen(argv) + 1);
      
      if (u == NULL) {
        perror("malloc : ");
        exit(EXIT_FAILURE);
      }
      sprintf(u, "%s/%s", s, argv);
      
      if (fileExist(u)) {
        free(u);
        return s;
      }
      free(u);
    }
  }
  
  return NULL;
}

getenv et chdir ne sont pas portables sous windows. ;)
  • Partager sur Facebook
  • Partager sur Twitter
30 août 2011 à 18:36:25

Citation : Pouet_forever


getenv et chdir ne sont pas portables sous windows. ;)



Le fonction getenv est portable, elle est déclarée dans l'en-tête stdlib.h ;)
Par contre, chdir ne fait en effet pas partie du standard.
  • Partager sur Facebook
  • Partager sur Twitter
30 août 2011 à 18:46:21

C'est bien ce qui me semblait, mais je ne l'ai pas retrouvé dans la norme. :-°
-->[]
  • Partager sur Facebook
  • Partager sur Twitter
30 août 2011 à 19:12:24

@ Pouet_forever :
char * t = getcwd(NULL, 0);
C'est bizarre, selon le man :

Citation : man getcwd

Comme extension du standard POSIX.1, getcwd() alloue le buffer dynamiquement, en utilisant malloc(), si buf est NULL lors de l'appel. Alors, le buffer alloué a la longueur size à moins que size soit inférieure à zéro, dans ce cas buf a la taille nécessaire.

Vu que size n'est pas inférieur à zéro (size vaut zéro), il devrait y avoir un malloc(0)… En tous cas, c'est peut-être sécurisé contre ça, mais il n'est pas dit dans le man que ça renvoie bien un buffer alloué à la bonne taille.
D'ailleurs, c'est bizarre parce que size est de type size_t, donc non signé…
Chez moi aussi, avec size à 0, ça fonctionne mais pas avec size à -1…


Bon allez, je me suis lancé aussi (il aura fallu ce sujet pour m'y convaincre). :) Mon code est pratiquement fini, je le posterais tout à l'heure.

PS: getcwd non plus n'est pas présente sous Windows (bien que j'ai du mal avec les différents standards et conformités :-° ).
  • Partager sur Facebook
  • Partager sur Twitter
31 août 2011 à 1:31:26

Citation : Maëlan

C'est bizarre, selon le man


Ce man contient probablement une erreur.
Le mien dit:

Citation : man getcwd

As an extension to the POSIX.1-2001 standard, Linux (libc4, libc5, glibc) getcwd() allocates the buffer dynamically using malloc(3) if buf is NULL. In this case, the allocated buffer has the length size unless size is zero, when buf is allocated as big as necessary. The caller should free(3) the returned buffer.


  • Partager sur Facebook
  • Partager sur Twitter
31 août 2011 à 9:15:40

Citation : Maëlan

PS: getcwd non plus n'est pas présente sous Windows (bien que j'ai du mal avec les différents standards et conformités :-° ).


Elle est présente dans la bibliothèque C de Visual C++, mais sous le nom _getcwd, et définie dans direct.h (doc).

Il y a aussi GetCurrentDirectory (doc), directement dans l’API Windows.
  • Partager sur Facebook
  • Partager sur Twitter
31 août 2011 à 17:44:29

@ Marc Mongenet : En effet, je viens de consulter d'autres manuels (dont celui de mon système) et ils disent tous comme le tien. Seul celui cité précédemment dit « inférieur à zéro ». Manque de pot, c'est le premier référencé par Google en tapant « man getcwd »…

@ gouttegd : Héhé, voilà un renseignement utile pour la version Windows. :)


Je voulais signaler deux choses qu'on a (que j'ai) oubliées :
  • Sous Linux, un chemin relatif peut commencer par "./", mais aussi par "../" !
  • Toujours sous Linux, $PATH ne contient pas forcément un chemin absolu ! On peut spécifier un chemin relatif (et même sans le faire commencer par "./", ou "../").

J'édite mon gros message pour le compléter.
  • Partager sur Facebook
  • Partager sur Twitter
31 août 2011 à 19:08:52

@ Maëlan : À noter que pour un programme ciblant uniquement Windows, il y a aussi la fonction GetModuleFileName (doc), qui permet de faire plus directement ce que le PO semble vouloir faire. En lui passant NULL comme premier argument, elle remplit un tampon avec le chemin complet de l’exécutable courant — il n’y a plus qu’à chercher le dernier séparateur de répertoires ('/' ou '\') pour obtenir le dossier contenant ledit exécutable.
  • Partager sur Facebook
  • Partager sur Twitter
31 août 2011 à 20:55:59

Citation : gouttegd

@ Maëlan : À noter que pour un programme ciblant uniquement Windows, il y a aussi la fonction GetModuleFileName (doc), qui permet de faire plus directement ce que le PO semble vouloir faire. En lui passant NULL comme premier argument, elle remplit un tampon avec le chemin complet de l’exécutable courant — il n’y a plus qu’à chercher le dernier séparateur de répertoires ('/' ou '\') pour obtenir le dossier contenant ledit exécutable.


Hey, mais c'est merveilleux, il n'y a même plus besoin de coder la fonction soi-même ! :magicien: Merci beaucoup gouttegd pour ses informations précieuses.

Par ailleurs, j'ai encore rajouté des choses dans mon gros message (celui en vert) :
  • sous Windows, difficulté supplémentaire de la méthode proposée (parcours de PATH), due aux extensions implicites ;
  • général, fiabilité non garantie de la méthode « parcours de PATH », mise en défaut si un fichier du même nom que l'exécutable a été créé dans un dossier du PATH après le lancement (voire faille de sécurité exploitable ?).

Sinon, je voulais dire qu'utiliser chdir peut être pratique, mais l'utilisateur ne va peut-être pas aimer que le programme change tout seul de répertoire de travail. Par exemple, si tu codes un éditeur de texte en console, alors quand l'utilisateur le lancera, il s'attendra à ce que le programme travaille dans le dossier dans lequel il se trouvait, et que les chemins qu'il entrera y seront relatifs. Il faut voir si le problème se pose dans ton cas (notamment s'il y a manipulation de fichiers/dossiers spécifiés par l'utilisateur).


Enfin, voici mon code pour Linux (bien commenté) :
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>



/* buffer global (restreint à ce fichier source) pour y stocker le chemin absolu
   d'installation du programme, et éventuellement un chemin relatif après*/
static struct {
	char buf[4096];
	size_t size;   /* taille du chemin d'installation lui-même (car le
	                  buffer peut contenir du contenu supplémentaire après) */
}  appDir=  {"", 0};
#define  SIZEOFBUF   (sizeof(appDir.buf)/sizeof(*appDir.buf))



/* vérifie l'existence du fichier dont le chemin a été passé en argument ;
   renvoie 1 si l'entité existe et est un fichier, 0 sinon. */
int fileExists(const char* path) {
	struct stat s;
	return !stat(path, &s)  &&  S_ISREG(s.st_mode);
}



/* écrit, dans le buffer global `appDir`, une chaîne au format */
static char* writePathInAppDir(const char* path, const char* stopP, const char* file, const char* stopF) {
	if(path==NULL || file==NULL)   return NULL;
	size_t size1= (stopP!=NULL)? (unsigned)(stopP-path) : strlen(path),
	       size2= (stopF!=NULL)? (unsigned)(stopF-file) : strlen(file);
	appDir.size= size1 + 1 + size2;   // +1 for '/'
	if(SIZEOFBUF <= appDir.size)   return NULL;
	/* on copie le contenu de `path` jusqu'à `stopP` exclus, puis '/',
	   puis le contenu de `file` jusqu'à `stopF` exclus */
	strncpy(appDir.buf, path, size1);
	appDir.buf[size1]= '/';
	strncpy(appDir.buf+size1+1, file, size2);
	appDir.buf[appDir.size]= '\0';
	return appDir.buf;
}



/* écrit le chemin d'installation d'après le chemin absolu passé en argument ;
   `stop` indique la fin (exclue) du chemin à lire. */
static char* writeAppDirFromAbs(const char* abs, const char* stop) {
	if(abs==NULL)   return NULL;
	/* on copie le contenu de `abs` jusqu'à `stop` exclus */
	appDir.size= (stop!=NULL)? (unsigned)(stop-abs) : strlen(abs);
	if(SIZEOFBUF <= appDir.size)   return NULL;
	strncpy(appDir.buf, abs, appDir.size);
	/* si le chemin ne se finit pas par '/' et que la taille du buffer nous
	   le permet, on l'ajoute */
	if(appDir.buf[appDir.size-1]!='/' && SIZEOFBUF > appDir.size+1)
		appDir.buf[appDir.size++]= '/';
	appDir.buf[appDir.size]= '\0';
	return appDir.buf;
}



/* écrit le chemin d'installation d'après le chemin relatif passé en argument ;
   `stop` indique la fin (exclue) du chemin à lire. */
static char* writeAppDirFromRel(const char* rel, const char* stop) {
	if(rel==NULL)   return NULL;
	size_t size1, size2;
	/* on écrit le chemin absolu 'exécution */
	if(getcwd(appDir.buf, SIZEOFBUF) == NULL)
		return NULL;
	size1= strlen(appDir.buf);
	size2= (stop!=NULL)? (unsigned)(stop-rel) : strlen(rel);
	appDir.size= size1 + 1 + size2;   // +1 for '/'
	if(SIZEOFBUF <= appDir.size)   return NULL;
	/* on ajoute '/' */
	appDir.buf[size1]= '/';
	/* on copie le contenu de `rel` jusqu'à `stop` exclus */
	strncpy(appDir.buf+size1+1, rel, size2);
	/* si le chemin ne se finit pas par '/' et que la taille du buffer nous
	   le permet, on l'ajoute */
	if(appDir.buf[appDir.size-1]!='/' && SIZEOFBUF > appDir.size+1)
		appDir.buf[appDir.size++]= '/';
	appDir.buf[appDir.size]= '\0';
	return appDir.buf;
}



/* retourne le chemin d'exécution du programme, en fonction de `argv` ; celui-ci
   doit correspondre à `argv[0]`, où `argv` est le 2è argument de `main` (de
   type `char**`) ; retourne NULL en cas d'échec ; le buffer retourné n'est pas
   à libérer. */
const char* getAppDir(const char* argv) {
	if(argv==NULL)   return NULL;
	/* si on a déjà écrit avec succès le chemin d'installation */
	if(*appDir.buf!='\0') {
		appDir.buf[appDir.size]= '\0';   // on réajuste la fin de la chaîne (la fonction `getAbsoluteResourcePath` peut l'agrandir)
		return appDir.buf;
	}
	
	const char* p;
	
	p= strrchr(argv, '/');
	
	/* nom du fichier */
	if(p==NULL) {   // pas de '/'
		p= getenv("PATH");
		if(p==NULL)   return NULL;
		
		const char* q;
		do {   /* pour chaque chemin contenu dans $PATH … */
			q= strchr(p, ':');
			/* … on écrit "chemin/argv" dans `appDir.buf` … */
			if(writePathInAppDir(p, q, argv, NULL) == NULL)
				return NULL;
			/* … pour pouvoir tester l'existence du fichier */
			if(fileExists(appDir.buf))   q= NULL;   // si le fichier est trouvé, on sort de la boucle
			else {
				*appDir.buf= '\0';   // indique qu'on a pas trouvé
				p= q+1;   // on passe au chemin suivant
			}
		} while(q!=NULL);
		
		if(*appDir.buf=='\0')   return NULL;   // si on n'a pas trouvé
		if(*appDir.buf=='/') {   // si le chemin trouvé est absolu
			/* on garde ce qui est déjà écrit dans `appDir`, en
			  tronquant juste le nom du fichier */
			strrchr(appDir.buf, '/')[1]= '\0';
			appDir.size= (q!=NULL)? (unsigned)(q-p): strlen(appDir.buf);
		} else   // si le chemin trouvé est relatif
			if(writeAppDirFromRel(p, q) == NULL)
				{*appDir.buf='\0'; return NULL;}
	}
	
	/* chemin absolu */
	else if(*argv=='/') {   // commence par "/"
		if(writeAppDirFromAbs(argv, p) == NULL)
			return NULL;
	}
	
	/* chemin relatif */
	else /*if(argv[0]=='.' && (argv[1]=='/' || argv[1]=='.' && argv[2]=='/'))*/ {   // commence par "./" ou "../"
		if(writeAppDirFromRel(argv, p) == NULL)
			{*appDir.buf='\0'; return NULL;}
	}
	
	/* invalide (contient au moins un '/' mais n'est ni absolu ni relatif) */
	//else   return NULL;
	
	
	return appDir.buf;
}



/* retourne le chemin absolu du fichier ou dossier dont le chemin, relatif au
   dossier d'installation, est passé en argument ; le buffer retourné n'est pas
   à libérer (écrit directement dans le buffer global `appDir`) */
const char* getAbsoluteResourcePath(const char* path) {
	if(path==NULL)   return NULL;
	if(*path=='/')   return path;   // si le chemin est déjà absolu
	if(SIZEOFBUF <= appDir.size+strlen(path))   return NULL;
	strcpy(appDir.buf+appDir.size, path);
	return appDir.buf;
}

Contrairement à Pouet, je gère tout ça avec un buffer statique (et unique). À ce titre, je me demandais si 4096 chars suffisaient, si ce n'est pas assez, beaucoup trop …? Dans la grande majorité des cas, ça suffira plus que largement, mais quels sont les cas extrêmes sous Linux ?

Je vais peut-être coder la version Windows, mais au vu de l'intervention de gouttegd, ça n'aura d'autre intérêt que de m'entraîner (et ça va être plus dur que sous Linux…).

ÉDIT: Petites corrections du code (cf ici)
  • Partager sur Facebook
  • Partager sur Twitter
31 août 2011 à 21:27:19

@Maelan: Le style est peu aéré, mais c'est pas mal :)

Il me semble juste qu'il y a une erreur dans la fonction writeAppDirFromAbs. En effet, tu vérifies si le chemin est terminé par un '/' et si ce n'est pas le cas, tu ajoutes ce caractère s'il y a encore de la place. Le problème c'est qu'après tu ajoutes le '\0' final, or il se pourrait très bien que le chemin fasse tout juste la taille du tampon avec le '/' et que tu écrives donc le '\0' une case au delà du tableau.

Sinon, la conversion en unsigned n'est pas nécessaire lorsque tu effectues la différence entre le pointeur vers le début et celui vers la fin du chemin car la conversion en size_t aura lieu de part l'affectation ;)
  • Partager sur Facebook
  • Partager sur Twitter
31 août 2011 à 22:07:14

Citation : Taurre

Il me semble juste qu'il y a une erreur dans la fonction writeAppDirFromAbs. En effet, tu vérifies si le chemin est terminé par un '/' et si ce n'est pas le cas, tu ajoutes ce caractère s'il y a encore de la place. Le problème c'est qu'après tu ajoutes le '\0' final, or il se pourrait très bien que le chemin fasse tout juste la taille du tampon avec le '/' et que tu écrives donc le '\0' une case au delà du tableau.


En effet, c'est appDir.size+1 et non appDir.size dans la condition. Merci. :)
À ce propos, je me suis rendu compte que je devais mettre sizeof(appDir.buf)/sizeof(*appDir.buf) au lieu de sizeof(appDir.buf) un peu partout… Ce n'est qu'un détail (vu que c'est un tableau de char), mais ce n'est pas très rigoureux.

Citation : Taurre

Sinon, la conversion en unsigned n'est pas nécessaire lorsque tu effectues la différence entre le pointeur vers le début et celui vers la fin du chemin car la conversion en size_t aura lieu de part l'affectation ;)


Ça, c'était juste pour faire taire le compilateur.
  • Partager sur Facebook
  • Partager sur Twitter
31 août 2011 à 23:11:11

Citation : Maëlan

À ce titre, je me demandais si 4096 chars suffisaient, si ce n'est pas assez, beaucoup trop …? Dans la grande majorité des cas, ça suffira plus que largement, mais quels sont les cas extrêmes sous Linux ?


Le « cas extrême » est qu’il n’est tout simplement pas possible de prévoir quelle peut être la taille maximale d’un chemin d’accès.

POSIX propose une constante PATH_MAX, mais celle-ci n’existe pas forcément sur tous les systèmes. POSIX n’impose de la définir que quand elle a un sens, c’est-à-dire quand il existe bel et bien sur le système une limite maximale à la taille d’un chemin d’accès. Or sous GNU/Linux (et probablement tous les autres unixoïdes modernes), cette limite, soit n’existe pas, soit existe mais est variable d’un système de fichiers à l’autre.

Il est théoriquement possible d’utiliser pathconf(3) pour récupérer la limite pour un système de fichiers donné, mais en pratique ça n’avance à rien, puisqu’il y a toujours la possibilité de tomber sur un système de fichiers n’imposant pas de limite à la taille d’un chemin d’accès (auquel cas pathconf renverra -1), ou sur un système de fichiers dans lequel la limite est tellement élevée qu’il serait déraisonnable d’allouer un tampon correspondant à la taille maximale d’un chemin d’accès.

En étant réaliste, on peut légitimement supposer que 4096 chars suffiront amplement « la plupart du temps », mais en toute rigueur le seul moyen de gérer tous les cas est d’allouer dynamiquement le tampon et d’augmenter progressivement sa taille quand on constate que le chemin d’accès devient trop long.
  • Partager sur Facebook
  • Partager sur Twitter
31 août 2011 à 23:12:35

Citation : Maëlan

  • Sous Linux, un chemin relatif peut commencer par "./", mais aussi par "../" !

Sur Unix, je dirais plutôt que:
  • tout chemin qui commence par / est absolu (/bin/ls, /usr/bin/emacs)
  • tout chemin qui ne contient pas de / est un fichier dans $PATH
  • les autres chemins sont relatifs (./a.out, foo/bar)
  • Partager sur Facebook
  • Partager sur Twitter
1 septembre 2011 à 11:34:49

@ gouttegd : Encore merci pour ces précisions. Pour PATH_MAX, j'avais vu ça en effet, mais comme tu dis il faut éviter de l'utiliser.

Sinon, j'avais trouvé une fonction intéressante en lien avec ce qu'on veut faire : realpath (man). Elle écrit le chemin absolu du chemin passé en argument (relatif au dossier de travail, j'imagine - pratique, ça nous évite de réinventer la roue), en éliminant les "." et les ".." inutiles (réduit potentiellement la taille du chemin). Elle résout aussi les liens symboliques (ça par contre, ça peut rallonger).
En plus, elle est déclarée dans <stdlib.h>.
Sinon, c'est assez facile à recoder (sans la résolution des liens symboliques).

@ Marc Mongenet : C'est exact, mais si on veut appeler foo/bar depuis le shell, il faudra taper ./foo/bar et non foo/bar.
  • Partager sur Facebook
  • Partager sur Twitter
1 septembre 2011 à 12:44:18

Citation : Maëlan

En plus, elle est déclarée dans <stdlib.h>.


Attention, le fait qu’une fonction soit déclarée dans un fichier d’en-tête standard ne veut aucunement dire que la fonction elle-même soit standard. Les développeurs d’une bibliothèque C sont parfaitement libres de déclarer dans ces fichiers d’en-tête des fonctions de leur cru (et personne ne se prive de le faire).

En l’occurence toutefois, realpath est standardisée (par POSIX), donc on peut compter sur sa présence à peu près partout (pas sur Windows néanmoins).
  • Partager sur Facebook
  • Partager sur Twitter
4 septembre 2011 à 18:55:41

On peut plus prendre quelques jours de vacances sans être largué :soleil:

Merci à tous pour ses solutions, je pose un jour de congé et je dissèque tout ça.

J'avoue aussi avoir un peut de mal à trouver ce qui est standard et ce qui ne l'est pas.
Avant de continuer à m'enfoncer dans le code, je vais me tourner vers la littérature du parfait petit programmeur.
Même si je retiens pas tout, j'aurai de meilleurs intuitions.

Je posterai ce que j'aurais pu pondre fort de tout vos conseils.

A +

:)
  • Partager sur Facebook
  • Partager sur Twitter
4 septembre 2011 à 23:09:52

Citation : Maëlan

@ Marc Mongenet : C'est exact, mais si on veut appeler foo/bar depuis le shell, il faudra taper ./foo/bar et non foo/bar.


Tu as un shell bizarre.
Pour exécuter foo/bar je tape foo/bar. Et . n'est évidemment pas dans mon PATH.
  • Partager sur Facebook
  • Partager sur Twitter
5 septembre 2011 à 11:31:51

La fonction get_current_dir_name existe sous linux et est plus simple d'utilisation je trouve.

char *get_current_dir_name(void)
  • Partager sur Facebook
  • Partager sur Twitter
Pour ceux qui souhaiteraient apprendre à développer en Rust, un tuto en français est dispo ici. Pour voir mes projets : github.
7 septembre 2011 à 16:43:14

Citation : Marc Mongenet

Citation : Maëlan

@ Marc Mongenet : C'est exact, mais si on veut appeler foo/bar depuis le shell, il faudra taper ./foo/bar et non foo/bar.


Tu as un shell bizarre.
Pour exécuter foo/bar je tape foo/bar. Et . n'est évidemment pas dans mon PATH.



Ah ? C'est curieux. Je vais regarder ça plus en détail. Il me semble bien avoir vu sur le Web des lignes de commande me suggérant que leurs auteurs sont dans le même cas que moi.

Après une rapide recherche sur Internet, je trouve ceci (voir la section "Exécuter") qui semble être en accord avec moi. Voir aussi ce qu'en dit Wikipédia.

Citation : Ce qu'en dit Wikipédia

Le piège du répertoire courant


Sous Unix, pour exécuter un fichier qui est dans le répertoire courant, on est en général obligé de préfixer la commande par « ./ », ce qui permet d'indiquer que le fichier est dans le répertoire courant.

Cette particularité étonne les utilisateurs qui ont l'habitude de Windows (ou de l'ancien MS-DOS) où on peut appeler directement un programme qui est dans le répertoire courant.

Cependant, il s'agit là d'une mesure de sécurité : à titre d'exemple, si un intrus malveillant parvient à placer un programme néfaste nommé ls dans le répertoire courant, ce programme sera exécuté dès que vous souhaiterez lister le répertoire (au lieu de la commande ls se trouvant normalement dans le répertoire /bin/, qui lui se trouve dans le PATH, mais qui n'est modifiable que par l'administrateur du système).

Si malgré tout l'utilisateur souhaite retrouver cette « ergonomie » de Microsoft, il faut qu'il rajoute le chemin « ./ » dans son PATH :

PATH=$PATH:./


Note bien qu'il est dit "en général". M'est avis que c'est ton shell qui est plus exotique. :-°
  • Partager sur Facebook
  • Partager sur Twitter