Como abreviar / path / to / file para / p / t / file

9

Eu estou procurando por uma linha única elegante (por exemplo, awk) que encurte uma string de um caminho Unix usando o primeiro caractere de cada nível pai / intermediário, mas o nome completo da base. Mais fácil de mostrar por exemplos:

  • /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
    À luz dos pontos positivos de @ MichaelKjörling e @ChrisH abaixo, este exemplo mostra como podemos mostrar os dois primeiros caracteres quando o primeiro caractere é um ponto.
Joshua Huber
fonte
Uma sugestão (não conheço seu caso de uso): abrevie para /f/b/.c/wizard_magic. O ponto geralmente é tão comum em um diretório específico que é uma pista muito pequena de onde você deve procurar.
Chris H
Além do que o @ChrisH disse, .normalmente significa apenas "diretório atual". O /f/b/./wizard_magicmesmo acontece com o /f/b/wizard_magicfato de o elemento path ser ./compactado em um elemento path vazio.
a CVn
Por que você precisa daquilo? Você não pode usar alguns autocompletion inteligente em sua shell interativo (talvez mudar seu shell para algo adequado)
Basile Starynkevitch

Respostas:

7

Para este arquivo de teste:

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

As abreviações podem ser geradas com 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: Usando dois caracteres para nomes de pontos

Esta versão abrevia nomes de diretório para um caractere, exceto para nomes que começam com os .quais são abreviados para dois 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

Como funciona

  • -F/

    Isso diz ao awk para usar uma barra como o separador de campos na entrada.

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

    Isso faz um loop em cada campo, exceto o último, e o substitui apenas pelo seu primeiro caractere.

    EDIT1: Na versão revisada, aumentamos o comprimento da substring 2 quando o campo começa com ..

  • 1

    Isso informa ao awk para imprimir a linha revisada.

  • OFS=/

    Isso informa ao awk para usar uma barra como o separador de campos na saída.

John1024
fonte
Resposta excelente, pequena modificação para usar o separador: awk -F/ '{for (i=1;i<NF;i++) $i=substr($i,1,1+($i~/^[.]/))(i==1||length($i)<2?"":"‥")} 1' OFS=/ <<<$PWDdá: /foo/bar/.config/wizard_magic/f‥/b‥/.c‥/wizard_magic
ideasman42
12

Muito fácil no sed (supondo que não haja novas linhas nos nomes dos arquivos):

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

Menos fácil no awk porque não possui referências anteriores (exceto no Gawk, mas com uma sintaxe desajeitada):

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

No zsh (com o caminho $full_path):

echo "${(j:/:)${(@r:1:)${(@s:/:)${full_path:h}}}}/${full_path:t}"
Gilles 'SO- parar de ser mau'
fonte
2
IIRC, "backreferences" são referências para capturar grupos que ocorrem no padrão, não na sequência de substituição.
Rhymoid
@Rhymoid \1no texto de substituição não significa uma referência a um grupo de captura no padrão. Uma referência anterior é uma referência anterior, não importa onde você a utilize.
Gilles 'SO- stop be evil'
8

você pode fazer isso como:

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

/u/s/m/man1

e aqui está um 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/

que chega bem perto de fazer as mesmas coisas que a função faz abaixo. ele não abrevia com tildes ou insere a $PWDcabeça de uma não barra invertida como a função (e de fato, nunca imprime a barra invertida), mas isso pode ser tratado posteriormente. ele processa componentes de caminho nulo, pontos únicos e elimina ..casos.

dado o mesmo mancaminho cdacima, ele imprime:

u/s/m/man1

também imprimirá um ou dois pontos iniciais extras para cada componente do caminho que começa com esse e não é apenas um ou dois pontos.

você perguntou sobre fazer mais do que o caractere para um componente de caminho que começa com a .. para fazer isso, imaginei que cada componente precisaria de atenção individual de qualquer maneira e, como estava curioso, tentei trabalhar um caminho canônico sem o diretório de alterações. depois de alguma tentativa e erro, decidi que a única maneira de fazer isso direito era fazê-lo duas vezes - para trás e para frente:

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 mude o diretório ou tente confirmar a existência de qualquer componente do caminho, mas aperta /delimitadores repetidos e elimina /./completamente os componentes de ponto único e processa /../os componentes de ponto duplo adequadamente.

Quando $IFSé definido como um caractere que não é um espaço em branco , uma sequência de dois ou mais $IFScaracteres resultará em um ou mais campos nulos. para que várias barras consecutivas funcionem com argumentos de valor nulo. o mesmo vale para um $IFSpersonagem principal . e assim, quando se set -- $1divide, se o resultado $1for nulo, ele começará com uma barra, caso contrário, ${1:+$PWD}se não for nulo, insiro $PWD. em outras palavras, se o primeiro argumento não começar com uma barra, ele será $PWDanexado. é o mais próximo possível da validação de caminho .

caso contrário, o primeiro forloop inverte recursivamente a ordem dos componentes do caminho, como:

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

... ao fazer isso, ignora qualquer componente de ponto único ou nulo, e por ..isso ...

      1 .. 3
1     .. 3
      3
3

... a segunda passagem inverte esse efeito e, ao fazê-lo, comprime cada componente em 2 pontos + caractere ou 1 ponto + caractere ou caractere .

portanto, deve seguir um caminho canônico, independentemente da existência.

eu adicionei / subtraí um pouco ao segundo loop. agora seté menos frequente (apenas uma vez para cada [!./]*componente) e caseavaliações de padrão de curto-circuito na maioria das vezes (graças ao padrão mencionado acima) e inclui uma avaliação de correspondência de chamada de cauda contra ~. se todo ou uma parte inicial (dividida em componentes inteiros) do caminho finalmente canônico puder corresponder ~, o bit correspondente será retirado e um literal ~será substituído. para fazer isso, eu tive que manter uma cópia completa do caminho ao lado do abreviado também (porque combinar o caminho abreviado ~provavelmente não seria muito útil) , e isso é mantido $3. o últimowhileA ramificação de loop é executada apenas se ~corresponder a um subconjunto de $3.

se você executá-lo com o set -xrastreamento ativado, poderá vê-lo 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
fonte
4
Legal, mas meus olhos doem.
Glenn Jackman
1
@don_crissti - yes!
mikeserv
2

O tema Zsh "suspeito" do Oh My Zsh contém um trecho de Perl para fazer exatamente isso que tem suporte a Unicode:

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

Deseja ter o nome abreviado de s ou usá-lo para sua linha de comando?
Para a linha de comando, tenho as seguintes sugestões:
A conclusão do arquivo no seu shell não ajuda?
Às vezes, você tem sorte e não precisa fazer 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

Quando você tem apenas alguns diretórios nos quais está interessado, pode usar aliases:

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"

Ou você pode configurar variáveis ​​para seus dirs favoritos

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

Eu acho que essas opções fazem mais sentido do que tentar resolver isso com uma função definida em .bashrc (ou .profile) como

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

e chamando essa função x com espaços entre suas letras:

 # cd /path/to
 x /p t

 # cd /tmp 
 x /t

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