Ajouter la dernière ligne de stdin à stdin entier

9

Considérez ce script:

tmpfile=$(mktemp)

cat <<EOS > "$tmpfile"
line 1
line 2
line 3
EOS

cat <(tail -1 "$tmpfile") "$tmpfile"

Cela fonctionne et génère:

line 3
line 1
line 2
line 3

Disons que notre source d'entrée, plutôt que d'être un fichier réel, était plutôt stdin:

cat <<EOS | # what goes here now?
line 1
line 2
line 3
EOS

Comment modifions-nous la commande:

cat <(tail -1 "$tmpfile") "$tmpfile"

Alors qu'il produit toujours la même sortie, dans ce contexte différent?

REMARQUE: Le Heredoc spécifique que je raconte, ainsi que l'utilisation d'un Heredoc lui-même, sont simplement illustratifs. Toute réponse acceptable doit supposer qu'elle reçoit des données arbitraires via stdin .

Jonas
la source
1
stdin est toujours un "fichier réel" (un fifo / socket / etc est également un fichier; tous les fichiers ne sont pas recherchables). La réponse à votre question est soit une simple utilisation d'un fichier temporaire, soit une certaine horreur qui chargera le fichier entier dans la mémoire. "Comment puis-je récupérer les anciennes données d'un flux sans les avoir stockées n'importe où ?" ne peut pas avoir une bonne réponse.
mosvy
1
@mosvy C'est une réponse parfaitement acceptable si vous souhaitez l'ajouter.
Jonah
2
@mosvy Comme Jonah l'a dit, les réponses doivent être publiées dans la boîte de réponse. Je sais qu'il est difficile de lire le site Web pour le moment, mais veuillez ignorer le rouge qui dégouline lentement sur votre vision et utilisez la zone de texte inférieure.
wizzwizz4

Réponses:

7

Essayer:

awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'

Exemple

Définissez une variable avec notre entrée:

$ input="line 1
> line 2
> line 3"

Exécutez notre commande:

$ echo "$input" | awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'
line 3
line 1
line 2
line 3

Alternativement, bien sûr, nous pourrions utiliser un ici-doc:

$ cat <<EOS | awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'
line 1
line 2
line 3
EOS
line 3
line 1
line 2
line 3

Comment ça fonctionne

  • x=x $0 ORS

    Cela ajoute chaque ligne d'entrée à la variable x.

    Dans awk, ORSest le séparateur d'enregistrement de sortie . Par défaut, il s'agit d'un caractère de nouvelle ligne.

  • END{printf "%s", $0 ORS x}

    Après la que nous avons lu dans le fichier entier, ce imprime la dernière ligne, $0, suivi par le contenu du fichier entier, x.

Puisqu'il lit l'intégralité de l'entrée en mémoire, il ne conviendrait pas pour les entrées volumineuses ( par exemple, gigaoctets).

John1024
la source
Merci John. N'est-il donc pas possible de le faire d'une manière analogue à mon exemple de fichier nommé dans l'OP? J'imaginais que le stdin était dupliqué d'une manière ou d'une autre ... en quelque sorte tee, mais d'un stdin et d'un fichier, nous serions en train de canaliser le même stdin en deux substitutions de processus différentes. ou quelque chose qui serait à peu près équivalent à cela?
Jonah
5

Si stdin pointe vers un fichier pouvant être recherché (comme dans le cas des documents bash (mais pas tous les autres shell) ici qui sont implémentés avec des fichiers temporaires), vous pouvez obtenir la queue puis rechercher avant de lire le contenu complet:

les opérateurs de recherche sont disponibles dans les shells zshou ksh93, ou dans des langages de script comme tcl / perl / python, mais pas dans bash. Mais vous pouvez toujours appeler ces interprètes plus avancés bashsi vous devez utiliser bash.

ksh93 -c 'tail -n1; cat <#((0))' <<...

Ou

