您如何规范Bash中的文件路径?


203

我想转变/foo/bar/../foo

是否有执行此操作的bash命令?


编辑:在我的实际情况下,该目录确实存在。


4
这是否重要,/foo/bar甚至/foo实际上是否存在,还是仅根据路径名规则对字符串操作方面感兴趣?
Chen Levy 2010年

5
@twalberg ...有点做作...
Camilo Martin

2
@CamiloMartin根本不做作-它确实按照问题的要求进行了-转换/foo/bar/../foo,并使用bash命令。如果还有其他未说明的要求,那么也许应该...
twalberg 2014年

14
@twalberg您做的TDD太多了-_-'–
Camilo Martin

Answers:


192

如果您想从路径中截取一部分文件名,则“ 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/../../

因为不需要标准化路径。


5
对于那些需要OS X解决方案的人,请查看下面的Adam Liss的答案。
特伦顿

stackoverflow.com/a/17744637/999943 这是一个密切相关的答案!我有一天遇到了这两个质量检查帖子,我想将它们链接在一起。
phyatt

realpath似乎已在2012年添加到coreutils中。请参见github.com/coreutils/coreutils/commits/master/src/realpath.c中的文件历史记录。
诺亚·拉文

1
两者realpathreadlink都来自GNU核心实用程序,因此很可能两者都不存在。如果我没记错的话,Mac上的readlink版本与GNU版本稍有不同:-\
Antoine'hashar'Musso

100

我不知道是否有直接的bash命令来执行此操作,但我通常会这样做

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

而且效果很好。


9
这将规范化但不能解决软链接。这可能是错误或功能。:-)
Adam Liss

4
如果定义了$ CDPATH,它也有一个问题。因为“ cd foo”将切换到$ CDPATH的子目录中的任何“ foo”目录,而不仅仅是当前目录中的“ foo”。我认为您需要执行以下操作:CDPATH =“” cd“ $ {dirToNormalize}” && pwd -P。
mjs

7
蒂姆的答案肯定是最简单,最便携的。CDPATH很容易处理:dir =“ $(未设置CDPATH && cd” $ dir“ && pwd)”
David Blevins

2
rm -rf $normalDir如果dirToNormalize不存在,这可能非常危险()!
Frank Meulenaar 2012年

4
是的,最好使用&&@DavidBlevins的注释。
Elias Dorneles 2014年

54

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

1
这是否包括在标准bash安装中?我在系统上收到“找不到命令”(RHEL上的bash 3.00.15(1))
Tim Whitcomb,


8
这是ubuntu / BSD上的标准,而不是Centos / OSX上的标准
Erik Aronesty 2014年

4
您应该真正通过“ Bonus:它的bash命令,遍及所有地方!”来完成该部分。关于realpath。它也恰好是我的最爱,而不是从源头上编译您的工具。全部都是重量级的,而且大多数时候您只想要readlink -f... BTW,readlink它也不是内置的bash,而是coreutilsUbuntu上的一部分。
Tomasz Gandor 2014年

4
您已经说过要将其捐赠给公共领域,但是标头中也显示“用于任何非营利用途” –您真的要把它捐赠给公共领域吗?这意味着它既可以用于商业性营利目的,也可以用于非营利性……
me_and

36

一个可移植且可靠的解决方案是使用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可以是相对路径或绝对路径。


7
谢谢,那是唯一有效的方法。readlink或realpath在OS X下不可用。Python应该在大多数平台上都可用。
索林2012年

1
只是为了澄清,readlink它在OS X上可用,只是不带该-f选项。便携式解决方法讨论这里
2014年

3
从字面上让我困惑的是,如果您不想跟随链接,这是唯一理智的解决方案。我的脚步是Unix。
DannoHung

34

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

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

1
BSD没有-f标志,这意味着即使在最新的MacOS Mojave和许多其他系统上,它也将失败。如果您想要可移植性,请不要使用-f,这会影响许多OS-es。
索林

1
@sorin。问题不是关于Mac,而是关于Linux。
mattalxndr 18-10-13

14

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上显示错误。


