如何在 bash 中手动展开特殊变量(例如:~ 波浪线)

发布于 2024-09-28 01:01:52 字数 502 浏览 1 评论 0原文

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

~/a/b/c

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

我如何设法将其传递到我的脚本而不扩展?我用双引号将参数括起来。

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

ls -lt "~"

这正是我所处的情况。我希望扩展波浪号。换句话说,我应该用什么替换 magic 以使这两个命令相同:

ls -lt ~/abc/def/ghi

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

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

I have a variable in my bash script whose value is something like this:

~/a/b/c

Note that it is unexpanded tilde. When I do ls -lt on this variable (call it $VAR), I get no such directory. I want to let bash interpret/expand this variable without executing it. In other words, I want bash to run eval but not run the evaluated command. Is this possible in bash?

How did I manage to pass this into my script without expansion? I passed the argument in surrounding it with double quotes.

Try this command to see what I mean:

ls -lt "~"

This is exactly the situation I am in. I want the tilde to be expanded. In other words, what should I replace magic with to make these two commands identical:

ls -lt ~/abc/def/ghi

and

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

Note that ~/abc/def/ghi may or may not exist.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(19

-柠檬树下少年和吉他 2024-10-05 01:01:52

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

eval var=$var  # Do not use this!

原因是:用户可能会不小心使用(或按目的)输入例如 var="$(rm -rf $HOME/)" 可能会带来灾难性的后果。

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

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

If the variable var is input by the user, eval should not be used to expand the tilde using

eval var=$var  # Do not use this!

The reason is: the user could by accident (or by purpose) type for example var="$(rm -rf $HOME/)" with possible disastrous consequences.

A better (and safer) way is to use Bash parameter expansion:

var="${var/#\~/$HOME}"
久而酒知 2024-10-05 01:01:52

由于 StackOverflow 的性质,我不能简单地让这个答案不被接受,但是在我发布这个答案的 5 年里,已经有比我公认的基本且相当糟糕的答案更好的答案了(我还年轻,不要别杀我)。

该线程中的其他解决方案是更安全、更好的解决方案。最好,我会选择这两个中的任何一个:


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

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

#!/bin/bash

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

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

Due to the nature of StackOverflow, I can't just make this answer unaccepted, but in the intervening 5 years since I posted this there have been far better answers than my admittedly rudimentary and pretty bad answer (I was young, don't kill me).

The other solutions in this thread are safer and better solutions. Preferably, I'd go with either of these two:


Original answer for historic purposes (but please don't use this)

If I'm not mistaken, "~" will not be expanded by a bash script in that manner because it is treated as a literal string "~". You can force expansion via eval like this.

#!/bin/bash

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

Alternatively, just use ${HOME} if you want the user's home directory.

决绝 2024-10-05 01:01:52

抄袭我自己的之前的答案,以便在不存在与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
}

Plagarizing myself from a prior answer, to do this robustly without the security risks associated with 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%:}"
}

...used as...

path=$(expandPath '~/hello')

Alternately, a simpler approach that uses eval carefully:

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
}
老娘不死你永远是小三 2024-10-05 01:01:52

这是一个荒谬的解决方案:

$ echo "echo $var" | bash

解释此命令的作用:

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

基本上,我们正在运行的当前 bash 实例会取代我们作为另一个 bash 实例的用户的位置,并为我们输入命令“echo ~...”。

Here is a ridiculous solution:

$ echo "echo $var" | bash

An explanation of what this command does:

  1. create a new instance of bash, by... calling bash;
  2. take the string "echo $var" and substitute $var with the value of the variable (thus after the substitution the string will contain the tilde);
  3. take the string produced by step 2 and send it to the instance of bash created in step one, which we do here by calling echo and piping its output with the | character.

Basically the current bash instance we're running takes our place as the user of another bash instance and types in the command "echo ~..." for us.

梦幻的味道 2024-10-05 01:01:52

这个怎么样:

path=`realpath "$1"`

或者:

path=`readlink -f "$1"`

How about this:

path=`realpath "$1"`

Or:

path=`readlink -f "$1"`
妥活 2024-10-05 01:01:52

使用 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} 一样简单

A safe way to use eval is "$(printf "~/%q" "$dangerous_path")". Note that is bash specific.

#!/bin/bash

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

See this question for details

Also, note that under zsh this would be as as simple as echo ${~dangerous_path}

︶葆Ⅱㄣ 2024-10-05 01:01:52

扩展(没有双关语)birriree 和 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 //>} 删除了可能会破坏 a 的 > 字符在eval期间文件。
  • eval echo ... 是实际的波形符扩展。
  • -e 参数周围的双引号用于支持带空格的文件名。

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

