web-dev-qa-db-fra.com

Comment normaliser un chemin de fichier dans Bash?

Je veux transformer /foo/bar/.. en /foo

Y at-il une commande bash qui fait cela?


Edit: dans mon cas pratique, le répertoire existe.

164
Fabien

si vous voulez supprimer une partie du nom de fichier du chemin, "dirname" et "basename" sont vos amis, et "realpath" est également pratique. 

dirname /foo/bar/baz 
# /foo/bar 
basename /foo/bar/baz
# baz
dirname $( dirname  /foo/bar/baz  ) 
# /foo 
realpath ../foo
# ../foo: No such file or directory
realpath /tmp/../tmp/../tmp
# /tmp

realpath alternatives

Si realpath n'est pas supporté par votre shell, vous pouvez essayer 

readlink -f /path/here/.. 

Également

readlink -m /path/there/../../ 

Fonctionne comme 

realpath -s /path/here/../../

en ce que le chemin n'a pas besoin d'exister pour être normalisé. 

161
Kent Fredric

Je ne sais pas s'il existe une commande directe bash pour le faire, mais je le fais habituellement

normalDir="`cd "${dirToNormalize}";pwd`"
echo "${normalDir}"

et ça marche bien.

86
Tim Whitcomb

Essayez realpath. Ci-dessous, la source dans son intégralité, par la présente, reversée au domaine public.

// realpath.c: display the absolute path to a file or directory.
// Adam Liss, August, 2007
// This program is provided "as-is" to the public domain, without express or
// implied warranty, for any non-profit use, provided this notice is maintained.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libgen.h>   
#include <limits.h>

static char *s_pMyName;
void usage(void);

int main(int argc, char *argv[])
{
    char
        sPath[PATH_MAX];


    s_pMyName = strdup(basename(argv[0]));

    if (argc < 2)
        usage();

    printf("%s\n", realpath(argv[1], sPath));
    return 0;
}    

void usage(void)
{
    fprintf(stderr, "usage: %s PATH\n", s_pMyName);
    exit(1);
}
53
Adam Liss

Une solution portable et fiable consiste à utiliser python, qui est préinstallé un peu partout (y compris Darwin). Vous avez deux options:

  1. abspath renvoie un chemin absolu mais ne résout pas les liens symboliques:

    python -c "import os,sys; print os.path.abspath(sys.argv[1])" path/to/file

  2. realpath renvoie un chemin absolu et résout ainsi les liens symboliques, générant un chemin canonique:

    python -c "import os,sys; print os.path.realpath(sys.argv[1])" path/to/file

Dans chaque cas, path/to/file peut être un chemin relatif ou absolu.

35
loevborg

Utilisez l'utilitaire readlink du package coreutils.

MY_PATH=$(readlink -f "$0")
34
mattalxndr

readlink est la norme bash pour obtenir le chemin absolu. Cela a aussi l'avantage de retourner des chaînes vides si un chemin ou un chemin n'existe pas (en fonction des drapeaux nécessaires).

Pour obtenir le chemin absolu d'un répertoire qui peut exister ou non, mais dont les parents existent, utilisez:

abspath=$(readlink -f $path)

Pour obtenir le chemin absolu d'un répertoire qui doit exister avec tous les parents:

abspath=$(readlink -e $path)

Pour canoniser le chemin donné et suivre les liens symboliques s’ils existent, sinon ignorer les répertoires manquants et simplement renvoyer le chemin, c’est:

abspath=$(readlink -m $path)

Le seul inconvénient est que readlink suivra les liens. Si vous ne souhaitez pas suivre les liens, vous pouvez utiliser cette convention alternative:

abspath=$(cd ${path%/*} && echo $PWD/${path##*/})

Cela permettra de passer à la partie répertoire de $ path et d’imprimer le répertoire actuel avec la partie fichier de $ path. Si chdir échoue, vous obtenez une chaîne vide et une erreur sur stderr.

13
Craig

