Tajemnice wejścia/wyjścia – jak zrozumieć deskryptory plików, strumienie i potoki

 

Wstęp

Wielu początkujących programistów bardzo często ma problemy ze zrozumieniem i poprawnym zaimplementowaniem jednego z podstawowych elementów programu – systemu wejścia/wyjścia. Mowa rzecz jasna o środowisku tekstowym, w którym nie mamy do dyspozycji przycisków, pól tekstowych czy okienek dialogowych. O ile problem z początku może wydawać się banalny (z tego powodu większość wydaje się nie zwracać na niego uwagi) to na późniejszym etapie jego ignorowanie prowadzi do powstawania wielu absurdów.

W dalszej części postaram się rozwiać wszelkie wątpliwości na temat niżej wymienionych kwestii:

  • jak działa i czym jest wejście/wyjście programu,
  • jak ujednolicić obsługę wejścia/wyjścia w swoim programie:
    • jak umożliwić użytkownikowi programu podjęcie decyzji o sposobie wprowadzania danych (z klawiatury, z pliku a może z innego komputera?),
    • jak zminimalizować nakład pracy;

Zanim jednak przejdziemy do kwestii programowania, ważne jest, aby dobrze znać środowisko w którym będziemy uruchamiać programy. Poznanie środowiska ma kluczowe znaczenie w kwestii rozumienia wewnętrznej struktury programu, pozwala na głębsze myślenie na temat optymalizacji kodu, upraszcza testowanie i sprawia, że programy zyskają na jakości – staną się bardziej uniwersalne.

Ten artykuł dotyczy środowisk zgodnych z POSIX, w moim przypadku będzie to GNU/Linux. Jeżeli korzystasz z Windowsa, konieczne będzie zainstalowanie środowiska Cygwin, które umożliwi korzystanie z POSIX-owego interfejsu programistycznego oraz dostarczy powłokę tekstową (np. Bash).

Uwaga! Do pełnego zrozumienia treści tego artykułu wymagana jest podstawowa wiedza na temat uruchamiania programów w powłoce tekstowej (rozróżnianie programu od jego argumentów) oraz minimalne doświadczenie z programowaniem. Jeżeli nie jesteś programistą, ale korzystasz ze środowiska zgodnego z POSIX (GNU/Linux, Mac OS X), ten artykuł również jest dla Ciebie.

 

Standardowe strumienie

Najpierw trochę teorii… W programowaniu standardowe strumienie są to kanały komunikacji między programem komputerowym a środowiskiem, w którym program jest uruchamiany (zwykle terminalem). Kanały te są podłączane w początkowym procesie uruchamiania programu. Rozróżniamy trzy standardowe strumienie:

  • standard input (stdin, standardowy strumień wejściowy)
  • standard output (stdout, standardowy strumień wyjściowy)
  • standard error (stderr, standardowy strumień błędów lub standardowy strumień diagnostyczny)
 

Deskryptory plików

Z pojęciem strumienia związane jest również inne ważne pojęcie – deskryptor pliku. Deskryptor pliku jest to identyfikator używany przez system operacyjny do obsługi operacji wejścia/wyjścia, w standardzie POSIX deksryptor pliku jest liczbą całkowitą (typ int z języka C/C++) a tablica deskryptorów plików jest odrębna dla każdego procesu (każdego uruchomionego programu). Każdy proces po uruchomieniu ma standardowo otwarte 3 deskryptory plików:

  • 0 – STDIN
  • 1 – STDOUT
  • 2 – STDERR

Zgodnie z POSIX – deskryptory plików są zwracane przez funkcje z rodziny open() (języka C). Uwaga! Nie należy mylić deksryptora pliku ze strukturą FILE – wskaźnik na tą strukturę zwracany jest przez funkcję fopen().

 

Potoki

