Questa bash shell permette di:
$? per l’ultimo stato di uscita.La shell dovrà avere un ciclo principale che:
readline(), che fornisce anche una history e la modifica della linea).cd, echo, ecc.); in questo caso lo esegue direttamente.fork() e execve().readline() per mostrare il prompt e catturare l’input dell’utente. Questa funzione gestisce anche la history in automatico. Esempio:
char *line = readline("minishell> ");
if (line == NULL) { // Ad esempio, quando viene premuto ctrl-D (EOF)
printf("exit\n");
exit(0);
}
if (*line)
add_history(line);
volatile sig_atomic_t) per registrare il numero del segnale ricevuto. Questa variabile non deve fornire altro tipo di informazione o accesso a strutture dati interne. Ad esempio:
volatile sig_atomic_t g_signal = 0;
void signal_handler(int signum) {
g_signal = signum;
// In modalità interattiva, per ctrl-C potresti voler stampare una nuova riga
// o impostare un flag per il ciclo principale.
}
sigaction() per impostare il gestore dei segnali per SIGINT (ctrl-C) e SIGQUIT (ctrl-):
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // Riavvia automaticamente le syscall interrotte
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
readline() restituisce NULL, la shell deve terminare.Implementa un parser che faccia le seguenti operazioni:
'): il contenuto compreso tra due virgolette singole va interpretato letteralmente."): il contenuto va preso letteralmente, ad eccezione del carattere $ che serve per l’espansione delle variabili.<, >, >>, << per le redirezioni e il carattere | per le pipe.$ (oppure $?), sostituisci il token con il valore della variabile d’ambiente (oppure con l’ultimo codice di uscita, che puoi memorizzare in una variabile locale ad ogni esecuzione del comando).Suggerimento: Scrivi una funzione dedicata (o un insieme di funzioni) che scansiona la stringa di input e costruisce una lista di token (o nodi) che saranno successivamente usati per costruire la struttura dei comandi.
Definisci una struttura per rappresentare un comando. Ad esempio:
typedef enum {
CMD_SIMPLE,
CMD_PIPELINE
} cmd_type_t;
typedef struct s_redir {
int type; // ad esempio: IN, OUT, APPEND, HEREDOC
char *filename; // il file o, in caso di heredoc, il delimitatore
} redir_t;
typedef struct s_command {
char **args; // vettore degli argomenti (argv)
redir_t *redirs; // array dinamico degli oggetti di redirezione
int num_redirs;
} command_t;
typedef struct s_pipeline {
command_t *commands; // array dinamico dei comandi della pipeline
int num_commands;
} pipeline_t;
<, >, >> e <<. Nel caso del heredoc (<<), dovrai chiedere all’utente di inserire le righe fino a quando non viene digitato il delimitatore. Il contenuto del heredoc può essere memorizzato in un file temporaneo o in una pipe da usare nel processo figlio.Implementa le seguenti funzioni builtins, ognuna in funzione separata:
-n.chdir() per cambiare directory; accetta solo percorsi relativi o assoluti.getcwd()).setenv()).unsetenv()).Quando viene analizzato un comando, controlla se il primo token corrisponde a uno di questi builtins. Se sì, esegui la funzione corrispondente e non esegui un fork().
Se il comando non è un builtin, procedi così:
Fork del processo figlio:
Crea un nuovo processo con fork().
Nel processo figlio:
<: apri il file in lettura e duplica il descrittore nel file descriptor standard input (STDIN).>: apri/crea il file in scrittura (troncandolo) e duplica su STDOUT.>>: apri/crea il file in modalità append e duplica su STDOUT.<< (heredoc): prepara una pipe o un file temporaneo con il contenuto heredoc e redirigi STDIN.access() iterando sui percorsi contenuti in PATH.execve() per eseguire il comando. Se l’exec fallisce, stampa un messaggio d’errore e chiudi il processo figlio.Nel processo padre:
waitpid().$?).// Supponiamo che pipeline->num_commands >= 1
int prev_pipe_fd[2] = {-1, -1};
for (int i = 0; i < pipeline->num_commands; i++) {
int pipe_fd[2] = {-1, -1};
if (i < pipeline->num_commands - 1) {
if (pipe(pipe_fd) < 0) {
perror("pipe");
// Gestisci l'errore
}
}
pid_t pid = fork();
if (pid == 0) {
// Processo figlio
if (i > 0) {
// Collega l'estremità di lettura della pipe precedente allo STDIN
dup2(prev_pipe_fd[0], STDIN_FILENO);
close(prev_pipe_fd[0]);
close(prev_pipe_fd[1]);
}
if (i < pipeline->num_commands - 1) {
// Collega l'estremità di scrittura della pipe corrente allo STDOUT
dup2(pipe_fd[1], STDOUT_FILENO);
close(pipe_fd[0]);
close(pipe_fd[1]);
}
// Applica eventuali redirezioni specifiche per questo comando
// Esegui il comando
execve(...);
perror("execve");
exit(1);
} else if (pid < 0) {
perror("fork");
// Gestisci l'errore
}
// Nel processo padre: chiudi le estremità della pipe utilizzate
if (i > 0) {
close(prev_pipe_fd[0]);
close(prev_pipe_fd[1]);
}
prev_pipe_fd[0] = pipe_fd[0];
prev_pipe_fd[1] = pipe_fd[1];
}
// Attendi la terminazione di tutti i processi figli
while (wait(NULL) > 0)
;
Durante il parsing:
$:
$?, sostituiscilo con una stringa che rappresenta l’ultimo stato di uscita.getenv() per ottenere il valore della variabile e sostituisci il token (o parte di esso) con il valore corrispondente.$ e lascia invariati gli altri caratteri speciali.Puoi scrivere una funzione dedicata (ad esempio char *expand_vars(const char *token)) che scorra il token, rilevi i caratteri $ e costruisca una nuova stringa con le sostituzioni appropriate.
free() una volta eseguito il comando.readline() dopo aver elaborato la linea.Nota: La funzione readline() può generare alcune perdite di memoria che puoi ignorare, ma il codice che scrivi tu non deve avere perdite.
Un esempio semplificato del ciclo principale potrebbe essere:
int main(void) {
// Imposta i gestori dei segnali
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
// Ciclo principale della shell
while (1) {
// Mostra il prompt e leggi l'input
char *line = readline("minishell> ");
if (!line) { // Se viene premuto ctrl-D
printf("exit\n");
break;
}
if (*line) {
add_history(line);
// Parso l'input e costruisco la struttura del comando
command_t *cmd = parse_input(line);
if (!cmd) {
free(line);
continue;
}
// Verifico se si tratta di una pipeline o di un comando semplice
if (is_pipeline(cmd)) {
execute_pipeline(cmd);
} else {
// Controlla se il comando è un builtin
if (is_builtin(cmd->args[0])) {
execute_builtin(cmd);
} else {
execute_external_command(cmd);
}
}
// Libera la memoria associata alla struttura del comando
free_command(cmd);
}
free(line);
}
return 0;
}
Le funzioni parse_input(), is_pipeline(), execute_pipeline(), is_builtin(), execute_builtin(), execute_external_command() e free_command() devono essere implementate separatamente, ciascuna con una responsabilità chiara (ad esempio, parse_input() si occupa della tokenizzazione e del controllo sintattico, mentre execute_external_command() si occupa del fork e dell’exec).
readline()), i segnali (ctrl-C, ctrl-D, ctrl-) producano il comportamento atteso.readline()).