Bash

Published: 02-09-2015

Updated: 04-09-2018

By: Maxime de Roucy

tags: bash shell zsh

Scripting

if

Par ce que j’oublie tous le temps :

if test …
then
	…
elif test …
then
	…
elif …
then
	…
else
	…
fi

Parameter Expansion

Sources :

variables non définies

Ces syntaxes permettent de traiter le cas des variables non definies.

Exemples :

max@laptop % unset a
max@laptop % b=c
max@laptop % echo ${a:-b}
b
max@laptop % echo ${a:-$b}
c
max@laptop % echo $a

max@laptop % echo ${a:+x}

max@laptop % echo ${b:+x}
x
max@laptop % echo $a

max@laptop % echo ${a:=b}
b
max@laptop % echo $a
b
max@laptop % echo ${a:=$b}
b
max@laptop % unset a
max@laptop % echo ${a:=$b}
c
max@laptop % echo $a
c

Vérifier qu’une variable est set

Sources :

Pour vérifier qu’une variable est set en bash on peut donc utiliser la syntax suivante :

if test -n "${variable:+x}"
then
	# variable est set
else
	# variable est unset
fi

chaines de caractères

Quelques exemples de « Parameter Expansion » permettant de traité des chaines de caractères.

Exemples :

max@laptop $ a="bbb-ccc"
max@laptop $ echo "${a#*b}"
bb-ccc
max@laptop $ echo "${a##*b}"
-ccc
max@laptop $ echo "${a%c}"
bbb-cc
max@laptop $ echo "${a%%c*}"
bbb-
max@laptop $ echo "${a/-/@}"
bbb@ccc
max@laptop $ echo "${a/-}"
bbbccc
max@laptop $ echo "${a//b/d}"
ddd-ccc
max@laptop $ echo "${a//b}"
-ccc
max@laptop $ echo "${a^}"
Bbb-ccc
max@laptop $ echo "${a^c}"
bbb-ccc
max@laptop $ echo "${a^b}"
Bbb-ccc
max@laptop $ echo "${a^^}"
BBB-CCC
max@laptop $ echo "${a^^c}"
bbb-CCC
max@laptop $ A="${a^^}"
max@laptop $ echo "${A,}"
bBB-CCC
max@laptop $ echo "${A,,}"
bbb-ccc
max@laptop $ echo "${A,,C}"
BBB-ccc

autres

max@laptop $ a=b
max@laptop $ b=c
max@laptop $ echo "${!a}"
c

deux-points « : »

Source :

: est un builtin qui ne fait rien sinon interpreter ses arguments. Les argument ne sont pas executé mais interprétés. Ça ne sert pas à grand chose…

max@laptop % a=1
max@laptop % : $((a += 1))
max@laptop % echo $a
2

set

Source : The Set Builtin sur www.gnu.org

set -e -o pipefail -u -x

Pour réécrire les arguments d’un script shell :

max@laptop % cat /tmp/test.sh
#!/bin/bash

set -u

echo $1
echo $2

set -- titi

echo $1
echo $2
max@laptop % bash /tmp/test.sh toto tata
toto
tata
titi
/tmp/test.sh: ligne 11: $2 : variable sans liaison

set -e

Quitte à la première erreur… en fait c’est un peu plus complexe que ça.

Le manuel est assez claire :

Exit immediately if a pipeline, which may consist of a single simple command, a list, or a compound command returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !. If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit. A trap on ERR, if set, is executed before the shell exits.
This option applies to the shell environment and each subshell environment separately, and may cause subshells to exit before executing all the commands in the subshell.
If a compound command or shell function executes in a context where -e is being ignored, none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status. If a compound command or shell function sets -e while executing in a context where -e is ignored, that setting will not have any effect until the compound command or the command containing the function call completes.

Point interressant : si la commande contient un « && » ou un « || » seul l’exit code du dernier élément de la ligne est pris en compte. Si cette éléments n’est pas exécuté, alors la ligne ne peut pas faire quitté le script. Idem avec les pipes.

max@laptop % false
→ quit
max@laptop % true && false
→ quit
max@laptop % false && true
→ don't quit
max@laptop % false | cat
→ don't quit

Par défaut, l’exit status d’une ligne contenant un ou plusieurs pipe correspond à l’exit status de la commande la plus à droite. Avec -o pipefail, l’exit status de la ligne correspond à l’exit status de la commande en échec la plus à gauche.

max@laptop % bash -e
max@laptop $ false | echo toto
toto
max@laptop $ exit
max@laptop % bash -e -o pipefail
max@laptop $ false | echo toto
toto
max@laptop %