7
readlink如果可用,是一个不错的选择。OS X版本不支持-e-f选项。在前三个示例中,应该使用双引号引起来$path以处理文件名中的空格或通配符。+1用于参数扩展,但这具有安全漏洞。如果path为空,它将cd进入您的主目录。您需要双引号。abspath=$(cd "${path%/*}" && echo "$PWD/${path##*/}")
toxalot

这只是一个例子。如果您对安全性一无所知,那么您根本就不应该使用bash或任何其他shell变体。另外,bash在跨平台兼容性方面也有其自身的问题,并且在主要版本之间的功能更改方面也存在问题。OSX只是许多与Shell脚本相关的平台之一,更不用说它基于BSD。当您必须是真正的多平台平台时,您需要与POSIX兼容,因此参数扩展实际上是不可能的。请看一下Solaris或HP-UX。
克雷格

6
这里并不意味着任何冒犯,但指出诸如此类的晦涩问题很重要。我只是想快速解决这个琐碎的问题,如果不是上面的注释,我会信任带有任何/所有输入的代码。在这些bash讨论中支持OS-X也很重要。不幸的是,OS-X上不支持很多命令,许多论坛在讨论Bash时都认为这是理所当然的,这意味着我们将继续遇到很多跨平台问题,除非尽快解决。
Rebs 2014年

13

这是个老问题,但是如果要在Shell级别处理完整路径名,则有一种更简单的方法:

   abspath =“ $(cd” $ path“ && pwd)”

由于CD发生在子外壳中,因此不会影响主脚本。

假设您的shell内置命令接受-L和-P,有两种变体:

   abspath =“ $(cd -P” $ path“ && pwd -P)”#具有已解析符号链接的物理路径
   abspath =“ $(cd -L” $ path“ && pwd -L)”#保留逻辑路径的符号链接

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

仅供参考:获取脚本开始目录的变体,即使该脚本稍后更改了它的当前目录,该脚本仍然有效。

name0 =“ $(基本名称” $ 0“)”; #脚本的基本名称
dir0 =“ $(cd” $(目录名“ $ 0”)“ && pwd)”; #绝对起始目录

使用CD可以确保您始终拥有绝对目录,即使脚本由./script.sh之类的命令运行,如果没有cd / pwd,该命令通常也只会提供..如果脚本稍后再执行cd,则无用。


8

正如亚当·利斯(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"
} 

如果您希望它解析符号链接,只需替换pwdpwd -P


一个带有pwd -P这种情况下选项的陷阱……考虑一下,如果$(basename "$1")符号链接到另一个目录中的文件,将会发生什么。该pwd -P只解决符号链接路径的目录部分,但不是基本名称部分。
toxalot

7

我最近的解决方案是:

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

基于蒂姆·惠特科姆的答案。


我怀疑如果参数不是目录,这将失败。假设我想知道/ usr / bin / java指向何处?
爱德华·福尔克

1
如果您知道这是一个文件,可以pushd $(dirname /usr/bin/java)尝试一下。
schmunk

5

不完全是一个答案,而是一个后续问题(原始问题并不明确):

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可以-s选择不解析符号链接,而仅解析对的引用/.//../并删除多余的/字符。当与该-m选项组合使用时,realpath仅对文件名起作用,而不接触任何实际文件。这听起来像是一个完美的解决方案。但是可惜的realpath是,在许多系统上仍然缺少。
toxalot

..当涉及符号链接时,无法从句法上删除组件。 /one/two/../three是不一样的/one/three,如果two是一个符号链接/foo/bar
jrw32982支持Monica

@ jrw32982是的,正如我在响应中所说的,这是用于不需要或不需要符号链接规范化的用例。
杰西·格里克

@JesseGlick这不仅是您是否要规范符号链接的一种情况。您的算法实际上产生了错误的答案。为了使答案正确,您必须先验地知道不涉及符号链接(或它们只是某种形式)。您的回答是您不想规范它们,而不是路径中没有符号链接。
jrw32982

在某些情况下,必须执行规范化而无需假设任何固定的现有目录结构。URI规范化是相似的。在这些情况下,一个固有的限制是,如果稍后在附近应用结果的目录附近出现符号链接,则结果通常不会正确。
杰西·格里克

5

