Como expandir manualmente uma variável especial (ex: ~ til) no bash

135

Eu tenho uma variável no meu script bash cujo valor é algo como isto:

~/a/b/c

Observe que é um til não expandido. Quando eu faço ls -lt nessa variável (chame-a de $ VAR), não recebo esse diretório. Eu quero deixar o bash interpretar / expandir essa variável sem executá-la. Em outras palavras, quero que o bash execute eval, mas não execute o comando avaliado. Isso é possível no bash?

Como consegui passar isso para o meu script sem expansão? Passei o argumento entre aspas duplas.

Tente este comando para ver o que quero dizer:

ls -lt "~"

Esta é exatamente a situação em que estou. Quero que o til seja expandido. Em outras palavras, com o que devo substituir a magia para tornar esses dois comandos idênticos:

ls -lt ~/abc/def/ghi

e

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

Observe que ~ / abc / def / ghi pode ou não existir.

madiyaan damha
fonte
4
Você também pode achar útil a expansão Tilde entre aspas . Evita principalmente, mas não totalmente, o uso eval.
Jonathan Leffler
2
Como sua variável foi atribuída com um til não expandido? Talvez tudo o que seja necessário seja atribuir essa variável com as aspas externas do til. foo=~/"$filepath"oufoo="$HOME/$filepath"
Chad Skeeters
dir="$(readlink -f "$dir")"
Jack Wasey

Respostas:

98

Devido à natureza do StackOverflow, não posso apenas tornar essa resposta inaceitável, mas nos cinco anos seguintes que publiquei isso houve respostas muito melhores do que a minha resposta admitidamente rudimentar e muito ruim (eu era jovem, não mate mim).

As outras soluções neste segmento são melhores e mais seguras. De preferência, eu iria com um destes dois:


Resposta original para fins históricos (mas não use isso)

Se não me engano, "~"não será expandido por um script bash dessa maneira porque é tratado como uma string literal "~". Você pode forçar a expansão evaldessa maneira.

#!/bin/bash

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

Como alternativa, basta usar ${HOME}se você quiser o diretório inicial do usuário.

wkl
fonte
3
Você tem uma correção para quando a variável tem um espaço nela?
Hugo
34
Eu achei ${HOME}mais atraente. Existe algum motivo para não fazer desta a sua recomendação principal? De qualquer forma, obrigado!
sálvia
1
+1 - Eu estava precisando expandir ~ $ some_other_user e eval funciona bem quando $ HOME não funcionará porque eu não preciso da página inicial do usuário atual.
Olivecoder 10/09
11
Usar evalé uma sugestão horrível, é muito ruim que receba tantos votos. Você terá todos os tipos de problemas quando o valor da variável contiver metacaracteres do shell.
user2719058
1
Não pude terminar meu comentário naquele momento e, então, não me foi permitido editá-lo mais tarde. Por isso, sou grato (obrigado novamente @birryree) por esta solução, pois ajudou no meu contexto específico na época. Obrigado Charles por me informar.
Olivecoder
114

Se a variável varfor inserida pelo usuário, nãoeval deve ser usado para expandir o til usando

eval var=$var  # Do not use this!

O motivo é: o usuário pode acidentalmente (ou por objetivo) digitar, por exemplo, var="$(rm -rf $HOME/)"com possíveis conseqüências desastrosas.

Uma maneira melhor (e mais segura) é usar a expansão do parâmetro Bash:

var="${var/#\~/$HOME}"
Håkon Hægland
fonte
8
Como você pode mudar ~ userName / em vez de apenas ~ /?
usar o seguinte
3
Qual é o propósito de #em "${var/#\~/$HOME}"?
Jahid
3
@Jahid Está explicado no manual . Força o til a corresponder apenas no início de $var.
Håkon Hægland
1
Obrigado. (1) Por que precisamos, \~ou seja, escapar ~? (2) Sua resposta assume que esse ~é o primeiro caractere $var. Como podemos ignorar os principais espaços em branco $var?
Tim
1
@ Tim Obrigado pelo comentário. Sim, você está certo, não precisamos escapar de um til, a menos que seja o primeiro caractere de uma sequência não citada ou esteja seguindo a :em uma sequência não citada. Mais informações nos documentos . Para remover o espaço em branco inicial, consulte Como aparar o espaço em branco de uma variável do Bash?
Håkon Hægland
24

