Как вы нормализуете путь к файлу в Bash?

Я хочу преобразовать /foo/bar/.. в /foo

Есть команда bash, которая делает это?


Изменить: в моем практическом случае, каталог существует.

24 ответа

Решение

Если вы хотите скомпоновать часть имени файла из пути, "dirname" и "basename" - ваши друзья, и "realpath" также удобен.

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альтернативы

Если realpath не поддерживается вашей оболочкой, вы можете попробовать

readlink -f /path/here/.. 

Также

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

Работает так же, как

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

в том, что путь не должен существовать, чтобы быть нормализованным.

Я не знаю, есть ли прямая команда bash, чтобы сделать это, но я обычно делаю

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

и это работает хорошо.

Пытаться realpath, Ниже приведен источник в полном объеме, настоящим пожертвовано в общественное достояние.

// 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);
}

Переносимое и надежное решение - использовать python, который установлен практически везде (включая Darwin). У вас есть два варианта:

  1. abspath возвращает абсолютный путь, но не разрешает символические ссылки:

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

  2. realpath возвращает абсолютный путь и при этом разрешает символические ссылки, генерируя канонический путь:

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

В каждом случае, path/to/file может быть относительным или абсолютным путем.

Используйте утилиту readlink из пакета coreutils.

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

Старый вопрос, но есть гораздо более простой способ, если вы имеете дело с полными путями на уровне оболочки:

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

Поскольку компакт-диск происходит в подоболочке, он не влияет на основной скрипт.

Два варианта, предполагающие, что встроенные команды вашей оболочки принимают -L и -P:

   abspath = "$ (cd -P" $ path "&& pwd -P)" # физический путь с разрешенными символическими ссылками
   abspath="$( cd -L "$path" && pwd -L)"    # логический путь, сохраняющий символические ссылки

Лично мне этот поздний подход редко нужен, если я по какой-то причине не увлечен символическими ссылками.

К вашему сведению: вариант получения начальной директории скрипта, который работает, даже если скрипт впоследствии меняет свою текущую директорию.

name0 = "$ (базовое имя"$0")";                  # базовое имя скрипта
dir0="$( cd "$( dirname "$0")" && pwd)"; #absolute начальный каталог

Использование компакт-диска гарантирует, что у вас всегда будет абсолютный каталог, даже если скрипт запускается такими командами, как./script.sh, который, без cd / pwd, часто дает просто... Бесполезно, если в дальнейшем скрипт выполняет cd.

readlink это стандарт bash для получения абсолютного пути. Он также имеет преимущество, заключающееся в возврате пустых строк, если пути или пути не существует (учитывая флаги для этого).

Чтобы получить абсолютный путь к каталогу, который может существовать или не существовать, но чьи родители существуют, используйте:

abspath=$(readlink -f $path)

Чтобы получить абсолютный путь к каталогу, который должен существовать вместе со всеми родителями:

abspath=$(readlink -e $path)

Чтобы канонизировать данный путь и следовать символическим ссылкам, если они существуют, но в противном случае игнорировать отсутствующие каталоги и просто возвращать путь в любом случае, это:

abspath=$(readlink -m $path)

Единственным недостатком является то, что readlink будет следовать ссылкам. Если вы не хотите переходить по ссылкам, вы можете использовать это альтернативное соглашение:

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

Это приведет к переходу в директорию $ path и выводит текущий каталог вместе с файловой частью $ path. Если это не удалось chdir, вы получите пустую строку и ошибку в stderr.

Как отметил Адам Лисс realpath не входит в комплект поставки Который является позором, потому что это лучшее решение. Предоставленный исходный код великолепен, и я, вероятно, начну использовать его сейчас. Вот то, что я использовал до сих пор, и я делюсь здесь только для полноты:

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

Если вы хотите разрешить символические ссылки, просто замените pwd с pwd -P,

Мое недавнее решение было:

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

На основании ответа Тима Уиткомба.

Я опаздываю на вечеринку, но это решение, которое я разработал после прочтения нескольких тем вроде этого:

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

Это разрешит абсолютный путь в $1, будет хорошо играть с ~, сохранит символические ссылки там, где они есть, и это не помешает вашему стеку каталогов. Возвращает полный путь или ничего, если он не существует. Он ожидает, что $ 1 будет каталогом и, возможно, потерпит неудачу, если это не так, но это легко проверить самостоятельно.

Не совсем ответ, но, возможно, дополнительный вопрос (первоначальный вопрос не был явным):

