Quelques astuces SHell. Des bouts de commandes, ou simplement les arguments qui vont bien. # Script Style guide : # 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 : ## 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) : ~~~{.bash} /etc/alternatives/ ~~~ Sinon en ligne de commande : ~~~{.bash} $ EDITOR=nano vipw ~~~ # Manipulations ## Tâches de fond ~~~{.bash} $ 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" ~~~ ~~~{.bash} $ 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 Utilisation de **tac** pour inverser la liste, sans la trier : ~~~{.bash} $ ./liste_serveur.sh | tac | grep -n NOUVEAU_SERVEUR 2:NOUVEAU_SERVEUR ~~~ -> 'grep -n' reviens au même que 'nl | grep' ## Ajout mot en début de chaque ligne d'un buffer ~~~{.bash} $ sed 's/^/Coucou /g' <<<"$VAR" ~~~ ## Avoir le 'fingerprint' SSH d'une liste de serveur -> 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 : ~~~{.bash} $ (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) ~~~ # Fichiers et FS ## Ordinaire ### Lister les répertoires monté sur le même FS ~~~{.bash} 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 ~~~{.bash} $ uniq -d ~~~ ou autrement (appliquer un filtre différent) : ~~~{.bash} $ diff <(cat | uniq) ~~~ ### Comparer deux fichiers quant à l'existence de nouvelles lignes ~~~{.bash} $ grep -F -x -v -f ~~~ ### Supprimer des vieux fichiers - Par exemple, si + vieux de 30 jours en modification : ~~~{.bash} $ find DIR/ -type f -mtime +30 -delete $ find DIR/ -type f -mtime +30 -exec rm '{}' \; ~~~ ### Comparer deux fichiers à travers SSH ~~~{.bash} $ diff <(ssh REMOTE cat ) ~~~ ### Changer le propriétaire des fichiers selon le user ~~~{.bash} $ find /path/ -user www-user -exec chown user: '{}' \; ~~~ ### Lister fichiers Connaître la taille totale des derniers fichiers copiés : ~~~{.bash} $ find /path/ -type f -mtime -1 -printf "'%p' " | xargs du -ch ~~~ ## Droits ### Changer propriétaires owner et group selon actuels ~~~{.bash} $ 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 ~~~{.bash} $ iwatch ~~~ ### Savoir les différents users qui ont écris dans /tmp ~~~{.bash} $ 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 : ~~~{.bash} $ 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 ~~~ Sinon voir du côté de [HowtoDUC](/HowtoDUC). ### Tester l'écriture disque Simplement, en écriture (fichier de 5.1GB) : ~~~{.bash} $ 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) ~~~{.bash} du --inodes -x /path | sort -n ~~~ #### Autres versions Sinon étapes par étapes (sans la commande *du --inodes*) : ~~~{.bash} 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) : ~~~{.bash} # 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 ~~~{.bash} $ for user in $(getent passwd | awk -F ':' '$3 > 10000 {printf $1 " "}'); do groups $user; done ~~~ ### LDAP ~~~{.bash} $ 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. ~~~{.bash} #!/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 ~~~{.bash} $ date; (timeout 60 tail -f access.log | cut -d' ' -f1) | sort | uniq -c | sort -n ~~~ ### En direct - Version simple : ~~~{.bash} $ tail -f access.log | stdbuf -oL cut -d ' ' -f1 | uniq -c ~~~ - Version couleur : ~~~{.bash} $ 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. ~~~{.bash} 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/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[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+' | sed 's/.*\([0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\)<\/td>.*/\1/g' | sort -n | uniq -c | sort -n ~~~ Le faire pour un créneau horaire : ~~~{.bash} $ cd /var/www/apache-status $ DATE=2017-10-12-14- $ cat ${DATE}*.html | grep --color '[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+' | sed 's/.*\([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 ~~~{.bash} $ ps -e -o etimes=,pid,cmd | sort -rn | awk '{if ($1!=0 && $3!~/\[.*\]/) print $0 }' ~~~ ### il y a moins de X minutes ~~~{.bash} $ 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 : ~~~{.bash} $ 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) : ~~~{.bash} $ 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 : ~~~{.bash} top -b -d 1 ~~~ ## Lister avec le plus de fils (/fork) ~~~{.bash} (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 ~~~{.bash} (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) ~~~{.bash} $ top -o RES ~~~ ### Par process ~~~{.bash} $ ps o user:20,pid,pcpu,pmem,vsz,rss,tty,stat,start,time,comm -p 3108 ~~~ ### Par utilisateur ~~~{.bash} 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 : ~~~{.bash} $ lsof -Pan -p PID -i ~~~ - Connaître le pid qui écoute sur un port (2ème colonne) : ~~~{.bash} $ 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*100] ~~~ Pour 100Mio : ~~~ dd if=/dev/zero of=100M.bin bs=1024 count=0 seek=$[1024*1000] ~~~ 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' ~~~ # Serveur mail ## Avoir vision des différentes erreurs mailq (MAILER-DAEMON) ~~~{.bash} (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/