Potok (ang. pipe) jest to mechanizm komunikacji międzyprocesowej umożliwiający bezpośrednią wymianę danych między uruchomionymi procesami. Komunikacja ta jest możliwia dzięki podłączeniu standardowego wyjścia jednego procesu do standardowego wejścia drugiego. Jest to bardzo wygodny, prosty mechanizm, który nie wymaga żadnych dodatkowych zmian w programie – komunikacja odbywa się z użyciem standardowych funkcji do obsługi wejścia/wyjścia takich jak printf(), scanf() czy znany z Javy System.out.print().

   -----------------------   -----------------------
   |      PROCESS_A      |   |      PROCESS_B      |
   -----------------------   -----------------------
   [ STDIN STDOUT STDERR ]   [ STDIN STDOUT STDERR ]
             ^                   ^
             |                   |
             ---------------------

O ile programistom korzystającym z użytkownikom Windowsa może być ciężko zrozumieć potęgę tego mechanizmu, tak bardziej doświadczeni użytkownicy systemu GNU/Linux są w pełni świadomi jego możliwości i wiedzą jak duże ma to znaczenie w codziennej pracy (nie tylko administratorów ale również zwykłych użytkowników). To właśnie dzięki mechanizmowi potoków możliwy jest podział narzędzi na małe programy wyspecjalizowane do wykonywania jednej konkretnej rzeczy np. program sortujący, program usuwający znaki, program zamieniający znaki czy filtrujący linie. Dzięki rozdzieleniu narzędzi tekstowych na kilka osobnych programów i wykorzystania potoków mamy do dyspozycji w pełni uniwersalne środowisko któremu nie straszne żadne zadanie. Nie musimy posiadać skomplikowanych, dużych programów do filtrowania, sortowania, wyodrębniania klientów z bazy danych, wystarczą bardzo podstawowe narzędzia i umiejętność posługiwania się nimi.

Przyjrzyjmy się kilku wybranym narzędziom z pakietu GNU/textutils (pakiet narzędzi tekstowych GNU):

  • cat – łączenie i wypisywanie plików
  • nl – numerowanie linii i wypisywanie plików
  • fold – zawijanie linii wejściowych do zadanej szerokości
  • head – wypisywanie początku plików
  • tail – wypisywanie początku plików
  • split – podział pliku na części stałej wielkości
  • wc – wypisywanie liczby bajtów, słów i linii
  • sort – sortowanie
  • uniq – filtrowanie unikalnych linii
  • cut – wypisywanie wybranych części linii
  • tr – zamiana, ściskanie, usuwanie znaków
  • grep – filtrowanie
 

Powłoka tekstowa

Powłoka tekstowa (ang. shell) jest to program pełniący rolę komunikacji użytkownika z systemem operacyjnym. Odpowiada za analizę i przetwarzanie danych wprowadzonych przez użytkownika (tzn. poleceń). Przetwarzanie to najczęściej uruchomienie innych programów i zwrócenie wyników, ale nie tylko – powłoka tekstowa zwykle intepretuje również w specjalny sposób szereg znaków specjalnych np. > < | & itd.

Najczęściej używaną powłoką tekstową w systemach GNU/Linux jest Bash (Bourne Again SHell) stworzona i rozwiajana w ramach Projektu GNU i udostępniana na zasadach licencji wolnego i otwartego oprogramowania. Bash jest również domyślną powłoką w Mac OS X. Można z niej również korzystać w systemach operacyjych Microsoftu, jest rozprowadzana z Cygwinem i MinGWem.

Operacje na deskryptorach

W powłoce tekstowej mamy możliwość przekierowania, zamykania, łączenia deksryptorów plików w uruchamianych programach, służą do tego operatory: < oraz >, >>.

Przekierowanie standardowego wejścia

Do przekierowania standardowego wejścia służy operator <, wykorzystujemy go w sytuacji, w której dane wejściowe do programu posiadamy np. w pliku tekstowym. Nie musimy otwierać pliku w edytorze i wklejać zawartości na standardowe wejście programu, wystarczy nam coś takiego:

$ moj_program < dane_wejsciowe.txt

Powłoka tekstowa po otrzymaniu takiego polecenia najpierw otworzy plik dane_wejsciowe.txt w trybie tylko do odczytu, po poprawnym otwarciu uruchomi nasz program (moj_program) „podpinając” deksryptor pliku o numerze 0 (STDIN) do deskryptora zwróconego przy otwieraniu pliku z danymi.

