如何在 bash 中手动展开一个特殊的变量(例如: ~ tilde)

我在 bash 脚本中有一个变量,它的值是这样的:

~/a/b/c

注意它是未展开的波浪。当我对这个变量(称为 $VAR)执行 ls-lt 操作时,不会得到这样的目录。我想让 bash 解释/展开这个变量,而不执行它。换句话说,我希望 bash 运行 eval,但不运行已计算命令。在 bash 里这可能吗?

我是如何设法在没有扩展的情况下将其传递到我的脚本中的?我用双引号包围它来传递参数。

试试这个命令,看看我的意思:

ls -lt "~"

这就是我现在的处境。我希望波浪能够扩大。换句话说,我应该用什么来替代魔法,使这两个命令完全相同:

ls -lt ~/abc/def/ghi

还有

ls -lt $(magic "~/abc/def/ghi")

注意 ~/abc/def/ghi 可能存在也可能不存在。

105765 次浏览

由于 StackOverflow 的性质,我不能只是让这个答案不被接受,但在我发布这篇文章后的5年里,我得到的答案远远好于我公认的基本和相当糟糕的答案(我当时还年轻,不要杀了我)。

这个线程中的其他解决方案是更安全、更好的解决方案:


原始答案的历史目的(但请不要使用这一点)

如果我没有弄错的话,"~"将不会以这种方式被 bash 脚本展开,因为它被视为一个字符串 "~"。你可以像这样通过 eval强制扩展。

#!/bin/bash


homedir=~
eval homedir=$homedir
echo $homedir # prints home path

或者,如果需要用户的主目录,只需使用 ${HOME}

这样吧:

path=`realpath "$1"`

或者:

path=`readlink -f "$1"`

只是为了扩展 Birryree对带有空格的路径的解答: 您不能按原样使用 eval命令,因为它使用空格将求值分隔开。一种解决方案是临时替换 eval 命令的空格:

mypath="~/a/b/c/Something With Spaces"
expandedpath=${mypath// /_spc_}    # replace spaces
eval expandedpath=${expandedpath}  # put spaces back
expandedpath=${expandedpath//_spc_/ }
echo "$expandedpath"    # prints e.g. /Users/fred/a/b/c/Something With Spaces"
ls -lt "$expandedpath"  # outputs dir content

这个例子当然依赖于这样的假设: mypath从来不包含字符序列 "_spc_"

对 birryree 和 halloleo 的答案进行扩展(没有双关语的意思) : 一般的方法是使用 eval,但它附带了一些重要的警告,即变量中的空格和输出重定向(>)。以下几点似乎对我有用:

mypath="$1"


if [ -e "`eval echo ${mypath//>}`" ]; then
echo "FOUND $mypath"
else
echo "$mypath NOT FOUND"
fi

试着用下面的每一个论点:

'~'
'~/existing_file'
'~/existing file with spaces'
'~/nonexistant_file'
'~/nonexistant file with spaces'
'~/string containing > redirection'
'~/string containing > redirection > again and >> again'

解释

  • ${mypath//>}去掉了 >字符,这些字符可以在 eval期间重击文件。
  • eval echo ...就是实际的波浪膨胀
  • -e参数周围的双引号用于支持带空格的文件名。

也许还有更好的解决办法但这是我能想到的。

使用 eval 的一种安全方法是 "$(printf "~/%q" "$dangerous_path")"

#!/bin/bash


relativepath=a/b/c
eval homedir="$(printf "~/%q" "$relativepath")"
echo $homedir # prints home path

详情请参阅 这个问题

另外,请注意,在 zsh 下,这将与 echo ${~dangerous_path}一样简单

如果变量 var是由用户输入的,eval应该使用 没有来展开波浪

eval var=$var  # Do not use this!

原因是: 用户可能偶然(或故意)输入例如 var="$(rm -rf $HOME/)",从而可能造成灾难性的后果。

一种更好(也更安全)的方法是使用 Bash 参数展开:

var="${var/#\~/$HOME}"

剽窃自己从 先验的答案,做到这一点,没有与 eval相关的安全风险:

expandPath() {
local path
local -a pathElements resultPathElements
IFS=':' read -r -a pathElements <<<"$1"
: "${pathElements[@]}"
for path in "${pathElements[@]}"; do
: "$path"
case $path in
"~+"/*)
path=$PWD/${path#"~+/"}
;;
"~-"/*)
path=$OLDPWD/${path#"~-/"}
;;
"~"/*)
path=$HOME/${path#"~/"}
;;
"~"*)
username=${path%%/*}
username=${username#"~"}
IFS=: read -r _ _ _ _ _ homedir _ < <(getent passwd "$username")
if [[ $path = */* ]]; then
path=${homedir}/${path#*/}
else
path=$homedir
fi
;;
esac
resultPathElements+=( "$path" )
done
local result
printf -v result '%s:' "${resultPathElements[@]}"
printf '%s\n' "${result%:}"
}

用作..。

path=$(expandPath '~/hello')

另一种更简单的方法是谨慎使用 eval:

expandPath() {
case $1 in
~[+-]*)
local content content_q
printf -v content_q '%q' "${1:2}"
eval "content=${1:0:2}${content_q}"
printf '%s\n' "$content"
;;
~*)
local content content_q
printf -v content_q '%q' "${1:1}"
eval "content=~${content_q}"
printf '%s\n' "$content"
;;
*)
printf '%s\n' "$1"
;;
esac
}

我相信这就是你要找的

magic() { # returns unexpanded tilde express on invalid user
local _safe_path; printf -v _safe_path "%q" "$1"
eval "ln -sf ${_safe_path#\\} /tmp/realpath.$$"
readlink /tmp/realpath.$$
rm -f /tmp/realpath.$$
}

