Devin.KR

셸 경로·설정·로그

100분 안팎

학습 목표

상대 경로 실행과 잘못된 설정을 검사하는 실행 스크립트를 만듭니다.

개념

같은 프로그램을 다른 위치에서 실행하기

동료가 프로그램을 자신의 작업 폴더에서 실행했더니 config.json을 찾지 못하는 경우가 있습니다. 코드는 같아도 상대 경로가 가리키는 위치가 달라진 것입니다. 이번 레슨은 이미 작성한 센서 읽기 함수를 바탕으로 bash 실행 스크립트와 설정 검증, 결과 파일을 연결합니다. 목표는 어디에서 스크립트를 호출하든 같은 fixture와 설정을 읽는 것입니다. 오류 행이 있어도 파일 처리 결과를 남기고, 설정이나 파일 자체를 확보하지 못하면 실패 종료 코드로 호출자에게 알립니다.

현재 경로와 스크립트 경로

현재 작업 디렉터리는 명령을 호출한 셸이 서 있는 위치입니다. Python에서 open("config.json")은 그 위치를 기준으로 찾습니다. 스크립트 파일이 저장된 위치를 자동으로 기준으로 삼지 않습니다. run.sh는 dirname "$0"으로 스크립트가 들어 있는 경로를 얻고 그 폴더로 이동한 뒤 pwd -P로 기준을 고정합니다. 이 실습은 심볼릭 링크로 스크립트를 호출하는 배치를 지원하지 않으며 압축을 푼 실제 경로로 실행합니다. 링크 배치에는 별도의 경로 해석 정책이 필요합니다.

LAB_DIR="$(cd -- "$(dirname -- "$0")" && pwd -P)"에서 명령 치환은 경로를 계산한 결과를 변수에 담습니다. 따옴표는 공백이 있는 폴더 이름을 하나의 인자로 유지합니다. cd "$LAB_DIR"를 따옴표 없이 쓰면 경로가 여러 단어로 나뉠 수 있습니다. 그 뒤 cli.py와 config.json, fixtures/sensors.jsonl의 상대 경로를 사용합니다. 출력 인자를 상대 경로로 주는 경우도 이 이동 후 실습 폴더 기준으로 해석된다는 정책을 README에 명시합니다.

설정은 실행 전에 확인하기

config.json은 min_range_m와 max_range_m를 갖는 JSON 객체입니다. 두 값은 bool을 제외한 유한 숫자여야 하며 0.05 이상, 8.0 이하이고 최소값이 최대값 이하여야 합니다. 설정은 센서 계약 안에서 범위를 좁히는 역할입니다. 최대값을 9로 늘려 기존 센서 계약을 바꾸는 용도가 아닙니다. cli.py는 설정을 읽고 검증한 다음 기록을 처리하므로 잘못된 설정을 발견했을 때 출력 폴더를 새로 만들지 않습니다. 이전 실행 결과가 있으면 그것이 새 결과라고 오해하지 않도록 실행 폴더를 구분합니다.

JSON의 문법이 맞는다고 설정 값의 의미까지 맞는 것은 아닙니다. 최소값 9와 최대값 8은 숫자이지만 범위 순서가 거꾸로입니다. true도 JSON 값으로 가능하지만 거리 숫자로 허용하지 않습니다. 설정 오류를 기본값으로 조용히 대체하면 같은 명령이 작성자의 의도와 다른 조건으로 실행됩니다. fatal: config range must stay within 0.05..8.0은 입력 메시지가 아닌 실행 조건을 고쳐야 한다는 진단입니다. 이 경우 재시도보다 config.json 내용을 먼저 확인합니다.

표준 출력과 표준 오류

성공 요약 valid=3 errors=2는 stdout에 출력합니다. 처리할 수 없는 설정 또는 파일 문제는 fatal:로 시작하는 메시지를 stderr에 쓰고 종료 코드 2를 반환합니다. 오류 행은 개별적으로 분류 가능한 입력이라 errors.jsonl에 남기며 실행 자체는 종료 코드 0입니다. 종료 0은 프로그램이 기록 처리 계약을 완료했다는 뜻이고 로봇 이동이 안전하다는 뜻이 아닙니다. 이후 제어 단계는 유효 개수와 최신성, 정지 조건을 따로 판단해야 합니다.

