Cómo abreviar / ruta / a / archivo a / p / t / archivo

9

Estoy buscando una línea elegante (p. Ej. awk) Que acorte una cadena de una ruta Unix usando el primer carácter de cada nivel primario / intermedio, pero el nombre base completo. Más fácil de mostrar con ejemplos:

  • /path/to/file/p/t/file
  • /tmp/tmp
  • /foo/bar/.config/wizard_magic/f/b/./wizard_magic
  • /foo/bar/.config/wizard_magic/f/b/.c/wizard_magic
    A la luz de los buenos puntos de @ MichaelKjörling y @ChrisH a continuación, este ejemplo muestra cómo podemos mostrar los dos primeros caracteres cuando el primer carácter es un punto.
Joshua Huber
fuente
Una sugerencia (no sé tu caso de uso): abrevia en su lugar a /f/b/.c/wizard_magic. El punto es a menudo tan común en un directorio particular que puede ser una pista muy pequeña de dónde debería estar buscando.
Chris H
Además de lo que dijo @ChrisH, .normalmente solo significa "directorio actual". Entonces /f/b/./wizard_magices lo mismo que /f/b/wizard_magicporque el elemento de ruta se ./comprime en un elemento de ruta vacío.
un CVn
¿Por qué necesitas eso? ¿No puedes usar un poco de terminación automática inteligente en su shell interactivo (tal vez cambiar su shell a algo adecuado)
Basile Starynkevitch

Respuestas:

7

Para este archivo de prueba:

$ cat path
/path/to/file
/tmp
/foo/bar/.config/wizard_magic

Las abreviaturas se pueden generar con este código awk:

$ awk -F/ '{for (i=1;i<NF;i++) $i=substr($i,1,1)} 1' OFS=/ path
/p/t/file
/tmp
/f/b/./wizard_magic

Edit1: uso de dos caracteres para nombres de puntos

Esta versión abrevia los nombres de directorio a un carácter, excepto los nombres que comienzan con los .cuales se abrevian a dos caracteres:

$ awk -F/ '{for (i=1;i<NF;i++) $i=substr($i,1,1+($i~/^[.]/))} 1' OFS=/ path
/p/t/file
/tmp
/f/b/.c/wizard_magic

Cómo funciona

  • -F/

    Esto le dice a awk que use una barra como el separador de campo en la entrada.

  • for (i=1;i<NF;i++) $i=substr($i,1,1)

    Esto recorre cada campo, excepto el último, y lo reemplaza solo con su primer carácter.

    EDITAR1: en la versión revisada, hacemos la longitud de la subcadena 2 cuando el campo comienza con ..

  • 1

    Esto le dice a awk que imprima la línea revisada.

  • OFS=/

    Esto le dice a awk que use una barra como el separador de campo en la salida.

John1024
fuente
Excelente respuesta, modificación menor para usar el separador: awk -F/ '{for (i=1;i<NF;i++) $i=substr($i,1,1+($i~/^[.]/))(i==1||length($i)<2?"":"‥")} 1' OFS=/ <<<$PWDda: /foo/bar/.config/wizard_magic/f‥/b‥/.c‥/wizard_magic
ideasman42
12

Bastante fácil en sed (suponiendo que no haya nuevas líneas en los nombres de archivo):

sed 's!\([^/]\)[^/]*/!\1/!g'

Menos fácil en awk porque carece de referencias (excepto en Gawk, pero con una sintaxis torpe):

awk -v FS=/ -v OFS=/ '{for (i=1; i<NF; i++) $i=substr($i,1,1)} 1'

En zsh (con la ruta en $full_path):

echo "${(j:/:)${(@r:1:)${(@s:/:)${full_path:h}}}}/${full_path:t}"
Gilles 'SO- deja de ser malvado'
fuente
2
IIRC, "backreferences" son referencias a grupos de captura que ocurren en el patrón, no en la cadena de reemplazo.
Rhymoid
@Rhymoid \1en la cadena de reemplazo no significa una referencia a un grupo de captura en el patrón. Una referencia inversa es una referencia inversa sin importar dónde la use.
Gilles 'SO- deja de ser malvado'
8

puedes hacerlo como:

