Skip to content

Instantly share code, notes, and snippets.

@jcavar
Created March 25, 2021 10:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcavar/81c2b5a4bc53528b21b99e4e0dd353e8 to your computer and use it in GitHub Desktop.
Save jcavar/81c2b5a4bc53528b21b99e4e0dd353e8 to your computer and use it in GitHub Desktop.
A shell in C and Swift
#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);
}
}
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)
}
}
@jcavar
Copy link
Author

jcavar commented Mar 25, 2021

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:

  • Memory management (explicit allocation and deallocation compared to reference counting in Swift)
  • A lack of higher level types (in particular strings, working with arrays is cumbersome and error prone)
  • Error handling (special return codes)
  • A lack of convenience functions (e.g map)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment