V 실전: Bash를 V로 대체하기 — 크로스 플랫폼 스크립트

V 실전: Bash를 V로 대체하기 — 크로스 플랫폼 스크립트

개발하다 보면 반복적인 작업을 자동화하고 싶을 때가 있다. 폴더를 만들고, 파일을 복사하고, git을 초기화하고, 의존성을 설치하는 일들. 보통 Bash나 PowerShell 스크립트를 쓰지만, Windows에서 Bash가 안 되고, Linux에서 PowerShell이 없고 — 크로스 플랫폼이 골칫거리다. V는 이 문제의 깔끔한 대안이 된다.

왜 V로 스크립트를 쓸까?

Bash 스크립트와 V 스크립트를 비교해보자.

항목 Bash V
크로스 플랫폼 ❌ Linux/Mac만 ✅ Windows/Mac/Linux 모두
에러 처리 약함 (set -e 정도) 강함 (!, or {})
타입 안전성 없음 (문자열 기반) 있음 (컴파일 타임 검사)
실행 방식 인터프리터 v run (컴파일+실행 즉시)
IDE 지원 제한적 자동 완성, 에러 표시
가독성 $?, &&, \|\| 등 복잡 일반 프로그래밍 문법

비유하자면, Bash는 만능 칼이다. 가볍고 어디서든 쓸 수 있지만, 복잡한 작업에는 불편하다. V는 전동 공구 세트다. 약간 무겁지만, 더 안전하고, 더 복잡한 작업을 정확하게 할 수 있다.


1단계: v run — 스크립트처럼 실행

V 파일은 v run으로 컴파일과 실행을 한 번에 할 수 있다. 별도의 빌드 과정 없이 스크립트처럼 사용할 수 있다.

v run script.v

컴파일이 매우 빠르기 때문에, 체감상 Python 스크립트를 실행하는 것과 거의 비슷한 속도다. bash에서 ./script.sh를 실행하는 것처럼 자연스럽게 쓸 수 있다.


2단계: 파일과 디렉토리 조작

1편에서 os 모듈의 파일 조작을 배웠다. 스크립트에서 자주 쓰는 작업들을 정리하자.

Bash vs V 비교

# Bash
mkdir -p project/src project/tests
cp template.txt project/README.md
echo "Hello" > project/src/main.txt
rm -rf old_project
// V
import os

fn main() {
    os.mkdir_all('project/src') or {}
    os.mkdir_all('project/tests') or {}
    os.cp('template.txt', 'project/README.md') or {}
    os.write_file('project/src/main.txt', 'Hello') or {}
    os.rmdir_all('old_project') or {}
}

주요 파일 조작 함수

Bash V (os 모듈) 설명
mkdir -p dir os.mkdir_all('dir') 디렉토리 생성 (중간 경로 포함)
cp src dst os.cp('src', 'dst') 파일 복사
mv src dst os.rename('src', 'dst') 파일 이동/이름 변경
rm file os.rm('file') 파일 삭제
rm -rf dir os.rmdir_all('dir') 디렉토리 삭제 (재귀)
cat file os.read_file('file') 파일 내용 읽기
echo "x" > file os.write_file('file', 'x') 파일에 쓰기
ls dir os.ls('dir') 디렉토리 목록
pwd os.getwd() 현재 디렉토리
test -f file os.exists('file') 파일 존재 확인
test -d dir os.is_dir('dir') 디렉토리 확인

이 표를 옆에 놓고 참조하면, Bash 스크립트를 V로 옮기는 것이 매우 쉽다. 함수 이름이 직관적이어서, 대응되는 Bash 명령을 알면 V 함수도 바로 찾을 수 있다.

파일 존재 확인 후 작업

스크립트에서 가장 흔한 패턴 — "있으면 이것, 없으면 저것"이다.

import os

fn main() {
    // 설정 파일이 있으면 읽고, 없으면 새로 만든다
    config_path := 'config.json'

    if os.exists(config_path) {
        content := os.read_file(config_path) or {
            println('❌ 설정 읽기 실패: ${err}')
            return
        }
        println('📄 기존 설정 발견:')
        println(content)
    } else {
        default_config := '{\n  "name": "my-project",\n  "version": "0.1.0"\n}'
        os.write_file(config_path, default_config) or {
            println('❌ 설정 생성 실패: ${err}')
            return
        }
        println('✅ 기본 설정 파일 생성 완료')
    }
}