Plagar-me a partir de uma resposta anterior , para fazer isso de maneira robusta, sem os riscos de segurança associados a 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%:}"
}

...usado como...

path=$(expandPath '~/hello')

Como alternativa, uma abordagem mais simples que use com evalcuidado:

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
}
Charles Duffy
fonte
4
Olhando para o seu código, parece que você está usando um canhão para matar um mosquito. Há tem que haver uma maneira muito mais simples ..
Gino
2
@ Gino, certamente existe uma maneira mais simples; a questão é se existe uma maneira mais simples que também seja segura.
Charles Duffy
2
@Gino, ... Eu não suponho que se pode usar printf %qpara escapar tudo, mas o til, e , em seguida, usar evalsem risco.
Charles Duffy
1
@Gino, ... e assim implementado.
Charles Duffy
3
Seguro, sim, mas muito incompleto. Meu código não é complexo por diversão - é complexo porque as operações reais feitas pela expansão til são complexas.
Charles Duffy
10

Uma maneira segura de usar eval é "$(printf "~/%q" "$dangerous_path")". Observe que é específico do bash.

#!/bin/bash

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

Veja esta pergunta para detalhes

Observe também que, no zsh, isso seria tão simples quanto echo ${~dangerous_path}

eddygeek
fonte
echo ${~root}me dê nenhuma saída em zsh (Mac OS X)
Orwellophile
export test="~root/a b"; echo ${~test}
Gyscos
9

Que tal agora:

path=`realpath "$1"`

Ou:

path=`readlink -f "$1"`
Jay
fonte
parece bom, mas o caminho real não existe no meu mac. E você teria que escrever o caminho = $ (realpath "$ 1") #
728 Hugo Hugo
Olá @Hugo. Você pode compilar o seu próprio realpathcomando no exemplo C. Para, você pode gerar um executável realpath.exeusando o bash e gcc a partir desta linha de comando: 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 24/10
@Quuxplusone não é verdade, pelo menos no Linux: realpath ~->/home/myhome
blueFast
iv'e usou-o com fermentação em mac
nhed
1
@dangonfast Isso não funcionará se você definir o til entre aspas, o resultado é <workingdir>/~.
Murphy
7

Expandir (sem trocadilhos) as respostas de birryree e halloleo: A abordagem geral é usar eval, mas vem com algumas advertências importantes, como espaços e redirecionamento de saída ( >) na variável. O seguinte parece funcionar para mim:

mypath="$1"

if [ -e "`eval echo ${mypath//>}`" ]; then
    echo "FOUND $mypath"
else
    echo "$mypath NOT FOUND"
fi

Experimente com cada um dos seguintes argumentos:

'~'
'~/existing_file'
'~/existing file with spaces'
'~/nonexistant_file'
'~/nonexistant file with spaces'
'~/string containing > redirection'
'~/string containing > redirection > again and >> again'

Explicação

  • O ${mypath//>}retira >personagens que poderia espancar um arquivo durante a eval.
  • O eval echo ...que é a expansão de til atual
  • As aspas duplas ao redor do -eargumento são para suporte de nomes de arquivos com espaços.

Talvez exista uma solução mais elegante, mas foi isso que consegui encontrar.

Noach Magedman
fonte
3
Você pode considerar o comportamento de nomes que contenham $(rm -rf .).
Charles Duffy
1
Isso não quebra os caminhos que realmente contêm >caracteres?
Radon Rosborough 9/10/19
2

Eu acredito que é isso que você está procurando

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.$$
}