zsh -c 'zmodload zsh/system; tail -n1; sysseek 0; cat' <<...

Maintenant, cela ne fonctionnera pas lorsque stdin pointe vers des fichiers non recherchables comme un tuyau ou une socket. Ensuite, la seule option est de lire et de stocker (en mémoire ou dans un fichier temporaire ...) l'ensemble de l'entrée.

Certaines solutions de stockage en mémoire ont déjà été proposées.

Avec un fichier temporaire, avec zsh, vous pouvez le faire avec:

seq 10 | zsh -c '{ cat =(sed \$w/dev/fd/3); } 3>&1'

Si sous Linux, avec bashou zshou n'importe quel shell qui utilise des fichiers temporaires pour les documents ici, vous pouvez réellement utiliser le fichier temporaire créé par un document ici pour stocker la sortie:

seq 10 | {
  chmod u+w /dev/fd/3 # only needed in bash5+
  cat > /dev/fd/3
  tail -n1 /dev/fd/3
  cat <&3
} 3<<EOF
EOF
Stéphane Chazelas
la source
4
cat <<EOS | sed -ne '1{h;d;}' -e 'H;${G;p;}'
line 1
line 2
line 3
EOS

Le problème avec la traduction de ceci en quelque chose qui utilise tailest qu'il tailfaut lire le fichier entier pour en trouver la fin. Pour utiliser cela dans votre pipeline, vous devez

  1. Fournissez le contenu complet du document à tail.
  2. Fournir à nouveau à cat.
  3. Dans cet ordre.

L'astuce n'est pas de dupliquer le contenu du document ( teefait cela) mais de faire en sorte que la sortie de tailse produise avant la sortie du reste du document, sans utiliser de fichier temporaire intermédiaire.

L'utilisation sed(ou awk, comme le fait John1024 ) supprime la double analyse des données et le problème de commande en stockant les données en mémoire.

La sedsolution que je propose est de

  1. 1{h;d;}, stockez la première ligne dans l'espace d'attente, telle quelle, et passez à la ligne suivante.
  2. H, ajoutez-vous une ligne à l'espace d'attente avec une nouvelle ligne incorporée.
  3. ${G;p;}, ajoutez l'espace d'attente à la dernière ligne avec une nouvelle ligne incorporée et imprimez les données résultantes.

Il s'agit d'une traduction assez littérale de la solution de John1024 sed, avec la mise en garde que la norme POSIX garantit uniquement que l'espace de blocage est d'au moins 8192 octets (8 Ko), mais il recommande que ce tampon soit alloué dynamiquement et étendu selon les besoins, ce que GNU tous les deux sedet BSD sedfait).


Si vous vous autorisez à utiliser un canal nommé:

mkfifo mypipe
cat <<EOS | tee mypipe | cat <( tail -n 1 mypipe ) -
line 1
line 2
line 3
EOS
rm -f mypipe

Cela permet teed'envoyer les données vers le bas mypipeet en même temps vers cat. L' catutilitaire lira d'abord la sortie de tail(qui lit mypipe, qui teeécrit), puis ajoutera la copie du document provenant directement de tee.

Il y a cependant un sérieux défaut, dans le cas où si le document est trop volumineux (plus grand que la taille de la mémoire tampon du tube), il teeécrit mypipeet catse bloquerait en attendant que le tube (sans nom) se vide. Il ne serait pas vidé avant d'être catlu. catne le lirait pas avant d' tailavoir terminé. Et tailne finirait pas avant d' teeavoir fini. Il s'agit d'une situation de blocage classique.

La variation

tee >( tail -n 1 >mypipe ) | cat mypipe -

a le même problème.

Kusalananda
la source
2
Celui- sedci ne fonctionne pas si l'entrée n'a qu'une seule ligne (peut-être sed '1h;1!H;$!d;G'). Notez également que plusieurs sedimplémentations ont une limite basse sur la taille de leur modèle et de l'espace de stockage.
Stéphane Chazelas
La solution de tuyau nommée est le genre de chose que je cherchais. La limitation est dommage. J'ai compris votre explication à l'exception de «Et la queue ne finirait pas tant que le tee n'aurait pas fini» - pourriez-vous expliquer pourquoi c'est le cas?
Jonah
2

