bashで特殊変数(例:〜チルダ)を手動で展開する方法


135

私のbashスクリプトには、次のような値の変数があります。

~/a/b/c

チルドが拡張されていないことに注意してください。この変数に対してls -ltを実行すると($ VARと呼びます)、そのようなディレクトリは取得されません。この変数を実行せずにbashに解釈/展開させたい。つまり、bashでevalを実行し、評価されたコマンドは実行しないようにしたいと考えています。これはbashで可能ですか?

これを拡張せずにスクリプトに渡すにはどうすればよいですか?引数を二重引用符で囲んで渡しました。

このコマンドを試して、私の意味を確認してください。

ls -lt "~"

これがまさに私が置かれている状況です。チルダを拡張して欲しいのです。つまり、これらの2つのコマンドを同一にするために、何を魔法で置き換える必要がありますか。

ls -lt ~/abc/def/ghi

そして

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

〜/ abc / def / ghiが存在する場合と存在しない場合があります。


4
引用符で囲まれたティルダ展開も役立つかもしれません。完全ではありませんが、ほとんどの場合、の使用を回避しevalます。
Jonathan Leffler、2014

2
どのようにして変数が展開されていないチルダで割り当てられましたか?たぶん必要なのは、引用符の外側のチルダでその変数を割り当てることです。 foo=~/"$filepath"またはfoo="$HOME/$filepath"
Chad Skeeters 2017年

dir="$(readlink -f "$dir")"
Jack Wasey

回答:


98

StackOverflowの性質上、この回答を受け入れられないようにすることはできませんが、投稿してから5年間は、明らかに初歩的でかなり悪い回答よりもはるかに良い回答がありました(私は若かったので、殺さないでください)私)。

このスレッドの他のソリューションは、より安全で優れたソリューションです。できれば、次の2つのいずれかを使用します。


歴史的な目的のための元の回答(ただし、これは使用しないでください)

私が間違っていない場合"~"は、リテラル文字列として扱われるため、その方法でbashスクリプトによって展開されません"~"evalこのようにして拡張を強制できます。

#!/bin/bash

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

または、${HOME}ユーザーのホームディレクトリが必要な場合にのみ使用します。


3
変数にスペースがある場合の修正はありますか?
Hugo

34
${HOME}一番魅力的だと思いました。これを主な推奨事項にしない理由はありますか?とにかくありがとう!
セージ2013

1
+1-私は〜$ some_other_userを展開する必要があり、現在のユーザーの家が必要ないために$ HOMEが機能しない場合、evalは正常に機能します。
オリーブコーダー2013

11
使用evalは恐ろしい提案です。非常に多くの賛成票を獲得することは本当に悪いことです。変数の値にシェルメタ文字が含まれていると、あらゆる種類の問題が発生します。
user2719058 14

1
その時点でコメントを完成できなかったため、後で編集することはできませんでした。そのため、このソリューションが当時の私の特定の状況で役立ったので、このソリューションに感謝します(@birryreeに感謝します)。Charlesに知らせてくれてありがとう。
olivecoder、2015

114

変数varがユーザーによって入力された場合、チルダを使用して展開するために使用しないevalください

eval var=$var  # Do not use this!

その理由は、ユーザーが誤って(または目的によって)タイプvar="$(rm -rf $HOME/)"する可能性があり、たとえば、悲惨な結果をもたらす可能性があるためです。

より良い(そしてより安全な)方法は、Bashパラメーター展開を使用することです。

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

8
〜/ではなく〜userName /をどのように変更できますか?
aspergillusOryzae 14

3
目的は何である#では"${var/#\~/$HOME}"
Jahid

3
@Jahid マニュアルで説明されています。チルダがの最初にのみ一致するように強制し$varます。
ホーコンHægland

1
ありがとう。(1)なぜ\~脱出が必要なの~ですか?(2)返信では、~がの最初の文字であると想定してい$varます。の先頭の空白を無視するにはどうすればよい$varですか?
Tim、

1
@ティムコメントありがとうございます。はい、そうです。引用符で囲まれていない文字列の最初の文字であるか、引用符で囲まれていない文字列の後に続く場合を除き、チルダをエスケープする必要はありません:。詳細については、ドキュメントをご覧ください。先頭の空白
ホーコン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
コードを見ると、大砲を使って蚊を殺しているようです。あります得たはるかに簡単な方法であることが...
ジーノ

2
@ジーノ、確かにもっと簡単な方法があります。問題は、安全でもあるより簡単な方法があるかどうかです。
Charles Duffy

2
@Gino、...私はない 1が使用できると仮定しprintf %q、すべてが、チルダをエスケープするために、そしてその後、使用evalリスクなし。
Charles Duffy

1
@Gino、...実装されました。
Charles Duffy

3
はい、安全ですが、非常に不完全です。私のコードは、おもしろいほど複雑ではありません。チルド展開によって行われる実際の操作は複雑なので、コードは複雑です。
Charles Duffy

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}