Exemplo de uso:

$ magic ~nobody/would/look/here
/var/empty/would/look/here

$ magic ~invalid/this/will/not/expand
~invalid/this/will/not/expand
Orwellophile
fonte
Surpreende-me que printf %q não escape dos principais estilos - é quase tentador arquivar isso como um bug, pois é uma situação em que falha com o objetivo declarado. Entretanto, entretanto, uma boa ligação!
Charles Duffy
1
Na verdade - esse bug foi corrigido em algum momento entre 3.2.57 e 4.3.18, portanto esse código não funciona mais.
Charles Duffy
1
Bom ponto, eu ajustei o código para remover o \ principal, se ele existir, portanto tudo foi corrigido e funcionou :) Eu estava testando sem citar os argumentos, portanto estava expandindo antes de chamar a função.
Orwellophile
1

Aqui está a minha solução:

#!/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"
Gino
fonte
Além disso, contar com echomeios que expandTilde -nnão vão se comportar conforme o esperado, e o comportamento com nomes de arquivos contendo barras invertidas não é definido pelo POSIX. Veja pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Charles Duffy
Boa pegada. Eu normalmente uso uma máquina de um usuário, então não pensei em lidar com esse caso. Mas acho que a função poderia ser facilmente aprimorada para lidar com esse outro caso, através do arquivo / etc / passwd para o outro usuário. Vou deixar isso como um exercício para outra pessoa :).
Gino
Já fiz esse exercício (e lidei com o caso OLDPWD e outros) em uma resposta que você considerou muito complexa. :)
Charles Duffy
Na verdade, eu acabei de encontrar uma solução de linha bastante simples que devem lidar com o caso otheruser: path = $ (eval echo $ orgPath)
Gino
1
FYI: Acabei de atualizar minha solução para que agora possa lidar com ~ nome de usuário corretamente. E também deve ser bastante seguro. Mesmo se você colocar um '/ tmp / $ (rm -rf / *)' como argumento, ele deve lidar com isso normalmente.
Gino
1

Basta usar evalcorretamente: com validação.

case $1${1%%/*} in
([!~]*|"$1"?*[!-+_.[:alnum:]]*|"") ! :;;
(*/*)  set "${1%%/*}" "${1#*/}"       ;;
(*)    set "$1" 
esac&& eval "printf '%s\n' $1${2+/\"\$2\"}"
mikeserv
fonte
Provavelmente isso é seguro - não encontrei um caso em que ele falhe. Dito isso, se quisermos usar o eval "corretamente", eu diria que a resposta do Orwellophile segue a melhor prática: confio no shell printf %qpara escapar das coisas com segurança mais do que confio no código de validação escrito à mão para não haver bugs .
Charles Duffy
@ Charles Duffy - isso é bobagem. o shell pode não ter um% q - e printfé um $PATHcomando 'd.
mikeserv
1
Esta pergunta não está marcada bash? Se assim printffor , é um builtin e %qé garantida a presença.
Charles Duffy
@ Charles Duffy - qual versão?
mikeserv
1
@ Charles Duffy - isso é ... muito cedo. mas ainda acho estranho que você confie em um% q mais do que codificaria diante de seus olhos, já usei o bashsuficiente antes para saber que não confio nele. tente:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv 13/12/2015
1

Aqui está a função POSIX equivalente à resposta Bash de Håkon Hægland

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

10-12-2017 editar: adicione '%s'por @CharlesDuffy nos comentários.

go2null
fonte
1
printf '%s\n' "$tilde_less", possivelmente? Caso contrário, ele se comportará mal se o nome do arquivo expandido contiver barras invertidas %sou outra sintaxe significativa printf. Fora isso, porém, essa é uma ótima resposta - correta (quando as extensões bash / ksh não precisam ser cobertas), obviamente segura (sem sujeira eval) e concisa.
Charles Duffy
1

