Projet

Général

Profil

Petit script shell pour créer des liste TODO

C'est ma première incursion dans les script shell. Ce code n'est donc surement pas bien optimisé et il peut contenir des erreurs.

Mon but pour ce projet était tout d'abord d'apprendre les bases des script shell. Plus précisément j'ai voulu écrire un petit programme qui permete de créer un mémo.

Voici le code final :

#! /bin/sh
FILE=.todo
touch $FILE
COMMAND=$1 #the first argument (add remove check uncheck renum show or empty)
shift #discard the first argument

case $COMMAND in
add)
    for X in "$@" # quotes for adding sentences
    do
    LASTLINE=$(tail -n 1 $FILE)
    NUMBER=$((${LASTLINE%% *} + 1))
    echo "$NUMBER  [ ] $X" >> $FILE
    done    
    ;;
remove)
    for X in $@
    do
    touch tmp_file
    #-v to only keep the lines that do not contain the search term
    #-w whole word search so that typing 2 does not remove line 12
    grep -vw "$X" $FILE > tmp_file
    mv tmp_file $FILE
        done
    ;;
check)
    for X in $@
    do
    LINE=$(grep -w "$X" $FILE) #find the entire line
    NUMBER=${LINE%% *} #keep only the beginning of the line
    ITEM=${LINE##*] } #only the end
    sed -i "s/$NUMBER  \[ \] $ITEM/$NUMBER  \[X\] $ITEM/g" $FILE
        done
    #echo -e "\e[9m$LINE\e[0m" >> $FILE #other way of checking items
    ;;
uncheck)
    for X in $@
    do
    LINE=$(grep -w "$X" $FILE)
    NUMBER=${LINE%% *}
    ITEM=${LINE##*] }
    sed -i "s/$NUMBER  \[X\] $ITEM/$NUMBER  \[ \] $ITEM/g" $FILE
        done
    ;;
renum)
    NEW_NUMBER=1
    cat $FILE | while read LINE
        do
        NUMBER=${LINE%% *}
    sed -i "s/\<$NUMBER\>/$NEW_NUMBER/" $FILE #\< \> to match whole word
    ((NEW_NUMBER+=1))
    done
    ;;
show)
    cat $FILE
    ;;
empty)
    rm $FILE
    touch $FILE
    ;;
help)
    echo "todo add thing1 thing2 #to add items to the list" 
    echo "todo remove 1 thing2 #to remove item 1 and item containing thing2" 
    echo "todo check 1 thing2 #to mark item as done" 
    echo "todo uncheck 1 thing2 #to remove marks" 
    echo "todo show" 
    echo "todo empty" 
    echo "todo renum" 
    ;;
*)
    echo "Invalid option: try todo help" 
    ;;
esac

Et voici un exemple d'utilisation (le lignes commençant par $ représentent le prompt) :

$ todo add banane
$ todo add pomme orange kiwi
$ todo add "faire exo 3 page 193" 
$ todo show
1  [ ] banane
2  [ ] pomme
3  [ ] orange
4  [ ] kiwi
5  [ ] faire exo 3 page 193
$ todo check 1 pomme 4
$ todo show
1  [X] banane
2  [X] pomme
3  [ ] orange
4  [X] kiwi
5  [ ] faire exo 3 page 193
$ todo uncheck 2
$ todo show
1  [X] banane
2  [ ] pomme
3  [ ] orange
4  [X] kiwi
5  [ ] faire exo 3 page 193
$ todo remove X
$ todo show
2  [ ] pomme
3  [ ] orange
5  [ ] faire exo 3 page 193
$ todo remove pomme
$ todo show
3  [ ] orange
5  [ ] faire exo 3 page 193
$ todo add stylo cahier gomme
$ todo show
3  [ ] orange
5  [ ] faire exo 3 page 193
6  [ ] stylo
7  [ ] cahier
8  [ ] gomme
$ todo renum
$ todo show
1  [ ] orange
2  [ ] faire exo 1 page 193
3  [ ] stylo
4  [ ] cahier
5  [ ] gomme

Explication

La première ligne est un "shebang". Il permet de préciser quel interpréteur (sh, bash, zsh, python...) utiliser pour exécuter le scrip.

On affecte alors à la variable FILE le nom du fichier dans lequel on veut que la liste soit écrite. Puis on s'assure que le fichier existe avec la commande :

touch $FILE

En shell on utilise "$" pour indiquer qu'on veut utiliser la valeur de la variable et non pas la chaîne de caractères.
Donc FILE correspont à la chaîne de caractères "FILE" alors que $FILE est la valeur de la variable FILE qui vaut ici .todo .

Pour lire les arguments donnés au script on utilise $1, $2, $3 ... pour les argumets 1, 2, et 3, respectivement. Attention on commence à compter à 1 et non pas à 0 (techniquement $0 est le nom du programme).

COMMANT=$1

Sert donc à savoir quelle est la commande qui doit être executée (add, remove, check, show...).

(On fait aussi un "shift" dont l'utilité sera expliqué plus tard)

Un case switch sert alors à exécuter des commandes différentes en fonction de la commande spécifié par l'utilisateur.

La syntaxe d'un case en shell est :

case variable in
    pattern1)
        do
            ...
        ;;
    pattern2)
        do
            ...
        ;;
esac

add

Je voulais que l'on puisse ajouter plusieurs items à la liste d'un coup, mais aussi que l'on puisse ajouter des phrases :

$ todo add tomate banane salade

Donne :

1  [ ] tomate
2  [ ] banane
3  [ ] salade

Mais

$ todo add "tomate banane salade" 

Donne :

1  [ ] tomate banane salade

Cela est accomplit en itérant sur les arguments avec "$@" :

for X in "$@" 
    do
        ...
    done

X va prendre les valeurs des arguments, plusieurs mots séparés par des espaces comptent comme différents arguements, sauf s'il sont entre " ".

Comme un shift est réalisé dans la première partie du code, le premiers argument (add, remove, check...) à été suprimé et les indices des arguments suivants ont été décalés.

Je voulait aussi que la numérotation des items soit toujours croissante, même si des éléments ont étés supprimés. Pour cela il faut connaitre le numéro du dernier itém, mais simplement compter le nombre de lignes ne suffit pas.

Il faut donc extraire la dernière ligne du fichier :

LASTLINE=$(tail -n 1 $FILE)

Puis extraire le numéro :

NUMBER=$((${LASTLINE%% *} + 1))

Les syntaxes

${VAR%%séparateur}
${VAR##séparateur}

sont très pratiques et sont utilisés plusieurs fois dans ce script. La première signifie garder tout jusqu'au séparateurs et la deuxière garder tout après le séparateur. EX :
$ VAR="apple orange" 
$ echo "${VAR%% }" 
    apple
$ echo ${VAR## }
    orange

Il ne reste plus qu'a ajouter a nouvelle ligne à la fin du fichier :

echo "$NUMBER  [ ] $X" >> $FILE

ajoute la ligne à la fin du fichier alors que > écrase le contenu du fichier.

remove

Ceci est assez simple à implémenter grâce à grep. Notament avec le flag -v, qui renvoie uniquement les lignes qui n'ont pas de coincidence.

Avec cette simple implémentation on peut effacer les lignes en donnant leur numéro, une partie de l'item ou même effacer tous les éléments qui ont étés marqués par un X avec la commande check.

Le flag -w permet d'effacer la ligne qui si le mot entier est présent.

check et uncheck

Ces deux commandes utilisent l'option "substitute" de sed pour remplacer [ ] par [X] et vice versa.

renum