¿Cómo puedo agregar limpiamente a $ PATH?

31

Me gustaría una manera de agregar cosas a $ PATH, en todo el sistema o para un usuario individual, sin potencialmente agregar la misma ruta varias veces.

Una razón para querer hacer esto es para que se puedan hacer adiciones .bashrc, lo que no requiere un inicio de sesión, y también es más útil en sistemas que usan (por ejemplo) lightdm, que nunca llama .profile.

Soy consciente de las preguntas que tratan sobre cómo limpiar duplicados de $ PATH, pero no quiero eliminar duplicados . Me gustaría una forma de agregar rutas solo si aún no están presentes.

encerrada dorada
fuente
posible duplicado de mantener duplicados fuera de $ PATH en la fuente
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
Goldi, no sé por qué, pero había visto tu primer comentario incluso con el vacío. Pero sí, los prefijos de nombre también funcionan, ¡no se preocupe! Cerrar por el otro lado también está bien.
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
Bien, siempre y cuando recibas mi mensaje. A veces, hacer una inversión como esta causa un poco de caos, creo que veremos qué sucede.
Ricitos
Vea también Agregar directorio a $ PATH si aún no está allí (en Super Usuario ).
G-Man dice 'restablecer a Monica' el

Respuestas:

35

Supongamos que la nueva ruta que queremos agregar es:

new=/opt/bin

Luego, usando cualquier shell POSIX, podemos probar para ver si newya está en la ruta y agregarlo si no lo está:

case ":${PATH:=$new}:" in
    *:"$new":*)  ;;
    *) PATH="$new:$PATH"  ;;
esac

Tenga en cuenta el uso de dos puntos. Sin los dos puntos, podríamos pensar que, por ejemplo, new=/binya estaba en el camino porque el patrón coincidía /usr/bin. Mientras que las RUTA normalmente tienen muchos elementos, también se manejan los casos especiales de cero y un elemento en la RUTA. El caso de la PATH inicialmente no tener elementos (siendo vacío) es manejado por el uso de ${PATH:=$new}que asigna PATHa $newsi está vacío. Establecer valores predeterminados para los parámetros de esta manera es una característica de todos los shells POSIX: consulte la sección 2.6.2 de los documentos POSIX ).

Una función invocable

Por conveniencia, el código anterior se puede poner en una función. Esta función se puede definir en la línea de comando o, para tenerla disponible permanentemente, ponerla en el script de inicialización de su shell (para usuarios bash, eso sería ~/.bashrc):

pupdate() { case ":${PATH:=$1}:" in *:"$1":*) ;; *) PATH="$1:$PATH" ;; esac; }

Para usar esta función de actualización de ruta para agregar un directorio a la RUTA actual:

pupdate /new/path
John1024
fuente
1
Puede guardar 2 distinciones de casos - cf. unix.stackexchange.com/a/40973/1131 .
maxschlepzig
3
Si PATHestá vacío, esto agregará una entrada vacía (es decir, el directorio actual) al PATH. Creo que necesitas otro caso.
CB Bailey
2
@CharlesBailey No es otro case. Solo hazlo case "${PATH:=$new}". Vea mi propia respuesta para fallos similares.
mikeserv
1
@ mc0e Agregué un ejemplo de cómo usar una función de shell para ocultar el "ruido de línea".
John1024
1
@Doogle: uniq solo detecta duplicados si están adyacentes entre sí, por lo que no creo que esto elimine los duplicados si aparece una ruta al principio y al final de $ PATH.
Ralph hace
9

Cree un archivo /etc/profile.dllamado, por ejemplo, mypath.sh(o lo que quiera). Si está utilizando lightdm, asegúrese de que sea viable o de lo contrario utilice /etc/bashrco un archivo procedente de la misma. Agregue a eso las siguientes funciones:

checkPath () {
        case ":$PATH:" in
                *":$1:"*) return 1
                        ;;
        esac
        return 0;
}

# Prepend to $PATH
prependToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$a:$PATH
                fi
        done
        export PATH
}

# Append to $PATH
appendToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$PATH:$a
                fi
        done
        export PATH
}

Las cosas al comienzo de (antes) $ PATH tienen prioridad sobre lo que sigue, y por el contrario, las cosas al final (agregadas) serán reemplazadas por lo que viene antes. Esto significa que si su $ PATH es /usr/local/bin:/usr/biny hay un ejecutable gotchaen ambos directorios, el de adentro /usr/local/binse usará por defecto.

