commit a787f65b8f97e3d94edf852e74bdd7db0beb7f53 Author: Paze SH Date: Sun Aug 16 12:25:26 2026 +0800 Initial commit: psh - minimal bash-like shell with && || operators and tab completion diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..86b8294 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +CC = gcc +CFLAGS = -Wall -Wextra -std=c99 -O2 +LDFLAGS = + +TARGET = psh.exe +SRC = psh.c + +.PHONY: all clean test + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +test: $(TARGET) + @echo "Testing psh..." + ./$(TARGET) -c "echo hello" + ./$(TARGET) -c "pwd" + ./$(TARGET) -c "dir | findstr .exe" + @echo "Tests passed!" + +clean: + rm -f $(TARGET) *.o + +# Build with readline support (if available) +readline: $(SRC) + $(CC) $(CFLAGS) -DHAVE_READLINE -lreadline -o $(TARGET) $(SRC) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d36a05 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# psh - A minimal bash-like shell in pure C + +A lightweight POSIX shell implementation featuring: + +- Command execution via fork/exec (Windows: CreateProcess) +- Built-in commands: `cd`, `pwd`, `echo`, `exit`, `help`, `env` +- Input/output redirection (`<`, `>`, `>>`) +- Piping (`|`) +- Background execution (`&`) +- Command history with up/down arrows +- Tab completion +- `&&` and `||` operators +- Basic environment variable support +- Tilde expansion (`~`) + +## Building + +```bash +make +``` + +Requires: +- GCC or compatible C compiler +- GNU Readline library (for line editing) + +## Running + +```bash +./psh +``` + +## Features + +### Built-ins +- `cd [dir]` - Change directory +- `pwd` - Print working directory +- `echo [args]` - Print text (supports `$VAR` expansion) +- `exit` - Exit shell +- `help` - Show help +- `env [var=val]` - Show or set environment variables +- `history` - Show command history + +### Redirection +```bash +ls > output.txt # Redirect output +cat < input.txt # Redirect input +echo hi >> log.txt # Append output +``` + +### Piping +```bash +ps aux | grep python | wc -l +``` + +### Background +```bash +sleep 100 & +``` + +### Logical Operators +```bash +cmd1 && cmd2 # Execute cmd2 only if cmd1 succeeds +cmd1 || cmd2 # Execute cmd2 only if cmd1 fails +``` + +### Tab Completion +Press `Tab` to complete commands: +- Built-in commands (cd, pwd, echo, etc.) +- Executable files in PATH and current directory +- Environment variables + +### Command History +- ↑/↓ arrows: Navigate history +- Tab: Try completion +- Backspace: Delete character + +## Example Session + +``` +user@localhost:F:\Paze SH> echo Hello World +Hello World + +user@localhost:F:\Paze SH> ls && echo "List completed" +psh.c README.md Makefile +List completed + +user@localhost:F:\Paze SH> false || echo "Fallback executed" +Fallback executed + +user@localhost:F:\Paze SH> help +psh - minimal bash-like shell (Windows) + +Built-in commands: + cd [dir] Change directory + pwd Print working directory + echo [args] Print arguments + exit Exit shell + env [var=val] Show/set environment + help Show this help + +Features: + cmd1 | cmd2 Pipe output + cmd > file Redirect output + cmd >> file Append output + cmd < file Redirect input + cmd & Run in background + cmd1 && cmd2 Execute cmd2 if cmd1 succeeds + cmd1 || cmd2 Execute cmd2 if cmd1 fails + history View command history + Tab Try command completion +``` + +## License + +MIT diff --git a/psh.c b/psh.c new file mode 100644 index 0000000..c3075ae --- /dev/null +++ b/psh.c @@ -0,0 +1,1121 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __GNUC__ +#ifdef HAVE_READLINE +#include +#include +#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; +} diff --git a/psh.exe b/psh.exe new file mode 100644 index 0000000..6a489fb Binary files /dev/null and b/psh.exe differ diff --git a/test.c b/test.c new file mode 100644 index 0000000..0bc68e8 --- /dev/null +++ b/test.c @@ -0,0 +1,84 @@ +/* + * psh - Test cases for the shell + */ + +#include +#include +#include +#include + +/* Test helper */ +int run_test(const char *cmd, const char *expected_substring) { + char output[4096]; + FILE *fp; + int passed = 0; + + printf("Testing: %s\n", cmd); + + /* Run command and capture output */ + char full_cmd[4096]; + snprintf(full_cmd, sizeof(full_cmd), "psh.exe -c \"%s\" 2>&1", cmd); + + fp = _popen(full_cmd, "r"); + if (fp == NULL) { + printf(" FAILED: Could not execute command\n"); + return 0; + } + + memset(output, 0, sizeof(output)); + if (fread(output, 1, sizeof(output) - 1, fp) > 0) { + /* Remove trailing newline */ + output[strcspn(output, "\n")] = '\0'; + + if (strstr(output, expected_substring) != NULL) { + printf(" PASSED: Got expected output containing '%s'\n", expected_substring); + passed = 1; + } else { + printf(" FAILED: Expected output containing '%s', got: '%s'\n", expected_substring, output); + } + } else { + printf(" FAILED: No output from command\n"); + } + + _pclose(fp); + return passed; +} + +int main() { + int passed = 0; + int total = 0; + + printf("=== psh Test Suite ===\n\n"); + + /* Test 1: echo */ + total++; + if (run_test("echo hello", "hello")) passed++; + + /* Test 2: pwd */ + total++; + if (run_test("pwd", "F:\\Paze SH")) passed++; + + /* Test 3: environment variables */ + total++; + if (run_test("echo $HOME", "")) passed++; /* Just check it doesn't crash */ + + /* Test 4: help */ + total++; + if (run_test("help", "Built-in commands")) passed++; + + /* Test 5: history (empty) */ + total++; + if (run_test("history", "history")) passed++; + + /* Test 6: && operator */ + total++; + if (run_test("echo first && echo second", "first")) passed++; + + /* Test 7: || operator */ + total++; + if (run_test("false || echo fallback", "fallback")) passed++; + + printf("\n=== Results: %d/%d tests passed ===\n", passed, total); + + return (passed == total) ? 0 : 1; +} diff --git a/test.exe b/test.exe new file mode 100644 index 0000000..3570b2b Binary files /dev/null and b/test.exe differ diff --git a/test_input.txt b/test_input.txt new file mode 100644 index 0000000..555d458 --- /dev/null +++ b/test_input.txt @@ -0,0 +1,8 @@ +test1: echo Hello World +test2: pwd +test3: ls | findstr .exe +test4: echo $HOME +test5: mkdir testdir && cd testdir && pwd +test6: false || echo "This should print" +test7: true && echo "This should also print" +test8: env