Vieille question, mais il y a beaucoup plus simple si vous avez affaire à des noms de chemins complets au niveau de la coquille:

    abspath = "$ (cd" $ path "&& pwd)"

Comme le cd se passe dans un sous-shell, cela n’a pas d’impact sur le script principal.

Deux variantes, en supposant que vos commandes intégrées à Shell acceptent -L et -P, sont les suivantes:

 abspath = "$ (cd -P" $ chemin "&& pwd -P)" # chemin physique avec liens symboliques résolus 
 abspath = "$ (cd -L" $ chemin "&& pwd -L)" chemin_logique préservant les liens symboliques 

Personnellement, j’ai rarement besoin de cette approche ultérieure à moins que je ne sois fasciné par les liens symboliques pour une raison quelconque.

FYI: variation sur l’obtention du répertoire de départ d’un script qui fonctionne même si le script change son répertoire courant ultérieurement.

name0 = "$ (nom de base" $ 0 ")"; #base nom du script 
 dir0 = "$ (cd" $ (dirname "$ 0") "&& pwd)"; #absolute start dir

L'utilisation de CD garantit que vous avez toujours le répertoire absolu, même si le script est exécuté avec des commandes telles que ./script.sh qui, sans le cd/pwd, donnent souvent la valeur .. Inutile si le script effectue un cd ultérieurement. 

7
Gilbert

Comme Adam Liss l'a noté, la variable realpath n'est pas fournie avec chaque distribution. Ce qui est dommage, car c'est la meilleure solution. Le code source fourni est excellent, et je vais probablement commencer à l’utiliser maintenant. Voici ce que j'ai utilisé jusqu'à présent, que je partage ici pour être complet:

get_abs_path() {
     local PARENT_DIR=$(dirname "$1")
     cd "$PARENT_DIR"
     local ABS_PATH="$(pwd)"/"$(basename "$1")"
     cd - >/dev/null
     echo "$ABS_PATH"
} 

Si vous voulez résoudre les liens symboliques, remplacez simplement pwd par pwd -P.

7
Jeet

Ma solution récente était:

pushd foo/bar/..
dir=`pwd`
popd

Basé sur la réponse de Tim Whitcomb.

7
schmunk

Pas exactement une réponse, mais peut-être une question de suivi (la question initiale n'était pas explicite):

readlink convient si vous voulez réellement suivre les liens symboliques. Mais il existe également un cas d'utilisation consistant à normaliser simplement les séquences ./ et ../ et //, ce qui peut être fait de manière purement syntaxique, sans canonisation des liens symboliques. readlink n'est pas bon pour cela, et realpath non plus.

for f in $paths; do (cd $f; pwd); done

fonctionne pour les chemins existants, mais casse pour les autres.

Un script sed semble être un bon choix, sauf que vous ne pouvez pas remplacer les séquences de manière itérative (/foo/bar/baz/../.. -> /foo/bar/.. -> /foo) sans utiliser quelque chose comme Perl, qu'il n'est pas prudent d'assumer sur tous les systèmes, ou d'utiliser une boucle laide pour comparer la sortie de sed à son entrée.

FWIW, one-liner utilisant Java (JDK 6+):

jrunscript -e 'for (var i = 0; i < arguments.length; i++) {println(new Java.io.File(new Java.io.File(arguments[i]).toURI().normalize()))}' $paths
5
Jesse Glick

Réponse bavarde et un peu tardive. Je dois en écrire un car je suis coincé dans d'anciennes RHEL4/5 . Je gère les liens absolus et relatifs, et simplifie les entrées //, /./ et somedir /../.

test -x /usr/bin/readlink || readlink () {
        echo $(/bin/ls -l $1 | /bin/cut -d'>' -f 2)
    }


test -x /usr/bin/realpath || realpath () {
    local PATH=/bin:/usr/bin
    local inputpath=$1
    local changemade=1
    while [ $changemade -ne 0 ]
    do
        changemade=0
        local realpath=""
        local token=
        for token in ${inputpath//\// }
        do 
            case $token in
            ""|".") # noop
                ;;
            "..") # up one directory
                changemade=1
                realpath=$(dirname $realpath)
                ;;
            *)
                if [ -h $realpath/$token ] 
                then
                    changemade=1
                    target=`readlink $realpath/$token`
                    if [ "${target:0:1}" = '/' ]
                    then
                        realpath=$target
                    else
                        realpath="$realpath/$target"
                    fi
                else
                    realpath="$realpath/$token"
                fi
                ;;
            esac
        done
        inputpath=$realpath
    done
    echo $realpath
}

