Pourquoi awk s'arrête et attend si le nom de fichier contient = et comment contourner cela?

25
awk 'processing_script_here' my=file.txt

semble s'arrêter et attendre indéfiniment ...
Que se passe-t-il ici et comment puis-je le faire fonctionner?

don_crissti
la source

Réponses:

19

Comme le dit Chris , les arguments du formulaire variablename=anythingsont traités comme des affectations de variables (qui sont effectuées au moment où les arguments sont traités par opposition aux (plus récents) -v var=valuequi sont effectués avant les BEGINinstructions) au lieu des noms de fichiers d'entrée.

Cela peut être utile dans des choses comme:

awk '{print $1}' FS=/ RS='\n' file1 FS='\n' RS= file2

Où vous pouvez spécifier un fichier différent FS/ RSpar. Il est également couramment utilisé dans:

awk '!file1_processed{a[$0]; next}; {...}' file1 file1_processed=1 file2

Qui est une version plus sûre de:

awk 'NR==FNR{a[$0]; next}; {...}' file1 file2

(qui ne fonctionne pas s'il file1est vide)

Mais cela gêne lorsque vous avez des fichiers dont le nom contient des =caractères.

Maintenant, ce n'est qu'un problème lorsque ce qui reste du premier =est un awknom de variable valide .

Ce qui constitue un nom de variable valide dans awkest plus strict que dans sh.

POSIX exige que ce soit quelque chose comme:

[_a-zA-Z][_a-zA-Z0-9]*

Avec uniquement des caractères du jeu de caractères portable. Cependant, /usr/xpg4/bin/awkSolaris 11 au moins n'est pas conforme à cet égard et autorise tous les caractères alphabétiques dans les paramètres régionaux dans les noms de variables, pas seulement a-zA-Z.

Ainsi, un argument comme x+y=fooou =barou ./foo=barest toujours traité comme un nom de fichier d'entrée et non comme une affectation car ce qui reste du premier =n'est pas un nom de variable valide. Un argument comme Stéphane=Chazelas.txtmay ou may not, selon l' awkimplémentation et les paramètres régionaux.

C'est pourquoi avec awk, il est recommandé d'utiliser:

awk '...' ./*.txt

au lieu de

awk '...' *.txt

par exemple pour éviter le problème si vous ne pouvez pas garantir que le nom des txtfichiers ne contiendra pas de =caractères.

Gardez également à l'esprit qu'un argument comme celui-ci -vfoo=bar.txtpeut être traité comme une option si vous utilisez:

awk -f file.awk -vfoo=bar.txt

(applique également awk '{code}' -vfoo=bar.txtavec les awkdes versions busybox avant 1.28.0, voir correspondant rapport de bogue ).

Encore une fois, l'utilisation ./*.txtfonctionne autour de cela (l'utilisation d'un ./préfixe aide également avec un fichier appelé -qui autrement awkcomprend comme signifiant une entrée standard à la place).

C'est aussi pourquoi

#! /usr/bin/awk -f

les bangs ne fonctionnent pas vraiment. Alors que var=valueceux-ci peuvent être contournés en fixant les ARGVvaleurs (ajoutez un ./préfixe) dans une BEGINinstruction:

#! /usr/bin/awk -f
BEGIN {
  for (i = 1; i < ARGC; i++)
    if (ARGV[i] ~ /^[_[:alpha:]][_[:alnum:]]*=/)
      ARGV[i] = "./" ARGV[i]
}
# rest of awk script

Cela n'aidera pas les options car celles-ci sont vues par awket non le awkscript.

Un problème cosmétique potentiel avec l'utilisation de ce ./préfixe est qu'il se termine FILENAME, mais vous pouvez toujours l'utiliser substr(FILENAME, 3)pour le supprimer si vous ne le souhaitez pas.

L'implémentation GNU awkcorrige tous ces problèmes avec son -Eoption.

Après -E, gawk n'attend que le chemin du awkscript (où -signifie toujours stdin) puis une liste de chemins de fichiers d'entrée uniquement (et là, même pas -traité spécialement).

Il est spécialement conçu pour:

#! /usr/bin/gawk -E

shebangs où la liste des arguments sont toujours des fichiers d'entrée (notez que vous êtes toujours libre de modifier cette ARGVliste dans une BEGINdéclaration).

Vous pouvez également l'utiliser comme:

gawk -e '...awk code here...' -E /dev/null *.txt

Nous utilisons -Eavec un script vide ( /dev/null) juste pour nous assurer que ces *.txtderniers sont toujours traités comme des fichiers d'entrée, même s'ils contiennent des =caractères.

Stéphane Chazelas
la source
Je ne vois pas en quoi le chemin explicite se terminant dans FILENAME est un problème. Soit le script awk est général, auquel cas il doit gérer tous les types de chemins se terminant par FILENAME (y compris, mais sans s'y limiter ../foo, /path/to/fooet les chemins qui sont dans un codage différent) - dans ce cas, substr(FILENAME,3)ce ne sera pas suffisant, ou c'est un script unique où l'utilisateur sait essentiellement quels sont les noms de fichiers - dans ce cas, il ne devrait probablement pas se soucier de l'un d'eux contenant =non plus ;-)
mosvy
2
@mosvy Je ne pense pas que cela énonce autant de ./problèmes, mais qu'il peut être indésirable dans certaines conditions, telles que les cas où le nom de fichier doit être inclus dans la sortie, auquel cas il ./doit être redondant et inutile, donc vous je vais devoir m'en débarrasser. Voici au moins un exemple . En ce qui concerne l'utilisateur sachant quels sont les noms de fichiers - eh bien, dans ce cas, nous savons également quel est le nom de fichier, mais cela =empêche toujours un traitement approprié. Le leader peut donc se -mettre en travers.
Sergiy Kolodyazhnyy
@mosvy, oui, l'idée est que vous souhaitez utiliser le ./préfixe pour contourner cette awk(mauvaise) fonctionnalité, mais vous vous retrouvez avec une ./sortie en sortie que vous voudrez peut-être supprimer. Voir comment vérifier si la première ligne de fichier contient une chaîne spécifique? par exemple.
Stéphane Chazelas
Ce n'est pas seulement le local (par rapport à ce répertoire) ./mais aussi le global (chemin absolu) /qui fait que awk interprète l'argument comme un fichier.
Isaac
21

Dans la plupart des versions d'awk, les arguments après le programme à exécuter sont soit:

  1. Un fichier
  2. Une cession du formulaire x=y

Étant donné que votre nom de fichier est interprété comme le cas # 2, awk attend toujours que quelque chose soit lu sur stdin (car il ne perçoit pas qu'un nom de fichier a été transmis).

Portablement, ce comportement est documenté dans POSIX :

L'un des deux types d'arguments suivants peut être mélangé:

  • fichier: nom de chemin d'un fichier qui contient l'entrée à lire, qui est comparé à l'ensemble des modèles du programme. Si aucun opérande de fichier n'est spécifié, ou si un opérande de fichier est «-», l'entrée standard doit être utilisée.
  • affectation: un opérande qui commence par un trait de soulignement ou un caractère alphabétique du jeu de caractères portable (voir le tableau dans le volume Définitions de base de IEEE Std 1003.1-2001, Section 6.1, Jeu de caractères portable), suivi d'une séquence de traits de soulignement, de chiffres, et les caractères alphabétiques du jeu de caractères portable, suivis du caractère «=», doivent spécifier une affectation de variable plutôt qu'un nom de chemin.

En tant que tel, de manière portable, vous avez quelques options (# 1 est probablement le moins intrusif):

  1. Utiliser awk ... ./my=file, qui contourne cela car ce .n'est pas "un caractère de soulignement ou alphabétique du jeu de caractères portable".
  2. Mettez le fichier sur stdin en utilisant awk ... < my=file. Cependant, cela ne fonctionne pas bien avec plusieurs fichiers.
  3. Créez temporairement un lien physique vers le fichier et utilisez-le. Vous pouvez faire quelque chose comme ln my=file my_file, puis l'utiliser my_filecomme d'habitude. Aucune copie ne sera effectuée et les deux fichiers seront sauvegardés par les mêmes données et métadonnées d'inode. Après l'avoir utilisé, il est sûr de supprimer le lien créé car le nombre de références à l'inode sera toujours supérieur à 0.
Chris Down
la source
6
Ça ne ./my=file marche pas ? % awk 'processing_script_here' ./my=file.txt awk: fatal: cannot open file ./my=file.txt' for reading (No such file or directory). Cela devrait être portable car ./myn'est pas un nom de variable valide, donc ne devrait pas être analysé de cette façon.
Stephen Harris
2
Comme le dit ce texte POSIX, le problème ne se pose que lorsque le premier =est précédé d' un trait de soulignement ou d' un caractère alphabétique du jeu de caractères portable (voir le tableau dans le volume Définitions de base de IEEE Std 1003.1-2001, Section 6.1, Jeu de caractères portable), suivi d'une séquence de traits de soulignement, de chiffres et d'alphabets du jeu de caractères portable . donc un chemin de fichier comme ++foo=bar.txtou =fooou ./foo=barsont tous OK comme ça .ou +n'est pas un [_a-zA-Z].
Stéphane Chazelas
1
@SergiyKolodyazhnyy awk est externe au shell, donc peu importe celui que vous utilisez. ./my=filesera transmis mot pour mot.
Chris Down
1
@SergiyKolodyazhnyy, idem pour awk '{print $1,$2}' /etc/passwd. Le fait est que le fait d'ouvrir le shell par opposition à awk ne fait aucune différence quant à savoir s'il le rend recherchable ou non. En fait, dans awk '{exit}' < /etc/passwd, vous vous attendez awkà revenir à la fin du premier enregistrement exitpour vous assurer qu'il laisse la position au sein de stdin. POSIX l'exige. /usr/xpg4/bin/awkle fait sur Solaris, mais ni gawkne mawksemble le faire sur GNU / Linux.
Stéphane Chazelas
3
@mosvy, voir la section INPUT FILES sur pubs.opengroup.org/onlinepubs/9699919799/utilities/… Il est utile dans un certain nombre de modèles d'utilisation qui n'ont de sens qu'avec des fichiers normaux comme lorsque vous souhaitez tronquer un fichier ou y écrire des données à l' adresse une position identifiée de awkcette façon.
Stéphane Chazelas
3

Pour citer la documentation de Gawk (note soulignée ajoutée):

Tous les arguments supplémentaires sur la ligne de commande sont normalement traités comme des fichiers d'entrée à traiter dans l'ordre spécifié. Cependant, un argument qui a la forme var = value, assigne la valeur value à la variable var — il ne spécifie pas du tout de fichier.

Pourquoi la commande s'arrête-t-elle et attend-elle? Parce que dans le formulaire, awk 'processing_script_here' my=file.txt il n'y a pas de fichier spécifié par la définition ci-dessus - my=file.txtest interprété comme une affectation de variable, et s'il n'y a pas de fichier défini awklira stdin (également évident à partir de stracece qui montre que awk dans une telle commande attend read(0,'...)syscall.

Ceci est également documenté dans les spécifications POSIX awk , voir la section OPERANDS et les affectations qui en font partie)

L'affectation des variables est évidente dans la mesure awk '{print foo}' foo=bar /etc/passwdoù la valeur de fooest imprimée pour chaque ligne dans / etc / passwd. La spécification ./foo=barou le chemin complet fonctionne cependant.

Notez que l' exécution stracesur awk '1' foo=barainsi que de vérifier avec cat foo=barmontre que ce problème est awk spécifique et exec ne nom de fichier show comme argument passé, de sorte que des obus ont rien à voir avec les assignations de variables env dans ce cas.

De plus, veuillez noter que awk '...script...' foo=barcela ne provoquera pas la création de variables d'environnement par le shell, car les affectations de variables d'environnement doivent précéder une commande pour prendre effet. Voir Règles de grammaire du shell POSIX , point numéro 7. De plus, cela peut être vérifié viaawk '{print ENVIRON["foo"]}' foo=bar /etc/passwd

Sergiy Kolodyazhnyy
la source