Table of Contents

Programmation Shell

Les différents shells peuvent avoir des fonctions sensiblement différentes voir incompatibles. Ce n'est pas le cas pour la majorité d'entre elles. Ici nous parlerons de bash car c'est le plus couramment utilisé.

Pour afficher la liste des options de bash il faut saisir bash -c “help set”. Pour lire un script sans exécuter les commandes (mode “dry-run) : bash -n.

Les mots d'une ligne suivants un # sont considérés comme des commentaires (ils ne sont pas interprétés) ; les commentaires multi-lignes sont encadrés de :' et '.

$ touch toto
$ echo !!
echo touch toto
touch toto

Les variables

En bash, chaque variable est précédée de $. On affecte un contenu ainsi : VAR=“toto” et on récupère le contenu en ajoutant $ devant.

NOM="robert"
echo "Salut, je suis le gros $NOM"

On peut les manipuler sans les déclarer proprement au préalable ; mais on peut être clean et les définir (declare VAR) ou les supprimer (unset VAR).

Pour déclarer une variable non modifiable = en readonly (une constante) :

readonly CONST="constante"
# ou
declare -r CONST="constante"

Par défaut une variable est globale et est vue dans le script (y compris ses fonctions) ; dans une fonction on peut déclarer des variables locales afin d'éviter les écrasements de variables globales non souhaités avec :

local VAR="pwet"

Les variables déclarées dans une fonction ont toujours une portée locale (elles sont détruites à la fin de la fonction), à moins qu'elles ne soient préalablement déclarées dans le script appelant.

Pour déclarer une constante locale à une fonction :

local -r CONST="constante locale"
 
# ou en 2 lignes
local CONST="constante locale"
readonly CONST
 
# marche aussi, puisque lorsque declare est invoqué dans une fonction,
#  la variable qui suit est déclarée localement
function test() {
  declare -r CONST="constante locale"
  }

C'est assez touffu je suis d'accord.

Tests de définition

Pour tester si une variable est non définie : ${VAR?message d'erreur} ou vide : ${VAR:?message d'erreur}.

unset VAR
${VAR?var non def}     # -bash: VAR: variable non def (sur stderr)
declare VAR
${VAR?var non def}     # -bash: VAR: variable non def (sur stderr)
VAR=
${VAR?var non def}     # (rien car VAR est définie)
${VAR:?vide}           # -bash: VAR: vide (sur stderr)

Pour utiliser une valeur par défaut au cas ou la variable n'est pas définie : ${VAR-non-def} ; on peut également vérifier si cette dernière est non nulle (non-vide) : ${VAR:-non-def}.

declare VAR
echo ${VAR-defaut}     # (vide car VAR est définie)
echo ${VAR:-defaut}    # vide
 
unset VAR
echo ${VAR-defaut}     # defaut
echo ${VAR:-defaut}    # defaut

Cela ne définit ni n'affecte VAR ; pour cela il faut utiliser la syntaxe ${VAR=defaut} et ${VAR:=defaut}.

declare VAR
echo ${VAR=defaut}     # (ne produit rien)
echo ${VAR:=defaut}    # defaut (VAR prend également cette valeur)

Typage

Il n'y a pas de typage strict en bash, toutes les variables sont considérées comme des chaînes de caractères, sur lesquelles on peut toutefois réaliser des opérations arithmétiques par exemple, si elles ne contiennent que des chiffres.

Pour “émuler” une variable booléenne par exemple, il convient de tester son contenu puisque c'est en réalité une string :

is_ready=true   # is_ready est une string qui vaut "true"
if [ "$is_ready" = true ]
   then echo "I'm ready"
   else echo "I'm not"
fi

Pour plus de détails sur le pseudo-typage en bash, voir la fonction declare.

Manipulation des variables

scp user@server:/tmp/fic.txt ${REP_LOCAL}_/

Sans les {}, l'interpréteur présumera que la variable se nomme $REP_LOCAL_ et non $REP_LOCAL.

VAR=undeuxtroisquatre
echo ${VAR:2}          # deuxtroisquatre
echo ${VAR:(-6)}       # quatre
echo ${VAR:(-6):2}     # qu
echo ${VAR::2}         # un
echo ${VAR::(-6)}      # undeuxtrois