Ahora puede, en este mismo archivo, en otro archivo de configuración de shell o desde la línea de comandos, usar:

appendToPath /some/path /another/path
prependToPath /some/path /yet/another/path

Si esto está en a .bashrc, evitará que el valor aparezca más de una vez cuando inicie un nuevo shell. Hay una limitación en que si desea agregar algo que se antepuso (es decir, mover una ruta dentro de $ PATH) o viceversa, tendrá que hacerlo usted mismo.

encerrada dorada
fuente
dividir el $PATHcon IFS=:finalmente es más flexible que case.
mikeserv
@mikeserv Sin duda. Este es un tipo de uso de pirateo para case, IMO. Me imagino que también awkpodría usarse aquí.
Ricitos
Ese es un buen punto. Y, como creo, gawkpodría asignar directamente $PATH.
mikeserv
5

Puedes hacerlo de esta manera:

echo $PATH | grep /my/bin >/dev/null || PATH=$PATH:/my/bin

Nota: si crea RUTA a partir de otras variables, verifique que no estén vacías, ya que muchos shells interpretan "" me gusta "". .

contramodo
fuente
+1 Según la página de manual, -qPOSIX requiere grep, pero no sé si eso significa que todavía hay algunos greps (no POSIX) que no lo tienen.
Ricitos
1
Tenga en cuenta que el patrón grep es demasiado amplio. Considere usar egrep -q "(^ |:) / my / bin (: | \ $)" en lugar de grep / my / bin> / dev / null. Con esa modificación, su solución es correcta, y creo que esta es una solución más legible que la respuesta actualmente preferida de @ john1024. Tenga en cuenta que he usado comillas dobles para que use la sustitución de variables en lugar de/my/bin
mc0e
5

La parte importante del código es verificar si PATHcontiene una ruta específica:

printf '%s' ":${PATH}:" | grep -Fq ":${my_path}:"

Es decir, asegúrese de que cada ruta en PATHesté delimitada en ambos lados por el PATHseparador ( :), luego verifique ( -q) si la cadena literal ( -F) que consiste en un PATHseparador, su ruta y otro PATHseparador existe allí. Si no es así, puede agregar la ruta de forma segura:

if ! printf '%s' ":${PATH-}:" | grep -Fq ":${my_path-}:"
then
    PATH="${PATH-}:${my_path-}"
fi

Esto debería ser compatible con POSIX y debería funcionar con cualquier ruta que no contenga un carácter de nueva línea. Es más complejo si desea que funcione con rutas que contengan nueva línea mientras sea compatible con POSIX, pero si tiene un grepsoporte -zque pueda usar, puede usarlo.

l0b0
fuente
4

He estado llevando esta pequeña función conmigo en varios ~/.profilearchivos durante años. Yo creo que fue escrito por el administrador de sistemas en un laboratorio que solía trabajar en, pero no estoy seguro. De todos modos, es similar al enfoque de Goldilock pero ligeramente diferente:

pathmunge () {
        if ! echo $PATH | /bin/grep -Eq "(^|:)$1($|:)" ; then
           if [ "$2" = "after" ] ; then
              PATH=$PATH:$1
           else
              PATH=$1:$PATH
           fi
        fi
}

Entonces, para agregar un nuevo directorio al comienzo de PATH:

pathmunge /new/path

y hasta el final:

pathmunge /new/path after
terdon
fuente
Esto funciona para mi! Pero cambié a la lógica para ponerlo después de forma predeterminada y anular con "antes". :)
Kevin Pauli
pathmunge es parte de la distribución de Linux Centos / etc / profile, tiene un parámetro antes y después. No lo veo en mi último ubuntu 16.
Kemin Zhou
Parece funcionar bien en macOS 10.12 después de /bin/grep->grep
Ben Creasy
4

ACTUALIZAR:

Me di cuenta de que su propia respuesta tenía una función separada cada una para agregar o anteponer al $PATH. Me gustó la idea. Así que agregué un pequeño manejo de argumentos. También lo _separé correctamente :