Przekierowanie standardowego wyjścia

Do przekierowania standardowego wyjścia służy operator >, wykorzystujemy go, gdy wynik (STDOUT) lub wyjście diagnostyczne (STDERR) naszego programu chcemy zapisać w pliku tekstowym.

$ moj_program > wynik.txt

Wykonanie tego polecenia spowoduje podpięcie deskryptora nr 1 (STDOUT) do deskryptora otwartego pliku, tym razem z opcją do zapisu i opcją obcięcia/czyszczenia (jeżeli plik istnieje, zostanie wyczyszczony), tak jak w poprzednim wypadku – nasz program zostanie uruchomiony dopiero po poprawnym otwarciu pliku wyjściowego.

Możemy również użyć operatora >> który zachowuje się identycznie jak operator > z tą różnicą że zamiast opcji obcięcia, używana jest opcja dodawania czego efektem jest dopisywanie wyjścia naszego programu na koniec wskazanego pliku (plik nie jest czyszczony).

Wybór deskryptora

Przy operacji przekierowania deksryptorów możemy odnieść się do konkretnego, używając składni: N>PLIK gdzie N to numer deskryptora, możemy np. przekierować standardowe wyjście do pliku z danymi, a wyjście diagnostyczne do pliku z logami błędów:

$ moj_program 1> wynik.txt 2> błedy.log
$ moj_program > wynik.txt 2> /dev/null

W drugiej linijce przekierowujemy STDERR do specjalnego pliku /dev/null, w systemach Uniksowych jest to wirtualne urządzenie usuwające wszelkie dane, które do niego trafiają (tzw. pustka). Należy zauważyc, że zapis 1> jest równoznaczny z > (domyślnie dotyczy deskryptora nr 1, podobnie jak < dotyczy tego z numerem 0).

Dokładnie tak samo możemy postąpić z operatorem >>.

Trochę więcej przykładów:

# przypomnienie: program cat otwiera wszystkie
# pliki podane jako argumenty i (w podanej kolejności) wyświetla ich
# zawartość na STDOUT
 
$ cat klienci_krakow.db klienci_warszawa.db > klienci.db
# połączyliśmy bazę klientów z dwóch miast
 
$ cat klienci_gransk.db >> klienci.db
# dokładamy jeszcze klientów z Gdańska
 
$ sort < klienci.db > klienci-posortowani.db
# sortujemy klientów i zapisujemy wynik
 
$ cat klienci-posortowani.db > /media/pendrive/baza.db
# Kopiujemy plik
 
$ cp klienci-posortowani.db /media/pendrive/baza.db
# Równoznaczne z poprzednim
 
$ echo abc > out
# Tworzymy/zastępujemy plik out zawartością "abc"
 
$ echo KONIEC >> out
# Dodajemy słowo KONIEC na koniec pliku out

Łączenie i zamykanie deskryptorów

Aby połączyć deskryptory, należy skorzystać z następującej składni: N>&M nastapi to połączenie deskryptora N z deskryptorem M. Powiedzmy, że chcemy zapisać dane z naszego programu pochądzące zarówno z STDOUT jak i z STDERR, do pliku o nazwie out.log:

$ moj_program > out.log 2>&1

Do zamykania deksryptora możemy użyć następującej składni: N>&-. Przykład wyłączenia wypisywania błędów (zamknięcie STDERR):

$ moj_program 2>&-

Używanie potoków

Używanie potoków w powłoce tekstowej jest bardzo proste, wystarczy użyć składni: program_a | program_b np.:

$ cat baza-klientow.db | sort

Powyższy przykład spowoduje uruchomienie dwóch programów i podpięcie standardowego wyjścia pierwszego z nich (cat) do wejścia drugiego (sort). W naszym przypadku efektem będzie wyświetlenie posortowanej zawartości pliku baza-klientow.db na standardowym wyjściu. Oczywiście wynik możemy zapisać do pliku, wykorzystując wcześniej poznany operator >.

$ cat baza-klientow.db | sort > posortowana-baza.db

