关于linux:如何在Bash中规范化文件路径?

How do you normalize a file path in Bash?

我想把/foo/bar/..改成/foo

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

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


如果你想从路径中选择部分文件名,"dirname"和"basename"是你的朋友,"realpath"也很方便。

1
2
3
4
5
6
7
8
9
10
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,您可以尝试

1
readlink -f /path/here/..

阿尔索

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

工作原理与

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

在这一点上,路径不需要存在来进行规范化。


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

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

而且效果很好。


试试realpath。以下是其全部来源,特此捐赠给公共领域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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
"
, realpath(argv[1], sPath));
    return 0;
}    

void usage(void)
{
    fprintf(stderr,"usage: %s PATH
"
, s_pMyName);
    exit(1);
}


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

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


一个可移植和可靠的解决方案是使用python,它在几乎所有地方(包括达尔文)都预先安装了。您有两种选择:

  • 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是获取绝对路径的bash标准。如果路径或路径不存在,它还具有返回空字符串的优势(给定要这样做的标志)。

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

    1
    abspath=$(readlink -f $path)

    要获取必须与所有父目录一起存在的目录的绝对路径,请执行以下操作:

    1
    abspath=$(readlink -e $path)

    要规范化给定的路径,并遵循符号链接(如果它们恰好存在),但如果不存在,则忽略缺少的目录并返回路径,它是:

    1
    abspath=$(readlink -m $path)

    唯一的缺点是readlink会跟随链接。如果不想使用链接,可以使用此替代约定:

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

    这将chdir指向$path的目录部分,并打印当前目录以及$path的文件部分。如果chdir失败,您将得到一个空字符串和stderr上的一个错误。


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

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

    当CD发生在子shell中时,它不会影响主脚本。

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

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

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

    仅供参考:获取脚本的起始目录时的变化,即使脚本稍后更改了当前目录,也可以正常工作。

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

    使用cd可以确保您始终拥有绝对目录,即使脚本是由诸如./script.sh之类的命令运行的,而没有cd/pwd,这些命令通常只给出……如果脚本稍后执行CD,则无效。


    正如AdamLiss所指出的,realpath并不是与每个发行版捆绑在一起的。很遗憾,因为这是最好的解决办法。提供的源代码非常好,我现在可能会开始使用它。以下是我到目前为止一直在使用的内容,我在这里只是为了完整性而分享:

    1
    2
    3
    4
    5
    6
    7
    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


    我最近的解决方案是:

    1
    2
    3
    pushd foo/bar/..
    dir=`pwd`
    popd

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


    不完全是答案,但可能是后续问题(原始问题不明确):

    如果你真的想使用symlinks,那么readlink就可以了。但是也有一个仅仅规范化./..///序列的用例,这些序列完全可以在语法上完成,而无需规范化符号链接。readlink对这个没有好处,realpath也没有好处。

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

    适用于现有路径,但适用于其他路径。

    一个sed脚本似乎是一个很好的选择,除非你不能在不使用诸如perl之类的东西的情况下迭代替换序列(/foo/bar/baz/../..->/foo/bar/..->/foo,这在所有系统上都是不安全的,或者使用一些丑陋的循环将sed的输出与输入进行比较。

    FWWW,使用Java(JDK 6 +)的一个内衬:

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


    健谈,回答有点晚。我需要写一封信,因为我困在旧的瑞尔4/5上。我处理绝对链接和相对链接,并简化//、//和somedir/./条目。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    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`

    我参加派对迟到了,但这是我在阅读了一堆这样的线索后制定的解决方案:

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

    这将解析$1的绝对路径,使用~,保持符号链接在它们所在的路径中,并且不会干扰您的目录堆栈。它返回完整的路径,如果不存在则不返回任何内容。它期望$1是一个目录,如果不是,它可能会失败,但这是一个很容易检查自己。


    尝试我们在Github上放置的新bash库产品realpath lib,以供免费和无障碍使用。它被完整地记录下来,是一个很好的学习工具。

    它解析局部、相对和绝对路径,除了bash 4+之外没有任何依赖关系;因此它应该可以在任何地方工作。它是免费的,干净的,简单的和有教育意义的。

    你可以做到:

    1
    get_realpath

    此功能是库的核心:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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

    }

    它还包含获取目录名、获取文件名、获取词干名和验证路径的函数。跨平台尝试,并帮助改进它。


    根据@andre的回答,我可能会有一个稍微好一点的版本,以防有人想要一个完全基于字符串操作的无循环解决方案。对于那些不想取消引用任何符号链接的人也很有用,这是使用realpathreadlink -f的缺点。

    它适用于bash 3.2.25及更高版本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function normpath() {
      # Remove all /./ sequences.
      local path=${1//\/.\//\/}

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

    注意,这个变量也不需要存在路径。


    我需要一个能解决这三个问题的解决方案:

    • 在股票市场工作。realpathreadlink -f是附加项。
    • 解析符号链接
    • 有错误处理

    没有一个答案同时有1和2。我加了3以节省其他任何进一步的牦牛剃须。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #!/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")"

    下面是一个简短的测试用例,在路径中有一些扭曲的空间来充分地执行报价

    1
    2
    3
    4
    5
    6
    7
    8
    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"

    基于Loveborg优秀的python代码片段,我写了以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #!/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

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

    即使文件不存在,也可以这样做。它确实要求包含该文件的目录存在。


    我知道这是个古老的问题。我仍在提供另一种选择。最近我遇到了同样的问题,没有发现任何现有的和可移植的命令可以做到这一点。所以我写了下面的shell脚本,其中包含一个可以实现这个技巧的函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #! /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


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

    所以对于像"~/documents"这样的目录:

    您可以运行此:

    stat -f %N ~/Documents

    要获得完整路径:

    /Users/me/Documents

    对于symlinks,可以使用%y格式选项:

    stat -f %Y example_symlink

    可能会返回如下结果:

    /usr/local/sbin/example_symlink

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


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

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