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_code가 0이면 성공, 그 외의 값이면 실패다. 이것은 모든 운영체제에서 동일한 규칙이다. 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.execute는 git, 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_code와output반환 - [x]
$if windows— 컴파일 타임 OS 분기 - [x] 멱등성 — 이미 존재하면 건너뛰기, 여러 번 실행해도 안전
- [x] Bash 대비 장점 — 크로스 플랫폼, 에러 처리, 타입 안전, IDE 지원
다음 편 예고
실전 7편: 테스트와 디버깅 — 버그 없는 V 코드
코드를 짰으면 테스트를 해야 한다! V의 내장 테스트 프레임워크(
v test,_test.v),assert문, 테이블 기반 테스트, 벤치마크까지 — 이전 편에서 만든 코드에 테스트를 추가하면서 배운다.