Filtrer ou diriger certaines sections d'un fichier

14

J'ai un fichier d'entrée avec certaines sections qui sont délimitées avec des balises de début et de fin, par exemple:

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D

Je veux appliquer une transformation à ce fichier de telle sorte que les lignes X, Y, Z soient filtrées via une commande ( nl, par exemple), mais le reste des lignes passe inchangé. Notez que nl(lignes numériques) accumule l'état sur plusieurs lignes, donc ce n'est pas une transformation statique qui est appliquée à chacune des lignes X, Y, Z. ( Edit : il a été souligné que cela nlpeut fonctionner dans un mode qui ne nécessite pas d'état accumulé, mais je ne fais qu'utiliser nlcomme exemple pour simplifier la question. En réalité, la commande est un script personnalisé plus complexe. Ce que je cherche vraiment car est une solution générique au problème de l'application d'un filtre standard à une sous-section d'un fichier d'entrée )

La sortie doit ressembler à:

line A
line B
     1 line X
     2 line Y
     3 line Z
line C
line D

Il peut y avoir plusieurs de ces sections dans le fichier qui nécessitent la transformation.

Mise à jour 2 Je n'ai pas spécifié à l'origine ce qui devait arriver s'il y avait plus d'une section, par exemple:

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D
 @@inline-code-start
line L
line M
line N
@@inline-code-end

Je m'attendais à ce que l'état ne doive être maintenu que dans une section donnée, donnant:

line A
line B
     1 line X
     2 line Y
     3 line Z
line C
line D
     1 line L
     2 line M
     3 line N

mais, je pense qu'interpréter le problème comme exigeant que l'état soit conservé entre les sections est valide et utile dans de nombreux contextes.

Fin de la mise à jour 2

Ma première pensée est de construire une machine à états simple qui suit dans quelle section nous sommes:

#!/usr/bin/bash
while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
  elif [[ $active = true ]]
  then
    # pipe
  echo $line | nl
  else
    # output
    echo $line
  fi
done

Avec qui je cours:

cat test-inline-codify | ./inline-codify

Cela ne fonctionne pas car chaque appel à nlest indépendant, donc les numéros de ligne n'augmentent pas:

line A
line B
     1  line X
     1  line Y
     1  line Z
line C
line D

Ma prochaine tentative a été d'utiliser un fifo:

#!/usr/bin/bash
mkfifo myfifo
nl < myfifo &
while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
  elif [[ $active = true ]]
  then
    # pipe
    echo $line > myfifo
  else
    # output
    echo $line
  fi
done
rm myfifo

Cela donne la sortie correcte, mais dans le mauvais ordre:

line A
line B
line C
line D
     1  line 1
     2  line 2
     3  line 3

Il y a probablement une mise en cache.

Est-ce que je me trompe? Cela semble être un problème assez générique. Je pense qu'il devrait y avoir un simple pipeline pour résoudre ce problème.

James Scriven
la source
nln'a pas à accumuler d'état . Regardez nl -det vérifiez vos man/ infopages pour plus d'informations sur nlle délimiteur de section .
mikeserv
nl est juste un exemple. Dans mon cas, je lance un script personnalisé plutôt que nl.
James Scriven
Dans ce cas, veuillez clarifier ce que fait votre script.
terdon
J'ai précisé dans la question que je n'utilise nlque comme exemple de filtre. Je pensais que cela simplifierait la question en masquant les détails de ce que faisait exactement le filtre, mais j'ai probablement causé plus de confusion. En fait, je filtre la sous-section via un surligneur de code, pour un générateur de blog statique maison. En ce moment, j'utilise gnu source-highlight, mais cela pourrait changer, et je pourrais aussi ajouter plus de filtres, comme un formateur.
James Scriven

Réponses:

7

Je suis d' accord avec vous - il probablement est un problème générique. Cependant, certains utilitaires communs ont certaines installations pour le gérer.


nl