Bash에서는 if [ -f config.json ]; then ... fi 형태로 쓰는데, V에서는 if os.exists(...) 로 훨씬 읽기 쉽다.


3단계: 환경변수 읽기

스크립트에서 환경변수는 필수다. 사용자 홈 디렉토리, PATH, 프로젝트 설정 등을 환경변수에서 읽는 경우가 많다.

import os

fn main() {
    // 환경변수 읽기
    home := os.getenv('HOME')                    // Linux/Mac
    user_profile := os.getenv('USERPROFILE')     // Windows

    // 사용 가능한 값 선택 (크로스 플랫폼)
    user_home := if home != '' { home } else { user_profile }

    println('홈 디렉토리: ${user_home}')

    // PATH 출력
    path := os.getenv('PATH')
    println('PATH: ${path}')

    // 환경변수가 설정되지 않았을 때
    api_key := os.getenv('API_KEY')
    if api_key == '' {
        println('⚠️  API_KEY가 설정되지 않았습니다')
        println('   export API_KEY=your_key_here')
    } else {
        println('✅ API_KEY 발견')
    }
}

os.getenv('변수명') — 환경변수를 읽는다. 설정되지 않았으면 빈 문자열을 반환한다. (에러가 나지 않는다.)

크로스 플랫폼에서 주의할 점이 있다. Linux/Mac은 홈 디렉토리가 HOME, Windows는 USERPROFILE이다. V 코드에서 둘 다 확인하면 어디서든 동작한다.


4단계: os.execute — 외부 명령 실행

V 스크립트에서 시스템 명령을 실행할 수 있다. git init, npm install 같은 명령을 V 스크립트 안에서 호출하는 것이다.

기본 사용법

import os

fn main() {
    // 외부 명령 실행
    result := os.execute('echo Hello from V!')

    println('종료 코드: ${result.exit_code}')
    println('출력: ${result.output}')
}
// 종료 코드: 0
// 출력: Hello from V!

os.execute('명령어') — 명령어를 실행하고, 종료 코드출력을 함께 반환한다.

os.execute() 반환값:
┌──────────────────────────────┐
│ exit_code: int               │  0 = 성공, 그 외 = 실패
│ output:    string            │  명령의 표준 출력(stdout)
└──────────────────────────────┘

성공/실패 확인

import os

fn run_cmd(cmd string) bool {
    println('▶ 실행: ${cmd}')
    result := os.execute(cmd)

    if result.exit_code != 0 {
        println('  ❌ 실패 (코드: ${result.exit_code})')
        if result.output.len > 0 {
            println('  출력: ${result.output.trim_space()}')
        }
        return false
    }

    println('  ✅ 성공')
    if result.output.trim_space().len > 0 {
        println('  출력: ${result.output.trim_space()}')
    }
    return true
}

fn main() {
    run_cmd('git --version')
    run_cmd('git status')
}

exit_code0이면 성공, 그 외의 값이면 실패다. 이것은 모든 운영체제에서 동일한 규칙이다. run_cmd 같은 헬퍼 함수를 만들어두면 여러 명령을 깔끔하게 실행할 수 있다.

크로스 플랫폼 명령

Windows와 Linux에서 일부 명령이 다르다. V에서 운영체제를 감지해서 적절한 명령을 실행할 수 있다.

import os

fn clear_screen() {
    $if windows {
        os.execute('cls')
    } $else {
        os.execute('clear')
    }
}

fn list_files(dir string) {
    $if windows {
        result := os.execute('dir /b "${dir}"')
    } $else {
        result := os.execute('ls -la "${dir}"')
    }
}

$if windows { ... } $else { ... } — V의 컴파일 타임 조건문이다. 컴파일할 때 운영체제를 감지해서, 해당하지 않는 코드는 아예 포함하지 않는다. Bash에서 if [[ "$OSTYPE" == "linux-gnu"* ]]로 분기하는 것보다 훨씬 깔끔하다.

하지만 가능하면 os.execute 대신 os 모듈의 함수를 쓰는 것이 좋다. os.ls(), os.mkdir_all() 등은 이미 크로스 플랫폼으로 동작하기 때문이다. os.executegit, npm처럼 V에서 직접 할 수 없는 작업에만 사용하자.