_path_assign() { oFS=$IFS ; IFS=: ; add=$* ; unset P A ; A=
    set -- ${PATH:=$1} ; for p in $add ; do {
        [ -z "${p%-[AP]}" ] && { unset P A
                eval ${p#-}= ; continue ; }
        for d ; do [ -z "${d%"$p"}" ] && break
        done ; } || set -- ${P+$p} $* ${A+$p}
        done ; export PATH="$*" ; IFS=$oFS
}

% PATH=/usr/bin:/usr/yes/bin
% _path_assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/bin/nope \
    -P \
    /usr/nope/bin \
    /usr/bin \
    -A \
    /nope/usr/bin \
    /usr/nope/bin

% echo $PATH

SALIDA:

/usr/nope/bin:/usr/bin:/usr/yes/bin:/usr/bin/nope:/nope/usr/bin

De forma predeterminada, -Adependerá de $PATH, pero puede modificar este comportamiento para que se -Pagregue agregando un -Plugar en su lista de argumentos. Puede volver a -Aponerlo en espera entregándolo -Anuevamente.

EVALO SEGURO

En la mayoría de los casos, recomiendo que las personas eviten el uso de eval. Pero esto, creo, se destaca como un ejemplo de su uso para bien. En este caso, la única declaración que eval puede ver es P=o A=. Los valores de sus argumentos se prueban estrictamente antes de que se llame. Esto es para lo que eval sirve.

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- ${PATH:=$1} ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH="$*" ; IFS=$oFS
}

Esto aceptará tantos argumentos como le des y agregará cada uno $PATHsolo una vez y solo si aún no está en $PATH. Solo utiliza el script de shell POSIX totalmente portátil, se basa solo en los shell incorporados y es muy rápido.

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin
mikeserv
fuente
@ TAFKA'goldilocks 'vea la actualización aquí: me inspiró.
mikeserv
+1 Por curiosidad (tal vez esta sería una buena pregunta y respuesta por separado), ¿de dónde _viene la idea de que las funciones de prefijo de shell las hacen "espaciadas correctamente"? En otros idiomas, generalmente indicaría una función global interna (es decir, una que debe ser global, pero no está destinada a ser utilizada externamente como parte de una API). Mis nombres ciertamente no son buenas opciones, pero me parece que el solo uso _no resuelve los problemas de colisión en absoluto; sería mejor agregar un espacio de nombres real, por ejemplo. mikeserv_path_assign().
Ricitos
@ TAFKA 'goldilocks': sería mejor ser aún más específico con él, pero cuanto más largo sea el nombre, menos conveniente será su uso. Pero si tiene un binario ejecutable apropiado con el prefijo, _entonces necesita cambiar los administradores de paquetes. En cualquier caso, esto, esencialmente, es solo una "función global, interna" : es global para cada shell invocado desde el shell en el que se declara, y es solo un poco de script de lenguaje interpretado que cuelga en la memoria del intérprete . unix.stackexchange.com/questions/120528/…
mikeserv
¿No puedes unset a(o equivalente) al final del perfil?
sourcejedi
0

¡Mirad! La función de 12 líneas de fuerza industrial ... técnicamente bash y zsh-portable que adora tu script de inicio ~/.bashrco ~/.zshrcde elección:

# void +path.append(str dirname, ...)
#
# Append each passed existing directory to the current user's ${PATH} in a
# safe manner silently ignoring:
#
# * Relative directories (i.e., *NOT* prefixed by the directory separator).
# * Duplicate directories (i.e., already listed in the current ${PATH}).
# * Nonextant directories.
+path.append() {
    # For each passed dirname...
    local dirname
    for   dirname; do
        # Strip the trailing directory separator if any from this dirname,
        # reducing this dirname to the canonical form expected by the
        # test for uniqueness performed below.
        dirname="${dirname%/}"

        # If this dirname is either relative, duplicate, or nonextant, then
        # silently ignore this dirname and continue to the next. Note that the
        # extancy test is the least performant test and hence deferred.
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue

        # Else, this is an existing absolute unique dirname. In this case,
        # append this dirname to the current ${PATH}.
        PATH="${PATH}:${dirname}"
    done

    # Strip an erroneously leading delimiter from the current ${PATH} if any,
    # a common edge case when the initial ${PATH} is the empty string.
    PATH="${PATH#:}"

    # Export the current ${PATH} to subprocesses. Although system-wide scripts
    # already export the ${PATH} by default on most systems, "Bother free is
    # the way to be."
    export PATH
}

Prepárate para la gloria instantánea. Entonces, en lugar de hacer esto y deseando lo mejor:

export PATH=$PATH:~/opt/bin:~/the/black/goat/of/the/woods/with/a/thousand/young

Haga esto en su lugar y tenga la garantía de obtener lo mejor, ya sea que realmente quiera eso o no:

+path.append ~/opt/bin ~/the/black/goat/of/the/woods/with/a/thousand/young

Muy bien, defina "Mejor".

Anexar y anteponer con seguridad a la corriente ${PATH}no es el asunto trivial que comúnmente se supone que es. Si bien son convenientes y aparentemente razonables, las frases ingeniosas del formulario export PATH=$PATH:~/opt/bininvitan a complicaciones diabólicas con:

  • Nombres de directorio relativos accidentalmente (por ejemplo, export PATH=$PATH:opt/bin). Si bien bashy en zshsilencio aceptan e ignoran en su mayoría los nombres de directorios relativos en la mayoría de los casos, los nombres de directorios relativos con el prefijo uno ho t(y posiblemente otros caracteres nefastos) hacen que ambos se mutilen vergonzosamente por Harakiri, la obra maestra seminal de 1962 de Masaki Kobayashi :

    # Don't try this at home. You will feel great pain.
    $ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:harakiri && echo $PATH
    /usr/local/bin:/usr/bin:arakiri
    $ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:tanuki/yokai && echo $PATH
    binanuki/yokai   # Congratulations. Your system is now face-up in the gutter.
  • Accidentalmente nombres de directorio duplicados. Si bien los ${PATH}nombres de directorio duplicados son en gran medida inocuos, también son indeseados, engorrosos, ligeramente ineficientes, impiden la depuración y promueven el desgaste de la unidad, como esta respuesta. Mientras que los SSD de estilo NAND son ( por supuesto ) inmunes al desgaste de lectura, los HDD no lo son. El acceso innecesario al sistema de archivos en cada intento de comando implica un desgaste innecesario del cabezal de lectura al mismo tempo. Los duplicados son particularmente untuosos cuando se invocan proyectiles anidados en subprocesos anidados, momento en el cual las frases ingenuas aparentemente inocuas export PATH=$PATH:~/watexplotan rápidamente en el Séptimo Círculo del ${PATH}Infierno PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat. Solo Beelzebubba puede ayudarlo si luego agrega nombres de directorio adicionales. (No dejes que esto le pase a tus preciosos hijos. )

  • Nombres de directorio que faltan accidentalmente. Una vez más, si bien los ${PATH}nombres de directorio faltantes son en gran medida inocuos, también suelen ser no deseados, engorrosos, ligeramente ineficientes, impiden la depuración y promueven el desgaste de la unidad.

Ergo, automatización amigable como la función de shell definida anteriormente. Debemos salvarnos de nosotros mismos.

Pero ... ¿Por qué "+ path.append ()"? ¿Por qué no simplemente append_path ()?

Para de desambiguación (por ejemplo, con comandos externos en las actuales ${PATH}funciones de shell o del sistema definida en otra parte), funciones de shell definidos por el usuario son idealmente el prefijo o sufijo única subseries soportados por bashy zshpero prohibidos de otro modo para nombres base comando estándar - como, por ejemplo, +.

Oye. Funciona. No me juzgues

Pero ... ¿Por qué "+ path.append ()"? ¿Por qué no "+ path.prepend ()"?

Debido a que agregar a la corriente ${PATH}es más seguro que anteponer a la corriente ${PATH}, todas las cosas son iguales, lo que nunca son. Anular comandos de todo el sistema con comandos específicos del usuario puede ser insalubre en el mejor de los casos y loco en el peor En Linux, por ejemplo, las aplicaciones posteriores comúnmente esperan las variantes de comandos GNU coreutils en lugar de las derivadas o alternativas no estándar personalizadas.

Dicho esto, absolutamente hay casos de uso válidos para hacerlo. Definir la +path.prepend()función equivalente es trivial. Sans prolix nebulosity, por su cordura compartida:

+path.prepend() {
    local dirname
    for dirname in "${@}"; do
        dirname="${dirname%/}"
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue
        PATH="${dirname}:${PATH}"
    done
    PATH="${PATH%:}"
    export PATH
}

Pero ... ¿por qué no Gilles?

La respuesta aceptada de Gilles en otra parte es impresionantemente óptima en el caso general como un "agregado idempotente agnóstico de caparazón" . En el caso común de bashy zshcon no hay enlaces simbólicos indeseables, sin embargo, la penalización de rendimiento requerido para ello entristece la arrocera de Gentoo en mí. Incluso en presencia de enlaces simbólicos indeseables, es discutible si la bifurcación de un subshell por add_to_PATH()argumento vale la inserción potencial de duplicados de enlaces simbólicos.

Para casos de uso estrictos que exigen que se eliminen incluso los duplicados de enlaces simbólicos, esta zshvariante específica lo hace a través de builtins eficientes en lugar de tenedores ineficientes:

+path.append() {
    local dirname
    for   dirname in "${@}"; do
        dirname="${dirname%/}"
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname:A}:"* &&
           -d "${dirname}" ]] || continue
        PATH="${PATH}:${dirname}"
    done
    PATH="${PATH#:}"
    export PATH
}

