Como expandir um til ~ como parte de uma variável?

12

Quando abro um prompt do bash e digite:

$ set -o xtrace
$ x='~/someDirectory'
+ x='~/someDirectory'
$ echo $x
+ echo '~/someDirectory'
~/someDirectory

Eu esperava que a 5ª linha acima tivesse passado + echo /home/myUsername/someDirectory. Existe uma maneira de fazer isso? No meu script Bash original, a variável x está realmente sendo preenchida a partir de dados de um arquivo de entrada, através de um loop como este:

while IFS= read line
do
    params=($line)
    echo ${params[0]}
done <"./someInputFile.txt"

Ainda assim, estou obtendo um resultado semelhante, com o echo '~/someDirectory'invés de echo /home/myUsername/someDirectory.

Andrew
fonte
Em ZSH é isso x='~'; print -l ${x} ${~x}. Desisti depois de vasculhar o bashmanual por um tempo.
thrig
@ Thrig: Isso não é um basismo, esse comportamento é POSIX.
WhiteWinterWolf
Extremamente relacionado (se não for burro
Kusalananda
@ Kusalananda: Não tenho certeza se isso é uma bobagem, pois a razão aqui é um pouco diferente: o OP não colocou $ x entre aspas ao repeti-lo.
WhiteWinterWolf
Você não coloca tildes no arquivo de entrada. Problema resolvido.
Chepner # 21/17

Respostas:

11

O padrão POSIX impõe que a expansão de palavras seja feita na seguinte ordem (a ênfase é minha):

  1. Expansão de til (consulte Expansão de til), expansão de parâmetro (consulte Expansão de parâmetro), substituição de comando (consulte Substituição de comando) e expansão aritmética (consulte Expansão aritmética) devem ser executadas, do início ao fim. Veja o item 5 em Reconhecimento de token.

  2. A divisão de campo (consulte Divisão de campo) deve ser realizada nas partes dos campos gerados pela etapa 1, a menos que o IFS seja nulo.

  3. A expansão do nome do caminho (consulte Expansão do nome do caminho) deve ser executada, a menos que o conjunto -f esteja em vigor.

  4. A remoção de cotação (consulte Remoção de cotação) deve sempre ser realizada por último.

O único ponto que nos interessa aqui é o primeiro: como você pode ver, a expansão til é processada antes da expansão do parâmetro:

  1. O shell tenta expandir o til echo $x, não há nenhum til a ser encontrado e, portanto, prossegue.
  2. O shell tenta uma expansão de parâmetro echo $x, $xé encontrado e expandido e a linha de comando se torna echo ~/someDirectory.
  3. O processamento continua, já que a expansão do til já foi processada, o ~personagem permanece como está.

Ao usar as aspas ao atribuir o $x, você estava solicitando explicitamente para não expandir o til e tratá-lo como um caractere normal. Uma coisa que muitas vezes se perde é que, nos comandos do shell, você não precisa citar toda a cadeia, para que você possa fazer a expansão acontecer corretamente durante a atribuição de variável:

user@host:~$ set -o xtrace
user@host:~$ x=~/'someDirectory'
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$

E você também pode fazer com que a expansão ocorra na echolinha de comando, desde que isso ocorra antes da expansão do parâmetro:

user@host:~$ x='someDirectory'
+ x=someDirectory
user@host:~$ echo ~/$x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$

Se, por algum motivo, você realmente precisar afetar o til da $xvariável sem expansão e poder expandi-lo no echocomando, prossiga duas vezes para forçar $xa ocorrência de duas expansões da variável:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ echo "$( eval echo $x )"
++ eval echo '~/someDirectory'
+++ echo /home/user/someDirectory
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 

No entanto, esteja ciente de que, dependendo do contexto em que você usa essa estrutura, ela pode ter efeitos colaterais indesejados. Como regra geral, prefira evitar usar qualquer coisa que exija evalquando você tiver outra maneira.

Se você deseja resolver especificamente o problema de til, em oposição a qualquer outro tipo de expansão, essa estrutura seria mais segura e portátil:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ case "$x" in "~/"*)
>     x="${HOME}/${x#"~/"}"
> esac
+ case "$x" in
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 

Essa estrutura verifica explicitamente a presença de um líder ~e o substitui pelo diretório inicial do usuário, caso ele seja encontrado.

Após o seu comentário, isso x="${HOME}/${x#"~/"}"pode ser realmente surpreendente para alguém que não é usado na programação de shell, mas está de fato vinculado à mesma regra POSIX citada acima.

