diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9ba4db6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python +python: + - 2.7 + - 3.3 + - 3.4 + - 3.5 + +before_install: + - pip install pep8 + +script: + - find . -name \*.py -exec pep8 {} + diff --git a/CHANGES.md b/CHANGES.md index e6db989..e5e8b69 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +2016-08-01, Version 0.2.0 +========================= +* Add `export` built-in command (Yuliang Tan) +* Add `getenv` built-in command (Yuliang Tan) +* Use subprocess module to fork and exec (Yuliang Tan) +* Get an environment variable if a token begins with a dollar sign (Yuliang Tan) +* Support Windows (Yuliang Tan) + 2016-07-08, Version 0.1.0 ========================= * Add main shell lopp (Supasate Choochaisri) diff --git a/README.md b/README.md index 1843909..2e6a3c8 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ A simple shell written in Python -The implementation details can be found at **Create Your Own Shell in Python** [Part I](https://hackercollider.com/articles/2016/07/05/create-your-own-shell-in-python-part-1/) and [Part II](https://hackercollider.com/articles/2016/07/06/create-your-own-shell-in-python-part-2/) +> Note: If you found this repo from the article **Create Your Own Shell in Python** [Part I](https://hackercollider.com/articles/2016/07/05/create-your-own-shell-in-python-part-1/) and [Part II](https://hackercollider.com/articles/2016/07/06/create-your-own-shell-in-python-part-2/), you can check out the `tutorial` branch for the source code used in the article. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4b66c58 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +from setuptools import setup + +setup(name='yosh', + version='1.0', + description='Your own shell', + author='Supasate Choochaisri', + author_email='supasate.c@gmail.com', + url='https://github.com/supasate/yosh', + packages=['yosh', 'yosh.builtins'], + entry_points=""" + [console_scripts] + yosh = yosh.shell:main + """, + ) diff --git a/yosh/builtins/__init__.py b/yosh/builtins/__init__.py index c9bf801..b8b0169 100644 --- a/yosh/builtins/__init__.py +++ b/yosh/builtins/__init__.py @@ -1,2 +1,5 @@ from yosh.builtins.cd import cd from yosh.builtins.exit import exit +from yosh.builtins.export import export +from yosh.builtins.getenv import getenv +from yosh.builtins.history import history diff --git a/yosh/builtins/cd.py b/yosh/builtins/cd.py index f0a3b52..490a6b7 100644 --- a/yosh/builtins/cd.py +++ b/yosh/builtins/cd.py @@ -3,5 +3,8 @@ def cd(args): - os.chdir(args[0]) + if len(args) > 0: + os.chdir(args[0]) + else: + os.chdir(os.getenv('HOME')) return SHELL_STATUS_RUN diff --git a/yosh/builtins/export.py b/yosh/builtins/export.py new file mode 100644 index 0000000..0e080f3 --- /dev/null +++ b/yosh/builtins/export.py @@ -0,0 +1,9 @@ +import os +from yosh.constants import * + + +def export(args): + if len(args) > 0: + var = args[0].split('=', 1) + os.environ[var[0]] = var[1] + return SHELL_STATUS_RUN diff --git a/yosh/builtins/getenv.py b/yosh/builtins/getenv.py new file mode 100644 index 0000000..a9358db --- /dev/null +++ b/yosh/builtins/getenv.py @@ -0,0 +1,8 @@ +import os +from yosh.constants import * + + +def getenv(args): + if len(args) > 0: + print(os.getenv(args[0])) + return SHELL_STATUS_RUN diff --git a/yosh/builtins/history.py b/yosh/builtins/history.py new file mode 100644 index 0000000..42c1bb1 --- /dev/null +++ b/yosh/builtins/history.py @@ -0,0 +1,24 @@ +import os +import sys +from yosh.constants import * + + +def history(args): + with open(HISTORY_PATH, 'r') as history_file: + lines = history_file.readlines() + + # default limit is whole file + limit = len(lines) + + if len(args) > 0: + limit = int(args[0]) + + # start history line to print out + start = len(lines) - limit + + for line_num, line in enumerate(lines): + if line_num >= start: + sys.stdout.write('%d %s' % (line_num + 1, line)) + sys.stdout.flush() + + return SHELL_STATUS_RUN diff --git a/yosh/constants.py b/yosh/constants.py index d7037b7..38da54a 100644 --- a/yosh/constants.py +++ b/yosh/constants.py @@ -1,2 +1,5 @@ +import os + SHELL_STATUS_STOP = 0 SHELL_STATUS_RUN = 1 +HISTORY_PATH = os.path.expanduser('~') + os.sep + '.yosh_history' diff --git a/yosh/shell.py b/yosh/shell.py index 6e67b90..49f3468 100644 --- a/yosh/shell.py +++ b/yosh/shell.py @@ -1,6 +1,11 @@ import os import sys import shlex +import getpass +import socket +import signal +import subprocess +import platform from yosh.constants import * from yosh.builtins import * @@ -12,56 +17,104 @@ def tokenize(string): return shlex.split(string) -def execute(cmd_tokens): - # Extract command name and arguments from tokens - cmd_name = cmd_tokens[0] - cmd_args = cmd_tokens[1:] - - # If the command is a built-in command, invoke its function with arguments - if cmd_name in built_in_cmds: - return built_in_cmds[cmd_name](cmd_args) - - # Fork a child shell process - # If the current process is a child process, its `pid` is set to `0` - # else the current process is a parent process and the value of `pid` - # is the process id of its child process. - pid = os.fork() - - if pid == 0: - # Child process - # Replace the child shell process with the program called with exec - os.execvp(cmd_tokens[0], cmd_tokens) - elif pid > 0: - # Parent process - while True: - # Wait response status from its child process (identified with pid) - wpid, status = os.waitpid(pid, 0) - - # Finish waiting if its child process exits normally or is - # terminated by a signal - if os.WIFEXITED(status) or os.WIFSIGNALED(status): - break +def preprocess(tokens): + processed_token = [] + for token in tokens: + # Convert $-prefixed token to value of an environment variable + if token.startswith('$'): + processed_token.append(os.getenv(token[1:])) + else: + processed_token.append(token) + return processed_token + + +def handler_kill(signum, frame): + raise OSError("Killed!") + +def execute(cmd_tokens): + with open(HISTORY_PATH, 'a') as history_file: + history_file.write(' '.join(cmd_tokens) + os.linesep) + + if cmd_tokens: + # Extract command name and arguments from tokens + cmd_name = cmd_tokens[0] + cmd_args = cmd_tokens[1:] + + # If the command is a built-in command, + # invoke its function with arguments + if cmd_name in built_in_cmds: + return built_in_cmds[cmd_name](cmd_args) + + # Wait for a kill signal + signal.signal(signal.SIGINT, handler_kill) + # Spawn a child process + if platform.system() != "Windows": + # Unix support + p = subprocess.Popen(cmd_tokens) + # Parent process read data from child process + # and wait for child process to exit + p.communicate() + else: + # Windows support + command = "" + for i in cmd_tokens: + command = command + " " + i + os.system(command) # Return status indicating to wait for next command in shell_loop return SHELL_STATUS_RUN -def shell_loop(): - status = SHELL_STATUS_RUN +# Display a command prompt as `[@ ]$ ` +def display_cmd_prompt(): + # Get user and hostname + user = getpass.getuser() + hostname = socket.gethostname() - while status == SHELL_STATUS_RUN: - # Display a command prompt - sys.stdout.write('> ') - sys.stdout.flush() + # Get base directory (last part of the curent working directory path) + cwd = os.getcwd() + base_dir = os.path.basename(cwd) - # Read command input - cmd = sys.stdin.readline() + # Use ~ instead if a user is at his/her home directory + home_dir = os.path.expanduser('~') + if cwd == home_dir: + base_dir = '~' - # Tokenize the command input - cmd_tokens = tokenize(cmd) + # Print out to console + sys.stdout.write("[%s@%s %s]$ " % (user, hostname, base_dir)) + sys.stdout.flush() - # Execute the command and retrieve new status - status = execute(cmd_tokens) + +def ignore_signals(): + # Ignore Ctrl-Z stop signal + if platform.system() != "Windows": + signal.signal(signal.SIGTSTP, signal.SIG_IGN) + # Ignore Ctrl-C interrupt signal + signal.signal(signal.SIGINT, signal.SIG_IGN) + + +def shell_loop(): + status = SHELL_STATUS_RUN + + while status == SHELL_STATUS_RUN: + display_cmd_prompt() + + # Ignore Ctrl-Z and Ctrl-C signals + ignore_signals() + + try: + # Read command input + cmd = sys.stdin.readline() + # Tokenize the command input + cmd_tokens = tokenize(cmd) + # Preprocess special tokens + # (e.g. convert $ into environment value) + cmd_tokens = preprocess(cmd_tokens) + # Execute the command and retrieve new status + status = execute(cmd_tokens) + except: + _, err, _ = sys.exc_info() + print(err) # Register a built-in function to built-in command hash map @@ -73,6 +126,9 @@ def register_command(name, func): def init(): register_command("cd", cd) register_command("exit", exit) + register_command("export", export) + register_command("getenv", getenv) + register_command("history", history) def main():