Est-il possible pour un programme d'obtenir le nombre d'espaces entre les arguments de ligne de commande dans POSIX?

23

Dites si j'ai écrit un programme avec la ligne suivante:

int main(int argc, char** argv)

Maintenant, il sait quels arguments de ligne de commande lui sont transmis en vérifiant le contenu de argv.

Le programme peut-il détecter le nombre d'espaces entre les arguments? Comme quand je les tape dans bash:

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

L'environnement est un Linux moderne (comme Ubuntu 16.04), mais je suppose que la réponse devrait s'appliquer à tous les systèmes compatibles POSIX.

iBug
la source
22
Par curiosité, pourquoi votre programme aurait-il besoin de le savoir?
nxnev
2
@nxnev J'avais l'habitude d'écrire des programmes Windows et je sais que c'est possible là-bas, donc je me demande s'il y a quelque chose de similaire sous Linux (ou Unix).
iBug
9
Je me souviens vaguement dans CP / M que les programmes devaient analyser leurs propres lignes de commande - cela signifiait que chaque runtime C devait implémenter un analyseur shell. Et ils l'ont tous fait légèrement différemment.
Toby Speight
3
@iBug Oui, mais vous devez citer les arguments lors de l'appel de la commande. C'est ainsi que cela se fait sur les shells POSIX (et similaires).
Konrad Rudolph
3
@iBug, ... Windows a le même design que Toby mentionne dans CP / M ci-dessus. UNIX ne le fait pas - du point de vue du processus appelé, il n'y a pas de ligne de commande impliqué dans l' exécuter.
Charles Duffy

Réponses:

39

Il n'est pas significatif de parler d '"espaces entre les arguments"; c'est un concept de coquille.

Le travail d'un shell est de prendre des lignes entières d'entrée et de les former en tableaux d'arguments avec lesquels démarrer les commandes. Cela peut impliquer l'analyse de chaînes entre guillemets, l'expansion de variables, les caractères génériques de fichier et les expressions tilde, etc. La commande est lancée avec un execappel système standard , qui accepte un vecteur de chaînes.

Il existe d'autres façons de créer un vecteur de chaînes. De nombreux programmes bifurquent et exécutent leurs propres sous-processus avec des appels de commande prédéterminés - dans ce cas, il n'y a jamais de "ligne de commande". De même, un shell graphique (de bureau) peut démarrer un processus lorsqu'un utilisateur fait glisser une icône de fichier et la dépose sur un widget de commande - là encore, il n'y a pas de ligne textuelle pour avoir des caractères "entre" les arguments.

En ce qui concerne la commande invoquée, ce qui se passe dans un shell ou un autre processus parent / précurseur est privé et caché - nous ne voyons que le tableau de chaînes que le standard C spécifie qui main()peut accepter.

Toby Speight
la source
Bonne réponse - il est important de le signaler aux débutants Unix, qui supposent souvent que s'ils s'exécutent, tar cf texts.tar *.txtle programme tar obtient deux arguments et doit développer le second ( *.txt) lui-même. Beaucoup de gens ne réalisent pas comment cela fonctionne vraiment avant de commencer à écrire leurs propres scripts / programmes qui gèrent les arguments.
Laurence Renshaw
58

En général, non. L'analyse de la ligne de commande est effectuée par le shell qui ne met pas la ligne non analysée à la disposition du programme appelé. En fait, votre programme peut être exécuté à partir d'un autre programme qui a créé l'argv non pas en analysant une chaîne mais en construisant un tableau d'arguments par programme.

Hans-Martin Mosner
la source
9
Vous voudrez peut-être mentionner execve(2).
iBug
3
Vous avez raison, comme une excuse boiteuse, je peux dire que j'utilise actuellement un téléphone et que la recherche de pages de manuel est un peu fastidieuse :-)
Hans-Martin Mosner
1
Il s'agit de la section pertinente de POSIX.
Stephen Kitt
1
@ Hans-MartinMosner: Termux ...? ;-)
DevSolar
9
"en général" était censé être une garantie contre la citation d'un cas particulier compliqué où cela est possible - par exemple, un processus racine suid pourrait être en mesure d'inspecter la mémoire du shell appelant et de trouver la chaîne de ligne de commande non analysée.
Hans-Martin Mosner
16

Non, ce n'est pas possible, sauf si les espaces font partie d' un argument.

