파이썬으로 터미널 명령어 ls와 cd를 직접 만들어보는 실전 가이드

파이썬으로 터미널 프로그램을 직접 구현하면서 운영체제의 동작 원리를 이해할 수 있어요. macOS 터미널에서 매일 사용하는 ls와 cd 명령어를 파이썬 코드로 재현해보면, 쉘이 어떻게 파일 시스템과 상호작용하는지 명확하게 파악할 수 있어요.


파이썬 로고 - 두 개의 뱀이 교차하는 형태의 파이썬 프로그래밍 언어 공식 심볼


파이썬 os 모듈로 파일 시스템 제어하기


파이썬의 os 모듈은 운영체제와 직접 소통하는 창구 역할을 해요. 이 모듈 하나만으로도 디렉토리 탐색, 파일 목록 조회, 경로 이동 등 터미널의 핵심 기능을 모두 구현할 수 있어요.

import os
import sys

# 현재 작업 디렉토리 확인
current_dir = os.getcwd()
print(f"현재 위치: {current_dir}")

# 디렉토리 내용 확인
files = os.listdir()
print(f"파일 목록: {files}")


os.getcwd()는 현재 프로세스가 실행되고 있는 디렉토리 경로를 반환해요. os.listdir()는 지정한 경로의 모든 파일과 폴더를 리스트로 가져와요. 이 두 함수만 알아도 미니 쉘의 절반은 완성된 셈이에요.


ls 명령어 구현: 파일 목록을 보기 좋게 출력하기


실제 ls 명령어처럼 동작하려면 단순히 파일 이름만 출력하는 것으로는 부족해요. 파일 크기, 수정 시간, 권한 정보까지 함께 표시해야 진짜 터미널처럼 보이거든요.

import os
import stat
from datetime import datetime

def ls_basic(path="."):
    """기본 ls 명령어 구현"""
    try:
        # 지정된 경로의 파일/디렉토리 목록 가져오기
        items = os.listdir(path)
        
        # 숨김 파일 필터링 (점으로 시작하는 파일 제외)
        visible_items = [item for item in items if not item.startswith('.')]
        
        # 정렬해서 출력
        for item in sorted(visible_items):
            print(item)
            
    except FileNotFoundError:
        print(f"ls: {path}: No such file or directory")
    except PermissionError:
        print(f"ls: {path}: Permission denied")


기본 버전은 이렇게 간단하지만, 실무에서는 더 상세한 정보가 필요해요. -l 옵션처럼 상세 정보를 보여주는 기능을 추가해볼게요.

def ls_detailed(path="."):
    """ls -l 스타일로 상세 정보 출력"""
    try:
        items = os.listdir(path)
        
        for item in sorted(items):
            # 전체 경로 생성
            full_path = os.path.join(path, item)
            
            # 파일 정보 가져오기
            file_stat = os.stat(full_path)
            
            # 파일 타입 구분
            if os.path.isdir(full_path):
                file_type = 'd'
            elif os.path.islink(full_path):
                file_type = 'l'
            else:
                file_type = '-'
            
            # 권한 정보 파싱
            permissions = stat.filemode(file_stat.st_mode)
            
            # 파일 크기
            size = file_stat.st_size
            
            # 수정 시간 포맷팅
            mod_time = datetime.fromtimestamp(file_stat.st_mtime)
            time_str = mod_time.strftime("%b %d %H:%M")
            
            # 출력
            print(f"{permissions:10} {size:8} {time_str} {item}")
            
    except Exception as e:
        print(f"Error: {e}")

# 실행 예시
ls_detailed("/Users/username/Documents")


파일 권한, 크기, 수정 시간을 함께 출력하니 진짜 터미널 명령어처럼 보이기 시작해요. stat 모듈의 filemode() 함수는 Unix 스타일 권한 문자열을 자동으로 생성해줘요.


cd 명령어 구현: 작업 디렉토리 변경의 비밀


cd 명령어는 겉보기엔 단순해 보이지만, 프로세스의 작업 디렉토리를 실제로 변경하는 중요한 작업이에요. 잘못 구현하면 쉘 전체가 엉뚱한 곳을 가리킬 수 있어요.