nl, par exemple, sépare l'entrée en pages logiques comme -ddélimitées par un délimiteur de section à deux caractères . Trois occurrences sur une ligne indiquent à elles seules le début d'un cap , deux le corps et un le pied de page . Il remplace tous ceux trouvés en entrée par une ligne vierge en sortie - qui sont les seules lignes vierges qu'il imprime

J'ai modifié votre exemple pour inclure une autre section et je l'ai ajouté ./infile. Il ressemble donc à ceci:

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D
@@start
line M
line N
line O
@@end

Ensuite, j'ai exécuté ce qui suit:

sed 's/^@@.*start$/@@@@@@/
     s/^@@.*end$/@@/'  <infile |
nl -d@@ -ha -bn -w1

nlpeut être chargé d' accumuler l'état sur les pages logiques, mais ce n'est pas le cas par défaut. Au lieu de cela, il numérotera les lignes de son entrée en fonction des styles et par section . Cela -hasignifie donc numéroter toutes les lignes d'en- tête et -bnsignifie pas de lignes de corps - comme cela commence dans un corps état de .

Jusqu'à ce que j'apprenne cela, je l'utilisais nlpour n'importe quelle entrée, mais après avoir réalisé que cela nlpouvait fausser la sortie selon son -délimiteur par défaut, \:j'ai appris à être plus prudent avec lui et j'ai commencé à utiliser grep -nF ''à la place pour une entrée non testée. Mais une autre leçon apprise ce jour-là est que cela nlpeut être très utilement appliqué à d'autres égards - comme celui-ci - si vous modifiez juste un peu son entrée - comme je le fais sedci-dessus.

PRODUCTION

  line A
  line B

1       line X
2       line Y
3       line Z

  line C
  line D

1       line M
2       line N
3       line O

