Shell-script leest laatste regel ontbreekt

Ik heb een … vreemd probleem met een bash-shellscript waarvan ik hoopte enig inzicht te krijgen.

Mijn team werkt aan een script dat de regels in een bestand doorloopt en in elk bestand controleert op inhoud. We hadden een bug waarbij, wanneer uitgevoerd via het geautomatiseerde proces dat verschillende scripts aan elkaar koppelt, de laatste regel niet werd gezien.

De code die werd gebruikt om de regels in het bestand te herhalen (naam opgeslagen in DATAFILEwas

cat "$DATAFILE" | while read line 

We zouden het script vanaf de opdrachtregel kunnen uitvoeren en het zou elke regel in het bestand zien, inclusief de laatste, prima. Wanneer het echter wordt uitgevoerd door het geautomatiseerde proces (dat het script uitvoert dat het DATAFILE genereert net voor het betreffende script), wordt de laatste regel nooit gezien.

We hebben de code bijgewerkt om het volgende te gebruiken om de regels te herhalen, en het probleem is verholpen:

for line in `cat "$DATAFILE"` 

Opmerking: DATAFILE heeft nooit een nieuwe regel aan het einde van het bestand geschreven.

Mijn vraag bestaat uit twee delen… Waarom zou de laatste regel niet worden gezien door de originele code, en waarom zou deze verandering een verschil maken?

Ik dacht alleen dat ik kon bedenken waarom de laatste regel niet zou worden gezien:

  • Het vorige proces, dat het bestand schrijft, vertrouwde op het beëindigen van het proces om de bestandsdescriptor te sluiten.
  • Het probleemscript startte en opende het bestand eerder zo snel dat, terwijl het vorige proces was “beëindigd”, het niet voldoende was “afgesloten/opgeruimd” zodat het systeem de bestandsdescriptor er automatisch voor kon sluiten .

Dat gezegd hebbende, lijkt het alsof, als je 2 commando’s in een shellscript hebt, de eerste volledig moet zijn afgesloten tegen de tijd dat het script de tweede uitvoert.

Enig inzicht in de vragen, vooral de eerste, wordt zeer op prijs gesteld.


Antwoord 1, autoriteit 100%

De C-standaard zegt dat tekstbestanden moeten eindigen met een nieuwe regel, anders worden de gegevens na de laatste nieuwe regel mogelijk niet goed gelezen.

ISO/IEC 9899:2011 §7.21.2 Streams

Een tekststroom is een geordende reeks karakters samengesteld in regels, elke regel
bestaande uit nul of meer tekens plus een eindigend teken voor een nieuwe regel. Of de
laatste regel vereist een eindigend teken van een nieuwe regel is door de implementatie gedefinieerd. karakters
moet mogelijk worden toegevoegd, gewijzigd of verwijderd op invoer en uitvoer om te voldoen aan verschillende
conventies voor het weergeven van tekst in de hostomgeving. Er hoeft dus niet één op één
één correspondentie tussen de karakters in een stream en die in de externe
vertegenwoordiging. Gegevens die uit een tekststroom worden ingelezen, zijn noodzakelijkerwijs gelijk aan de gegevens
die eerder alleen naar die stream zijn weggeschreven als: de gegevens alleen uit afdrukken bestaan
tekens en de controletekens horizontale tab en nieuwe regel; geen teken van een nieuwe regel is
onmiddellijk voorafgegaan door spatietekens; en het laatste teken is een teken voor een nieuwe regel.
Of spatietekens die direct voor een teken van een nieuwe regel worden weggeschreven
verschijnen wanneer het inlezen door de implementatie is gedefinieerd.

Ik had niet verwacht dat een ontbrekende nieuwe regel aan het einde van het bestand problemen zou veroorzaken in bash(of een Unix-shell), maar dat lijkt het probleem reproduceerbaar te zijn ($ is de prompt in deze uitvoer):

$ echo xxx\\c
xxx$ { echo abc; echo def; echo ghi; echo xxx\\c; } > y
$ cat y
abc
def
ghi
xxx$
$ while read line; do echo $line; done < y
abc
def
ghi
$ bash -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ ksh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ zsh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ for line in $(<y); do echo $line; done      # Preferred notation in bash
abc
def
ghi
xxx
$ for line in $(cat y); do echo $line; done   # UUOC Award pending
abc
def
ghi
xxx
$

Het is ook niet beperkt tot bash– Korn Shell (ksh) en zshGedraag je ook zo. Ik woon, ik leer; Bedankt voor het verhogen van het probleem.

Zoals aangetoond in de bovenstaande code, leest de opdracht cathet hele bestand. De for line in `cat $DATAFILE` -techniek verzamelt alle uitvoer en vervangt willekeurige reeksen witte ruimte met een enkele blanco (ik concludeer dat elke regel in het bestand geen lege plekken bevat).

Getest op Mac OS X 10.7.5.


Wat zegt Posix?

De POSIX readopdrachtspecificatie zegt:

Het leeshulpprogramma las een enkele regel van standaardinvoer.

Standaard, tenzij de optie -ris opgegeven, & LT; Backslash & GT; zal optreden als een ontsnappingspercentage. Een ongecaped & lt; backslash & gt; zal de letterlijke waarde van het volgende karakter behouden, met uitzondering van A & LT; Newline & GT;. Als A & LT; Newline & GT; Volgt de & lt; backslash & gt;, het leeshulpprogramma interpreteert dit als lijnvoortzetting. The & LT; Backslash & GT; en <newline>wordt verwijderd voordat de invoer in velden wordt bespot. Alle andere ongecaped & lt; backslash & gt; tekens moeten worden verwijderd nadat de invoer in velden wordt bespot.

Indien standaardinvoer een eindapparaat is en de aanroepende schaal interactief is, wordt gelezen om een ​​vervolglijn wanneer het een ingangslijn wordt beëindigd die eindigt met A & LT; Backslash & GT; & LT; Newline & GT;, tenzij de optie -ris opgegeven.

De terminating & lt; newline & gt; (indien aanwezig) wordt uit de ingang verwijderd en moeten de resultaten worden opgesplitst in velden zoals in de schaal voor de resultaten van parameteruitbreiding (zie veldsplitsing); […]

Merk op dat ‘(indien aanwezig)’ (nadruk toegevoegd in quote)! Het lijkt mij dat als er geen nieuwe lijn is, het resultaat nog steeds het resultaat zou moeten lezen. Aan de andere kant zegt het ook:

stdin

De standaardinvoer is een tekstbestand.

En dan kom je terug naar het debat over de vraag of een bestand dat niet eindigt met een nieuwlijn een tekstbestand is of niet.

De redenering op dezelfde pagina-documenten:

Hoewel de standaardinvoer vereist is om een ​​tekstbestand te zijn en zal daarom altijd eindigen met A & LT; Newline & GT; (Tenzij het een leeg bestand is), kan de verwerking van vervolgleidingen wanneer de optie -rniet wordt gebruikt, het gevolg is van de ingang die niet eindigt met A & LT; Newline & GT;. Dit gebeurt als de laatste regel van het invoerbestand eindigt met A & LT; Backslash & GT; & LT; Newline & GT;. Het is om deze reden dat “indien aanwezig” wordt gebruikt in “de terminating & lt; newline & gt; (indien aanwezig), uit de ingang worden verwijderd” in de beschrijving. Het is geen ontspanning van de vereiste voor standaardinvoer om een ​​tekstbestand te zijn.

Die reden moet betekenen dat het tekstbestand moet eindigen met een nieuwe lijn.

De POSIX-definitie van een tekstbestand is:

3.395 tekstbestand

Een bestand dat tekens bevat die worden georganiseerd in nul of meer lijnen. De regels bevatten geen NUL-tekens en geen kan overschrijden {line_max} bytes in lengte, inclusief de & lt; Newline & GT; karakter. Hoewel Posix.1-2008 geen onderscheid maakt tussen tekstbestanden en binaire bestanden (zie de ISO C-standaard), produceren vele hulpprogramma’s alleen voorspelbare of zinvolle output bij gebruik op tekstbestanden. De standaardhulpprogramma’s die dergelijke beperkingen hebben, specificeren altijd “tekstbestanden” in de secties van de stdin- of invoerbestanden.

Dit bepaalt niet ‘eindigt met a & lt; newline & gt;’ Direct, maar wordt uitgesteld aan de C-standaard en het zegt wel “Een bestand dat tekens bevat die is georganiseerd in nul of meer lijnen ” en wanneer we kijken naar de POSIX-definitie van een “lijn”, zegt het:

3.206 lijn

een sequentie van nul of meer niet- en lt; newline & GT; tekens plus een
Terminating & LT; Newline & GT; karakter.

dus volgens de POSIX-definitie moet een bestand eindigen op een eindigende nieuwe regel omdat het uit regels bestaat en elke regel moet eindigen op een eindigende nieuwe regel.


Een oplossing voor het ‘no terminal newline’-probleem

Opmerking Gordon Davisson‘s antwoord. Een eenvoudige test toont aan dat zijn waarneming nauwkeurig is:

$ while read line; do echo $line; done < y; echo $line
abc
def
ghi
xxx
$

Daarom is zijn techniek van:

while read line || [ -n "$line" ]; do echo $line; done < y

of:

cat y | while read line || [ -n "$line" ]; do echo $line; done

werkt voor bestanden zonder een nieuwe regel aan het einde (tenminste op mijn computer).


Het verbaast me nog steeds dat de shells het laatste segment (het kan geen regel worden genoemd omdat het niet op een nieuwe regel eindigt) van de invoer laten vallen, maar er kan in POSIX voldoende rechtvaardiging zijn om dit te doen dus. En het is duidelijk dat het het beste is om ervoor te zorgen dat uw tekstbestanden echt tekstbestanden zijn die eindigen op een nieuwe regel.


Antwoord 2, autoriteit 73%

Volgens de POSIX-specificatie voor het leescommandozou het moeten retourneer een niet-nulstatus als “End-of-file werd gedetecteerd of er een fout is opgetreden.” Aangezien EOF wordt gedetecteerd terwijl het de laatste “regel” leest, stelt het $linein en retourneert vervolgens een foutstatus, en de foutstatus verhindert dat de lus wordt uitgevoerd op die laatste “regel”. De oplossing is eenvoudig: laat de lus uitvoeren als het leescommando slaagt OF als er iets is ingelezen in $line.

while read line || [ -n "$line" ]; do

Antwoord 3, autoriteit 27%

Wat extra informatie toevoegen:

  1. Het is niet nodig om catte gebruiken met een while-lus. while ...;do something;done<filegenoeg is.
  2. Lees geen regels met for.

Bij gebruik van while-lus om regels te lezen:

  1. Stel de IFScorrect in (anders kunt u de inspringing kwijtraken).
  2. Je zou bijna altijd de -r optie met read moeten gebruiken.

als aan de bovenstaande vereisten wordt voldaan, ziet een goede while-lus er als volgt uit:

while IFS= read -r line; do
  ...
done <file

En om het te laten werken met bestanden zonder een nieuwe regel aan het einde (ik plaats mijn oplossing hier):

while IFS= read -r line || [ -n "$line" ]; do
  echo "$line"
done <file

Of gebruik grepmet while-lus:

while IFS= read -r line; do
  echo "$line"
done < <(grep "" file)

Antwoord 4, autoriteit 2%

Als tijdelijke oplossing kan een nieuwe regel aan het bestand worden toegevoegd voordat er uit het tekstbestand wordt gelezen.

echo -e "\n" >> $file_path

Dit zorgt ervoor dat alle regels die eerder in het bestand stonden, worden gelezen. We moeten het argument -e doorgeven aan echo om interpretatie van escape-reeksen mogelijk te maken.
https://superuser.com/questions/313938/shell-script-echo- nieuwe-regel-naar-bestand


Antwoord 5

Ik heb dit getest in de opdrachtregel

# create dummy file. last line doesn't end with newline
printf "%i\n%i\nNo-newline-here" >testing

Test met je eerste vorm (piping naar while-loop)

cat testing | while read line; do echo $line; done

Hiermee mist de laatste regel, wat logisch is aangezien readalleen invoer krijgt die eindigt op een nieuwe regel.


Test met uw tweede formulier (opdrachtvervanging)

for line in `cat testbed1` ; do echo $line; done

Dit krijgt ook de laatste regel


readkrijgt alleen invoer als het wordt beëindigd door een nieuwe regel, daarom mis je de laatste regel.

Aan de andere kant, in de tweede vorm

`cat testing` 

wordt uitgevouwen in de vorm van

line1\nline2\n...lineM 

die door de shell wordt gescheiden in meerdere velden met behulp van IFS, dus je krijgt

line1 line2 line3 ... lineM 

Daarom krijg je nog steeds de laatste regel.

p/s: Wat ik niet begrijp is hoe je het eerste formulier werkend krijgt…


Antwoord 6

Gebruik sed om de laatste regel van een bestand te matchen, waarna een nieuwe regel wordt toegevoegd als die niet bestaat en laat het een inline vervanging van het bestand uitvoeren:

sed -i '' -e '$a\' file

De code is van deze stackexchange link

Opmerking: ik heb lege enkele aanhalingstekens toegevoegd aan -i ''omdat, tenminste in OS X, -i-eals bestandsextensie voor het back-upbestand. Ik had graag gereageerd op het originele bericht, maar miste 50 punten. Misschien levert dit me wat op in deze thread, bedankt.


Antwoord 7

Ik had een soortgelijk probleem.
Ik was bezig met een kat van een bestand, het naar een soort doorsluizen en het resultaat vervolgens doorsturen naar een ‘terwijl gelezen var1 var2 var3’.
d.w.z:
cat $FILE|sort -k3|tijdens lezen Count IP-naam
doen

Het werk onder de “do” was een if-statement dat veranderende gegevens in het $Name-veld identificeerde en op basis van verandering of geen verandering sommen van $Count deed of de gesommeerde regel naar het rapport drukte.
Ik kwam ook het probleem tegen waarbij ik de laatste regel niet kon krijgen om naar het rapport af te drukken.
Ik ging met het eenvoudige hulpmiddel om de kat/sort naar een nieuw bestand om te leiden, een nieuwe regel naar dat nieuwe bestand te herhalen en DAARNA voerde ik mijn “terwijl het lezen Count IP Name” op het nieuwe bestand uit met succesvolle resultaten.
d.w.z:
cat $FILE|sort -k3 > NIEUW BESTAND
echo “\n” >> NIEUW BESTAND
cat NEWFILE |tijdens het lezen IP-naam tellen
doen

Soms is de simpele, onelegante de beste manier om te gaan.

Other episodes