def cd(path=None):
    """cd 명령어 구현 - 홈 디렉토리 지원 포함"""
    try:
        if path is None or path == "~":
            # 인자가 없거나 ~인 경우 홈 디렉토리로 이동
            home_dir = os.path.expanduser("~")
            os.chdir(home_dir)
            
        elif path == "-":
            # 이전 디렉토리로 이동 (환경변수 OLDPWD 활용)
            if 'OLDPWD' in os.environ:
                old_dir = os.environ['OLDPWD']
                os.chdir(old_dir)
            else:
                print("cd: OLDPWD not set")
                return
                
        else:
            # 일반 경로로 이동
            # ~ 경로 확장 처리
            expanded_path = os.path.expanduser(path)
            os.chdir(expanded_path)
        
        # 이동 후 현재 위치 출력
        print(f"Changed to: {os.getcwd()}")
        
    except FileNotFoundError:
        print(f"cd: {path}: No such file or directory")
    except NotADirectoryError:
        print(f"cd: {path}: Not a directory")
    except PermissionError:
        print(f"cd: {path}: Permission denied")

# 사용 예시
cd("~/Desktop")  # 홈 디렉토리의 Desktop으로 이동
cd("..")         # 상위 디렉토리로 이동
cd("-")          # 이전 디렉토리로 돌아가기


os.chdir() 함수는 현재 프로세스의 작업 디렉토리를 실제로 변경해요. 이 변경사항은 프로그램이 실행되는 동안 계속 유지되므로, 다음 명령어들도 변경된 위치를 기준으로 동작해요.


미니 쉘 메인 루프: 명령어를 계속 받아들이기


개별 명령어를 구현했으니 이제 실제 쉘처럼 동작하는 메인 루프를 만들어볼게요. 사용자 입력을 계속 받아서 처리하는 인터랙티브한 프로그램이 되는 거예요.

import os
import sys
import shlex  # 명령어 파싱을 위한 모듈

class MiniShell:
    def __init__(self):
        self.commands = {
            'ls': self.cmd_ls,
            'cd': self.cmd_cd,
            'pwd': self.cmd_pwd,
            'echo': self.cmd_echo,
            'exit': self.cmd_exit,
            'clear': self.cmd_clear
        }
        self.running = True
        
    def cmd_ls(self, args):
        """ls 명령어 처리"""
        path = args[0] if args else "."
        
        try:
            items = os.listdir(path)
            # 컬럼 형태로 출력
            if items:
                # 터미널 너비에 맞춰 컬럼 수 계산
                max_len = max(len(item) for item in items)
                cols = 80 // (max_len + 2)  # 기본 터미널 너비 80
                
                for i, item in enumerate(sorted(items)):
                    # 디렉토리는 색상 추가 (macOS 터미널)
                    full_path = os.path.join(path, item)
                    if os.path.isdir(full_path):
                        print(f"\033[34m{item:<{max_len}}\033[0m", end="  ")
                    else:
                        print(f"{item:<{max_len}}", end="  ")
                    
                    if (i + 1) % cols == 0:
                        print()  # 줄바꿈
                print()  # 마지막 줄바꿈
                
        except Exception as e:
            print(f"ls: {e}")
    
    def cmd_cd(self, args):
        """cd 명령어 처리"""
        if not args:
            path = os.path.expanduser("~")
        else:
            path = os.path.expanduser(args[0])
            
        try:
            os.chdir(path)
        except Exception as e:
            print(f"cd: {e}")
    
    def cmd_pwd(self, args):
        """현재 디렉토리 출력"""
        print(os.getcwd())
    
    def cmd_echo(self, args):
        """echo 명령어 - 텍스트 출력"""
        print(" ".join(args))
    
    def cmd_clear(self, args):
        """화면 지우기"""
        os.system('clear')  # macOS/Linux
    
    def cmd_exit(self, args):
        """쉘 종료"""
        print("Goodbye!")
        self.running = False
    
    def parse_command(self, input_str):
        """명령어와 인자 분리"""
        try:
            # shlex로 안전하게 파싱 (따옴표 처리 포함)
            parts = shlex.split(input_str)
            if not parts:
                return None, []
            return parts[0], parts[1:]
        except ValueError as e:
            print(f"Parse error: {e}")
            return None, []
    
    def run(self):
        """메인 루프"""
        print("Mini Shell v1.0 - Type 'exit' to quit")
        
        while self.running:
            try:
                # 프롬프트 표시 (현재 디렉토리 포함)
                current_dir = os.getcwd()
                home = os.path.expanduser("~")
                
                # 홈 디렉토리는 ~로 표시
                if current_dir.startswith(home):
                    display_dir = "~" + current_dir[len(home):]
                else:
                    display_dir = current_dir
                
                # 색상 있는 프롬프트
                prompt = f"\033[32m{display_dir}\033[0m $ "
                user_input = input(prompt).strip()
                
                if not user_input:
                    continue
                
                # 명령어 파싱
                cmd, args = self.parse_command(user_input)
                
                if cmd in self.commands:
                    self.commands[cmd](args)
                else:
                    # 알 수 없는 명령어
                    print(f"mini-shell: {cmd}: command not found")
                    
            except KeyboardInterrupt:
                # Ctrl+C 처리
                print("\nUse 'exit' to quit")
            except EOFError:
                # Ctrl+D 처리
                print()
                self.cmd_exit([])

