¿Cuál es la forma más elegante de eliminar una ruta de la variable $ PATH en Bash?

O, en términos más generales, ¿cómo elimino un elemento de una lista separada por dos puntos en una variable de entorno Bash?

Pensé que había visto una forma simple de hacer esto hace años, usando las formas más avanzadas de la expansión variable de Bash, pero si es así, lo he perdido de vista. Una búsqueda rápida de Google arrojó sorprendentemente pocos resultados relevantes y ninguno que yo llamaría “simple” o “elegante”. Por ejemplo, dos métodos que usan sed y awk, respectivamente:

PATH=$(echo $PATH | sed -e 's;:\?/home/user/bin;;' -e 's;/home/user/bin:\?;;') PATH=!(awk -F: '{for(i=1;i<=NF;i++){if(!($i in a)){a[$i];printf s$i;s=":"}}}'<<<$PATH) 

¿No existe nada sencillo? ¿Hay algo análogo a una función split () en Bash?

Actualizar:
Parece que necesito disculparme por mi pregunta intencionalmente vaga; Estaba menos interesado en resolver un caso de uso específico que en provocar una buena discusión. ¡Afortunadamente, lo tengo!

Aquí hay algunas técnicas muy inteligentes. Al final, agregué las siguientes tres funciones a mi caja de herramientas. La magia ocurre en path_remove, que se basa principalmente en el uso astuto de Martin York de la variable RS de awk .

 path_append () { path_remove $1; export PATH="$PATH:$1"; } path_prepend () { path_remove $1; export PATH="$1:$PATH"; } path_remove () { export PATH=`echo -n $PATH | awk -v RS=: -v ORS=: '$0 != "'$1'"' | sed 's/:$//'`; } 

El único cruft real allí es el uso de sed para eliminar el colon que se arrastra. Considerando lo sencillo que es el rest de la solución de Martin, ¡estoy dispuesto a vivir con eso!


Pregunta relacionada: ¿Cómo manipulo los elementos $ PATH en scripts de shell?

Un minuto con awk:

 # Strip all paths with SDE in them. # export PATH=`echo ${PATH} | awk -v RS=: -v ORS=: '/SDE/ {next} {print}'` 

Editar: responde a los comentarios a continuación:

 $ export a="/a/b/c/d/e:/a/b/c/d/g/k/i:/a/b/c/d/f:/a/b/c/g:/a/b/c/d/g/i" $ echo ${a} /a/b/c/d/e:/a/b/c/d/f:/a/b/c/g:/a/b/c/d/g/i ## Remove multiple (any directory with a: all of them) $ echo ${a} | awk -v RS=: -v ORS=: '/a/ {next} {print}' ## Works fine all removed ## Remove multiple including last two: (any directory with g) $ echo ${a} | awk -v RS=: -v ORS=: '/g/ {next} {print}' /a/b/c/d/e:/a/b/c/d/f: ## Works fine: Again! 

Editar en respuesta a un problema de seguridad: (eso no es relevante para la pregunta)

 export PATH=$(echo ${PATH} | awk -v RS=: -v ORS=: '/SDE/ {next} {print}' | sed 's/:*$//') 

Esto elimina los puntos restantes que quedan al eliminar las últimas entradas, lo que efectivamente se agrega . a tu camino

Mi sucio truco:

 echo ${PATH} > t1 vi t1 export PATH=$(cat t1) 

Dado que el gran problema con la sustitución es el caso final, ¿qué hay de hacer que los casos finales no sean diferentes a los otros casos? Si el camino ya tenía dos puntos al principio y al final, podríamos simplemente buscar nuestra cadena deseada envuelta con dos puntos. Tal como está, podemos agregar fácilmente esos dos puntos y eliminarlos después.

 # PATH => /bin:/opt/a dir/bin:/sbin WORK=:$PATH: # WORK => :/bin:/opt/a dir/bin:/sbin: REMOVE='/opt/a dir/bin' WORK=${WORK/:$REMOVE:/:} # WORK => :/bin:/sbin: WORK=${WORK%:} WORK=${WORK#:} PATH=$WORK # PATH => /bin:/sbin 

Pure bash :).