Voici un peu plus nl- remarquez-vous ci-dessus comment toutes les lignes sauf celles numérotées commencent par des espaces? Lorsque des nllignes numérotées, il insère un certain nombre de caractères dans la tête de chacun. Pour ces lignes, il ne numérote pas - même les blancs - il correspond toujours au retrait en insérant ( -wnombre d' -sidées + séparateur len) * des espaces en tête des lignes non numérotées. Cela vous permet de reproduire le contenu non numéroté exactement en le comparant au contenu numéroté - et avec peu d'effort. Lorsque vous considérez que nlcela divisera son entrée en sections logiques pour vous et que vous pouvez insérer des -schaînes arbitraires en tête de chaque ligne numérotée, il devient assez facile de gérer sa sortie:

sed 's/^@@.*start$/@@@@@@/
     s/^@@.*end/@@/; t
     s/^\(@@\)\{1,3\}$/& /' <infile |
nl -d@@ -ha -bn -s' do something with the next line!
'

Les impressions ci-dessus ...

                                        line A
                                        line B

 1 do something with the next line!
line X
 2 do something with the next line!
line Y
 3 do something with the next line!
line Z

                                        line C
                                        line D

 1 do something with the next line!
line M
 2 do something with the next line!
line N
 3 do something with the next line!
line O

GNOU sed

Si ce nln'est pas votre application cible, un GNU sedpeut eexécuter une commande shell arbitraire pour vous en fonction d'une correspondance.

sed '/^@@.*start$/!b
     s//nl <<\\@@/;:l;N
     s/\(\n@@\)[^\n]*end$/\1/
Tl;e'  <infile

Ci-dessus sedrecueille les données d'entrée dans l'espace de motif jusqu'à ce qu'il en ait assez pour réussir la substitution Tet arrêter le branching vers l' :label. Quand il le fait, il exécute nlavec une entrée représentée comme un <<document ici pour tout le reste de son espace de motif.

Le workflow est comme ceci:

  1. /^@@.*start$/!b
    • si un ^ ligne entière $ne correspond !pas au modèle ci-dessus, elle est retirée du script et imprimée automatiquement - donc à partir de ce moment, nous ne travaillons qu'avec une série de lignes commençant par le modèle.//b
  2. s//nl <<\\@@/
    • le vide s// champ/ correspond à la dernière adresse sedessayée de correspondre - donc cette commande remplace la @@.*startligne entière à la nl <<\\@@place.
  3. :l;N
    • La :commande définit une étiquette de branche - ici j'en ai défini une nommée :label. La Ncommande ext ajoute la ligne d'entrée suivante à l'espace de motif suivie d'un\n caractère de ligne électronique. C'est l'une des rares façons d'obtenir une ligne \nélectronique dans un sedespace de motif - le \ncaractère de ligne électronique est un délimiteur sûr pour un sedder qui le fait depuis un certain temps.
  4. s/\(\n@@\)[^\n]*end$/\1/
    • cette s///substitution ne peut être réussie qu'après un début et uniquement lors de la première occurrence suivante d'une ligne de fin . Il \nn'agira que sur un espace de motif dans lequel la dernière ligne électronique est immédiatement suivie en @@.*endmarquant la toute fin$ de l'espace de motif. Lorsqu'il agit, il remplace toute la chaîne correspondante par le \1premier \(groupe \), ou \n@@.
  5. Tl
    • la Tcommande est se branche sur une étiquette (si elle est fournie) si une substitution réussie ne s'est pas produite depuis la dernière fois qu'une ligne d'entrée a été tirée dans l'espace modèle (comme je le fais avec N) . Cela signifie que chaque fois qu'une ligne \nélectronique est ajoutée à un espace de modèle qui ne correspond pas à votre délimiteur de fin, la Tcommande est échoue et se ramifie vers l' :label, ce qui entraîne l' sedextraction de la Nligne ext et la boucle jusqu'à ce qu'elle réussisse.
  6. e

    • Lorsque la substitution de la correspondance de fin est réussie et que le script ne se ramifie pas pour un Test échoué , exécutera une commande qui ressemble à ceci:sedel

      nl <<\\@@\nline X\nline Y\nline Z\n@@$

Vous pouvez le constater par vous-même en modifiant la dernière ligne pour ressembler à Tl;l;e .

Il imprime:

line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D
     1  line M
     2  line N
     3  line O

while ... read

Une dernière façon de le faire, et peut-être la manière la plus simple, est d'utiliser une while readboucle, mais pour une bonne raison. Le shell - (plus particulièrement un bashshell) - est généralement assez épouvantable pour gérer les entrées en grandes quantités ou en flux réguliers. Cela a également du sens - le travail du shell est de gérer l'entrée caractère par caractère et d'appeler d'autres commandes qui peuvent gérer les choses plus importantes.

Mais ce qui est important à propos de son rôle, c'est que le shell ne doit pas read occuper une grande partie de l'entrée - il est spécifié de ne pas mettre en mémoire tampon d'entrée ou de sortie au point qu'il consomme tellement ou qu'il ne relaie pas suffisamment à temps que les commandes qu'il appelle sont laissées manquantes. - à l'octet. Fait donc readun excellent test d' entrée - pourreturn savoir s'il reste des entrées et vous devez appeler la prochaine commande pour la lire - mais ce n'est généralement pas la meilleure façon de procéder.

Voici un exemple, cependant, de la façon dont on pourrait utiliser read et d' autres commandes pour traiter l'entrée en synchronisation:

while   IFS= read -r line        &&
case    $line in (@@*start) :;;  (*)
        printf %s\\n "$line"
        sed -un "/^@@.*start$/q;p";;
esac;do sed -un "/^@@.*end$/q;=;p" |
        paste -d: - -
done    <infile

La première chose qui se produit pour chaque itération est de readtirer une ligne. Si elle réussit, cela signifie que la boucle n'a pas encore atteint EOF et que dans le cas caseelle correspond à un délimiteur de début, le dobloc est immédiatement exécuté. Sinon, printfimprime le$line on readet sedest appelé.

sedva primer chaque ligne jusqu'à ce qu'il rencontre le début marqueur - quand il quits entièrement entrée. Le -ucommutateur nbuffered est nécessaire pour GNU sedcar il peut tamponner plutôt avidement sinon, mais - selon les spécifications - les autres POSIX seddevraient fonctionner sans aucune considération particulière - tant qu'il <infiles'agit d'un fichier normal.

Lors de la première sed qsortie, le shell exécute le dobloc de la boucle - qui en appelle un autre sedqui imprime chaque ligne jusqu'à ce qu'il rencontre le marqueur de fin . Il dirige sa sortie verspaste , car il imprime chacun des numéros de ligne sur leur propre ligne. Comme ça:

1
line M
2
line N
3
line O

paste puis colle ceux ensemble sur : caractères, et la sortie entière ressemble à ceci:

line A
line B
1:line X
2:line Y
3:line Z
line C
line D
1:line M
2:line N
3:line O

Ce ne sont que des exemples - tout peut être fait dans le test ou dans les blocs ici, mais le premier utilitaire ne doit pas consommer trop d'entrée.

Tous les utilitaires impliqués lisent la même entrée - et impriment leurs résultats - chacun à leur tour. Ce genre de chose peut être difficile d'obtenir le blocage de - parce que différents utilitaires tampon plus que d' autres - mais vous pouvez généralement compter sur dd, headet sedde faire la bonne chose (bien que, pour GNU sed, vous avez besoin du cli-switch) et vous devriez toujours pouvoir compter read- car c'est, par nature, très lent . Et c'est pourquoi la boucle ci-dessus ne l'appelle qu'une seule fois par bloc d'entrée.

mikeserv
la source
J'ai testé le deuxième sedexemple que vous avez donné, et cela fonctionne, mais j'ai vraiment du mal à fouiller la syntaxe. (mon sed est assez faible et se limite généralement à s / findthis / replacethis / g. Je vais devoir faire un effort pour m'asseoir et vraiment comprendre sed.)
James Scriven
@JamesScriven - Je viens de modifier pour mieux l'expliquer. Faites-moi savoir si cela n'aide pas. J'ai également beaucoup changé la commande - elle est maintenant plus petite et plus sensible.
mikeserv
4

Une possibilité est de le faire avec l'éditeur de texte vim. Il peut diriger des sections arbitraires via des commandes shell.

Pour ce faire, vous pouvez utiliser les numéros de ligne en utilisant :4,6!nl . Cette commande ex s'exécutera nl sur les lignes 4-6 inclusivement, réalisant ce que vous voulez sur votre exemple d'entrée.

Un autre moyen plus interactif consiste à sélectionner les lignes appropriées à l'aide du mode de sélection de ligne (shift-V) et des touches fléchées ou à rechercher, puis à l'aide de :!nl. Une séquence de commandes complète pour votre exemple d'entrée pourrait être

/@@inline-code-start
jV/@@inline-code-end
k:!nl

Ce n'est pas très adapté à l'automatisation (les réponses utilisant par exemple sed sont mieux pour cela), mais pour les modifications ponctuelles, il est très utile de ne pas avoir recours à des scripts shell de 20 lignes.

Si vous n'êtes pas familier avec vi (m), vous devez au moins savoir qu'après ces modifications, vous pouvez enregistrer le fichier en utilisant :wq.

marcelm
la source
Oui, vim est génial! Mais je suis, dans ce cas, à la recherche d'une solution scriptable.
James Scriven
@JamesScriven, toute personne qui dit que vim n'est pas scriptable de manière insuffisamment déterminée. Créez d'abord un répertoire de projet et dans ce répertoire copiez tous les fichiers de démarrage de vim à partir de votre répertoire personnel (ln -s fonctionne très bien sauf pour .vimrc que nous sommes sur le point de modifier et .viminfo qui peut être rempli de bruit). Ajoutez la définition de fonction qui fera le travail dans le nouveau fichier .vimrc, puis appelez vim as HOME=$(pwd) vim -c 'call Mf()' f. Si vous utilisez xargs, vous souhaiterez peut-être utiliser gvim sur un serveur x dédié pour éviter de corrompre votre tty (vnc est indépendant de la carte vidéo et peut être surveillé).
hildred
@hildred Hmmm ... Ne pourrais-je pas simplement utiliser [XSendEvent] ( tronche.com/gui/x/xlib/event-handling/XSendEvent.html ) pour simuler les clics de souris vers vim?
James Scriven
2

La solution la plus simple à laquelle je peux penser est de ne pas utiliser nlmais de compter les lignes vous-même:

#!/usr/bin/env bash
while read line
do
    if [[ $line == @@inline-code-start* ]]
    then
        active=true
    elif [[ $line == @@inline-code-end* ]]
    then
        active=false
    elif [[ $active = true ]]
    then
        ## Count the line number
        let num++;
        printf "\t%s %s\n" "$num" "$line"
    else
        # output
        printf "%s\n" "$line"
    fi
done

Vous l'exécutez ensuite sur le fichier:

$ foo.sh < file
line A
line B
    1 line X
    2 line Y
    3 line Z
line C
line D
terdon
la source
Merci Terdon. J'ai mis à jour la question pour clarifier que je cherche une solution générique pour filtrer une sous-section d'une entrée, plutôt l'exemple spécifique de lignes de numérotation. peut-être un meilleur exemple de commande aurait été "tac" (lignes inversées)
James Scriven
2

Si votre objectif est d'envoyer le bloc de code entier à une seule instance de processus, vous pouvez accumuler les lignes et retarder la canalisation jusqu'à la fin du bloc de code:

#!/bin/bash

acc=""

while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
    acc=""
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
    # Act on entire block of code
    echo "${acc:1}" | nl  # Chops off first leading new-line character using ${VAR:1}
  elif [[ $active = true ]]
  then
    acc=$( printf "%s\n%s" "$acc" "$line" )
  else
    # output
    echo $line
  fi
done

Cela produit ce qui suit pour un fichier d'entrée qui répète le scénario de test trois fois:

line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D
line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D
line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D

Pour faire quelque chose d' autre avec le bloc de code, par exemple inverse, puis le numéro, juste l'envoie à autre chose: echo -E "${acc:1}" | tac | nl. Résultat:

line A
line B
     1  line Z
     2  line Y
     3  line X
line C
line D

Ou nombre de mots echo -E "${acc:1}" | wc:

line A
line B
      3       6      21
line C
line D
Supr
la source
2

Modifier a ajouté une option pour définir un filtre fourni par l'utilisateur

#!/usr/bin/perl -s
use IPC::Open2;
our $p;
$p = "nl" unless $p;    ## default filter

$/ = "\@\@inline-code-end\n";
while(<>) { 
   chomp;
   s/\@\@inline-code-start\n(.*)/pipeit($1,$p)/se;
   print;
}

sub pipeit{my($text,$pipe)=@_;
  open2(my $R, my $W,$pipe) || die("can open2");
  local $/ = undef;
  print $W $text;
  close $W;
  return <$R>;
}

Par défaut, le filtre est "nl". Pour modifier le filtre, utilisez l'option "-p" avec une commande fournie par l'utilisateur:

codify -p="wc" file

ou

codify -p="sed -e 's@^@ ║ @; 1s@^@ ╓─\n@; \$s@\$@\n ╙─@'" file

Ce dernier filtre affichera:

line A
line B
 ╓─
  line X
  line Y
  line Z
 ╙─
line C
line D

Mise à jour 1 L'utilisation d'IPC :: Open2 a des problèmes de mise à l'échelle: si la taille de la mémoire tampon est dépassée, elle peut se bloquer. (dans ma machine, la taille du tampon du tube si 64K correspond à 10_000 x "ligne Y").

Si nous avons besoin de plus grandes choses (si nous avons besoin de plus de 10000 "ligne Y"):

(1) installer et utiliser use Forks::Super 'open2';

(2) ou remplacer la fonction pipeit par:

sub pipeit{my($text,$pipe)=@_;
  open(F,">","/tmp/_$$");
  print F $text;
  close F;
  my $out = `$pipe < /tmp/_$$ `;
  unlink "/tmp/_$$";
  return $out;
}
JJoao
la source
C'est vraiment cool. Je suppose que les astuces sont que vous ne traitez pas ligne par ligne (en redéfinissant $/et le sdrapeau), et l'utilisation du edrapeau pour faire l'appel réel à la commande externe. J'aime vraiment le deuxième exemple (art ascii)!
James Scriven
Ce que j'ai remarqué cependant, c'est que cela ne semble pas s'étendre au-delà de quelques milliers de lignes dans la sous-section. Je suppose que cela a à voir avec le fait de traiter la sous-section comme un gros bloc de texte.
James Scriven
Merci. Oui: `/ e` = eval; /s= ("." signifie (.|\n)); $/redéfinit le séparateur de registre.
JJoao
@JamesScriven, vous avez raison (le tuyau bloque). Permettez-moi de tester ce qui se passe ...
JJoao
@JamesScriven, veuillez voir ma mise à jour ...
JJoao
1

C'est un travail pour awk.

#!/usr/bin/awk -f
$0 == "@@inline-code-start" {pipe = 1; next}
$0 == "@@inline-code-end" {pipe = 0; close("nl"); next}
pipe {print | "nl"}
!pipe {print}

Lorsque le script voit le marqueur de début, il note qu'il doit commencer à canaliser nl. Lorsque la pipevariable est vraie (non nulle), la sortie est dirigée vers la nlcommande; lorsque la variable est fausse (non définie ou nulle), la sortie est imprimée directement. La commande canalisée est bifurquée la première fois que la construction de tube est rencontrée pour chaque chaîne de commande. Les évaluations ultérieures de l'opérateur de tuyau avec la même chaîne réutilisent le tuyau existant; une valeur de chaîne différente créerait un tuyau différent. La closefonction ferme le canal pour la chaîne de commande donnée.


Il s'agit essentiellement de la même logique que votre script shell utilisant un canal nommé, mais beaucoup plus facile à énoncer et la logique de fermeture est bien exécutée. Vous devez fermer le tuyau au bon moment pour faire nlsortir la commande en vidant ses tampons. Votre script ferme en fait le tuyau trop tôt: le tuyau est fermé dès que le premier a echo $line >myfifoterminé son exécution. Cependant, la nlcommande ne voit la fin du fichier que si elle obtient une tranche de temps avant la prochaine exécution du script echo $line >myfifo. Si vous aviez un grand volume de données, ou si vous ajoutez sleep 1après avoir écrit dans myfifo, vous verrez que nlne traite que la première ligne ou le premier groupe rapide de lignes, puis il se ferme car il a vu la fin de son entrée.

En utilisant votre structure, vous devez garder le tuyau ouvert jusqu'à ce que vous n'en ayez plus besoin. Vous devez avoir une redirection de sortie unique dans le tuyau.

nl <myfifo &
exec 3>&1
while IFS= read -r line
do
  if [[ $line == @@inline-code-start* ]]
  then
    exec >myfifo
  elif [[ $line == @@inline-code-end* ]]
  then
    exec >&3
  else
    printf '%s\n' "$line"
  fi
done

(J'ai également profité de l'occasion pour ajouter des citations correctes et autres - voir Pourquoi mon script shell s'étouffe-t-il sur les espaces ou d'autres caractères spéciaux? )

Si vous faites cela, vous pouvez tout aussi bien utiliser un pipeline plutôt qu'un tube nommé.

while IFS= read -r line
do
  if [[ $line == @@inline-code-start* ]]
  then
    while IFS= read -r line && [[ $line != @@inline-code-end* ]] do
      printf '%s\n' "$line"
    done | nl
  else
    printf '%s\n' "$line"
  fi
done
Gilles 'SO- arrête d'être méchant'
la source
votre solution awk est vraiment sympa! Je pense que c'est de loin la solution la plus concise (mais très lisible). Est-ce que le comportement de l'awk de réutiliser le tuyau à nl est garanti, ou pourrait-il décider, "hé, vous en avez assez pour le moment ... Je vais fermer ce tuyau et en ouvrir un nouveau"?. Votre solution "pipeline" est également très agréable. J'ai à l'origine écarté une approche avec des boucles while intégrées, car je pensais que cela pouvait être un peu déroutant, mais je pense que ce que vous avez est génial. Il manque un point-virgule avant le do. (Je n'ai pas le représentant ici pour faire un petit montage.)
James Scriven
1
... Je n'ai pas pu faire fonctionner votre solution de tuyaux nommée. Il semble y avoir une condition de concurrence telle que la section canalisée vers nl se perd parfois complètement. De plus, ff il y a une deuxième section @@ inline-code-start / end, elle se perd toujours.
James Scriven
0

D'accord, tout d'abord; Je comprends que vous ne cherchez pas à numéroter les lignes dans les sections de votre dossier. Puisque vous n'avez pas donné d'exemple réel de ce que pourrait être votre filtre (autre que nl), supposons qu'il soit

tr "[[:lower:]]" "[[:upper:]]"

c'est-à-dire, convertir le texte en majuscules; donc, pour une entrée de

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D

vous voulez une sortie de

line A
line B
LINE X
LINE Y
LINE Z
line C
line D

Voici ma première approximation d'une solution:

#!/bin/sh
> file0
> file1
active=0
nl -ba "$@" | while IFS= read -r line
do
        case "$line" in
            ([\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9]"        @@inline-code-start")
                active=1
                ;;
            ([\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9]"        @@inline-code-end")
                active=0
                ;;
            (*)
                printf "%s\n" "$line" >> file$active
        esac
done
(cat file0; tr "[[:lower:]]" "[[:upper:]]" < file1) | sort | sed 's/^[ 0-9]\{6\}        //'

où les espaces avant les @@chaînes et près de la fin de la dernière ligne sont des tabulations. Veuillez noter que j'utilise nl à mes propres fins . (Bien sûr, je le fais pour résoudre votre problème, mais pas pour vous donner une sortie numérotée.)

Cela numérote les lignes de l'entrée afin que nous puissions la séparer au niveau des marqueurs de section et savoir comment la reconstituer plus tard. Le corps principal de la boucle est basé sur votre première tentative, en tenant compte du fait que les marqueurs de section portent des numéros de ligne. Il sépare l'entrée en deux fichiers: file0(inactif; pas dans une section) et file1(actif; dans une section). Voici à quoi ils ressemblent pour l'entrée ci-dessus:

file0:
     1  line A
     2  line B
     8  line C
     9  line D

file1:
     4  line X
     5  line Y
     6  line Z

Ensuite, nous courons file1(qui est la concaténation de tous les lignes de section) le filtre de capitalisation; combinez cela avec les lignes hors-section non filtrées; trier, pour les remettre dans leur ordre d'origine; puis supprimez les numéros de ligne. Cela produit la sortie affichée en haut de ma réponse.

Cela suppose que votre filtre laisse les numéros de ligne seuls. Si ce n'est pas le cas (par exemple, s'il insère ou supprime des caractères au début de la ligne), je pense que cette approche générale peut toujours être utilisée, mais nécessitera un codage légèrement plus délicat.

Scott
la source
nlfait déjà la plupart du travail là-bas - c'est à cela que -dsert son option d'élimitation.
mikeserv
0

Un script shell qui utilise sed pour sortir des morceaux de lignes non délimitées et introduire des morceaux de lignes délimités dans un programme de filtrage:

#!/bin/bash

usage(){
    echo "  usage: $0 <input file>"
}

# Check input file
if [ ! -f "$1" ]; then
    usage
    exit 1
fi

# Program to use for filtering
# e.g. FILTER='tr X -'
FILTER='./filter.sh'

# Generate arrays with starting/ending line numbers of demarcators
startposs=($(grep -n '^@@inline-code-start$' "$1" | cut -d: -f1))
endposs=($(grep -n '^@@inline-code-end$' "$1" | cut -d: -f1))

nums=${#startposs[*]}
nume=${#endposs[*]}

# Verify both line number arrays have the same number of elements
if (($nums != $nume)); then
    echo "Tag mismatch"
    exit 2
fi

lastline=1
i=0
while ((i < nums)); do
    # Exclude lines with code demarcators
    sprev=$((${startposs[$i]} - 1))
    snext=$((${startposs[$i]} + 1))
    eprev=$((${endposs[$i]} - 1))

    # Don't run this bit if the first demarcator is on the first line
    if ((sprev > 1)); then
        # Output lines leading up to start demarcator
        sed -n "${lastline},${sprev} p" "$1"
    fi

    # Filter lines between demarcators
    sed -n "${snext},${eprev} p" "$1" | $FILTER

    lastline=$((${endposs[$i]} + 1))
    let i++
done

# Output lines (if any) following last demarcator
sed -n "${lastline},$ p" "$1"

J'ai écrit ce script dans un fichier nommé detagger.sh et l'ai utilisé comme suit: ./detagger.sh infile.txt . J'ai créé un fichier filter.sh distinct pour imiter la fonctionnalité de filtrage dans la question:

#!/bin/bash
awk '{ print "\t" NR " " $0}'

Mais l'opération de filtrage peut être modifiée dans le code.

J'ai essayé de suivre l'idée d'une solution générique avec ceci afin que les opérations comme les lignes de numérotation ne nécessitent pas de comptage supplémentaire / interne. Le script effectue une vérification rudimentaire pour voir que les balises de démarcation sont en paires et ne gère pas du tout les balises imbriquées avec élégance.

Caca
la source
-1

Merci pour toutes ces bonnes idées. J'ai trouvé ma propre solution en gardant une trace de la sous-section dans un fichier temporaire et en la canalisant à la fois vers ma commande externe. C'est très similaire à ce que Supr a suggéré (mais avec une variable shell au lieu d'un fichier temporaire). De plus, j'aime vraiment l'idée d'utiliser sed, mais la syntaxe de ce cas semble un peu exagérée pour moi.

Ma solution:

(J'utilise nljuste comme exemple de filtre)

#!/usr/bin/bash

while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
    tmpfile=$(mktemp)
    trap "rm -f $tmpfile" EXIT
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
    <$tmpfile nl
    rm $tmpfile
  elif [[ $active = true ]]
  then
    echo $line >> $tmpfile
  else
    echo $line
  fi
done

Je préférerais ne pas avoir à gérer la gestion des fichiers temporaires, mais je comprends que les variables shell peuvent avoir des limites de taille assez faibles, et je ne connais aucune construction bash qui fonctionnerait comme un fichier temporaire, mais disparaîtrait automatiquement lorsque le le processus se termine.

James Scriven
la source
Je pensais que vous vouliez être en mesure "d'accumuler de l'état sur plusieurs lignes", donc, par exemple, en utilisant les données de test, les lignes M,N etO est numéroté 4, 5et 6. Cela ne fait pas ça. Ma réponse le fait (à part le fait que, dans son incarnation actuelle, il ne fonctionne pas nlcomme un filtre). Si cette réponse vous donne la sortie que vous voulez, alors qu'entendez-vous par «accumuler l'état sur plusieurs lignes»? Vouliez-vous dire que vous vouliez conserver l'état uniquement à travers chaque section, mais pas entre (entre) sections? (Pourquoi n'avez-vous pas mis un exemple en plusieurs sections dans votre question?)
Scott
@Scott - utilisez nl -ppour obtenir M,N,O==4,5,6.
mikeserv
J'ai mis à jour la question pour préciser que je suis seulement intéressé à maintenir l'état dans la sous-section, bien que je pense que l'autre interprétation soit tout aussi intéressante.
James Scriven