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

时间:2008-11-12 17:16:07

标签: linux bash unix shell

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

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


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

23 个答案:

答案 0 :(得分:167)

如果您想要从路径中选择部分文件名,“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/../../

因为路径不需要存在才能被标准化。

答案 1 :(得分:90)

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

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

并且效果很好。

答案 2 :(得分:53)

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

答案 3 :(得分:35)

便携可靠的解决方案是使用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

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

答案 4 :(得分:34)

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

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

答案 5 :(得分:13)

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

要获取可能存在或可能不存在的目录的绝对路径,但是父母确实存在,请使用:

abspath=$(readlink -f $path)

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

abspath=$(readlink -e $path)

规范化给定路径并遵循符号链接(如果它们碰巧存在),否则忽略丢失的目录并且只返回路径,它是:

abspath=$(readlink -m $path)

唯一的缺点是readlink将遵循链接。如果您不想关注链接,可以使用此替代约定:

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

这将是chd到$ path的目录部分并打印当前目录以及$ path的文件部分。如果它没有chdir,你得到一个空字符串和stderr上的错误。

答案 6 :(得分:9)

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

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

由于cd在子shell中发生,因此不会影响主脚本。

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

   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则无用稍后的。

答案 7 :(得分:7)

我最近的解决方案是:

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

根据Tim Whitcomb的回答。

答案 8 :(得分:7)

正如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

答案 9 :(得分:5)

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

如果您真的想要遵循符号链接,那么

readlink就可以了。但是,还有一个用例仅用于规范./..///序列,这​​可以完全在语法上完成,没有规范化符号链接。 readlink对此无益,realpath也不是。

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

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

sed脚本似乎是一个不错的选择,除了你不能迭代地替换序列(/foo/bar/baz/../.. - &gt; /foo/bar/.. - &gt; /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

答案 10 :(得分:4)

我迟到了,但这是我在阅读了这样一堆线程后制作的解决方案:

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

这将解析$ 1的绝对路径,与〜一起使用,将符号链接保留在它们所在的路径中,并且它不会弄乱你的目录堆栈。它返回完整路径,如果不存在则返回任何内容。它希望1美元成为一个目录,如果不是,它可能会失败,但这是一个很容易自己做的检查。

答案 11 :(得分: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`

答案 12 :(得分:3)

尝试使用我们在GitHub上免费且无阻碍使用的新Bash库产品realpath-lib。它是完整的文档,是一个很好的学习工具。

它解析了本地路径,相对路径和绝对路径,除了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的函数。跨平台尝试,并帮助改进它。

答案 13 :(得分:2)

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

答案 14 :(得分:1)

realpath的问题在于它在BSD(或OSX)上不可用。这是一个从a rather old (2009) article from Linux Journal中提取的简单配方,非常便携:

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

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

请注意,此变体也要求存在路径。

答案 15 :(得分: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

答案 16 :(得分:0)

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

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

答案 17 :(得分: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
}

collection

答案 18 :(得分:0)

我需要一个可以完成所有这三项的解决方案:

  • 在股票Mac上工作。 realpathreadlink -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 "

答案 19 :(得分: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倍,主要是因为不需要每次都执行新程序。

答案 20 :(得分:0)

如果你只想规范化一个路径,无论存在或不存在,不接触文件系统,不解析任何链接,不使用外部工具,这里是一个从 Python 的 { 翻译过来的纯 Bash 函数{1}}。

posixpath.normpath

示例:

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

就我个人而言,我无法理解为什么 Shell(一种经常用于操作文件的语言)不提供处理路径的基本功能。在 python 中,我们有很好的库,比如 os.path 或 pathlib,它提供了一大堆工具来提取文件名、扩展名、基名、路径段、分割或连接路径,获得绝对或规范化路径,确定路径之间的关系,没有太多的大脑做任何事情。他们处理边缘情况,而且很可靠。在 Shell 中,要执行其中任何一项操作,我们要么调用外部可执行文件,要么必须使用这些极其简陋和晦涩的语法重新发明轮子...

答案 21 :(得分:-1)

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

对于像&#34;〜/ Documents&#34;:

这样的目录

你可以运行:

stat -f %N ~/Documents

获取完整路径:

/Users/me/Documents

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

stat -f %Y example_symlink

可能会返回如下结果:

/usr/local/sbin/example_symlink

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

答案 22 :(得分:-3)

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

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