La commande accède aux arguments individuels à partir d'un tableau (sous une forme ou une autre selon le langage de programmation) et la ligne de commande réelle peut être enregistrée dans un fichier d'historique (si elle est tapée à une invite interactive dans un shell contenant des fichiers d'historique), mais est jamais transmis à la commande sous quelque forme que ce soit.

Toutes les commandes sous Unix sont finalement exécutées par l'une des exec()familles de fonctions. Ceux-ci prennent le nom de la commande et une liste ou un tableau d'arguments. Aucun d'eux ne prend une ligne de commande telle que tapée à l'invite du shell. La system()fonction le fait, mais son argument de chaîne est ensuite exécuté par execve(), qui, encore une fois, prend un tableau d'arguments plutôt qu'une chaîne de ligne de commande.

Kusalananda
la source
2
@LightnessRacesinOrbit J'ai mis cela là juste au cas où il y aurait une certaine confusion au sujet des "espaces entre les arguments". Mettre des espaces entre guillemets helloet worldest littéralement des espaces entre les deux arguments.
Kusalananda
5
@Kusalananda - Eh bien, non ... Mettre des espaces entre guillemets helloet worldest littéralement fournir le deuxième des trois arguments.
Jeremy
@Jeremy Comme je l'ai dit, au cas où il y aurait une confusion sur ce que l'on entendait par "entre les arguments". Oui, comme deuxième argument entre les deux autres si vous voulez.
Kusalananda
Vos exemples étaient bons et instructifs.
Jeremy
1
Eh bien, les gars, les exemples étaient une source évidente de confusion et d'incompréhension. Je les ai supprimés car cela n'ajoutait rien à la valeur de la réponse.
Kusalananda
9

En général, ce n'est pas possible, comme l'ont expliqué plusieurs autres réponses.

Cependant, les shells Unix sont des programmes ordinaires (et ils interprètent la ligne de commande et la globlent , c'est-à-dire développant la commande avant de la faire fork& execvepour elle). Voir cette explication sur bashles opérations shell . Vous pouvez écrire votre propre shell (ou patcher un shell de logiciel libre existant , par exemple GNU bash ) et l'utiliser comme shell (ou même comme shell de connexion, voir passwd (5) & shells (5) ).

Par exemple, vous pourriez avoir votre propre programme shell mettre la ligne de commande complète dans une variable d'environnement (imaginez MY_COMMAND_LINEpar exemple) -ou utiliser tout autre type de communication inter-processus pour transmettre la ligne de commande du shell au processus enfant-.

Je ne comprends pas pourquoi vous voudriez faire cela, mais vous pourriez coder un shell se comportant de cette manière (mais je recommande de ne pas le faire).

BTW, un programme pourrait être démarré par un programme qui n'est pas un shell (mais qui fait fork (2) puis execve (2) , ou juste un execvepour démarrer un programme dans son processus actuel). Dans ce cas, il n'y a pas de ligne de commande du tout, et votre programme pourrait être démarré sans commande ...

Notez que vous pourriez avoir un système Linux (spécialisé) sans aucun shell installé. C'est bizarre et inhabituel, mais possible. Vous devrez ensuite écrire un programme d' initialisation spécialisé en démarrant d'autres programmes selon vos besoins - sans utiliser de shell mais en faisant des appels fork& execvesystem.

Lire aussi Systèmes d'exploitation: trois éléments faciles et n'oubliez pas qu'il execves'agit pratiquement toujours d'un appel système (sous Linux, ils sont répertoriés dans syscalls (2) , voir aussi intro (2) ) qui réinitialise l' espace d'adressage virtuel (et quelques autres choses) du processus qui le fait.

Basile Starynkevitch
la source
C'est la meilleure réponse. Je suppose (sans l'avoir recherché) que argv[0] pour le nom du programme et les éléments restants pour les arguments sont des spécifications POSIX et ne peuvent pas être modifiés. Un environnement d'exécution pourrait spécifier argv[-1]la ligne de commande, je suppose ...
Peter - Rétablir Monica
Non, ça ne pouvait pas. Lisez plus attentivement la execvedocumentation. Vous ne pouvez pas l'utiliser argv[-1], c'est un comportement indéfini de l'utiliser.
Basile Starynkevitch
Ouais, bon point (également l'indication que nous avons un appel système) - l'idée est un peu artificielle. Les trois composants du runtime (shell, stdlib et OS) devraient collaborer. Le shell doit appeler une fonction spéciale non-POSIX execvepluscmdavec un paramètre supplémentaire (ou convention argv), le syscall construit un vecteur d'argument pour main qui contient un pointeur sur la ligne de commande avant le pointeur sur le nom du programme, puis passe l'adresse du pointeur sur le nom du programme comme argvlors de l'appel du programme main...
Peter - Rétablir Monica
Pas besoin de réécrire le shell, utilisez simplement les guillemets. Cette fonctionnalité était disponible à partir du shell bourn sh. Ce n'est donc pas nouveau.
ctrl-alt-delor
L'utilisation de guillemets nécessite de modifier la ligne de commande. Et OP n'en veut pas
Basile Starynkevitch
3

Vous pouvez toujours dire à votre shell d'indiquer aux applications quel code shell mène à leur exécution. Par exemple, avec zsh, en passant ces informations dans la $SHELL_CODEvariable d'environnement en utilisant le preexec()hook ( printenvutilisé comme exemple, vous utiliseriez getenv("SHELL_CODE")dans votre programme):

$ 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

Tous ceux-ci s'exécuteraient printenvcomme:

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

Permet printenvde récupérer le code zsh qui a conduit à l'exécution de printenvces arguments. Ce que vous voudriez faire de ces informations ne m'est pas clair.

Avec bash, la fonctionnalité la plus proche de zshs preexec()utiliserait son $BASH_COMMANDdans un DEBUGpiège, mais notez que cela bashfait un certain niveau de réécriture dans cela (et en particulier refacteurs certains des espaces utilisés comme délimiteur) et qui est appliqué à chaque commande (enfin, certains) exécutez, pas la ligne de commande entière comme entrée à l'invite (voir aussi l' functraceoption).

$ 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')

Voyez comment certains des espaces qui sont des délimiteurs dans la syntaxe du langage shell ont été compressés en 1 et comment la ligne de commande complète n'est pas toujours transmise à la commande. Donc probablement pas utile dans votre cas.

Notez que je ne conseillerais pas de faire ce genre de chose, car vous risquez de divulguer des informations sensibles à chaque commande comme dans:

echo very_secret | wc -c | untrustedcmd

divulguerait ce secret à la fois wcet untrustedcmd.

Bien sûr, vous pourriez faire ce genre de chose pour d'autres langues que le shell. Par exemple, en C, vous pouvez utiliser certaines macros qui exportent le code C qui exécute une commande vers l'environnement:

#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;
}