On peut utiliser des motifs (qui en ont l'aspect mais ne sont pas exactement des expressions régulières) pour substituer des bouts de chaine :

VAR=undeuxtroisquatre
echo ${VAR#u*e}        # uxtroisquatre
echo ${VAR##u*e}       # (rien !)
echo ${VAR%u*e}        # undeuxtroisq
echo ${VAR/trois/3}    # undeux3quatre
echo ${VAR//e/E}       # undEuxtroisquatrE
echo ${VAR/#un/1}      # 1deuxtroisquatre
echo ${VAR/%q*/4}      # 1deuxtroisquatre

Exemple usuels :

source : https://www.patpro.net/blog/index.php/2006/04/07/20-manipulations-sur-les-variables-dans-bash/

Variables spécifiques

Ce sont des variables liées au contexte du script.

Les tableaux

Les enregistrements commencent à l'index 0. Ainsi pour récupérer le contenu de la 5ème case on doit utiliser l'index 4.

Comme pour les variables classiques, on peut récupérer la longueur en le précèdent de # :

Pour parcourir facilement un tableau dans une boucle :

ARRAY=( un deux trois )
for i in "${ARRAY[@]}"; do echo $i; done
un
deux
trois

Exemple de découpage d'une chaîne dans un tableau, avec comme séparateur ”;” :

CHAINE='17;8 23;9'
IFS=";"                       # définition du séparateur
read -a TAB <<<"$CHAINE"
echo "${#TAB[@]}"              # affiche de la taille du tableau (nombre de cellule)
 3
printf '%s\n' "${TAB[@]}"      # affichage du contenu du tableau cellule par cellule
 17
 8 23
 9

Tableau à 2 dimensions

Ça n'eût pas existé sous bash :). Mais on peut l'émuler en créant un tableau associatif (qui, à une clé fait correspondre un contenu), en codant la clé pour émuler 2 dimensions. Exemple : pour émuler un tableau de 2 x 3 cases :

# déclaration d'un tableau associatif T
declare -A T
# remplissage clé => valeur, par exemple la clé "0:0" => "homme"
# c'est un tableau à une dimension
T[0:0]=homme
T[0:1]=femme
T[1:0]=coq
T[1:1]=poule
T[2:0]=lion
T[2:1]=lionne
 
# calcul de l'index max du tableau : (HAUTEUR / 2) - 1 = 2
T_SIZE=$((${#T[@]}/2-1))
 
# en forgeant la clé, on émule un tableau bidimensionnel :
for ((i=0;i<=T_SIZE;i++)); do
   echo "ligne=$i ; case0=${T[$i:0]} ; case1=${T[$i:1]}"
done

Tableau associatif

Créer et utiliser un vrai tableau associatif :

declare -A TA=([cle1]=valeur1 ["cle 2"]="valeur 2" [cle3]=valeur3)
echo "${TA[cle1]}"
# valeur1
 
# Parcourir le tableau
for cle in "${!TA[@]}"; do
   echo "cle=$cle ; val=${TA[$cle]}"
done
#cle=cle 2 ; val=valeur 2
#cle=cle3 ; val=valeur3
#cle=cle1 ; val=valeur1

Pour connaitre la taille de ce tableau : ${#TA[@]} (3 dans cet exemple).

getopts : analyse des arguments d'un script

Cette commande permet de récupérer facilement les options (paramètres) avec lesquels a été lancé un script. Une option est un caractère précédé d'un + ou d'un - (à la différence d'un argument qui est “juste” une chaîne de caractère) ; getopts analyse les options une par une, et doit donc être utilisée dans un boucle. Exemple d'utilisation :

while getopts "ab:" opt
do
	echo "Option traitée : $opt"
	case $opt in
		a)
			echo "-a trouvée !"
			echo "prochain indice a traiter : $OPTIND"
			;;
		b)
			echo "-b trouvée ! son paramètre est : $OPTARG"
			echo "prochain indice a traiter : $OPTIND"
			;;
	esac
done

La même option peut apparaitre plusieurs fois sans générer d'erreur. Une option non attendue génèrera une erreur (non bloquante) et prendra la valeur “?”. Une option nécessitant un argument non présent génèrera aussi une erreur non bloquante, mais prendra la valeur “:”.

On peut choisir de traiter les erreurs dans le script en préfixant la liste des options par un “:” (while getopts “:ab:c:” opt) ; dans ce cas :

Reprise de l'exemple avec gestion manuelle des erreurs :

echo "### Liste des arguments = $*"
while getopts ":ab:c:" opt
do
	echo "Option traitée : $opt"
	case $opt in
		a)
			echo "-a trouvée !"
			echo "prochain indice a traiter : $OPTIND"
			;;
		b)
			echo "-b trouvée ! son paramètre est : $OPTARG"
			echo "prochain indice a traiter : $OPTIND"
			;;
		c)
			echo "-c trouvée ! son paramètre est : $OPTARG"
			echo "prochain indice a traiter : $OPTIND"
			;;
		\?) # on doit échapper "?" car c'est un caractère interprété par le shell
			echo "$OPTARG : option incorrecte !!"
			;;
		:) # ceci n'est pas un smiley
			echo "argument manquant pour l'option $OPTARG !!"
			;;
	esac
done

Pour récupérer les éventuels arguments restants, on utilise la commande shift qui permet de décaler (supprimer) les options déjà traitées de la liste des paramètres du script. Pour finir d'analyser ces arguments, ajouter ces lignes à la fin du script précédent :

shift $(($OPTIND-1)) # on supprime les options déjà traitées
 
echo "### Liste des arguments restants : $*"
while [ -n "$1" ]
do
	echo "encore un argument ! : $1"
	shift 1
done

Descripteurs de fichier

Il existe 3 descripteurs de fichier standards, utilisés par toutes les commandes : 0, 1 et 2 correspondants à STDIN, STDOUT et STDERR (input, output et erreur). 0 pointe par défaut sur le clavier, 1 et 2 sont affichés par défaut à l'écran.

Pour lire depuis un fichier plutôt que depuis le clavier :

echo toto > /tmp/fichier.txt
read VAR < /tmp/fichier.txt
echo $VAR
 toto

Les redirecteurs

Ces redirecteurs sont restaurés à leur valeur par défaut à chaque nouvelle ligne ; en revanche tous les processus fils héritent des descripteurs de leur processus père, à moins que celui-ci ne les ferme (voir plus bas).

| est aux flux ce que > est aux fichiers : il permet de rediriger le flux de sortie d'une commande vers l'entrée d'une autre commande.

Pour rediriger le flux d'erreur dans un fichier plutôt que l'afficher à l'écran, on réassigne 2 :

./prog 2>/tmp/prog_erreur.log

Pour rediriger le flux d'erreur sur STDOUT et donc l'envoyer dans le pipe :

./prog 2>&1 | ./prog2  # prog2 va recevoir en entrée les flux STDOUT et STDERR de prog1
# équivaut à
./prog |& ./prog2
# (depuis bash4, "|&" est reconnue comme abréviation de "2>&1 |")

Par défaut > écrase le fichier existant ; pour ajouter les erreurs à la fin du fichier, on utilise » :

./prog 2>>/tmp/prog_erreur.log

Ainsi on conserve les erreurs de chaque exécution du programme.

Heredoc

Un document “ici-même” (traduction française de qualitaÿ) est une manière de saisir une chaîne de caractères en conservant l'indentation et les sauts de ligne dans un interpréteur de commande. C'est utile pour les longs textes sur plusieurs lignes qui sont alors beaucoup plus lisibles dans le code. On précède ledit texte par un double chevron << suivi d'un délimiteur arbitraire puis par un saut de ligne ; la fin de la saisie du texte sera marquée par une ligne contenant uniquement le délimiteur (donc suivi d'un saut de ligne). Plus concrètement, si on choisit le délimiteur “EOF” :

cat << EOF
Ce   texte
 sera affiché tel quel
EOF

On peut utiliser la variante <<- pour ignorer les tabulations de début de ligne (cela permet d'intenter le code).

Herestring

Dans la même veine que le heredoc, la herestring permet d'envoyer une chaîne de caractères dans une commande ; il peut d'agir d'une variable :

VAR="dad text"
cat <<< $VAR
 dad text
 
bc <<< 2*2
 4

Descripteurs de fichier additionnels

On peut utiliser les descripteurs 3 et plus également, en les initialisant pour ne pas générer d'erreur (ils ne le sont pas par défaut) :

echo "toto" >&3       # erreur "Bad file descriptor"
echo "toto" 3>&1 >&3  # OK, va afficher le résultat sur 3 qui pointe sur 1 qui est la STDOUT
 toto

Pour fermer un descripteur de fichier (le 3 par ex) : 3>&-

On peut faire pointer un descripteur vers un fichier du système :

exec 3<> /tmp/foo   # ouverture du fd 3
echo toto >&3
exec 3>&-           # fermeture du fd

Exemple d'utilisation d'un descripteur de fichier sur un fichier : écrire un caractère au milieu de ce dernier (src : tldp.org).

#  An application of this is writing at a specified place in a file. 
echo 1234567890 > File    # Write string to "File".
exec 3<> File             # Open "File" and assign fd 3 to it.
read -n 4 <&3             # Read only 4 characters.
echo -n . >&3             # Write a decimal point there.
exec 3>&-                 # Close fd 3.
cat File                  # ==> 1234.67890

Les fonctions

function

Elles permettent de mutualiser le code afin qu'il soit appeler plusieurs fois sans avoir à le ré-écrire.

function mafonction() {
  echo "exécution de mafonction"
}
 
# appelle de la fonction, plus bas dans le code
mafonction

A l'intérieur d'une fonction on peut utiliser des variables locales en les déclarant local VAR=“”. Pour envoyer un code de retour on utilise return (pas exit comme dans le script principal, sinon on quitte le script !).

La variable bash $FUNCNAME permet d'afficher le nom de la fonction courante.

pipe ("|")

Le pipe (tube) est une fonction qui permet de chainer la sortie d'une première commande sur l'entrée de la seconde (c'est un tube/canal inter-processus).

echo "toto et tata" | awk '{$3}'
 tata

Le soucis c'est qu'on ne récupère que le code de retour de la dernière commande du pipe ; et dans l'exemple précédent, awk renvoie toujours 0. Pour résoudre ce soucis, on peut :

false | true
echo "${PIPESTATUS[@]}"
 1 0
$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

read

read toto permet de demander une saisie clavier à l'utilisateur qui sera enregistrée dans la variable $toto.

Si aucune variable n'est précisée, la saisie sera enregistrée dans la variable $REPLY par défaut.

Pour afficher un texte avant la saisie on utilise -p ; on peut récupérer plusieurs saisie d'un seul coup :

read -p "Quel est ton nom ?" NOM

Pour masquer la saisie, s'il s'agit d'un mot de passe par exemple, on utilise -s :

read -s -p "Et ton numéro de CB ?" CARD_NUMBER

Lire depuis un fichier

read < fichier.txt
 
# c'est plutôt utilisé dans une boucle while, pour le lire ligne par ligne :
while read line; do
  echo $line
done < fichier.txt

Pour lire mot par mot, si le fichier est formaté (par exemple s'il contient une liste de “nom prénom”) :

while read nom prenom; do
  echo "nom=$nom ; prenom=$prenom"
done < fichier.txt

Pour lire depuis une liste de fichiers :

while read line; do
  echo $line
done < <(find . -name foo* -print0)
 
# équivalent à :
find . -name foo* -print0 | while read line; do
  echo $line
done

Le premier exemple utilise une process substitution, qui permet de référencer l'entrée (>(list)) ou la sortie (<(list)) d'une commande comme un fichier* et non plus un flux ; le second un pipe, mais les 2 se valent.

* : en réalité bash utilise réellement un fichier temporaire, la preuve :

$ echo <(echo foo)
/dev/fd/63
 
$ cat <(echo foo)
foo
 
$ xargs echo < <(echo foo)
foo

Lire depuis une variable

read <<< $var

Seconde façon de faire, et plus adéquate si la variable contient des sauts de ligne :

ECHO="toto1 titi1\ntoto2 titi2"; echo -e "$ECHO" | while read a b; do echo "a=$a ; b=$b"; done
 a=toto1 ; b=titi1
 a=toto2 ; b=titi2

Expressions arithmétiques

Pour faire un calcul il faut l'encadrer de $((calcul)) ou $[calcul]

echo 1+1=$((1+1))

La fonction puissance s'écrit ** :

echo 2^5=$((2**5))

Pour incrémenter une variable : plusieurs possibilités :

Conversions

Convertir $VAR_HEXA de l'hexadecimal en décimal :

VAR_DECIMAL=$((16#$VAR_HEXA))
# ou, avec la commande let
let VAR_DECIMAL=0x$VAR_HEXA

Instructions conditionnelles

if

test expr

ou

if [ expr ]
  then
  else
fi
if [ -n "" ]; then echo "1"; else echo "0"; fi
  0

Conditions multiples

Pour tester des conditions multiples, on doit encadrer les tests avec un double crochet :

if [[ TRUE && TRUE || FALSE ]]
then
  echo "TRUE"
else
  echo "TRUE QUAND MÊME !"
fi

On peut également procéder ainsi :

if [ TRUE ] && [ TRUE ] || [ FALSE ]

Les conditions multiples sans parenthèses sont lues dans l'ordre d'apparition (de gauche à droite), ici cela équivaut à : (TRUE ET TRUE) OU FALSE. Donc TRUE quand même.

if [ expr ]
  then
elif
  then
elif
  then
[..]
else
fi

Expression sur les fichiers

Chaînes de caractères

[[ "la réponse est 42" =~ ^.*([0-9]{2}).*$ ]] && echo "${BASH_REMATCH[1]} est la réponse"
42 est la réponse

Nombres, comparaison

Comme il n'y a pas de typage en bash, pour tester si la valeur d'une variable est un nombre (entier) il faut réaliser le tester :

# Solution à base de REGEX
[[ $VAR =~ ^[0-9]+$ ]] || echo "VAR n'est pas un entier"
# NB : l'expression régulière ne doit pas être quotée,
#      ou doit être enregistrée dans une variable sinon ça ne matche pas
 
# "auto-test" numérique :)
[ $VAR -eq $VAR ] 2>/dev/null || echo "VAR n'est pas un entier"

Opérateurs

case

Test plusieurs valeurs de <chaîne> (~ifs imbriqués).

case <chaîne> in
 motif1)
    echo "motif 1"
    echo "C'est le premier !!"
    ;;
 motif2|motif3)
    echo "motif 2 ou motif 3"
    ;;
 motif*)
    echo "tous les autres motifs"
    ;;
 *)
    echo "Tout le reste"
    ;;
esac

for

Permet de créer une boucle déterministe : pour chaque valeur de i, on exécute la boucle.

Exemples :

for (( i = 1; i <= 5; i++ ))
  do
  echo -n "$i "
done

Affichera : 1 2 3 4 5 .

Cela équivaut à : for i in {1..5} qui est une autre façon simplifiée d'incrémenter i de 1 à 5. Cette méthode ne permet pas d'utiliser des variables ; pour cela il faut utiliser l'expression longue :

DEBUT=1
FIN=5
# ne fonctionne pas : for i in {$DEBUT..$FIN}
# fonctionne :
for (( i = $DEBUT; i <= $FIN; i++ ))

On peut spécifier l'incrément qui vaut par défaut 1, avec : for i in {1..5..2} (ici incrément de 2).

On peut aussi lui fournir une liste de mots :

for i in serveur1 serveur2 serveur3
do
   ssh $i "(uname -r)"
done

On peut l'exécuter sur une seule ligne de commande en respectant cette syntaxe :

for i in serveur1 serveur2 serveur3; do ssh $i "(uname -r)"; done

break

Pour sortir prématurément d'une boucle for, while ou repeat, on utilise le mot-clé break :

for i in {1..5}
do
   if [ $i -eq 3 ]
   then
      echo "Huston nous avons un probleme !"
      break
   fi
   echo $i
done

Ce qui donne :

1
2
Huston nous avons un probleme !

continue

Pour sortir de l'occurrence courante de la boucle, on utilise continue. On quitte l'occurrence courante et on passe à la suivante :

for i in {1..5}
do
   if [ $i -eq 3 ]
   then
      echo "Huston nous avons un probleme !"
      continue
   fi
   echo $i
done

Ce qui donne :

1
2
Huston nous avons un probleme !
4
5

while

Exécute la boucle tant que la condition suivant l'instruction while est remplie :

exemple : compter de 1 à 5…

cpt=1
while [ $cpt -le 5 ]; do
   echo $cpt
   (( cpt++ ))
done

exemple2 : lire un fichier ligne par ligne

while read line
do
  echo $line
done < fic.txt

La même mais avec la sortie d'une commande :

cat fic.txt | while read line
do
  echo $line
done

ou sur une seule ligne :

while read n; do echo $line; done < fic.txt

until

Exécute la boucle jusqu'à ce que la condition soit remplie :

until <condition>
do
   echo "Toujours pas !"
done

select

Permet de créer un menu interactif, c'est à dire une saisie utilisateur ;

PS3="Prompt du menu"
select i in {2..5}
do
    # $REPLY contient l'index de la réponse ;
    # $i contient la valeur correspondante
    echo $REPLY-$i
 
    # pour sortir du menu (car il s'agit d'une boucle)
    break
done

Divers

debug

Pour débugger un script, on insère en début de script la commande set -x.

en vrac

rsh -4 -n $serveur $commande >/dev/null 2>&1
ping -c3 -w 0.05 soekris-${cpt} >/dev/null 2>&1
retour=$?
trap "echo 'Exiting..' ; exit 0" 30

Substitution de chaine

Pour n'afficher qu'une partie d'une chaine (une sous-chaine donc), on utilise la syntaxe ${tst:<offset>:<length>}. L'offset, c'est l'origine (elle commence à 0 pour le premier caractère, comme souvent en informatique), et length c'est le longueur de la chaine affichée (facultative si on veut afficher jusqu'à la fin).

Par exemple :

TEST="Je suis chanceux"
 
# on affiche la sous-chaine à partir du caractère n°3 (soit le 4ème caractère) :
echo ${TEST:3}
suis chanceux
 
# idem, mais on se limite à 4 caractères affichés :
echo ${TEST:3:4}
suis
 
# on peut spécifier un offset négatif pour compter l'offset à partir de la fin de la chaîne
# cependant il faudra ajouter un espace avant le "-" (ou l'encadrer de parenthèses)
echo ${TEST: -8}
chanceux
echo ${TEST:(-8)}
chanceux

Remplacement de chaine

Syntaxe : ${<parameter>/<pattern>/<string>}

<parameter> c'est la chaine d'origine, <pattern> l'expression de la chaine recherchée, et <string> la chaine de remplacement.

<pattern> peut commencer par ces caractères spéciaux (à défaut, il ne remplace que la première occurrence rencontrée) :

TEST="toto titi tata"
 
# remplacer tous les "a" par des "i"
echo ${TEST//a/i}
toto titi titi
 
# ne remplacer que "a" de fin de chaîne
echo ${TEST/%a/i}
toto titi tati
 
# ne remplacer que le premier "t", par un "p"
echo ${TEST/#t/p}
poto titi tata

Modifier la casse

Pour modifier la casse, on utilise la syntaxe :

<pattern> étant optionnel, auquel cas seule la première lettre sera prise en compte :

TEST="toto TITI tata"
 
echo ${TEST^}
Toto TITI tata

On peut doubler les lettres pour traiter toute la chaine de caractère :

echo ${TEST^^}
TOTO TITI TATA
 
echo ${TEST,,}
toto titi tata

Enfin, on peut spécifier la pattern à la suite :

# on UPPERCASE tous les "t"
echo ${TEST^^t}
ToTo TITI TaTa
 
# on UPPERCASE les "t" et les "a"
echo ${TEST^^[ta]}
ToTo TITI TATA

Préfixes et suffixes

Syntaxe : ${<parameter>#<word>} pour les préfixes, ${<parameter>%<word>} pour les suffixes, ce qui va supprimer la plus courte occurrence de la chaîne. Pour supprimer la plus longue occurrence, on utilisera respectivement ## et %%.

On s'en sert souvent pour manipuler les chemins et les noms de fichier :

FIC=/chemin/ve.rs/mon/fichier.ext
 
# afficher uniquement son extension
# (on supprime le plus long préfixe se terminant par ".")
echo ${FIC##*.}
fichier.ext
 
# afficher le chemin du fichier :
# (on supprime le suffixe le plus court contenant un "/")
echo ${FIC%/*}
/chemin/ve.rs/mon

trim (suppression des espaces dans une chaîne)

bash ne possède pas de fonction prédéfinie pour “trimmer” les chaînes de caractère. trim est une fonction, habituellement incluse dans les langages de programmation, permettant de supprimer les espaces parasites avant et après une chaîne : par exemple “ cette chaîne” ou “ celle-ci ”.

Pour faire cette office il existe plusieurs possibilités d'afficher les cellules du tableau suivant sans les espaces :

arr=('foo bar' 'test@domain.com ' \
' test@domain.com ' '    test@domain.com         ')
shopt -s extglob                     # activation des extended glob
# (pour reconnaître [[:blank:]] qui définit l'ensemble des espaces et tabulations)
arr=( "${arr[@]/#+([[:blank:]])/}" ) # suppression des espaces de début de chaîne
arr=( "${arr[@]/%+([[:blank:]])/}" ) # suppression des espaces de fin de chaîne
function trim() {
	mapfile -t t_input<<<"$*"    # place chaque paramètre dans une cellule du tableau (un par ligne)
	[[ ${#t_input[@]} -eq 0 ]] && { echo "Aucun texte à afficher"; exit 1; }
	set -- ${t_input[@]}         # affecte chaque case du tableau à un paramètre ($1, $2, etc)
	printf '%s\n' "$*"           # affiche chaque paramètre suivi d'un saut de ligne
}
trim "${arr[@]}"

Ressources