saber - ¿Cuál es la forma más elegante de eliminar una ruta de la variable $ PATH en Bash?
variables de entorno ubuntu (30)
¿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))")"
}
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 resto de la solución de Martin, ¡estoy dispuesto a vivir con eso!
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 :-)
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
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" <[email protected]>
#Message-Id: <[email protected]>
#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 <[email protected]>
# @(#)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;:/$;;"`
}
Al igual que con @litb, contribuí con una respuesta a la pregunta " ", 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 ".
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<n;i++)); do p="${t[i]%%*usr*}"; [ "${p}" ] && a[i]="${p}"; done;echo "${a[*]}");
o
IFS='':''
t=($PATH)
n=${#t[*]}
a=()
for ((i=0;i<n;i++)); do
p="${t[i]%%*usr*}"
[ "${p}" ] && a[i]="${p}"
done
echo "${a[*]}"
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 $_
.
Aquí hay una línea que, a pesar de las respuestas actualizadas y más accepted , 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.
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 p d p=":$1:" d=":$PATH:" d=${d//$p/:} d=${d/#:/} PATH=${d/%:/} }
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
Bueno, en bash, como admite la expresión regular, simplemente lo haría:
PATH=${PATH/://home//user//bin/}
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
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 :).
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 compilaciones 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 compilación para establecer la RUTA y otras variables de entorno basadas en el ensamblaje de las piezas que controle, como la ubicación del compilador, 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 compilació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.
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=":"}'')"
}
En path_remove_all (por proxxy):
-newPATH="${newPATH//:+(${@:${i}:1})*([^:])/}"
+newPATH="${newPATH//:${@:${i}:1}*([^:])/}" # s/://path[^:]*//g
Escribí una respuesta here (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 .
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" ":")
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" ":"`
PD: No sé cómo hacer que mis ticks de atrás se muestren en el código. Entonces, las alternativas son:
PATH=$(echo $PATH | sed ''s/:[^:]*$1[^:]*//g'')
PATH=$(echo $PATH | tr ":" "/n" | grep -v $1 | tr "/n" ":")
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; }
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''
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.
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"
}
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"
Mi sucio truco:
echo ${PATH} > t1
vi t1
export PATH=$(cat t1)
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 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 :)
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")
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 dotfiles . Admiten completamente eliminar / agregar / anteponer / insertar al principio / medio / final de la cadena PATH.
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