Archives par mot-clé : for

Programme cal « vertical » optimisé

Dans un précédent article traitant de calendriers pèles-mêles, j’avais proposé l’écriture d’un programme cal permettant l’affichage vertical d’un mois.

Dans un souci d’apporter des solutions toujours meilleures, voici une nouvelle version de ce programme, qui apporte son lot d’améliorations :

  • plus robuste
  • plus performant
  • plus pérenne et évolutif

Bref, que du bonheur pour un gourou linuxien !

Ici, il n’est plus question d’utiliser la commande cal… Le texte parsé : aux oubliettes ! A la place, nous allons utiliser la commande date. Cette dernière va avoir plusieurs rôles, à savoir le nom des jours à afficher dans notre calendrier vertical, et la détection de la fin du mois via son code d’erreur en sortie (récupéré grâce au « $? »).

Commençons par définir des variables simples, que nous pourrons remplacer par des paramètres « dynamiques » ultérieurement : nous fixons une année et un mois comme ceci :

Y=’2010′
m=’07’

Le reste du programme consiste ensuite en une boucle for, prenant comme paramètres les numéros de jours allant de 01 à 31. Une autre possibilité serait d’utiliser une boucle while en incrémentant une variable de 1 à 31, mais voilà, j’ai pas envie… C’est donc dans la boucle ci-dessous que nous allons mettre « tout » notre code ; autant dire pas grand chose, vous allez voir…

for d in 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
do

[…]

done

A ce stade, nous connaissons l’année « $Y », le numéro de mois « $m » et le numéro de jour « $d ». Il nous faut connaître le nom du jour. La commande date permet d’afficher le nom du jour de deux façons :

  • avec « %A » pour l’affichage du nom complet (par exemple « lundi »)
  • avec « %a » pour l’affichage du nom abrégé (par exemple « lun »)

A l’aide du paramètre « -d », nous allons pouvoir soumettre nos 31 dates à la-dite commande, qui va donc gentiment nous retourner les jours correspondant… lorsque ces dates existent, ou bien nous gueuler dessus dans le cas contraire. Cette faculté à rejeter une date non valide va justement nous intéresser dans la suite.

date +’%a’ -d "$Y-$m-$d"

L’exécution de la commande ci-dessus retourne :

  • soit le nom abrégé du jour à la date précisée, ainsi que le code retour « 0 » pour « succès »
  • soit un message d’erreur car la date est non valide, et le code retour « 1 » pour « échec »

Nous devons stocker dans une variable le nom du jour obtenu, et empêcher l’affichage d’un éventuel message d’erreur en le redirigeant vers « /dev/null ». Il ne nous reste plus qu’à tester le résultat du code retour ainsi :

  • si c’est « 0 » (la date existe bien), on affiche le jour et son numéro dans le terminal
  • si c’est « 1 » (la date est non valide), on sort de la boucle for, le programme s’arrête normalement

Voici le programme (presque !) complet :

function cal2 ()
{
	Y='2010'
	m='07'
	
	for d in 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
	do
		j=`date +'%a' -d "$Y-$m-$d" 2>> /dev/null`
		
		if test "$?" = 0
		then
			printf "%3s %2s\n" "$j" "$d"
		else
			break
		fi
	done
}

Le résultat est déjà pas mal, mais on peut faire plus pratique en retirant désormais les limitations de développement sur l’année et le numéro de mois. Si ces données sont passées en arguments, alors nous nous en servons. Dans le cas contraire, nous utiliserons la date actuelle.

test "$1" != "" && Y=$1 || Y=`date +’%Y’`
test "$2" != "" && m=$2 || m=`date +’%m’`

Pour s’assurer de la pertinence des arguments passés à notre fonction, nous allons les contrôler à l’aide des expressions régulières. Ainsi, en cas de valeur non valide, le programme est stoppé et retourne la valeur « 1 » sans rien écrire dans le terminal.

case $Y in [1-2][09][0-9][0-9]) ;; *) return 1 ; esac
case $m in [01][0-9]) ;; *) return 1 ; esac

Il ne nous reste qu’à ajouter ces quatre petites lignes dans notre fonction, et le tour est joué.

