关于脚本:如何在bash中手动扩展特殊变量(例如:~tottde)

How to manually expand a special variable (ex: ~ tilde) in bash

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

1
~/a/b/c

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

我是如何在不扩展的情况下将其传递到我的脚本中的? 我用双引号传递了围绕它的论点。

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

1
ls -lt"~"

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

1
ls -lt ~/abc/def/ghi

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

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


如果用户输入变量var,则不应使用eval来扩展波形符

1
eval var=$var  # Do not use this!

原因是:用户可能偶然(或按目的)键入例如var="$(rm -rf $HOME/)",可能带来灾难性后果。

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

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


由于StackOverflow的性质,我不能仅仅接受这个答案,但是在我发布这篇文章的5年间,我得到的答案要远远超过我公认的基本和非常糟糕的答案(我很年轻,不要杀人)我)。

该主题中的其他解决方案是更安全,更好的解决方案。我最好选择以下两种方法之一:

  • 查理的达菲的解决方案
  • H?kon H?gland的解决方案

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

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

1
2
3
4
5
#!/bin/bash

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

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


先前的答案中整理自己,以便在没有与eval相关的安全风险的情况下做到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
'
"${result%:}"
}

......用作......

1
path=$(expandPath '~/hello')

或者,一个更简单的方法,小心使用eval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
expandPath() {
  case $1 in
    ~[+-]*)
      local content content_q
      printf -v content_q '%q'"${1:2}"
      eval"content=${1:0:2}${content_q}"
      printf '%s
'
"$content"
      ;;
    ~*)
      local content content_q
      printf -v content_q '%q'"${1:1}"
      eval"content=~${content_q}"
      printf '%s
'
"$content"
      ;;
    *)
      printf '%s
'
"$1"
      ;;
  esac
}


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

1
2
3
4
5
#!/bin/bash

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

有关详情,请参阅此问题

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


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

1
2
3
4
5
6
7
mypath="$1"

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

尝试使用以下每个参数:

1
2
3
4
5
6
7
'~'
'~/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参数周围的双引号用于支持带空格的文件名。

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


这个怎么样:

1
path=`realpath"$1"`

要么:

1
path=`readlink -f"$1"`


我相信这就是你要找的东西

1
2
3
4
5
6
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.$$
}

用法示例:

1
2
3
4
5
$ magic ~nobody/would/look/here
/var/empty/would/look/here

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


这里的POSIX函数相当于H?kon H?gland的Bash答案

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

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


只需正确使用eval:验证。

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


这是我的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/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"


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

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

额外的好处是,如果没有波形,变量没有任何变化,如果有波浪但不在第一个位置,它也会被忽略。

(因为我在循环中使用它所以包含-i用于读取,因此如果出现问题,用户可以修复路径。)


最简单:用'eval echo'代替'magic'。

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

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

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

请注意,第一次扩展时不会发生注入问题。因此,如果您只是将magic替换为eval echo,那么您应该没问题。但是,如果你做echo $(eval echo ~),那将很容易注射。

类似地,如果你执行eval echo ~而不是eval echo"~",那将被视为两倍扩展,因此可以立即注入。


您可能会发现在python中更容易做到这一点。

(1)从unix命令行:

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

结果是:

1
/Users/someone/fred

(2)在bash脚本中作为一次性 - 将其保存为test.sh

1
2
3
4
5
#!/usr/bin/env bash

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

echo $thepath

运行bash ./test.sh会导致:

1
/Users/someone/fred

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

1
2
3
4
5
6
#!/usr/bin/env python

import sys
import os

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

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

1
expanduser ~/fred

或者在脚本中:

1
2
3
4
5
#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath


只是为了扩展birryree对带空格的路径的答案:你不能按原样使用eval命令,因为它用空格分隔评估。一种解决方案是临时替换eval命令的空格:

1
2
3
4
5
6
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_"