cd /usr///.//share/../share//man/man1 || exit
IFS=/; set -f
printf %.1s/  ${PWD%/*}
printf %s\\n "${PWD##*/}"

/u/s/m/man1

y aquí hay un sed:

printf %s "$file" |
tr /\\n \\n/      | sed -et$ \
    -e '\|^\.\.$|{x;s|\(.*\)\n.*$|\1|;x;}'  \
    -e 's|^\.\{0,2\}$||;\|.|H;$!d;x'        \
-e$ -e '\|\(\.\{0,2\}.\)\(.*\)\(\n\)|!b'    \
    -e 's||\1\3\2\3|;P;s|\n||;D' |
tr /\\n \\n/

eso se acerca bastante a hacer las mismas cosas que la función hace a continuación. no se abrevia con tildes ni se inserta $PWDen la cabeza para una barra no inicial como lo hace la función (y, de hecho, nunca imprime la barra diagonal), pero eso podría manejarse después. procesa componentes de ruta nula y puntos únicos, y elimina los ..casos.

dada la misma manruta que la cdanterior imprime:

u/s/m/man1

también imprimirá uno o dos puntos iniciales adicionales para cada componente de ruta que comience con tal y no sea solo uno o dos puntos.

usted preguntó por hacer más que el comienzo de caracteres para un trayecto de componente con una .. para hacerlo, pensé que cada componente necesitaría atención individual de todos modos, y como tenía curiosidad, intenté descifrar una ruta canónica sin el directorio de cambio. Después de un poco de prueba y error, finalmente decidí que la única forma de hacerlo bien era hacerlo dos veces, hacia atrás y hacia adelante:

pathbytes(){
    local IFS=/   o="$-" p
    set -f${ZSH_VERSION+LFy}
    set -- ${1:-$PWD}
    for p   in      /${1:+$PWD} $*
    do      case    $p in   (.|"")  ;;
            (..)    ${1+shift}      ;;
            (/)     set --          ;;
            (*)     set -- $p $*;   esac
    done
    for p   in      //$* ""
    do      case   ${p:-/$3}        in
            ([!./]*)                ;;
            (..*)   set "..$@"      ;;
            (.*)    set ".$@"       ;;
            (//*) ! set "" $1 $1    ;;
            (~)   ! p=\~            ;;
            (~/*)   p="~/$2";set $HOME
                  ! while "${2+shift}" 2>&3
                    do   p="~/${p#??*/}"
                    done 3>/dev/null;;
            esac&&  set ""  "${p%"${p#$1?}"}/$2" "$p/$3"
    done;   printf %s\\n "${p:-$2}"
    set +f  "-${o:--}"
}

para que nunca cambie el directorio o intente confirmar la existencia de algún componente de ruta, pero exprime /delimitadores repetidos y descarta /./por completo los componentes de un solo punto, y procesa /../los componentes de doble punto de manera apropiada.

cuando $IFSse establece en algún carácter que no sea un espacio en blanco , una secuencia de dos o más $IFScaracteres dará como resultado uno o más campos nulos. por lo que varias barras inclinadas consecutivas resultan en argumentos de valor nulo. Lo mismo es cierto para un $IFSpersonaje principal . y así, cuando se set -- $1divide, si el resultado $1es nulo, entonces comenzó con una barra, de lo contrario, ${1:+$PWD}si no es nulo, entonces lo inserto $PWD. en otras palabras, si el primer argumento no comienza con una barra inclinada, se $PWDantepondrá. eso es lo más cercano a la validación de ruta .

de lo contrario, el primer forbucle invierte recursivamente el orden de los componentes de la ruta, como:

      1 2 3
1     2 3
2 1   3
3 2 1

... mientras lo hace, ignora cualquier componente de punto único o nulo, y para ..ello ...

      1 .. 3
1     .. 3
      3
3

... la segunda pasada invierte este efecto, y mientras lo hace, exprime cada componente a 2 puntos + char , o 1 punto + char , o char .

por lo que debería funcionar en un camino canónico independientemente de la existencia.

Agregué / resté un poco al segundo bucle. ahora setes menos frecuente (solo una vez para cada [!./]*componente) , y las caseevaluaciones de patrones de cortocircuitos la mayor parte del tiempo (gracias al patrón mencionado anteriormente) , e incluye una evaluación de coincidencia de llamada de cola contra ~. si todas o una parte inicial (como se divide en componentes completos) de la ruta canónica finalmente puede coincidir ~, el bit de coincidencia se eliminará y ~se sustituirá un literal . Para hacer esto, tuve que mantener una copia completa de la ruta junto a la abreviada (porque hacer coincidir la ruta abreviada con ~probablemente no sería muy útil) , y así se mantiene $3. el últimowhileLa rama de bucle solo se ejecuta si ~coincide con un subconjunto de $3.

si lo ejecuta con el set -xrastreo habilitado, puede verlo funcionar.

$ (set -x;pathbytes ..abc/def/123///././//.././../.xzy/mno)
+ pathbytes ..abc/def/123///././//.././../.xzy/mno
+ local IFS=/ o=xsmi p
+ set -f
+ set -- ..abc def 123   . .   .. . .. .xzy mno
+ set --
+ set -- home
+ set -- mikeserv home
+ set -- ..abc mikeserv home
+ set -- def ..abc mikeserv home
+ set -- 123 def ..abc mikeserv home
+ shift
+ shift
+ set -- .xzy ..abc mikeserv home
+ set -- mno .xzy ..abc mikeserv home
+ set  mno mno
+ set . mno mno
+ set  .x/mno .xzy/mno
+ set .. .x/mno .xzy/mno
+ set  ..a/.x/mno ..abc/.xzy/mno
+ set  m/..a/.x/mno mikeserv/..abc/.xzy/mno
+ set  h/m/..a/.x/mno home/mikeserv/..abc/.xzy/mno
+ p=~/h/m/..a/.x/mno
+ set  home mikeserv
+ shift
+ p=~/m/..a/.x/mno
+ shift
+ p=~/..a/.x/mno
+
+ printf %s\n ~/..a/.x/mno
~/..a/.x/mno
+ set +f -xsmi
mikeserv
fuente
44
Genial, pero me duelen los ojos.
Glenn Jackman
1
@don_crissti - ¡sí!
mikeserv
2

El tema Zsh "a pescado" de Oh My Zsh contiene un fragmento de Perl para hacer exactamente eso que tiene soporte Unicode:

perl -pe '
   BEGIN {
      binmode STDIN,  ":encoding(UTF-8)";
      binmode STDOUT, ":encoding(UTF-8)";
   }; s|^$HOME|~|g; s|/([^/.])[^/]*(?=/)|/$1|g; s|/\.([^/])[^/]*(?=/)|/.$1|g;
'
nwk
fuente
1

¿Quieres tener un nombre corto o usarlo para tu línea de comando?
Para la línea de comandos, tengo las siguientes sugerencias:
¿No te ayuda completar el archivo en tu shell?
A veces tienes suerte y no tienes que hacer algo especial:

# /path/to/file -> /p/t/file
ls -l /*/*/file 

# /tmp -> /tmp
cd /tmp

# /foo/bar/.config/wizard_magic -> /f/b/./wizard_magic
ls -l /*/*/*/wizard_magic -> /f/b/./wizard_magic

Cuando solo tiene algunos directorios que le interesan, puede usar alias:

alias cdto="cd /path/to"
alias cdtmp="cd /tmp"
alias cdcfg="cd /foo/bar/.config"
alias cddeep="cd /home/john/workdir/project1/version3/maven/x/y/z/and/more"

O puede configurar variables para sus directorios favoritos

export p="/path/to"
export f="/foo/bar/.config"
ls -l $p/file
ls -l $f/wizard_magic

Creo que estas opciones tienen más sentido que intentar resolver esto con una función definida en .bashrc (o .profile) como

function x { 
   xxpath=""
   while [ $# -ne 0 ]; do
     xxpath+="${1}*/"
     shift
   done
   cd $(echo "${xxpath}")
}

y llamando a esta función x con espacios entre sus letras:

 # cd /path/to
 x /p t

 # cd /tmp 
 x /t

 # cd /foo/bar/.config
 x /f b 
Walter A
fuente