본문 바로가기
빌드노트 검색

빌드노트

Bash에서 for i in {1..$n}이 한 번만 도는 이유

Bash의 {1..$n}은 매개변수 확장보다 먼저 처리되어 숫자 목록이 되지 않는다. 짧은 출력 비교로 문자열 하나가 남는 것을 확인하고, 0..99 입력 검증과 산술 for로 변수 횟수를 표현한다. 0회·선행 0·POSIX sh의 경계도 구분한다.

Bash에서 n=3을 넣었는데 for i in {1..$n}이 한 번만 돈다면, 중괄호 안에 변수를 넣은 부분을 확인한다. brace expansion은 $n을 값으로 바꾸는 매개변수 확장보다 먼저 일어난다. 그 단계에서는 숫자 범위가 만들어지지 않는다. 횟수가 변수로 정해지는 반복은 입력을 정수로 검증한 뒤 for ((i=1; i<=n; i++))로 바꿀 수 있다.

숫자 셋이 아니라 문자열 하나가 남는다

Linux 또는 WSL에서 Bash가 필요하다. 터미널에서 bash --noprofile --norc -B로 초기화 파일을 읽지 않는 새 Bash를 열고 아래 코드를 실행한다. -B는 brace expansion을 켠다. 리터럴 범위와 변수 범위가 실제로 어떤 값을 만드는지 비교하는 입력이다.

n=3
printf 'literal=<%s>\n' {1..3}
printf 'variable=<%s>\n' {1..$n}
for i in {1..$n}; do printf 'i=<%s>\n' "$i"; done

Ubuntu 24.04.4 LTS의 Bash 5.2.21에서 직접 실행하니 literal=<1>, literal=<2>, literal=<3>이 각각 한 줄씩 나왔다. 이어서 variable=<{1..3}>과 i=<{1..3}>이 한 줄씩 나왔다. printf는 값마다 형식을 반복하고, 꺾쇠는 각 값의 경계를 표시한다.

변수 쪽에도 숫자 3은 보인다. 하지만 중괄호를 포함한 문자열 {1..3} 하나다. 마지막 반복문은 이 한 항목으로 한 번 실행됐다. 별도로 인자 수와 반복 횟수를 센 비교에서도 리터럴 범위는 인자 3개·반복 3회, 변수 범위는 인자 1개·반복 1회였다. 여기서 인자는 확장 결과를 받은 함수의 위치 인자이며, for가 별도 실행 파일의 argv를 만든다는 뜻은 아니다.

GNU Bash 매뉴얼의 Brace Expansion (새 탭에서 열림)은 범위의 양 끝에 정수나 문자가 와야 하며, 올바른 범위가 아니면 원문을 남긴다고 설명한다. 이 입력은 다음 순서로 해석된다.

  1. brace expansion 단계에서 {1..$n}을 만난다. $n은 아직 정수 끝값이 아니므로 범위가 확장되지 않는다.
  2. 매개변수 확장에서 $n이 3으로 바뀌어 {1..3}이 된다.
  3. brace expansion 단계로 돌아가지 않는다. 이 예제에서는 다른 단어 분리도 일어나지 않아 문자열 하나가 남는다.

확장의 전체 순서는 Shell Expansions (새 탭에서 열림)에 나와 있다. n=3을 더 일찍 대입해도 이 순서는 같다. 별도 실행에서 {1.."$n"}도 {1..3} 한 항목이었다. 큰따옴표를 붙여도 범위 생성 시점은 바뀌지 않는다.

횟수가 변수라면 숫자로 비교한다

같은 새 Bash에서 다음 코드를 실행하면, n을 0부터 99까지의 정수 표기로 제한한 뒤 세 번 반복한다. 자기 Bash 스크립트에서는 첫 줄을 실제 입력을 받는 대입으로 바꾸고 검사와 반복문을 적용한다. 상한 99는 작은 예제를 위한 정책이며 Bash 자체의 제한이 아니다.

