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

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


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


当前回答

由于所介绍的解决方案都不适合我,所以在文件不存在的情况下,我实现了我的想法。 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
}

其他回答

话多,回答有点晚。我需要写一个,因为我卡住了旧的RHEL4/5。 I处理绝对和相对链接,并简化//,/。/和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`

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上报错。

如果你只想规范化一个路径,不管是否存在,不涉及文件系统,不解析任何链接,也不使用外部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)

Personally, I cannot understand why Shell, a language often used for manipulating files, doesn't offer basic functions to deal with paths. In python, we have nice libraries like os.path or pathlib, which offers a whole bunch of tools to extract filename, extension, basename, path segments, split or join paths, to get absolute or normalized paths, to determine relations between paths, to do everything without much brain. And they take care of edge cases, and they're reliable. In Shell, to do any of these, either we call external executables, or we have to reinvent wheels with these extremely rudimentary and arcane syntaxes...

老问题了,但是如果你在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,则无用。

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
}

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