Expanding (no pun intended) on birryree's and halloleo's answers: The general approach is to use eval, but it comes with some important caveats, namely spaces and output redirection (>) in the variable. The following seems to work for me:

mypath="$1"

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

Try it with each of the following arguments:

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

Explanation

  • The ${mypath//>} strips out > characters which could clobber a file during the eval.
  • The eval echo ... is what does the actual tilde expansion
  • The double-quotes around the -e argument are for support of filenames with spaces.

Perhaps there's a more elegant solution, but this is what I was able to come up with.

情定在深秋 2024-10-05 01:01:52

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

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

why not delve straight into getting the user's home directory with getent?

$ getent passwd mike | cut -d: -f6
/users/mike
天赋异禀 2024-10-05 01:01:52

这是相当于 Håkon Hægland 的 Bash 答案

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

2017-12-10 编辑的 POSIX 函数:添加 '%s' 根据评论中的@CharlesDuffy。

Here is the POSIX function equivalent of Håkon Hægland's Bash answer

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

2017-12-10 edit: add '%s' per @CharlesDuffy in the comments.

别把无礼当个性 2024-10-05 01:01:52

供任何人参考,模仿 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
}

For anyone's reference, a function to mimic python's os.path.expanduser() behavior (no eval usage):

# _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

And the function:

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
}
笨笨の傻瓜 2024-10-05 01:01:52

我相信这就是您正在寻找的

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

I believe this is what you're looking for

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.$
}

Example usage:

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

$ magic ~invalid/this/will/not/expand
~invalid/this/will/not/expand
贱贱哒 2024-10-05 01:01:52

最简单:将“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

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

同样,如果您使用 eval echo ~ 而不是 eval echo "~",则将算作两次扩展,因此可以立即进行注入。

Simplest: replace 'magic' with 'eval echo'.

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

Problem: You're going to run into issues with other variables because eval is evil. For instance:

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

Note that the issue of the injection doesn't happen on the first expansion. So if you were to simply replace magic with eval echo, you should be okay. But if you do echo $(eval echo ~), that would be susceptible to injection.

Similarly, if you do eval echo ~ instead of eval echo "~", that would count as twice expanded and therefore injection would be possible right away.

梦忆晨望 2024-10-05 01:01:52

在使用 read -e (以及其他)读取路径后,我已经通过变量参数替换来完成此操作。因此,用户可以使用 Tab 键补全路径,如果用户输入 ~ 路径,则会对其进行排序。

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

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

(我包含 -i 来读取,因为我在循环中使用它,以便用户可以在出现问题时修复路径。)

I have done this with variable parameter substitution after reading in the path using read -e (among others). So the user can tab-complete the path, and if the user enters a ~ path it gets sorted.

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

The added benefit is that if there is no tilde nothing happens to the variable, and if there is a tilde but not in the first position it is also ignored.

(I include the -i for read since I use this in a loop so the user can fix the path if there is a problem.)

柏拉图鍀咏恒 2024-10-05 01:01:52

这是我的解决方案:

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

Here's my solution:

#!/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"
南街九尾狐 2024-10-05 01:01:52

只是为了扩展 birrryree 对带有空格的路径的回答:您不能将 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_" 的假设。

Just to extend birryree's answer for paths with spaces: You cannot use the eval command as is because it seperates evaluation by spaces. One solution is to replace spaces temporarily for the eval command:

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

This example relies of course on the assumption that mypath never contains the char sequence "_spc_".

真心难拥有 2024-10-05 01:01:52

您可能会发现在 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< /code> 的结果是:

/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

You might find this easier to do in python.

(1) From the unix command line:

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

Results in:

/Users/someone/fred

(2) Within a bash script as a one-off - save this as test.sh:

#!/usr/bin/env bash

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

echo $thepath

Running bash ./test.sh results in:

/Users/someone/fred

(3) As a utility - save this as expanduser somewhere on your path, with execute permissions:

#!/usr/bin/env python

import sys
import os

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

This could then be used on the command line:

expanduser ~/fred

Or in a script:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath
昔梦 2024-10-05 01:01:52

只需正确使用eval:进行验证。

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

Just use eval correctly: with validation.

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

由于某种原因,当字符串已经被引用时,只有 perl 可以挽救局面

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

for some reason when the string is already quoted only perl saves the day

  #val="${val/#\~/$HOME}" # for some reason does not work !!
  val=$(echo $val|perl -ne 's|~|'$HOME'|g;print')
牛↙奶布丁 2024-10-05 01:01:52

我认为这

thepath=( ~/abc/def/ghi )

比所有其他解决方案更容易......或者我错过了一些东西?即使该路径并不真正存在,它仍然有效。

I think that

thepath=( ~/abc/def/ghi )

is easier than all the other solutions... or I am missing something? It works even if the path does not really exists.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文