n=3
if [[ ! $n =~ ^(0|[1-9][0-9]?)$ ]]; then
  printf 'n must be 0..99 without leading zeros\n' >&2
  exit 2
fi
for ((i=1; i<=n; i++)); do
  printf 'i=<%d>\n' "$i"
done

실제 출력은 i=<1>, i=<2>, i=<3> 세 줄이었고 종료 상태는 0이었다. 산술 for는 i=1에서 시작해 i<=n인 동안 실행하고 i++로 증가시킨다. 숫자 목록을 먼저 만들지 않으며 산술식 안에서 변수 이름 n으로 값을 참조한다. 반복문의 평가 순서 (새 탭에서 열림)

검사는 산술식에 넣기 전에 한다. =~는 정규식 비교이고, 양 끝의 ^와 $는 문자열 전체가 맞아야 한다는 뜻이다. 위 식은 0 또는 0으로 시작하지 않는 한두 자리 숫자만 허용한다. Bash의 정규식 조건 (새 탭에서 열림)

두 번째 코드의 첫 줄을 n=0으로 바꾸자 출력 없이 종료 상태 0이었다. n=08, n=1+2, n=-1, n=100, n='', n=' 3 '으로 각각 바꾼 실행에서는 오류 문구만 표준 오류로 나오고 종료 상태 2였다. 반복 출력은 없었다. 잘못된 값을 0회로 처리하지 않고 거부한다. 이때 exit 2는 실행 중인 새 Bash를 종료하므로 다음 비교에는 새 Bash를 다시 연다. 정상 확인 뒤에는 exit로 원래 터미널에 돌아간다.

Bash 산술식은 변수 값을 다시 산술식으로 평가하고, 0으로 시작하는 상수는 8진수로 해석한다. 그래서 이 예제는 1+2 같은 식과 08 같은 표기를 함께 거부한다. Shell Arithmetic (새 탭에서 열림)의 규칙이다. 허용 범위를 늘릴 때는 필요한 최대 횟수와 입력 표기를 먼저 정하고 검사를 바꾼다.

고정 목록에는 {1..3}을 쓸 수 있다. 다만 {1..0}은 빈 목록이 아니다. 직접 실행했을 때 1, 0 두 항목으로 두 번 반복했다. 변수 횟수와 0회를 표현하려면 검증한 값으로 산술 비교를 한다. 문자열을 eval로 다시 해석할 필요는 없다.

리터럴 범위도 그대로 나오면 셸과 옵션을 확인한다

자기 코드에서 {1..3}마저 한 항목으로 남는다면 실제 실행 셸과 옵션을 확인한다. sh 파일명으로 실행하면 첫 줄에 Bash를 지정해도 실행 셸이 바뀌지 않는다. Bash에서도 set +B로 brace expansion을 끈 비교에서는 리터럴 범위가 그대로 남았다. 앞서 새 Bash를 열 때 준 -B는 이 옵션을 켜기 위한 것이다.

for ((...))와 [[ ... =~ ... ]]는 POSIX sh에 그대로 옮길 문법이 아니다. 이번 환경의 /bin/sh는 dash 0.5.12였고, {1..3}을 한 문자열로 남겼으며 산술 for에는 Bad for loop variable 구문 오류와 종료 상태 2를 반환했다. POSIX sh를 유지해야 하는 스크립트라면 입력 검증과 카운터를 증가시키는 while로 별도 작성한다.

문서 설명은 Bash 유지관리자 사이트의 GNU Bash 5.3 매뉴얼을 읽어 대조했다. 직접 실행은 WSL2 Ubuntu의 Bash 5.2.21·LC_ALL=C와 위 dash 비교이며, 반복 본문은 값 출력뿐이다. Bash 5.3 실행, 다른 로케일·셸 전반, 큰 정수·오버플로와 실제 업무 명령의 실패 처리는 검증하지 않았다.

← 전체 글 보기