Как вручную расширить специальную переменную (например: ~ тильда) в bash

135

У меня есть переменная в моем сценарии bash, значение которой примерно такое:

~/a/b/c

Обратите внимание, что это нерасширенная тильда. Когда я выполняю ls -lt для этой переменной (назовем ее $ VAR), я не получаю такой каталог. Я хочу, чтобы bash интерпретировал / расширял эту переменную, не выполняя ее. Другими словами, я хочу, чтобы bash запускал eval, но не запускал оцениваемую команду. Возможно ли это в баше?

Как мне удалось передать это в мой скрипт без расширения? Я передал аргумент, заключив его в двойные кавычки.

Попробуйте эту команду, чтобы понять, что я имею в виду:

ls -lt "~"

Это как раз та ситуация, в которой я нахожусь. Я хочу, чтобы тильда была расширена. Другими словами, на что мне заменить magic, чтобы эти две команды стали идентичными:

ls -lt ~/abc/def/ghi

и

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

Обратите внимание, что ~ / abc / def / ghi может существовать, а может и не существовать.

Мадияан Дамха
источник
4
Вы также можете найти расширение Тильды в кавычках . В основном, но не полностью, он избегает использования eval.
Джонатан Леффлер
2
Как вашей переменной была присвоена нерасширенная тильда? Возможно, все, что требуется, - это присвоить этой переменной тильду вне кавычек. foo=~/"$filepath"илиfoo="$HOME/$filepath"
Чад Скитерс
dir="$(readlink -f "$dir")"
Джек Васи,

Ответы:

98

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

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


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

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

#!/bin/bash

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

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

WKL
источник
3
Есть ли у вас исправление, когда в переменной есть пробел?
Hugo
34
Я нашел ${HOME}наиболее привлекательным. Есть ли причина не делать это своей основной рекомендацией? В любом случае спасибо!
sage
1
+1 - Мне нужно было расширить ~ $ some_other_user, и eval отлично работает, когда $ HOME не работает, потому что мне не нужен текущий пользователь домой.
Olivecoder
12
Использование eval- ужасное предложение, очень плохо, что оно набирает столько голосов. Вы столкнетесь со всевозможными проблемами, если значение переменной будет содержать метасимволы оболочки.
user2719058
1
В то время я не мог закончить свой комментарий, а потом мне не разрешили редактировать его. Поэтому я благодарен (еще раз спасибо @birryree) за это решение, поскольку оно помогло в моем конкретном контексте в то время. Спасибо, Чарльз, что предупредил меня.
Olivecoder
114

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

eval var=$var  # Do not use this!

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

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

var="${var/#\~/$HOME}"
Håkon Hægland
источник
8
Как вы могли изменить ~ userName / вместо ~ /?
aspergillusOryzae
3
Какова цель #в "${var/#\~/$HOME}"?
Джахид
4
@Jahid Это объясняется в руководстве . Это заставляет тильду соответствовать только в начале $var.
Håkon Hægland
1
Спасибо. (1) Зачем нам нужно \~убегать ~? (2) Ваш ответ предполагает, что ~это первый символ в $var. Как мы можем игнорировать начальные пробелы в $var?
Тим
1
@Tim Спасибо за комментарий. Да, вы правы, нам не нужно экранировать тильду, если она не является первым символом строки без кавычек или не следует за :символом в строке без кавычек. Больше информации в документации . Чтобы удалить начальные пробелы, см. Раздел Как обрезать пробелы в переменной Bash?
Håkon Hægland
24

Плагируя себя из предыдущего ответа , чтобы сделать это надежно без рисков безопасности, связанных с 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
}
Чарльз Даффи
источник
4
Глядя на ваш код, похоже, что вы используете пушку, чтобы убить комара. Там это есть , чтобы быть гораздо более простой способ ..
Gino
2
@ Джино, конечно, есть способ попроще; вопрос в том, есть ли более простой и безопасный способ.
Чарльз Даффи,
2
@Gino ... Я действительно полагаю , что можно использовать , printf %qчтобы избежать все , кроме тильды, а затем использовать evalбез риска.
Чарльз Даффи
1
@Gino, ... и так реализовано.
Чарльз Даффи
4
Безопасно, да, но очень неполно. Мой код не сложен для развлечения - он сложен, потому что фактические операции, выполняемые с помощью тильды, сложны.
Чарльз Даффи,
10

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