Attention cependant, même avec pipefail les erreurs contenu dans le in d’une boucle for ne sont pas pris en compte :

max@laptop % bash -e -o pipefail
max@laptop $ for i in $(false); do echo $i; done
max@laptop $

Ce qu’il faut faire pour contourner ce problème :

max@laptop % bash -e -o pipefail
max@laptop $ i_list=$(false) ; for i in $i_list; do echo $i; done
max@laptop %

trap

Pour effacer les fichier temporaire (mktemp) utilisé dans mes script j’utilise trap. Le premier argument de trap est la commande qui sera exécutée à la sorti du script. Elle doit être placée entre apostrophe « ’ ». Pour certaines variables qui pourrait contenir des espaces il est recommandé d’ajouter des guillemets en protection.

trap '"$HOME" …' EXIT

La deuxième invocation de trap, portant sur le même signal, écrase la première. Dans l’exemple suivant seul B sera supprimé.

trap 'rm A' EXIT
trap 'rm B' EXIT

Il vaut mieux utiliser une variable.

avec tableau

Dans l’exemple suivant on est sure que «a» et «b» seront toujours supprimé (attention au noms de fichier avec espace).

# création du tableau "suppr"
declare -a suppr
trap 'rm -f "${suppr[@]-}"' EXIT
touch a && suppr+=("a")
touch 'b c' && suppr+=("b c")
d=`mktemp` && suppr+=("$d")

On utilise ${suppr[@]-} et non ${suppr[@]} car même après un declare un tableau vide est considéré comme unset.

An array variable is considered set if a subscript has been assigned a value. The null string is a valid value.

sans tableau

Une autre méthode, n’utilisant pas de tableau, moins pertinente et plus risquée selon moi :

suppr=""
trap 'eval rm -f $suppr' EXIT
touch a && suppr+=" a"
touch 'b c' && suppr+=" 'b c'"
d=`mktemp` && suppr+=" $d"

Dans cette deuxième méthode :

plusieurs commandes

On ne peut pas modifier un trap existant, on peut seulement le remplacer par un autre (cf. section trap).

Pour pouvoir exécuter, via trap, plusieurs commande à la sortie d’un script on peut :

script de trap
max@mde-oxalide % cat test.sh
#!/usr/bin/bash

set -e -x

TRAP_SCRIPT=`mktemp`
# $TRAP_SCRIPT doit au moins avoir 1 ligne, sinon les `sed -i "1i…` ne passeront pas (ils ne modifierons pas le fichier)
echo "rm $TRAP_SCRIPT" > $TRAP_SCRIPT
# `set +e` pour faire en sorte que $TRAP_SCRIPT soit executé jusqu'a la fin
# même en cas d'erreur d'une dés commandes qu'il contient
trap 'set +e; source $TRAP_SCRIPT; echo "$(date) - script finished"' EXIT

touch "toto"
sed -i "1irm -f 'toto'" $TRAP_SCRIPT

sed -i "1iecho '1'\n\
        echo '2'\n\
        echo '3'" $TRAP_SCRIPT

sed -i '1iecho "4"\
        echo "5"\
        echo "6"' $TRAP_SCRIPT
max@mde-oxalide % bash test.sh
++ mktemp
+ TRAP_SCRIPT=/tmp/tmp.tmTtgOonVB
+ echo 'rm /tmp/tmp.tmTtgOonVB'
+ trap 'set +e; source $TRAP_SCRIPT; echo "$(date) - script finished"' EXIT
+ touch toto
+ sed -i '1irm -f '\''toto'\''' /tmp/tmp.tmTtgOonVB
+ sed -i '1iecho '\''1'\''\n    echo '\''2'\''\n        echo '\''3'\''' /tmp/tmp.tmTtgOonVB
+ sed -i '1iecho "4"\
        echo "5"\
        echo "6"' /tmp/tmp.tmTtgOonVB
+ set +e
+ source /tmp/tmp.tmTtgOonVB
++ echo 4
4
++ echo 5
5
++ echo 6
6
++ echo 1
1
++ echo 2
2
++ echo 3
3
++ rm -f toto
++ rm /tmp/tmp.tmTtgOonVB
++ date
+ echo 'mar. sept.  4 15:15:20 CEST 2018 - script finished'
mar. sept.  4 15:15:20 CEST 2018 - script finished

signal

Le deuxième argument de trap spécifie le signal qui doit déclencher la commande. J’utilise :

Exemple :

