É possível executar a substituição de comandos do shell sem usar um subshell?

11

Eu tenho um cenário que exige substituição de comando sem usar um subshell. Eu tenho uma construção como esta:

pushd $(mktemp -d)

Agora eu quero sair e remover o diretório temporário de uma só vez:

rmdir $(popd)

No entanto, isso não funciona porque popdnão retorna o diretório pop-up (retorna o novo diretório, agora atual) e também porque é executado em uma subshell.

Algo como

dirs -l -1 ; popd &> /dev/null

retornará o diretório popped, mas ele não pode ser usado assim:

rmdir $(dirs -l -1 ; popd &> /dev/null)

porque popdisso afetará apenas o subshell. O que é necessário é a capacidade de fazer isso:

rmdir { dirs -l -1 ; popd &> /dev/null; }

mas isso é sintaxe inválida. É possível alcançar esse efeito?

(nota: eu sei que posso salvar o diretório temporário em uma variável; estava tentando evitar a necessidade de fazer isso e aprender algo novo no processo!)

starfry
fonte
1
Eu salvaria o diretório em uma variável; dessa maneira, um trapmanipulador pode limpar o diretório, caso o processo seja cutucado por um sinal.
thrig
A resposta é não .
H0tw1r3 3/08
Parece que fish, o equivalente à substituição de comando (), altera a pasta do shell externo. Geralmente é irritante, mas em casos como esse é útil, tenho certeza.
trysis

Respostas:

10

A escolha do título da sua pergunta é um pouco confusa.

pushd/ popd, um cshrecurso copiado por bashe zsh, é uma maneira de gerenciar uma pilha de diretórios lembrados.

pushd /some/dir

