如何在bash中手动扩展特殊变量(例如:〜波浪号)


135

我的bash脚本中有一个变量,其值如下所示:

~/a/b/c

请注意,它是未扩展的代字号。当我对该变量执行ls -lt(称为$ VAR)时,没有得到这样的目录。我想让bash解释/扩展这个变量而不执行它。换句话说,我希望bash运行eval但不运行评估的命令。bash有可能吗?

我如何在不扩展的情况下将其传递到脚本中?我用双引号将参数引起来。

尝试以下命令以了解我的意思:

ls -lt "~"

这正是我所处的情况。我希望扩展代字号。换句话说,我应该用魔术代替什么来使这两个命令相同:

ls -lt ~/abc/def/ghi

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

注意〜/ abc / def / ghi可能存在或可能不存在。


4
您可能会发现引号中的Tilde扩展很有用。它主要(但不是全部)避免使用eval
Jonathan Leffler 2014年

2
如何为变量分配未扩展的代字号?也许所需要做的就是给该变量加上引号之外的波浪号。 foo=~/"$filepath"foo="$HOME/$filepath"
Chad Skeeters,2015年

dir="$(readlink -f "$dir")"
Jack Wasey

Answers:


98

由于StackOverflow的性质,我不能只是使这个答案不被接受,但是自从我发布这个问题以来的5年间,比我公认的基本和相当差的答案要好得多(我还很年轻,请不要杀了。我)。

此线程中的其他解决方案是更安全,更好的解决方案。最好是,我会选择以下两个之一:


出于历史目的的原始答案(但请不要使用此答案)

如果我没记错的话,"~"bash脚本不会以这种方式对其进行扩展,因为它被视为文字字符串"~"。您可以通过eval这样强制扩展。

#!/bin/bash

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

或者,仅${HOME}在需要用户的主目录时使用。


3
当变量中有空格时,您是否有解决方法?
雨果

34
我发现${HOME}最有吸引力。有什么理由不将此作为您的主要推荐?无论如何,谢谢!
sage 2013年

1
+1-我需要扩展〜$ some_other_user,并且当$ HOME无法工作时eval可以正常工作,因为我不需要当前用户家。
olivecoder 2013年

11
使用eval是一个可怕的建议,它获得如此多的支持是非常糟糕的。当变量的值包含外壳元字符时,您将遇到各种问题。
user2719058 2014年

1
那时我无法完成我的评论,因此,我后来不允许对其进行编辑。因此,我很感谢(再次感谢@birryree)此解决方案,因为它对当时的特定情况有所帮助。感谢查尔斯使我意识到。
Olivecoder

114

如果变量var是由用户输入的,eval应该使用利用扩展波形符

eval var=$var  # Do not use this!

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

更好(更安全)的方法是使用Bash参数扩展:

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

8
您如何更改〜userName /而不是〜/呢?
aspergillusOryzae 2014年

3
#in 的目的是"${var/#\~/$HOME}"什么?
Jahid

3
@Jahid在手册中进行了说明。强制波浪号仅在的开头匹配$var
哈康Hægland

1
谢谢。(1)为什么我们需要\~逃避~?(2)您的答复假设~是中的第一个字符$var。我们如何才能忽略其中的前导空白$var
蒂姆(Tim)

1
@Tim感谢您的评论。是的,您是对的,除非波浪号是未加引号的字符串的第一个字符或:在未加引号的字符串之后,否则我们不需要转义波浪号。在docs中有更多信息。要删除前导空格,请参阅如何从Bash变量中修剪空格?
哈康·海格兰

24

使自己脱离先前的回答,可以稳健地做到这一点,而不会带来以下安全隐患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
}

4
查看您的代码,似乎您正在使用大炮杀死蚊子。还有的成为一个更简单的方法..
吉诺

2
@Gino,肯定有一种更简单的方法;问题是是否有一种更简单的方法也很安全。
2015年

2
@Gino,...我确实认为,printf %q除了波浪号以外,其他任何人都可以逃避使用,然后再无eval风险使用。
2015年

1
@Gino,...如此实现。
查尔斯·达菲

2
安全,是的,但是非常不完整。我的代码并不是很有趣,它很复杂,因为波浪号扩展所完成的实际操作很复杂。
查尔斯·达菲

10

使用eval的安全方法是"$(printf "~/%q" "$dangerous_path")"。请注意,这是特定于bash的。

#!/bin/bash

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

看到这个问题的细节

另外,请注意,在zsh下,这就像 echo ${~dangerous_path}


echo ${~root}在zsh(mac os x)上不提供输出
Orwellophile 2015年

export test="~root/a b"; echo ${~test}
吉斯科斯(Gyscos)2015年

9

这个怎么样:

path=`realpath "$1"`

要么:

path=`readlink -f "$1"`

看起来不错,但是我的Mac上不存在realpath。而且您必须编写path = $(realpath“ $ 1”)
Hugo

嗨@雨果 您可以realpath在C语言中编译自己的命令。例如,可以从以下命令行realpath.exe使用bashgcc生成可执行文件gcc -o realpath.exe -x c - <<< $'#include <stdlib.h> \n int main(int c,char**v){char p[9999]; realpath(v[1],p); puts(p);}'。干杯
olibre

@Quuxplusone不是真的,至少在linux上是:realpath ~->/home/myhome
blueFast

iv'e与MAC酿造用它
nhed