Exemple:

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

Voyez comment certains espaces ont été condensés par le pré-processeur C comme dans le cas bash. Dans la plupart, sinon toutes les langues, la quantité d'espace utilisée dans les délimiteurs ne fait aucune différence, il n'est donc pas surprenant que le compilateur / interprète prenne une certaine liberté avec eux ici.

Stéphane Chazelas
la source
Lorsque je testais cela, BASH_COMMANDne contenait pas les arguments de séparation d'espaces d'origine, donc ce n'était pas utilisable pour la demande littérale de l'OP. Cette réponse inclut-elle une démonstration dans un cas ou dans l'autre pour ce cas d'utilisation particulier?
Charles Duffy
@CharlesDuffy, je voulais juste indiquer l'équivalent le plus proche de preshec () de zsh dans bash (car c'est le shell auquel l'OP faisait référence) et souligner qu'il ne pouvait pas être utilisé pour ce cas d'utilisation spécifique, mais je suis d'accord qu'il ne l'était pas très clair. Voir modifier. Cette réponse est destinée à être plus générique sur la façon de passer le code source (ici dans zsh / bash / C) qui a provoqué l'exécution de la commande en cours d'exécution (pas quelque chose d'utile, mais j'espère que ce faisant, et surtout avec les exemples, je démontre que ce n'est pas très utile)
Stéphane Chazelas
0

J'ajouterai simplement ce qui manque dans les autres réponses.

Non

Voir d'autres réponses

Peut-être, en quelque sorte

Il n'y a rien qui puisse être fait dans le programme, mais il y a quelque chose qui peut être fait dans le shell lorsque vous exécutez le programme.

Vous devez utiliser des guillemets. Donc au lieu de

./myprog      aaa      bbb

vous devez en faire une

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

Cela transmettra un seul argument au programme, avec tous les espaces. Il y a une différence entre les deux, le second est littéral, exactement la chaîne telle qu'elle apparaît (sauf qu'elle 'doit être tapée comme \'). Le premier interprétera certains caractères, mais divisé en plusieurs arguments. Voir les citations du shell pour plus d'informations. Donc pas besoin de réécrire le shell, les concepteurs du shell y ont déjà pensé. Cependant, comme il s'agit désormais d'un seul argument, vous devrez en faire plus en passant dans le programme.

Option 2

Passez les données via stdin. Il s'agit de la manière normale d'obtenir de grandes quantités de données dans une commande. par exemple

./myprog << EOF
    aaa      bbb
EOF

ou

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

(Les italiques sont la sortie du programme)

ctrl-alt-delor
la source
Techniquement, le code shell: ./myprog␣"␣␣␣␣␣aaa␣␣␣␣␣␣bbb"exécute (généralement dans un processus enfant) le fichier stocké dans ./myproget lui transmet deux arguments: ./myproget ␣␣␣␣␣aaa␣␣␣␣␣␣bbb( argv[0]et argc[1], argcétant 2) et comme dans les OP, l'espace qui sépare ces deux arguments n'est transmis d'aucune façon à myprog.
Stéphane Chazelas
Mais vous changez la commande, et OP ne veut pas la changer
Basile Starynkevitch
@BasileStarynkevitch Suite à votre commentaire, j'ai relu la question. Vous faites une supposition. Nulle part le PO ne dit qu'il ne veut pas changer la façon dont le programme est exécuté. C'est peut-être vrai, mais ils n'avaient rien à dire là-dessus. Par conséquent, cette réponse peut être ce dont ils ont besoin.
ctrl-alt-delor
OP demande explicitement sur les espaces entre les arguments, pas sur un seul argument contenant des espaces
Basile Starynkevitch