# 실행
if __name__ == "__main__":
    shell = MiniShell()
    shell.run()


클래스로 구조화하니 명령어 추가가 쉬워졌어요. 새로운 명령어는 메서드로 추가하고 commands 딕셔너리에 등록하기만 하면 돼요.


고급 기능: 파이프와 리다이렉션 지원하기


진짜 쉘처럼 만들려면 파이프(|)와 리다이렉션(>, <) 기능도 필요해요. subprocess 모듈을 활용하면 외부 명령어와의 연동도 가능해요.

import subprocess

def execute_with_pipe(command_line):
    """파이프를 지원하는 명령어 실행"""
    # 파이프로 명령어 분리
    commands = command_line.split('|')
    
    if len(commands) == 1:
        # 파이프 없는 단일 명령어
        subprocess.run(commands[0], shell=True)
    else:
        # 파이프 체인 처리
        processes = []
        for i, cmd in enumerate(commands):
            stdin = processes[i-1].stdout if i > 0 else None
            stdout = subprocess.PIPE if i < len(commands)-1 else None
            
            proc = subprocess.Popen(
                cmd.strip(), 
                shell=True,
                stdin=stdin,
                stdout=stdout
            )
            processes.append(proc)
        
        # 마지막 프로세스 대기
        processes[-1].wait()

# 사용 예시
execute_with_pipe("ls -la | grep python | wc -l")


자동완성 기능 추가하기


readline 모듈을 사용하면 탭 키로 파일명을 자동완성하는 기능도 구현할 수 있어요.

import readline
import glob

def complete_path(text, state):
    """경로 자동완성 함수"""
    # 입력된 텍스트로 시작하는 파일/디렉토리 찾기
    matches = glob.glob(text + '*')
    
    try:
        return matches[state]
    except IndexError:
        return None

# 자동완성 설정
readline.set_completer(complete_path)
readline.parse_and_bind("tab: complete")

# 이제 input()에서 탭 키를 누르면 자동완성이 동작해요
user_input = input("$ ")


미니 쉘을 만들면서 운영체제가 파일 시스템을 어떻게 관리하는지, 프로세스가 어떻게 상호작용하는지 깊이 이해할 수 있어요. 여기서 배운 개념들은 시스템 프로그래밍, 자동화 스크립트, DevOps 도구 개발 등 다양한 분야에서 활용할 수 있어요.


실제로 이런 미니 쉘은 제한된 환경에서 커스텀 인터페이스가 필요하거나, 교육 목적으로 운영체제 동작을 설명할 때, 또는 특정 작업을 위한 전용 쉘을 만들 때 유용하게 쓰여요. 코드 몇 줄로 터미널의 핵심 기능을 재현할 수 있다는 것 자체가 파이썬의 강력함을 보여주는 좋은 예시예요.


macOS에서 launchd로 파이썬 스크립트 부팅 시 자동 실행하는 방법