eddygeek
источник
echo ${~root}не выводить на zsh (mac os x)
Orwellophile
export test="~root/a b"; echo ${~test}
Gyscos
9

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

path=`realpath "$1"`

Или:

path=`readlink -f "$1"`
сойка
источник
выглядит неплохо, но на моем Mac нет realpath. И вам нужно будет написать path = $ (realpath "$ 1")
Хьюго
Привет, @Hugo. Вы можете скомпилировать свою собственную realpathкоманду на C. Например, вы можете сгенерировать исполняемый файл realpath.exeс помощью bash и gcc из этой командной строки:gcc -o realpath.exe -x c - <<< $'#include <stdlib.h> \n int main(int c,char**v){char p[9999]; realpath(v[1],p); puts(p);}' . Cheers
olibre
@Quuxplusone не соответствует действительности, по крайней мере, в Linux: realpath ~ ->/home/myhome
blueFast
iv'e использовал его с пивом на Mac
nhed
1
@dangonfast Это не сработает, если вы установите тильду в кавычки, результат будет <workingdir>/~.
Мерфи
7

Расширение (без каламбура) ответов birryree и 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//>}Полоски из >символов , которые могут затирать файл во время eval.
  • Вот eval echo ...что делает фактическое расширение тильды
  • -eАргумент заключен в двойные кавычки для поддержки имен файлов с пробелами.

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

Ноах Магедман
источник
3
Вы можете рассмотреть поведение с именами, содержащими $(rm -rf .).
Чарльз Даффи
1
Но разве это не нарушает пути, которые на самом деле содержат >символы?
Радон Росборо
2

Я считаю, что это то, что ты ищешь

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
Orwellophile
источник
Я удивлен, что printf %q не избегает ведущих тильд - почти соблазнительно зарегистрировать это как ошибку, поскольку это ситуация, в которой она не выполняет заявленную цель. Тем не менее, пока что хороший звонок!
Чарльз Даффи
1
На самом деле - эта ошибка исправлена ​​в какой-то момент между 3.2.57 и 4.3.18, поэтому этот код больше не работает.
Чарльз Даффи
1
Хороший момент, я скорректировал код, чтобы удалить ведущий \, если он существует, поэтому все исправлено и работало :) Я тестировал без цитирования аргументов, поэтому он расширялся перед вызовом функции.
Orwellophile
1

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

#!/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"
Джино
источник
Кроме того, полагаться на echoсредства, которые expandTilde -nне будут вести себя так, как ожидалось, и поведение с именами файлов, содержащими обратную косую черту, не определено POSIX. См. Pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Чарльз Даффи,
Хороший улов. Обычно я использую однопользовательскую машину, поэтому мне не приходило в голову заниматься этим случаем. Но я думаю, что эту функцию можно легко улучшить, чтобы справиться с этим другим случаем, перейдя через файл / etc / passwd для другого пользователя. Я оставлю это как упражнение для кого-нибудь другого :).
Джино
Я уже проделал это упражнение (и рассмотрел случай OLDPWD и другие) и получил ответ, который вы сочли слишком сложным. :)
Чарльз Даффи
на самом деле, я только что нашел довольно простое однострочное решение, которое должно обрабатывать случай другого пользователя: path = $ (eval echo $ orgPath)
Gino
1
К вашему сведению: я только что обновил свое решение, чтобы теперь оно могло правильно обрабатывать ~ имя пользователя. И это также должно быть довольно безопасно. Даже если вы укажете в качестве аргумента '/ tmp / $ (rm -rf / *)', он должен обработать его изящно.
Джино
1

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

