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.
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}"
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.
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
}
How about this:
path=`realpath "$1"`
Or:
path=`readlink -f "$1"`
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}
Here is a ridiculous solution:
$ echo "echo $var" | bash
An explanation of what this command does:
create a new instance of bash, by... calling bash;
take the string "echo $var" and substitute $var with the value of the variable (thus after the substitution the string will contain the tilde);
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.
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.
why not delve straight into getting the user's home directory with getent?
$ getent passwd mike | cut -d: -f6
/users/mike
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
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.
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"
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.
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
}
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_".
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
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\"}"
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.)
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')
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.
Related
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.
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}"
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.
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
}
How about this:
path=`realpath "$1"`
Or:
path=`readlink -f "$1"`
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}
Here is a ridiculous solution:
$ echo "echo $var" | bash
An explanation of what this command does:
create a new instance of bash, by... calling bash;
take the string "echo $var" and substitute $var with the value of the variable (thus after the substitution the string will contain the tilde);
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.
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.
why not delve straight into getting the user's home directory with getent?
$ getent passwd mike | cut -d: -f6
/users/mike
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
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.
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"
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.
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
}
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_".
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
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\"}"
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.)
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')
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.
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.
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}"
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.
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
}
How about this:
path=`realpath "$1"`
Or:
path=`readlink -f "$1"`
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}
Here is a ridiculous solution:
$ echo "echo $var" | bash
An explanation of what this command does:
create a new instance of bash, by... calling bash;
take the string "echo $var" and substitute $var with the value of the variable (thus after the substitution the string will contain the tilde);
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.
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.
why not delve straight into getting the user's home directory with getent?
$ getent passwd mike | cut -d: -f6
/users/mike
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
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.
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"
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.
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
}
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_".
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
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\"}"
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.)
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')
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.
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.
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}"
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.
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
}
How about this:
path=`realpath "$1"`
Or:
path=`readlink -f "$1"`
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}
Here is a ridiculous solution:
$ echo "echo $var" | bash
An explanation of what this command does:
create a new instance of bash, by... calling bash;
take the string "echo $var" and substitute $var with the value of the variable (thus after the substitution the string will contain the tilde);
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.
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.
why not delve straight into getting the user's home directory with getent?
$ getent passwd mike | cut -d: -f6
/users/mike
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
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.
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"
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.
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
}
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_".
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
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\"}"
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.)
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')
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.
so i have the following string:
packageinfo="package: name='nl.test.app' versionCode='64' versionName='5.1' platformBuildVersionName='6.0-2438415'"
...set as a shell variable, to allow the following:
echo $packageinfo
i want to extract the value in between quotes after name= so i need variable that will contain nl.test.app how would i do this?
Assuming bash, not POSIX sh:
#!/bin/bash
# ^^^^ IMPORTANT, not /bin/sh
re="name='([^']*)'"
if [[ $packageinfo =~ $re ]]; then
echo "name is ${BASH_REMATCH[1]}"
fi
That said, there are certainly POSIX sh solutions available as well. To pick one that doesn't require external tools such as awk or sed:
#!/bin/sh
getval() {
target=$1; shift
q="'"
for arg; do
case $arg in
*=*)
k=${arg%%=*}
v=${arg#*=}; v=${v#$q}; v=${v%$q}
if [ "$k" = "$target" ]; then
printf '%s\n' "$v"
return 0
fi
;;
esac
done
}
packageinfo="package: name='nl.test.app' versionCode='64' versionName='5.1' platformBuildVersionName='6.0-2438415'"
set -f # disable globbing
# expand without quotes to break into words; only safe with globbing disabled
getval name $packageinfo
To complement Charles Duffy's helpful pure Bash/shell solutions with a POSIX-compliant awk solution, assuming that - as is common - tagging a question with bash or shell simply means that a solution that can be called from the shell is desired, so that using standard Unix utilities is an option:
packageinfo="package: name='nl.test.app' versionCode='64' versionName='5.1' platformBuildVersionName='6.0-2438415'"
packagename=$(echo "$packageinfo" | awk -F".* name='|'" '{ print $2 }')
You can use the POSIX expr command:
name=$(expr "$packageinfo" : ".*name='\([^']*\)'")
I'm having a problem with a shell script (POSIX shell under HP-UX, FWIW). I have a function called print_arg into which I'm passing the name of a parameter as $1. Given the name of the parameter, I then want to print the name and the value of that parameter. However, I keep getting an error. Here's an example of what I'm trying to do:
#!/usr/bin/sh
function print_arg
{
# $1 holds the name of the argument to be shown
arg=$1
# The following line errors off with
# ./test_print.sh[9]: argval=${"$arg"}: The specified substitution is not valid for this command.
argval=${"$arg"}
if [[ $argval != '' ]] ; then
printf "ftp_func: $arg='$argval'\n"
fi
}
COMMAND="XYZ"
print_arg "COMMAND"
I've tried re-writing the offending line every way I can think of. I've consulted the local oracles. I've checked the online "BASH Scripting Guide". And I sharpened up the ol' wavy-bladed knife and scrubbed the altar until it gleamed, but then I discovered that our local supply of virgins has been cut down to, like, nothin'. Drat!
Any advice regarding how to get the value of a parameter whose name is passed into a function as a parameter will be received appreciatively.
You could use eval, though using direct indirection as suggested by SiegeX is probably nicer if you can use bash.
#!/bin/sh
foo=bar
print_arg () {
arg=$1
eval argval=\"\$$arg\"
echo "$argval"
}
print_arg foo
In bash (but not in other sh implementations), indirection is done by: ${!arg}
Input
foo=bar
bar=baz
echo $foo
echo ${!foo}
Output
bar
baz
This worked surprisingly well:
#!/bin/sh
foo=bar
print_arg () {
local line name value
set | \
while read line; do
name=${line%=*} value=${line#*=\'}
if [ "$name" = "$1" ]; then
echo ${value%\'}
fi
done
}
print_arg foo
It has all the POSIX clunkiness, in Bash would be much sorter, but then again, you won't need it because you have ${!}. This -in case it proves solid- would have the advantage of using only builtins and no eval. If I were to construct this function using an external command, it would have to be sed. Would obviate the need for the read loop and the substitutions. Mind that asking for indirections in POSIX without eval, has to be paid with clunkiness! So don't beat me!
Even though the answer's already accepted, here's another method for those who need to preserve newlines and special characters like Escape ( \033 ): Storing the variable in base64.
You need: bc, wc, echo, tail, tr, uuencode, uudecode
Example
#!/bin/sh
#====== Definition =======#
varA="a
b
c"
# uuencode the variable
varB="`echo "$varA" | uuencode -m -`"
# Skip the first line of the uuencode output.
varB="`NUM=\`(echo "$varB"|wc -l|tr -d "\n"; echo -1)|bc \`; echo "$varB" | tail -n $NUM)`"
#====== Access =======#
namevar1=varB
namevar2=varA
echo simple eval:
eval "echo \$$namevar2"
echo simple echo:
echo $varB
echo precise echo:
echo "$varB"
echo echo of base64
eval "echo \$$namevar1"
echo echo of base64 - with updated newlines
eval "echo \$$namevar1 | tr ' ' '\n'"
echo echo of un-based, using sh instead of eval (but could be made with eval, too)
export $namevar1
sh -c "(echo 'begin-base64 644 -'; echo \$$namevar1 | tr ' ' '\n' )|uudecode"
Result
simple eval:
a b c
simple echo:
YQpiCmMK ====
precise echo:
YQpiCmMK
====
echo of base64
YQpiCmMK ====
echo of base64 - with updated newlines
YQpiCmMK
====
echo of un-based, using sh instead of eval (but could be made with eval, too)
a
b
c
Alternative
You also could use the set command and parse it's output; with that, you don't need to treat the variable in a special way before it's accessed.
A safer solution with eval:
v=1
valid_var_name='[[:alpha:]_][[:alnum:]_]*$'
print_arg() {
local arg=$1
if ! expr "$arg" : "$valid_var_name" >/dev/null; then
echo "$0: invalid variable name ($arg)" >&2
exit 1
fi
local argval
eval argval=\$$arg
echo "$argval"
}
print_arg v
print_arg 'v; echo test'
Inspired by the following answer.