如何在Bash中规范化文件路径?

我想将/foo/bar/..转换为/foo

是否有bash命令可以做到这一点?


编辑:在我的实际案例中,目录确实存在。

172681 次浏览

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

我不知道是否有直接的bash命令可以做到这一点,但我通常会这样做

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

而且效果很好。

如果您想要从路径中删除部分文件名,“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替代品

如果你的shell不支持realpath,你可以试试

readlink -f /path/here/..

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

工作原理与

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

在这种情况下,路径不需要存在就可以被规范化。

话多,回答有点晚。我需要写一个,因为我卡住了旧的RHEL4/5。 I处理绝对和相对链接,并简化//,/。/和somedir/../条目。< / p >

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`

使用coreutils包中的readlink实用程序。

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

一种可移植且可靠的解决方案是使用python,几乎所有地方都预装了python(包括Darwin)。你有两个选择:

  1. abspath返回绝对路径,但不解析符号链接:

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

  2. realpath返回绝对路径并解析符号链接,生成规范路径:

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

在每种情况下,path/to/file既可以是相对路径也可以是绝对路径。

正如Adam Liss所指出的,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

根据蒂姆·惠特科姆的回答。

不完全是一个答案,但可能是一个后续问题(最初的问题不明确):

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

我来晚了,但这是我在阅读了一堆这样的帖子后精心设计的解决方案:

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

这将解析$1的绝对路径,很好地处理~,将符号链接保留在它们所在的路径中,并且不会打乱目录堆栈。它返回完整的路径,如果不存在则不返回。它期望$1是一个目录,如果不是的话可能会失败,但这是一个很容易自己做的检查。

尝试我们的新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函数。在不同平台上尝试它,并帮助改进它。

readlink是获取绝对路径的bash标准。如果路径或路径不存在(给定了这样做的标志),它还具有返回空字符串的优点。

要获得一个目录的绝对路径,该目录可能存在,也可能不存在,但其父目录确实存在,请使用:

abspath=$(readlink -f $path)

获取一个必须与所有父目录一起存在的目录的绝对路径:

abspath=$(readlink -e $path)

规范化给定的路径并遵循符号链接,如果它们恰好存在,但忽略丢失的目录,无论如何只返回路径,它是:

abspath=$(readlink -m $path)

唯一的缺点是readlink将跟随链接。如果你不想遵循链接,你可以使用这个替代约定:

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

这将chdir到$path的目录部分,并打印当前目录和$path的文件部分。如果它不能进行chdir,则会得到一个空字符串,并在stderr上报错。

我今天发现你可以使用stat命令来解析路径。

对于"~/Documents"这样的目录:

你可以运行这个:

stat -f %N ~/Documents

获取完整路径:

/Users/me/Documents

对于符号链接,你可以使用%Y格式选项:

stat -f %Y example_symlink

这可能会返回如下结果:

/usr/local/sbin/example_symlink

格式选项可能在*NIX的其他版本上有所不同,但这些在OSX上对我有用。

realpath的问题是它在BSD(或OSX)上不可用。下面是一个从来自Linux Journal的一篇相当老的(2009年)文章中提取的简单配方,它是非常可移植的:

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


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

注意这个变量也要求路径存在。

基于@Andre的回答,我可能有一个稍微更好的版本,以防有人在一个无循环,完全基于字符串操作的解决方案。它对于那些不想解引用任何符号链接的人也很有用,这是使用realpathreadlink -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

使用node.js的简单解决方案:

#!/usr/bin/env node
process.stdout.write(require('path').resolve(process.argv[2]));

老问题了,但是在shell级别有更简单的方法如果您正在处理完整的路径名:

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

As the cd happens in a subshell it does not impact the main script.

Two variations, supposing your shell built-in commands accept -L and -P, are:

abspath="$( cd -P "$path" && pwd -P )"    #physical path with resolved symlinks
abspath="$( cd -L "$path" && pwd -L )"    #logical path preserving symlinks

就我个人而言,我很少需要这种后面的方法,除非我出于某种原因对符号链接着迷。

供参考:获取脚本起始目录的变体,即使脚本稍后更改了当前目录也能工作。

name0="$(basename "$0")";                  #base name of script
dir0="$( cd "$( dirname "$0" )" && pwd )"; #absolute starting dir

使用CD可以确保始终拥有绝对目录,即使脚本是通过./script.sh等命令运行的,如果没有CD /pwd,通常只会给出..如果脚本稍后执行cd,则无用。

基于loveborg出色的python代码片段,我这样写:

#!/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
FILEPATH="file.txt"
echo $(realpath $(dirname $FILEPATH))/$(basename $FILEPATH)

即使文件不存在,这也可以工作。它需要包含该文件的目录存在。

我知道这是一个古老的问题。我仍在提供另一种选择。最近我遇到了同样的问题,并且发现没有现有的可移植命令来执行此操作。因此,我编写了下面的shell脚本,其中包括一个可以实现此功能的函数。

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

我需要一个同时满足这三点的解决方案:

  • 在普通Mac上工作。realpathreadlink -f是插件
  • 解决符号链接
  • 有错误处理

没有一个人的答案同时包含了第一条和第二条。我加了第三条是为了让其他人不用再剃牦牛了。

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

我做了一个内置函数来处理这个问题,并专注于最高的性能(为了好玩)。它不解析符号链接,因此基本上与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;
}

注意:此函数不处理以//开头的路径,因为路径开头的两个双斜杠是实现定义的行为。但是,它可以很好地处理////等等。

这个函数似乎正确地处理了所有的边缘情况,但可能还有一些我没有处理的情况。

性能注意:当使用数千个参数调用时,abspathrealpath -sm慢大约10倍;当使用单个参数调用时,在我的机器上,abspath运行>的速度比realpath -sm快110倍,主要是因为不需要每次都执行一个新程序。

如果你只想规范化一个路径,不管是否存在,不涉及文件系统,不解析任何链接,也不使用外部utils,这里有一个从Python的posixpath.normpath转换而来的纯Bash函数。

#!/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中,要做到这些,我们要么调用外部可执行文件,要么就必须重新发明这些极其基本和神秘的语法……

因为没有一个提出的解决方案对我有效,在文件不存在的情况下,我实现了我的想法。 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
}