可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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:
- Charle\'s Duffy\'s solution
- Håkon Hægland\'s solution
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.
回答2:
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}\"
回答3:
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 _ _ _ _ _ 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
}
回答4:
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}
回答5:
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.
回答6:
How about this:
path=`realpath \"$1\"`
Or:
path=`readlink -f \"$1\"`
回答7:
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
回答8:
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\"
回答9:
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\\\"}\"
回答10:
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.
回答11:
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_\"
.
回答12:
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
回答13:
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.