셸에서 >는 stdout을 파일로 보내고 2>는 stderr를 다른 파일로 보냅니다. 두 채널을 분리하면 정상 요약을 기계가 읽으면서 치명 오류를 사람이 확인할 수 있습니다. 실행 직후 $?로 상태를 확인합니다. 다음 명령을 실행하고 나서 $?를 읽으면 그 다음 명령의 상태가 되므로 즉시 저장합니다. 파이프로 출력만 넘기는 방식은 앞 프로그램의 실패를 놓칠 수 있어 이번 run.sh에는 파이프를 넣지 않습니다. 테스트도 반환 코드와 두 채널을 독립적으로 확인합니다.

실행 스크립트의 책임

set -e는 실패한 단순 명령에서 스크립트가 계속 진행하지 않도록 돕고 set -u는 미정의 변수 사용을 잡습니다. 다만 조건문과 일부 문맥에서는 -e의 동작이 달라지므로 모든 실패가 자동 처리된다고 설명하지 않습니다. 이 스크립트는 경로 이동 후 exec python3로 Python 프로세스에 실행을 넘깁니다. exec를 사용하면 Python이 반환한 종료 코드가 호출자에게 전달됩니다. 인자 하나가 선택 사항이므로 ${1:-out}을 써 인자가 없어도 기본 출력 폴더를 결정합니다.

check.sh도 자신의 위치로 이동한 뒤 python3 -m unittest discover -s tests를 실행합니다. 테스트 탐색을 호출자 폴더에서 수행하면 tests 디렉터리를 찾지 못하거나 엉뚱한 테스트를 읽을 수 있습니다. 검증 스크립트는 테스트 실패를 성공으로 덮지 않으며 마지막 명령의 상태를 그대로 전달합니다. starter의 run.sh는 경로 고정이 빠져 있으므로 실습 폴더 안에서 실행하면 우연히 성공하고 밖에서는 실패합니다. 이런 우연한 성공을 잡기 위해 테스트가 다른 임시 작업 디렉터리에서 스크립트를 호출합니다.

결과 파일과 재실행 정책

cli.py는 valid.jsonl에 유효 메시지를, errors.jsonl에 행 번호와 이유를 한 줄씩 씁니다. 출력 파일을 w 모드로 열어 재실행 시 덮어씁니다. append 모드로 쓰면 같은 fixture를 다시 실행할 때 유효 3건이 6건으로 늘어 실험 비교가 어려워집니다. 과거 기록을 보존하려면 실행마다 다른 출력 폴더를 지정합니다. 두 결과 파일은 동시에 원자적으로 교체되는 구조가 아니므로 저장 도중 실패한 실행의 폴더는 분석 결과로 사용하지 않습니다.

출력 경로에 공백이 있어도 하나의 인자로 넘겨야 합니다. bash run.sh "/tmp/robot result"처럼 따옴표를 사용합니다. 기본 out 폴더는 실행 스크립트가 있는 실습 폴더 아래이며 절대 경로를 주면 지정한 위치로 갑니다. PermissionError나 No such file or directory가 보이면 오류 메시지의 경로를 읽고 입력, 설정, 출력 중 어느 경로인지 구분합니다. 권한을 넓히는 명령부터 시도하지 않고 현재 경로와 파일 존재, 쓰기 가능 여부를 먼저 확인합니다.

맥에서 확인한 것과 Linux 후속 확인

bash run.sh로 호출하면 파일의 실행 비트가 없어도 셸이 내용을 읽어 실행합니다. ./run.sh 형태로 직접 호출하려면 chmod +x run.sh가 필요합니다. 맥과 Linux에서는 설치된 python3 위치와 파일명 대소문자 처리, 기본 umask 및 로그 저장 위치가 다를 수 있습니다. 실습 스크립트는 맥 기본 도구에도 없는 readlink -f 같은 옵션에 의존하지 않습니다. 실제 Linux 환경에서는 python3 실행 가능 여부와 파일 권한, 외부 경로 호출을 다시 확인하고 그 결과를 별도 증거로 남깁니다.