Tenga en cuenta el *":${dirname:A}:"*más que *":${dirname}:"*el original. :Aes un zshismos maravilloso tristemente ausente debajo de la mayoría de los otros caparazones, incluido bash. Para citar man zshexpn:

R : Convierta un nombre de archivo en una ruta absoluta como lo hace el amodificador, y luego pase el resultado a través de la realpath(3)función de biblioteca para resolver enlaces simbólicos. Nota: en los sistemas que no tienen una realpath(3)función de biblioteca, enlaces simbólicos no se resuelven, por lo que en esos sistemas a, y Ason equivalentes.

No más preguntas.

De nada. Disfruta del bombardeo seguro. Ahora te lo mereces.

Cecil Curry
fuente
0

Aquí está mi versión de estilo de programación funcional.

  • Funciona para cualquier *PATHvariable delimitada por dos puntos , no solo PATH.
  • No accede al estado global
  • Solo funciona con / en sus entradas inmutables dadas
  • Produce una salida única
  • Sin efectos secundarios
  • Memoizable (en principio)

También digno de mención:

  • Agnóstico con respecto a exporting; eso le queda a la persona que llama (ver ejemplos)
  • Puro bash; sin bifurcación
path_add () {
  # $ 1: Elemento para asegurar que está en la cadena de ruta dada exactamente una vez
  # $ 2: valor de cadena de ruta existente ("$ PATH", no "PATH")
  # $ 3 (opcional, cualquier cosa): si se proporciona, agregue $ 1; de lo contrario, anteponer
  # #
  # Ejemplos:
  # $ export PATH = $ (path_add '/ opt / bin' "$ PATH")
  # $ CDPATH = $ (path_add '/ Music' "$ CDPATH" at_end)

  local -r already_present = "(^ |:) $ {1} ($ | :)"
  if [["$ 2" = ~ $ ya_presenta]]; luego
    echo "$ 2"
  elif [[$ # == 3]]; luego
    echo "$ {2}: $ {1}"
  más
    echo "$ {1}: $ {2}"
  fi
}
Phil Hudson
fuente
0

Este script le permite agregar al final de $PATH:

PATH=path2; add_to_PATH after path1 path2:path3
echo $PATH
path2:path1:path3

O agregue al comienzo de $PATH:

PATH=path2; add_to_PATH before path1 path2:path3
echo $PATH
path1:path3:path2

# Add directories to $PATH iff they're not already there
# Append directories to $PATH by default
# Based on https://unix.stackexchange.com/a/4973/143394
# and https://unix.stackexchange.com/a/217629/143394
add_to_PATH () {
  local prepend  # Prepend to path if set
  local prefix   # Temporary prepended path
  local IFS      # Avoid restoring for added laziness

  case $1 in
    after)  shift;; # Default is to append
    before) prepend=true; shift;;
  esac

  for arg; do
    IFS=: # Split argument by path separator
    for dir in $arg; do
      # Canonicalise symbolic links
      dir=$({ cd -- "$dir" && { pwd -P || pwd; } } 2>/dev/null)
      if [ -z "$dir" ]; then continue; fi  # Skip non-existent directory
      case ":$PATH:" in
        *":$dir:"*) :;; # skip - already present
        *) if [ "$prepend" ]; then
           # ${prefix:+$prefix:} will expand to "" if $prefix is empty to avoid
           # starting with a ":".  Expansion is "$prefix:" if non-empty.
            prefix=${prefix+$prefix:}$dir
          else
            PATH=$PATH:$dir  # Append by default
          fi;;
      esac
    done
  done
  [ "$prepend" ] && [ "$prefix" != "" ] && PATH=$prefix:$PATH
}
Tom Hale
fuente