Comment abréger / chemin / vers / fichier en / p / t / fichier

9

Je recherche une ligne élégante (par exemple, awk) qui raccourcira une chaîne d'un chemin Unix en utilisant le premier caractère de chaque niveau parent / intermédiaire, mais le nom de base complet. Plus facile à montrer par des exemples:

  • /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
    À la lumière des bons points de @ MichaelKjörling et @ChrisH ci-dessous, cet exemple montre comment nous pouvons afficher les deux premiers caractères lorsque le premier caractère est un point.
Joshua Huber
la source
Une suggestion (je ne connais pas votre cas d'utilisation): abréger plutôt en /f/b/.c/wizard_magic. Le point est souvent si commun dans un répertoire particulier qu'il est un très petit indice de l'endroit où vous devriez chercher.
Chris H
En plus de ce que @ChrisH a dit, cela .signifie normalement "répertoire courant". C'est /f/b/./wizard_magicla même chose que /f/b/wizard_magicparce que l'élément de chemin se ./comprime en un élément de chemin vide.
un CVn du
Pourquoi as-tu besoin de cela? Tu ne peux pas utiliser une saisie semi - automatique intelligent dans votre shell interactif (peut - être changer votre shell à quelque chose adéquat)
Basile STARYNKEVITCH

Réponses:

7

Pour ce fichier de test:

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

Les abréviations peuvent être générées avec ce code 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: Utilisation de deux caractères pour les noms de points

Cette version abrège les noms de répertoire en un seul caractère, à l'exception des noms commençant par .qui sont abrégés en deux caractères:

$ 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

Comment ça fonctionne

  • -F/

    Cela indique à awk d'utiliser une barre oblique comme séparateur de champ en entrée.

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

    Cela boucle sur chaque champ, sauf le dernier, et le remplace par son premier caractère uniquement.

    EDIT1: Dans la version révisée, nous faisons la longueur de la sous-chaîne 2 lorsque le champ commence par ..

  • 1

    Cela indique à awk d'imprimer la ligne révisée.

  • OFS=/

    Cela indique à awk d'utiliser une barre oblique comme séparateur de champ en sortie.

John1024
la source
Excellente réponse, modification mineure pour utiliser le séparateur: awk -F/ '{for (i=1;i<NF;i++) $i=substr($i,1,1+($i~/^[.]/))(i==1||length($i)<2?"":"‥")} 1' OFS=/ <<<$PWDdonne: /foo/bar/.config/wizard_magic/f‥/b‥/.c‥/wizard_magic
ideasman42
12

Assez facile dans sed (en supposant qu'il n'y ait pas de nouvelle ligne dans les noms de fichiers):

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

Moins facile dans awk car il manque de références (sauf dans Gawk, mais avec une syntaxe maladroite):

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

En zsh (avec le chemin d'accès $full_path):

echo "${(j:/:)${(@r:1:)${(@s:/:)${full_path:h}}}}/${full_path:t}"
Gilles 'SO- arrête d'être méchant'
la source
2
IIRC, "backreferences" sont des références à des groupes de capture qui se produisent dans le modèle, pas dans la chaîne de remplacement.
Rhymoid
@Rhymoid \1dans la chaîne de remplacement ne signifie pas une référence à un groupe de capture dans le motif. Une référence est une référence, peu importe où vous l'utilisez.
Gilles 'SO- arrête d'être méchant'
8

vous pouvez le faire comme:

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

/u/s/m/man1

et voici 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/

qui est assez proche de faire toutes les mêmes choses que la fonction ci-dessous. il n'abrège pas de tildes ni n'insère le $PWDdans la tête pour une non-barre oblique comme le fait la fonction (et en fait, n'imprime jamais la barre oblique) mais cela pourrait être géré par la suite. il traite les composants de chemin nul et les points uniques et élimine les ..cas.

étant donné le même manchemin que cdci-dessus, il imprime:

u/s/m/man1

il imprimera également un ou deux points de tête supplémentaires pour chaque composant de chemin commençant par tel et non pas seulement un ou deux points.

vous avez demandé de faire plus que le seul caractère d'un composant de chemin commençant par un .. pour ce faire, je pensais que chaque composant aurait besoin d'une attention individuelle de toute façon, et parce que j'étais curieux, j'ai essayé de trouver un chemin canonique sans le répertoire de changement. après quelques essais et erreurs, j'ai finalement décidé que la seule façon de le faire correctement était de le faire deux fois - en arrière et en avant:

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:--}"
}

de sorte que ne modifie jamais le répertoire ou n'essaie pas de confirmer l'existence d'un composant de chemin, mais il serre les /délimiteurs répétés et supprime /./entièrement les composants à point unique, et traite /../les composants à double point de manière appropriée.

lorsque $IFSest défini sur un caractère non blanc , une séquence de deux caractères ou plus $IFSse traduira par un ou plusieurs champs nuls. donc plusieurs barres obliques consécutives entraînent des arguments de valeur nulle. il en va de même pour un $IFSpersonnage principal . et donc quand set -- $1se divise, si le résultat $1est nul alors il a commencé par une barre oblique, sinon, ${1:+$PWD}s'il n'est pas nul, alors j'insère $PWD. en d'autres termes, si le premier argument ne commence pas par une barre oblique, il sera $PWDajouté au début . c'est aussi proche que cela vient de la validation du chemin .

sinon, la première forboucle inverse récursivement l'ordre des composants du chemin, comme:

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

... ce faisant, il ignore tous les composants à point unique ou nul, et pour ..cela ...

      1 .. 3
1     .. 3
      3
3

... le deuxième passage inverse cet effet, et ce faisant , il comprime chaque composant soit 2-points + carbonisation , ou 1-Dot + carbonisation ou de carbonisation .

il devrait donc fonctionner sur un chemin canonique indépendamment de l'existence.

j'ai ajouté / soustrait un peu à la deuxième boucle. elle est désormais setmoins fréquente (une seule fois pour chaque [!./]*composant) , et court-circuite les caseévaluations de modèles la plupart du temps (grâce au modèle susmentionné) , et inclut une évaluation de correspondance d'appel final ~. si tout ou une partie principale (divisée sur des composants entiers) du chemin finalement canonique peut correspondre ~, le bit correspondant sera supprimé et un littéral ~sera substitué. Pour ce faire, j'ai dû également conserver une copie complète du chemin à côté de l'abrégé (car faire correspondre le chemin abrégé à ~ne serait probablement pas très utile) , et donc cela est conservé $3. le dernierwhilela branche de boucle n'est exécutée que si elle ~correspond à un sous-ensemble de $3.

si vous l'exécutez avec set -xtrace activée, vous pouvez le regarder fonctionner.

$ (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
la source
4
Cool, mais mes yeux me font mal.
glenn jackman
1
@don_crissti - oui!
mikeserv
2

Le thème "fishy" Zsh de Oh My Zsh contient un extrait Perl pour faire exactement cela qui prend en charge Unicode:

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

Voulez-vous avoir un nom abrégé ou l'utiliser pour votre ligne de commande?
Pour la ligne de commande, j'ai les suggestions suivantes:
L'achèvement de fichier dans votre shell ne vous aide-t-il pas?
Parfois, vous avez de la chance et n'avez pas à faire quelque chose de spécial:

# /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

Lorsque vous n'avez que quelques répertoires qui vous intéressent, vous pouvez utiliser des 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"

Ou vous pouvez configurer des variables pour vos répertoires préférés

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

Je pense que ces options ont plus de sens que d'essayer de résoudre ce problème avec une fonction définie en .bashrc (ou .profile) comme

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

et en appelant cette fonction x avec des espaces entre vos lettres:

 # cd /path/to
 x /p t

 # cd /tmp 
 x /t

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