Как вручную развернуть специальную переменную (например, ~ тильда) в Bash - PullRequest
114 голосов
/ 19 октября 2010

В моем скрипте 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 может существовать или не существовать.

Ответы [ 14 ]

95 голосов
/ 15 декабря 2014

Если переменная var введена пользователем, eval должен не использоваться для расширения тильды с использованием

eval var=$var  # Do not use this!

Причина в том, что пользователь мог случайно (или по назначению) напечатать, например, var="$(rm -rf $HOME/)" с возможными катастрофическими последствиями.

Лучшим (и более безопасным) способом является расширение параметров Bash:

var="${var/#\~/$HOME}"
86 голосов
/ 19 октября 2010

Из-за характера StackOverflow я не могу просто сделать этот ответ неприемлемым, но за прошедшие 5 лет с тех пор, как я его опубликовал, ответы были намного лучше, чем мой заведомо элементарный и довольно плохой ответ (я былмолодой, не убивай меня).

Другие решения в этой теме - более безопасные и лучшие решения.Предпочтительно, я бы пошел с любым из этих двух:


Оригинальный ответ для исторических целей (но, пожалуйста, не используйте это)

Если я не ошибаюсь, "~" не будет расширяться bashтаким образом, потому что он обрабатывается как буквенная строка "~".Вы можете принудительно выполнить расширение через eval следующим образом.

#!/bin/bash

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

В качестве альтернативы, просто используйте ${HOME}, если вы хотите домашний каталог пользователя.

18 голосов
/ 28 марта 2015

Прагеринг себя от предыдущего ответа , чтобы сделать это надежно, без рисков безопасности, связанных с 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%:}"
}

... используется как ...

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
}
8 голосов
/ 28 февраля 2013

Безопасный способ использовать 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}

7 голосов
/ 14 августа 2012

Расширение (без каламбура) ответов Беррири и Аллолео: Общий подход заключается в использовании 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 предназначены для поддержки имен файлов с пробелами.

Возможно, есть более элегантное решение, но это то, что я смог придумать.

7 голосов
/ 19 октября 2010

Как насчет этого:

path=`realpath "$1"`

Или:

path=`readlink -f "$1"`
2 голосов
/ 11 июня 2015

Я думаю, это то, что вы ищете

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
1 голос
/ 25 августа 2016

Вот функция POSIX, эквивалентная 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: добавьте '%s' per @CharlesDuffy в комментарии.

1 голос
/ 13 декабря 2015

Просто используйте eval правильно: с проверкой.

case $1${1%%/*} in
([!~]*|"$1"?*[!-+_.[:alnum:]]*|"") ! :;;
(*/*)  set "${1%%/*}" "${1#*/}"       ;;
(*)    set "$1" 
esac&& eval "printf '%s\n' $1${2+/\"\$2\"}"
1 голос
/ 21 августа 2015

Вот мое решение:

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