5단계: 완성 — 프로젝트 초기 세팅 자동화

모든 도구를 합쳐서, 새 프로젝트를 자동으로 세팅하는 스크립트를 만들자.

이 스크립트가 하는 일:
1. 프로젝트 폴더 구조 생성
2. 기본 파일 생성 (README, .gitignore, 설정 파일)
3. git 초기화
4. 결과 요약 출력

완성 코드

import os
import time

// 프로젝트 이름 받기
fn get_project_name() string {
    if os.args.len >= 2 {
        return os.args[1]
    }
    return 'my-project'
}

// 디렉토리 생성 (이미 있으면 건너뛰기)
fn ensure_dir(path string) bool {
    if os.is_dir(path) {
        println('  ⏭️  ${path}/ (이미 존재)')
        return true
    }
    os.mkdir_all(path) or {
        println('  ❌ ${path}/ 생성 실패: ${err}')
        return false
    }
    println('  📁 ${path}/ 생성')
    return true
}

// 파일 생성 (이미 있으면 건너뛰기)
fn create_file(path string, content string) bool {
    if os.exists(path) {
        println('  ⏭️  ${path} (이미 존재)')
        return true
    }
    os.write_file(path, content) or {
        println('  ❌ ${path} 생성 실패: ${err}')
        return false
    }
    println('  📄 ${path} 생성')
    return true
}

// 외부 명령 실행
fn run_cmd(cmd string) bool {
    result := os.execute(cmd)
    return result.exit_code == 0
}

fn main() {
    project := get_project_name()
    start := time.now()

    println('')
    println('🚀 프로젝트 초기화: ${project}')
    println('─'.repeat(50))

    // 1. 폴더 구조 생성
    println('')
    println('📁 폴더 구조 생성:')
    dirs := [
        project,
        '${project}/src',
        '${project}/tests',
        '${project}/docs',
        '${project}/scripts',
    ]
    for d in dirs {
        ensure_dir(d)
    }

    // 2. README.md 생성
    println('')
    println('📄 파일 생성:')
    readme := '# ${project}

> 프로젝트 설명을 여기에 적으세요.

## 시작하기

v run src/main.v

## 구조

${project}/
├── src/        # 소스 코드
├── tests/      # 테스트
├── docs/       # 문서
└── scripts/    # 자동화 스크립트
'
    create_file('${project}/README.md', readme)

    // 3. .gitignore 생성
    gitignore := '# V 컴파일 결과물
*.exe
*.o
*.so
*.dll

# 빌드 디렉토리
/build/

# OS 파일
.DS_Store
Thumbs.db

# IDE
.vscode/
.idea/
'
    create_file('${project}/.gitignore', gitignore)

    // 4. 메인 소스 파일 생성
    main_v := "import os

fn main() {
    println('Hello from ${project}!')
    println('현재 디렉토리: ' + os.getwd())
}
"
    create_file('${project}/src/main.v', main_v)

    // 5. v.mod 생성
    vmod := "Module {
    name: '${project}'
    description: ''
    version: '0.1.0'
    dependencies: []
}
"
    create_file('${project}/v.mod', vmod)

    // 6. git 초기화
    println('')
    println('🔧 Git 초기화:')
    if os.is_dir('${project}/.git') {
        println('  ⏭️  .git/ (이미 초기화됨)')
    } else {
        if run_cmd('git -C ${project} init') {
            println('  ✅ git init 완료')
        } else {
            println('  ⚠️  git이 설치되지 않았습니다 (건너뜀)')
        }
    }

    // 7. 결과 요약
    elapsed := time.since(start)
    file_count := os.walk_ext(project, '.md').len +
                  os.walk_ext(project, '.v').len +
                  os.walk_ext(project, '.gitignore').len

    println('')
    println('─'.repeat(50))
    println('✅ 프로젝트 "${project}" 초기화 완료!')
    println('   📁 디렉토리: ${dirs.len}개')
    println('   ⏱️  소요 시간: ${elapsed.milliseconds()}ms')
    println('')
    println('시작하기:')
    println('   cd ${project}')
    println('   v run src/main.v')
    println('')
}

실행

# 기본 프로젝트 이름으로 실행
v run init_project.v