我参加聚会很晚,但是这是我在阅读了如下这样的主题后制定的解决方案:

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

这将解析$ 1的绝对路径,与〜配合使用,将符号链接保留在它们所在的路径中,并且不会与目录堆栈混淆。它返回完整路径,如果不存在,则不返回任何内容。它期望$ 1是一个目录,如果不是,可能会失败,但这是您自己进行的一项简单检查。


4

健谈,回答迟了一点。由于我停留在较旧的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`

3

尝试我们免费提供给我们的新的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_temname和validate_path的函数。跨平台尝试,并帮助改进它。


2

问题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
}

注意,此变体也不需要路径。


但是,这不能解决符号链接。Realpath处理从根开始的路径,并随着其进行而遵循符号链接。所有这些都是折叠父级引用。
马丁·彼得斯

2

根据@Andre的回答,如果有人使用了无循环的,完全基于字符串操作的解决方案,我可能会提供一个更好的版本。对于不想取消引用任何符号链接的人也很有用,这是使用realpathor 的缺点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

这是一个好主意,但我浪费了20分钟来尝试使它在各种不同版本的bash上运行。事实证明,需要启用extglob shell选项才能使它起作用,并且默认情况下未启用该选项。当涉及bash功能时,重要的是同时指定所需的版本和非默认选项,因为这些详细信息在操作系统之间可能会有所不同。例如,最新版本的Mac OSX(Yosemite)仅附带过时版本的bash(3.2)。
drwatsoncode 2015年

抱歉@ricovox; 我现在更新了那些。我很想知道那里的Bash确切版本。上面的公式(已更新)在CentOS 5.8上有效,该版本附带bash 3.2.25
δοδεMεδιϲ 15-10-12

对困惑感到抱歉。一旦打开extglob,此代码DID就可以在我的Mac OSX bash版本(3.2.57)上使用。我对bash版本的注释是一个较为笼统的注释(实际上,它对bash中的正则表达式的回答更多)。
drwatsoncode 2015年

2
我很感谢你的回答。我将其用作自己的基础。顺便说一句,我注意到了几种失败的情况:(1)相对路径hello/../world(2)文件名中的点/hello/..world(3)双斜杠后的点/hello//../world(4)双斜杠之前或之后的点 /hello//./world/hello/.//world (5)当前后的父项:/hello/./../world/ (6)父项父后: /hello/../../world等- 一些,这些可以通过使用一个循环,直到路径停止变化改正固定。(也删除dir/../,而不是 从末尾/dir/..删除dir/..。)
drwatsoncode 2015年

0

基于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

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

即使文件不存在,此方法也有效。它确实需要包含该文件的目录。


GNU Realpath也不要求路径中的最后一个元素存在,除非您使用realpath -e
Martijn Pieters

0

我需要一个可以同时完成这三个任务的解决方案:

  • 在股票Mac上工作。realpath并且readlink -f是插件
  • 解决符号链接
  • 有错误处理

答案都没有#1和#2。我添加了#3来保存其他further牛草。

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

0

我知道这是一个古老的问题。我仍在提供替代方案。最近,我遇到了同样的问题,发现没有现成的可移植命令可以做到这一点。因此,我编写了以下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


0

我做了一个仅内置的函数来处理此问题,重点放在尽可能高的性能上(以娱乐为目的)。它不会解析符号链接,因此与基本上相同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运行速度比realpath -sm; 慢约10倍;当使用单个参数调用时,其abspath运行速度比realpath -sm我的计算机快110倍以上,这主要是因为不需要每次都执行新程序。


-1

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

因此,对于“〜/ Documents”之类的目录:

您可以运行以下命令:

stat -f %N ~/Documents

要获取完整路径:

/Users/me/Documents

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

stat -f %Y example_symlink

可能返回如下结果:

/usr/local/sbin/example_symlink

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


1
stat -f %N ~/Documents行是一条红色鲱鱼...您的shell替换~/Documents/Users/me/Documents,并stat逐字打印其参数。
danwyand 2015年

-4

一个简单的解决方案,使用node.js

#!/usr/bin/env node
process.stdout.write(require('path').resolve(process.argv[2]));
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.