É possível que um programa obtenha um número de espaços entre os argumentos da linha de comando no POSIX?

23

Digamos se eu escrevi um programa com a seguinte linha:

int main(int argc, char** argv)

Agora ele sabe quais argumentos da linha de comando são passados ​​para ele, verificando o conteúdo de argv.

O programa pode detectar quantos espaços entre argumentos? Como quando eu digito estes no bash:

ibug@linux:~ $ ./myprog aaa bbb
ibug@linux:~ $ ./myprog       aaa      bbb

O ambiente é um Linux moderno (como o Ubuntu 16.04), mas suponho que a resposta deva se aplicar a qualquer sistema compatível com POSIX.

iBug
fonte
22
Só por curiosidade, por que seu programa precisaria saber disso?
Nxnev 15/0318
2
@nxnev Eu costumava escrever alguns programas do Windows e sei que é possível por lá, então me pergunto se há algo semelhante no Linux (ou Unix).
IBug 15/0318
9
Lembro-me vagamente no CP / M que os programas precisavam analisar suas próprias linhas de comando - isso significava que todo tempo de execução em C tinha que implementar um analisador de shell. E todos fizeram isso de maneira um pouco diferente.
precisa
3
@iBug Existe, mas você precisa citar os argumentos ao invocar o comando. É assim que é feito nos shells POSIX (e similares).
Konrad Rudolph
3
@iBug, ... O Windows tem o mesmo design que Toby menciona no CP / M acima. O UNIX não fazer isso - do ponto de vista chamado de processo, não é nenhuma linha de comando envolvido em executá-lo.
Charles Duffy

Respostas:

39

Não é significativo falar de "espaços entre argumentos"; esse é um conceito de concha.

O trabalho de um shell é pegar linhas inteiras de entrada e transformá-las em matrizes de argumentos para iniciar os comandos. Isso pode envolver a análise de seqüências de caracteres entre aspas, a expansão de variáveis, curingas de arquivos e expressões de til e muito mais. O comando é iniciado com uma execchamada de sistema padrão , que aceita um vetor de cadeias.

Existem outras maneiras de criar um vetor de strings. Muitos programas bifurcam e executam seus próprios subprocessos com invocações de comandos predeterminadas - nesse caso, nunca existe uma "linha de comando". Da mesma forma, um shell gráfico (da área de trabalho) pode iniciar um processo quando um usuário arrasta um ícone de arquivo e o solta em um widget de comando - novamente, não há linha de texto para ter caracteres "entre" argumentos.

No que diz respeito ao comando invocado, o que ocorre em um shell ou outro processo pai / precursor é privado e oculto - só vemos a matriz de strings que o padrão C especifica que main()pode aceitar.

Toby Speight
fonte
Boa resposta - é importante ressaltar isso para iniciantes no Unix, que geralmente assumem que, se executados tar cf texts.tar *.txt, o programa tar recebe dois argumentos e precisa expandir o segundo ( *.txt). Muitas pessoas não percebem como realmente funciona até começarem a escrever seus próprios scripts / programas que lidam com argumentos.
Laurence Renshaw
58

Em geral, não. A análise da linha de comando é feita pelo shell, que não disponibiliza a linha não analisada para o programa chamado. De fato, seu programa pode ser executado a partir de outro programa que criou o argumento não analisando uma string, mas construindo uma matriz de argumentos programaticamente.

Hans-Martin Mosner
fonte
9
Você pode mencionar execve(2).
IBug 15/0318
3
Você está certo, como uma desculpa esfarrapada que eu posso dizer que eu estou usando atualmente um telefone e olhando para cima homem páginas é um tedioso pouco :-)
Hans-Martin Mosner
1
Esta é a seção relevante do POSIX.
Stephen Kitt
1
@ Hans-MartinMosner: Termux ...? ;-)
DevSolar 15/03/19
9
"em geral" foi concebido como uma salvaguarda contra a citação de um caso complicado e especial em que é possível - por exemplo, um processo raiz suid pode ser capaz de inspecionar a memória do shell de chamada e encontrar a seqüência de caracteres da linha de comando não analisada.
Hans-Martin Mosner 16/03/19
16