max@laptop $ trap 'echo "err"' ERR
max@laptop $ trap 'echo "exit"' EXIT
max@laptop $ true
max@laptop $ false
err
max@laptop $ set -e
max@laptop $ false
err
exit

case

La documentation officiel de case est bien faite, mais il manque quelques subtilités selon moi.

case $test in
	a)
		echo "a"
		;;
	b)
		echo "b"
		;;
	*)
		echo "other"
		;;
esac

Quelques exemple standard :

max@laptop $ test="a"; case $test in a) echo "a";; b) echo "b";; *) echo "other";; esac
a
max@laptop $ test="eruset"; case $test in a) echo "a";; b) echo "b";; *) echo "other";; esac
other
max@laptop $ test="aaa"; case $test in a) echo "a"; test="b";; b) echo "b";; *) echo "other";; esac
other
max@laptop $ test="a"; case $test in a|b) echo "a|b";; *) echo "other";; esac
a|b
max@laptop $ test="b"; case $test in a|b) echo "a|b";; *) echo "other";; esac
a|b
max@laptop $ test="ccc aa bb ccc"; case $test in *aa bb*) echo "a";; *) echo "other";; esac
bash: erreur de syntaxe près du symbole inattendu « bb* »
max@laptop $ test="ccc aa bb ccc"; case $test in '*aa bb*') echo "a";; *) echo "other";; esac
other
max@laptop $ test="ccc aa bb ccc"; case $test in *'aa bb'*) echo "a";; *) echo "other";; esac
a
max@laptop $ test="ccc aa bb ccc"; case $test in *aa\ bb*) echo "a";; *) echo "other";; esac
a

Par défaut la recherche est « case sensitive », on peut changer ça avec shopt.

max@laptop $ shopt -u nocasematch
max@laptop $ shopt nocasematch
nocasematch     off
max@laptop $ test="A"; case $test in a) echo "a";; *) echo "other";; esac
other
max@laptop $ shopt -s nocasematch
max@laptop $ shopt nocasematch
nocasematch     on
max@laptop $ test="A"; case $test in a) echo "a";; *) echo "other";; esac
a

;& permet de poursuivre un bloque avec le bloque suivant.

max@laptop $ test="a"; case $test in a) echo "a";& b) echo "b";; *) echo "other";; esac
a
b

;;& permet de poursuivre avec le premier bloque suivant qui match. Attention, même si on change la variable testé dans un des bloque ça n’est pas pris en compte dans les tests (cf. dernier exemple).

max@laptop $ test="a"; case $test in a) echo "a";;& b) echo "b";; *) echo "other";; esac
a
other
max@laptop $ test="a"; case $test in a) echo "a";;& a) echo "a2";; *) echo "other";; esac
a
a2
max@laptop $ test="a"; case $test in a) echo "a"; test="b";;& b) echo "b";; *) echo "other, pourtant test est bien égal à $test";; esac
a
other, pourtant test est bien égal à b

bracket

( … )
la commande est lancée dans un sub-shell
{ … ;}
la commande est lancée dans le shell courant

for

max@laptop $ for i in "a" "b c"; do echo $i; done
a
b c
max@laptop $ test=("a" "b c")
max@laptop $ for i in "${test[@]}"; do echo $i; done
a
b c
max@laptop $ for i in `seq -w 01 03`; do echo $i; done
01
02
03

Sources :

flock

La commande flock permet de poser un lock (en écriture ou lecture) sur un fichier ou un dossier. Ça permet de gérer, de façon minimaliste, la concurence entre script shell (un peut comme un mutex/sémaphore).

tty

cat … | while read a
do
	read b
done

read b ne peut pas fonctionner car il sont fd0 correspond au pipe. Pour résoudre se problème on peut utiliser les formes suivantes :

… | while read a
do
	read b < /dev/tty
done

# ou

exec 3< <( … )
while read -u 3 a; do
	read b
done
exec 3>&-

Source : Fixing stdin inside a redirected loop…

tableau

Sources :