case $1${1%%/*} in
([!~]*|"$1"?*[!-+_.[:alnum:]]*|"") ! :;;
(*/*)  set "${1%%/*}" "${1#*/}"       ;;
(*)    set "$1" 
esac&& eval "printf '%s\n' $1${2+/\"\$2\"}"
mikeserv
источник
Это, вероятно, безопасно - я не нашел случая, когда это не подходит. Тем не менее, если мы собираемся говорить об использовании eval "правильно", я бы сказал, что ответ Орвеллофила следует лучшей практике: я верю, что оболочка printf %qбезопасно избегает вещей больше, чем я верю, что рукописный код проверки не содержит ошибок ,
Чарльз Даффи
@ Чарльз Даффи - это глупо. в оболочке может не быть% q - и printfэто $PATHкоманда 'd.
mikeserv
1
Разве этот вопрос не помечен bash? Если да, то printfэто встроенная функция, %qкоторая гарантированно присутствует.
Чарльз Даффи,
@ Чарльз Даффи - какая версия?
mikeserv
1
@ Чарльз Даффи - это ... довольно рано. но я все еще думаю, что это странно, что вы доверяете% q arg больше, чем кодируете прямо у вас на глазах, когда я уже использовал bashдостаточно, чтобы знать, что ему нельзя доверять. попробуйте:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv
1

Вот функция POSIX эквивалент Bash Хакон Hægland в ответ

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

2017-12-10 редактирование: добавить '%s'за @CharlesDuffy в комментарии.

go2null
источник
1
printf '%s\n' "$tilde_less"возможно? В противном случае он будет вести себя неправильно, если расширяемое имя файла будет содержать обратную косую черту %s, или другой синтаксис, имеющий значение для printf. В остальном, однако, это отличный ответ - правильный (когда расширения bash / ksh не нужно покрывать), очевидно безопасный (без гадания eval) и краткий.
Чарльз Даффи
1

почему бы не углубиться в получение домашнего каталога пользователя с помощью getent?

$ getent passwd mike | cut -d: -f6
/users/mike
Пол М
источник
0

Просто чтобы расширить ответ birryree для путей с пробелами: вы не можете использовать 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_".

halloleo
источник
1
Не работает с вкладками, новой строкой или чем-либо еще в IFS ... и не обеспечивает безопасность для метасимволов, таких как пути, содержащие$(rm -rf .)
Чарльз Даффи
0

Возможно, вам будет проще сделать это в 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результатов в:

/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
Крис Джонсон
источник
Или как насчет того, чтобы передать Python только '~' и вернуть "/ home / fred"?
Том Рассел
1
Требуются цитаты из Моара. echo $thepathглючит; необходимо echo "$thepath"исправить менее редкие случаи (имена с табуляцией или пробелами преобразуются в одиночные пробелы; имена с расширенными глобусами) или printf '%s\n' "$thepath"также исправить необычные (например, файл с именем -nили файл с литералы обратной косой черты в XSI-совместимой системе). Точно так жеthepath=$(expanduser "$1")
Чарльз Даффи
... чтобы понять, что я имел в виду относительно литералов с обратной косой чертой, см. pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html - POSIX позволяет echoвести себя полностью определяемым реализацией образом, если какой-либо аргумент содержит обратную косую черту; необязательные расширения XSI для POSIX предписывают поведение по умолчанию (не требуется -eили не -Eтребуется) расширения для таких имен.
Чарльз Даффи
0

Самый простой : замените «магия» на «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 "~", это будет считаться двойным расширением, и поэтому инъекция станет возможной сразу.

Карим Алибхай
источник
1
Вопреки тому, что вы сказали, этот код небезопасен . Например, test s='echo; EVIL_COMMAND'. (Это не удастся, потому EVIL_COMMANDчто не существует на вашем компьютере. Но если бы эта команда была, rm -r ~например, она бы удалила ваш домашний каталог.)
Конрад Рудольф
0

Я сделал это с помощью подстановки переменных параметров после прочтения пути с помощью read -e (среди прочего). Таким образом, пользователь может заполнить путь табуляцией, и если пользователь введет путь ~, он будет отсортирован.

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

Дополнительным преимуществом является то, что при отсутствии тильды с переменной ничего не происходит, а если тильда есть, но не в первой позиции, она также игнорируется.

(Я включаю -i для чтения, так как использую его в цикле, чтобы пользователь мог исправить путь в случае возникновения проблемы.)

JamesIsIn
источник