Conforme imposto pelo padrão POSIX, a remoção da cotação ocorre por último e a expansão dos parâmetros ocorre muito cedo. Assim, ${#"~"}é avaliado e expandido muito antes da avaliação das aspas externas. Por sua vez, conforme definido nas regras de expansão de parâmetros :

Em cada caso em que um valor de palavra é necessário (com base no estado do parâmetro, conforme descrito abaixo), a palavra deve ser sujeita a expansão de til, expansão de parâmetro, substituição de comando e expansão aritmética.

Portanto, o lado direito do #operador deve ser citado ou escapado adequadamente para evitar a expansão do til.

Portanto, para dizer de maneira diferente, quando o interpretador de shell olha x="${HOME}/${x#"~/"}", ele vê:

  1. ${HOME}e ${x#"~/"}deve ser expandido.
  2. ${HOME}é expandido para o conteúdo da $HOMEvariável
  3. ${x#"~/"}aciona uma expansão aninhada: "~/"é analisado, mas, sendo citado, é tratado como um literal 1 . Você poderia ter usado aspas simples aqui com o mesmo resultado.
  4. ${x#"~/"}a própria expressão agora é expandida, resultando na ~/remoção do prefixo do valor de $x.
  5. O resultado do exposto agora é concatenado: a expansão ${HOME}, o literal /, a expansão ${x#"~/"}.
  6. O resultado final é colocado entre aspas duplas, impedindo funcionalmente a divisão de palavras. Eu digo funcionalmente aqui, porque essas aspas duplas não são tecnicamente necessárias (veja aqui e ali, por exemplo), mas como um estilo pessoal, assim que as atribuições vão além a=$b, geralmente acho mais claro adicionar aspas duplas.

A propósito, se olharmos mais de perto a casesintaxe, você verá a "~/"*construção que se baseia no mesmo conceito que x=~/'someDirectory'eu expliquei acima (aqui novamente, aspas simples e duplas podem ser usadas de forma intercambiável).

Não se preocupe se essas coisas parecerem obscuras à primeira vista (talvez até na segunda ou mais tarde!). Na minha opinião, a expansão de parâmetros é, com subconjuntos, um dos conceitos mais complexos a serem compreendidos ao se programar em linguagem shell.

Sei que algumas pessoas podem discordar vigorosamente, mas se você gostaria de aprender mais sobre a programação do shell, incentivo-o a ler o Advanced Bash Scripting Guide : ele ensina o Bash scripting, portanto, com muitas extensões e sinos. assobios em comparação ao script de shell POSIX, mas achei bem escrito com vários exemplos práticos. Depois que você gerencia isso, é fácil restringir-se aos recursos do POSIX quando necessário; pessoalmente, acho que entrar diretamente no domínio POSIX é uma curva de aprendizado desnecessária para iniciantes (compare meu substituto do POSIX com o Bash do mexdular, como o regex equivalente a ter uma idéia do que quero dizer;)!).


1 : O que me leva a encontrar um bug no Dash que não implementa a expansão de til aqui corretamente (uso verificável x='~/foo'; echo "${x#~/}"). A expansão de parâmetros é um campo complexo, tanto para o usuário quanto para os próprios desenvolvedores de shell!

WhiteWinterWolf
fonte
Como o shell bash está analisando a linha x="${HOME}/${x#"~/"}"? Parece que uma concatenação de 3 cordas: "${HOME}/${x#", ~/, e "}". O shell permite aspas duplas aninhadas quando o par interno de aspas duplas está dentro de um ${ }bloco?
21817 Andrew Andrew
@ Andrew: Eu completei minha resposta com informações adicionais, esperançosamente, abordando seu comentário.
WhiteWinterWolf
Obrigado, esta é uma ótima resposta. Eu aprendi muito lendo isso. Gostaria de poder upvote-lo mais de uma vez :)
Andrew
@WhiteWinterWolf: ainda assim, o shell não vê as aspas aninhadas, seja qual for o resultado.
avp
6

Uma resposta possível:

eval echo "$x"

Como você está lendo a entrada de um arquivo, eu não faria isso.

Você pode procurar e substituir o ~ pelo valor de $ HOME, assim:

x='~/.config'
x="${x//\~/$HOME}"
echo "$x"

Dá-me:

/home/adrian/.config
m0dular
fonte
Observe que a ${parameter/pattern/string}expansão é uma extensão do Bash e pode não estar disponível em outros shells.
WhiteWinterWolf
Verdade. O OP mencionou que ele estava usando o Bash, então acho que é uma resposta apropriada.
M0dular
Eu concordo, desde que alguém se apegue ao Bash, por que não tirar proveito dele (nem todo mundo precisa de portabilidade em todos os lugares), mas vale a pena notar isso para usuários que não são do Bash (várias distribuições agora são enviadas com Dash em vez de Bash, por exemplo ) para que os usuários afetados não sejam surpreendidos.
WhiteWinterWolf #
Tomei a liberdade de mencionar sua postagem na minha digressão sobre as diferenças entre as extensões do Bash e os scripts de shell POSIX, pois acho que sua declaração do tipo regex de linha única do Bash em comparação com a minha caseestrutura POSIX ilustra bem como o script do Bash é mais fácil de usar, especialmente para iniciantes.
WhiteWinterWolf