#!/usr/bin/env bash # delete_chrome_profile — borra por completo uno o varios perfiles Chrome/Chromium: # elimina la carpeta del perfil y limpia todas las referencias en Local State # (info_cache, profiles_order, last_active_profiles, last_used, variations_google_groups). set -euo pipefail delete_chrome_profile() { # ── defaults ────────────────────────────────────────────────────────────── local _user_data_dir="" local _profiles=() local _dry_run=0 # ── parse args ───────────────────────────────────────────────────────────── _usage() { cat >&2 <<'EOF' Usage: delete_chrome_profile --user-data-dir --profile [--profile ]... [--dry-run] --user-data-dir Ruta raíz del user-data-dir de Chrome/Chromium (obligatorio). --profile Nombre de la carpeta del perfil, ej. "Default" o "Profile 1" (repetible, al menos uno obligatorio). --dry-run Muestra qué borraría y qué claves de Local State quitaría sin tocar nada. Exit codes: 0 éxito (o dry-run completado) 1 error de argumento o validación 2 chromium está corriendo (solo en modo real) EOF return 1 } while [[ $# -gt 0 ]]; do case "$1" in --user-data-dir) _user_data_dir="$2"; shift 2 ;; --profile) _profiles+=("$2"); shift 2 ;; --dry-run) _dry_run=1; shift ;; -h|--help) _usage; return 0 ;; *) echo "delete_chrome_profile: argumento desconocido: $1" >&2; return 1 ;; esac done # ── validaciones de argumentos ──────────────────────────────────────────── if [[ -z "$_user_data_dir" ]]; then echo "delete_chrome_profile: --user-data-dir es obligatorio" >&2 return 1 fi if [[ ${#_profiles[@]} -eq 0 ]]; then echo "delete_chrome_profile: se requiere al menos un --profile" >&2 return 1 fi if [[ ! -d "$_user_data_dir" ]]; then echo "delete_chrome_profile: user-data-dir no encontrado: ${_user_data_dir}" >&2 return 1 fi local _local_state="${_user_data_dir}/Local State" if [[ ! -f "$_local_state" ]]; then echo "delete_chrome_profile: Local State no encontrado: ${_local_state}" >&2 return 1 fi # ── guard: ningún chromium debe tener ESTE user-data-dir abierto (excepto en dry-run) ── # Por-udd, no global. Comprobamos por PID con comm=chromium (pgrep -x) y leemos su cmdline, # para NO auto-matchear el propio `grep`/`pgrep` del pipe: como el path del udd contiene la # cadena "chromium" (p.ej. ~/.config/chromium-cdp), un `pgrep -af '[c]hromium' | grep ` # se detecta a sí mismo. pgrep -x chromium solo lista procesos cuyo nombre es exactamente # "chromium" (el navegador), nunca grep/pgrep/bash. if [[ $_dry_run -eq 0 ]]; then local _p _busy=0 for _p in $(pgrep -x chromium 2>/dev/null); do if tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_user_data_dir"; then _busy=1; break fi done if [[ $_busy -eq 1 ]]; then echo "delete_chrome_profile: hay un chromium con este user-data-dir abierto — ciérralo antes de borrar perfiles:" >&2 echo " pkill -TERM chromium" >&2 echo "(Chromium reescribe Local State desde memoria al cerrar y desharía el borrado)" >&2 return 2 fi fi local _today _today="$(date +%Y%m%d)" # ── modo dry-run ────────────────────────────────────────────────────────── if [[ $_dry_run -eq 1 ]]; then echo "=== delete_chrome_profile DRY-RUN ===" >&2 local _p for _p in "${_profiles[@]}"; do local _pdir="${_user_data_dir}/${_p}" if [[ -d "$_pdir" ]]; then echo " [borraría] rm -rf ${_pdir}" >&2 else echo " [no existe] ${_pdir}" >&2 fi echo " [Local State] quitaría claves para perfil: '${_p}'" >&2 echo " profile.info_cache.${_p}" >&2 echo " profile.profiles_order (entrada '${_p}')" >&2 echo " profile.last_active_profiles (entrada '${_p}')" >&2 echo " profile.last_used (si == '${_p}', reasignar)" >&2 echo " variations_google_groups.${_p} (si existe)" >&2 done # Construir JSON de dry-run inline local _dry_items="" _dry_first=1 for _p in "${_profiles[@]}"; do local _pdir="${_user_data_dir}/${_p}" local _sep="" _exists="false" [[ $_dry_first -eq 0 ]] && _sep="," _dry_first=0 [[ -d "$_pdir" ]] && _exists="true" _dry_items+="${_sep}{\"profile\":\"${_p}\",\"dir_exists\":${_exists},\"would_remove\":${_exists},\"local_state_would_clean\":true}" done printf '{"dry_run":true,"would_delete":[%s]}\n' "$_dry_items" return 0 fi # ── backup de Local State (no sobreescribir el del día) ─────────────────── local _backup="${_local_state}.bak.${_today}" if [[ ! -f "$_backup" ]]; then cp "$_local_state" "$_backup" fi # ── borrar carpetas de perfil ────────────────────────────────────────────── local _deleted_results=() # "profile|dir_removed|ls_cleaned" local _p for _p in "${_profiles[@]}"; do local _pdir="${_user_data_dir}/${_p}" local _dir_removed=false if [[ -d "$_pdir" ]]; then rm -rf "$_pdir" _dir_removed=true fi _deleted_results+=("${_p}|${_dir_removed}|false") done # ── construir lista Python de perfiles a eliminar ───────────────────────── local _py_profiles_list="" for _p in "${_profiles[@]}"; do _py_profiles_list+="\"${_p}\"," done _py_profiles_list="[${_py_profiles_list%,}]" # ── editar Local State con python3 ──────────────────────────────────────── local _ls_cleaned=false if command -v python3 >/dev/null 2>&1; then python3 - "$_local_state" "$_py_profiles_list" <<'PY' import sys, json ls_path = sys.argv[1] profiles_to_delete = json.loads(sys.argv[2]) with open(ls_path, "r", encoding="utf-8") as f: data = json.load(f) profile_section = data.get("profile", {}) # 1. profile.info_cache — eliminar cada perfil info_cache = profile_section.get("info_cache", {}) for p in profiles_to_delete: info_cache.pop(p, None) # 2. profile.profiles_order — quitar entradas del perfil if "profiles_order" in profile_section and isinstance(profile_section["profiles_order"], list): profile_section["profiles_order"] = [ x for x in profile_section["profiles_order"] if x not in profiles_to_delete ] # 3. profile.last_active_profiles — quitar entradas del perfil if "last_active_profiles" in profile_section and isinstance(profile_section["last_active_profiles"], list): profile_section["last_active_profiles"] = [ x for x in profile_section["last_active_profiles"] if x not in profiles_to_delete ] # 4. profile.last_used — reasignar si apunta a un perfil borrado last_used = profile_section.get("last_used", "") if last_used in profiles_to_delete: remaining = [k for k in info_cache.keys() if k not in profiles_to_delete] profile_section["last_used"] = remaining[0] if remaining else "" # 5. variations_google_groups — limpiar entradas del perfil (si existe) vgg = data.get("variations_google_groups", {}) for p in profiles_to_delete: vgg.pop(p, None) with open(ls_path, "w", encoding="utf-8") as f: json.dump(data, f, separators=(",", ":")) PY _ls_cleaned=true # ── fallback con jq ─────────────────────────────────────────────────────── elif command -v jq >/dev/null 2>&1; then local _tmp_ls _tmp_ls="$(mktemp)" local _jq_expr="." for _p in "${_profiles[@]}"; do _jq_expr+=" | del(.profile.info_cache[\"${_p}\"])" _jq_expr+=" | del(.variations_google_groups[\"${_p}\"])" _jq_expr+=" | if .profile.profiles_order then .profile.profiles_order -= [\"${_p}\"] else . end" _jq_expr+=" | if .profile.last_active_profiles then .profile.last_active_profiles -= [\"${_p}\"] else . end" done if jq "${_jq_expr}" "$_local_state" > "$_tmp_ls" 2>/dev/null; then mv "$_tmp_ls" "$_local_state" _ls_cleaned=true else echo "delete_chrome_profile: advertencia — jq falló editando Local State" >&2 rm -f "$_tmp_ls" fi else echo "delete_chrome_profile: advertencia — ni python3 ni jq disponibles; carpetas borradas pero Local State no modificado" >&2 fi # ── validar que el JSON resultante sigue siendo parseable ───────────────── if [[ "$_ls_cleaned" == "true" ]]; then if command -v python3 >/dev/null 2>&1; then if ! python3 -c "import sys, json; json.load(open(sys.argv[1]))" "$_local_state" 2>/dev/null; then echo "delete_chrome_profile: JSON de Local State inválido tras edición — restaurando backup" >&2 cp "$_backup" "$_local_state" return 1 fi fi fi # ── actualizar _deleted_results con ls_cleaned ──────────────────────────── local _updated_results=() for _entry in "${_deleted_results[@]}"; do local _ep _edr _els IFS='|' read -r _ep _edr _els <<< "$_entry" _updated_results+=("${_ep}|${_edr}|${_ls_cleaned}") done # ── leer last_used resultante ────────────────────────────────────────────── local _new_last_used="" if command -v python3 >/dev/null 2>&1; then _new_last_used="$(python3 -c " import sys, json data = json.load(open(sys.argv[1])) print(data.get('profile', {}).get('last_used', '')) " "$_local_state" 2>/dev/null || echo "")" fi # ── construir JSON de resultado inline ──────────────────────────────────── local _result_items="" _res_first=1 for _entry in "${_updated_results[@]+"${_updated_results[@]}"}"; do local _pn _dr _lc IFS='|' read -r _pn _dr _lc <<< "$_entry" local _rsep="" [[ $_res_first -eq 0 ]] && _rsep="," _res_first=0 _result_items+="${_rsep}{\"profile\":\"${_pn}\",\"dir_removed\":${_dr},\"local_state_cleaned\":${_lc}}" done printf '{"deleted":[%s],"last_used":"%s","backup":"Local State.bak.%s"}\n' \ "$_result_items" "$_new_last_used" "$_today" } # ── auto-ejecución ──────────────────────────────────────────────────────────── if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]]; then delete_chrome_profile "$@" fi