示例用法:

$ magic ~nobody/would/look/here
/var/empty/would/look/here


$ magic ~invalid/this/will/not/expand
~invalid/this/will/not/expand

我的解决办法是:

#!/bin/bash




expandTilde()
{
local tilde_re='^(~[A-Za-z0-9_.-]*)(.*)'
local path="$*"
local pathSuffix=


if [[ $path =~ $tilde_re ]]
then
# only use eval on the ~username portion !
path=$(eval echo ${BASH_REMATCH[1]})
pathSuffix=${BASH_REMATCH[2]}
fi


echo "${path}${pathSuffix}"
}






result=$(expandTilde "$1")


echo "Result = $result"

您可能会发现在 python 中这样做更容易。

(1)从 unix 命令行:

python -c 'import os; import sys; print os.path.expanduser(sys.argv[1])' ~/fred

结果:

/Users/someone/fred

(2)在 bash 脚本中,将其保存为 test.sh:

#!/usr/bin/env bash


thepath=$(python -c 'import os; import sys; print os.path.expanduser(sys.argv[1])' $1)


echo $thepath

运行 bash ./test.sh的结果是:

/Users/someone/fred

(3)作为一个实用程序-保存为 expanduser在您的路径上的某个地方,执行权限:

#!/usr/bin/env python


import sys
import os


print os.path.expanduser(sys.argv[1])

然后可以在命令行上使用:

expanduser ~/fred

或者在剧本里:

#!/usr/bin/env bash


thepath=$(expanduser $1)


echo $thepath

只要正确使用 eval: 带有验证。

case $1${1%%/*} in
([!~]*|"$1"?*[!-+_.[:alnum:]]*|"") ! :;;
(*/*)  set "${1%%/*}" "${1#*/}"       ;;
(*)    set "$1"
esac&& eval "printf '%s\n' $1${2+/\"\$2\"}"

下面是与 Håkon Hægland 的 bash回答等效的 POSIX 函数

expand_tilde() {
tilde_less="${1#\~/}"
[ "$1" != "$tilde_less" ] && tilde_less="$HOME/$tilde_less"
printf '%s' "$tilde_less"
}

2017-12-10编辑: 添加 '%s' per@CharlesDuffy 在评论中。

最简单的 : 将“ magic”替换为“ eval echo”。

$ eval echo "~"
/whatever/the/f/the/home/directory/is

问题: 你会遇到其他变量的问题,因为 eval 是邪恶的。例如:

$ # home is /Users/Hacker$(s)
$ s="echo SCARY COMMAND"
$ eval echo $(eval echo "~")
/Users/HackerSCARY COMMAND

注意,注入问题不会发生在第一次膨胀时。因此,如果您只是用 eval echo替换 magic,那么应该没有问题。但是,如果你做 echo $(eval echo ~),这将是容易受到注射。

类似地,如果你使用 eval echo ~而不是 eval echo "~",那将被视为两次展开,因此立刻就可以进行注入。

在使用 read-e 读取路径之后,我使用变量参数替换完成了这项工作。因此用户可以选项卡完成路径,如果用户输入一个 ~ path,它将被排序。

read -rep "Enter a path:  " -i "${testpath}" testpath
testpath="${testpath/#~/${HOME}}"
ls -al "${testpath}"

附加的好处是,如果没有波浪,没有什么发生在变量,如果有一个波浪,但不是在第一个位置,它也被忽略。

(我包含了-i 作为 read,因为我在循环中使用了这个,所以如果有问题,用户可以修复路径。)

为什么不直接使用 getent 获取用户的主目录呢?

$ getent passwd mike | cut -d: -f6
/users/mike

这里有一个荒谬的解决方案:

$ echo "echo $var" | bash

这个命令的作用说明如下:

  1. 通过调用 bash创建一个新的 bash 实例;
  2. 取字符串 "echo $var"并用变量的值替换 $var(因此在替换之后字符串将包含波浪线) ;
  3. 获取步骤2生成的字符串并将其发送到步骤1中创建的 bash 实例,在这里我们通过调用 echo并使用 |字符管道输出来完成。

基本上,我们正在运行的当前 bash 实例代替我们作为另一个 bash 实例的用户,并为我们键入命令 "echo ~..."

由于某种原因,当字符串已经被引用时,只有 perl 可以节省时间

  #val="${val/#\~/$HOME}" # for some reason does not work !!
val=$(echo $val|perl -ne 's|~|'$HOME'|g;print')

我觉得

thepath=( ~/abc/def/ghi )

比其他方法都简单,还是我漏掉了什么?即使路径并不真正存在,它也能工作。

对于任何人的参考,一个模仿 python 的 os.path.expanduser ()行为的函数(没有 eval 用法) :

# _expand_homedir_tilde ~/.vim
/root/.vim
# _expand_homedir_tilde ~myuser/.vim
/home/myuser/.vim
# _expand_homedir_tilde ~nonexistent/.vim
~nonexistent/.vim
# _expand_homedir_tilde /full/path
/full/path

功能:

function _expand_homedir_tilde {
(
set -e
set -u
p="$1"
if [[ "$p" =~ ^~ ]]; then
u=`echo "$p" | sed 's|^~\([a-z0-9_-]*\)/.*|\1|'`
if [ -z "$u" ]; then
u=`whoami`
fi


h=$(set -o pipefail; getent passwd "$u" | cut -d: -f6) || exit 1
p=`echo "$p" | sed "s|^~[a-z0-9_-]*/|${h}/|"`
fi
echo $p
) || echo $1
}