mkdir -p /tmp/bar
(cd /tmp ; ln -s /tmp/bar foo; ln -s ../.././usr /tmp/bar/link2usr)
echo `realpath /tmp/foo`
4
alhernau

Je suis en retard pour la fête, mais c'est la solution que j'ai élaborée après avoir lu un tas de sujets comme celui-ci:

resolve_dir() {
        (builtin cd `dirname "${1/#~/$HOME}"`'/'`basename "${1/#~/$HOME}"` 2>/dev/null; if [ $? -eq 0 ]; then pwd; fi)
}

Cela résoudra le chemin absolu de $ 1, jouera Nice avec ~, gardera les liens symboliques dans le chemin où ils se trouvent et cela ne gâchera pas votre pile de répertoires. Il retourne le chemin complet ou rien s'il n'existe pas. Il s'attend à ce que $ 1 soit un répertoire et échouera probablement si ce n'est pas le cas, mais c'est une vérification facile à faire par vous-même.

4
apottere

Essayez notre nouveau produit de bibliothèque Bash realpath-lib que nous avons placé sur GitHub pour un usage gratuit et sans entrave. Il est parfaitement documenté et constitue un excellent outil d'apprentissage. 

Il résout les chemins locaux, relatifs et absolus et n'a aucune dépendance, sauf Bash 4+; donc ça devrait marcher à peu près n'importe où. C'est gratuit, propre, simple et instructif.

Tu peux faire:

get_realpath <absolute|relative|symlink|local file path>

Cette fonction est le noyau de la bibliothèque:

function get_realpath() {

if [[ -f "$1" ]]
then 
    # file *must* exist
    if cd "$(echo "${1%/*}")" &>/dev/null
    then 
        # file *may* not be local
        # exception is ./file.ext
        # try 'cd .; cd -;' *works!*
        local tmppwd="$PWD"
        cd - &>/dev/null
    else 
        # file *must* be local
        local tmppwd="$PWD"
    fi
else 
    # file *cannot* exist
    return 1 # failure
fi

# reassemble realpath
echo "$tmppwd"/"${1##*/}"
return 0 # success

}

Il contient également des fonctions telles que get_dirname, get_filename, get_stname et validate_path. Essayez-le sur plusieurs plates-formes et contribuez à l'améliorer.

3
AsymLabs

D'après la réponse de @Andre, je pourrais avoir une version légèrement meilleure, au cas où quelqu'un voudrait une solution sans boucle et entièrement basée sur la manipulation de chaînes. C'est également utile pour ceux qui ne veulent pas déréférencer les liens symboliques, ce qui représente l'inconvénient d'utiliser realpath ou readlink -f.

Cela fonctionne sur les versions 3.2.25 et supérieures de bash.

shopt -s extglob

normalise_path() {
    local path="$1"
    # get rid of /../ example: /one/../two to /two
    path="${path//\/*([!\/])\/\.\./}"
    # get rid of /./ and //* example: /one/.///two to /one/two
    path="${path//@(\/\.\/|\/+(\/))//}"
    # remove the last '/.'
    echo "${path%%/.}"
}

$ normalise_path /home/codemedic/../codemedic////.config
/home/codemedic/.config
2
ϹοδεMεδιϲ

