Як вручну розгорнути спеціальну змінну (наприклад: ~ tilde) в bash


135

У моєму скрипті bash є змінна, значення якої приблизно така:

~/a/b/c

Зауважте, що це нерозгорнута тильда. Коли я роблю ls -lt на цій змінній (називаю це $ VAR), я не отримую такого каталогу. Я хочу дозволити bash інтерпретувати / розширювати цю змінну, не виконуючи її. Іншими словами, я хочу, щоб bash запустив eval, але не запустив оцінювану команду. Це можливо в баш?

Як мені вдалося передати це у свій сценарій без розширення? Я передав аргумент в оточенні його подвійними цитатами.

Спробуйте цю команду, щоб побачити, що я маю на увазі:

ls -lt "~"

Це саме така ситуація, в якій я перебуваю. Я хочу, щоб тильда була розширена. Іншими словами, на що я повинен замінити магію, щоб зробити ці дві команди однаковими:

ls -lt ~/abc/def/ghi

і

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

Зауважте, що ~ / abc / def / ghi може існувати або не існувати.


4
Можливо, ви також знайдете корисні розширення Tilde в котируваннях . Він в основному, але не повністю, уникає використання 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}якщо ви хочете домашній каталог користувача.


3
Чи є у вас виправлення, коли змінна має пробіл у ній?
Гюго

34
Я знайшов ${HOME}найбільш привабливим. Чи є якісь причини не зробити це вашою основною рекомендацією? У будь-якому випадку, дякую!
шавлія

1
+1 - мені потрібно було розширити ~ $ some_other_user, і eval прекрасно працює, коли $ HOME не працюватиме, оскільки мені не потрібен дім поточного користувача.
olivecoder

11
Використання eval- жахлива пропозиція, це дуже погано, що він отримує так багато відгуків. Ви зіткнетеся з усілякими проблемами, коли значення змінної містить мета символи оболонки.
користувач2719058

1
Я не міг закінчити свій коментар на той час, і тоді мені не було дозволено редагувати його пізніше. Тож я вдячний (ще раз дякую @birryree) за це рішення, оскільки це допомогло в моєму конкретному контексті на той час. Дякую Чарльзу, що дав мені знати.
olivecoder

114

Якщо змінну varвводить користувач, evalїї не слід використовувати для розширення тильди за допомогою

eval var=$var  # Do not use this!

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

Кращий (і безпечніший) спосіб - розширення параметра Bash:

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

8
Як ви могли змінити ~ userName / замість просто ~ /?
aspergillusOryzae

3
Яка мета #в "${var/#\~/$HOME}"?
Джахід

3
@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
@Gino, звичайно, є простіший спосіб; питання полягає в тому, чи існує простіший спосіб, який також є безпечним.
Чарльз Даффі

2
@Gino ... Я дійсно вважаю , що можна використовувати , printf %qщоб уникнути все , крім тильди, а потім використовувати evalбез ризику.
Чарльз Даффі

1
@Gino, ... і так реалізовано.
Чарльз Даффі

3
Безпечно, так, але дуже неповно. Мій код не є складним для його задоволення - він складний, оскільки фактичні операції, зроблені розширенням tilde, є складними.
Чарльз Даффі

10

Безпечним способом використання eval є "$(printf "~/%q" "$dangerous_path")". Зауважте, що це специфічний баш.

#!/bin/bash

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

Дивіться це питання для деталей

Також зауважте, що під zsh це було б так само просто echo ${~dangerous_path}


echo ${~root}не дай мені виводу на zsh (mac os x)
Orwellophile

export test="~root/a b"; echo ${~test}
Gyscos

9

Як щодо цього:

path=`realpath "$1"`

Або:

path=`readlink -f "$1"`

виглядає приємно, але realpath не існує на моєму комп'ютері. І вам доведеться записати path = $ (realpath "$ 1")
Гюго

Привіт @Hugo. Ви можете скласти свою власну realpathкоманду в разі C. Для, ви можете створити виконуваний файл з realpath.exeдопомогою Баша і 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);}'. Ура
олібре

@Quuxplusone неправда, принаймні на Linux: realpath ~->/home/myhome
blueFast

iv'e вживали його з пивом на mac
nhed

1
@dangonfast Це не спрацює, якщо тильда встановити в лапки, результат є <workingdir>/~.
Мерфі

7

Розширення (не призначене для каламбуру) на відповіді берірі та халолео: Загальний підхід полягає у використанні 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

Я здивований, що printf %q не уникнути провідних тильдів - це майже спокусливо подати це як помилку, оскільки це ситуація, в якій він не справляється із заявленою метою. Однак у проміжку хороший дзвінок!
Чарльз Даффі

1
Насправді - ця помилка виправлена ​​в певний момент між 3.2.57 та 4.3.18, тому цей код більше не працює.
Чарльз Даффі

1
Добре, я налаштував на код, щоб видалити провідний \, якщо він існує, тому все виправлено і працював :) Я тестував без цитування аргументів, тому він розширювався перед викликом функції.
Орвелофіл

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)
Джино,

1
FYI: Я щойно оновив своє рішення, щоб тепер воно могло правильно обробляти ~ ім'я користувача. І це також має бути досить безпечним. Навіть якщо ви введете в якості аргументу '/ tmp / $ (rm -rf / *)', він повинен витончено поводитися з ним.
Джино

1

Просто evalправильно використовувати : з валідацією.

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

Це, мабуть, безпечно - я не знайшов справи, в якій це не вдалося. Це означає, що якщо ми будемо говорити з використанням eval "правильно", я б стверджував, що відповідь Орвелофіла слідує кращій практиці: я довіряю оболонці printf %qбезпечно уникати речей, ніж я довіряю рукописному коду перевірки, щоб не було помилок .
Чарльз Даффі

@Charles Duffy - це нерозумно. оболонка може не мати% q - і printfє $PATHкомандою 'd.
mikeserv

1
Чи це питання не позначене bash? Якщо так, printfце вбудований і %qгарантовано присутній.
Чарльз Даффі

@Charles Duffy - яка версія?
mikeserv

1
@Charles Duffy - це ... досить рано. але я все ще думаю, що дивно, що ви б довіряли% q аргументу більше, ніж ви б кодували прямо перед вашими очима, я використовував 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.


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

1

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

$ 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_".


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) У баш-скрипті як разовий - збережіть це як 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 до мандату розширення за замовчуванням (немає -eабо -Eнеобхідності) для таких імен.
Чарльз Даффі

0

Найпростіший : замініть "магію" на "еваль-ехо".

$ 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
Всупереч сказаному, цей код небезпечний . Наприклад, тест s='echo; EVIL_COMMAND'. (Не вдасться, оскільки EVIL_COMMANDйого немає на вашому комп’ютері. Але якби ця команда була, rm -r ~наприклад, вона видалила б ваш домашній каталог.)
Konrad Rudolph

0

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

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

Додатковою перевагою є те, що якщо немає тильду, зі змінною нічого не відбувається, а якщо тильда є, але не в першій позиції, вона також ігнорується.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.