Possível em bash? Transformando expansão de comando em argumentos

0

São as únicas opções para expansão do comando bash:

  • sem aspas $(..)cuja saída é sempre analisada com divisão de strings mudas, ou
  • citado "$(..)"cuja saída é sempre passada junto como uma unidade?

Eu estou tentando replicar no bash uma função de shell de peixe que criei para uso no Mac OS. Minha função de peixe selection https://superuser.com/a/1165855 pega a seleção na janela frontal e gera os caminhos para uso na substituição de comandos ls -l (selection). Eu estava esperando para conseguir a mesma coisa em bash talvez como ls -l $(selection).

Eu pensei que era uma questão de citar e assim tentei passar caminhos delimitados por alimentação de linha para o bash printf "%q ". No entanto, descobri que não importa em qual citação coloquei a saída de substituição de comando, ela estava sendo dividida em espaço em branco.

Exemplo simplificado:

$ touch file\ a file\ b
$ ls $( echo "file\ a file\ b" ) # want expansion equiv to: ls 'file a' 'file b'
ls: a: No such file or directory
ls: b: No such file or directory
ls: file\: No such file or directory
ls: file\: No such file or directory

Não seria o fim do mundo se eu tivesse que usar a substituição de comandos citada como, ls -l "$(selection)"mas fazendo isso, a saída do comando nunca é dividida, deixa de observar a minha cuidadosa citação. A sintaxe antiga do backtick é diferente?

Engraçado, bash, você tem muitos recursos. Não tem ninguém para permitir cmda $(cmdb-that-generates-parameters-for-cmda)? Ou um usuário bash simplesmente evita espaços ou símbolos em nomes de arquivos (como um animal) para tornar tudo mais fácil? Obrigado por qualquer resposta.

Pierre Houston
fonte

Respostas:

2

Existem várias maneiras de fazer o que você quer. Por padrão, bashusar espaços em branco como separador padrão, mas você pode facilmente substituir isso usando IFS(Separador de campo interno) ou usar técnicas diferentes, como enumeração. Abaixo estão alguns exemplos que primeiro vêm à mente.

Variante # 1

Usando uma variável interna especial IFS(Separador de Campo Interno)

#!/bin/bash

touch 'file a' 'file b'

# Set IFS to new line as a separator
IFS='
'
ls -la $( echo 'file a'; echo 'file b' )

Variante # 2

Usando o forloop

#!/bin/bash

touch 'file a' 'file b'
for variable in 'file a' 'file b';do
  ls -la "${variable}"
done

Variante # 3

Usando o forloop de valores predefinidos

#!/bin/bash

MultiArgs='
arg 0 1
arg 0  2
arg 0   3
arg 0    N
'

# Using only new line as a separator
IFS='
'

for variable in ${MultiArgs};do
  touch "${variable}"
  ls -la "${variable}"
done

Variante # 4

Usando ~(tilda) como separador

#!/bin/bash

MultiArgs='arg 0 1~arg 0 2~arg0 3~ar g 0 N' # Arguments with spaces

IFS='~'
for file in ${MultiArgs};do
 touch "${file}"
 ls -la "${file}";
done

A sintaxe antiga do backtick é diferente?

Não, é o mesmo, mas os backticks têm alguma limitação que $()não tem.

Ou um usuário bash simplesmente evita espaços ou símbolos em nomes de arquivos (como um animal) para tornar tudo mais fácil?

Não, está tudo bem em usar espaços em nomes de arquivos na medida em que se usaria a citação correta.

Mais ou menos $(cmdb-that-generates-parameters-for-cmda)

#!/bin/bash

# first command generate text for `sed` command that replacing . to ###
echo $( for i in {1..5}; do echo "${i} space .";done | sed 's/\./###/g' )

#############################################
# Another example: $() inside of another $()
for i in {1..5}; do touch "${i} x.file";done # Prepare some files with spaces in filenames

IFS='
'
echo "$( ls -la $(for i in ls *.file; do echo "$i"; done))"