1
@dangonfast如果将波浪号设置为引号,则将不起作用,结果为<workingdir>/~
墨菲

7

对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参数周围的双引号用于支持带空格的文件名。

也许有一个更优雅的解决方案,但这就是我能想到的。


3
您可以考虑使用名称包含的行为$(rm -rf .)
Charles Duffy 2015年

1
但是,这不会在实际上包含>字符的路径上中断吗?
Radon Rosborough

2

我相信这就是您要寻找的

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

我很惊讶printf %q 没有摆脱领先的波浪线-将其提交为bug几乎是诱人的,因为在这种情况下,它无法达到指定的目的。但是,在此期间,请致电!
2015年

1
实际上-此错误已在3.2.57和4.3.18之间的某个位置修复,因此该代码不再起作用。
查尔斯·达菲

1
好点,我已经调整了代码以删除前导\(如果存在的话),因此所有的工作都固定了:)我在测试时未引用参数,因此在调用函数之前已进行了扩展。
Orwellophile 2015年

1

这是我的解决方案:

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

同样,依赖echo意味着它expandTilde -n不会表现出预期的行为,并且POSIX未定义文件名包含反斜杠的行为。参见pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Charles Duffy

接得好。我通常使用一台用户计算机,因此我不认为要处理这种情况。但是我认为可以通过/ etc / passwd文件为其他用户grepping来轻松增强该功能以处理其他情况。我将其留给其他人练习:)。
吉诺,2015年

我已经做了一个练习(并处理了OLDPWD案件和其他案件),答案是您认为太复杂了。:)
查尔斯·达菲

实际上,我只是找到了一个可以处理其他用户情况的相当简单的单行解决方案:path = $(eval echo $ orgPath)
Gino

1
仅供参考:我刚刚更新了解决方案,以便它现在可以正确处理〜username。而且,它也应该相当安全。即使您将'/ tmp / $(rm -rf / *)'作为参数,它也应正常处理。
吉诺2015年

1

只需eval正确使用即可:带有验证。

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

这可能很安全-我还没有找到失败的案例。就是说,如果我们要说的是“正确”使用eval,我认为Orwellophile的答案遵循了更好的做法:我相信外壳程序printf %q比其他人相信手写的验证代码没有错误更安全地逃避事情。
查尔斯·达菲

@查尔斯·达菲-真傻。外壳可能没有%q-并且printf是一个$PATH'd命令。
mikeserv

1
这个问题没有标记bash吗?如果是这样,printf则是内置的,并%q保证存在。
查尔斯·达菲

@Charles Duffy-什么版本?
mikeserv

1
@查尔斯·达菲(Charles Duffy)-太早了。但是我仍然觉得很奇怪,您对%q arg的信任程度要比您在眼前编码的信任bash程度高,我在知道信任它之前就花了足够的时间。尝试:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv

1

这是相当于HåkonHægland的Bash 答案的POSIX函数

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

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


1
printf '%s\n' "$tilde_less", 也许?否则,如果要扩展的文件名包含反斜杠%s或其他对有意义的语法,则会出现问题printf。但是,除此之外,这是一个很好的答案-正确(当不需要覆盖bash / ksh扩展名时),显然很安全(无需添加eval)和简洁。
查尔斯·达菲

1

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

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

0

只是为了扩展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从不包含char序列的假设"_spc_"


1
不适用于制表符,换行符或IFS中的其他任何内容...,也无法为包含路径的元字符提供安全性$(rm -rf .)
Charles Duffy 2015年

0

您可能会发现在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

还是只将“〜”传递给Python,返回“ / home / fred”呢?
汤姆·罗素

1
需要摩尔报价。echo $thepath越野车 需要echo "$thepath"解决不常见的情况(带有制表符或空格的名称将转换为单个空格;具有扩展它们的glob的名称),或者printf '%s\n' "$thepath"也修复不常见的情况(即名为-n的文件或带有XSI兼容系统上的反斜杠文字)。同样地,thepath=$(expanduser "$1")
Charles Duffy 2015年

...要了解我对反斜杠文字的含义,请参阅pubs.opengroup.org/onlinepubs/009604599/utilities/echo.htmlecho如果任何参数包含反斜杠,POSIX都可以以完全实现定义的方式运行;此类名称的可选XSI扩展到POSIX强制性默认(不需要-e-E不需要)扩展行为。
Charles Duffy 2015年

0

最简单:将“魔术”替换为“评估回声”。

$ 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

请注意,在第一次扩展中不会发生注入问题。因此,如果您要简单地替换magiceval echo,则应该可以。但是,如果这样做echo $(eval echo ~),很容易被注入。

同样,如果您使用eval echo ~代替eval echo "~",则将被视为扩展了两倍,因此可以立即进行注入。


1
与您所说的相反,此代码不安全。例如,测试s='echo; EVIL_COMMAND'。(它将失败,因为EVIL_COMMAND您的计算机上不存在。但是rm -r ~,例如,如果该命令已被删除,则它将删除您的主目录。)
Konrad Rudolph,

0

在使用read -e(以及其他方法)读取路径后,我用可变参数替换完成了此操作。因此,用户可以使用制表符完成路径,如果用户输入〜路径,则会对其进行排序。

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

附加的好处是,如果没有波浪号,则变量不会发生任何变化;如果有波浪号但不在第一个位置,则也将被忽略。

(由于在循环中使用了-i,因此我将其包括-i以便读取,以便在出现问题时用户可以修复路径。)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.