function cal2 ()
{
	test "$1" != "" && Y=$1 || Y=`date +'%Y'`
	test "$2" != "" && m=$2 || m=`date +'%m'`
	
	case $Y in [1-2][09][0-9][0-9]) ;; *) return 1 ; esac
	case $m in [01][0-9]) ;; *) return 1 ; esac
	
	for d in 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
	do
		j=`date +'%a' -d "$Y-$m-$d" 2>> /dev/null`
		
		if test "$?" = 0
		then
			printf "%3s %2s\n" "$j" "$d"
		else
			break
		fi
	done
}

Après exécution de notre nouvelle commande, voici le résultat dans le terminal :

[benoit:~]$ cal2 2010 07
jeu 01
ven 02
sam 03
dim 04
lun 05
mar 06
mer 07
jeu 08
ven 09
sam 10
dim 11
lun 12
mar 13
mer 14
jeu 15
ven 16
sam 17
dim 18
lun 19
mar 20
mer 21
jeu 22
ven 23
sam 24
dim 25
lun 26
mar 27
mer 28
jeu 29
ven 30
sam 31

Je vous épargnerai davantage de code, mais, pour une utilisation exclusivement en ligne de commande, on pourrait aussi ajouter quelques améliorations visuelles, comme par exemple afficher en rouge les dimanches, ou bien souligner la date du jour… Bref, de quoi jouer encore quelques heures sur un programme finalement peu utile ! :mrgreen:

Afficher un texte multi-colore dans le terminal

Dans le précédent article, nous avons vu comment mettre en couleur du texte dans le terminal avec la commande printf.

Il serait parfois intéressant de pouvoir distinguer, dans le terminal, des informations plus ou moins « tabulées ». L’objectif de cet article est de présenter une solution de mise en couleurs d’un texte, c’est-à-dire un ensemble de mots séparés par des espaces. Nous allons rapidement constater une limitation liée à ces fameux espaces, ce que nous tenterons de résoudre dans un deuxième temps.

Concentrons-nous d’abord sur la manière de changer de couleur chaque mot d’un texte. Nous avons tout de même une piste : la fonction printcolor réalisée précédemment, à laquelle nous pouvons passer comme paramètre une valeur de couleur comprise entre 0 et 7. Si nous partons du principe que le fond de notre terminal est noir, nous n’utiliserons pas la valeur 0 et commencerons à 1 par conséquent.

Reste à trouver une façon simple de changer de valeur pour chaque nouveau mot de notre texte. Nous pouvons par exemple nous servir d’une boucle for à laquelle nous allons passer le texte en paramètre. Ainsi, pour chaque mot du texte, nous pouvons exécuter notre fonction printcolor. Et dans le même temps, il suffit d’incrémenter une variable initialisée à 1. Si nous détectons que notre variable $couleur vaut 8, alors nous lui réattribuons la valeur 1. La fonction echorainbow ci-dessous répond à cette première analyse.

function echorainbow ()
{
    couleur=1

    for word in $chaine
    do
        printcolor "$couleur" "$word"

        couleur=$((couleur+1))
        test $couleur -eq 8 && couleur=1
    done
}

Après chargement en session de notre script (via la commande source), ce seul exemple devrait déjà faire de votre terminal une vraie discothèque pour geek :

[benoit:~]$ echorainbow "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam."

Mais comme un texte n’est pas non plus ce qui nous intéresse dans un terminal la plupart du temps, nous allons plutôt voir un exemple un peu plus complexe avec la commande ls. Pour un affichage ligne par ligne, il est dans ce cas nécessaire d’utiliser un petit while derrière un pipe… Si vous ne me croyez pas, essayez d’exécuter « echorainbow `ls` » ! Voici le résultat — nettement perfectible ! — de cette commande :

Première version de la fonction echorainbow
Première version de la fonction echorainbow

Nous nous apercevons que les espaces n’ont pas été conservés, ce qui est logique, puisque les espaces sont utilisés comme séparateurs lors de l’utilisation du for. Ajouter un espace derrière chaque mot n’est pas forcément une solution valable, puisque pour des données tabulées comme c’est le cas avec la commande ls, nous serions quand-même susceptibles de perdre l’alignement vertical permis par l’affichage de plusieurs espaces consécutifs !