Aquí está la solución más simple que puedo diseñar:

 #!/bin/bash IFS=: # convert it to an array t=($PATH) unset IFS # perform any array operations to remove elements from the array t=(${t[@]%%*usr*}) IFS=: # output the new array echo "${t[*]}" 

El ejemplo anterior eliminará cualquier elemento en $ PATH que contenga “usr”. Puede reemplazar “* usr *” con “/ home / user / bin” para eliminar solo ese elemento.

actualización por sschuberth

Aunque creo que los espacios en $PATH son una idea horrible , aquí hay una solución que lo maneja:

 PATH=$(IFS=':';t=($PATH);n=${#t[*]};a=();for ((i=0;i 

o

 IFS=':' t=($PATH) n=${#t[*]} a=() for ((i=0;i 

Aquí hay una línea que, a pesar de las respuestas actualizadas y más aceptadas , no agrega caracteres invisibles a PATH y puede hacer frente a rutas que contienen espacios:

 export PATH=$(p=$(echo $PATH | tr ":" "\n" | grep -v "/cygwin/" | tr "\n" ":"); echo ${p%:}) 

Personalmente, también me parece fácil de leer / comprender, y solo implica comandos comunes en lugar de usar awk.

función __path_remove () {
local D = “: $ {PATH}:”;
[“$ {D /: $ 1: /:}”! = “$ D”] && RUTA = “$ {D /: $ 1: /:}”;
RUTA = “$ {RUTA / #: /}”;
export PATH = “$ {PATH /%: /}”;
}

Lo desenterré de mi archivo .bashrc. Cuando juegas con PATH y se pierde, awk / sed / grep deja de estar disponible 🙂

La mejor opción de bash pura que he encontrado hasta ahora es la siguiente:

 function path_remove { PATH=${PATH/":$1"/} # delete any instances in the middle or at the end PATH=${PATH/"$1:"/} # delete any instances at the beginning } 

Esto se basa en la respuesta no del todo correcta para Agregar directorio a $ PATH si aún no está allí en Superusuario.

Aquí hay una solución que:

  • es puro Bash,
  • no invoca otros procesos (como ‘sed’ o ‘awk’),
  • no cambia IFS ,
  • no bifurca un subconjunto,
  • maneja caminos con espacios, y
  • elimina todas las apariciones del argumento en PATH .

     removeFromPath () {
        local pd
        p = ": $ 1:"
        d = ": $ RUTA:"
        d = $ {d // $ p /:}
        d = $ {d / #: /}
        RUTA = $ {d /%: /}
     } 

Acabo de utilizar las funciones en la distribución bash, que han estado allí aparentemente desde 1991. Todavía están en el paquete bash-docs en Fedora, y solían usarse en /etc/profile , pero no más …

 $ rpm -ql bash-doc |grep pathfunc /usr/share/doc/bash-4.2.20/examples/functions/pathfuncs $ cat $(!!) cat $(rpm -ql bash-doc |grep pathfunc) #From: "Simon J. Gerraty"  #Message-Id: <199510091130.VAA01188@zen.void.oz.au> #Subject: Re: a shell idea? #Date: Mon, 09 Oct 1995 21:30:20 +1000 # NAME: # add_path.sh - add dir to path # # DESCRIPTION: # These functions originated in /etc/profile and ksh.kshrc, but # are more useful in a separate file. # # SEE ALSO: # /etc/profile # # AUTHOR: # Simon J. Gerraty  # @(#)Copyright (c) 1991 Simon J. Gerraty # # This file is provided in the hope that it will # be of use. There is absolutely NO WARRANTY. # Permission to copy, redistribute or otherwise # use this file is hereby granted provided that # the above copyright notice and this notice are # left intact. # is $1 missing from $2 (or PATH) ? no_path() { eval "case :\$${2-PATH}: in *:$1:*) return 1;; *) return 0;; esac" } # if $1 exists and is not in path, append it add_path () { [ -d ${1:-.} ] && no_path $* && eval ${2:-PATH}="\$${2:-PATH}:$1" } # if $1 exists and is not in path, prepend it pre_path () { [ -d ${1:-.} ] && no_path $* && eval ${2:-PATH}="$1:\$${2:-PATH}" } # if $1 is in path, remove it del_path () { no_path $* || eval ${2:-PATH}=`eval echo :'$'${2:-PATH}: | sed -e "s;:$1:;:;g" -e "s;^:;;" -e "s;:\$;;"` } 

Bueno, en bash, como admite la expresión regular, simplemente lo haría:

 PATH=${PATH/:\/home\/user\/bin/} 

Escribí una respuesta aquí (usando awk también). ¿Pero no estoy seguro de que sea eso lo que estás buscando? Por lo menos, me parece claro lo que hace, en lugar de tratar de encajar en una línea. Para un simple trazador de líneas, sin embargo, que solo elimina cosas, recomiendo

 echo $PATH | tr ':' '\n' | awk '$0 != "/bin"' | paste -sd: 

Reemplazar es

 echo $PATH | tr ':' '\n' | awk '$0 != "/bin"; $0 == "/bin" { print "/bar" }' | paste -sd: 

o (más corto pero menos legible)

 echo $PATH | tr ':' '\n' | awk '$0 == "/bin" { print "/bar"; next } 1' | paste -sd: 

De todos modos, para la misma pregunta, y un montón de respuestas útiles, mira aquí .

Sí, poner un punto al final de PATH, por ejemplo, hace que eliminar un camino sea menos torpe y propenso a errores.

 path_remove () { declare i newPATH newPATH="${PATH}:" for ((i=1; i<=${#@}; i++ )); do #echo ${@:${i}:1} newPATH="${newPATH//${@:${i}:1}:/}" done export PATH="${newPATH%:}" return 0; } path_remove_all () { declare i newPATH shopt -s extglob newPATH="${PATH}:" for ((i=1; i<=${#@}; i++ )); do newPATH="${newPATH//+(${@:${i}:1})*([^:]):/}" #newPATH="${newPATH//+(${@:${i}:1})*([^:])+(:)/}" done shopt -u extglob export PATH="${newPATH%:}" return 0 } path_remove /opt/local/bin /usr/local/bin path_remove_all /opt/local /usr/local 

Si le preocupa eliminar duplicados en $ PATH, la forma más elegante, en mi humilde opinión, sería no agregarlos en primer lugar. En 1 línea:

 if ! $( echo "$PATH" | tr ":" "\n" | grep -qx "$folder" ) ; then PATH=$PATH:$folder ; fi 

$ folder puede ser reemplazado por cualquier cosa, y puede contener espacios (“/ home / user / my documents”)

La solución de bash pura más elegante que he encontrado hasta la fecha:

 pathrm () { local IFS=':' local newpath local dir local pathvar=${2:-PATH} for dir in ${!pathvar} ; do if [ "$dir" != "$1" ] ; then newpath=${newpath:+$newpath:}$dir fi done export $pathvar="$newpath" } pathprepend () { pathrm $1 $2 local pathvar=${2:-PATH} export $pathvar="$1${!pathvar:+:${!pathvar}}" } pathappend () { pathrm $1 $2 local pathvar=${2:-PATH} export $pathvar="${!pathvar:+${!pathvar}:}$1" } 

La mayoría de las otras soluciones sugeridas se basan solo en la coincidencia de cadenas y no tienen en cuenta los segmentos de ruta que contienen nombres especiales como . , .. , o ~ . La función bash a continuación resuelve cadenas de directorios en su argumento y en segmentos de ruta para encontrar coincidencias de directorio lógico y cadenas.

 rm_from_path() { pattern="${1}" dir='' [ -d "${pattern}" ] && dir="$(cd ${pattern} && pwd)" # resolve to absolute path new_path='' IFS0=${IFS} IFS=':' for segment in ${PATH}; do if [[ ${segment} == ${pattern} ]]; then # string match continue elif [[ -n ${dir} && -d ${segment} ]]; then segment="$(cd ${segment} && pwd)" # resolve to absolute path if [[ ${segment} == ${dir} ]]; then # logical directory match continue fi fi new_path="${new_path}${IFS}${segment}" done new_path="${new_path/#${IFS}/}" # remove leading colon, if any IFS=${IFS0} export PATH=${new_path} } 

Prueba:

 $ mkdir -p ~/foo/bar/baz ~/foo/bar/bif ~/foo/boo/bang $ PATH0=${PATH} $ PATH=~/foo/bar/baz/.././../boo/././../bar:${PATH} # add dir with special names $ rm_from_path ~/foo/boo/../bar/. # remove same dir with different special names $ [ ${PATH} == ${PATH0} ] && echo 'PASS' || echo 'FAIL' 

Me gustan las tres funciones que se muestran en la actualización de @ BenBlank a su pregunta original. Para generalizarlos, utilizo un formulario de 2 argumentos, que me permite establecer PATH o cualquier otra variable de entorno que desee:

 path_append () { path_remove $1 $2; export $1="${!1}:$2"; } path_prepend () { path_remove $1 $2; export $1="$2:${!1}"; } path_remove () { export $1="`echo -n ${!1} | awk -v RS=: -v ORS=: '$1 != "'$2'"' | sed 's/:$//'`"; } 

Ejemplos de uso:

 path_prepend PATH /usr/local/bin path_append PERL5LIB "$DEVELOPMENT_HOME/p5/src/perlmods" 

Tenga en cuenta que también agregué algunas comillas para permitir el procesamiento adecuado de las rutas que contienen espacios.

¿Cuál es la forma más elegante de eliminar una ruta de la variable $ PATH en Bash?

¿Qué es más elegante que awk?

 path_remove () { export PATH=`echo -n $PATH | awk -v RS=: -v ORS=: '$0 != "'$1'"' | sed 's/:$//'`; 

¡Pitón! Es una solución más legible y fácil de mantener, y es fácil de inspeccionar para ver que realmente está haciendo lo que quiere.

Supongamos que quiere eliminar el primer elemento de ruta?

 PATH="$(echo "$PATH" | python -c "import sys; path = sys.stdin.read().split(':'); del path[0]; print(':'.join(path))")" 

(En lugar de canalizar desde echo , os.getenv['PATH'] sería un poco más corto y proporcionaría el mismo resultado que el anterior, pero me preocupa que Python pueda hacer algo con esa variable de entorno, por lo que probablemente sea mejor páselo directamente del entorno que le interesa).

Del mismo modo para eliminar desde el final:

 PATH="$(echo "$PATH" | python -c "import sys; path = sys.stdin.read().split(':'); del path[-1]; print(':'.join(path))")" 

Para hacer estas funciones de shell reutilizables que puede, por ejemplo, pegar en su archivo .bashrc:

 strip_path_first () { PATH="$(echo "$PATH" | python -c "import sys; path = sys.stdin.read().split(':'); del path[0]; print(':'.join(path))")" } strip_path_last () { PATH="$(echo "$PATH" | python -c "import sys; path = sys.stdin.read().split(':'); del path[-1]; print(':'.join(path))")" } 

Dado que esto tiende a ser bastante problemático, ya que NO HAY manera elegante, recomiendo evitar el problema reorganizando la solución: construya su PATH en lugar de intentar derribarla.

Podría ser más específico si supiera tu contexto de problema real. Mientras tanto, usaré una construcción de software como contexto.

Un problema común con las comstackciones de software es que se rompe en algunas máquinas, en última instancia debido a la forma en que alguien ha configurado su shell predeterminado (PATH y otras variables de entorno). La solución elegante es hacer que tus scripts de construcción sean inmunes al especificar completamente el entorno del shell. Codifique sus scripts de comstackción para establecer la RUTA y otras variables de entorno basadas en el ensamblaje de las piezas que controle, como la ubicación del comstackdor, bibliotecas, herramientas, componentes, etc. Haga que cada elemento configurable sea algo que pueda establecer, verificar y luego usa apropiadamente en tu script.

Por ejemplo, tengo una versión de Java orientada a WebLogic basada en Maven que heredé en mi nuevo empleador. El script de construcción es notorio por ser frágil, y otro empleado nuevo y yo pasamos tres semanas (no a tiempo completo, solo aquí y allá, pero aún muchas horas) para que funcione en nuestras máquinas. Un paso esencial fue que tomé el control de PATH para saber exactamente qué Java, qué Maven y qué WebLogic se estaba invocando. Creé variables de entorno para apuntar a cada una de esas herramientas, luego calculé la RUTA en función de esas y algunas otras. Técnicas similares dominaron las otras configuraciones configurables, hasta que finalmente creamos una comstackción reproducible.

Por cierto, no use Maven, Java está bien, y solo compre WebLogic si necesita absolutamente su agrupamiento (pero de lo contrario no, y especialmente no sus características propietarias).

Los mejores deseos.

Al igual que con @litb, contribuí con una respuesta a la pregunta ” ¿Cómo manipulo elementos de $ PATH en scripts de shell? “, Entonces mi respuesta principal está ahí.

La funcionalidad ‘dividir’ en bash y otros derivados de Bourne shell se logra con mayor precisión con $IFS , el separador entre campos. Por ejemplo, para establecer los argumentos posicionales ( $1 , $2 , …) a los elementos de PATH, use:

 set -- $(IFS=":"; echo "$PATH") 

Funcionará bien siempre que no haya espacios en $ PATH. Hacer que funcione para los elementos de ruta que contienen espacios es un ejercicio no trivial, que queda para el lector interesado. Probablemente sea más sencillo tratar con un lenguaje de scripting como Perl.

También tengo un script, clnpath , que utilizo extensamente para configurar mi RUTA. Lo documenté en la respuesta a ” Cómo evitar la duplicación de la variable PATH en csh “.

Lo que hace que este problema sea molesto son los casos del fencepost entre el primer y el último elemento. El problema se puede resolver elegantemente cambiando IFS y usando una matriz, pero no sé cómo reintroducir los dos puntos una vez que la ruta se convierte en forma de matriz.

Aquí hay una versión ligeramente menos elegante que elimina un directorio de $PATH utilizando solo la manipulación de cadenas. Lo he probado

 #!/bin/bash # # remove_from_path dirname # # removes $1 from user's $PATH if [ $# -ne 1 ]; then echo "Usage: $0 pathname" 1>&2; exit 1; fi delendum="$1" NEWPATH= xxx="$IFS" IFS=":" for i in $PATH ; do IFS="$xxx" case "$i" in "$delendum") ;; # do nothing *) [ -z "$NEWPATH" ] && NEWPATH="$i" || NEWPATH="$NEWPATH:$i" ;; esac done PATH="$NEWPATH" echo "$PATH" 

Aquí hay un trazador de líneas Perl:

 PATH=`perl -e '$a=shift;$_=$ENV{PATH};s#:$a(:)|^$a:|:$a$#$1#;print' /home/usr/bin` 

La variable $a consigue que se elimine la ruta. Los comandos s (sustituto) e print operan implícitamente en la variable $_ .

Buenas cosas aquí. Yo uso este para no agregar engaños en primer lugar.

 #!/bin/bash # ###################################################################################### # # Allows a list of additions to PATH with no dupes # # Patch code below into your $HOME/.bashrc file or where it # will be seen at login. # # Can also be made executable and run as-is. # ###################################################################################### # add2path=($HOME/bin .) ## uncomment space separated list if [ $add2path ]; then ## skip if list empty or commented out for nodup in ${add2path[*]} do case $PATH in ## case block thanks to MIKE511 $nodup:* | *:$nodup:* | *:$nodup ) ;; ## if found, do nothing *) PATH=$PATH:$nodup ## else, add it to end of PATH or esac ## *) PATH=$nodup:$PATH prepend to front done export PATH fi ## debug add2path echo echo " PATH == $PATH" echo 

Con globbing extendido habilitado, es posible hacer lo siguiente:

 # delete all /opt/local paths in PATH shopt -s extglob printf "%s\n" "${PATH}" | tr ':' '\n' | nl printf "%s\n" "${PATH//+(\/opt\/local\/)+([^:])?(:)/}" | tr ':' '\n' | nl man bash | less -p extglob 

Extended globbing one-liner (bueno, más o menos):

 path_remove () { shopt -s extglob; PATH="${PATH//+(${1})+([^:])?(:)/}"; export PATH="${PATH%:}"; shopt -u extglob; return 0; } 

Parece que no hay necesidad de escapar de las barras en $ 1.

 path_remove () { shopt -s extglob; declare escArg="${1//\//\\/}"; PATH="${PATH//+(${escArg})+([^:])?(:)/}"; export PATH="${PATH%:}"; shopt -u extglob; return 0; } 

Añadiendo dos puntos a PATH también podríamos hacer algo como:

 path_remove () { declare i newPATH # put a colon at the beginning & end AND double each colon in-between newPATH=":${PATH//:/::}:" for ((i=1; i<=${#@}; i++)); do #echo ${@:${i}:1} newPATH="${newPATH//:${@:${i}:1}:/}" # s/:\/fullpath://g done newPATH="${newPATH//::/:}" newPATH="${newPATH#:}" # remove leading colon newPATH="${newPATH%:}" # remove trailing colon unset PATH PATH="${newPATH}" export PATH return 0 } path_remove_all () { declare i newPATH extglobVar extglobVar=0 # enable extended globbing if necessary [[ ! $(shopt -q extglob) ]] && { shopt -s extglob; extglobVar=1; } newPATH=":${PATH}:" for ((i=1; i<=${#@}; i++ )); do newPATH="${newPATH//:+(${@:${i}:1})*([^:])/}" # s/:\/path[^:]*//g done newPATH="${newPATH#:}" # remove leading colon newPATH="${newPATH%:}" # remove trailing colon # disable extended globbing if it was enabled in this function [[ $extglobVar -eq 1 ]] && shopt -u extglob unset PATH PATH="${newPATH}" export PATH return 0 } path_remove /opt/local/bin /usr/local/bin path_remove_all /opt/local /usr/local 

En path_remove_all (por proxxy):

 -newPATH="${newPATH//:+(${@:${i}:1})*([^:])/}" +newPATH="${newPATH//:${@:${i}:1}*([^:])/}" # s/:\/path[^:]*//g 

Si bien este es un hilo muy antiguo, pensé que esta solución podría ser de interés:

 PATH="/usr/lib/ccache:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games" REMOVE="ccache" # whole or part of a path :) export PATH=$(IFS=':';p=($PATH);unset IFS;p=(${p[@]%%$REMOVE});IFS=':';echo "${p[*]}";unset IFS) echo $PATH # outputs /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games 

lo encontré en esta publicación de blog . Creo que me gusta más 🙂

Tomé un enfoque ligeramente diferente al de la mayoría de las personas aquí y me centré específicamente en la manipulación de cadenas, de la siguiente manera:

 path_remove () { if [[ ":$PATH:" == *":$1:"* ]]; then local dirs=":$PATH:" dirs=${dirs/:$1:/:} export PATH="$(__path_clean $dirs)" fi } __path_clean () { local dirs=${1%?} echo ${dirs#?} } 

Lo anterior es un ejemplo simplificado de las funciones finales que uso. También creé path_add_before y path_add_after permitirle insertar una ruta antes / después de una ruta especificada que ya está en PATH.

El conjunto completo de funciones está disponible en path_helpers.sh en mis archivos dotfiles . Admiten completamente eliminar / agregar / anteponer / insertar al principio / medio / final de la cadena PATH.

El ‘:’ final es causado por el hecho de que está configurando el final de línea, no el separador. Utilizo unidades limitadas a los recursos y me gusta empaquetar todo en un solo script, sin estas rarezas:

 path_remove () { PATH="$(echo -n $PATH | awk -v RS=: -v ORS= '$0 != "'$1'"{print s _ $0;s=":"}')" } 

Esto es ciertamente elegante, pero usa el sed externo. Además, elimina todas las rutas que contienen la cadena de búsqueda $ 1. Tampoco deja un colgante: al final en el caso de que el camino eliminado sea el último en el PATH.

 PATH=`echo $PATH | sed 's/:[^:]*$1[^:]*//g'` 

Sin embargo, esta alternativa deja una final pendiente.

 PATH=`echo $PATH | tr ":" "\n" | grep -v $1 | tr "\n" ":"` 

Las alternativas sin backticks son:

 PATH=$(echo $PATH | sed 's/:[^:]*$1[^:]*//g') PATH=$(echo $PATH | tr ":" "\n" | grep -v $1 | tr "\n" ":")