Terminál
Do teď jste se naučili pouze vypisovat text řádek po řádku, písmenko za písmenkem. Víc toho jazyk C ani nenabízí. Terminály ovšem umožňují mnohem více. Můžete psát barevně, můžete posouvat kurzor různě po obrazovce (tedy vlastně chci říct po terminálu), můžete načítat znaky od uživatele, aniž by se mu na terminálu zobrazovali (třeba při zadávání hesla) atp. A o tom bude tato kapitola.
Terminál
Terminál (nebo se tomu také říká konzole) je jakékoliv (vstupně)výstupní zařízení. Nejčastěji se pod pojmem terminál rozumí obrazovka, nebo xterm (konzole). Xterm je vlastně emulátor terminálu. Terminálem ale může být i tiskárna.
V ranných dobách počítačového věku, kdy muži ještě byli muži, existovalo spoustu výrobců terminálů. A každý používal různé řídící instrukce. Různé sekvence znaků ovládali kurzor (posouvali ho po obrazovce), měnili barvy popředí a pozadí písma, měnili obnovovací frekvenci atd. Aby se z toho programátor nezbláznil, vytvořila se knihovna termios, která poskytuje funkce pro ovládání terminálu (vychází ze starší knihovny termio ze staršího unixu System V). Některé z funkcí této knihovny představím v této kapitole.
Ovládání terminálu v Bashi
Než se pustím do ukázek v jazyku C, dovolím si malou odbočku k bashi. Pokud se omezíte na to, že vám stačí, aby váš program fungoval jen v linuxovém emulátoru (konzole, xterm), můžete používat jako řídící znaky escape sekvence.
Popis toho, jak se používají escape sekvence najdete na stránce věnované příkazu echo. Dočtete se tam, že escape sekvence je vše, co začíná zpětným lomítkem. Příkaz echo pak znaky za zpětným lomítkem interpretuje nějakým speciálním způsobem. To už znáte i z C, '\n' je sekvence označující nový řádek…
Znak escape
se označuje escape sekvencí \033
,
což je osmičkový zápis čísla 27. 27 je ascii kód escape znaku. A escape znak je právě
řídící znak pro linuxový emulátor terminálu.
Je to trošku nedokonalé řešení. Problém je v tom, že když stisknete escape, terminál se musí rozhodnout, jestli má tento znak předat programu, který běží (například editoru vim), nebo jestli tímto znakem začíná escape sekvence, kterou bude nějak speciálně interpretovat (třeba změnou barvy). Terminál se rozhoduje podle toho, jak rychle přijde následující znak za esacpe. Pokud hodně rychle, pak to zřejmě nemohl stihnout člověk namačkat na klávesnici a je to escape znak. Problém ale je, pokud pracujete po síti. Může se totiž stát, že se escape sekvence rozdělí do několika packetů a terminál to zmate :-(. Řešení neexistuje, musíte jen doufat, že se to nestane …
Ale teď zpět k veselým věcem :-) Na základě toho, co jste si mohli přečíst v kapitole o příkazu echo jsem vytvořil skript v bashi, který vám bude zobrazovat v emulátoru terminálu datum v pravém horním rohu.
- #!/bin/bash
- # bash-datum.sh
- #
- if [ "$TERM" != "xterm" -a "$TERM" != "linux" ]
- then
- exit 1
- fi
- ESC="\033[" # ulozeni escape sekvence do promenne ESC
- COLOR="${ESC}0;34;47m" # esc. sekv. pro zmenu barvy
- END_COLOR="${ESC}0m" # obnoveni defaultni barvy
- SAVE_CURSOR="${ESC}s" # ulozeni aktualni pozice kurzoru
- RESTORE_CURSOR="${ESC}u" # nastaveni pozice kurozru na tu ulozenou
- DATE=`date` # ziskani data
- size=${#DATE} # delka retezce v promenne DATE
- col=`tput cols` # pocet sloupcu terminalu
- col=`expr $col - $size` # pocet sloupcu terminalu - delka DATE
- line=0
- GO_TO_POSITION="${ESC}${line};${col}f" # presun na pozici
- echo -en "${SAVE_CURSOR}${GO_TO_POSITION}${COLOR}${DATE}${END_COLOR}${RESTORE_CURSOR}"
Pokud spustíte skript v něčem jiném než xtermu nebo linuxové konzoli (do které se přepnete pomocí CTLR+ALT+F1),
pak se skript ukončí. Toto jsou jediné dva terminály, u kterých vím, že tam řídící znaky escape sekvencí
fungují. (Fungují ale určitě i na jiných terminálech, to už si musíte vyzkoušet sami.)
Pokud jste v programování bashe noví, dejte si pozor na mezery kolem hranatých závorek u if
příkazu,
jsou povinné. Uvozovky kolem "$TERM" taky, protože jinak v případě, že nebude $TERM
definován, dojde k chybě.
Výsledek můžete vidět na obrázku.
Tento skript vypíše datum na obrazovku jen jednou. Pokud ho tam budete chtít mít trvale, můžete spustit v konzoli nekonečnou smyčku na pozadí:
Bez sleep 1
by vám
tento cyklus strašně vytěžoval procesor.
Už vidím vaše nadšení, jak se chystáte naučit
programovat v bashi, aby jste zjistili,
co všechno ještě můžete udělat, jak si skript upravit a co všechno si můžete
na konzoli zobrazit. Možná už jste si výše zmíněný cyklus zapsali do
svého souboru ~/.bashrc
, aby se spustil automaticky při každém
spuštění bashe. To ale raději nedělejte!
Bash se spouští při mnoha příležitostech, o kterých ani netušíte. A ne vždy je přitom připojen k terminálu. Bohužel, výše napsaný skript v takovém případě způsobuje chybu. Podmínka, která skript ukončí, pokud není $TERM nastaven na xterm nebo linux sice většinu problémů řeší, ale třeba při startu KDE se spouští bash s $TERM nastaveným na xterm a z nějakého důvodu mi bash-skript.sh start KDE zablokoval. Takže automatické spuštění tohoto skriptu v xtermu silně nedoporučuji! S konzolou linux jsem zatím problémy neměl, takže můžete zkusit přidat cyklus s podmínkou,že se má spustit jen pokud je $TERM roven linux.
Pokud se někdy něco na terminálu špatně překreslí, zkuste CTRL+L pro jeho vyčištění.
Příkaz stty
Příkaz stty
se používá k zobrazení a nastavení
vlastností terminálu. Volba -a
vypíše aktuální nastavení v pro
člověka čitelném formátu.
speed 38400 baud; rows 20; columns 80; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?;
swtch = M-^?; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
-iuclc ixany imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
echoctl echoke
Z výstupu se dočtete třeba to, že obnovovací frekvence je 38400
baudů.
Což je pro emulátor terminálu vcelku irelevantní informace. Počet řádků je 20,
sloupců 80 (protože sem si terminál zvětšil na tuto velikost), klávesová
zkratka pro přerušení (SIGINT) je CTRL+c atd. Na předposledním řádku vidíte,
že je zapnuté echo
. V tomto módu se vypisuje na obrazovku vše,
co uživatel píše. Echo můžete vypnout takto:
Od této chvíle neuvidíte, co na terminál píšete. To se hodí, pokud nemáte rádi, když vám někdo kouká přes rameno, co děláte :-). Ale spíš se to používá v bash skriptech při zadávání hesla.
Echo režim můžete nastavit zpět příkazem stty echo
.
Další užitečná věc, kterou stty
umí, je dát terminál
„do pořádku“. Pokud si terminál rozhodíte tak, že ani nemůžete
zadat znak nového řádku pomocí enteru, můžete zkusit klávesovou
kombinaci CTRL+j.
Kanonický a nekanonický režim
Kanonický, nebo též normální režim je takový, ve kterém odesíláte příkaz programu až odesláním nového řádku (klávesou Enter).
Nekanonický režim odesílá programu každý stisknutý znak. V tomto režimu se musí program postarat o takové věci, jako je správná interpretace stisknutí klávesy na smazání znaku (backspace) atp. Nekanonický režim se může hodit při psaní her, kdy chcete ihned reagovat na stisk klávesy.
V nekanonickém režimu jsou důležité dvě hodnoty: min
a time
. Hodnota min udává, kolik je potřeba
minimálně stisknout kláves, aby se odeslali programu. Hodnota time udává
časový interval, po kterém se vstup odešle do programu, ikdyž nebylo stisknuto
min kláves. Můžou nastat tyto 4 kombinace min a time:
min | time | Jak se bude chovat čtení |
---|---|---|
0 | 0 | pokus o čtení ihned skončí, nic se nenačte. Tuto kombinaci raději nezkoušejte :-) |
0 | >0 | čtení skončí po time desetin sekundy (funkce read() vrací 0), nebo při vstupu ukončeném enterem (read() vrací počet načtených znaků) |
>0 | 0 | při čtení se čeká, dokud se nenačte alespoň min znaků |
>0 | >0 | po obdržení prvního znaku se spustí mezičasovač time (běžící se restartuje). Čtení končí po načtení min znaků, nebo když vyprší časový limit time. Může se to hodit pro rozlišování mezi escape sekvencí a stiskem escape klávesy. Při práci po síti je to ale bohužel nespolehlivé. |
Nekanonický režim můžete nastavit pomocí stty
takto:
Praktické využití ukáži v této kapitole o kousek níže.
A to je asi tak k ovládání terminálu z bashe všechno. Podrobně vám vysvětlovat skriptování v bashi nebudu, na to si přečtěte tutoriál o příkazech v Linuxu.
Ovládání terminálu v C
Předchozí výlet do bashe tu není náhodou :-). Escape sekvence můžete používat i pomocí standardních funkcí na tisk znaků jazyka C.
Následující příkaz tiskne pomocí escape sekvencí text v různých barvách.
- /*------------------------------------------------*/
- /* 50terminal/colors.c */
- #include <stdio.h>
- #define ESC "\033["
- {
- int i;
- }
- }
- /*------------------------------------------------*/
Výstup si můžete prohlédnout na obrázku:
Všiměte si, že na prvním řádku je černý text na černém pozadí, takže není nic vidět. Uživatel si může nastavit různé barevné schémata, takže je nutné, aby jste při změně barvy změnili i pozadí textu, pokud chcete mít jistotu, že bude text čitelný.
Můžete zkoušet i další řídíci sekvence, jak jsou popsány u příkazu echo. Vyzkoušejte si, co který terminál podporuje a co ne. Třeba takové blikání podporuje konzole z KDE, ale jenom když máte v nastavení zaškrtnuto, že je povolené! Konzole z KDE také jako jeden z mála terminálů podporuje italic písmo.
Nemusím asi moc zdůrazňovat, že používání těchto escape sekvencí je málo přenositelné. Ve Windows si s tím neškrtnete, v Linuxu ne každý terminál podporuje každou vymoženost (jako třeba ten italic text nebo blikání). A v některých unixech třeba nebude podporavaná ani ta změna barev.
Přiřazení IO k terminálu
Uživatel může standardní vstup, výstup i chybový výstup přesměrovat.
Někdy se může hodit zjistit, jestli je některý souborový proud přesměrován,
nebo je přiřazen k terminálu. K tomu slouží funkce isatty()
z knihovny <unistd.h>.
Funkce vrací 1, pokud je deskriptor souboru fd
přiřazen k terminálu,
0 pokud není.
Co když je standardní výstup přesměrován a vy stejně chcete něco napsat na
obrazovku (terminál)? Obvykle se pro to využívá standardní chybový výstup
stderr, ale i ten může být přesměrován. V Linuxu ale existuje speciální zařízeni
/dev/tty
, které je vždy přiřazeno aktuálnímu
terminálu, ve kterém program běží.
Dle normy POSIX je toto zařízení přistupné pro čtení i zápis, stejně jako
obyčejný soubor. Výstup pak jde vždy na aktuální terminál spuštěného programu
a vstup se čte z tohoto terminálu. Díky tomu, že je /dev/tty
součástí normy POSIX, se můžete spolehnout na to, že jej najdete v každém
dobrém Linuxu i Unixu.
A protože jeden příklad vydá za tisíc slov:
- /*------------------------------------------------*/
- /* 50terminal/isatty.c */
- #include <string.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- {
- FILE *input, *output;
- "Nelze otevřít /dev/tty pro čtení nebo zápis.\n");
- return EXIT_FAILURE;
- }
- "stdin neni připojeno k terminálu, končím!\n");
- return EXIT_FAILURE;
- }
- "stdout neni připojeno k terminálu, končím!\n");
- return EXIT_FAILURE;
- }
- "stderr neni připojeno k terminálu, končím!\n");
- return EXIT_FAILURE;
- }
- return EXIT_FAILURE;
- }
- /*------------------------------------------------*/
V příkladu by mělo být asi všechno jasné. Pomocí funkce fileno()
získám deskriptor souboru reprzentující příslušný souborový proud a pak jej
otestuji funkcí isatty()
.
To, že na řádkách 40 a 41 pro výstup používám output
a pak stdout
přičtěte na vrub mé rozmařilosti, v tomto příkladu je to úplně jedno, protože
na těchto řádkách už je jasné, že je stdout
přiřazen k terminálu,
a tak půjde výstup na obrazovku.
Příklad použití programu isatty
:
stdout neni připojeno k terminálu, končím!
$ ./isatty 2> /tmp/xxx
stderr neni připojeno k terminálu, končím!
$ ./isatty < /tmp/xxx
stdin neni připojeno k terminálu, končím!
$ ./isatty
OK, můžeme pracovat :-)
Něco mi řekni: Mám tě rád!
"Mám tě rád!" říkáš?
Končím!
Tuto kontrolu můžete použít třeba k tomu, abyste uživatele
přinutili zadávat heslo ručně, aby si ho neukládal někam do
skritpu. Taky to znepříjemní práci případným crackerům vašeho
programu, protože si nebudou moci generovat hesla v nějakém
programu a pak je přesměrovávat do vašeho programu. Ale pozor,
každý dobrý hacker dokáže váš program obelstít tak, aby kontrola
pomocí isatty()
byla obalamucena. Takže na to
zase tak moc nespoléhejte ;-).
Zkouška kanonického režimu
Z programem isatty
si můžete vyzkoušet, jak se bude
chovat v nekanonickém režimu. Tak schválně:
$ ./isatty
OK, můžeme pracovat :-)
Něco mi řekni: Ahoj
"Ahoj" říkáš?
Končím!
Hmm, nic zvláštního se nestalo. Funkce fgets()
sice dostává znak po znaku, ale stejně neskončí, dokud nenarazí
na konec řádku, nebo nenačte max. počet znaků. Co se ale asi tak
stane, když budu chtít něco z toho, co píšu, smazat?
$ ./isatty
OK, můžeme pracovat :-)
Něco mi řekni: Ahoj^?^?^?
"Ahoj###" říkáš?
Končím!
No vida, přeci jen nějaká změna. Místo toho, aby klávesa backspace smazala znak, vypíše se stříška a otazník. A v programu se uloží kód backspace, jako by se jednalo o normální znak. Při výpisu se místo toho zobrazí nějaký nesmysly, já sem tu pro ilustraci použil znak #. (Ano, zmáčkl jsem backspace 3x.)
A co takle nastavit min a time na 0?
$ ./isatty
OK, můžeme pracovat :-)
Něco mi řekni: "" říkáš?
Končím!
Funkce fgets()
tentokrát hned dostala info o konci
souboru, takže na ni nečekala a vrátila se bez načtení čehokoliv.
Vlatně se divím, že program neskončil neoprávněným přistupem do paměti,
protože řádka v kódu tmp[strlen(tmp) - 1] = '\0';
předpokládá, že byl načten alespoň jeden znak (znak nového řádku), což nebyl.
To je tak, když člověk nekontroluje návratové hodnoty, že :-).
A nezapomeňte si na konec po sobě uklidit.
Knihovna termios.h
Tak jo, konečně se dostává řada na knihvnu termios.h.
Informace o terminálu se získávají a nastavují pomocí struktury
struct termios
.
tcflag_t c_iflag;
tcflag_t c_oflag;
tcflag_t c_cflag;
tcflag_t c_lflag;
cc_t c_cc[];
}
Položka c_iflag
slouží k nastavení příznaků pro vstupní režim (input),
c_oflag
pro výstupní režim a c_cflag
řídí hardwarové
charakteristiky terminálu. Tyto příznaky jsou více méně nezajímavé (zvláště
c_cflag
je pro emulátory terminálů k ničemu).
Zajímavá je položka c_lflag
(lokální režim). Ta slouží k nastavování různých
charakteristik terminálu, jako je echo nebo kanonický režim.
Pole c_cc
se používá k nastavování různých speciálních znaků, jako
jsou např. min a max hodnoty pro nekanonický režim. Délka pole je „neznámá“.
K prvkům se přistupuje pomocí definovaných maker.
Změna vlastností terminálu probýhá obvykle tak, že se nejdřív načte současné nastavení terminálu do struktury termios a uloží se někam bokem, zkopíruje se, kopie se upraví a použije se pro nové nastavení terminálu. Při skončení programu se nastaví původní struktura termios.
K získání aktuálního nastavení se používá funkce tcgetattr()
a k nastavení
funkce tcsetattr()
.
První argument je dekriptor souborového proudu, pro který se příznaky získávají nebo
nastavují. Poslední je odkaz na strukturu termios. Funkce tcsetattr()
má ještě jeden parametr, optional_actions, který může nabývat jedné
z těchto hodnot:
optional_action | Význam |
---|---|
TSCANOW | změny se nastaví ihned |
TCSADRAIN | změny se nastaví po dokončení aktuálního vstupu |
TCSAFLUSH | zruší se všechen doposud zadaný vstup a změny se nastaví ihned |
Pořád tu mluvím o nějakých příznacích, ale ještě jsem neřekl, jaké to vlastně jsou.
Zmíním jenom tři nejzajímavější, pro položku c_lflag
.
- ECHO
- Zapíná echo režim.
- ISIG
- Povoluje generování signálů pro kláv. zkratky CTRL+c atp.
- ICANON
- Zapíná kanonický režim.
Tyto tři příznaky v příkladu vypnu. Tj. vypnu echo, vypnu signály a vypnu kanonický režim.
V poli c_cc
nastavím hodnoty min a time
pro nekanonický režim na 1 a 0.
Teď už zbývá vysvětlit jen použití funkce atexit()
z knihovny
<stdlib.h>. Abych si byl jistý, že se po skončení programu obnoví původní nastavení terminálu,
registroval jsem funkci obnovTerminal()
pomocí atexit()
. Ať už program skončí
normálním návratem z funkce main()
, nebo zavoláním exit()
kdekoliv v programu,
nebo když program ukončí uživatel stiskem CTRL+c,
funkce obnovTerminal()
bude zavolána. V příkladu je to tak trochu zbytečné, protože
program po změně terminálu končí jen na jednom místě
( return EXIT_SUCCESS;
) a ukončení pomocí CTRL+c je zakázáno.
- /*------------------------------------------------*/
- /* 50terminal/termios.c */
- #include <termios.h>
- #include <errno.h>
- #include <stdlib.h>
- #include <stdio.h>
- {
- tcsetattr(fileno(stdin), TCSANOW, &oldone);
- }
- {
- char ch;
- struct termios newone;
- tcgetattr(fileno(stdin), &oldone);
- /* pozor, c_cc pro kanonicky a nekanonicky rezim se muzou lisit,
- * znamenaji pro kazdy rezim neco trochu jineho,
- * nemeli by se zamenovat */
- newone = oldone;
- newone.c_lflag &= ~ECHO;
- newone.c_lflag &= ~ISIG;
- newone.c_lflag &= ~ICANON;
- newone.c_cc[VMIN] = 1;
- newone.c_cc[VTIME] = 0;
- return EXIT_FAILURE;
- }
- /* nezapomenout na obnoveni nastaveni terminalu! */
- do {
- return EXIT_SUCCESS;
- }
- /*------------------------------------------------*/
Výstup z programu může vypadat třeba takto:
$ ./termios Zadávání znaků ukončíte klávesou Enter: ch = S ch = a ch = l ch = l ch = y ch = x ch = # ch = # ch = Obnovuji nastavení terminálu
Všiměte si, že není vidět co píšu :-). Vidíte jen to, co vytiskl program.
Předposlední dva znaky ukazují můj pokus o ukončení programu pomocí Ctrl+c. Protože jsem ale vypnul generování signálů, k ukončení programu nedošlo. Místo toho se na obrazovku vypsal nějaký nesmysl, který jsem tady nahradil znaky #. Posledním zadaným znakem byl enter.
Knihovna termios.h poskytuje řadu funkcí a maker, které můžete využít pro přenositelnou (v rámci linuxů a unixů) práci s terminálem. Pokud vám stačí to, co jsem ukázal v tomto posledním příkladu, tak si s tím určitě vystačíte. Pokud budete chtít ale něco víc, například pracovat s barvami, pohybovat kurzorem po obrazovce atp., neobejdete se bez pracné práce s různými řídícími sekvencemi. Existuje ale daleko jednodušší způsob jak na to – knihovna <ncurses.h>. O tom bude příští kapitola.