1122 行
34 KiB
C
1122 行
34 KiB
C
/*
|
|
* psh - A minimal bash-like shell implemented in pure C (Windows version)
|
|
*
|
|
* Features:
|
|
* - Command execution with CreateProcess
|
|
* - Built-in commands: cd, pwd, echo, exit, help, env, history
|
|
* - Input piping (|)
|
|
* - Output redirection (>, >>)
|
|
* - Background execution (&)
|
|
* - Command history with up/down arrows
|
|
* - Tab completion (basic)
|
|
* - && and || operators
|
|
* - Simple environment variable expansion
|
|
*/
|
|
|
|
#define _WIN32_WINNT 0x0501
|
|
#include <windows.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <ctype.h>
|
|
#include <signal.h>
|
|
#include <conio.h>
|
|
#include <direct.h>
|
|
#include <process.h>
|
|
|
|
#ifdef __GNUC__
|
|
#ifdef HAVE_READLINE
|
|
#include <readline/readline.h>
|
|
#include <readline/history.h>
|
|
#endif
|
|
#else
|
|
/* Minimal readline replacement for MSVC */
|
|
#define rl_on_new_line()
|
|
#define rl_replace_line(text, clear) strcpy(rl_line_buffer, text); rl_point = strlen(text)
|
|
#define add_history(s) /* not implemented */
|
|
extern char *readline(const char *prompt);
|
|
extern int rl_done;
|
|
#endif
|
|
|
|
#define MAX_ARGS 256
|
|
#define MAX_LINE 8192
|
|
#define MAX_HIST 512
|
|
#define MAX_CMDS 64
|
|
#define MAX_ENV_VARS 256
|
|
#define MAX_PATH_LEN 260
|
|
|
|
/* Token types for the parser */
|
|
typedef enum {
|
|
TOKEN_WORD,
|
|
TOKEN_PIPE,
|
|
TOKEN_REDIR_OUT, /* > */
|
|
TOKEN_REDIR_APP, /* >> */
|
|
TOKEN_REDIR_IN, /* < */
|
|
TOKEN_BG, /* & */
|
|
TOKEN_AND, /* && */
|
|
TOKEN_OR, /* || */
|
|
TOKEN_SUBSTITUTE /* ; */
|
|
} TokenType_t;
|
|
|
|
typedef struct {
|
|
char *text;
|
|
TokenType_t type;
|
|
} Token;
|
|
|
|
typedef struct {
|
|
char **argv;
|
|
int argc;
|
|
int bg; /* background flag */
|
|
char *input_redir; /* for < */
|
|
char *output_redir; /* for > */
|
|
char *append_redir; /* for >> */
|
|
int exit_code; /* exit code of the command */
|
|
} SimpleCmd;
|
|
|
|
typedef struct {
|
|
SimpleCmd cmds[MAX_CMDS];
|
|
int cmd_count;
|
|
} Pipeline;
|
|
|
|
/* Global state */
|
|
static char *history[MAX_HIST];
|
|
static int hist_size = 0;
|
|
|
|
/* Tab completion state */
|
|
static const char *completion_dirs[64];
|
|
static int completion_dir_count = 0;
|
|
|
|
/* Command line state for custom reader */
|
|
static char cmd_line_buf[MAX_LINE];
|
|
static int cmd_line_pos = 0;
|
|
static int cmd_line_len = 0;
|
|
static int cmd_hist_idx = 0;
|
|
static int cmd_hist_pos = 0;
|
|
|
|
/* Environment variables */
|
|
static char *env_table[256];
|
|
static int env_count = 0;
|
|
|
|
/* Forward declarations */
|
|
static char *get_env_var(const char *name);
|
|
static void set_env_var(const char *name, const char *value);
|
|
static void free_argv(char **argv);
|
|
static void free_pipeline(Pipeline *p);
|
|
static int tokenize_line(const char *line, Token *tokens, int max_tokens);
|
|
static int parse_pipeline(Token *tokens, int token_count, Pipeline *pipeline);
|
|
static int execute_simple(SimpleCmd *cmd);
|
|
static int execute_pipeline(Pipeline *pipeline);
|
|
static int execute_command_list(Token *tokens, int token_count);
|
|
static void free_tokens(Token *tokens, int count);
|
|
static void handle_exit(int argc, char **argv);
|
|
static void handle_cd(int argc, char **argv);
|
|
static void handle_pwd(char **argv);
|
|
static void handle_echo(int argc, char **argv);
|
|
static void handle_env(int argc, char **argv);
|
|
static void handle_help();
|
|
static int is_builtin(const char *cmd);
|
|
static int run_builtin(int argc, char **argv);
|
|
static void add_history_entry(const char *line);
|
|
static void print_prompt();
|
|
static void cleanup_history();
|
|
static void init_command_line();
|
|
static char *read_command_line(const char *prompt);
|
|
static void free_command_line();
|
|
static void build_completion_list(const char *prefix);
|
|
static void try_tab_completion(char *line, int *pos);
|
|
|
|
/* Get environment variable value */
|
|
static char *get_env_var(const char *name) {
|
|
char prefix[512];
|
|
snprintf(prefix, sizeof(prefix), "%s=", name);
|
|
|
|
for (int i = 0; i < env_count; i++) {
|
|
if (strncmp(env_table[i], prefix, strlen(prefix)) == 0) {
|
|
return env_table[i] + strlen(prefix);
|
|
}
|
|
}
|
|
return getenv(name);
|
|
}
|
|
|
|
/* Set environment variable */
|
|
static void set_env_var(const char *name, const char *value) {
|
|
char assignment[1024];
|
|
snprintf(assignment, sizeof(assignment), "%s=%s", name, value);
|
|
|
|
/* Check if already exists */
|
|
char prefix[512];
|
|
snprintf(prefix, sizeof(prefix), "%s=", name);
|
|
|
|
for (int i = 0; i < env_count; i++) {
|
|
if (strncmp(env_table[i], prefix, strlen(prefix)) == 0) {
|
|
free(env_table[i]);
|
|
env_table[i] = strdup(assignment);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (env_count < MAX_ENV_VARS) {
|
|
env_table[env_count++] = strdup(assignment);
|
|
}
|
|
}
|
|
|
|
/* Free an argv array */
|
|
static void free_argv(char **argv) {
|
|
if (!argv) return;
|
|
for (int i = 0; argv[i]; i++) {
|
|
free(argv[i]);
|
|
}
|
|
free(argv);
|
|
}
|
|
|
|
/* Tokenize input line */
|
|
static int tokenize_line(const char *line, Token *tokens, int max_tokens) {
|
|
int token_count = 0;
|
|
const char *p = line;
|
|
int in_single = 0, in_double = 0;
|
|
char *word_buf = NULL;
|
|
int word_len = 0;
|
|
int word_cap = 64;
|
|
|
|
word_buf = malloc(word_cap);
|
|
if (!word_buf) return 0;
|
|
word_buf[0] = '\0';
|
|
|
|
while (*p && token_count < max_tokens) {
|
|
/* Skip whitespace */
|
|
if (isspace((unsigned char)*p) && !in_single && !in_double) {
|
|
p++;
|
|
continue;
|
|
}
|
|
|
|
/* Handle quotes */
|
|
if (*p == '\'' && !in_double) {
|
|
in_single = !in_single;
|
|
p++;
|
|
continue;
|
|
}
|
|
if (*p == '"' && !in_single) {
|
|
in_double = !in_double;
|
|
p++;
|
|
continue;
|
|
}
|
|
|
|
/* Handle special tokens */
|
|
if (!in_single && !in_double) {
|
|
if (strncmp(p, "&&", 2) == 0) {
|
|
tokens[token_count++].type = TOKEN_AND;
|
|
p += 2;
|
|
continue;
|
|
}
|
|
if (strncmp(p, "||", 2) == 0) {
|
|
tokens[token_count++].type = TOKEN_OR;
|
|
p += 2;
|
|
continue;
|
|
}
|
|
if (*p == '|') {
|
|
tokens[token_count++].type = TOKEN_PIPE;
|
|
p++;
|
|
continue;
|
|
}
|
|
if (*p == '>') {
|
|
if (*(p+1) == '>') {
|
|
tokens[token_count++].type = TOKEN_REDIR_APP;
|
|
p += 2;
|
|
} else {
|
|
tokens[token_count++].type = TOKEN_REDIR_OUT;
|
|
p++;
|
|
}
|
|
continue;
|
|
}
|
|
if (*p == '<') {
|
|
tokens[token_count++].type = TOKEN_REDIR_IN;
|
|
p++;
|
|
continue;
|
|
}
|
|
if (*p == '&') {
|
|
tokens[token_count++].type = TOKEN_BG;
|
|
p++;
|
|
continue;
|
|
}
|
|
if (*p == ';') {
|
|
tokens[token_count++].type = TOKEN_SUBSTITUTE;
|
|
p++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* Collect word characters */
|
|
if (word_len + 1 >= word_cap) {
|
|
word_cap *= 2;
|
|
word_buf = realloc(word_buf, word_cap);
|
|
}
|
|
word_buf[word_len++] = *p++;
|
|
word_buf[word_len] = '\0';
|
|
}
|
|
|
|
/* Add final word if any */
|
|
if (word_len > 0 && token_count < max_tokens) {
|
|
tokens[token_count].type = TOKEN_WORD;
|
|
tokens[token_count].text = strdup(word_buf);
|
|
token_count++;
|
|
}
|
|
|
|
free(word_buf);
|
|
return token_count;
|
|
}
|
|
|
|
static void free_tokens(Token *tokens, int count) {
|
|
for (int i = 0; i < count; i++) {
|
|
if (tokens[i].type == TOKEN_WORD && tokens[i].text) {
|
|
free(tokens[i].text);
|
|
tokens[i].text = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Parse tokens into pipeline structure */
|
|
static int parse_pipeline(Token *tokens, int token_count, Pipeline *pipeline) {
|
|
memset(pipeline, 0, sizeof(Pipeline));
|
|
|
|
int cmd_idx = 0;
|
|
int arg_idx = 0;
|
|
int i = 0;
|
|
|
|
pipeline->cmds[cmd_idx].argv = malloc(sizeof(char*) * MAX_ARGS);
|
|
if (!pipeline->cmds[cmd_idx].argv) return -1;
|
|
memset(pipeline->cmds[cmd_idx].argv, 0, sizeof(char*) * MAX_ARGS);
|
|
pipeline->cmds[cmd_idx].argc = 0;
|
|
|
|
while (i < token_count) {
|
|
Token *t = &tokens[i];
|
|
|
|
switch (t->type) {
|
|
case TOKEN_WORD:
|
|
if (arg_idx < MAX_ARGS - 1) {
|
|
pipeline->cmds[cmd_idx].argv[arg_idx++] = t->text;
|
|
pipeline->cmds[cmd_idx].argc++;
|
|
}
|
|
t->text = NULL; /* Transfer ownership */
|
|
break;
|
|
|
|
case TOKEN_PIPE:
|
|
pipeline->cmds[cmd_idx].argv[arg_idx] = NULL;
|
|
cmd_idx++;
|
|
if (cmd_idx >= MAX_CMDS) goto done;
|
|
pipeline->cmds[cmd_idx].argv = malloc(sizeof(char*) * MAX_ARGS);
|
|
if (!pipeline->cmds[cmd_idx].argv) goto done;
|
|
memset(pipeline->cmds[cmd_idx].argv, 0, sizeof(char*) * MAX_ARGS);
|
|
pipeline->cmds[cmd_idx].argc = 0;
|
|
arg_idx = 0;
|
|
break;
|
|
|
|
case TOKEN_REDIR_OUT:
|
|
i++;
|
|
if (i < token_count && tokens[i].type == TOKEN_WORD) {
|
|
pipeline->cmds[cmd_idx].output_redir = tokens[i].text;
|
|
tokens[i].text = NULL;
|
|
}
|
|
break;
|
|
|
|
case TOKEN_REDIR_APP:
|
|
i++;
|
|
if (i < token_count && tokens[i].type == TOKEN_WORD) {
|
|
pipeline->cmds[cmd_idx].append_redir = tokens[i].text;
|
|
tokens[i].text = NULL;
|
|
}
|
|
break;
|
|
|
|
case TOKEN_REDIR_IN:
|
|
i++;
|
|
if (i < token_count && tokens[i].type == TOKEN_WORD) {
|
|
pipeline->cmds[cmd_idx].input_redir = tokens[i].text;
|
|
tokens[i].text = NULL;
|
|
}
|
|
break;
|
|
|
|
case TOKEN_BG:
|
|
pipeline->cmds[cmd_idx].bg = 1;
|
|
break;
|
|
|
|
case TOKEN_AND:
|
|
case TOKEN_OR:
|
|
case TOKEN_SUBSTITUTE:
|
|
/* These are handled in execute_command_list */
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
pipeline->cmds[cmd_idx].argv[arg_idx] = NULL;
|
|
pipeline->cmd_count = cmd_idx + 1;
|
|
|
|
done:
|
|
return 0;
|
|
}
|
|
|
|
static void free_pipeline(Pipeline *p) {
|
|
for (int i = 0; i < p->cmd_count; i++) {
|
|
if (p->cmds[i].argv) free_argv(p->cmds[i].argv);
|
|
if (p->cmds[i].output_redir) free(p->cmds[i].output_redir);
|
|
if (p->cmds[i].append_redir) free(p->cmds[i].append_redir);
|
|
if (p->cmds[i].input_redir) free(p->cmds[i].input_redir);
|
|
}
|
|
}
|
|
|
|
/* Execute a single simple command using CreateProcess */
|
|
static int execute_simple(SimpleCmd *cmd) {
|
|
if (cmd->argc == 0) {
|
|
cmd->exit_code = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* Build command line */
|
|
char cmd_line[MAX_LINE * 2];
|
|
cmd_line[0] = '\0';
|
|
for (int i = 0; i < cmd->argc; i++) {
|
|
if (i > 0) strcat(cmd_line, " ");
|
|
/* Quote arguments with spaces */
|
|
if (strchr(cmd->argv[i], ' ') || strchr(cmd->argv[i], '\t')) {
|
|
strcat(cmd_line, "\"");
|
|
strcat(cmd_line, cmd->argv[i]);
|
|
strcat(cmd_line, "\"");
|
|
} else {
|
|
strcat(cmd_line, cmd->argv[i]);
|
|
}
|
|
}
|
|
|
|
/* Set up startup info */
|
|
STARTUPINFOA si;
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
|
|
|
PROCESS_INFORMATION pi;
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
/* Handle redirections */
|
|
if (cmd->input_redir || cmd->output_redir || cmd->append_redir) {
|
|
char full_cmd[MAX_LINE * 3];
|
|
sprintf(full_cmd, "cmd /c \"%s\"", cmd_line);
|
|
|
|
if (cmd->input_redir) {
|
|
strcat(full_cmd, " < ");
|
|
strcat(full_cmd, cmd->input_redir);
|
|
}
|
|
if (cmd->output_redir) {
|
|
strcat(full_cmd, " > ");
|
|
strcat(full_cmd, cmd->output_redir);
|
|
} else if (cmd->append_redir) {
|
|
strcat(full_cmd, " >> ");
|
|
strcat(full_cmd, cmd->append_redir);
|
|
}
|
|
|
|
if (!CreateProcessA(NULL, full_cmd, NULL, NULL, TRUE,
|
|
cmd->bg ? CREATE_NO_WINDOW : 0,
|
|
NULL, NULL, &si, &pi)) {
|
|
fprintf(stderr, "psh: failed to execute '%s'\n", cmd->argv[0]);
|
|
cmd->exit_code = 127;
|
|
return 1;
|
|
}
|
|
} else {
|
|
if (!CreateProcessA(NULL, cmd_line, NULL, NULL, TRUE,
|
|
cmd->bg ? CREATE_NO_WINDOW : 0,
|
|
NULL, NULL, &si, &pi)) {
|
|
fprintf(stderr, "psh: failed to execute '%s'\n", cmd->argv[0]);
|
|
cmd->exit_code = 127;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/* Parent process */
|
|
if (!cmd->bg) {
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
DWORD exit_code = 0;
|
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
cmd->exit_code = (int)exit_code;
|
|
return (int)exit_code;
|
|
} else {
|
|
printf("[%lu] %lu\n", GetCurrentProcessId(), pi.dwProcessId);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
cmd->exit_code = 0;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Execute a pipeline */
|
|
static int execute_pipeline(Pipeline *pipeline) {
|
|
if (pipeline->cmd_count == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (pipeline->cmd_count == 1) {
|
|
return execute_simple(&pipeline->cmds[0]);
|
|
}
|
|
|
|
/* For multiple commands with pipes, use cmd /c with pipe syntax */
|
|
char cmd_line[MAX_LINE * 4];
|
|
cmd_line[0] = '\0';
|
|
|
|
for (int i = 0; i < pipeline->cmd_count; i++) {
|
|
if (i > 0) strcat(cmd_line, " | ");
|
|
|
|
/* Add command */
|
|
for (int j = 0; j < pipeline->cmds[i].argc; j++) {
|
|
if (j > 0) strcat(cmd_line, " ");
|
|
if (strchr(pipeline->cmds[i].argv[j], ' ')) {
|
|
strcat(cmd_line, "\"");
|
|
strcat(cmd_line, pipeline->cmds[i].argv[j]);
|
|
strcat(cmd_line, "\"");
|
|
} else {
|
|
strcat(cmd_line, pipeline->cmds[i].argv[j]);
|
|
}
|
|
}
|
|
|
|
/* Add redirections */
|
|
if (pipeline->cmds[i].input_redir) {
|
|
strcat(cmd_line, " < ");
|
|
strcat(cmd_line, pipeline->cmds[i].input_redir);
|
|
}
|
|
if (pipeline->cmds[i].output_redir) {
|
|
strcat(cmd_line, " > ");
|
|
strcat(cmd_line, pipeline->cmds[i].output_redir);
|
|
} else if (pipeline->cmds[i].append_redir) {
|
|
strcat(cmd_line, " >> ");
|
|
strcat(cmd_line, pipeline->cmds[i].append_redir);
|
|
}
|
|
}
|
|
|
|
/* Execute via cmd /c */
|
|
char full_cmd[MAX_LINE * 4];
|
|
sprintf(full_cmd, "cmd /c \"%s\"", cmd_line);
|
|
|
|
DWORD flags = pipeline->cmds[pipeline->cmd_count - 1].bg ? CREATE_NO_WINDOW : 0;
|
|
|
|
STARTUPINFOA si;
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
|
|
PROCESS_INFORMATION pi;
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (!CreateProcessA(NULL, full_cmd, NULL, NULL, TRUE, flags,
|
|
NULL, NULL, &si, &pi)) {
|
|
fprintf(stderr, "psh: failed to execute pipeline\n");
|
|
return 1;
|
|
}
|
|
|
|
if (!pipeline->cmds[pipeline->cmd_count - 1].bg) {
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
DWORD exit_code = 0;
|
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
pipeline->cmds[pipeline->cmd_count - 1].exit_code = (int)exit_code;
|
|
return (int)exit_code;
|
|
} else {
|
|
printf("[%lu] %lu\n", GetCurrentProcessId(), pi.dwProcessId);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Execute a command list with && and || operators */
|
|
static int execute_command_list(Token *tokens, int token_count) {
|
|
if (token_count == 0) return 0;
|
|
|
|
/* Find the first && or || operator */
|
|
int op_idx = -1;
|
|
TokenType_t op_type = TOKEN_WORD;
|
|
int i = 0;
|
|
|
|
/* Skip leading semicolons */
|
|
while (i < token_count && tokens[i].type == TOKEN_SUBSTITUTE) i++;
|
|
|
|
/* Find the first AND or OR operator */
|
|
while (i < token_count) {
|
|
if (tokens[i].type == TOKEN_AND || tokens[i].type == TOKEN_OR) {
|
|
/* Check if this is part of && or || */
|
|
if (tokens[i].type == TOKEN_AND) {
|
|
if (i + 1 < token_count && tokens[i + 1].type == TOKEN_AND) {
|
|
op_idx = i;
|
|
op_type = TOKEN_AND;
|
|
i += 2; /* Skip both & tokens */
|
|
break;
|
|
}
|
|
} else if (tokens[i].type == TOKEN_OR) {
|
|
if (i + 1 < token_count && tokens[i + 1].type == TOKEN_OR) {
|
|
op_idx = i;
|
|
op_type = TOKEN_OR;
|
|
i += 2; /* Skip both | tokens */
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
i++;
|
|
}
|
|
|
|
/* If no AND/OR found, try semicolon separation */
|
|
if (op_idx == -1) {
|
|
/* Find semicolon */
|
|
i = 0;
|
|
while (i < token_count) {
|
|
if (tokens[i].type == TOKEN_SUBSTITUTE) {
|
|
op_idx = i;
|
|
op_type = TOKEN_SUBSTITUTE;
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (op_idx == -1) {
|
|
/* No operators found - parse and execute single command */
|
|
Pipeline pipeline;
|
|
memset(&pipeline, 0, sizeof(Pipeline));
|
|
if (parse_pipeline(tokens, token_count, &pipeline) == 0) {
|
|
if (pipeline.cmd_count > 0) {
|
|
if (is_builtin(pipeline.cmds[0].argv[0])) {
|
|
run_builtin(pipeline.cmds[0].argc, pipeline.cmds[0].argv);
|
|
return 0;
|
|
} else {
|
|
return execute_pipeline(&pipeline);
|
|
}
|
|
}
|
|
}
|
|
free_pipeline(&pipeline);
|
|
return 0;
|
|
}
|
|
|
|
/* Split tokens into left and right parts */
|
|
Token left_tokens[MAX_LINE];
|
|
Token right_tokens[MAX_LINE];
|
|
int left_count = 0;
|
|
int right_count = 0;
|
|
|
|
for (int j = 0; j < op_idx; j++) {
|
|
left_tokens[left_count++] = tokens[j];
|
|
}
|
|
|
|
int skip = (op_type == TOKEN_AND || op_type == TOKEN_OR) ? 2 : 1;
|
|
for (int j = op_idx + skip; j < token_count; j++) {
|
|
right_tokens[right_count++] = tokens[j];
|
|
}
|
|
|
|
/* Execute left side */
|
|
Pipeline left_pipeline;
|
|
memset(&left_pipeline, 0, sizeof(left_pipeline));
|
|
if (parse_pipeline(left_tokens, left_count, &left_pipeline) == 0) {
|
|
if (left_pipeline.cmd_count > 0) {
|
|
if (is_builtin(left_pipeline.cmds[0].argv[0])) {
|
|
run_builtin(left_pipeline.cmds[0].argc, left_pipeline.cmds[0].argv);
|
|
} else {
|
|
execute_pipeline(&left_pipeline);
|
|
}
|
|
}
|
|
}
|
|
free_pipeline(&left_pipeline);
|
|
free_tokens(left_tokens, left_count);
|
|
|
|
int left_exit_code = 0;
|
|
if (left_pipeline.cmd_count > 0) {
|
|
left_exit_code = left_pipeline.cmds[left_pipeline.cmd_count - 1].exit_code;
|
|
}
|
|
|
|
/* Execute right side based on operator */
|
|
if (op_type == TOKEN_AND) {
|
|
/* && : only execute right if left succeeded */
|
|
if (left_exit_code != 0) {
|
|
printf("[[&&]] left command failed (exit %d), skipping right\n", left_exit_code);
|
|
free_tokens(right_tokens, right_count);
|
|
return left_exit_code;
|
|
}
|
|
} else if (op_type == TOKEN_OR) {
|
|
/* || : only execute right if left failed */
|
|
if (left_exit_code == 0) {
|
|
printf("[[||]] left command succeeded, skipping right\n");
|
|
free_tokens(right_tokens, right_count);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
Pipeline right_pipeline;
|
|
memset(&right_pipeline, 0, sizeof(right_pipeline));
|
|
if (parse_pipeline(right_tokens, right_count, &right_pipeline) == 0) {
|
|
if (right_pipeline.cmd_count > 0) {
|
|
if (is_builtin(right_pipeline.cmds[0].argv[0])) {
|
|
run_builtin(right_pipeline.cmds[0].argc, right_pipeline.cmds[0].argv);
|
|
} else {
|
|
execute_pipeline(&right_pipeline);
|
|
}
|
|
}
|
|
}
|
|
free_pipeline(&right_pipeline);
|
|
free_tokens(right_tokens, right_count);
|
|
|
|
int right_exit_code = 0;
|
|
if (right_pipeline.cmd_count > 0) {
|
|
right_exit_code = right_pipeline.cmds[right_pipeline.cmd_count - 1].exit_code;
|
|
}
|
|
|
|
return right_exit_code;
|
|
}
|
|
|
|
/* Free history entries */
|
|
static void cleanup_history() {
|
|
for (int i = 0; i < hist_size; i++) {
|
|
if (history[i]) free(history[i]);
|
|
}
|
|
}
|
|
|
|
/* Add entry to history */
|
|
static void add_history_entry(const char *line) {
|
|
if (line && strlen(line) > 0) {
|
|
if (hist_size < MAX_HIST) {
|
|
history[hist_size++] = strdup(line);
|
|
} else {
|
|
/* Shift and replace oldest */
|
|
for (int i = 0; i < hist_size - 1; i++) {
|
|
free(history[i]);
|
|
history[i] = history[i + 1];
|
|
}
|
|
history[hist_size - 1] = strdup(line);
|
|
}
|
|
cmd_hist_idx = hist_size;
|
|
}
|
|
}
|
|
|
|
/* Print prompt */
|
|
static void print_prompt() {
|
|
const char *user = getenv("USERNAME");
|
|
const char *host = getenv("COMPUTERNAME");
|
|
if (!host) host = "localhost";
|
|
if (!user) user = "user";
|
|
|
|
char cwd[MAX_PATH];
|
|
GetCurrentDirectoryA(MAX_PATH, cwd);
|
|
|
|
printf("%s@%s:%s> ", user, host, cwd);
|
|
fflush(stdout);
|
|
}
|
|
|
|
/* Built-in command handlers */
|
|
static void handle_exit(int argc, char **argv) {
|
|
(void)argc;
|
|
(void)argv;
|
|
cleanup_history();
|
|
exit(0);
|
|
}
|
|
|
|
static void handle_cd(int argc, char **argv) {
|
|
const char *dir = (argc > 1) ? argv[1] : getenv("HOME");
|
|
if (!dir) {
|
|
dir = getenv("USERPROFILE");
|
|
}
|
|
if (!dir) {
|
|
dir = ".";
|
|
}
|
|
|
|
if (chdir(dir) != 0) {
|
|
fprintf(stderr, "cd: cannot change directory to '%s': %s\n", dir, strerror(errno));
|
|
}
|
|
}
|
|
|
|
static void handle_pwd(char **argv) {
|
|
(void)argv;
|
|
char buf[MAX_PATH];
|
|
if (GetCurrentDirectoryA(MAX_PATH, buf)) {
|
|
printf("%s\n", buf);
|
|
} else {
|
|
fprintf(stderr, "pwd: failed to get current directory\n");
|
|
}
|
|
}
|
|
|
|
static void handle_echo(int argc, char **argv) {
|
|
for (int i = 1; i < argc; i++) {
|
|
if (i > 1) printf(" ");
|
|
|
|
/* Handle -n flag */
|
|
if (strcmp(argv[i], "-n") == 0) {
|
|
continue;
|
|
}
|
|
|
|
/* Simple variable expansion */
|
|
const char *p = argv[i];
|
|
while (*p) {
|
|
if ((*p == '$' && isalpha((unsigned char)*p)) || *p == '_') {
|
|
p++;
|
|
const char *start = p;
|
|
while (*p && (isalnum((unsigned char)*p) || *p == '_')) p++;
|
|
char varname[256];
|
|
int len = p - start;
|
|
if ((int)len >= (int)sizeof(varname)) len = sizeof(varname) - 1;
|
|
strncpy(varname, start, len);
|
|
varname[len] = '\0';
|
|
const char *val = get_env_var(varname);
|
|
if (val) printf("%s", val);
|
|
} else {
|
|
printf("%c", *p);
|
|
p++;
|
|
}
|
|
}
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
static void handle_env(int argc, char **argv) {
|
|
if (argc > 1) {
|
|
/* Set variable */
|
|
for (int i = 1; i < argc; i++) {
|
|
char *eq = strchr(argv[i], '=');
|
|
if (eq) {
|
|
*eq = '\0';
|
|
set_env_var(argv[i], eq + 1);
|
|
*eq = '=';
|
|
}
|
|
}
|
|
} else {
|
|
/* Print environment - use Windows API */
|
|
wchar_t **wenv = _wenviron;
|
|
if (wenv) {
|
|
for (wchar_t **we = wenv; *we; we++) {
|
|
// Convert wide string to narrow string for printing
|
|
size_t len = wcslen(*we);
|
|
char *narrow = malloc(len * 2 + 1);
|
|
if (narrow) {
|
|
wcstombs(narrow, *we, len * 2 + 1);
|
|
printf("%s\n", narrow);
|
|
free(narrow);
|
|
}
|
|
}
|
|
}
|
|
for (int i = 0; i < env_count; i++) {
|
|
printf("%s\n", env_table[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void handle_help() {
|
|
printf("psh - minimal bash-like shell (Windows)\n\n");
|
|
printf("Built-in commands:\n");
|
|
printf(" cd [dir] Change directory\n");
|
|
printf(" pwd Print working directory\n");
|
|
printf(" echo [args] Print arguments\n");
|
|
printf(" exit Exit shell\n");
|
|
printf(" env [var=val] Show/set environment\n");
|
|
printf(" help Show this help\n\n");
|
|
printf("Features:\n");
|
|
printf(" cmd1 | cmd2 Pipe output\n");
|
|
printf(" cmd > file Redirect output\n");
|
|
printf(" cmd >> file Append output\n");
|
|
printf(" cmd < file Redirect input\n");
|
|
printf(" cmd & Run in background\n");
|
|
printf(" cmd1 && cmd2 Execute cmd2 if cmd1 succeeds\n");
|
|
printf(" cmd1 || cmd2 Execute cmd2 if cmd1 fails\n");
|
|
printf(" history View command history\n");
|
|
printf(" Tab Try command completion\n");
|
|
}
|
|
|
|
static int is_builtin(const char *cmd) {
|
|
return (strcmp(cmd, "cd") == 0 ||
|
|
strcmp(cmd, "pwd") == 0 ||
|
|
strcmp(cmd, "echo") == 0 ||
|
|
strcmp(cmd, "exit") == 0 ||
|
|
strcmp(cmd, "help") == 0 ||
|
|
strcmp(cmd, "env") == 0 ||
|
|
strcmp(cmd, "history") == 0);
|
|
}
|
|
|
|
static int run_builtin(int argc, char **argv) {
|
|
if (argc == 0) return 0;
|
|
|
|
if (strcmp(argv[0], "cd") == 0) {
|
|
handle_cd(argc, argv);
|
|
} else if (strcmp(argv[0], "pwd") == 0) {
|
|
handle_pwd(argv);
|
|
} else if (strcmp(argv[0], "echo") == 0) {
|
|
handle_echo(argc, argv);
|
|
} else if (strcmp(argv[0], "exit") == 0) {
|
|
handle_exit(argc, argv);
|
|
} else if (strcmp(argv[0], "help") == 0) {
|
|
handle_help();
|
|
} else if (strcmp(argv[0], "env") == 0) {
|
|
handle_env(argc, argv);
|
|
} else if (strcmp(argv[0], "history") == 0) {
|
|
for (int i = 0; i < hist_size; i++) {
|
|
printf("%d %s\n", i + 1, history[i]);
|
|
}
|
|
} else {
|
|
return 1; /* Not a builtin */
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Build completion list from available commands and files */
|
|
static void build_completion_list(const char *prefix) {
|
|
completion_dir_count = 0;
|
|
const char *builtin_cmds[] = {"cd", "pwd", "echo", "exit", "help", "env", "history", NULL};
|
|
for (int i = 0; builtin_cmds[i]; i++) {
|
|
if (strncmp(builtin_cmds[i], prefix, strlen(prefix)) == 0) {
|
|
if (completion_dir_count < 64) {
|
|
completion_dirs[completion_dir_count++] = builtin_cmds[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Add environment variable names from shell */
|
|
for (int i = 0; i < env_count; i++) {
|
|
char *eq = strchr(env_table[i], '=');
|
|
if (eq) {
|
|
*eq = '\0';
|
|
if (strncmp(env_table[i], prefix, strlen(prefix)) == 0) {
|
|
if (completion_dir_count < 64) {
|
|
completion_dirs[completion_dir_count++] = env_table[i];
|
|
}
|
|
}
|
|
*eq = '=';
|
|
}
|
|
}
|
|
|
|
/* Add executable files in PATH and current directory */
|
|
const char *path = getenv("PATH");
|
|
if (!path) path = "";
|
|
|
|
WIN32_FIND_DATAA find_data;
|
|
HANDLE hFind;
|
|
|
|
char search_pattern[MAX_PATH];
|
|
sprintf(search_pattern, ".\\%s*", prefix);
|
|
hFind = FindFirstFileA(search_pattern, &find_data);
|
|
if (hFind != INVALID_HANDLE_VALUE) {
|
|
do {
|
|
if (!(find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
|
|
if (completion_dir_count < 64) {
|
|
completion_dirs[completion_dir_count++] = find_data.cFileName;
|
|
}
|
|
}
|
|
} while (FindNextFileA(hFind, &find_data));
|
|
FindClose(hFind);
|
|
}
|
|
|
|
/* Check PATH directories */
|
|
char path_copy[MAX_PATH * 2];
|
|
strncpy(path_copy, path, sizeof(path_copy) - 1);
|
|
path_copy[sizeof(path_copy) - 1] = '\0';
|
|
|
|
char *dir = strtok(path_copy, ";");
|
|
while (dir && completion_dir_count < 64) {
|
|
char dir_pattern[MAX_PATH];
|
|
sprintf(dir_pattern, "%s\\%s*", dir, prefix);
|
|
hFind = FindFirstFileA(dir_pattern, &find_data);
|
|
if (hFind != INVALID_HANDLE_VALUE) {
|
|
do {
|
|
if (!(find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
|
|
if (completion_dir_count < 64) {
|
|
completion_dirs[completion_dir_count++] = find_data.cFileName;
|
|
}
|
|
}
|
|
} while (FindNextFileA(hFind, &find_data));
|
|
FindClose(hFind);
|
|
}
|
|
dir = strtok(NULL, ";");
|
|
}
|
|
}
|
|
|
|
/* Try tab completion */
|
|
static void try_tab_completion(char *line, int *pos) {
|
|
/* Find the word at cursor position */
|
|
int word_start = *pos;
|
|
while (word_start > 0 && line[word_start - 1] != ' ') {
|
|
word_start--;
|
|
}
|
|
|
|
/* Extract the prefix */
|
|
char prefix[MAX_LINE];
|
|
strncpy(prefix, line + word_start, *pos - word_start);
|
|
prefix[*pos - word_start] = '\0';
|
|
|
|
if (strlen(prefix) == 0) {
|
|
/* Tab on empty line - show built-in commands */
|
|
build_completion_list("");
|
|
if (completion_dir_count > 0) {
|
|
printf("\n");
|
|
for (int i = 0; i < completion_dir_count; i++) {
|
|
printf(" %s\n", completion_dirs[i]);
|
|
}
|
|
printf("%s", line);
|
|
for (int i = 0; i < *pos; i++) printf(" ");
|
|
printf("\n");
|
|
}
|
|
return;
|
|
}
|
|
|
|
build_completion_list(prefix);
|
|
|
|
if (completion_dir_count == 0) {
|
|
return; /* No matches */
|
|
}
|
|
|
|
if (completion_dir_count == 1) {
|
|
/* Single match - complete it */
|
|
int match_len = strlen(completion_dirs[0]);
|
|
memmove(line + word_start + match_len + 1, line + *pos, strlen(line + *pos) + 1);
|
|
strncpy(line + word_start, completion_dirs[0], match_len);
|
|
line[word_start + match_len] = ' ';
|
|
*pos = word_start + match_len + 1;
|
|
cmd_line_len = *pos;
|
|
printf("%s", line);
|
|
for (int i = *pos; i < cmd_line_len; i++) printf(" ");
|
|
printf("\b");
|
|
} else {
|
|
/* Multiple matches - show them */
|
|
printf("\n");
|
|
for (int i = 0; i < completion_dir_count; i++) {
|
|
printf(" %s\n", completion_dirs[i]);
|
|
}
|
|
printf("%s", line);
|
|
for (int i = *pos; i < cmd_line_len; i++) printf(" ");
|
|
printf("\b");
|
|
}
|
|
fflush(stdout);
|
|
}
|
|
|
|
/* Simple command line reader */
|
|
static char *read_command_line(const char *prompt) {
|
|
printf("%s", prompt);
|
|
fflush(stdout);
|
|
|
|
cmd_line_pos = 0;
|
|
cmd_line_len = 0;
|
|
cmd_line_buf[0] = '\0';
|
|
|
|
int c;
|
|
while ((c = getchar()) != EOF && c != '\n' && c != '\r') {
|
|
if (c == 8 || c == 127) { /* Backspace */
|
|
if (cmd_line_pos > 0) {
|
|
cmd_line_pos--;
|
|
cmd_line_len--;
|
|
memmove(cmd_line_buf + cmd_line_pos,
|
|
cmd_line_buf + cmd_line_pos + 1,
|
|
cmd_line_len - cmd_line_pos + 1);
|
|
printf("\b \b");
|
|
fflush(stdout);
|
|
}
|
|
} else if (c == 27) { /* Escape - check for arrow keys */
|
|
c = getchar();
|
|
if (c == '[') {
|
|
c = getchar();
|
|
if (c == 'A') { /* Up arrow */
|
|
if (cmd_hist_pos > 0) {
|
|
cmd_hist_pos--;
|
|
strncpy(cmd_line_buf, history[cmd_hist_pos], MAX_LINE - 1);
|
|
cmd_line_buf[MAX_LINE - 1] = '\0';
|
|
cmd_line_pos = strlen(cmd_line_buf);
|
|
cmd_line_len = cmd_line_pos;
|
|
printf("\r%s", cmd_line_buf);
|
|
for (int i = cmd_line_pos; i < cmd_line_len; i++) printf(" ");
|
|
printf("\r%s", cmd_line_buf);
|
|
fflush(stdout);
|
|
}
|
|
} else if (c == 'B') { /* Down arrow */
|
|
if (cmd_hist_pos < cmd_hist_idx) {
|
|
cmd_hist_pos++;
|
|
if (cmd_hist_pos == cmd_hist_idx) {
|
|
cmd_line_buf[0] = '\0';
|
|
cmd_line_pos = 0;
|
|
cmd_line_len = 0;
|
|
printf("\r");
|
|
fflush(stdout);
|
|
} else {
|
|
strncpy(cmd_line_buf, history[cmd_hist_pos], MAX_LINE - 1);
|
|
cmd_line_buf[MAX_LINE - 1] = '\0';
|
|
cmd_line_pos = strlen(cmd_line_buf);
|
|
cmd_line_len = cmd_line_pos;
|
|
printf("\r%s", cmd_line_buf);
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (c == 9) { /* Tab - try completion */
|
|
try_tab_completion(cmd_line_buf, &cmd_line_pos);
|
|
cmd_line_len = strlen(cmd_line_buf);
|
|
} else if (c >= 32 && c < 127 && cmd_line_len < MAX_LINE - 1) {
|
|
if (cmd_line_pos < cmd_line_len) {
|
|
memmove(cmd_line_buf + cmd_line_pos + 1,
|
|
cmd_line_buf + cmd_line_pos,
|
|
cmd_line_len - cmd_line_pos);
|
|
}
|
|
cmd_line_buf[cmd_line_pos++] = (char)c;
|
|
cmd_line_len++;
|
|
printf("%c", (char)c);
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
|
|
cmd_line_buf[cmd_line_pos] = '\0';
|
|
printf("\n");
|
|
return cmd_line_buf;
|
|
}
|
|
|
|
static void init_command_line() {
|
|
cmd_hist_idx = 0;
|
|
cmd_hist_pos = 0;
|
|
}
|
|
|
|
static void free_command_line() {
|
|
(void)0; /* No dynamic allocation in simple reader */
|
|
}
|
|
|
|
/* Main loop */
|
|
int main(int argc, char **argv) {
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
/* Print welcome message */
|
|
printf("Welcome to psh! Type 'help' for instructions.\n\n");
|
|
|
|
init_command_line();
|
|
|
|
char *line;
|
|
while (1) {
|
|
print_prompt();
|
|
|
|
line = read_command_line("");
|
|
|
|
/* Skip empty lines and comments */
|
|
if (strlen(line) == 0 || line[0] == '#') {
|
|
continue;
|
|
}
|
|
|
|
/* Add to history */
|
|
add_history_entry(line);
|
|
|
|
/* Tokenize */
|
|
Token tokens[MAX_LINE];
|
|
memset(tokens, 0, sizeof(tokens));
|
|
int token_count = tokenize_line(line, tokens, MAX_LINE);
|
|
|
|
/* Execute with && and || support */
|
|
execute_command_list(tokens, token_count);
|
|
|
|
/* Cleanup */
|
|
free_tokens(tokens, token_count);
|
|
}
|
|
|
|
free_command_line();
|
|
cleanup_history();
|
|
|
|
return 0;
|
|
}
|