readlink хорошо, если вы действительно хотите следовать символическим ссылкам. Но есть также случай использования для простой нормализации ./ а также ../ а также // последовательности, которые могут быть сделаны чисто синтаксически, без канонизации символических ссылок. readlink не годится для этого и не realpath,

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

работает для существующих путей, но ломается для других.

sed сценарий может показаться хорошим вариантом, за исключением того, что вы не можете итеративно заменять последовательности (/foo/bar/baz/../.. -> /foo/bar/.. -> /foo) без использования чего-либо вроде Perl, что небезопасно предположить во всех системах, или использования некрасивого цикла для сравнения вывода sed на его вход.

FWIW, однострочник с использованием 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

Я сделал встроенную функцию, чтобы справиться с этим, сосредоточив внимание на максимально возможной производительности (для развлечения). Он не разрешает символические ссылки, поэтому он в основном такой же, какrealpath -sm.

## A bash-only mimic of `realpath -sm`. 
## Give it path[s] as argument[s] and it will convert them to clean absolute paths
abspath () { 
  ${*+false} && { >&2 echo $FUNCNAME: missing operand; return 1; };
  local c s p IFS='/';  ## path chunk, absolute path, input path, IFS for splitting paths into chunks
  local -i r=0;         ## return value

  for p in "$@"; do
    case "$p" in        ## Check for leading backslashes, identify relative/absolute path
    '') ((r|=1)); continue;;
    //[!/]*)  >&2 echo "paths =~ ^//[^/]* are impl-defined; not my problem"; ((r|=2)); continue;;
    /*) ;;
    *)  p="$PWD/$p";;   ## Prepend the current directory to form an absolute path
    esac

    s='';
    for c in $p; do     ## Let IFS split the path at '/'s
      case $c in        ### NOTE: IFS is '/'; so no quotes needed here
      ''|.) ;;          ## Skip duplicate '/'s and '/./'s
      ..) s="${s%/*}";; ## Trim the previous addition to the absolute path string
      *)  s+=/$c;;      ### NOTE: No quotes here intentionally. They make no difference, it seems
      esac;
    done;

    echo "${s:-/}";     ## If xpg_echo is set, use `echo -E` or `printf $'%s\n'` instead
  done
  return $r;
}

Примечание. Эта функция не обрабатывает пути, начинающиеся с //, поскольку ровно две двойные косые черты в начале пути являются поведением, определяемым реализацией. Однако он обрабатывает/, ///и так далее.

Эта функция, кажется, правильно обрабатывает все крайние случаи, но все же могут быть некоторые, с которыми я еще не работал.

Примечание по производительности: при вызове с тысячами аргументов abspath работает примерно в 10 раз медленнее, чем realpath -sm; при вызове с одним аргументом,abspath работает>110 раз быстрее, чем realpath -sm на моей машине, в основном из-за того, что мне не нужно каждый раз запускать новую программу.

Разговорчивый и немного запоздалый ответ. Мне нужно написать один, так как я застрял на старшей RHEL4/5. Я обрабатываю абсолютные и относительные ссылки и упрощаю записи //, /./ и 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`

Проблема с realpath является то, что он не доступен на BSD (или OSX в этом отношении). Вот простой рецепт, извлеченный из довольно старой (2009 г.) статьи из Linux Journal, которая довольно переносима:

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

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

Обратите внимание, что этот вариант также не требует пути для существования.

Попробуйте наш новый продукт библиотеки Bash realpath-lib, который мы поместили на GitHub для бесплатного и свободного использования. Он тщательно задокументирован и является отличным инструментом обучения.

Он разрешает локальные, относительные и абсолютные пути и не имеет никаких зависимостей, кроме Bash 4+; так что должно работать где угодно. Это бесплатно, чисто, просто и поучительно.

Ты можешь сделать:

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

Эта функция является ядром библиотеки:

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

}

Он также содержит функции для get_dirname, get_filename, get_ stemname и validate_path. Попробуйте на разных платформах и помогите улучшить его.

Основываясь на ответе @Andre, у меня может быть немного лучшая версия, если кто-то ищет решение без петель, полностью основанное на обработке строк. Это также полезно для тех, кто не хочет разыменовывать любые символические ссылки, что является недостатком использования realpath или же readlink -f,

Работает на bash версий 3.2.25 и выше.

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

Если вы просто хотите нормализовать путь, существующий или несуществующий, не касаясь файловой системы, не разрешая никаких ссылок и без внешних утилит, вот чистая функция Bash , переведенная с языка Python. .

      #!/usr/bin/env bash

# Normalize path, eliminating double slashes, etc.
# Usage: new_path="$(normpath "${old_path}")"
# Translated from Python's posixpath.normpath:
# https://github.com/python/cpython/blob/master/Lib/posixpath.py#L337
normpath() {
  local IFS=/ initial_slashes='' comp comps=()
  if [[ $1 == /* ]]; then
    initial_slashes='/'
    [[ $1 == //* && $1 != ///* ]] && initial_slashes='//'
  fi
  for comp in $1; do
    [[ -z ${comp} || ${comp} == '.' ]] && continue
    if [[ ${comp} != '..' || (-z ${initial_slashes} && ${#comps[@]} -eq 0) || (\
      ${#comps[@]} -gt 0 && ${comps[-1]} == '..') ]]; then
      comps+=("${comp}")
    elif ((${#comps[@]})); then
      unset 'comps[-1]'
    fi
  done
  comp="${initial_slashes}${comps[*]}"
  printf '%s\n' "${comp:-.}"
}

Примеры:

      new_path="$(normpath '/foo/bar/..')"
echo "${new_path}"
# /foo

normpath "relative/path/with trailing slashs////"
# relative/path/with trailing slashs

normpath "////a/../lot/././/mess////./here/./../"
# /lot/mess

normpath ""
# .
# (empty path resolved to dot)

Лично я не могу понять, почему Shell, язык, часто используемый для работы с файлами, не предлагает основных функций для работы с путями. В python у нас есть хорошие библиотеки, такие как os.path или pathlib, которые предлагают целый набор инструментов для извлечения имени файла, расширения, базового имени, сегментов пути, разделения или объединения путей, получения абсолютных или нормализованных путей, определения отношений между путями, делать все без особых мозгов. И они заботятся о крайних случаях, и они надежны. В Shell, чтобы сделать что-либо из этого, мы либо вызываем внешние исполняемые файлы, либо нам приходится заново изобретать колеса с этим чрезвычайно рудиментарным и загадочным синтаксисом...

Мне нужно решение, которое сделало бы все три:

  • Работа на стоковом Mac. realpath а также readlink -f аддоны
  • Разрешить символические ссылки
  • Обработка ошибок

Ни в одном из ответов не было ни № 1, ни № 2. Я добавил #3, чтобы спасти других от дальнейшего бритья.

#!/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")"

Вот короткий тестовый пример с некоторыми искривленными пробелами в путях, чтобы полностью использовать цитату

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 "

Я знаю, что это древний вопрос. Я все еще предлагаю альтернативу. Недавно я столкнулся с той же проблемой и не нашел существующей и переносимой команды для этого. Поэтому я написал следующий скрипт оболочки, который включает в себя функцию, которая может сделать свое дело.

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

FILEPATH="file.txt"
echo $(realpath $(dirname $FILEPATH))/$(basename $FILEPATH)

Это работает, даже если файл не существует. Для этого требуется каталог, содержащий файл.

Основываясь на отличном фрагменте Python от Loveborg, я написал это:

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

Поскольку ни одно из представленных решений у меня не сработало, в случае, когда файла не существует, я реализовал свою идею. Решение André Anjos имело проблему, состоящую в том, что пути, начинающиеся с ../../, были разрешены неправильно. Например, ../../a/b/ стало a/b/.

      function normalize_rel_path(){
  local path=$1
  result=""
  IFS='/' read -r -a array <<< "$path"
  i=0
  for (( idx=${#array[@]}-1 ; idx>=0 ; idx-- )) ; do
    c="${array[idx]}"
    if [ -z "$c" ] || [[ "$c" == "." ]];
    then
      continue
    fi
    if [[ "$c" == ".." ]]
    then
      i=$((i+1))
    elif [ "$i" -gt "0" ];
    then
      i=$((i-1))
    else
      if [ -z "$result" ];
      then
        result=$c
      else
        result=$c/$result
      fi
    fi
  done
  while [ "$i" -gt "0" ]; do
    i=$((i-1))
    result="../"$result
  done  
  unset IFS
  echo $result
}

Сегодня я обнаружил, что вы можете использовать stat Команда для разрешения путей.

Так что для директории типа "~/Documents":

Вы можете запустить это:

stat -f %N ~/Documents

Чтобы получить полный путь:

/Users/me/Documents

Для символических ссылок вы можете использовать опцию формата%Y:

stat -f %Y example_symlink

Который может вернуть результат как:

/usr/local/sbin/example_symlink

Параметры форматирования могут отличаться в других версиях *NIX, но они работают для меня в OSX.

Простое решение с использованием node.js:

#!/usr/bin/env node
process.stdout.write(require('path').resolve(process.argv[2]));
Другие вопросы по тегам