Le problème avec realpath est qu’il n’est pas disponible sous BSD (ni sous OSX). Voici une recette simple extraite de un article assez ancien (2009) de Linux Journal , qui est assez portable:

function normpath() {
  # Remove all /./ sequences.
  local path=${1//\/.\//\/}

  # Remove dir/.. sequences.
  while [[ $path =~ ([^/][^/]*/\.\./) ]]; do
    path=${path/${BASH_REMATCH[0]}/}
  done
  echo $path
}

Remarquez que cette variante ne nécessite également pas le {pas} _ chemin d'accès.

1
André Anjos

J'avais besoin d'une solution qui ferait les trois:

  • Travailler sur un Mac de stock. realpath et readlink -f sont des addons
  • Résoudre les liens symboliques
  • Avoir la gestion des erreurs

Aucune des réponses n’avait les numéros 1 et 2. J'ai ajouté le n ° 3 pour sauver les autres du rasage de yak.

#!/bin/bash

P="${1?Specify a file path}"

[ -e "$P" ] || { echo "File does not exist: $P"; exit 1; }

while [ -h "$P" ] ; do
    ls="$(ls -ld "$P")"
    link="$(expr "$ls" : '.*-> \(.*\)$')"
    expr "$link" : '/.*' > /dev/null &&
        P="$link" ||
        P="$(dirname "$P")/$link"
done
echo "$(cd "$(dirname "$P")"; pwd)/$(basename "$P")"

Voici un court cas de test avec quelques espaces tordus dans les chemins pour exercer pleinement la citation

mkdir -p "/tmp/test/ first path "
mkdir -p "/tmp/test/ second path "
echo "hello" > "/tmp/test/ first path / red .txt "
ln -s "/tmp/test/ first path / red .txt " "/tmp/test/ second path / green .txt "

cd  "/tmp/test/ second path "
fullpath " green .txt "
cat " green .txt "
0
David Blevins

Basé sur l'excellent extrait de python de loveborg, j'ai écrit ceci:

#!/bin/sh

# Version of readlink that follows links to the end; good for Mac OS X

for file in "$@"; do
  while [ -h "$file" ]; do
    l=`readlink $file`
    case "$l" in
      /*) file="$l";;
      *) file=`dirname "$file"`/"$l"
    esac
  done
  #echo $file
  python -c "import os,sys; print os.path.abspath(sys.argv[1])" "$file"
done
0
Edward Falk
FILEPATH="file.txt"
echo $(realpath $(dirname $FILEPATH))/$(basename $FILEPATH)

Cela fonctionne même si le fichier n'existe pas. Il faut que le répertoire contenant le fichier existe.

0
user240515

Je sais que c'est une question ancienne. Je propose toujours une alternative. Récemment, j'ai rencontré le même problème et n'ai trouvé aucune commande existante et portable pour le faire. J'ai donc écrit le script Shell suivant, qui inclut une fonction qui peut faire l'affaire.

#! /bin/sh                                                                                                                                                

function normalize {
  local rc=0
  local ret

  if [ $# -gt 0 ] ; then
    # invalid
    if [ "x`echo $1 | grep -E '^/\.\.'`" != "x" ] ; then
      echo $1
      return -1
    fi

    # convert to absolute path
    if [ "x`echo $1 | grep -E '^\/'`" == "x" ] ; then
      normalize "`pwd`/$1"
      return $?
    fi

    ret=`echo $1 | sed 's;/\.\($\|/\);/;g' | sed 's;/[^/]*[^/.]\+[^/]*/\.\.\($\|/\);/;g'`
  else
    read line
    normalize "$line"
    return $?
  fi

  if [ "x`echo $ret | grep -E '/\.\.?(/|$)'`" != "x" ] ; then
    ret=`normalize "$ret"`
    rc=$?
  fi

  echo "$ret"
  return $rc
}

https://Gist.github.com/bestofsong/8830bdf3e5eb9461d27313c3c282868c

0
bestOfSong