20 KiB
Quelques astuces SHell. Des bouts de commandes, ou simplement les arguments qui vont bien.
Script
Style guide : https://google.github.io/styleguide/shell.xml
Configuration
Initialisation du shell
Entre les 3 modes ("login", "interactive non-login" et "non-interactive non-login") il y a de quoi se perdre à propos des fichiers chargés. Voici un rappel assez complet : https://github.com/rbenv/rbenv/wiki/Unix-shell-initialization
History Bash
bashrc
:
export HISTCONTROL=$HISTCONTROL${HISTCONTROL+:}ignoreboth:erasedups
export HISTSIZE=65535
export HISTTIMEFORMAT="%c : "
Note
: si besoin on peut
chattr +a /root/.bash_history
pour empêcher sa suppression
Pour avoir un historique qui ne sauvegarde à chaque commande saisie et se synchronise entre plusieurs terminaux :
shopt -s histappend
PROMPT_COMMAND="history -a;history -n;${PROMPT_COMMAND}"
Changer l'éditeur de texte par défaut pour une commande
Normalement le script (vipw, vigr, -ldapvi, ...) se réfère aux fichiers scripts (qui ne sont que des liens vers les binaires) :
/etc/alternatives/
Sinon en ligne de commande :
$ EDITOR=nano vipw
$ EDITOR=pico crontab -e
Manipulations
Déplacements et effacements
La plupart des terminaux/consoles utilisent les commandes readline. Il est donc possible d'utiliser ses commandes pour faciliter le déplacement du curseur ou la suppression de caractères dans une ligne.
Déplacement :
Ctrl-E
: avancer à la fin de la ligneAlt-F
: avancer à la fin du motCtrl-F
: avancer d'un caractèreCtrl-A
: revenir au début de la ligneAlt-B
: revenir au début du motCtrl-B
: revenir d'un caractère
Effacement :
Ctrl-K
: effacer jusqu'à la fin de la ligneAlt-D
: effacer jusqu'à la fin du motCtrl-U
: effacer jusqu'au début de la ligneCtrl-W
: effacer jusqu'au début du mot
NB : Certaines lettres sont faciles à retenir : E = End, F = Forward, B = Backward.
C'est aussi valable dans tous les outils utilisant la bibliothèque "readline", c'est à dire un grand nombre de logiciels.
Il est possible d’éditer la ligne actuelle dans un éditeur de texte avec Ctrl-X
puis Ctrl-E
.
Tâches de fond
$ bg = "mettre en arrière plan"
$ fg = "mettre en premier plan"
$ jobs = "lister les tâches de fond"
%% = "dernier job utilisé (représenté par un +)"
%x = "job numéro x"
$ vi foo
^Z
[1]+ Stopped vi foo
$ tail -f bar
...
^C
$ fg
^Z
[1]+ Stopped vi foo
$ kill -9 %%
[1]+ Killed vi foo
$ ( sleep 1m; echo "Premier !" ) &
[1] 13649
$ ( sleep 30; echo "Deuxième !" ) &
[2] 13651
$ jobs
[1]- Running sleep 1m && echo ...
[2]+ Running sleep 30 && echo ...
$ fg %2
^Z
[2]+ Stopped sleep 30 && echo ...
$ jobs
[1]- Running sleep 1m && echo ...
[2]+ Stopped sleep 30 && echo ...
$ sleep 30; bg
Premier !
[2]- Done sleep 30 && echo ...
Deuxième !
$ jobs
[1]- Done sleep 1m && echo ...
$ vi foo
^Z
[1]+ Stopped vi foo
$ exit
There are stopped jobs.
$ kill -9 %% #fg :x
$ exit
Connaître le rang d'un élément dans une liste
Avec grep
:
$ ./liste_serveur.sh | grep -n NOUVEAU_SERVEUR
2:NOUVEAU_SERVEUR
Avec nl
:
$ ./liste_serveur.sh | nl | grep NOUVEAU_SERVEUR
2 NOUVEAU_SERVEUR
Avec awk
:
$ ./liste_serveur.sh | awk '/NOUVEAU_SERVEUR/ { printf "%u\t%s\n", NR, $0 }'
2 NOUVEAU_SERVEUR
Ajout mot en début de chaque ligne d'un buffer
$ sed 's/^/Coucou /' <<<"$VAR"
Avoir l’empreinte SSH d'une liste de serveurs
-> pour s'assurer que le host soit connu (~/.ssh/known_hosts) et ainsi automatiser des tâches sur des serveurs même si toujours aucune connexion effectuée et acceptée :
$ (for host in machine1 machine2 ...; do echo $host; timeout -k 2 2 ssh -o 'StrictHostKeyChecking no' $host cat /etc/ssh/ssh_host_dsa_key.pub >> ~/.ssh/known_hosts; done)
Manipuler l’historique
Les éléments suivants permettent de rappeler des bouts des commandes précédentes. Voir la section [History Expansion(https://www.gnu.org/software/bash/manual/bash.html#History-Interaction) dans le manuel de Bash.
!!
: la dernière commande!-2
: l’avant dernière commande!$
: le dernier argument de la dernière commande!:2
: le deuxième argument de la dernière commande^old^new^
: le dernière commande, en remplaçantold
parnew
Lorsque qu’on entre une commande qui utilise l’history expansion, Bash affiche la commande qui est vraiment exécutée juste après le prompt.
Exemple :
$ echo a b c
a b c
$ !! d
echo a b c d
a b c d
$ echo !$
echo d
d
$ !-3
echo a b c
a b c
$ # copier un fichier puis l’éditer
$ cp -p file file.old
$ vi !:2
vi file
$ # lister puis extraire le contenu d’une archive
$ tar --list --file mon-archive.tar
[…]
$ ^list^extract^
tar --extract --file mon-archive.tar
Fichiers et FS
Ordinaire
Lister les répertoires monté sur le même FS
ROOT=/; get_fs(){ df $1 | tail -n1 | awk '{print $1}'; }; fs_root="$(get_fs $ROOT)"; for file in $(ls -1 $ROOT); do [ "$(get_fs $ROOT$file)" = "$fs_root" ] && echo "$ROOT$file"; done
Savoir si lignes en doublon dans un fichier
$ uniq -d <fichier>
ou autrement (appliquer un filtre différent) :
$ diff <fichier> <(cat <fichier> | uniq)
Comparer deux fichiers quant à l'existence de nouvelles lignes
$ grep -F -x -v -f <fichier1> <fichier2>
Supprimer des vieux fichiers
- Si + vieux de 30 jours en modification :
$ find DIR/ -type f -mtime +30 -delete
$ find DIR/ -type f -mtime +30 -exec rm '{}' \;
- Si + vieux depuis le 24 aout 2022 à 11h :
$ find DIR/ -type f ! -newermt "2022-08-24 11:00:00.00" -delete
Comparer deux fichiers à travers SSH
$ diff <fichier> <(ssh REMOTE cat <fichier>)
Changer le propriétaire des fichiers selon le user
$ find /path/ -user www-user -exec chown user: '{}' \;
Lister fichiers
Lister les fichiers dont le nom contient autre chose que des lettres sans accent, des chiffres, des points ou des tirets :
find /path/ | LC_ALL=C grep -Ev '^[a-zA-Z0-9./_-]+$'
La variable
LC_ALL=C
évite de faire correspondre les accents, par exempleé
ete
.
Connaître la taille totale des derniers fichiers copiés :
$ find /path/ -type f -mtime -1 -printf "'%p' " | xargs du -ch
Archiver
tar --create --file archive.tar --verbose -- directory
tar --create --file archive.tar --exclude='directory/subdir' --verbose -- directory
tar --create --file archive.tar --exclude='directory/subdir/*.mp3' --verbose -- directory
Lorsqu'on exclut un répertoire, il ne faut pas mettre de
/
à la fin du chemin.
Droits
Changer propriétaires owner et group selon actuels
$ chown -c -R --from user:user userbis:userbis .
$ chown -c -R --from www-user:user www-userbis:userbis .
Copier seulement les droits de deux hiérarchies de fichiers
Répertoire
Surveiller les ouvertures/écriture des fichiers présent dans un répertoire
$ iwatch <target>
Savoir les différents users qui ont écris dans /tmp
$ stat -c %U /tmp/* | sort | uniq -c | sort -n
Si «too arguments pour stat» (version bien plus lente) :
$ find /tmp -exec stat -c %U '{}' \; | sort | uniq -c | sort -n
Comparer deux répertoires à travers SSH
Générique :
$ DIR=$PWD
$ for file in $(rsync -rvn $DIR REMOTE:$DIR | grep -v "^skipping non-regular file" | head -n -2); do echo $DIR/$file :; diff $DIR/$file <(ssh REMOTE cat $DIR/$file); done
Pour /etc/ :
# for file in $(rsync -rvn /etc/ --exclude=*.log --exclude=ssh --exclude=ssl --exclude=.git --exclude=shadow* --exclude=gshadow* REMOTE:/etc/ | \
grep -v "^skipping non-regular file" | head -n -2); do \
echo /etc/$file :; diff /etc/$file <(ssh REMOTE cat /etc/$file); done
Espace et Inode
Analyse disque
Quand il s'agit de / - penser à exclure les autres partitions (si existante de toute évidence) :
# ncdu / --exclude /home --exclude /srv --exclude /var --exclude /tmp --exclude /boot --exclude /usr --exclude /proc
Pour certaines anciennes versions :
# ncdu --exclude "/home/*" /
ou plus simplement :
# du -chx / | sort -h | tail
Lister les dix plus gros fichiers réguliers sous /home
:
# find /home -xdev -type f -print0 | du -h --files0-from=- | sort -hr | head
Sinon voir du côté de HowtoDUC.
Tester l'écriture disque
Simplement, en écriture (fichier de 5.1GB) :
$ dd if=/dev/zero of=test count=10000000
Lister les répertoires ayant le plus de fichiers <=> max inode
À partir de Stretch (Debian 9)
On peut utiliser la commande ncdu, et lors du listage, appuyer sur la touche c (sort by items).
À partir de Jessie (Debian 8)
du --inodes -x /path | sort -n
Autres versions
Sinon étapes par étapes (sans la commande du --inodes) :
PATH_TO_WATCH='/var'; RESULT_FILE='list_max_inode.txt'; TMP=$(mktemp)
#Regarder dans le premier niveau
#OLD : find $PATH_TO_WATCH -type d -printf '%p\n' | sed 's/"/\\"/g' | sed 's/^\(.*\)$/"\1"/' | while read i; do echo $(echo $i | xargs ls -a | wc -l) $i; done | sort -n > $TMP
#OLD : export IFS="
#"; for i in $(find $PATH_TO_WATCH -type d); do echo $(ls -a | wc -l) $i; done | sort -n > $TMP
for i in $(du -x $PATH_TO_WATCH | awk '{print $2}'); do echo $(ls -1 -a $i| wc -l) $i; done | sort -n > $TMP
#compter dans les sous niveaux
cat $TMP | (while read line; do num=$(echo $line | awk '{ print $1 }'); path=$(echo $line | awk '{ print $2 }'); echo ${path%/*}; done) | sort | uniq | (while read line; do echo $(grep "$line" $TMP | cut -f1 -d' ' | xargs echo -n | tr -s ' ' '+' | xargs echo | bc -l) $line; done) | sort -n | tee $RESULT_FILE
rm $TMP
Comprendre pourquoi résultat d'un df
ne correspond pas un du
Si le résultat d'un df
indique une occupation disque plus importante que lorsque on fait un du -cx /to/path
, cela veut dire que sans doute un fichier a été supprimé mais est encore en lecture par un process.
On peut le rechercher en faisant :
# lsof /var/ | grep deleted
Ce qui équivaut à
# lsof -Fn | grep ^n/var/ | sed 's/^n//' | xargs -n1 -I file stat file | grep 'No such file or directory'
En tuant le process, la mémoire sur le disque devrait se «libérer» et être de nouveau disponible.
Utilisateurs UNIX
Créer HOME
Si l'utilisateur existe, et qu'il est nécessaire de créer un répertoire $HOME pour ce dernier (car non existant) :
# mkhomedir_helper user
Va appliquer les bon droits sur fichiers/répertoires + copier ce qu'il y a dans /etc/skel.
Lister les utilisateurs + groupe
UNIX
$ for user in $(getent passwd | awk -F ':' '$3 > 10000 {printf $1 " "}'); do groups $user; done
LDAP
$ for user in $(getent passwd | awk -F ':' '$3 > 10000 {printf $1 " "}'); do \
ldapsearch -x -h localhost -LLL -b "dc=MACHINE,dc=DOMAIN,dc=COM" cn | tail -n2 | \
tr '\n' ' ' | cut -d':' -f2 | echo -n "$(cat <&0)"; echo = $(groups $user); done
Lister les expirations des mot de passe utilisateurs par date
LDAP
On se sert pour ça du champ sambaPwdLastSet
indiquant la date du dernier changement et on y ajoute 200 jours (17280000 secondes) correspondant au champ shadowMax
indiquant la durée de validité d'un mot de passe. On n'affiche pas les utilisateurs ayant leur mot de passe déjà expirés depuis plus d'un mois.
#!/bin/bash
delete_before=$(date "+%Y %b" -d -1month)
/usr/bin/ldapsearch -x -b 'ou=people,dc=XXXXXXX,dc=com' | /bin/sed '/sambaPwdLastSet\|uid:/!d' | /usr/bin/awk 'NR%2{printf "%s ",$0;next;}1' | /bin/sed -e 's/uid: //' -e 's/sambaPwdLastSet: //' | /usr/bin/xargs -L1 bash -c 'echo $(($1+17280000)); echo $0' | /usr/bin/awk 'NR%2{printf "%s ",$0;next;}1' | /usr/bin/xargs -L1 bash -c 'date -d @$0; echo $1' | /usr/bin/awk 'NR%2{printf "%s ",$0;next;}1' | /bin/sed -e "s/^\(.*\)\ \(20..\)\ \(.*\)$/- \2 \1 \3/" | /bin/sed -r -e 's/(\s+)?\S+//3' -e 's/(\s+)?\S+//6' | /usr/bin/sort -n -k 2 -k 3M -k 4 | /usr/bin/awk "/$delete_before/{p=1}p"
Serveur web
Avoir un rendu des requêtes par IP à partir d'un access.log
Selon le format, il faudra peut-être changer la valeur du field de cut (-f1 par -f2).
Compte rendu pour un laps de temps
$ date; (timeout 60 tail -f access.log | cut -d' ' -f1) | sort | uniq -c | sort -n
En direct
- Version simple :
$ tail -f access.log | stdbuf -oL cut -d ' ' -f1 | uniq -c
- Version couleur :
$ SEUIL=5; tail -f access.log | stdbuf -oL cut -d ' ' -f1 | stdbuf -oL uniq -c | \
eval "awk '\$1 > $SEUIL {printf \"\\033[1;31m\" \$1 \" \" \$2 \"\\033[0m \\n\"; next;};{printf \$1 \" \" \$2 \"\\n\";}'"
Comparer les requêtes effectués dans access.log
Les différentes requêtes sont comparés aux nombres de caractères différent. La variable SEUIL est la limite de caractères différents pour 2 requêtes.
SEUIL=20; i=1; lastline=; cat access.log | sed 's/.*\] \(.*\)\" [0-9]\{3\}.*$/\1\"/' | \
(while read line; do diff=$(cmp -bl <(echo "$lastline") <(echo "$line") 2>/dev/null | wc -l); \
((diff<SEUIL)) && ((i=i+1)) || { echo "$i $line"; i=1; }; lastline="$line"; done)
Si on veut les adresses IPs, ou simplement trier le access.log avant l'analyse, il faut modifier après le : cat acccess.log |
Et si l'on considère les mots comme une seule différence (et non par caractères), on peut descendre le seuil :
SEUIL=3; i=1; lastline=; cat access.log | sed 's/.*\] \(.*\)\" [0-9]\{3\}.*$/\1\"/' | \
(while read line; do diff=$(cmp -bl <(echo "$lastline") <(echo "$line") 2>/dev/null | awk '{print $1}' | \
(compt=0; lastnumber=0; while read number; do ((lastnumber+1!=number)) && ((compt=compt+1)); lastnumber=$number; done; echo $compt)); \
((diff<SEUIL)) && ((i=i+1)) || { echo "$i $line"; i=1; }; lastline="$line"; done)
C'est à dire que entre 2 requêtes du type :
- /page.do?pseudo=Example&pass=0322
- /page.do?pseudo=Mail&pass=3892
il n'y a que 2 différences : le «pseudo», et le «pass».
Analyser un apache-status
Compter le nombre de requêtes par IPs :
cat FICHIER | grep --color '<td>[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+</td>' | sed 's/.*<td>\([0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\)<\/td>.*/\1/g' | sort -n | uniq -c | sort -n
Le faire pour un créneau horaire :
$ cd /var/www/apache-status
$ DATE=2017-10-12-14-
$ cat ${DATE}*.html | grep --color '<td>[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+</td>' | sed 's/.*<td>\([0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\)<\/td>.*/\1/g' | sort -n | uniq -c | sort -n
Processus / Process
Surveiller les nouveaux processus créés
Liste simple
$ ps -e -o etimes=,pid,cmd | sort -rn | awk '{if ($1!=0 && $3!~/\[.*\]/) print $0 }'
il y a moins de X minutes
$ MIN=5; ps -e -o etimes=,pid,cmd | sort -rn | awk '{if ($1<'$(( MIN * 60 ))' && $1>0 && $3!~/\[.*\]/) print $0 }'
Watch
Toutes les 5 secondes :
$ watch -n 5 -d "ps -e -o etimes=,pid,user,cmd | sort -n | awk '{if (\$1==0 || \$2==$$ || \$3~/watch/ || \$3~/\[.*\]/) {} else print \$0 }'"
Se baser seulement par rapport aux utilisateurs ayant créés dernièrement ces processus (SEUIL <=> processus vieux de moins de $SEUIL secondes) :
$ SEUIL=100; watch -n 5 -d "ps -e -o etimes=,user | sort -n | awk '{if (\$1<$SEUIL) print \$2 }' | sort | uniq -c | sort -n"
Cron
Recevoir le output de la commande top en cron :
top -b -d 1
Lister avec le plus de fils (/fork)
(total_procs=0; for foo in $(ps -e -o ppid | sed '1d' | sort -n | uniq -c | sort -n | awk '{ print $1 ":" $2 }'); do val=$(echo $foo | cut -d: -f1); total_procs=$((total_procs+val)); pid=$(echo $foo | cut -d: -f2); (( pid != 0 )) && { echo -n $val ') '; ps -p $pid -o pid,cmd | tail -n1; }; done; echo Total = $total_procs) | tail
Consommation Swap
(for file in /proc/*; do [ -e $file/status ] || continue; PID=$(basename $file); RES=$(grep VmSwap: $file/status | sed 's/VmSwap\:[[:space:]]*\(.*\)/\1/'); [ -n "$RES" ] && echo $RES ' = ' $PID ' ' $(ps -p $PID -o cmd --no-headers); done) | sort -n
Consommation RAM (VmSize)
$ top -o RES
Par process
$ ps o user:20,pid,pcpu,pmem,vsz,rss,tty,stat,start,time,comm -p 3108
Par utilisateur
for var in users1 users2; do echo '#' $var ':'; ps -u $var -o pid= | while read pid; do echo -n $pid ') threads: ' "$(ps -p $pid -T | wc -l)" "; $(grep VmSize /proc/$pid/status)"; echo; vmsize=$(grep VmSize /proc/$pid/status | sed 's/^VmSize:\s*\([0-9]*\) kB/\1/'); done; done
for var in $(getent passwd | cut -d':' -f1); do echo '#' $var ':'; ps -u $var -o pid= | while read pid; do echo -n $pid ') threads: ' "$(ps -p $pid -T | wc -l)" "; $(grep VmSize /proc/$pid/status)"; echo; vmsize=$(grep VmSize /proc/$pid/status | sed 's/^VmSize:\s*\([0-9]*\) kB/\1/'); done; done
Fichiers ouverts
socket/port
- Connaître les sockets ouvertes et ports en écoutent par un processus :
$ lsof -Pan -p PID -i
- Connaître le pid qui écoute sur un port (2ème colonne) :
$ lsof -i :Port
Selon l'utilisateur
# lsof -u UID
Si www-data a uid=33, lister fichiers ouvert par serveur-web :
# lsof -u 33 | awk '{ print $2 " = " $9 }' | grep "/home/.*$"
Créer un fichier "dummy" de différentes tailles
Pour tester les performances d'un FTP ou autre on a besoin d'envoyer un fichier volumineux ou pas, voici comment créer un fichier vide (dummy) d'une certaine taille avec dd :
Pour un fichier de 10Mio :
dd if=/dev/zero of=10M.bin bs=1024 count=0 seek=$[1024*10]
Pour 100Mio :
dd if=/dev/zero of=100M.bin bs=1024 count=0 seek=$[1024*100]
Pour 1Gio :
dd if=/dev/zero of=1G.bin bs=1024 count=0 seek=$[1024*1024]
stdout / stderr
Renvoyer stdout/stderr dans une même sortie (par exemple /dev/null) :
eject /dev/coin > /dev/null 2>&1
Détecter les tabulations
$ grep -P '\t'
Conflit sur stdin
Rediriger stdin
peut être nécéssaire, par exemple, lorsqu’on combine une boucle while read
, un here-document et des commandes interactives.
Dans la boucle suivante, lorsqu’on rentre dans la boucle, la variable hostname
contient bien « server1 ».
while read hostname
do
ssh "${hostname}"
done << eof
server1
server2
eof
On peut s’attendre à ce qu’au prochain tour de boucle, hostname
contienne « server2 », mais lorsque la commande ssh
sera exécutée durant le premier passage de la boucle, c’est bien ssh
qui va consommer la ligne « server2 » ! Pour empêcher ça, on peut faire lire un autre descripteur de fichier, comme 3
, à read
et envoyer le here-document dessus :
while read hostname 0<&3
do
ssh "${hostname}"
done 3<< eof
server1
server2
eof
On peut aussi faire lire au autre descripteur de fichier à ssh
à la place :
while read hostname
do
ssh "${hostname}" 0<&3
done 3<&0 << eof
server1
server2
eof
Serveur mail
Avoir vision des différentes erreurs mailq (MAILER-DAEMON)
(for id in $(mailq | grep MAILER\-DAEMON | cut -d' ' -f1); do postcat -q $id| grep Diagnostic\-Code\:; done) | sort | uniq -c | sort -n
Parsing
JSON avec jq
jq est un puissant outil de manipulation de JSON en cli. Il va aussi mettre en forme et colorer en fonction du terminal.
# apt install jq
$ curl --silent ipinfo.io | jq
On peut s'en servir pour extraire certaine partie du JSON :
- .[] : Addresser une entrée d'un tableau. Exemple ".[0]" pour la première entrée
- .foo : Récupérer la valeur de la clée foo
Exemple: Récupérer l'ip d'un container (f37ac628a4630da4aabbd23ba8eebf9c72dce5f3ba03675515a8b3619f8425d2) sur l'interface docker_gwbridge
# docker inspect docker_gwbridge | jq ".[0].Containers.f37ac628a4630da4aabbd23ba8eebf9c72dce5f3ba03675515a8b3619f8425d2.IPv4Address"
Tips : Pour faire des tests ou s'entrainer : https://jqplay.org/
Mot de passe
Générer un mot de passe hexadecimal
Pour générer un mot de passe hexadecimal de 6 octets :
openssl rand -hex 6