Created
March 25, 2021 10:50
-
-
Save jcavar/81c2b5a4bc53528b21b99e4e0dd353e8 to your computer and use it in GitHub Desktop.
A shell in C and Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <readline/readline.h> | |
#include <unistd.h> | |
#include <sys/wait.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <errno.h> | |
#include <ctype.h> | |
char *paths; | |
struct command { | |
char *command; | |
char **arguments; | |
char *standard_out; | |
}; | |
int is_empty(const char *str) { | |
while (*str != 0) { | |
if (isspace(*str)) { | |
str++; | |
} else { | |
return 0; | |
} | |
} | |
return 1; | |
} | |
char *construct_executable(char *program, char **args) { | |
if (strcmp("exit", program) == 0) { | |
if (args[1] != 0) { | |
return NULL; | |
} else { | |
return program; | |
} | |
} | |
if (strcmp("cd", program) == 0) { | |
if (args[1] == 0 || args[2] != 0) { | |
return NULL; | |
} else { | |
return program; | |
} | |
} | |
if (strcmp("path", program) == 0) { | |
return program; | |
} | |
if (paths == NULL || strcmp(paths, "") == 0) { | |
if (access(program, X_OK) == 0) { | |
return program; | |
} else { | |
return NULL; | |
} | |
} | |
char *to_separate = strdup(paths); | |
char *to_free = to_separate; | |
char *path; | |
while ((path = strsep(&to_separate, ":")) != NULL) { | |
char *executable = malloc(strlen(path) + strlen(program) + 1 + 1); | |
executable = strcpy(executable, path); | |
executable = strcat(executable, "/"); | |
executable = strcat(executable, program); | |
if (access(executable, X_OK) == 0) { | |
free(to_free); | |
return executable; | |
} | |
free(executable); | |
} | |
free(to_free); | |
return NULL; | |
} | |
int construct_command(char *line, struct command *command) { | |
char **args = NULL; | |
int size = 1; | |
char *before_redirect = strsep(&line, ">"); | |
char *after_redirect = strsep(&line, ">"); | |
char *after_after_redirect = strsep(&line, ">"); | |
if (after_after_redirect != NULL) { | |
return -1; | |
} | |
for (char *argument; (argument = strsep(&before_redirect, " ")) != NULL;) { | |
if (strcmp("", argument) == 0) { | |
continue; | |
} | |
size++; | |
args = realloc(args, size * sizeof(char *)); | |
args[size - 2] = argument; | |
} | |
if (args == NULL) { | |
return -1; | |
} | |
args[size - 1] = 0; | |
char *redirect_file = NULL; | |
if (after_redirect != NULL) { | |
char *argument; | |
while ((argument = strsep(&after_redirect, " ")) != NULL) { | |
if (strcmp("", argument) == 0) { | |
continue; | |
} | |
if (redirect_file != NULL) { | |
return -1; | |
} | |
redirect_file = argument; | |
} | |
if (redirect_file == NULL) { | |
return -1; | |
} | |
} | |
char *program = construct_executable(args[0], args); | |
if (program == NULL) { | |
return -1; | |
} | |
command->command = program; | |
command->arguments = args; | |
command->standard_out = redirect_file; | |
return 0; | |
} | |
struct command **construct_commands(char *line, int *size) { | |
int i = 0; | |
struct command **commands = NULL; | |
for (char *command; (command = strsep(&line, "&")) != NULL;) { | |
if (is_empty(command) == 1) { | |
continue; | |
} | |
i++; | |
commands = realloc(commands, i * sizeof(struct command)); | |
struct command *c = malloc(sizeof(struct command)); | |
if (construct_command(command, c) == -1) { | |
free(commands); | |
return NULL; | |
} | |
commands[i - 1] = c; | |
} | |
*size = i; | |
return commands; | |
} | |
void print_prompt(FILE *stream) { | |
if (stream == stdin) { | |
printf("%s", "wish> "); | |
} | |
} | |
void print_error() { | |
char error_message[30] = "An error has occurred\n"; | |
write(STDERR_FILENO, error_message, strlen(error_message)); | |
} | |
char *read_line(FILE *stream) { | |
char *line = NULL; | |
size_t linecap = 0; | |
if (getline(&line, &linecap, stream) == -1) { | |
return NULL; | |
} | |
line[strcspn(line, "\n")] = 0; | |
return line; | |
} | |
pid_t execute_command(struct command *command) { | |
if (strcmp("exit", command->command) == 0) { | |
exit(0); | |
} | |
if (strcmp("cd", command->command) == 0) { | |
return chdir(command->arguments[1]); | |
} | |
if (strcmp("path", command->command) == 0) { | |
free(paths); | |
paths = NULL; | |
char *argument; | |
int i = 1; | |
while ((argument = command->arguments[i]) != 0) { | |
int length = 0; | |
if (paths != NULL) { | |
length = strlen(paths) + 1; | |
} | |
paths = realloc(paths, length + strlen(argument) + 1); | |
if (length != 0) { | |
paths = strcat(paths, ":"); | |
paths = strcat(paths, argument); | |
} else { | |
paths = strcpy(paths, argument); | |
} | |
i++; | |
} | |
return 0; | |
} | |
pid_t pid = fork(); | |
if (pid == 0) { | |
if (command->standard_out != NULL) { | |
fclose(stdout); | |
fopen(command->standard_out, "w"); | |
} | |
execv(command->command, command->arguments); | |
perror("execv failed"); | |
exit(1); | |
} else { | |
return pid; | |
} | |
} | |
int execute_commands(struct command **commands, int size) { | |
int pids_size = 0; | |
pid_t *pids = NULL; | |
for (int i = 0; i < size; i++) { | |
struct command *command = commands[i]; | |
pid_t pid = execute_command(command); | |
if (pid == -1) { | |
return -1; | |
} else if (pid != 0) { | |
pids = realloc(pids, (pids_size + 1) * sizeof(pid_t)); | |
pids[pids_size] = pid; | |
pids_size++; | |
} | |
} | |
for (int j = 0; j < pids_size; j++) { | |
waitpid(pids[j], NULL, 0); | |
} | |
return 0; | |
} | |
int main(int argc, char *argv[]) { | |
if (argc > 2) { | |
print_error(); | |
exit(1); | |
} | |
FILE *stream = stdin; | |
if (argc == 2) { | |
stream = fopen(argv[1], "r"); | |
if (stream == NULL) { | |
print_error(); | |
fclose(stream); | |
exit(1); | |
} | |
} | |
paths = strdup("/bin"); | |
while (1) { | |
print_prompt(stream); | |
char *line = read_line(stream); | |
if (line == NULL) { | |
if (stream == stdin) { | |
print_error(); | |
continue; | |
} else { | |
exit(0); | |
} | |
} | |
int size = -1; | |
struct command **commands = construct_commands(line, &size); | |
if (size == 0) { | |
free(line); | |
free(commands); | |
continue; | |
} | |
if (commands == NULL || execute_commands(commands, size) != 0) { | |
print_error(); | |
} | |
free(line); | |
free(commands); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
struct ShellError: Error {} | |
enum Builtin { | |
case exit | |
case path([String]) | |
case cd(String) | |
static func create(line: String) throws -> Builtin? { | |
let parts = line.split(separator: " ", omittingEmptySubsequences: true).map { String($0) } | |
guard !parts.isEmpty else { return nil } | |
switch parts[0] { | |
case "exit": | |
guard parts.count == 1 else { throw ShellError() } | |
return .exit | |
case "path": | |
return .path(Array(parts.dropFirst())) | |
case "cd": | |
guard parts.count == 2 else { throw ShellError() } | |
return .cd(parts[1]) | |
default: | |
return nil | |
} | |
} | |
} | |
struct Command { | |
let command: URL | |
let arguments: [String] | |
let redirect: String? | |
static func create(line: String) throws -> Command? { | |
let redirectSplit = line.split(separator: ">", maxSplits: 1, omittingEmptySubsequences: false) | |
let commandLine = redirectSplit[0] | |
var redirect: String? | |
if redirectSplit.count > 1 { | |
let redirectLine = String(redirectSplit.last!).split(separator: " ", omittingEmptySubsequences: true) | |
guard redirectLine.count == 1 else { throw ShellError() } | |
redirect = String(redirectLine.first!) | |
} | |
let parts = commandLine.split(separator: " ", omittingEmptySubsequences: true).map { String($0) } | |
guard !parts.isEmpty else { | |
if redirect != nil { | |
throw ShellError() | |
} else { | |
return nil | |
} | |
} | |
let executable = try find(executable: parts[0], in: path) | |
let arguments = Array(parts.dropFirst()) | |
return Command(command: executable, arguments: arguments, redirect: redirect) | |
} | |
} | |
func find(executable: String, in paths: [String]) throws -> URL { | |
if let path = paths.map({ $0 + "/" + executable }).first(where: { FileManager.default.fileExists(atPath: $0) }) { | |
return URL(fileURLWithPath: path) | |
} else { | |
throw ShellError() | |
} | |
} | |
func createCommands(line: String) throws -> [Command] { | |
let commands = try line.split(separator: "&", omittingEmptySubsequences: true) | |
.compactMap { try Command.create(line: String($0)) } | |
return commands | |
} | |
func execute(commands: [Command]) throws { | |
let tasks = commands.map { command -> Process in | |
let task = Process() | |
task.executableURL = command.command | |
task.arguments = command.arguments | |
if let redirect = command.redirect { | |
if !FileManager.default.fileExists(atPath: redirect) { | |
FileManager.default.createFile(atPath: redirect, contents: nil, attributes: nil) | |
} | |
task.standardOutput = FileHandle.init(forWritingAtPath: redirect) | |
} | |
return task | |
} | |
try tasks.forEach { try $0.run() } | |
tasks.forEach { $0.waitUntilExit() } | |
} | |
func execute(builtin: Builtin) throws { | |
switch builtin { | |
case .exit: | |
exit(0) | |
case .path(let arguments): | |
path = arguments | |
case .cd(let path): | |
chdir(path) | |
} | |
} | |
func print_prompt() { | |
if prompt { | |
print("wish", terminator: "> ") | |
} | |
} | |
var path = ["/bin"] | |
let args = CommandLine.arguments | |
var prompt = true | |
if args.count > 1 { | |
let path = args[1] | |
if args.count > 2 || !FileManager.default.fileExists(atPath: path) { | |
fputs("An error has occurred\n", stderr) | |
exit(1) | |
} | |
close(STDIN_FILENO) | |
fopen(path, "r") | |
prompt = false | |
} | |
while true { | |
print_prompt() | |
guard let line = readLine(strippingNewline: true) else { break } | |
do { | |
if let builtin = try Builtin.create(line: line) { | |
try execute(builtin: builtin) | |
continue | |
} | |
let commands = try createCommands(line: line) | |
try execute(commands: commands) | |
} catch { | |
fputs("An error has occurred\n", stderr) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Shell project as part of Operating Systems course that I am taking.
I had difficult time writing original version in C. I was interested how would solution in Swift look like. I am way more proficient in Swift then in C, but it is still a reminder how easier is to work with high level, modern language.
Particular areas that make it difficult to work with C compared to Swift: