Scripting
if
Par ce que j’oublie tous le temps :
if test …
then
…
elif test …
then
…
elif …
then
…
else
…
fi
Parameter Expansion
Sources :
- Shell Parameter Expansion sur www.gnu.org
man bash
section « Parameter Expansion »
variables non définies
Ces syntaxes permettent de traiter le cas des variables non definies.
${a:-b}
affiche$a
si a est set et non null, sinon afficheb
.${a:=b}
affiche$a
si a est set et non null, sinon afficheb
et set la valeur de a àb
.${a:+b}
afficheb
si a est set et non null, sinon affiche “” (une chaine de caractères vide).
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.
${var#pattern}
: supprime du début de var la plus petite chaine de caractère qui match pattern.${var##pattern}
: supprime du début de var la plus grande chaine de caractère qui match pattern.${var%pattern}
: supprime de la fin de var la plus petite chaine de caractère qui match pattern.${var%%pattern}
: supprime de la fin de var la plus grande chaine de caractère qui match pattern.${var/pattern/str}
: remplace la première chaine de caractère qui match pattern par str dans var.${var//pattern/str}
: remplace toutes les chaine de caractère qui match pattern par str dans var.${var^char}
: transforme le premier caractère de var en majuscule si celui-ci match char.${var^^char}
: transforme tous les caractères de var en majuscule qui match char.${var,char}
: transforme le premier caractère de var en minuscule si celui-ci match char.${var,,char}
: transforme tous les caractères de var en minuscule qui match char.
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 :
- Bourne Shell Builtins sur www.gnu.org
man bash
section « SHELL BUILTIN COMMANDS »
:
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
-e -o pipefail
cette option quitte à la première erreur (explication simplifié… il y a des exceptions).-u
quitte en cas d’utilisation d’une variable non défini. Si on utilise la forme${x:-y}
ou d’autre « parameter expansion » bash ne quitte pas.-x
affiche les commande au fure et à mesure de leur execution.
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 :
trap 'rm -f $suppr' EXIT
ne fonctionne pas, dans ce casrm
essaye de supprimer les fichiers « a », « ’b », « c’ » et « /tmp/… ».trap 'rm -f "$suppr"' EXIT
ne fonctionne pas, dans ce casrm
essaye de supprimer le fichier « a ’b c’ /tmp/… ».trap 'eval rm -f "$suppr"' EXIT
fonctionne (avec ou sans guillemet autour de$suppr
).
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 :
- utiliser la syntax
trap 'rm toto; rm titi'
- utiliser une fonction, cf. section mysqldump, pipe et ssh
- utiliser un “script de trap”
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 :
- ERR : la commande est exécuté à chaque fois qu’une commande du script retourne une erreur
- EXIT : la commande est exécuté lorsque le script se termine, quelque soit la façon dont il se termine (kill, erreur…). Sous zsh le signal EXIT est déclanché à la fin d’une fonction, ce n’est pas le cas sous bash.
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 :
- entrée standard pour nc
- argument pour grep
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 :
- bdd-source : le serveur de base de donnée duquel on veut récupérer les données.
- bdd-destination : le seuveur de base de donnée sur lequel on veut envoyer les données.
- serveur-de-rebond : cette machine à accès à bdd-destination sur le port 3306 (mysql).
- machine-local : la machine sur lequel s’exécute le script. Cette machine à uniquement accès à bdd-source sur le port 3306 (mysql) et à serveur-de-rebond sur le port 22 (ssh).
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)
Ctrl + a
: aller au début de la ligne.Ctrl + e
: aller à la fin de la ligne.Ctrl + b
: se déplacer d’un caractère vers l’arrière (flèche gauche).Ctrl + f
: se déplacer d’un caractère vers l’avant (flèche droite).Alt + b
: se déplacer d’un mot vers l’arrière.Alt + f
: se déplacer d’un mot vers l’avant.Ctrl + xx
: passer de la position du curseur au début de la ligne et revenir à la position où se trouvait le curseur.TAB
: compléter automatiquement les noms de fichiers/dossiers/commandes.Ctrl + d
: effacer le caractère sur lequel se trouve le curseur.Ctrl + h
: effacer le caractère précédant le curseur (la fonction classique du retour arrière).Alt + d
: effacer le mot suivant le curseur.Alt + retour arrière
: effacer le mot précédant le curseur.Ctrl + l
: nettoyer l’écran (l’équivalent de la commandeclear
).Alt + t
: échanger la place du mot où se trouve le curseur avec celui le précédant.Ctrl + t
: échanger la place des deux caractères précédant le curseur.Ctrl + w
: couper le mot précédant le curseur.Ctrl + k
: couper la partie de la ligne suivant le curseur.Ctrl + u
: couper la partie de la ligne précédant le curseur.Ctrl + y
: coller la dernière chose coupée.Ctrl + _
: annuler la dernière modification (undo).Ctrl + j
: créer une nouvelle ligne.Ctrl + c
: arrêter la commande en cours et créer aussi une nouvelle ligne.Ctrl + r
: rechercher une commande précédente dans l’historique.Ctrl + g
: quitter la recherche dans l’historique.Ctrl + p
: Commande précédente (flèche haut).Ctrl + n
: Commande suivante (flèche bas).Ctrl + x + e
: lancer un traitement de texte (nano par ex.) pour écrire une longue commande.
Dernière commande
Source : Des “basheries” (linuxfr.org)
!!
: répéter la dernière commande. Notamment très utile quand on oublie de mettre sudo avant une commande.$ apt install apt # qui donne une erreur parce qu'on a oublié sudo $ sudo !!
alt + .
: ajouter le dernier argument de la dernière commande.$ vim todo.txt $ mv [alt + .]todo.txt ~/tmp
!$
: Également utile pour ajouter le dernier argument de la dernière commande.!*
: reproduire tous les arguments de la dernière commande. C’est utile si vous faites une erreur du genre :$ vim cd ~/downloads $ !*
Ce qui donnera :
$ cd ~/downloads
!foo
: réutiliser la dernière commande en commençant par foo (on peut aussi fairefoo !!
).!:-
: réutiliser la dernière commande sans le dernier argument.$ ping -c 3 linuxfr.org $ !:- framasoft.org
Ce qui donnera :
$ ping -c 3 framasoft.org
À ces commandes, vous pouvez ajouter à la fin
:p
(e.g.!*:p
) pour afficher la commande sans qu’elle se lance.
Extended Globbing
bash
Source : bash manual: Pattern-Matching
!(…)
: negate (inverse) le paterne …max@laptop $ shopt -s extglob max@laptop $ ls -- !(*.pdf) # tout sauf pdf max@laptop $ ls -- !(*.pdf|*.zip) # tout sauf pdf & zip max@laptop $ tree --noreport . ├── a-1 ├── a-2 ├── a-3 ├── b-1 ├── b-2 └── b-3 max@laptop $ ls !(*1) a-2 a-3 b-2 b-3 max@laptop $ ls a-!(1|2) a-3
Attention,
!
ne pas d’incidence sur ce qui précède.max@laptop $ ls *-!(*1) a-2 a-3 b-2 b-3 max@laptop $ ls *-!(1) a-2 a-3 b-2 b-3
@(X|Y)
: grouping ; X ou Ymax@laptop $ shopt -s extglob max@laptop $ tree --noreport . ├── a-1 ├── a-2 ├── a-3 ├── b-1 ├── b-2 └── b-3 max@laptop $ ls a-@(1|2) a-1 a-2
zsh
Source : Filename Generation
X~Y
: exclue de X tous ce qui match Ymax@laptop % tree --noreport . ├── a-1 ├── a-2 ├── a-3 ├── b-1 ├── b-2 └── b-3 max@laptop % ls a~a-1 ls: impossible d'accéder à 'a~a-1': Aucun fichier ou dossier de ce type max@laptop % ls a-*~a-1 a-2 a-3 max@laptop % ls *~*1 a-2 a-3 b-2 b-3 max@laptop % ls *-1~b* a-1
^
: negate (inverse) le paterne qui suit le symbolemax@laptop % setopt extendedglob max@laptop % rm ^(*.pdf|*.zip) # tout sauf pdf & zip max@laptop % tree --noreport . ├── a-1 ├── a-2 ├── a-3 ├── b-1 ├── b-2 └── b-3 max@laptop % ls ^*1 a-2 a-3 b-2 b-3 max@laptop % ls a-^(1|2) a-3
Attention,
^
ne pas d’incidence sur ce qui précède le symbole.max@laptop % ls *^*1 a-1 a-2 a-3 b-1 b-2 b-3 max@laptop % ls *^1 a-1 a-2 a-3 b-1 b-2 b-3
Divers
Source : Des “basheries” (linuxfr.org)
Commun à bash et zsh :
$ cp ~/.vimrc{,.old}
correspond à :$ cp ~/.vimrc ~/.vimrc.old
Ca marche aussi pour supprimer une extension par ex. pour transformer “blabla.txt” en “blabla” :
$ mv blabla{.txt,}
Ou pour utiliser l’extension markdown à la place du .txt :$ mv blabla{.txt,.md}
cd -
: emmène au dernier dossier dans lequel on se trouvait.En cas de coquille, on peut corriger la commande précédente erronée :
$ mvi todo.txt $ ^mvi^vim
zsh
globbing :
<x-y>
: range d’entier.max@laptop % tree --noreport . ├── test-00 ├── test-01 ├── test-02 ├── test-03 ├── test-04 ├── test-05 ├── test-06 ├── test-07 ├── test-08 ├── test-09 └── test-10 max@laptop % ls test-<2-6> test-02 test-03 test-04 test-05 test-06 max@laptop % ls test-<-6> test-00 test-01 test-02 test-03 test-04 test-05 test-06 max@laptop % ls test-<6-> test-06 test-07 test-08 test-09 test-10 max@laptop % ls test-<3-6> test-03 test-04 test-05 test-06 max@laptop % ls test-<-5> test-00 test-01 test-02 test-03 test-04 test-05 max@laptop % ls test-<5-> test-05 test-06 test-07 test-08 test-09 test-10 max@laptop % ls test-<> zsh: parse error near `\n'
(x|y)
: grouppingmax@laptop % tree --noreport . ├── a-1 ├── a-2 ├── b-1 ├── b-2 ├── c-1 └── c-2 max@laptop % ls (a-*|*-1) a-1 a-2 b-1 c-1
**/
: parcour récusivementmax@laptop % tree --noreport . ├── a │ ├── 1 │ ├── 2 │ ├── a │ │ ├── 1 │ │ └── 2 │ └── b │ ├── 1 │ └── 2 └── b ├── 1 ├── 2 ├── a │ ├── 1 │ └── 2 └── b ├── 1 └── 2 max@laptop % ls **/*1 a/1 a/a/1 a/b/1 b/1 b/a/1 b/b/1