${tableau[@]}
référence le tableau
${tableau[*]}
tous les éléments du tableau (en une seul string); équivalent de $tableau en zsh
${#tableau[@]}
nombre d’éléments dans le tableau
${tableau[@]:2}
référence les éléments du tableau à partir de l’index 2 (troisième position)
${tableau[@]:2:3}
référence 3 éléments du tableau, à partir de l’index 2 (troisième position) inclue. > In this example, ${Unix[@]:0:$pos} will give you 3 elements starting from 0th index i.e 0,1,2 and ${Unix[@]:4} will give the elements from 4th index to the last index. And merge both the above output. This is one of the workaround to remove an element from an array.

Exemples zsh :

max@laptop % tableau=("a" "b c" "d")
max@laptop % echo $tableau
a b c d
max@laptop % echo ${tableau[@]}
a b c d
max@laptop % for i in "$tableau"; do echo $i; done
a b c d
max@laptop % for i in "${tableau[@]}"; do echo $i; done
a
b c
d
max@laptop % echo "${tableau[@]:0:1}"
a
max@laptop % echo "${tableau[@]:0:2}"
a b c
max@laptop % echo "${tableau[@]:1:2}"
b c d
max@laptop % echo "${tableau[@]:2}"
d

Exemples bash :

max@laptop $ tableau=("a" "b c" "d")
max@laptop $ echo $tableau
a
max@laptop $ echo "${tableau[*]}"
a b c d
max@laptop $ echo ${tableau[@]}
a b c d
max@laptop $ for i in "$tableau"; do echo $i; done
a
max@laptop $ for i in "${tableau[*]}"; do echo $i; done
a b c d
max@laptop $ for i in "${tableau[@]}"; do echo $i; done
a
b c
d
max@laptop $ echo "${tableau[@]:0:1}"
a
max@laptop $ echo "${tableau[@]:0:2}"
a b c
max@laptop $ echo "${tableau[@]:1:2}"
b c d
max@laptop $ echo "${tableau[@]:2}"
d

string → array

bash

Il est possible de transformer une string en array via les builtin readarray et read.

Si le séparateur est un retour à la ligne :

max@laptop $ string="a
b c"
max@laptop $ declare -p string
declare -- string="a
b c"
max@laptop $ readarray -t array <<< "$string"
max@laptop $ declare -p array
declare -a array=([0]="a" [1]="b c")

max@laptop $ readarray -t array < <(echo -e "d\ne f")
max@laptop $ declare -p array
declare -a array=([0]="d" [1]="e f")

Si le séparateur est un autre caractère, ici l’espace :

max@laptop $ string="a b c"
max@laptop $ IFS=' ' read -a array <<< "$string"
max@laptop $ declare -p array
declare -a array=([0]="a" [1]="b" [2]="c")

Quelques truc que j’ai essayé mais qui ne fonctionne pas ou mal :

max@laptop $ declare -p string
declare -- string="a
b c"
max@laptop $ readarray array <<< "$string"
max@laptop $ declare -p array
declare -a array=([0]=$'a\n' [1]=$'b c\n')
max@laptop $ string="a
b c"
max@laptop $ readarray -d ' ' array <<< "$string"
max@laptop $ declare -p array
declare -a array=([0]="a " [1]="b " [2]=$'c\n')
max@laptop $ readarray -d ' ' -t array <<< "$string"
max@laptop $ declare -p array
declare -a array=([0]="a" [1]="b" [2]=$'c\n')
max@laptop $ read -d ' ' -a array <<< "$string"
max@laptop $ declare -p array
declare -a array=([0]="a")

zsh

Il est possible de transformer une string en array via les Parameter Expansion Flags.

max@laptop % test="a
dquote> b c"
max@laptop % declare test
test='a
b c'
max@laptop % test=( "${(f)test}" )
max@laptop % declare test
test=( a 'b c' )

variable d’environnement

Je n’aime pas ce terme car selon moi il a plusieurs signification.

Dans un shell, les variables créé via = ou set sont appelées « variable shell ».

environnement du shell

L’environnement du shell (le processus) est accessible via la commande suivante.

strings /proc/$$/environ

Un lancement du shell, celui-ci créé une variable shell pour chaque élément de cette environnement. Ses variable shell sont parfois appelé « variable d’environnement ».

Ces variables sont taguées pour être exportées dans l’environnement des processus enfants (cf. ci-dessous). Il est possible de les modifier, en revanche cela ne modifie pas l’environnement du shell courant. Il n’est pas possible de changer l’environnement d’un processus.

max@laptop % strings /proc/$$/environ | grep SSH_AGENT_PID
SSH_AGENT_PID=906
max@laptop % env | grep SSH_AGENT_PID
SSH_AGENT_PID=906
max@laptop % SSH_AGENT_PID=12345
max@laptop % strings /proc/$$/environ | grep SSH_AGENT_PID
SSH_AGENT_PID=906
max@laptop % env | grep SSH_AGENT_PID
SSH_AGENT_PID=12345

environnement des enfants

export et set -x permettent de taguer un variable shell pour faire partie de l’environnement des processus enfants. Ces variables taguer sont parfois appelée « variable d’environnement ». export ou export -p permette d’obtenir la liste de ces variable et leurs valeurs.

env permet de modifier de façon unitaire l’environnement du processus enfant. D’une façon détournée, il permet d’obtenir la liste des variables exportées. Dans la commande suivante, le processus enfant est démarré avec un environnement ne contenant que « TEST=toto ».

env -i TEST=toto enfant

Il est possible de dé-exporter une variable via typeset +x … (uniquement en bash : export -n).

TEST1=1
# variable shell (non exportée)
export TEST1
# variable shell exportée
typeset +x TEST1
# ou `export -n TEST1` en bash
# variable shell (non exportée)
export TEST2="truc"
# variable shell exportée
unset TEST2
# variable shell non défini et donc non exportée

Debug

Pour débugger un script bash, placer ces instruction en débug de fichier :

set -x
trap read DEBUG
mode debug

Le script s’arrêtera après chaque commande.

Exemples

manipulation d’arguments

Ce script passe ses propre arguments à nc, excepté le dernier et l’avant dernier, qu’il utilise comme :

Au final ce script permet de tester en TCP, si un service distant répond bien ce qu’il est censé répondre. Par exemple, ./mon_script toto 123 ping pong enverra “ping” sur le port “123” de la machine “toto” et vérifiera que la réponse contient bien “pong”.

#!/bin/bash

set -e -u -o pipefail

nargs="$#"
args=("$@")
nc "${args[@]:0:nargs-2}" <<< "${args[nargs-2]}" | grep -q "${args[nargs-1]}"

backup

Le script de backup de mon serveur

#!/bin/bash

set -e -o pipefail -u -x

declare -a suppr
trap 'rm -f "${suppr[@]}' EXIT

today=`date '+%Y-%m-%d'`
nb_jour_depuis_dimanche=`date '+%u'`

# https://btrfs.wiki.kernel.org/index.php/Incremental_Backup

# btrfs-send-receive src_dir dst_dir snapshost_name
function btrfs-send-receive-clean {
	src_dir=$1
	dst_dir=$2
	snapshost_name=$3

	test ! -d "${src_dir}/${snapshost_name}-backup.old"
	if test -d "${dst_dir}/${snapshost_name}-${today}"
	then
		return
	fi

	if test ! -d "${src_dir}/${snapshost_name}-backup"
	then
		btrfs subvolume snapshot -r "${src_dir}/${snapshost_name}" "${src_dir}/${snapshost_name}-backup"
		sync
		btrfs send "${src_dir}/${snapshost_name}-backup" | btrfs receive "$dst_dir"
		mv "${dst_dir}/${snapshost_name}-backup" "${dst_dir}/${snapshost_name}-${today}"
	else
		mv "${src_dir}/${snapshost_name}-backup" "${src_dir}/${snapshost_name}-backup.old"
		btrfs subvolume snapshot -r "${src_dir}/${snapshost_name}" "${src_dir}/${snapshost_name}-backup"
		sync
		btrfs send -p "${src_dir}/${snapshost_name}-backup.old" "${src_dir}/${snapshost_name}-backup" | btrfs receive "$dst_dir"

		btrfs subvolume delete "${src_dir}/${snapshost_name}-backup.old"

		mv "${dst_dir}/${snapshost_name}-backup" "${dst_dir}/${snapshost_name}-${today}"

		# supprime tt les backup qui ont plus de 7 jours
		# excepté ceux correspondant au 4 derniers dimanches

		# on ne peut pas trier par date de modification/change/access
		# le backup ne change aucun de ces trois éléments et il n'est pas possible de faire un "touch" car
		# ils sont en read-only

		dont_delete=`mktemp`
		suppr+=("$dont_delete")
		cat <(seq 0 $nb_jour_depuis_dimanche) <(seq $nb_jour_depuis_dimanche 7 28) | xargs -I @ date -d 'now - @ days' '+%Y-%m-%d' > $dont_delete
		sed -i "s/^/${snapshost_name}-/" $dont_delete
		sort -u $dont_delete | sponge $dont_delete

		list_dirs=`mktemp`
		suppr+=("$list_dirs")
		cd "${dst_dir}"
		ls --directory "${snapshost_name}-"* > $list_dirs

		comm -23 $list_dirs $dont_delete | xargs -I @ btrfs subvolume delete "${dst_dir}/@"
	fi
}

btrfs-send-receive-clean /mnt/sda /mnt/ddext/server-backup gentoo
btrfs-send-receive-clean /mnt/sdb /mnt/ddext/server-backup ftp
btrfs-send-receive-clean /mnt/sdb /mnt/ddext/server-backup rsync

## backup du laptop
## il faut que /mnt/ddext/laptop-backup/documents soit déjà créé et soit un subvolume
if ping -c 1 -q laptop.lan &> /dev/null
then
	max_laptop_fqdn="laptop.lan"
elif ping -c 1 -q wifi-laptop.lan &> /dev/null
then
	max_laptop_fqdn="wifi-laptop.lan"
fi

if test -n "${max_laptop_fqdn:+x}"
then
	dst_dir=/mnt/ddext/laptop-backup
	dst_name=documents
	if test -d "${dst_dir}/${dst_name}-${today}"
	then
		exit 0
	fi
	rsync --archive --delete --password-file="/etc/rsync_max_documents.secret" rsync://max@${max_laptop_fqdn}/max_documents "${dst_dir}/${dst_name}"
	# ici je me permet d'utiliser touch car je ne modifie pas la source mais la copie
	touch "${dst_dir}/${dst_name}"
	btrfs subvolume snapshot -r "${dst_dir}/${dst_name}" "${dst_dir}/${dst_name}-${today}"
	# grace au touch précédent on peut utiliser la date de dernière modification
	find ${dst_dir} -mindepth 1 -maxdepth 1 -name "${dst_name}-*" -mtime +7 $(seq -s ' ' -f "-not -mtime %g" $nb_jour_depuis_dimanche 7 28) -exec btrfs subvolume delete '{}' \;
fi

## backup du laptop oxalide
## il faut que /mnt/ddext/oxalide-backup/personnel soit déjà créé et soit un subvolume
if ping -c 1 -q mde-oxalide.lan &> /dev/null
then
	dst_dir=/mnt/ddext/oxalide-backup
	dst_name=personnel
	if test -d "${dst_dir}/${dst_name}-${today}"
	then
		exit 0
	fi
	rsync --archive --delete -e "ssh -i ~rsync/.ssh/id_ed25519" max@mde-oxalide.lan: "${dst_dir}/${dst_name}"
	# ici je me permet d'utiliser touch car je ne modifie pas la source mais la copie
	touch "${dst_dir}/${dst_name}"
	btrfs subvolume snapshot -r "${dst_dir}/${dst_name}" "${dst_dir}/${dst_name}-${today}"
	# grace au touch précédent on peut utiliser la date de dernière modification
	find ${dst_dir} -mindepth 1 -maxdepth 1 -name "${dst_name}-*" -mtime +7 $(seq -s ' ' -f "-not -mtime %g" $nb_jour_depuis_dimanche 7 28) -exec btrfs subvolume delete '{}' \;
fi

Le timer et le service systemd correspondant.

root@server # systemctl cat backup.timer
# /etc/systemd/system/backup.timer
[Unit]
Description=backup sda & sdb to ddext

[Timer]
OnCalendar=*-*-* 01:30:00

[Install]
WantedBy=timers.target

root@server # systemctl cat backup.service
# /etc/systemd/system/backup.service
[Unit]
Description=backup sda & sdb to ddext

OnFailure=cron-failure@%p.service

[Service]
Type=oneshot
ExecStart=/usr/bin/chronic /usr/local/bin/backup-service.sh

mysqldump, pipe et ssh

Dans un environement de quatre machines :

Ce script créé un tunnel ssh entre machine-local et serveur-de-rebond tel que tous les paquets envoyés sur le port 12345 sur machine-local passe dans le tunnel et sont envoyé sur bdd-destination sur le port 3306 à partir de serveur-de-rebond. Il fait ensuite transiter les données sql de bdd-source via ce tunnel.

#!/bin/bash
set -e -o pipefail -u

# ./script user-bdd-source nom-bdd-source user-bdd-destination nom-bdd-destination

trap_handler () {
	true
}
trap trap_handler EXIT

tmp_directory=$(mktemp -d)
trap_handler () {
	rm -rf $tmp_directory
}

ssh_ctrl=$tmp_directory/mysql-bdd-backup-ssh
ssh -C -o ExitOnForwardFailure=yes -o ControlMaster=yes -nNfS $ssh_ctrl -L 12345:bdd-destination:3306 serveur-de-rebond
trap_handler () {
	ssh -S $ssh_ctrl -O exit truc
	rm -rf $tmp_directory
}

read -s -p 'password bdd-01 : ' password
mkfifo -m 600 $tmp_directory/.my.cnf.1
echo -e "[client]\nuser=$1\npassword=$password" > $tmp_directory/.my.cnf.1 &
echo

read -s -p 'password bdd-02 : ' password
mkfifo -m 600 $tmp_directory/.my.cnf.2
echo -e "[client]\nuser=$3\npassword=$password" > $tmp_directory/.my.cnf.2 &

echo 'doing dump, please wait'
mysqldump --defaults-file=/tmp/.my.cnf.1 -h bdd-source $2 | mysql --defaults-file=/tmp/.my.cnf.2 -h 127.0.0.1 -P 12345 $4

La commande de création du tunnel ouvre une socket mysql-bdd-backup-ssh qui permet de contrôler ssh, ici pour le terminer et fermer le tunnel. Le script demande ensuite les mots de passe des deux bdd et les place dans des pipes només. Dès que les binaires mysqldump et mysql lise ces pipes leur contenu disparait. Le temps pendant lequel les mots de passe sont stoqué dans les pipes est donc très court.

chronic like

Dans un environment sur lequel chronic n’est pas disponible. Ce script perme de n’afficher sa sortie que si l’une des « supers commandes » a échouées.

#!/bin/bash

# exit le script en cas d'erreur
set -e -o pipefail -u

tmp_output=$(mktemp)
# fd1 = stdout -> fd1 = $tmp_output
# fd2 = stderr -> fd2 = $tmp_output
# fd3 =        -> fd3 = stderr
exec 3>&2 &> $tmp_output

trap_handler () {
	cat $tmp_output >&3
	rm $tmp_output
}
# lance trap_handler seulement en cas d'erreur
trap trap_handler ERR

# mes supers commandes
…

# s'il n'y a pas d'erreur on supprime le $tmp_output
rm $tmp_output

curl & worklog JIRA REST API

Ce script permet de loguer mon temps de travail sous JIRA.

#!/bin/bash

set -e -o pipefail -u

user=`whoami`
date_valeur=`date '+%FT%T.000%z'`
COLUMNS=`tput cols`
ticket="-"
savedir="$HOME/.ra"
mkdir "$savedir" &> /dev/null || true
# prevent others to access savedir elements
chmod 0700 "$savedir"

# ensure histfile exist
touch "${savedir}/histfile"

tmpdir=`mktemp -d`
trap 'rm -rf $tmpdir' EXIT
# prevent others to access tmpdir elements
chmod 0700 $tmpdir

# print_ticket          start_work         end_work      ticket        summary
#   non woking ticket: "last_date_valeur" "date_valeur" "last_ticket" "summary"
#   working ticket:    "date_valeur"      "now"         "ticket"      "summary"
#                       $1                 $2            $3            $4
function print_ticket {
	local duration end_work

	if test "$1" = "$date_valeur"
	then
		# new working ticket
		duration=""
	else
		duration=$(($(date -d "$2" '+%s') - $(date -d "$1" '+%s')))
		duration="($(date -u -d "@$duration" '+%_Hh %_Mm') )"
	fi

	if test "$2" = "now"
	then
		end_work="now"
	else
		end_work="$(date    -d  "$2" '+%_H:%M')"
	fi

	printf '%s → %5s %10s | %-22s | %s \n' \
		"$(date    -d  "$1" '+%_H:%M')"\
		"$end_work"\
		"$duration"\
		"$3" \
		"$4"
}

# get_cookie "url-to-sso"
#   Get a sso cookie and store it in cookie.
#   Create cookie if it doesn't exist.
#   Prevent others from accessing it.
#   Ask user for credentials.
#   Use fifo for maximum credentials protection.
function get_cookie {
	touch "${savedir}/cookie"
	chmod 600 "${savedir}/cookie"

	mkfifo -m 0600 ${tmpdir}/fifo
	local login password result
	read    -p 'login: ' login
	read -s -p 'password: ' password
	echo
	# echo is a buildin so password will not appear in `ps -ef`
	echo -n "$password" | curl -s -o /dev/null \
		--data "user=$login" \
		--data-urlencode 'password@-' \
		--cookie-jar "${savedir}/cookie" \
		"$1"
	if ! { tail -1 "${savedir}/cookie" | grep -q '^#' ;}
	then
		echo "auth failed"
		exit 1
	fi
}

# post_worklog
#   Stop last ticket stored in lastcall.
#   Delete lastcall if last ticket was successfully stopped.
#   call get_cookie and rerun post_worklog if the user isn't authenticated.
function post_worklog {
	local last_ticket last_date_valeur timeSpentSeconds result
	read last_ticket last_date_valeur summary < "${savedir}/lastcall"
	timeSpentSeconds=$(($(date -d "$date_valeur" '+%s') - $(date -d "$last_date_valeur" '+%s')))
	# use jira rest API
	# https://developer.atlassian.com/static/rest/jira/6.1.html#d2e1552
	curl -s -D ${tmpdir}/outputheaderfile -o /dev/null -b "${savedir}/cookie" -X POST --data '{"started": "'"$last_date_valeur"'", "timeSpentSeconds": '$timeSpentSeconds'}' -H "Content-Type: application/json" 'https://…/rest/api/latest/issue/'$last_ticket'/worklog'
	case `cat ${tmpdir}/outputheaderfile` in
		*201\ Created*)
			rm "${savedir}/lastcall"

			# record the ticket and dates in histfile (and ensure histfile is 10 lines max)
			cp ${savedir}/histfile ${tmpdir}/histfile
			print_ticket "$last_date_valeur" "$date_valeur" "$last_ticket" "$summary" \
				| tee -a "${tmpdir}/histfile" \
				| cut -c -$COLUMNS
			tail ${tmpdir}/histfile > ${savedir}/histfile
			;;
		*302\ Found*)
			sso_url=$(grep -o -P '(?<=^Location: ).*(?=\r)' ${tmpdir}/outputheaderfile)
			get_cookie "$sso_url"
			post_worklog
			;;
		*)
			echo "post_worklog failed"
			exit 1
			;;
	esac
}