Nous nous rendons compte qu’il serait beaucoup mieux de pouvoir garder les espaces. Pour ce faire, il devient donc nécessaire de remplacer chaque espace, non pas par un seul caractère, mais par une chaine de caractères, qui ne risque pas d’être rencontrée parmi tous les mots. A l’aide d’un pipe et de la commande sed, nous allons donc changer tout caractère espace, par exemple en "5PAC3". Ne voyez pas là un clin d’oeil au rappeur 2Pac, mon inspiration est surtout venue du 1337 5p34k… Voici la commande sed correspondante :

sed s/’ ‘/’5PAC3’/g

Cela ne suffit pas. Puisque les espaces nous servent de séparateurs de mots, si nous les supprimons, cela revient à ne plus avoir qu’un seul et unique « mot », mais très long. Comme quoi, parfois, plus c’est long, moins c’est bon… Euh… bref… Il nous faut donc séparer nos mots comme précédemment, par exemple en ajoutant un espace à la suite de chaque série d’espaces à conserver (et donc identifiés par notre super chaine « 5PAC »). Là encore, la commande sed va nous être utile. Mais cette fois-ci, c’est en sortie de la précédente commande sed que nous allons mettre un pipe, afin appliquer cette nouvelle transformation. Cette deuxième commande sed s’écrit ainsi (faites bien attention à l’espace derrière le « 1 » !) :

sed s/"((5PAC3)+)"/"1 "/g

Une fois les deux commandes sed mises bout à bout, voilà ce que ça donne :

sed s/’ ‘/’5PAC3’/g | sed s/"((5PAC3)+)"/"1 "/g

Il ne reste plus qu’à appliquer nos deux sed sur la chaine en entrée, puis, pour chaque mot, rechanger les "5PAC3" en espaces (ce qui peut être fait en sortie de la fonction printcolor, dans la boucle for). Revoyons notre fonction echorainbow avec ces nouveaux éléments pris en compte :

function echorainbow ()
{
    esp="5PAC3"
    chaine=`echo "$*" | sed s/' '/$esp/g | sed s/"\(\($esp\)\+\)"/"\1 "/g`

    j=1

    for word in $chaine
    do
        printcolor "$j" "$word" | sed s/$esp/' '/g

        j=$((j+1))
        test $j -eq 8 && j=1
        done
}

Cette fois-ci, l’exécution de l’exemple avec le ls et le while est bien plus convaincante !

Nouvelle version de la fonction echorainbow
Nouvelle version de la fonction echorainbow

Bien-sûr, l’exemple avec ls n’est pas forcément le plus intéressant… Par contre, l’utilisation de cette fonction peut trouver son sens dans le cas de données « chainées », c’est-à-dire de natures diverses et séparées par un séparateur comme « # » ou « ; ». Prenons l’exemple ci-dessous :

1;2;3;hello world     ;   TOTO;0;TITI;TATA;123;321;Lo;rem;  ip;su;m

La lecture de 10, 20 ou 30 lignes comme celle-ci peut devenir rapidement fastidieuse, surtout lorsqu’on recherche une information précise en plein milieu. Dans ce cas, notre fonction echorainbow va nous permettre de bien distinguer chaque partie… Mais à une condition : il va falloir remplacer les « ; » par des espaces… Mais à une condition (hé oui !) : il va falloir remplacer les bons espaces par un autre caractère… Puis re-remplacer tout ça par les caractères de départ, afin de profiter du même affichage, mais en couleur !

Pour ce faire, nous utiliserons (pour une seule ligne, sinon, ne pas oublier de boucler dans un while) quatre commandes sed, les deux premières avant exécution de la fonction echorainbow, les deux autres après. Ce qui donnera dans notre exemple :

[benoit:~]$ chaine=`echo ‘1;2;3;hello world     ;   TOTO;0;TITI;TATA;123;321;Lo;rem;  ip;su;m’ | sed s/’ ‘/#/g | sed s/’;’/’ ‘/g`
[benoit:~]$ echorainbow "$chaine" | sed s/’ ‘/’;’/g | sed s/#/’ ‘/g

Surtout ne dites rien ! Admirez juste votre terminal… :mrgreen:

Creative Commons License
Publié sous licence Creative Commons.