Textové súbory
Výpis textového súboru
Vypíšte obsah textového súboru na štandardný výstup. Predpokladajte, že každý riadok má dĺžku maximálne 128 znakov. Názov súboru nech je zadrôtovaný v súbore cez
#define
.
Riadkovo-orientované súbory načítavajme cez fgets()
. Tá potrebuje:
- reťazec (buffer), do ktorého načíta znaky
- počet znakov, ktorý sa má načítať
- vstupný súbor
Príklad:
fgets(riadok, 128, subor);
Načítavanie skončí, ak:
- načíta sa 127 znakov,
- alebo sa načíta znak konca riadka
\n
, ktorý sa zapíše do buffera - alebo sa naďabí na koniec súboru.
Funkcia potom ukončí reťazec znakom \0
.
Funkcia vráti NULL
, ak nastane chyba, alebo sa narazí na koniec súboru.
Jednoduchý príklad:
#include<stdio.h>
#define SUBOR "/etc/passwd"
int main()
{
FILE * subor;
char buf[128];
subor = fopen(SUBOR, "r");
if(subor == NULL) {
perror("Subor " SUBOR " sa nenasiel");
return -1;
}
while(fgets(buf, sizeof(buf), subor) != NULL) {
puts(buf);
}
fclose(subor);
return 0;
}
Výsledkom bude:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/false
bin:x:2:2:bin:/bin:/bin/false
sys:x:3:3:sys:/dev:/bin/false
teda očakávané riadky… ibaže medzi každým z nich bude medzera. Problém je spôsobený dvojitým znakom konca riadka \n
˙:
- jeden koniec riadka sa nachádza v reťazci v bufferi:
fscanf()
totiž načítava riadok vrátane koncového znaku - a druhý koniec riadka sa vo výsledku objaví vďaka
puts()
, ktoré svoj parameter vytlačí a ukončí ho znakom\n
. V Jave by to bola analógia príkazuSystem.out.println("root:x:0:0:root:/root:/bin/bash")
.
Namiesto puts(buf)
tak môžeme skúsiť aj analógiu
printf("%s", buf);
Pozor! Nikdy netlačte reťazec cez
printf(buf);
Prvý parameter funkcie printf()
je formátovací reťazec. Ak sa premenná buf
stane formátovacím reťazcom, a navyše jej obsah príde zvonku (= zo súboru), niekto ju môže zneužiť.
Napríklad uvedie do súboru jediný riadok s názvom %s
.
V tom prípade bude printf()
čakať ďalší argument a ak ho nedostane, program spadne.
Výpis /etc/passwd
Zo linuxového súboru s údajmi o používateľoch
/etc/passwd
vypíšte len tie riadky, ktoré patria používateľovi so zadaným UID.
Súbor obsahuje položky oddelené dvojbodkami
* login používateľa (maximálne 32 znakov) * indikátor uloženia hesla v externom súbore /etc/shadow. Vždy má hodnotu x. * ID používateľa (UID), jednoznačné vzhľadom na celý súbor. Prirodzené číslo. * ID skupiny používateľa (GID). Prirodzené číslo. * položku Gecos s textovým komentárom o používateľovi. Obvykle obsahuje podpoložky oddelené čiarkami, zväčša s kontaktnými informáciami o používateľovi * cestu k domovskému adresáru * program, ktorý sa má spustiť po prihlásení do systému: typicky je ním niektorý shell (napr.bash
)
Riešenie
- položiek je veľa, ale zaujímajú nás len prvá (login) a tretia (UID).
- predpokladajme zatiaľ, že aj UID je reťazec
- využime funkciu
strtok()
, ktorá tokenizuje reťazec, teda rozsekáva ho na podreťazce (tokeny) podľa špecifikovaného oddeľovača:- prvý parameter je typu
char *
, teda reťazec, ktorý má byť rozdeľovaný - druhý parameter je typu
char *
, obsahuje oddeľovače, ktorými sa bude reťazec deliť - funkcia vracia
char *
, čo možno chápať ako reťazec s príslušnou podpoložkou (teda token)- nezabúdame, že reťazec je pole znakov ukončené
\0
a teda ekvivalentné s pointerom na prvý znak.
- nezabúdame, že reťazec je pole znakov ukončené
- prvý parameter je typu
strtok()
sa používa v dvoch fázach:- vo 1. fáze zadáme reťazec, ktorý sa má rozsekať, a vráti sa prvý token
- v 2. fáze dávame namiesto vstupného podreťazca
NULL
astrtok()
vráti druhý token - v 3., a ďalšej fáze dávame namiesto vstupného podreťazca opäť
NULL
astrtok()
vráti tretí, atď. token
strtok()
modifikuje reťazec na vstupe! Majme reťazecroot:x:0
ktorý začína v pamäti na adrese 112. V skutočnosti je reprezentovaný takto:
@112 @113 @114 @115 @116 @117 @118 @119 @120 'r' | 'o' | 'o' | 't' | ':' | 'x' | ':' | '0' | '\0'
Po prvom zavolaní
token = strtok(vstup, ":")
sa “poorie” reťazec: funkcia nájde oddeľovač a nahradí ho znakom\0
a vráti pointer na začiatok reťazca@112 @113 @114 @115 @116 @117 @118 @119 @120 'r' | 'o' | 'o' | 't' | '\0' | 'x' | ':' | '0' | '\0'
V premennej
token
sa tak objaví adresa 112.Všimnime si, ako sa token správa ako korektný reťazec: je totiž ukončený
\0
! (Toto je majstertrik funkcie!)Po druhom zavolaní s parametrom
NULL
, tedatoken = strtok(NULL, ":")
sa ďalej orie reťazec: funkcia nájde ďalší výskyt oddeľovača a nahradí ho znakom\0
a vráti pointer na začiatok tokenu@112 @113 @114 @115 @116 @117 @118 @119 @120 'r' | 'o' | 'o' | 't' | '\0' | 'x' | '\0' | '0' | '\0'
V premennej
token
sa tak objaví adresa 117.Po treťom zavolaní (opäť s parametrom
NULL
) funkcia hľadá ďalší výskyt oddeľovača, ibaže .. v tomto reťazci už žiadny ďalší oddelovač neexistuje. Tretí token teda bude siahať až po koniec reťazca.V premennej
token
sa tak objaví adresa 119.dôležité:
strtok()
zdemoluje vstupný reťazec!- ak chceme po skončení tokenizovania pristupovať k pôvodnému vstupu, nedá sa to!
- reťazec
vstup
z príkladu je totiž pointer na prvý znak, teda obsahuje adresu112
. - keďže na adrese 116 bol oddeľovač prepísaný koncovým znakom
\0
, reťazecvstup
je stotožnený s prvým tokenom
vstupný reťazec môžeme odložiť do pomocnej premennej: priradením reťazcov
- priraďovanie reťazcov nefunguje cez
=
!- reťazec je totiž pole znakov ukončené nulou, a v našom prípade je to ekvivalentné s pointerom na prvý prvok
- priradenie teda priradzuje len adresy, ale nie ich obsahy
- mali by sme tak dve premenné s rovnakou adresou a problém orania reťazca funkciou
strtok()
by sme nevyriešili
- na priradenie reťazcov potrebujeme teda dva nezávislé reťazce, teda dve polia
- priradenie reťazca je vlastne kopírovanie obsahu polí
využime funkciu
strncpy()
char *strncpy(char *ciel, const char *zdroj, size_t pocet_znakov);
Napr:
strncpy(riadok2, riadok, MAX_DLZKA_RIADKA * sizeof(char) );
Keďže dĺžka premennej
riadok
ariadok2
je rovnaká, funkcia prebehne bez problémov.- Pozor v okrajových prípadoch:
- ak je cieľ kratší než zdroj, prepisujeme cudziu pamäť
- pri uvádzaní počtu kopírovaných znakov (počet krát veľkosť
char
u) nezabudnime na\0
! - funkcia neukončuje kopírovanie znakom
\0
! Ak máme zdroj'C', 'a', 'u', '!', '\0'
a kopírujeme len 3 znaky, v koncovom poli sa objaví'C', 'a', 'u'
, čo je pokazený reťazec!
- Pozor v okrajových prípadoch:
- priraďovanie reťazcov nefunguje cez
na porovnanie hľadaného UID využime porovnávanie reťazcov
- rozhodne nefunguje porovnanie cez
==
!- v Jave to takto tiež nefunguje.
- reťazec je totiž pole znakov ukončené nulou, a v našom prípade je to ekvivalentné s pointerom na prvý prvok
- cez
==
by sme porovnávali dva pointery, teda dve adresy, teda zisťovali, či sú reťazce na rovnakom mieste v pamäti.
na porovnanie reťazcov slúži
strcmp()
: funkcia vracia nulu, ak sa reťazce rovnajúif( strcmp(r1, r2) == 0 )
niekde vidieť aj obrátený zápis spoliehajúci sa na fakt, že “v C je 0 považovaná za
false
“:if( !strcmp(r1, r2) )
- rozhodne nefunguje porovnanie cez
Celý zdroják
#include<stdio.h>
#include<string.h>
#define SUBOR "/tmp/passwd"
#define HLADANE_ID "1134"
#define MAX_DLZKA_RIADKA 128
#define SEPARATOR ":"
int main() {
FILE * subor;
char riadok[MAX_DLZKA_RIADKA];
char cely_riadok[MAX_DLZKA_RIADKA];
char * token;
subor = fopen(SUBOR, "r");
if(subor == NULL) {
perror("Subor " SUBOR " sa nenasiel");
return -1;
}
while( fgets(riadok, MAX_DLZKA_RIADKA, subor) != NULL ) {
strncpy(cely_riadok, riadok, MAX_DLZKA_RIADKA * sizeof(char) );
if(strcmp(riadok, "\n") == 0) {
fprintf(stderr, "Prazdny riadok!");
continue;
}
token = strtok(riadok, SEPARATOR);
if(token == NULL) {
fprintf(stderr, "Chyba meno pouzivatela");
continue;
}
token = strtok(NULL, SEPARATOR);
if(token == NULL) {
fprintf(stderr, "Chyba 'x'");
continue;
}
token = strtok(NULL, SEPARATOR);
if(token == NULL) {
fprintf(stderr, "Chyba UID pouzivatela");
continue;
}
if(strcmp(HLADANE_ID, token) == 0) {
puts(cely_riadok);
}
}
fclose(subor);
return 0;
}