refactor(zsh): fiabiliser les helpers et accélérer le démarrage
This commit is contained in:
parent
bca26d4de2
commit
b84723fcc1
3 changed files with 264 additions and 30 deletions
|
|
@ -85,6 +85,34 @@ via les drop-ins systemd. La commande `trainlog` reste disponible.
|
|||
Le lien `labfy-investigation-web` a été préservé malgré sa cible
|
||||
actuellement absente, car son unité systemd référence encore ce chemin.
|
||||
|
||||
## Entretien Zsh (novembre 2026)
|
||||
|
||||
Le chargement interactif déduplique le tableau `path` après l'initialisation
|
||||
des outils (notamment pyenv). Les complétions Zsh utilisent désormais
|
||||
`${XDG_CACHE_HOME:-$HOME/.cache}/zsh/zcompdump` et créent leur répertoire au
|
||||
premier démarrage. Les associations Ctrl+R (Atuin) et Ctrl+F (FZF) restent
|
||||
inchangées ; le shell et sa configuration sont gérés par les dotfiles.
|
||||
|
||||
Pour les opérations potentiellement destructives :
|
||||
|
||||
- `update` effectue `sudo pacman -Syu`, puis `aurupdate` uniquement si
|
||||
la première commande a réussi ; plus de renouvellement sudo permanent ;
|
||||
- `orphans` transforme les noms de paquets Pacman en tableau Zsh afin de
|
||||
conserver un argument distinct par paquet ; Pacman demande confirmation ;
|
||||
- `clean-exe --dry-run` affiche les seuls exécutables ELF réguliers du
|
||||
répertoire courant et ignore les fichiers suivis par Git ; sans cette
|
||||
option, il faut taper exactement `SUPPRIMER` avant la suppression ;
|
||||
- `vrm --dry-run` affiche uniquement les répertoires `venv`/`.venv`
|
||||
contenant `pyvenv.cfg`, non symboliques ; une confirmation `SUPPRIMER`
|
||||
est obligatoire avant suppression. Aucun répertoire autre que ces deux
|
||||
noms n'est ciblé.
|
||||
|
||||
Pour l'OSINT, `dns` utilise `dig` au lieu de `dog`, `geoip <IP>` interroge
|
||||
explicitement ipapi.co, `shot [full|region]` capture sous Wayland avec
|
||||
`grim` et éventuellement `slurp` dans `~/Pictures/`. `subdomains <domaine>`
|
||||
nécessite `subfinder` : son absence est signalée sans exécution de secours.
|
||||
Ces commandes réseau ne sont pas lancées lors de l'ouverture du shell.
|
||||
|
||||
## Données locales
|
||||
|
||||
L'historique Zsh et le store Atuin sont des données utilisateur, pas de la
|
||||
|
|
|
|||
118
tests/zsh/test_shell_safety.py
Normal file
118
tests/zsh/test_shell_safety.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"""Tests Zsh isolés, sans toucher aux données personnelles ni au système."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def shell(command, *, cwd=None, stdin=None):
|
||||
env = os.environ.copy()
|
||||
env["ZDOTDIR"] = str(REPO / "zsh")
|
||||
return subprocess.run(
|
||||
["zsh", "-ic", command], cwd=cwd, input=stdin, text=True,
|
||||
capture_output=True, env=env, timeout=20
|
||||
)
|
||||
|
||||
|
||||
class ShellSafetyTests(unittest.TestCase):
|
||||
def test_unique_path(self):
|
||||
p = shell("print -- PATH_BEGIN; print -rl -- $path; print -- PATH_END")
|
||||
self.assertEqual(p.returncode, 0, p.stderr)
|
||||
entries = p.stdout.split("PATH_BEGIN\n", 1)[1].split("PATH_END", 1)[0].strip().splitlines()
|
||||
self.assertTrue(entries)
|
||||
self.assertEqual(len(entries), len(set(entries)))
|
||||
|
||||
def test_orphans_args_and_empty_result(self):
|
||||
p = shell('pacman() { print -rl -- alpha beta; }; '
|
||||
'sudo() { print -r -- "CALL $*"; }; orphans')
|
||||
self.assertEqual(p.returncode, 0, p.stderr)
|
||||
self.assertIn("CALL pacman -Rns -- alpha beta", p.stdout)
|
||||
empty = shell('pacman() { return 1; }; '
|
||||
'sudo() { print -- ERROR_CALL; }; orphans')
|
||||
self.assertEqual(empty.returncode, 0, empty.stderr)
|
||||
self.assertIn("Aucun paquet orphelin", empty.stdout)
|
||||
self.assertNotIn("ERROR_CALL", empty.stdout)
|
||||
|
||||
def test_update_stops_if_pacman_fails(self):
|
||||
success = shell('sudo() { print -r -- "CALL $*"; }; '
|
||||
'aurupdate() { print -- AUR_CALLED; }; update')
|
||||
self.assertEqual(success.returncode, 0, success.stderr)
|
||||
self.assertIn("CALL pacman -Syu", success.stdout)
|
||||
self.assertIn("AUR_CALLED", success.stdout)
|
||||
fail = shell('sudo() { return 7; }; '
|
||||
'aurupdate() { print -- SHOULD_NOT_RUN; }; update')
|
||||
self.assertEqual(fail.returncode, 7)
|
||||
self.assertNotIn("SHOULD_NOT_RUN", fail.stdout)
|
||||
|
||||
def test_clean_exe_confirms_and_preserves_scripts(self):
|
||||
with tempfile.TemporaryDirectory(prefix="zsh-test-elf-") as td:
|
||||
root = Path(td)
|
||||
elf = root / "binary"
|
||||
if not Path("/usr/bin/true").exists():
|
||||
self.skipTest("ELF fixture unavailable")
|
||||
shutil.copy2("/usr/bin/true", elf)
|
||||
elf.chmod(0o755)
|
||||
script = root / "utility.py"
|
||||
script.write_text("#!/usr/bin/env python3\nprint('safe')\n")
|
||||
script.chmod(0o755)
|
||||
preview = shell("clean-exe --dry-run", cwd=td)
|
||||
self.assertIn("./binary", preview.stdout)
|
||||
self.assertNotIn("./utility.py", preview.stdout)
|
||||
self.assertTrue(elf.exists())
|
||||
cancel = shell("clean-exe", cwd=td, stdin="NON\n")
|
||||
self.assertEqual(cancel.returncode, 1)
|
||||
self.assertTrue(elf.exists())
|
||||
confirm = shell("clean-exe", cwd=td, stdin="SUPPRIMER\n")
|
||||
self.assertEqual(confirm.returncode, 0, confirm.stderr)
|
||||
self.assertFalse(elf.exists())
|
||||
self.assertTrue(script.exists())
|
||||
|
||||
def test_clean_exe_ignores_git_tracked_binary(self):
|
||||
with tempfile.TemporaryDirectory(prefix="zsh-test-git-elf-") as td:
|
||||
root = Path(td)
|
||||
binary = root / "tracked-program"
|
||||
if not Path("/usr/bin/true").exists():
|
||||
self.skipTest("ELF fixture unavailable")
|
||||
shutil.copy2("/usr/bin/true", binary)
|
||||
binary.chmod(0o755)
|
||||
subprocess.run(["git", "init", "-q", td], check=True)
|
||||
subprocess.run(["git", "-C", td, "add", "tracked-program"], check=True)
|
||||
preview = shell("clean-exe --dry-run", cwd=td)
|
||||
self.assertEqual(preview.returncode, 0, preview.stderr)
|
||||
self.assertIn("Aucun exécutable ELF local", preview.stdout)
|
||||
self.assertTrue(binary.exists())
|
||||
|
||||
def test_vrm_confirms_only_real_virtualenv(self):
|
||||
with tempfile.TemporaryDirectory(prefix="zsh-test-venv-") as td:
|
||||
root = Path(td)
|
||||
env = root / "venv"
|
||||
env.mkdir()
|
||||
(env / "pyvenv.cfg").write_text("home=/usr/bin\n")
|
||||
unrelated = root / ".venv"
|
||||
unrelated.mkdir()
|
||||
(unrelated / "keep.txt").write_text("keep")
|
||||
preview = shell("vrm --dry-run", cwd=td)
|
||||
self.assertIn("venv", preview.stdout)
|
||||
self.assertTrue(env.exists())
|
||||
cancel = shell("vrm", cwd=td, stdin="NON\n")
|
||||
self.assertEqual(cancel.returncode, 1)
|
||||
self.assertTrue(env.exists())
|
||||
accept = shell("vrm", cwd=td, stdin="SUPPRIMER\n")
|
||||
self.assertEqual(accept.returncode, 0, accept.stderr)
|
||||
self.assertFalse(env.exists())
|
||||
self.assertTrue((unrelated / "keep.txt").exists())
|
||||
|
||||
def test_osint_invalid_arguments_do_not_run_commands(self):
|
||||
for command in ("geoip", "shot unknown", "subdomains"):
|
||||
with self.subTest(command=command):
|
||||
p = shell(command)
|
||||
self.assertEqual(p.returncode, 2, p.stderr)
|
||||
self.assertIn("Usage", p.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
148
zsh/.zshrc
148
zsh/.zshrc
|
|
@ -8,6 +8,8 @@
|
|||
# =========================
|
||||
# PATH & env
|
||||
# =========================
|
||||
# Un seul exemplaire de chaque chemin, y compris après pyenv.
|
||||
typeset -U path
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
export PATH="$HOME/.local/npm/bin:$PATH"
|
||||
|
|
@ -29,7 +31,6 @@ setopt SHARE_HISTORY
|
|||
setopt HIST_IGNORE_ALL_DUPS
|
||||
setopt HIST_REDUCE_BLANKS
|
||||
setopt HIST_FIND_NO_DUPS
|
||||
HISTDUP=erase
|
||||
bindkey '^[[A' history-beginning-search-backward
|
||||
bindkey '^[[B' history-beginning-search-forward
|
||||
|
||||
|
|
@ -48,7 +49,13 @@ bindkey '^[[3~' delete-char
|
|||
fpath=("$HOME/.zsh/completions" $fpath)
|
||||
|
||||
autoload -Uz compinit
|
||||
compinit -d ~/.cache/zsh/zcompdump
|
||||
# Dump persistant des complétions, sous le cache XDG.
|
||||
ZSH_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/zsh"
|
||||
if [[ -d "$ZSH_CACHE_DIR" ]] || command mkdir -p -m 700 -- "$ZSH_CACHE_DIR"; then
|
||||
compinit -d "$ZSH_CACHE_DIR/zcompdump"
|
||||
else
|
||||
compinit
|
||||
fi
|
||||
|
||||
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'
|
||||
zstyle ':completion:*' list-colors ${(s.:.)LS_COLORS}
|
||||
|
|
@ -187,14 +194,47 @@ alias lt='eza -T'
|
|||
# =========================
|
||||
|
||||
alias whois-ip='whois'
|
||||
alias geoip='function _geoip() { curl -s "https://ipapi.co/$1/json/" | jq; }; _geoip'
|
||||
geoip() {
|
||||
if (( $# != 1 )); then
|
||||
print -u2 -- "Usage : geoip <adresse IP>"
|
||||
return 2
|
||||
fi
|
||||
setopt localoptions pipefail
|
||||
command curl -fsS --max-time 10 -- "https://ipapi.co/$1/json/" | command jq .
|
||||
}
|
||||
alias myip='curl -s ifconfig.me'
|
||||
alias headers='curl -sI'
|
||||
alias dns='dog' # ou "dig"
|
||||
alias subdomains='subfinder -d'
|
||||
alias dns='dig'
|
||||
subdomains() {
|
||||
if (( $# != 1 )); then
|
||||
print -u2 -- "Usage : subdomains <domaine>"
|
||||
return 2
|
||||
fi
|
||||
if ! command -v subfinder >/dev/null 2>&1; then
|
||||
print -u2 -- "subfinder est absent : installer cet outil avant l'énumération."
|
||||
return 127
|
||||
fi
|
||||
command subfinder -d "$1"
|
||||
}
|
||||
|
||||
alias topcmd="history | awk '{print \$2}' | sort | uniq -c | sort -rn | head -20"
|
||||
alias shot='scrot ~/Pictures/osint_%Y%m%d_%H%M%S.png'
|
||||
# Wayland natif : capture complète ou sélection rectangulaire.
|
||||
shot() {
|
||||
local mode="${1:-full}" target geometry
|
||||
if (( $# > 1 )) || [[ "$mode" != full && "$mode" != region ]]; then
|
||||
print -u2 -- "Usage : shot [full|region]"
|
||||
return 2
|
||||
fi
|
||||
target="$HOME/Pictures/osint_$(date +%Y%m%d_%H%M%S_%N).png"
|
||||
if [[ "$mode" == region ]]; then
|
||||
geometry="$(command slurp)" || return 1
|
||||
[[ -n "$geometry" ]] || return 1
|
||||
command grim -g "$geometry" "$target" || return $?
|
||||
else
|
||||
command grim "$target" || return $?
|
||||
fi
|
||||
print -r -- "$target"
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Arch workflow
|
||||
|
|
@ -207,30 +247,21 @@ alias pQ='pacman -Q'
|
|||
alias pSs='pacman -Ss'
|
||||
alias pSyu='sudo pacman -Syu'
|
||||
orphans() {
|
||||
local pkgs
|
||||
pkgs="$(pacman -Qtdq 2>/dev/null)"
|
||||
|
||||
if [[ -n "$pkgs" ]]; then
|
||||
sudo pacman -Rns $pkgs
|
||||
else
|
||||
echo "Aucun paquet orphelin"
|
||||
fi
|
||||
local -a pkgs
|
||||
# Transformer la liste multiligne de pacman en vrai tableau Zsh.
|
||||
pkgs=( ${(f)"$(pacman -Qtdq 2>/dev/null)"} )
|
||||
if (( ${#pkgs[@]} == 0 )); then
|
||||
print -- "Aucun paquet orphelin"
|
||||
return 0
|
||||
fi
|
||||
print -r -- "Paquets orphelins : ${(j:, :)pkgs}"
|
||||
sudo pacman -Rns -- "${pkgs[@]}"
|
||||
}
|
||||
|
||||
update() {
|
||||
sudo -v || return 1
|
||||
|
||||
while true; do
|
||||
sudo -n true
|
||||
sleep 60
|
||||
kill -0 "$$" || exit
|
||||
done 2>/dev/null &
|
||||
local sudo_loop_pid=$!
|
||||
|
||||
trap 'kill $sudo_loop_pid 2>/dev/null' EXIT
|
||||
|
||||
sudo pacman -Syyu || return 1
|
||||
aurupdate
|
||||
# Pas de boucle sudo en arrière-plan et pas de -Syyu systématique.
|
||||
sudo pacman -Syu || return $?
|
||||
aurupdate
|
||||
}
|
||||
|
||||
# =========================
|
||||
|
|
@ -300,7 +331,33 @@ va() {
|
|||
|
||||
alias venv='python -m venv venv'
|
||||
alias vd='deactivate'
|
||||
alias vrm='rm -rf venv .venv'
|
||||
# Environnements Python reconnus, avec confirmation obligatoire.
|
||||
vrm() {
|
||||
local -a envs
|
||||
local candidate answer
|
||||
if (( $# > 1 )) || { (( $# == 1 )) && [[ "$1" != "--dry-run" ]]; }; then
|
||||
print -u2 -- "Usage : vrm [--dry-run]"
|
||||
return 2
|
||||
fi
|
||||
if [[ "$PWD" == "$HOME" || "$PWD" == "/" ]]; then
|
||||
print -u2 -- "Refus : lancer vrm dans un répertoire de projet."
|
||||
return 1
|
||||
fi
|
||||
for candidate in venv .venv; do
|
||||
if [[ -d "$candidate" && ! -L "$candidate" && -f "$candidate/pyvenv.cfg" ]]; then
|
||||
envs+=("$candidate")
|
||||
fi
|
||||
done
|
||||
if (( ${#envs[@]} == 0 )); then
|
||||
print -- "Aucun environnement virtuel Python local à retirer."
|
||||
return 0
|
||||
fi
|
||||
print -r -- "Environnements concernés : ${(j:, :)envs}"
|
||||
[[ "${1:-}" == "--dry-run" ]] && return 0
|
||||
read -r 'answer?Taper SUPPRIMER pour confirmer : ' || return 1
|
||||
[[ "$answer" == "SUPPRIMER" ]] || { print -- "Annulé."; return 1; }
|
||||
command rm -rf -- "${envs[@]}"
|
||||
}
|
||||
alias vreq='pip install -r requirements.txt'
|
||||
alias vfreeze='pip freeze > requirements.txt'
|
||||
|
||||
|
|
@ -316,8 +373,36 @@ alias gcc-strict='gcc -Wall -Wextra -Werror -pedantic -std=c17'
|
|||
# Alias pour Valgrind
|
||||
alias valgrind-check='valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes'
|
||||
|
||||
# Alias pour le nettoyage (limité au répertoire courant, non récursif)
|
||||
alias clean-exe='find . -maxdepth 1 -type f -executable -not -name "*.sh" -delete'
|
||||
# Nettoyage C : seulement les fichiers ELF exécutables au niveau courant.
|
||||
clean-exe() {
|
||||
local -a binaries
|
||||
local candidate answer
|
||||
local in_git=0
|
||||
command git rev-parse --is-inside-work-tree >/dev/null 2>&1 && in_git=1
|
||||
if (( $# > 1 )) || { (( $# == 1 )) && [[ "$1" != "--dry-run" ]]; }; then
|
||||
print -u2 -- "Usage : clean-exe [--dry-run]"
|
||||
return 2
|
||||
fi
|
||||
for candidate in ./*(N.); do
|
||||
[[ -x "$candidate" ]] || continue
|
||||
command readelf -h -- "$candidate" >/dev/null 2>&1 || continue
|
||||
# Ne jamais proposer la suppression d'un binaire suivi par Git.
|
||||
if (( in_git )) && command git ls-files --error-unmatch -- "$candidate" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
binaries+=("$candidate")
|
||||
done
|
||||
if (( ${#binaries[@]} == 0 )); then
|
||||
print -- "Aucun exécutable ELF local à retirer."
|
||||
return 0
|
||||
fi
|
||||
print -- "Exécutables ELF candidats (aucune suppression automatique) :"
|
||||
printf ' %s\n' "${binaries[@]}"
|
||||
[[ "${1:-}" == "--dry-run" ]] && return 0
|
||||
read -r 'answer?Taper SUPPRIMER pour confirmer : ' || return 1
|
||||
[[ "$answer" == "SUPPRIMER" ]] || { print -- "Annulé."; return 1; }
|
||||
command rm -- "${binaries[@]}"
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Ansible
|
||||
|
|
@ -402,3 +487,6 @@ export LESS_TERMCAP_us=$'\e[1;32m' # underline
|
|||
# Android SDK
|
||||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||
export PATH="$PATH:$ANDROID_HOME/platform-tools"
|
||||
|
||||
# Dédupliquer réellement le tableau après toutes les initialisations (pyenv inclus).
|
||||
path=("${path[@]}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue