Shell, quand tu nous tiens

Un jour, par la conjonction du hasard et de la curiosité, Mr Google a laché ces quelques caractères qui, mis bout à bout, synthétisent une adresse pointant vers une page web:

http://perl.plover.com/yak/commands-perl/samples/slide008.html

Quoi, Un Shell? Mais quèche que sait que che shell, le Favier vous?
#!/bin/perl
# World's Smallest Shell
$| = 1;
 while (1) {
  print "perl% ";
  my $command = <STDIN>;
  last  $command eq '';
  chomp $command;
  my ($program, @args) = split  /\s+/, $command;
  my $pid = fork;
  if (! defined $) { print  "Couldn't fork: $!\n" }
  elsif ($pid != 0) { wait          # The parent
  else  exec $program, @args;       # The child
  die "Couldn't exec: $!\n";
 }

Fantastique! Il faut l' admettre, c'est un shell. Il est petit, c'est vrai.

Hmm... Pourquoi appeler l' interpréteur Perl qui va charger à chaque fois ce pauvre script, laissé à découvert, que n'importe qui pourrait modifier par mégarde ... En plus c'est écrit dans une langue que même les chinois ne comprennent pas!

L'expression "World's smallest shell" est amusante. Superlatif qui pousse à l' extrapoler!
Ni une ni deux je prends mon C, et tada! Voilà:
// "Le plus petit shell au monde"
#include<stdio.h>
#include <stdlib.h>

int main()
{
  char str[255] = " ";

  while (str) 
  {
   printf("BXshell:: ");
   scanf("%s", str);
   system(str);
  }

  return 0;
}

C'est donc le "World's smallest than the smallest shell"! Le über-smallest shell Hahaha...

Mais, comme son parent spirituel écrit en Perl, il tient la route. Un peu trop: il ne veut pas lacher le volant. Obligé de tuer le processus (comme son papa-perl)! Et l' appel de la fonction system() est... brutal.
Sans parler du string d'entrée, limité à 255 caractères et sans vérification. Imaginons ce qui va se passer quand Eric (QI < 80) va y copier/coller le dernier rapport de bug de Windows XP juste avant d'appuyer sur Entrée?? Aye Aye Aye!
Pire, si c'est un des lieutenants de Bill Grates qui injecte du code interprétable au-delà de la limite, pour qu'il soit placé sciemment à un endroit de la mémoire d' ou il pourra exécuter diverses actions qui pourraient être regrettables? Oui, et si ma tante en avait on l' appelerait mon oncle! Trève de suppositions,
// "Le plus petit shell au monde" un peu moins miteux

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

int main ()
{
  char str[255] = " ";
  puts("Taper exit pour sortir");

  while (str) 
  {
    printf ("BXshell:: ");
    scanf ("%s", str);
    if (strcmp(str, "exit")==0) break;
    system(str);
  }

  return 0;
}
Ouf! L' utilisateur peut sortir proprement.

- Mais au fond c'est quoi un shell?
- C'est l' interface entre l' utilisateur et les outils du système d' exploitation. C'est la standardiste qui vous dit "Je l' appelle, patientez s'il vous plait... Ah non il n'est pas là!" Ou "L'inspecteur des impôts va vous recevoir tout de suite!" ou encore "Vous avez trois nouveaux messages."

L'avantage étant que, qand on sait lui parler, ses réponses sont intéressantes. Si on lui dit par exemple, sur l' entrée standard :

 echo (35.265 * 44.874) / 955;

... Elle répond par un ...

 echo (35.265 * 44.874) / 955;
     1.657048806282723

... Et attend les instructions, sans jacasser.
Si en plus, elle accepte aussi de lire et d' interpréter les commandes contenues dans un fichier, de reconnaître une liste de fonctions internes, d'accepter les tests de condition, les boucles, les fonctions perso déclarées juste pour la session en cours ... Ca m' intéresse!

Une sorte de calculatrice avancée. C'est ça, le shell. Un mini-langage de programmation.


Retour au vif du sujet. Réorganisons le code pour ajouter d'autres fonctions internes :
/* Reconnaissance et appel de fonctions internes */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* nombre de fonctions internes implémentées -------------------*/
#define NBCMDS 5

/* Chaque commande interne a une description, et une fonction:  */
struct commande
{
  char *nom;
  char *description;
  void (*fonction) ();
};

/* Prototypes des fonctions internes ---------------------------*/
void cmd_exit();
void cmd_echo(char *s);
void cmd_aide();

/* Toutes les commandes dans un tableau de structures: ---------*/
struct commande cmds[] =
{
  { "echo", "Afficher vers la sortie standard", cmd_echo},
  { "aide", "Afficher l' aide"                , cmd_aide},
  { "ciao", "Teminer le programme"            , cmd_exit},
  { "bye" , "Teminer le programme"            , cmd_exit},
  { "exit", "Teminer le programme"            , cmd_exit}
};

/* Fonctions internes ------------------------------------------*/
void cmd_exit()
{
  puts("bye!");
  exit(0);
}

void cmd_echo(char *s)
{
  printf("%s\n", s);
}

void cmd_aide()
{
  unsigned char n;

  puts("Liste des fonctions internes:");
  for (n=0; n<NBCMDS; n++)
  {
    printf("\t%s\t%s\n", cmds[n].nom, cmds[n].description);
  }
}

/* The main ----------------------------------------------------*/
int main(void)
{
  char str[255] = " ";
  unsigned char n, b, i=1;

  // Présente les fonctions internes à l' utilisateur
  cmd_aide();

  while (i)
  {
    printf ("BXshell:: ");
    fscanf (stdin, "%s", str);
    b=0;

    for (n=0; n<NBCMDS; n++)
    {
       if (strcmp(cmds[n].nom, str)==0) { b=1; break;}
    }
    if (b) cmds[n].fonction(); else system(str);
  }

  return 0;
}
Comme le code va grossir de façon conséquente, la mise à part de ces fonctions facilitera leur passage vers un fichier externe, pour éclaircir le code.

Bon, alors comment améliorer cette carcasse de "plus petit shell du monde qui tue"...
Il lui faut un analyseur lexical qui reconnaitra la sémantique, en commençant par les expressions mathématiques.

En effet ce qui cloche à présent, c'est la fonction echo. pour qu'elle fonctionne, il faut analyser la chaîne d'entrée, qui comporte un argument:

 echo "ok";

Première approche: En parcourant la chaîne caractère par caractère,on sait que dès qu'un espace est rencontré et que le mot précédent est 'écho', alors le reste sera l' argument passé à la fonction. C'est nul comme approche. Les espaces on s'en tape ce sont les guillemets et le point-virgule qui intéressent l' analyseur.

Bison, Lex et Yacc sont bien pour ça. Mais bien que très puissants, ils génèrent du code C plutôt obscur, sur lequel on a un contrôle assez limité.

Alors il faut tout faire à la main.

Les codes de calculatrices à ordre inversé polonais abondent sur le net, surtout dans les cursus de maths et d'informatique. Bien que pratique, ce n'est pas séduisant de taper les deux opérandes avant l'opérateur. Même déroutant. Et pas de parenthèses.
La solution? Le stack. Ca sonne bien le stack. Il suffit de lire la chaîne caractère par caractère. Pour isoler opérandes, opérateurs, et les fonctions, au fur et à mesure, afin de les empiler sur le stack.

Avant d' aller plus loin, il faut déterminer les spécifications du langage, qui serviront simultanément de support et d' objectif. Comme toujours, il vaut mieux savoir où on va avant d'y aller. Je veux une syntaxe similaire au C, mais en français. Avec deux ou trois choses issues du basic (qui est lui aussi un langage impératif, dit procédural).
Le langage par l' exemple:

VARIABLES
---------
//Créer variable et assigner valeur:
  ab = 2;
  ab = -5;
  ab = 15.568;
  ab = "ok";
  ab = 3 + 2;
  ab = 3.5 + 2.658;
  ab = (a+2)*3;
  ab = (4 -2) / 2 - 1;

OPERATEURS
----------
  Affectation
    = += -=
  Mathématiques
    + - * / ^
  Comparaison
    ==
    !=
    >
    <
    >=
    <=
  Inc/Décrémentation
    ++
    --

TEST DE CONDITION
-----------------
  si (cond) stmt;
  si (cond) stmt; sinon stmt;

  // 1re notation: sur 1 seule ligne
  si (a == 5) echo a; sinon echo b;

  // 2e notation: avec brackets
  si (a == 4) {
    echo a;
  }
  sinon {
    echo b;
  }

BOUCLES
-------
  de n = 1 à 5
  {
    echo n;
  }
  //Affiche 12345

  de n = 1 à 5
  {
    echo n & " ";
  }
  //Affiche 1 2 3 4 5

  de n = 1 à 10 par 2
  {
    echo n & " ";
  }
  // Affiche 1 3 5 7 9 11

FONCTIONS
---------
// une procédure qui ne retourne rien:

  func x()
  {
    echo "truc";
    echo $1; // argument 1 passé à la fonction
  }

// Du point de vue interpréteur c'est
  func x() { echo "truc"; echo (a+2)*3;}

// Prend en argument un string, retourne un type string
  str func x( str s )
  {
    echo s;
    a = "truc";
    ret a;
  }

  // et appel:
  a = x ("test");
  echo a;
  // OU
  echo x("test");

FONCTIONS INTERNES
------------------
  echo
  aide
  exit

  + tous les binaires system.
  Mieux: une fonction system() !

 MATH
  abs
  cos
  sin
  tgt
  sqr
  pow
 STRING
  concat
 IO
  ouvrir
  fermer



Un mot sur les variables:
  ab = 2;
  ab = -5;
  ab = 15.568;
  ab = "ok";
  ab = 3 + 2;
  ab = 3.5 + 2.658;
  ab = (a+2)*3;
  ab = (4 -2) / 2 - 1;


Pour la question d'interpréter ce qui vient de stdin OU d'un fichier:
// Saisie sur stdin, ou lecture de fichier.

int main( int argc, char **argv )
{
  FILE *pF;
  if (argc ==2)
  {
    if(!(fluxIN = fopen(argv[1], "r")))
    {
      puts("* Fichier introuvable.");
      return 1;
    }
  }
  else
  {
    printf("Interpréteur BX 0.0.2 (C) 2006 B-X Masta\n"
           "taper aide pour voir l' aide.\n> ");
    FluxIN = stdin;
  }

  // on a un pointeur de fichier pour l' entrée.
  // parse() obtient son contenu avec scanf et le traite
  while (str)
  {
    printf ("BXshell:: ");
	parse();
  }
  fclose (fluxIN);
  return 0;
}


Maintenant il faut écrire la fonction d'analyse lexicale du buffer d' entrée, qui pousse les arguments dans un arbre binaire, puis exécute.

Une version modifiée de l' interpréteur en compilateur pourra écrire un fichier au format ELF, ou plus simplement, exporter du code assembleur qui lui, sera compilé et linké avec un compilateur tel l' excellent FASM. Eh oui, il n' y a que 24 heures dans une journée même si en fait, certaines en font 26. A suivre ... 2006