Não, isso não é possível, a menos que os espaços façam parte de um argumento.

O comando acessa os argumentos individuais de uma matriz (de uma forma ou de outra, dependendo da linguagem de programação) e a linha de comando real pode ser salva em um arquivo de histórico (se digitado em um prompt interativo em um shell que possui arquivos de histórico), mas é nunca passou para o comando de qualquer forma.

Todos os comandos no Unix são executados no final por uma das exec()famílias de funções. Estes levam o nome do comando e uma lista ou matriz de argumentos. Nenhum deles usa uma linha de comando conforme digitado no prompt do shell. A system()função funciona, mas seu argumento de seqüência de caracteres é posteriormente executado por execve(), que, novamente, recebe uma matriz de argumentos em vez de uma linha de comando.

Kusalananda
fonte
2
@LightnessRacesinOrbit Coloquei isso lá para o caso de haver alguma confusão sobre "espaços entre argumentos". Colocar espaços entre aspas entre helloe worldé literalmente espaços entre os dois argumentos.
Kusalananda
5
@Kusalananda - Bem, não ... Colocar espaços entre aspas entre helloe literalmenteworld está fornecendo o segundo dos três argumentos.
Jeremy
@ Jeremy Como eu disse, no caso de haver alguma confusão sobre o que se entende por "entre os argumentos". Sim, como um segundo argumento entre os outros dois, se desejar.
Kusalananda
Seus exemplos foram bons e instrutivos.
Jeremy
1
Bem, pessoal, os exemplos foram uma fonte óbvia de confusão e mal-entendido. Eu os excluí porque o valor não foi adicionado ao valor da resposta.
Kusalananda
9

Em geral, não é possível, como várias outras respostas explicadas.

No entanto, os shells do Unix são programas comuns (e eles estão interpretando a linha de comando e dando uma olhada nela, ou seja, expandindo o comando antes de fazê fork- execvelo). Veja esta explicação sobre bashoperações shell . Você pode escrever seu próprio shell (ou corrigir algum shell de software livre existente , por exemplo, GNU bash ) e usá-lo como seu shell (ou mesmo como seu shell de login, consulte passwd (5) & shells (5) ).

Por exemplo, você pode ter seu próprio programa de shell colocando a linha de comando completa em alguma variável de ambiente (imagine MY_COMMAND_LINEpor exemplo) - ou usar qualquer outro tipo de comunicação entre processos para transmitir a linha de comando do shell para o processo filho-.

Não entendo por que você gostaria de fazer isso, mas você pode codificar um shell se comportando dessa maneira (mas eu recomendo não fazê-lo).

BTW, um programa pode ser iniciado por algum programa que não é um shell (mas que bifurca (2) e depois executa (2) , ou apenas um execvepara iniciar um programa em seu processo atual). Nesse caso, não há linha de comando e seu programa pode ser iniciado sem um comando ...

Observe que você pode ter algum sistema Linux (especializado) sem nenhum shell instalado. Isso é estranho e incomum, mas possível. Você precisará escrever um programa init especializado , iniciando outros programas conforme necessário - sem usar nenhum shell, mas fazendo fork& execvechamadas de sistema.

Leia também Sistemas operacionais: Três partes fáceis e não se esqueça que execvepraticamente sempre é uma chamada do sistema (no Linux, elas estão listadas em syscalls (2) , consulte também a introdução (2) ) que reinicializam o espaço de endereço virtual (e outras coisas) do processo .

