文件
PazeSH/psh.c
T

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;
}