Wyobraźmy sobie, że mamy duży plik tekstowy zawierający nieposortowaną bazę około dziesięciu tysięcy naszych klientów, każdy klient to jedna linia w pliku. Format wygląda następująco:

Imię Nazwisko Miejscowość adres-email

Teraz wyobraźmy sobie, że chcemy wyświetlić listę osób z Krakowa, przy czym interesują nas tylko adresy e-mail. Musimy zatem skorzystać z programu wyświetlającego plik tekstowy (cat), jego wyjście połączyć z programem filtrującym (grep) a następnie z programem do wypisywania wybranych części linii (cut):

$ cat baza-klientów.txt | grep "Kraków" | cut -d " " -f 4

Jeżeli chcemy dodatkowo posortować adresy e-mail i upewnić się, że są unikalne, musimy skorzystać również z programu sortującego (sort) i wypisującego unikalne linie (uniq).

$ cat baza-klientów.txt | grep "Kraków" | cut -d " " -f 4 | sort | uniq

Teraz spróbujemy wyświetlić listę wszystkich klientek o imieniu Anna i posortować wg nazwiska:

$ cat baza-klientów.txt | grep "Anna" | sort -k 2
 

Implementacja – ujednolicenie obsługi wejścia/wyjścia

Naszym celem będzie napisanie programów w trzech językach: C, C++ oraz Java w taki sposób, aby każdy z nich zachowywał się jak poniżej:

  • uruchomienie programu bez parametrów spowoduje oczekiwanie na dane z STDIN oraz spowoduje wypisanie danych na STDOUT, błędy będą trafiały na na STDERR
  • uruchomienie programu z opcją -i (od input) i podanie jako argument istniejącego pliku spowoduje, że program będzie korzystał z tego pliku zamiast z STDIN
  • uruchomienie programu z opcją -o (od output) i podanie jako argument pliku spowoduje, że program będzie korzystał z tego pliku zamiast z STDOUT
  • uruchomienie programu z opcją -q (od quiet) spowoduje że program przestanie wypisywać informacje o błędach
  • podanie znaku - (minus) jako argument do opcji -i lub -o nie spowoduje żadnych zmian w zachowaniu

Przykłady wykorzystania naszego przyszłego programu uniwersalne_io:

$ uniwersalne_io < plik_wejsciowy.in > plik_wyjsciowy.out
$ uniwersalne_io -i plik_wejsciowy.in
$ uniwersalne_io -q -i wejscie.in -o wyjscie.out

Najważniejszą zaletą może okazać się możliwość wykorzystania prostej pętli do uruchomienia zautomatyzowanego procesu testowania działania programu. Poniższy przykład zapewne nie raz przyda się studentom podczas testowania działania swoich programów. Załóżmy, że w katalogu z naszym programem posiadamy folder tests a w nim pliki o nazwach test_N.in oraz test_N.out, gdzie N to kolejne liczby naturalne. W plikach z końcówką .in umieszczone będą dane wejściowe do programu, natomiast w plikach .out oczekiwane dane wyjściowe, zakładamy, że program działa dobrze, jeżeli wynik zwrócony przez program po odczytaniu pliku .in jest identyczny z odpowiadającym mu plikiem .out.

Przykład, dzięki któremu po poprawnym zaimplementowaniu obsługi wejścia/wyjścia dowiemy się, czy program zakończy się powodzeniem po uruchomieniu każdego testu (na razie ignorujemy sprawdzanie poprawności danych wyjściowych):