# 프로젝트 이름 지정
v run init_project.v my-awesome-app

실행 결과

🚀 프로젝트 초기화: my-awesome-app
──────────────────────────────────────────────────

📁 폴더 구조 생성:
  📁 my-awesome-app/ 생성
  📁 my-awesome-app/src/ 생성
  📁 my-awesome-app/tests/ 생성
  📁 my-awesome-app/docs/ 생성
  📁 my-awesome-app/scripts/ 생성

📄 파일 생성:
  📄 my-awesome-app/README.md 생성
  📄 my-awesome-app/.gitignore 생성
  📄 my-awesome-app/src/main.v 생성
  📄 my-awesome-app/v.mod 생성

🔧 Git 초기화:
  ✅ git init 완료

──────────────────────────────────────────────────
✅ 프로젝트 "my-awesome-app" 초기화 완료!
   📁 디렉토리: 5개
   ⏱️  소요 시간: 12ms

시작하기:
   cd my-awesome-app
   v run src/main.v

두 번 실행하면?

이미 있는 파일은 건너뛰고, 없는 것만 생성한다. 멱등성(idempotency) — 스크립트를 여러 번 실행해도 같은 결과가 나온다. 이것은 좋은 자동화 스크립트의 핵심 특성이다.

🚀 프로젝트 초기화: my-awesome-app
──────────────────────────────────────────────────

📁 폴더 구조 생성:
  ⏭️  my-awesome-app/ (이미 존재)
  ⏭️  my-awesome-app/src/ (이미 존재)
  ...

🔧 Git 초기화:
  ⏭️  .git/ (이미 초기화됨)

Bash vs V 최종 비교

같은 자동화 스크립트를 Bash와 V로 비교해보자.

Bash 버전 (핵심 부분만)

#!/bin/bash
set -e

PROJECT=${1:-my-project}

mkdir -p "$PROJECT"/{src,tests,docs,scripts}

cat > "$PROJECT/README.md" << EOF
# $PROJECT
> 프로젝트 설명
EOF

cat > "$PROJECT/.gitignore" << EOF
*.exe
*.o
.DS_Store
EOF

cd "$PROJECT" && git init

echo "✅ 완료: $PROJECT"

비교

항목 Bash V
Windows 지원
에러 처리 set -e (전체 중단) or {} (개별 처리)
이미 존재하는 파일 덮어쓰기 건너뛰기 (안전)
멱등성 추가 코드 필요 기본 제공
실행 시간 표시 추가 코드 필요 time 모듈로 간단
코드 자동 완성 ✅ (IDE 지원)
타입 안전성

V 버전이 길지만, 더 안전하고 더 유연하다. 파일이 이미 있으면 건너뛰고, 에러가 나면 개별적으로 처리하며, Windows에서도 동작한다.


문법 시리즈 연결

활용된 문법 배운 편 이번 편에서
os 모듈 파일 조작 실전 1편 폴더/파일 생성, 존재 확인
os.args 실전 1편 프로젝트 이름 인자
or {} 에러 처리 8편 파일 작업 에러 처리
for...in 루프 4편 디렉토리 목록 순회
if/else 4편 존재 확인 분기
문자열 보간 ${} 2편 파일 내용 동적 생성
time 모듈 실행 시간 측정 (새로 소개)

📝 정리

  • [x] v run — V 파일을 스크립트처럼 즉시 실행
  • [x] os 모듈 — Bash 명령 대신 V 함수로 파일/디렉토리 조작 (크로스 플랫폼)
  • [x] os.getenv — 환경변수 읽기
  • [x] os.execute — 외부 명령 실행, exit_codeoutput 반환
  • [x] $if windows — 컴파일 타임 OS 분기
  • [x] 멱등성 — 이미 존재하면 건너뛰기, 여러 번 실행해도 안전
  • [x] Bash 대비 장점 — 크로스 플랫폼, 에러 처리, 타입 안전, IDE 지원

다음 편 예고

실전 7편: 테스트와 디버깅 — 버그 없는 V 코드

코드를 짰으면 테스트를 해야 한다! V의 내장 테스트 프레임워크(v test, _test.v), assert문, 테이블 기반 테스트, 벤치마크까지 — 이전 편에서 만든 코드에 테스트를 추가하면서 배운다.