Se você gostaria de passar todos os parâmetros em uma única linha para alimentar seu programa, transcodevocê pode adicionar ao final a ~/.bashrc seguinte função do seu arquivo:

_tr() {
  local debug
  debug=1
  [ $debug -eq 1 ] && {
    echo "Number of Arguments: $#"
    echo "Arguments: $@"
  }

  transcode "$@"
}

e chame essa função da linha de comando assim:

eval _tr $(echo "$out")

Onde variável outdeve ser assim: out="'file a' 'file b'".
Se digitar nomes de arquivos manualmente, então, chamar para a _trfunção pode parecer com:

eval _tr $(echo "'file a' 'file b'")

Se você usasse algum script externo no lugar do $()script / programa externo, deveria retornar a lista de arquivos citados exatamente assim:

"'file a' 'file b'"
Alex
fonte
mude de "$ {i} .file" para "$ {i} x.file" ... o que você estava dizendo sobre estar ok para usar espaços? ; ^)
Pierre Houston
E eu estou olhando para fazer algo assim como uma conveniência a partir da linha de comando, não de um script, então clichê como configurar o IFS etc não é apropriado. Mas obrigado de qualquer maneira, especialmente para respostas diretas às minhas perguntas específicas
Pierre Houston
@PierreHouston Bug corrigido :)))
Alex
@PierreHouston Para operações de linha de comando (sem fazer script) use a variante # 2, apenas separe os comandos com ;caractere
Alex
Boa correção rápida de erros: thumbsup :. Eu não vejo como o # 2 me ajuda em tudo. I (quase) literalmente deseja digitar no prompt bash: cmda $(cmdb-that-generates-parameters-for-cmda), um exemplo mais literal: transcode $(selection). Parece que não é possível, vou começar a usar fish-shell no meu Mac de trabalho.
Pierre Houston
1

Com xargs:

echo '"file a" "file b"' | xargs ls

Mente as citações. Nota xargsnão é um Bash builtin (estou mencionando isso em um contexto de seu "Bash, você tem um monte de recursos").

Com o evalqual é um Bash builtin:

eval ls $( echo '"file a" "file b"' )

Mais uma vez, lembre-se das citações. Isso pode sair pela culatra facilmente . Métodos baseados em mudar a IFS( esta outra resposta ) parecem mais seguros. Por outro lado, evalvocê pode até fazer isso:

eval ls $( echo '"file "{a,b}' )

Infelizmente, nada disso é tão simples quanto a ls (selection)sintaxe do outro shell com a qual você começou. No entanto, você pode simplificar a sintaxe de qualquer solução (?) Com uma função personalizada, por exemplo:

_(){ "$2" | xargs "$1"; }

Isso permite que você ligue _ ls selection, se apenas selectiongerar entrada para a xargsdireita. Melhor ainda, isso:

_(){ "${@:$#}" | xargs "${@:1:$(($#-1))}"; }

torna _ ls -l "file c" selectionpossível. Eu acho que você poderia criar uma função semelhante que usa o IFSmétodo "alterar o " de outra resposta.

Ainda assim, isso não é tão flexível e elegante como o peixe ls (selection). Eu espero que você possa fazer ls (selection1) (selection2)em peixe; a _função acima não cobre este caso.

Por outro lado, eval ls $(selection1) $(selection2)pode funcionar se selection1e selection2retornar cadeias higienizadas com cotação e / ou escape adequados. Se um invasor puder fazer com que você selecione um arquivo chamado, ; rm -rf ~/;é melhor higienizar bem; ou não usar evalem primeiro lugar.

Kamil Maciorowski
fonte
Certo, eu não estava falando apenas bash, mas a usabilidade da linha de comando ao usar o bash shell em um aplicativo terminal em um sistema unix-y, especificamente no Mac OS no meu caso. Então, todos os sinais apontam para Não, bash simplesmente não pode fazer isso. Obrigado.
Pierre Houston
quis dizer .. bash não pode fazer isso sem usar eval.
Pierre Houston