empurra o actual directório de trabalho para uma pilha, e , em seguida, modifica a pasta de trabalho actual (e, em seguida, imprime /some/dirseguido pelo teor de que a pilha (espaço-separadas).

popd

imprime o conteúdo da pilha (novamente, com espaço separado) e depois muda para o elemento superior da pilha e o remove da pilha.

(também tomar cuidado para que alguns diretórios será representado lá com sua ~/xou ~user/xnotação).

Portanto, se a pilha atualmente possui /ae /b, o diretório atual é /heree você está executando:

 pushd /tmp/whatever
 popd

pushdimprimirá /tmp/whatever /here /a /be popdproduzirá /here /a /b, não /tmp/whatever. Isso é independente do uso de substituição de comando ou não. popdnão pode ser usado para obter o caminho do diretório anterior e, em geral, sua saída não pode ser pós-processada (consulte a $dirstackou a $DIRSTACKmatriz de alguns shells para acessar os elementos dessa pilha de diretórios)

Talvez você queira:

pushd "$(mktemp -d)" &&
popd &&
rmdir "$OLDPWD"

Ou

cd "$(mktemp -d)" &&
cd - &&
rmdir "$OLDPWD"

Porém, eu usaria:

tmpdir=$(mktemp -d) || exit
(
  cd "$tmpdir" || exit # in a subshell 
  # do what you have to do in that tmpdir
)
rmdir "$tmpdir"

De qualquer forma, pushd "$(mktemp -d)"não é executado pushdem um subshell. Caso isso acontecesse, não poderia alterar o diretório de trabalho. Isso é mktempexecutado em um subshell. Por ser um comando separado, ele deve ser executado em um processo separado. Ele grava sua saída em um tubo, e o processo de shell a lê na outra extremidade do tubo.

O ksh93 pode evitar o processo separado quando o comando é incorporado, mas mesmo assim, ainda é um subshell (um ambiente de trabalho diferente) que desta vez é emulado, em vez de depender do ambiente separado normalmente fornecido pelo bifurcação. Por exemplo, em ksh93, a=0; echo "$(a=1; echo test)"; echo "$a"nenhum garfo está envolvido, mas ainda echo "$a"gera saídas 0.

Aqui, se você deseja armazenar a saída de mktempuma variável, ao mesmo tempo em que a passa para pushd, com zsh, você pode:

pushd ${tmpdir::="$(mktemp -d)"}

Com outras conchas do tipo Bourne:

unset tmpdir
pushd "${tmpdir=$(mktemp -d)}"

Ou, para usar a saída $(mktemp -d)várias vezes sem armazená-la explicitamente em uma variável, você pode usar zshfunções anônimas:

(){pushd ${1?} && cd - && rmdir $1} "$(mktemp -d)"
Stéphane Chazelas
fonte
Entendo pushde popdtrabalho como você descreve e que eles são independentes da substituição de comandos - não há confusão aí! No entanto, você respondeu por seu próprio problema, revelando $OLDPWD. Eu posso fazer popd; rmdir $OLDPWD. Essa é a resposta para o meu problema - tudo o resto apenas confirma o que eu pensava. A substituição de comandos parece ser uma maneira de resolvê-lo, mas não é por causa do subshell e você não pode fazer a substituição de comandos sem um subshell. Então, obrigado por revelar o OLDPWD - é exatamente isso que eu preciso!
starfry
@starfry, rmdir $(popd)faz popdcom que seja executado em um sub-shell, o que significa que não mudará o diretório atual, mas mesmo que não tenha sido executado em um subshell, a saída de popdnão será o diretório temporário, será uma lista separada por espaço de diretórios que não incluem esse diretório temporário. É onde estou dizendo que você está confuso.
Stéphane Chazelas
mas pensei que tinha dito isso na minha pergunta: "popd não retorna o diretório popped (ele retorna o novo diretório, agora atual) e também porque é executado em uma subshell". É certo que ele retorna uma lista, mas o primeiro da lista é o que eu estava me referindo.
starfry
@starfry, OK desculpe. Digamos que fui eu que confundi com o título da pergunta e não li o resto com atenção suficiente.
Stéphane Chazelas
bem, sua resposta ainda foi boa e me apontou na direção certa. Eu nunca soube dessa variável de shell OLDPWD. Devo lembrar de um dia ler man bashde ponta a ponta!
starfry
2

Você pode desvincular o diretório primeiro, antes de deixá-lo:

rmdir "$(pwd -P)" && popd

ou

rmdir "$(pwd -P)" && cd ..   # yes, .. is still usable

Mas note que pushde popdsão realmente ferramentas para shells interativos, não para scripts (que é por isso que eles são tão falador; comandos reais de script são silenciosos quando conseguem).

Toby Speight
fonte
0

No bash, dirsforneça uma lista de diretórios lembrados pelo método pushd / popd.

Além disso, dirs -1imprime o último diretório incluído na lista.

Portanto, para remover o diretório criado anteriormente pela execução pushd $(mktmp -d), use:

rmdir $(dirs -1)

E então, popdo diretório já removido da lista:

popd > /dev/null

Tudo em uma linha:

rmdir $(dirs -1); popd > /dev/null

E adicionando a opção (-l) para evitar o ~/xou ~user/xnotação:

rmdir $(dirs -l -1); popd > /dev/null

O que é notavelmente semelhante à linha que você solicitou.
Exceto que eu não usaria &>, pois ocultaria qualquer relatório de erro por popd.

Nota: O diretório permanecerá depois do rmdirque está pwdnaquele ponto. E será realmente desvinculado após o popdcomando (nenhum link restante usado).


Existe uma opção para usar, para shells que suportam a variável "OLDPWD" (a maioria dos shells semelhantes a bourne: ksh, bash, zsh have $OLDPWD). É interessante notar que o ksh não implementa dirs, pod, pushd por padrão (lksh, dash e outros também não possuem popd disponível, portanto, não são utilizáveis ​​aqui):

popd && rmdir "$OLDPWD"

Ou, mais idiomático (mesma lista de conchas como acima):

popd && rmdir ~-
Comunidade
fonte
O problema com isso, e o que eu estava tentando resolver, é que você não pode encontrar rmdiro diretório atual, onde você estaria ao fazer isso. Você precisa efetuar o popdprocedimento antes de fazer o procedimento, rmdirmas precisa saber o que remover e popdnão diz isso. Eu, como você, pensei que dirs -l -1era a resposta, mas descobri que a resposta é realmente usar $OLDPWD.
starfry
@starfry Eu acredito que a resposta mais curta é a utilização: popd && rmdir ~-.
@ starfry Você pode remover o diretório atual, sem problemas, desde que esteja vazio (sem forçar a exclusão). Você pode até ligar rmdir "$PWD"tudo bem. Além disso, o diretório atual deve ser o criado por mktmp -d(se pushd $(mktemp -d)foi executado anteriormente) e para o qual pushdmove o pwd. Então, sim, depois pushd e antes do popd, o diretório criado é armazenado $(dirs -1), não vejo nenhum problema.