echo ${~root}zsh(mac os x)で何も出力しない
Orwellophile

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

9

これはどう:

path=`realpath "$1"`

または:

path=`readlink -f "$1"`

見た目はいいですが、私のMacにはリアルパスがありません。そして、あなたはpath = $(realpath "$ 1")を書く必要があります
Hugo

@Hugoさん、こんにちは。realpathCで独自のコマンドをコンパイルできます。たとえば、次のコマンドラインからbashgccrealpath.exeを使用して実行可能ファイルを生成できます。乾杯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);}'
olibre

@Quuxplusoneは真実ではありません、少なくともlinuxでは:realpath ~->/home/myhome
blueFast

iv'eがMacでbrewを使用
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 .)
Charles Duffy、

1
>しかし、これは実際に文字を含むパスで壊れませんか?
Radon Rosborough、2016年

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 しないことに驚いてます。これは、指定された目的で失敗する状況であるため、これをバグとして報告するのはほとんど魅力的です。しかし、暫定的には良い電話です!
Charles Duffy、

1
実際、このバグは3.2.57から4.3.18の間に修正されているため、このコードは機能しません。
Charles Duffy

1
良い点は、先頭の\が存在する場合はそれを削除するようにコードを調整したため、すべて修正して機能しました:)引数を引用せずにテストしていたため、関数を呼び出す前に展開していました。
Orwellophile 2015

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を
Charles Duffy

良いキャッチ。私は通常、1ユーザーのマシンを使用しているので、そのケースを処理することは考えていませんでした。ただし、otheruserの/ etc / passwdファイルをgrepすることで、この別のケースを処理するように機能を簡単に拡張できると思います。私はそれを他の誰かのための練習として残します:)。
ジノ

複雑すぎると思われる回答の中で、私はすでにその演習を行った(そしてOLDPWDのケースなどを処理した)。:)
Charles Duffy

実際、私は他のユーザーのケースを処理するかなりシンプルな1行のソリューションを見つけました:path = $(eval echo $ orgPath)
Gino

1
参考までに、〜usernameを正しく処理できるようにソリューションを更新しました。そして、それもかなり安全でなければなりません。'/ 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を「正しく」使用する場合は、Orwellophileの答えはより良い慣行に従うと私は主張します。手書きのprintf %q検証コードにバグがないことを信頼するよりも、シェルが物事を安全にエスケープすることを信頼しています。 。
Charles Duffy、2015

@Charles Duffy-それはばかげています。シェルには%qがない可能性があり、これprintf$PATH'dコマンドです。
mikeserv

1
この質問にはタグが付けられていませんbashか?その場合、printfは組み込みであり、%q存在することが保証されています。
Charles Duffy、

@Charles Duffy-どのバージョン?
mikeserv 2015

1
@Charles Duffy-それは...かなり早いです。しかし、私はまだあなたがより多くのあなたは、右のあなたの目の前にコード使用アイブなるより%のq引数を信頼したいというその奇妙なと思うbash知っている前に十分にないことを信頼します。試してください:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv

1

HåkonHæglandのBash 回答に相当するPOSIX関数は次のとおりです

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)、簡潔です。
Charles Duffy

1

getentを使用してユーザーのホームディレクトリを取得する方法を詳しく調べてみませんか?

$ getent passwd mike | cut -d: -f6
/users/mike

0

スペースのあるパスに対するbirryreeの回答を拡張するだけです:eval評価をスペースで区切るため、コマンドをそのまま使用することはできません。1つの解決策は、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、charシーケンスが含まれていないという前提に基づいています"_spc_"


1
タブや改行など、IFSのその他の機能には対応していません...パスを含むメタ文字の周囲にセキュリティを提供していません$(rm -rf .)
Charles Duffy

0

Pythonでこれを行う方が簡単な場合があります。

(1)UNIXコマンドラインから:

python -c 'import os; import sys; print os.path.expanduser(sys.argv[1])' ~/fred

結果:

/Users/someone/fred

(2)bashスクリプト内で1回限り-これを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")
Charles Duffy、

...バックスラッシュリテラルの意味を理解するには、pubs.opengroup.org / onlinepubs / 009604599 / utilities / echo.htmlを参照してください。POSIXではecho、引数にバックスラッシュが含まれている場合、完全に実装定義の方法で動作できます。POSIXへのオプションのXSI拡張機能は、そのような名前のデフォルトの(動作しない、-eまたは-E必要ない)拡張動作を要求します。
Charles Duffy

0

最も簡単な方法:「magic」を「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

インジェクションの問題は最初の拡張では発生しないことに注意してください。あなたは、単に交換した場合だmagiceval echo、あなたは大丈夫でなければなりません。しかし、それを行うとecho $(eval echo ~)、注射の影響を受けやすくなります。

同様に、のeval echo ~代わりに行うとeval echo "~"、2倍に展開されたと見なされるため、すぐに注入が可能になります。


1
あなたが言ったことに反して、このコードは安全ではありません。たとえば、test 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を読み取り用に含めています。)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.