Как вы нормализуете путь к файлу в 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). У вас есть два варианта:
abspath
возвращает абсолютный путь, но не разрешает символические ссылки:python -c "import os,sys; print os.path.abspath(sys.argv[1])" path/to/file
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]));