Basile Starynkevitch
fonte
Esta é a melhor resposta. Presumo (sem ter pesquisado isso) que, argv[0] para o nome do programa e os elementos restantes para os argumentos, são especificações POSIX e não podem ser alterados. Um ambiente de tempo de execução pode especificar argv[-1]para a linha de comando, presumo ...
Peter - Reintegrar Monica
Não, não podia. Leia a execvedocumentação com mais cuidado . Você não pode usar argv[-1], é um comportamento indefinido para usá-lo.
Basile Starynkevitch 17/03/2019
Sim, bom ponto (também a dica de que temos um syscall) - a idéia é um pouco artificial. Todos os três componentes do tempo de execução (shell, stdlib e OS) precisariam colaborar. O shell precisa chamar uma função especial não-POSIX execvepluscmdcom um parâmetro extra (ou convenção argv), o syscall constrói um vetor de argumento para main que contém um ponteiro para a linha de comando antes do ponteiro para o nome do programa e depois passa o endereço do ponteiro para o nome do programa como argvao chamar o programa main...
Peter - Restabelece Monica
Não há necessidade de reescrever o shell, basta usar as aspas. Esse recurso estava disponível no shell do bourn sh. Então não é novidade.
Ctrl-alt-delor 18/03/19
O uso de aspas requer a alteração da linha de comando. E OP não quer isso
Basile Starynkevitch
3

Você sempre pode dizer ao seu shell para dizer aos aplicativos que código de shell leva à sua execução. Por exemplo, com zsh, passando essas informações na $SHELL_CODEvariável de ambiente usando o preexec()gancho ( printenvusado como exemplo, você usaria getenv("SHELL_CODE")em seu programa):

$ preexec() export SHELL_CODE=$1
$ printenv SHELL_CODE
printenv SHELL_CODE
$ printenv  SHELL_CODE
printenv  CODE
$ $(echo printenv SHELL_CODE)
$(echo printenv SHELL_CODE)
$ for i in SHELL_CODE; do printenv "$i"; done
for i in SHELL_CODE; do printenv "$i"; done
$ printenv SHELL_CODE; : other command
printenv SHELL_CODE; : other command
$ f() printenv SHELL_CODE
$ f
f

Todos aqueles seriam executados printenvcomo:

execve("/usr/bin/printenv", ["printenv", "SHELL_CODE"], 
       ["PATH=...", ..., "SHELL_CODE=..."]);

Permitindo printenvrecuperar o código zsh que leva à execução printenvcom esses argumentos. O que você gostaria de fazer com essas informações não está claro para mim.

Com bash, o recurso mais próximo zsh's preexec()estaria usando seu $BASH_COMMANDem uma DEBUGarmadilha, mas nota que bashfaz algum nível de reescrever em que (e em refatora particulares alguns dos espaços em branco usado como delimitador) e que é aplicado a cada (bem, alguns) de comando executar, não toda a linha de comando, conforme inserido no prompt (consulte também a functraceopção).

$ trap 'export SHELL_CODE="$BASH_COMMAND"' DEBUG
$ printenv SHELL_CODE
printenv SHELL_CODE
$ printenv $(echo 'SHELL_CODE')
printenv $(echo 'SHELL_CODE')
$ for i in SHELL_CODE; do printenv "$i"; done; : other command
printenv "$i"
$ printf '%s\n' "$(printenv "SHELL_CODE")"
printf '%s\n' "$(printenv "SHELL_CODE")"
$ set -o functrace
$ printf '%s\n' "$(printenv "SHELL_CODE")"
printenv "SHELL_CODE"
$ print${-+env  }    $(echo     'SHELL_CODE')
print${-+env  } $(echo     'SHELL_CODE')

Veja como alguns dos espaços delimitadores na sintaxe da linguagem shell foram compactados em 1 e como nem toda a linha de comando nem sempre é passada para o comando. Então provavelmente não é útil no seu caso.

Observe que eu não recomendaria esse tipo de coisa, pois você está potencialmente vazando informações confidenciais para todos os comandos, como em:

echo very_secret | wc -c | untrustedcmd

vazaria esse segredo para ambos wce untrustedcmd.

Claro, você poderia fazer esse tipo de coisa para outros idiomas além do shell. Por exemplo, em C, você pode usar algumas macros que exportam o código C que executa um comando para o ambiente:

#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#define WRAP(x) (setenv("C_CODE", #x, 1), x)