미션은 파일 읽기와 실행 경로를 합친 프로젝트 기반입니다. 시작 코드는 앞 레슨의 읽기 함수와 실행 스크립트를 포함하되 거리 범위와 설정 순서 검사를 비워 둡니다. 이를 채워 빈 파일, 음수, 범위 초과, 파싱 실패, 외부 경로 실행을 함께 검증합니다. 제출할 때 README에 입력 단위·실패 정책·맥 실행 결과·Linux 확인 대기를 구분해 적습니다. 장기 실행 서버를 켜는 작업은 없으며 모든 명령은 기록을 처리하고 종료됩니다. 이 폴더가 다음 모듈의 좌표 변환 확장 출발점입니다.

따라하기

상대 경로의 기준 재현

임시 폴더 둘을 만들고 현재 폴더를 바꾸어 같은 상대 파일명이 다른 파일을 읽는 것을 확인합니다.

import os
import tempfile
from pathlib import Path
previous = Path.cwd()
with tempfile.TemporaryDirectory() as root:
    for name in ["caller", "lab"]:
        folder = Path(root) / name
        folder.mkdir()
        (folder / "config.txt").write_text(name, encoding="utf-8")
    try:
        os.chdir(Path(root) / "caller")
        print(Path("config.txt").read_text())
        os.chdir(Path(root) / "lab")
        print(Path("config.txt").read_text())
    finally:
        os.chdir(previous)

실행 결과

caller
lab

출력 채널과 상태 확인

별도 Python 프로세스의 stdout·stderr·종료 코드를 구분해 관찰합니다.

import subprocess
import sys
code = 'import sys; print("fatal: config range", file=sys.stderr); sys.exit(2)'
process = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True)
print("exit", process.returncode)
print("stdout", repr(process.stdout))
print("stderr", process.stderr.strip())

실행 결과

exit 2
stdout ''
stderr fatal: config range

경로 고정 실행 스크립트 작성

starter ZIP을 푼 폴더에서 run.sh를 다음 내용으로 작성합니다. 기존 센서 처리 함수와 cli.py를 연결하고 Python의 종료 코드를 exec로 전달합니다. 기본 출력은 실습 폴더의 out입니다.

cat > run.sh <<'SH'
#!/usr/bin/env bash
set -eu
LAB_DIR="$(cd -- "$(dirname -- "$0")" && pwd -P)"
cd "$LAB_DIR"
exec python3 cli.py --config config.json --input fixtures/sensors.jsonl --output "${1:-out}"
SH
bash run.sh

실행 결과

valid=3 errors=2

제공된 기록을 실제로 처리하기

solution ZIP을 푼 폴더에서 입력합니다. 하위 셸을 /tmp로 이동한 뒤 스크립트를 호출해도 같은 fixture를 읽고 결과 파일을 남기는지 확인합니다. LAB_WORK는 현재 실습 폴더를 저장한 변수입니다.

LAB_WORK="$(pwd -P)"
(cd /tmp && bash "$LAB_WORK/run.sh" "$LAB_WORK/out")
python3 - <<'PYCODE'
from pathlib import Path
for name in ("valid.jsonl", "errors.jsonl"):
    print(name, len((Path("out") / name).read_text().splitlines()))
PYCODE

실행 결과

valid=3 errors=2
valid.jsonl 3
errors.jsonl 2

확인 문제

실습

starter의 run.sh에 스크립트 폴더 기준 경로를 구현합니다. sensor.py와 cli.py는 제공된 파일 처리 구현입니다. bash check.sh로 파일 계약 12개와 실행 계약 3개 테스트를 통과합니다. 실행 계약은 외부 작업 폴더, 공백 출력 경로, 반복 실행의 덮어쓰기, 설정 오류의 stderr·종료 2를 검사합니다. 상대 출력 인자는 실습 폴더 기준으로 해석합니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

15개 테스트 통과(OK), 외부 폴더 실행 시 valid=3 errors=2.

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 다른 작업 폴더에서 실행할 때 설정과 로그 경로를 어떻게 고정합니까?
  • 맥에서 검증한 센서 처리 도구를 Linux로 옮길 때 무엇을 재확인합니까?