# get_summary "ticket_id"
#   Get summary of a ticket
#   call get_cookie and rerun get_summary if the user isn't authenticated.
function get_summary {

	curl -s -D ${tmpdir}/outputheaderfile -o ${tmpdir}/outputsummary -b "${savedir}/cookie" -X GET -H "Content-Type: application/json" "https://…/rest/api/2/issue/${1}?fields=summary"

	case `cat ${tmpdir}/outputheaderfile` in
		*200\ OK*)
			summary="$(jq --raw-output .fields.summary ${tmpdir}/outputsummary)"
			;;
		*302\ Found*)
			sso_url=$(grep -o -P '(?<=^Location: ).*(?=\r)' ${tmpdir}/outputheaderfile)
			get_cookie "$sso_url"
			get_summary $1
			;;
		*)
			echo "get_summary failed"
			exit 1
			;;
	esac
}

# parse command line options
while getopts ":d:u:s" opt; do
	case $opt in
		d)
			date_valeur=`date -d "$OPTARG" '+%FT%T.000%z'`
			;;
		u)
			ticket="$OPTARG"
			;;
		s)
			cut -c -$COLUMNS "${savedir}/histfile"
			if test -f "${savedir}/lastcall"
			then
				read ticket start_date summary < "${savedir}/lastcall"
				print_ticket "${start_date}" "now" "${ticket}" "${summary}"
			fi
			exit 0
			;;
		\?)
			echo "ra [-d '11:20'] [-u ['http://…'|-]]"
			echo "   -d : date de début du travail sur le ticket (default = now)"
			echo "        formaté pour être interprété par 'date -d'"
			echo "   -u : string contenant le nom du ticket, e.g. l'url du ticket"
			echo "        special value: 'stop' indique que vous arrêtez le ticket"
			echo "          précédemment démarré, sans en démarrer de nouveau"
			echo "   -s : affiche les 10 derniers tickets ainsi que celui en cours"
			echo
			echo "shortcuts :"
			echo "   reu     – …"

			exit 0
			;;
	esac
done

if test "$ticket" = "-"
then
	read -p "url: " ticket
fi

case $ticket in
	ac)
		xdg-open 'https://…/secure/BrowseProjects.jspa?selectedCategory=10500&selectedProjectType=all'
		exit 0
		;;
esac

# stop last ticket stored in lastcall
if test -f "${savedir}/lastcall"
then
	post_worklog
fi

case $ticket in
	stop)
		exit 0
		;;
	reu)
		ticket="…"
		;;
	*)
		ticket=`echo "$ticket" | grep -o -P '[_A-Z0-9]+-\d+'`
		;;
esac

# get the ticket summary
get_summary $ticket

# store the information we are starting $ticket at $date_valeur
echo "${ticket} ${date_valeur} ${summary}" > "${savedir}/lastcall"
print_ticket "${date_valeur}" "now" "${ticket}" "${summary}"

Interactif

Raccourcis

Source : Des “basheries” (linuxfr.org)

Dernière commande

Source : Des “basheries” (linuxfr.org)

Extended Globbing

bash

Source : bash manual: Pattern-Matching

zsh

Source : Filename Generation

Divers

Source : Des “basheries” (linuxfr.org)

Commun à bash et zsh :

zsh

globbing :