int main(int argc, char *argv[])
{
  if (!fork()) WRAP(execlp("printenv", "printenv", "C_CODE", NULL));
  wait(NULL);
  if (!fork()) WRAP(0 + execlp("printenv",   "printenv", "C_CODE", NULL));
  wait(NULL);
  if (argc > 1 && !fork()) WRAP(execvp(argv[1], &argv[1]));
  wait(NULL);
  return 0;
}

Exemplo:

$ ./a.out printenv C_CODE
execlp("printenv", "printenv", "C_CODE", NULL)
0 + execlp("printenv", "printenv", "C_CODE", NULL)
execvp(argv[1], &argv[1])

Veja como alguns espaços foram condensados ​​pelo pré-processador C, como no caso do bash. Na maioria dos idiomas, se não em todos os idiomas, a quantidade de espaço usada nos delimitadores não faz diferença; portanto, não é de surpreender que o compilador / intérprete tenha alguma liberdade com eles aqui.

Stéphane Chazelas
fonte
Quando eu estava testando isso, BASH_COMMANDnão continha os argumentos de separação de espaço em branco originais, portanto isso não era utilizável para a solicitação literal do OP. Esta resposta inclui alguma demonstração de qualquer maneira para esse caso de uso específico?
Charles Duffy
@CharlesDuffy, eu só queria indicar o equivalente mais próximo do preexec () do zsh no bash (como esse é o shell ao qual o OP estava se referindo) e ressaltar que não poderia ser usado para esse caso de uso específico, mas concordo que não era muito claro. Veja editar. Esta resposta pretende ser mais genérica sobre como passar o código-fonte (aqui em zsh / bash / C) que causou a execução do comando (não algo que seja útil, mas espero que ao fazê-lo e especialmente com os exemplos, eu demonstro que isso não é muito útil)
Stéphane Chazelas
0

Vou apenas adicionar o que está faltando nas outras respostas.

Não

Veja outras respostas

Talvez, mais ou menos

Não há nada que possa ser feito no programa, mas há algo que pode ser feito no shell quando você executa o programa.

Você precisa usar aspas. Então, ao invés de

./myprog      aaa      bbb

você precisa fazer um desses

./myprog "     aaa      bbb"
./myprog '     aaa      bbb'

Isso passará um único argumento para o programa, com todos os espaços. Há uma diferença entre os dois, o segundo é literal, exatamente a string que aparece (exceto que 'deve ser digitada como \'). O primeiro interpretará alguns caracteres, mas será dividido em vários argumentos. Veja shell citando para mais informações. Portanto, não há necessidade de reescrever o shell, os designers do shell já pensaram nisso. No entanto, como agora é um argumento, você precisará fazer mais alterações no programa.

opção 2

Passe os dados via stdin. Essa é a maneira normal de obter grandes quantidades de dados em um comando. por exemplo

./myprog << EOF
    aaa      bbb
EOF

ou

./myprog
Tell me what you want to tell me:
aaaa bbb
ctrl-d

(Itálico são resultados do programa)

ctrl-alt-delor
fonte
Tecnicamente, o código shell: ./myprog␣"␣␣␣␣␣aaa␣␣␣␣␣␣bbb"executa (geralmente em um processo filho) o arquivo armazenado no ./myproge passa dois argumentos: ./myproge ␣␣␣␣␣aaa␣␣␣␣␣␣bbb( argv[0]e argc[1], argcsendo 2) e como no OP de, o espaço que separa estes dois argumentos não é passado de qualquer maneira para myprog.
Stéphane Chazelas
Mas você está mudando de comando e OP não quer mudá-lo
Basile Starynkevitch
@BasileStarynkevitch Após o seu comentário, li a pergunta novamente. Você está fazendo uma suposição. Em nenhum lugar o OP diz que não deseja alterar a maneira como o programa é executado. Talvez isso seja verdade, mas eles não tinham nada a dizer sobre isso. Portanto, esta resposta pode ser o que eles precisam.
Ctrl-alt-delor 18/03/19
OP pedir explicitamente sobre espaços entre argumentos, não sobre um único argumento contendo espaços
Basile Starynkevitch