如何在 Bash 中规范化文件路径?
- 2024-10-18 09:00:00
- admin 原创
- 67
问题描述:
我想/foo/bar/..
转型/foo
是否有一个 bash 命令可以执行此操作?
编辑:就我的实际情况而言,该目录确实存在。
解决方案 1:
如果你想从路径中截取部分文件名,“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
你的 shell 不支持,你可以尝试
readlink -f /path/here/..
还
readlink -m /path/there/../../
工作原理相同
realpath -s /path/here/../../
因为该路径不需要存在才能被规范化。
解决方案 2:
我不知道是否有直接的 bash 命令可以做到这一点,但我通常会这样做
normalDir="`cd "${dirToNormalize}";pwd`"
echo "${normalDir}"
而且效果很好。
解决方案 3:
尝试一下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
", realpath(argv[1], sPath));
return 0;
}
void usage(void)
{
fprintf(stderr, "usage: %s PATH
", s_pMyName);
exit(1);
}
解决方案 4:
一个可移植且可靠的解决方案是使用 Python,它几乎在任何地方都预装了(包括 Darwin)。您有两个选择:
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
都可以是相对路径或绝对路径。
解决方案 5:
使用 coreutils 包中的 readlink 实用程序。
MY_PATH=$(readlink -f "$0")
解决方案 6:
老问题,但如果你在 shell 级别处理完整路径名,还有更简单的方法:
abspath="$( cd "$path" && pwd )"
由于 cd 发生在子 shell 中,因此它不会影响主脚本。
假设你的 shell 内置命令接受 -L 和 -P,则有两种变体:
abspath="$( cd -P "$path" && pwd -P )" # 具有已解析符号链接的物理路径
abspath="$( cd -L "$path" && pwd -L )" #逻辑路径保留符号链接
就我个人而言,我很少需要这种方法,除非我因为某种原因对符号链接很着迷。
仅供参考:获取脚本的起始目录的变化即使脚本稍后更改其当前目录也能起作用。
name0="$(basename "$0")";#脚本的基本名称
dir0="$( cd "$( dirname "$0" )" && pwd )"; #绝对起始目录
使用 CD 可确保您始终拥有绝对目录,即使脚本由诸如 ./script.sh 之类的命令运行,如果没有 cd/pwd,通常只会给出 .。如果脚本稍后执行 cd,则毫无用处。
解决方案 7:
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 上得到一个错误。
解决方案 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"
}
如果您希望它解析符号链接,只需将其替换pwd
为pwd -P
。
解决方案 9:
我最近的解决方案是:
pushd foo/bar/..
dir=`pwd`
popd
根据 Tim Whitcomb 的回答。
解决方案 10:
不完全是一个答案,但也许是一个后续问题(原始问题并不明确):
readlink
如果您确实想遵循符号链接,那么 是可以的。但是,还有一种用例,即仅规范化./
和../
和//
序列,这可以纯粹从语法上完成,而无需规范化符号链接。readlink
对此不适用, 也不适用realpath
。
for f in $paths; do (cd $f; pwd); done
适用于现有路径,但对其他路径无效。
脚本sed
似乎是个不错的选择,但是如果不使用像 Perl 这样的东西,你就无法迭代地替换序列(/foo/bar/baz/../..
-> /foo/bar/..
-> /foo
),这在所有系统上都是不安全的,或者使用一些丑陋的循环来比较其输出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
解决方案 11:
我迟到了,但这是我在阅读了一堆类似这样的帖子后想出的解决方案:
resolve_dir() {
(builtin cd `dirname "${1/#~/$HOME}"`'/'`basename "${1/#~/$HOME}"` 2>/dev/null; if [ $? -eq 0 ]; then pwd; fi)
}
这将解析 $1 的绝对路径,与 ~ 配合良好,将符号链接保留在它们所在的路径中,并且不会弄乱您的目录堆栈。如果不存在,它将返回完整路径或不返回任何内容。它期望 $1 是一个目录,如果不是,则可能会失败,但这是一个您可以轻松完成的检查。
解决方案 12:
回答有点晚,但很健谈。我需要写一个,因为我还在使用旧版 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`
解决方案 13:
问题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
}
请注意,此变体也不需要路径存在。
解决方案 14:
我制作了一个内置函数来处理这个问题,重点是尽可能提高性能(为了好玩)。它不解析符号链接,因此它与 基本相同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
'` instead
done
return $r;
}
注意:此函数不处理以 开头的路径//
,因为路径开头的两个双斜杠是实现定义的行为。但是,它可以很好地处理/
、///
等。
这个函数似乎可以正确处理所有边缘情况,但可能还有一些我还没有处理的情况。
性能说明:当使用数千个参数调用时,abspath
运行速度比慢 10 倍左右realpath -sm
;当使用单个参数调用时,abspath
运行速度比realpath -sm
在我的计算机上快 110 倍以上,主要是因为不需要每次都执行新程序。
解决方案 15:
尝试一下我们新的 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 函数。跨平台试用,并帮助改进。
解决方案 16:
根据@Andre 的回答,如果有人想要一个无循环、完全基于字符串操作的解决方案,我可能会有一个稍微好一点的版本。它对那些不想取消引用任何符号链接的人来说也很有用,这是使用realpath
或 的缺点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
解决方案 17:
如果您只想规范化一个路径,无论存在与否,而不触及文件系统,不解析任何链接,也不需要外部实用程序,这里有一个从 Python 翻译而来的纯Bashposixpath.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) || (\n ${#comps[@]} -gt 0 && ${comps[-1]} == '..') ]]; then
comps+=("${comp}")
elif ((${#comps[@]})); then
unset 'comps[-1]'
fi
done
comp="${initial_slashes}${comps[*]}"
printf '%s
' "${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 中,要执行任何这些操作,我们要么调用外部可执行文件,要么必须用这些极其基本和晦涩的语法重新发明轮子……
解决方案 18:
我需要一个可以解决这三个问题的解决方案:
可在普通 Mac 上运行。
realpath
并且readlink -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:
根据 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
解决方案 20:
FILEPATH="file.txt"
echo $(realpath $(dirname $FILEPATH))/$(basename $FILEPATH)
即使文件不存在,此方法也能奏效。它要求包含该文件的目录存在。
解决方案 21:
我知道这是一个古老的问题。我仍然提供另一种选择。最近我遇到了同样的问题,但找不到现有的可移植命令来执行此操作。所以我编写了以下 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
解决方案 22:
由于上述所有解决方案都不适合我,因此在文件不存在的情况下,我实现了我的想法。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
}
解决方案 23:
对于绝对的、规范化的、可能缺失的路径,我使用了:
"/$(realpath -m --relative-to / SOME_PATH)"
# example
echo "/$(realpath -m --relative-to / /etc/bogus/..)"
您可以看到更多选项realpath --help
解决方案 24:
今天我发现可以使用stat
命令来解析路径。
因此对于像“~/Documents”这样的目录:
你可以运行这个:
stat -f %N ~/Documents
获取完整路径:
/Users/me/Documents
对于符号链接,您可以使用 %Y 格式选项:
stat -f %Y example_symlink
可能会返回如下结果:
/usr/local/sbin/example_symlink
*NIX 的其他版本上的格式选项可能有所不同,但这些选项在 OSX 上对我来说是有效的。
解决方案 25:
一个简单的解决方案使用node.js
:
#!/usr/bin/env node
process.stdout.write(require('path').resolve(process.argv[2]));
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件