Esercitazione 5 - 2 dicembre 2016 (120 min)
[
soluzioni ]
Esercizio 1 - shell comandi minimale
Lo scopo dell'esercizio è sviluppare una shell minimale che realizza una CLI (command-line interface) in grado di eseguire:
- comandi interni (eseguiti cioè direttamente dalla shell), da realizzare nell'esercizio 2;
- comandi esterni (programmi lanciati dalla shell come processi figli).
Si lavori sullo scheletro fornito di seguito, complentando le funzioni:
- do_cmd_loop: esegue il ciclo di lettura/esecuzione dei comandi della shell;
- do_external_cmd: lancia un nuovo processo per eseguire un comando esterno e ne attende la terminazione;
- make_argv: costruisce la lista argv da fornire alla system call exec a partire da un array di token estratti dalla riga di comando (nel nostro contesto un token è semplicemente una sottostringa della riga di comando delimitata da uno spazio o da un ritorno a capo).
Seguire le indicazioni e i suggerimenti contenuti come commenti nel codice.
#include <stdio.h> // printf, puts
#include <string.h> // strtok, strcpy
#define MAX_LINE_SIZE 1024 // massima lunghezza riga di comando
#define MAX_NUM_ARGS 64 // massimo numero di token sulla riga di comando
// ---------------------------------------------------------------------
// parse_cmd_line -- [fornita]
// ---------------------------------------------------------------------
// estrae argomenti dalla riga di comando
// parametri:
// - cmd_line: [input] stringa di input contenente la riga di comando
// - tok_buf: [output] array in cui scrivere i token della riga di comando
// - tok_num_ptr: [output] puntatore a buffer in cui scrivere numero di token in tok_buf
void parse_cmd_line(char cmd_line[MAX_LINE_SIZE],
char tok_buf[MAX_NUM_ARGS][MAX_LINE_SIZE],
unsigned* tok_num_ptr) {
unsigned tok_num = 0; // numero token estratti da cmd_line
char buf[MAX_LINE_SIZE]; // buffer per non modificare cmd_line passata
strcpy(buf, cmd_line); // copia cmd_line: strtok fa side-effect
const char* delim = " \n\t"; // delimitatori tokenizzazione
char* token = strtok(buf, delim); // tokenizza buffer
while (token != NULL) {
strcpy(tok_buf[tok_num], token); // copia token corrente in tok_buf
token = strtok(NULL, delim); // estrae token successivo
tok_num++;
}
*tok_num_ptr = tok_num; // passa numero token estratti al chiamante
}
// ---------------------------------------------------------------------
// do_internal_cmd -- [fornita]
// ---------------------------------------------------------------------
// prova ad eseguire comando interno
// parametri:
// - tok_buf: [input] array di token della riga di comando
// - tok_num: [input] numero di token in tok_buf
// restituisce:
// -1 se quit
// 0 se non e' un comando interno
// 1 se e' un comando interno
int do_internal_cmd(char tok_buf[MAX_NUM_ARGS][MAX_LINE_SIZE],
unsigned tok_num) {
if (strcmp(tok_buf[0], "quit") == 0) return -1;
// altri comandi interni qui (vedi esercizio 2)...
// if (strcmp(tok_buf[0], "miocomando") {
// do_mio_comando_interno(tok_buf, tok_num); // esegue cmd interno
// return 1; // cmd eseguito
// }
return 0;
}
// ---------------------------------------------------------------------
// make_argv -- [da completare]
// ---------------------------------------------------------------------
// crea array di puntatori argv per comando esterno
// parametri:
// - argv: [output] buffer preallocato di puntatori agli argomenti del comando
// - tok_buf: [input] array di token della riga di comando
// - tok_num: [input] numero di token in tok_buf
void make_argv(char* argv[MAX_NUM_ARGS+1],
char tok_buf[MAX_NUM_ARGS][MAX_LINE_SIZE],
unsigned tok_num) {
// esempio risultato della funzione
// input:
// tok_num = 2
// tok_buf[0] = "/bin/ls"
// tok_buf[1] = "-l"
// output:
// argv[0] = "ls" (puntatore alla sottostringa ls in
// tok_buf[0])
// argv[1] = "-l" (puntatore a tok_buf[1])
// argv[2] = NULL (terminatore)
// note:
// 1. argv[i] deve puntare a zone di memoria gia' allocate
// all'interno dei buffer tok_buf e argv0
// 2. per inizializzare argv[0], si suggerisce di farlo puntare alla
// sottostringa di tok_buf[0] che rappresenta il nome del file
// (esempio, ls in /bin/ls) -- si suggerisce di usare la funzione
// POSIX strrchr
}
// ---------------------------------------------------------------------
// do_external_cmd -- [da completare]
// ---------------------------------------------------------------------
// esegue comando esterno
// parametri:
// - tok_buf: [input] array di token della riga di comando
// - tok_num: [input] numero di token in tok_buf
void do_external_cmd(char tok_buf[MAX_NUM_ARGS][MAX_LINE_SIZE],
unsigned tok_num) {
char* argv[MAX_NUM_ARGS+1]; // argv da passare a processo
make_argv(argv, tok_buf, tok_num); // crea argv per processo
// lancia un nuovo processo che esegue il comando esterno e ne
// attende la terminazione usando i parametri passati in argv
// usare lo schema fork/execvp/wait e gestire gli errori stampandoli
// con perror
}
// ---------------------------------------------------------------------
// do_cmd_loop -- [da completare]
// ---------------------------------------------------------------------
// esegue il ciclo di lettura/esecuzione dei comandi
void do_cmd_loop() {
char cmd_line[MAX_LINE_SIZE]; // buffer per contenere riga di cmd
char tok_buf[MAX_NUM_ARGS][MAX_LINE_SIZE]; // buffer token riga di comando
unsigned tok_num; // numero token riga di comando
// completa come segue:
// ciclo che ripete i seguenti passi
// 1. stampa su stdout il prompt dei comandi, es. "SC> "
// 2. legge da stdin una riga in cmd_line (usare fgets)
// ed esce dalla funzione se il canale stdin è terminato (CRTL+D durante fgets fornisce NULL)
// 3. chiama parse_cmd_line per estrarre i token da cmd_line e
// copiarli in tok_buf, con il loro numero in tok_num
// 4. se sono stati immessi token, allora prova ad eseguire
// il comando come interno richiamando do_internal_cmd: se non e'
// interno e non e' quit prova ad eseguire il comando come
// esterno. Se e' quit esce dalla funzione.
}
// ---------------------------------------------------------------------
// main -- [fornito]
// ---------------------------------------------------------------------
int main() {
do_cmd_loop();
puts("\nsayonara.");
return 0;
}
Esercizio 2 - estensione shell con comandi interni
In questo esercizio è richiesta l'implementazione dei due comandi interni
pwd e
cd.
Osservazione: i comandi interni devono essere implementati modificando la funzione
do_internal_cmd della shell.
Nota bene: lo studente
deve gestire correttamente eventuali errori dovuti all'invocazione di funzioni POSIX. In particolare, in caso di errore, occorre verificare l'errore utilizzando la variabile esterna
errno ed emettere un messaggio di errore utilizzando la funzione POSIX
perror.
Il comando
pwd deve supportare la seguente sintassi:
pwd
L'output del comando è la
working directory corrente. L'implementazione deve far uso della funzione POSIX
getcwd.
Il comando
cd deve supportare la seguente sintassi:
cd <path>
Il comando deve cambiare la
working directory della shell in base all'argomento
path. In caso di path non valido, un errore deve essere emesso in standard output dalla shell. L'implementazione deve far uso della funzione POSIX
chdir.
Esercizio 3 - comando esterno cat
In questo esercizio è richiesto di implementare il comando esterno unix
cat. Per i nostri scopi, il comando
cat dovrà supportare la seguente sintassi:
cat [<path-to-file>]
Se viene indicato un file (
path-to-file), allora
cat legge il contenuto del file e lo stampa in standard output. Se il file viene omesso,
cat legge dallo standard input. E' richiesto di implementare
cat utilizzando le funzioni POSIX
open,
close,
read,
write, e
exit. Eventuali errori ritornati da queste funzioni devono essere opportunamente verificati e gestiti. Sviluppare un implementazione di
cat in base al seguente scheletro:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
// ---------------------------------------------------------------------
// is_regular_file
// ---------------------------------------------------------------------
// Funzione che verifica se un path è relativo ad un file regolare.
// Non occorre modificare questa funzione. Va invocata ove necessario.
// Parametri:
// - path: un percorso nel filesystem
// Restituisce:
// - 0 se il path è un file regolare,
// - altrimenti -1
int is_regular_file(const char *path) {
struct stat path_stat;
int res = stat(path, &path_stat);
if (res == 0 && S_ISREG(path_stat.st_mode) == 1)
return 0;
else
return -1;
}
// ---------------------------------------------------------------------
// get_read_descriptor
// ---------------------------------------------------------------------
// Funzione che ritorna il file descriptor in lettura
// In caso di errore, termina l'esecuzione del programma.
// Parametri (uguali alla funzione main):
// - argc: numero di argomenti passati al comando
// - argv: argomenti passati al programma
// Restituisce: il file descriptor
int get_read_descriptor(int argc, char * argv[