Il existe un outil nommé peedans une collection d'utilitaires de ligne de commande généralement fournis avec le nom "moreutils" (ou autrement récupérable sur son site Web ).

Si vous pouvez l'avoir sur votre système, l'équivalent pour votre exemple serait comme:

cat <<EOS | pee 'tail -1' cat 
line 1
line 2
line 3
EOS

L'ordre des commandes exécutées peeest important car elles sont exécutées dans l'ordre indiqué.

LL3
la source
1

Essayer:

cat <<EOS # | what goes here now? Nothing!
line 3
line 1
line 2
line 3
EOS

Étant donné que le tout est constitué de données littérales (un "document ici"), et que la différence entre celles-ci et la sortie souhaitée est triviale, il suffit de masser ces données littérales juste là pour correspondre à la sortie.

Supposons maintenant que cela line 3vienne de quelque part et soit stocké dans une variable appelée lastline:

cat <<EOS # | what goes here now? Nothing!
$lastline
line 1
line 2
$lastline
EOS

Dans un doc ici, nous pouvons générer du texte en substituant des variables. Non seulement cela, mais nous pouvons calculer du texte en utilisant la substitution de commandes:

cat <<EOS
this is template text
here we have a hex conversion: $(printf "%x" 42)
EOS

On peut interpoler plusieurs lignes:

cat <<EOS
multi line
preamble
$(for x in 3 1 2 3; do echo line $x ; done)
epilog
EOS

En général, évitez de traiter le texte du modèle de doc ici; essayez de le générer en utilisant du code interpolé.

Kaz
la source
1
Honnêtement, je ne peux pas dire si c'est une blague ou non. Dans cat <<EOS...le PO n'était qu'un exemple de standin pour "attraper un fichier arbitraire", pour rendre le post spécifique et la question claire. N'était-ce pas vraiment évident pour vous, ou pensiez-vous simplement qu'il serait intelligent d'interpréter la question littéralement?
Jonah
@Jonah La question dit clairement "[l] et's disent que notre source d'entrée, plutôt que d'être un fichier réel, était plutôt stdin:". Rien sur les "fichiers arbitraires"; il s'agit ici de documents. Un doc ici n'est pas arbitraire. Ce n'est pas une entrée pour votre programme, mais un morceau de sa syntaxe que le programmeur choisit.
Kaz
1
Je pense que le contexte et les réponses existantes ont clairement montré que c'était le cas, ne serait-ce que parce que pour que votre interprétation soit correcte, vous deviez littéralement supposer que ni moi ni aucune des autres affiches qui ont répondu ont réalisé qu'il était possible de copier et coller un ligne de code. Néanmoins, je modifierai la question pour la rendre explicite.
Jonah
1
Kaz, merci pour la réponse, mais notez que même avec votre montage, l'intention de la question vous manque. Vous recevez une entrée multiligne arbitraire via un canal . Vous n'avez aucune idée de ce que ce sera. Votre tâche consiste à sortir la dernière ligne d'entrée, suivie de l'entrée entière.
Jonah
1
Kaz, l'entrée n'est là qu'à titre d'exemple. La plupart des gens, y compris moi-même, trouvent utile d'avoir un exemple d'entrée réelle et de sortie attendue, plutôt que seulement la question abstraite. Vous êtes le seul à être troublé par cela.
Jonah
0

Si vous ne vous souciez pas de la commande. Ensuite, cela fonctionnera cat lines | tee >(tail -1). Comme d'autres l'ont dit. Vous devez lire le fichier deux fois, ou mettre en mémoire tampon le fichier entier, pour le faire dans l'ordre que vous avez demandé.

ctrl-alt-delor
la source