por que não investigar diretamente o diretório home do usuário com o getent?

$ getent passwd mike | cut -d: -f6
/users/mike
Paul M
fonte
0

Apenas para estender a resposta da birryree para caminhos com espaços: Você não pode usar o evalcomando como está, porque ele separa a avaliação por espaços. Uma solução é substituir espaços temporariamente pelo comando 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

Este exemplo depende, é claro, da suposição de que mypathnunca contém a sequência de caracteres "_spc_".

halloleo
fonte
1
Não funciona com guias, ou novas linhas, ou qualquer outra coisa no IFS ... e não oferece segurança em torno metacaracteres como caminhos que contêm$(rm -rf .)
Charles Duffy
0

Você pode achar isso mais fácil de fazer em python.

(1) Na linha de comando do unix:

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

Resulta em:

/Users/someone/fred

(2) Dentro de um script bash como único - salve-o como 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.shResultados em execução :

/Users/someone/fred

(3) Como um utilitário - salve-o como expanduserum local no seu caminho, com permissões de execução:

#!/usr/bin/env python

import sys
import os

print os.path.expanduser(sys.argv[1])

Isso poderia ser usado na linha de comando:

expanduser ~/fred

Ou em um script:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath
Chris Johnson
fonte
Ou que tal passar apenas '~' para Python, retornando "/ home / fred"?
Tom Russell
1
Precisa de citações moar. echo $thepathé buggy; precisa echo "$thepath"corrigir os casos menos incomuns (nomes com guias ou execuções de espaços sendo convertidos em espaços únicos; nomes com globs expandidos) ou printf '%s\n' "$thepath"corrigir também os incomuns (por exemplo, um arquivo nomeado -nou um arquivo com literais de barra invertida em um sistema compatível com XSI). Da mesma forma,thepath=$(expanduser "$1")
Charles Duffy
... para entender o que eu quis dizer sobre literais de barra invertida, consulte pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html - O POSIX permite echose comportar de uma maneira completamente definida pela implementação, se algum argumento contiver barras invertidas; as extensões XSI opcionais ao POSIX exigem comportamentos de expansão padrão (não -eou -Enecessários) para esses nomes.
Charles Duffy
0

Mais simples : substitua 'magic' por 'eval echo'.

$ eval echo "~"
/whatever/the/f/the/home/directory/is

Problema: você terá problemas com outras variáveis ​​porque avaliar é mau. Por exemplo:

$ # home is /Users/Hacker$(s)
$ s="echo SCARY COMMAND"
$ eval echo $(eval echo "~")
/Users/HackerSCARY COMMAND

Observe que o problema da injeção não ocorre na primeira expansão. Então, se você tivesse que substituir simplesmente magiccom eval echo, você deve estar bem. Mas se você fizer echo $(eval echo ~)isso, seria suscetível à injeção.

Da mesma forma, se você fizer isso eval echo ~, em vez de eval echo "~", que contam como duas vezes ampliada e, portanto, a injeção seria possível de imediato.

Karim Alibhai
fonte
1
Ao contrário do que você disse, esse código não é seguro . Por exemplo, teste s='echo; EVIL_COMMAND'. (Ele irá falhar porque EVIL_COMMANDnão existe no seu computador Mas se esse comando tinha sido. rm -r ~Por exemplo, teria removido o seu diretório home.)
Konrad Rudolph
0

Eu fiz isso com a substituição de parâmetro variável depois de ler no caminho usando read -e (entre outros). Assim, o usuário pode preencher com tabulação o caminho e, se o usuário digitar um ~ caminho, será classificado.

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

O benefício adicional é que, se não houver til, nada acontece à variável e, se houver um til, mas não na primeira posição, ele também será ignorado.

(Incluo o -i para leitura, pois o uso em loop, para que o usuário possa corrigir o caminho se houver algum problema.)

JamesIsIn
fonte