$ for X in tests/*.in; do \
	./uniwersalne_io -q -i "$X" -o /dev/null && echo "$X - OK" || echo "$X - FAIL"; \
done;

Wartością zwracaną przez programy w środowisku POSIX jest numer błędu, jeżeli program zakończył się powodzeniem powinien zwrócić 0 (zero), co oznacza brak błędu. Przyjmujemy zatem, że każda wartość różna od zera mówi nam o tym, ze program nie zakończył się poprawnie. Należy to wziąć pod uwagę podczas tworzenia swoich programów dla tego środowiska.

Trochę bardziej skomplikowanym przykładem użycia może wydawać się sprawdzenie poprawności zwracanych danych, to nie powinno być jednak trudne:

$ for X in tests/*.in; do \
	./uniwersalne_io -q -i "$X" | \
	diff -w - ${X:0:(-2)}out 1>/dev/null 2>&1 && \
	echo "$X - OK" || echo "$X - FAIL"; \
done;

program diff porównuje pliki linia po linii, jeżeli są takie same – kończy zwracając zero, jeżeli się różnią – wypisuje wszystkie różnice i kończy zwracając 1. Opcja -w powoduje, że program ignoruje różnice w białych znakach (spacja, wcięcia, nowe linie). Podanie znaku - (minus) jako jednego z plików powoduje odczyt ze standardowego wejścia.

W powyższym przykładzie dziwny może wydawać się zapis ${X:0:(-2)}out, jest to zwykły substring w powłoce tekstowej. W każdym obiegu pętli, w zmiennej X mamy ścieżkę do pliku z wejściem programu np. w pierwszym obiegu może to być tests/test_1.in, my potrzebujemy uruchomić program diff, jako argumenty podając dwa pliki – jeden to STDIN (minus), ponieważ korzystamy z potoku do podłączenia STDOUT z naszego programu, drugi to plik z oczekiwanym wyjściem, który powinien wyglądać tak: tests/test_1.out. Mając do dyspozycji tylko zmienną X, musimy odciąć dwa ostatnie znaki („in„) i dodać „out„. To zadanie jest realizowanie dokładnie przez ten fragment: ${X:0:(-2)}out.

Implementacja – język C

Napiszemy prosty program demonstracyjny, który prosi użytkownika o podanie imienia, następnie wypisuje komunikat w postaci „Witaj PODANE_IMIE…„. Zależy nam, aby poprawnie zachowywał się w przypadku przekierowania strumieni, w szczególności jeżeli przekierujemy STDOUT do pliku – nie ma mowy, aby w pliku wynikowym były pytania o podanie danych (u nas prośba o imię), takie pytania muszą zostać widoczne dla użytkownika.

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
 
int init_io(int argc, char **argv) {
    int i;
    int fd;
    for (i = 1; i < argc; ++i) {
        if (strcmp("-i", argv[i]) == 0 && argc > i + 1) {
            if (strcmp("-", argv[++i]) == 0)
                continue;
            fd = open(argv[i], O_RDONLY);
            if (fd < 0)
                return -1;
            if (dup2(fd, 0) != 0)
                return -2;
            close(fd);
        } else if (strcmp("-o", argv[i]) == 0 && argc > i + 1) {
            if (strcmp("-", argv[++i]) == 0)
                continue;
            fd = open(argv[i], O_WRONLY | O_CREAT | O_TRUNC, 0666);
            if (fd < 0)
                return -1;
            if (dup2(fd, 1) != 1)
                return -2;
            close(fd);
        } else if (strcmp("-q", argv[i]) == 0) {
            fd = open("/dev/null", O_WRONLY);
            dup2(fd, 2);
            close(fd);
        }
    }
    return 0;
}
 
int main(int argc, char **argv) {
    if (init_io(argc, argv) < 0) {
        fprintf(stderr, "Nie można zainicjalizować wejścia/wyjścia...\n");
        return 1;
    }
 
    /*
     *
     * dalsza część programu - korzystamy ze standardowych strumieni
     * funkcja init_io() zduplikowała deksryptory plików jeżeli
     * była taka potrzeba (użytkownik wskazał poprawne ścieżki)
     *
     */
 
    fprintf(stderr, "Wpisz swoje imię: ");
    char name[50];
    scanf("%49s", name);
    printf("\nWitaj %s...\n", name);
}

W przykładzie kluczową rolę odgrywa funkcja init_io() przyjmująca ilość oraz listę argumentów. Funkcja ta analizuje tablicę argumentów przekazanych do programu (reaguje na ciągi -i, -o oraz -q) i podejmuje odpowiednie akcje. Należy zwrócić uwagę, na pętlę:

// (...)
    for (i = 1; i < argc; ++i) {
// (...)

Pętla ta rozpoczyna swój bieg od 1, nie od zera. Nie jest to błąd, wręcz przeciwnie – należy pamiętać że w argv[0] przechowywana jest ścieżka do programu (konkretnie ciąg, po którym program został uruchomiony), pomijamy ją przy analizie argumentów (te zaczynają się od 1 i kończą na argc – 1)

Wewnątrz pętli mamy trzy wykluczające się instrukcje warunkowe, dwie pierwsze są niemal identyczne, zadaniem ostatniej (-q) jest skopiowanie deskrptora pliku zwróconego przez wywołanie open("/dev/null", O_WRONLY) (jeżeli czytałeś uważnie, powinieneś wiedzieć dlaczego używamy tutaj /dev/null :) ) do deskryptora pliku nr 2 (STDERR) – spowoduje to zatrzymanie wyświetlania komunikatów kierowanych na standardowe wyjście błędów. Nie możemy po prostu zamknąć deskryptora nr 2, ponieważ działanie funkcji takich jak fprintf(), gdzie jako pierwszy argument podajemy wskaźnik do struktury FILE, która jest powiązana z danym plikiem jest bliżej nieokreślone i może powodować błędy.

Przeanalizujmy pierwszą instrukcję warunkową:

// (...)
        if (strcmp("-i", argv[i]) == 0 && argc > i + 1) {
(...)

Sprawdzamy tutaj, czy aktualnie analizowany element (argv[i]) jest równy ciągowi „-i”, jeżeli tak, to sprawdzamy, czy za nim występuje jeszcze jeden element – wymagamy, aby była to ścieżka do pliku lub znak „-”. Jeżeli wymagania są spełnione:

// (...)
            if (strcmp("-", argv[++i]) == 0)
                continue;
// (...)

Tutaj sprawdzamy, czy podany argument występujący za „-i” jest znakiem „-”, jeżeli tak, pomijamy go (nic nie robimy) i przechodzimy do analizy następnego argumentu programu – wracamy na początek pętli for. Należy zwrócić uwagę na preinkrementację zmiennej i, która zawsze zostanie wykonana, niezależnie od tego czy wyrażenie wewnątrz if zwróci prawdę, czy fałsz. Zobaczmy, co dalej:

// (...)
            fd = open(argv[i], O_RDONLY);
            if (fd < 0)
                return -1;
// (...)

Próbujemy tutaj otworzyć plik podany zaraz za „-i” (u nas będzie to argv[i] ze względu na wcześniejszą preinkrementacje). Wykorzystujemy do tego funkcję open(), która przyjmuje jako argumenty ścieżkę do pliku oraz opcje, w naszym przypadku jedyną opcją jest O_RDONLY, która spowoduje otwarcie pliku tylko do odczytu. Funkcja ta zwraca deskryptor nowo otwartego pliku lub wartość mniejszą od zera w przypadku niepowodzenia (brak uprawnień lub zła ścieżka do pliku etc.). Sprawdzamy, czy udało się otworzyć plik, jeżeli nie to przerywamy analizę argumentów zwracając -1. Następnie wykonujemy najważniejszą część funkcji:

// (...)
            if (dup2(fd, 0) != 0)
                return -2;
            close(fd);
// (...)

Funckja dup2 przyjmuje kolejno dwa argumenty: stary deksryptor pliku oraz nowy deskryptor pliku, jej wykonanie powoduje, że nowy deskryptor pliku staje się kopią starego deskryptora, po pomyślnym zakończeniu funkcji (zwróci wówczas nowy deskryptor pliku) oba deskryptory mogą być używane zamiennie – współdzielą one blokady, pozycje pliku i znaczniki. W naszym przypadku, jeżeli funkcja zakończy się poprawnie, zamykamy deskryptor pliku powiązany z plikiem, który podał użytkownik – nie będziemy z niego korzystać. Wykorzystywać będziemy jego kopię, którą w tym przypadku stanie się deksryptor wskazujący na standardowe wejście. Pozwala to korzystać w programie ze standardowych funkcji/operacji do obsługi standardowego wejścia jak np. scanf() z tą różnicą, że będą one się odnosić do naszego pliku…

W drugiej instrukcji warunkowej (dotyczącej opcji „-o”) postępujemy identycznie, różnią się jedynie flagi otwarcia pliku (funkcji open()). W związku z tym, że opcja ta ma spowodować przekierowanie STDOUT do pliku, plik ten musi zostać otwarty w trybie do zapisu (O_WRONLY), w przypadku gdy plik nie istnieje – musi zostać utworzony (O_CREAT), a w przypadku gdy istnieje – musi zostać wyczyszczony (O_TRUNC). W związku z tym, że wykorzystujemy flagę O_CREAT, musimy podać kolejny argument do funkcji open(), są nim uprawnienia, jakie mają być ustawione w przypadku tworzenia nowego pliku. Uprawnienia te podajemy w notacji ósemkowej, pamiętając o tym, że system użyje jeszcze umask. Czyli w rzeczywistości uprawnienia pliku będą wynikiem operacji (mode & ~umask), gdzie mode to podane podany przez nas argument do funkcji open().

Implamentacja – język C++

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
 
using namespace std;
 
int init_io(int argc, char **argv) {
	/* tak samo jak w przykładzie wyżej */
}
 
int main(int argc, char **argv) {
    if (init_io(argc, argv) < 0) {
        cerr << "Nie można zainicjalizować wejścia/wyjścia..." << endl;
        return 1;
    }
 
    cerr << "Wpisz swoje imię: ";
    char name[50];
    cin >> name;
    cout << endl << "Witaj " << name << endl;
}

Tutaj nic się nie zmienia, korzystamy jedynie ze standardowych metod obsługi wejścia/wyjścia w języku C++.

Implementacja – język Java

W Javie nie musimy korzystać już z funkcji systemowych takich jak open(), wystarczy, że skorzystamy z odpowiednich metod klasy System:

import java.io.FileInputStream;
import java.io.PrintStream;
import java.util.Scanner;
 
class Example {
 
    public Example() {
        in = new Scanner(System.in);
        System.err.print("Podaj swoje imię: ");
        String imie = in.next();
        System.out.println("\nWitaj " + imie + "...");
    }
 
    protected static boolean init_io(String[] args) {
        for (int i = 0; i < args.length; ++i) {
            System.err.println(args[i]);
            if (args[i].equals("-i") && args.length > i + 1) {
                if (args[++i].equals("-"))
                    continue;
                FileInputStream fis;
                try {
                    fis = new FileInputStream(args[i]);
                } catch (Exception e) {
                    return false;
                }
                System.setIn(fis);
            } else if (args[i].equals("-o") && args.length > i + 1) {
                if (args[++i].equals("-"))
                    continue;
                PrintStream ps;
                try {
                    ps = new PrintStream(args[i]);
                } catch (Exception e) {
                    return false;
                }
                System.setOut(ps);
            } else if (args[i].equals("-q")) {
                System.err.close();
            }
        }
        return true;
    }
 
    public static void main(String[] args) {
        if (!init_io(args)) {
            System.err.println("Nie można zainicjalizować wejścia/wyjścia...");
            System.exit(1);
        }
        new Example();
    }
 
    protected Scanner in;
 
}

Więcej przykładów w drodze…

Share:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • Blogplay
  • Blip
  • Blogger.com
  • Gadu-Gadu Live
  • Google Buzz
  • LinkedIn
  • MySpace
  • Twitter
  • Wykop
  • Śledzik

3 thoughts on “Tajemnice wejścia/wyjścia – jak zrozumieć deskryptory plików, strumienie i potoki

  1. Daniel pisze:

    Świetny wpis.
    Idealna proporcja treści do długości tekstu. Czytając, chłonąłem.

  2. komenda pisze:

    […] […]

  3. beginner pisze:

    Witam. Mam pytanie.moj_program < dane_wejsciowe.txtdane_wejsciowe.txt są podczepiane pod argv[] w programie czy pod funkcje takie jak scanf()?

Odpowiedz na „beginnerAnuluj pisanie odpowiedzi

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Please type the characters of this captcha image in the input box

Udowodnij, że jesteś człowiekiem - przepisz tekst z obrazka

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>