macOS에서 sed -i가 invalid command code로 실패할 때: 오류 없이 파일이 어긋나는 경우까지 직접 시험한 결과
리눅스식 sed -i 명령을 macOS에서 돌려 오류를 만났거나 오류 없이 결과가 이상한 독자가 원인을 가려내고 안전한 형태로 바꾸게 돕는다.
이 맥의 sed는 -i 바로 뒤 단어를 백업 접미사로 가져간다. 그래서 파일 이름이 스크립트로 읽혀 오류가 나고, -i 뒤에 -e나 -n을 하나 두면 오류 없이 이상한 백업이 생기거나 첫 줄이 복제된다. sed -i.bak ... && rm 파일.bak 이 이 맥에서 일반 파일에 시험한 안전한 형태다(같은 이름의 .bak 이 없을 때).
스크립트와 문서에서 sed -i 바로 뒤에 옵션이나 스크립트가 오는 곳을 찾아 -i.bak 형태로 바꾸고, 지금 폴더에 *-e, *-n 같은 이름의 파일이 있는지 확인한다.
한 줄 요약: 이 맥(26.6.2)의 sed는 -i 바로 뒤 단어를 백업 접미사로 가져가므로 같은 이름의 .bak 이 없을 때 sed -i.bak 형태로 쓰고 끝나면 지우며, -i 바로 뒤에 -e, -n, -E 를 두지 마라.
리눅스에서 쓰던 sed -i 's/old/new/' c.txt 를 맥 터미널에 붙였더니 아래처럼 실패한다.
sed: 1: "c.txt
": command c expects \ followed by text
이 글은 같은 명령이 파일 이름에 따라 invalid command code, extra characters at the end of d command 같은 서로 다른 오류로 보이는 이유를 이 맥에서 직접 만들어 보이고, 오류 없이 종료 코드 0 으로 끝나는데 파일이나 폴더가 어긋나는 경우까지 가려낸다. 확인한 환경은 macOS 26.6.2 arm64의 /usr/bin/sed(text_cmds-199), bash 3.2.57, perl 5.34.1이고, GNU sed와 리눅스는 시험하지 않았다. zsh는 zsh -c 로 같은 오류가 나는지만 확인했다.
용어 몇 개. sed는 텍스트를 규칙대로 바꾸는 명령이고, -i는 결과를 화면이 아니라 파일에 바로 쓴다. 접미사는 원본을 백업할 때 파일 이름 뒤에 붙이는 글자(.bak)다. 종료 코드는 0이 정상, 그 밖은 실패다. 표준 입력(stdin)은 파일이 아니라 키보드나 파이프로 들어오는 입력이고, BRE와 ERE는 sed의 기본 정규식과 -E 로 켜는 확장 정규식이다. '' 는 글자가 하나도 없는 빈 문자열 인자다.
영문자로 시작하는 파일 이름은 앞부분이 오류 문구를 정한다
같은 형태의 명령을 파일 이름만 바꿔 돌렸다. c.txt, d.txt, ./c.txt 는 host=old 로 시작하는 3줄짜리 파일로 만들어 두었고, config.yml, file.txt, x.txt 는 파일이 없는 이름이었다. 없는 이름도 sed가 스크립트를 읽는 단계에서 같은 오류를 냈다.
명령 (모두 sed -i 's/old/new/' 파일) |
오류 문구 |
|---|---|
c.txt |
command c expects \ followed by text |
config.yml |
command c expects \ followed by text |
./c.txt |
invalid command code . |
file.txt |
invalid command code f |
d.txt |
extra characters at the end of d command |
x.txt |
extra characters at the end of x command |
여섯 경우 모두 종료 코드는 1이었고, 내용을 확인한 c.txt 와 d.txt 는 바뀌지 않았다. 문구가 파일 이름 앞부분을 따라가는 것이 단서다. 이 맥의 man sed 는 -i 를 [-i extension] 으로 적는다. 즉 -i 가 바로 뒤 인자를 접미사로 가져간다. 이 글의 해석은 이렇다. -i 가 's/old/new/' 를 접미사로 먹고, 남은 첫 인자인 파일 이름이 스크립트로 읽히며, 첫 글자가 sed 명령 문자(c, d, x 등)이면 대부분 그 명령의 문법 오류가, 명령 문자가 아니면 invalid command code 가 나온다. 여섯 사례가 모두 이 설명으로 맞는다. 이 규칙은 영문자로 시작하는 이름에서 시험한 것이다. 표의 ./c.txt 처럼 영문자가 아닌 첫 글자는 아래에서 따로 적는다.
첫 글자를 더 바꿔 보니 문구는 글자마다 달랐다. 모두 없는 파일 이름으로 돌렸고 종료 코드는 1이었다.
| 첫 글자 | 오류 문구 |
|---|---|
a, i |
command a expects \ followed by text (i 도 같은 꼴) |
g, h, l, n, p, q |
extra characters at the end of g command (글자만 다름) |
D, G, H, N, P (대문자) |
extra characters at the end of D command (Dockerfile, Gemfile, Hosts, Nginx.conf, Procfile 로 확인. 대문자는 나열한 글자만 시험했다) |
b, t |
undefined label '.txt' (첫 글자를 뺀 나머지 이름이 라벨로 나오고 줄 번호는 2. backup.sh 는 'ackup.sh') |
s |
s.txt, sub/x.txt 는 unterminated substitute pattern, service.yml, senses.txt 는 unescaped newline inside substitute pattern. 둘째 글자가 구분자로 읽혀 뒤쪽 글자에 따라 갈리므로 첫 글자만으로 문구를 예측할 수 없다 |
y |
y.txt 는 unterminated transform source string, yeye.txt 는 unterminated transform target string. s 와 같은 이유로 뒤쪽 글자에 따라 갈린다 |
z, e, j, k, m, o, u, v, M, R, X, W |
invalid command code z (글자만 다름. Makefile, README, Xcode.txt, Wfile 포함) |
r |
-I or -i may not be used with stdin |
w |
같은 stdin 오류, 그리고 w 를 뺀 이름의 파일이 만들어지거나 0바이트로 잘림 |
r 과 w 는 이 글의 해석으로는 이름 앞 글자가 파일 읽기·쓰기 명령이 되고 입력 파일이 하나도 안 남아 표준 입력 오류가 난 것이다. 이 중 w 는 오류가 났는데도 데이터가 사라질 수 있다. w 명령은 나머지 이름의 파일을 열면서 비우기 때문이다.
- 이 맥에서
work.log를 주었더니 이미 있던ork.log가 8바이트에서 0바이트로 잘렸고,w.txt는 숨김 파일.txt를 만들거나 비웠다. w앞에 sed 주소로 읽히는 접두가 붙어도 같았다.3week.md는eek.md를,2w-report.txt는-report.txt를,weekly.md는eekly.md를 0바이트로 만들었고,1w.txt,$w.txt,1,2w.txt,1!w.txt,/x/!w.txt는.txt를 0바이트로 만들었다. 걸리는 것은 숫자 바로 뒤에w가 오는 이름(3week.md)이고, 앞의 표에 있는1.txt나2024.log는 비우지 않았다. 여기 든 것 말고도 주소 모양은 더 있을 수 있다.- 경로도 같다.
/tmp/w.txt는 현재 폴더의.txt를,/etc/wsl.conf는sl.conf를 0바이트로 만들었다. 조건은 첫/디렉터리/구간 바로 뒤에w가 오는 것이다./home/me/w.txt,/var/log/w.log처럼 구간 뒤 글자가m,l이면 거기서 막혀 비워지지 않았다. /var/www/ww처럼w뒤 이름이ww/ww로 읽히는 하위 경로이면 현재 폴더 상태에 따라 갈렸다. 폴더ww가 있으면 그 안에 새 파일이 생겼고, 없으면No such file or directory, 같은 이름의 일반 파일이면Not a directory오류였다.- 대문자
Wfile은invalid command code W일 뿐 비우지 않았다.
새 파일이 생기는 것만이 아니라 기존 파일을 덮어쓰는 셈이라, ls -A 로 .txt 가 보여도 원래 있던 것인지 이 명령이 비운 것인지 구분되지 않는다. 임시 폴더에서 직접 확인해 볼 수 있다.
cd "$(mktemp -d)" && printf 'keep\n' > .txt
sed -i 's/old/new/' w.txt ; wc -c .txt
# sed: -I or -i may not be used with stdin
# 0 .txt
이 코드는 .txt 를 먼저 만들어 두므로 0바이트가 나오면 w 명령이 비운 것이다.
이 명령을 돌린 적이 있고 파일 이름이 w 로 시작하거나, 숫자·$ 같은 주소 뒤에 w 가 오거나, /디렉터리/w… 꼴이었다면 이름에서 w 까지를 뺀 파일이 현재 폴더에 있었는지 확인하고 버전 관리나 백업으로 되돌린다.
영문자가 아닌 첫 글자는 규칙이 더 갈렸다. 모두 없는 이름으로 돌렸다.
| 파일 이름 | 오류 문구 |
|---|---|
/tmp/x.txt |
extra characters at the end of x command (/ 로 시작해 주소 모양으로 읽힌 뒤 x 에서 막힘) |
/etc/hosts |
extra characters at the end of h command |
1.txt, 2024.log |
invalid command code . |
#x.txt |
-I or -i may not be used with stdin |
}x.txt |
unexpected } |
=x.txt |
extra characters at the end of = command |
-x.txt |
illegal option -- x 와 사용법 |
그러니 “첫 글자가 오류를 정한다”는 말은 영문자 이름에서 확인한 규칙이고, 절대 경로나 숫자로 시작하는 파일은 문구가 다른 지점에서 정해진다.
공개된 사례도 같은 모양이다. 선행 글이 이미 접미사가 필요하다는 원인과 sed -i "" 해법을 적었고, 이 글이 더한 것은 첫 글자별 문구, 오류 없는 어긋남, 데이터 손실, 정규식 표다. 2011년 한 블로그 글은 sed -i 's/require/include/' Rakefile 에서 sed: 1: "Rakefile": invalid command code R 이 났다고 쓰고, ISC의 Stork 이슈에는 extra characters at the end of d command 가 실려 있다. 두 글의 맥 버전은 확인하지 않았다.
맥에서 되는 형태
접미사 자리를 채우면 된다. 이 맥에서 시험한 결과는 이렇다.
| 형태 | 결과 |
|---|---|
sed -i '' 's/old/new/' c.txt |
종료 코드 0, 치환됨, 백업 없음 |
sed -i.bak 's/old/new/' c.txt |
종료 코드 0, 치환됨, c.txt.bak 생김 |
sed -i'' 's/old/new/' c.txt |
실패. 셸이 빈 따옴표를 지워 -i 만 남아서 첫 표의 오류와 같다 |
sed --in-place ... |
illegal option -- -, 종료 코드 1 |
-i '' 는 이 맥에서 통과한 표기이고, GNU sed에서 어떻게 읽히는지는 시험하지 못했다. GNU sed의 Debian 매뉴얼은 이 옵션을 -i[SUFFIX] 로 적고 “makes backup if SUFFIX supplied” 라고 쓴다. 접미사를 옵션에 붙여 쓰는 표기이므로 -i.bak 형태는 GNU 쪽 문서에도 있는 꼴이다. 그래서 시험하지 못한 GNU 쪽을 보수적으로 보고, 양쪽에서 돌릴 스크립트에는 -i '' 를 쓰지 않는 쪽을 택한다. 이 맥에서 시험해 통과한 후보는 셋이고, 모두 -i 에 접미사를 붙이거나 -i 를 아예 안 쓴다.
sed -i.bak 's/old/new/' c.txt && rm c.txt.bak
perl -pi -e 's/old/new/' c.txt
sed 's/old/new/' c.txt > c.tmp && mv c.tmp c.txt
세 줄 모두 이 맥에서 종료 코드 0 으로 host=new 를 만들었다. 여기서 ’통과’의 기준은 이 두 가지뿐이고, 심볼릭 링크와 권한에서는 갈린다. 링크에 돌리면 sed -i.bak 는 오류로 멈춰 링크를 건드리지 않았지만 perl -pi -e 와 임시 파일 경유는 링크를 일반 파일로 바꿨고 원래 대상은 그대로였다. 권한 600 파일은 sed -i.bak 와 perl 이 600 을 유지했고 임시 파일 경유는 644 가 되었다. 리눅스에서도 되리라는 근거는 -i[SUFFIX] 표기와 perl, 리다이렉트의 일반 동작일 뿐 시험은 아니다. 첫 줄은 접미사를 -i 에 붙였을 뿐이라 .bak 을 남기고 지우는 한 단계가 더 있다. 이 맥의 man sed 는 -I 항목에서 길이가 0인 접미사로 제자리 편집하는 것을 디스크 공간이 모자랄 때 손상될 위험이 있어 권하지 않는다고 적는다. 그 이유로도 .bak 을 거치는 쪽이 낫다. 다만 같은 이름의 파일.bak 이 이미 있으면 sed -i.bak 단계에서 그 내용이 원본으로 덮이고 && rm 이 파일을 지우니 이름이 겹치는지 먼저 본다. zsh -c 로도 sed -i 's/old/new/' c.txt 는 같은 오류로 실패했고 -i '' 는 통과했다.
오류 없이 어긋나는 경우가 더 위험하다
-i 바로 뒤에 다른 옵션을 두는 습관이 있다면 이쪽을 먼저 확인한다. 이번에는 오류가 안 난다.
sed -i -e 's/old/new/' c.txt # 종료 코드 0, 치환됨
ls
# c.txt c.txt-e
-e 가 접미사로 읽혀 c.txt-e 라는 백업이 조용히 생긴다. 이쪽은 쓰레기 파일이 남는 정도지만 -n 은 파일 내용이 바뀐다. 첫 줄만 남기려고 쓴 명령이다.
sed -i -n '1p' c.txt # 종료 코드 0
cat c.txt
# host=old
# host=old
# name=a b
# line3
-n 이 접미사로 읽혀 -n 효과가 사라지고 1p 가 첫 줄을 한 번 더 출력해, 3줄이던 파일이 4줄이 되었다. 백업 c.txt-n 도 생겼다. 접미사를 비워 sed -i '' -n '1p' c.txt 로 쓰면 의도대로 1줄만 남았다. -E 는 다르게 터진다. sed -i -E 's/(old)/[\1]/' c.txt 는 -E 가 접미사로 읽혀 확장 정규식이 꺼진 채 실행되어 \1 not defined in the RE 로 종료 코드 1, 파일은 그대로다. sed -i '' -E ... 로 쓰면 host=[old] 가 나왔다.
스크립트를 고친 뒤에는 폴더에 파일이름-e, 파일이름-n 같은 꼴이 남아 있는지 한 번 찾아 지운다. -i -e A -e B 처럼 -e 를 두 번 주면 이 글의 해석으로는 첫 -e 가 접미사가 되어 A 가 스크립트로 먼저 읽히고 둘째 -e, B, 파일 이름이 모두 파일 자리로 밀려 -e: No such file or directory, 종료 코드 1이었고 c.txt-e 는 만들어지지 않았다(-i '' 로 쓰면 통과). 이 맥에서는 -i 바로 뒤에 하나씩 둔 -e, -n, -s, -r, -u, -a, -l, -H 가 모두 종료 코드 0 으로 c.txt-s 같은 백업 이름을 남겼으니 *-? 꼴의 파일을 모두 찾아본다. -f 스크립트파일 은 다르게 실패했다. 스크립트 파일 이름이 접미사가 아니라 스크립트로 읽혀 unterminated substitute pattern 에 종료 코드 1이었다.
정규식도 조용히 안 맞는다
오류가 사라진 뒤에도 치환이 안 되는 경우가 있다. 이 맥의 sed 기본 정규식(BRE)에서 입력 aaa b 로 시험했다.
| 정규식 | 결과 |
|---|---|
s/a\+/X/ |
aaa b (안 바뀜) |
s/a\?/X/ |
aaa b (안 바뀜) |
s/\s/_/, s/\bb/B/ |
aaa b (안 바뀜) |
-E 's/a+/X/' |
X b |
-E 's/\s/_/', -E 's/\bb/B/' |
aaa b (안 바뀜) |
-E 's/a\+/X/' |
aaa b (안 바뀜) |
s/[[:space:]]/_/ |
aaa_b |
s/[[:<:]]b/B/ |
aaa B |
-H 's/\s/_/', -H 's/\bb/B/' |
aaa_b, aaa B |
-H 's/a\+/X/' |
X b |
안 바뀐 줄은 오류 메시지도 없고 입력이 그대로 나온다. 교대(|)는 표에서 깨지기 쉬워 따로 적는다.
printf 'aaa b\n' | sed 's/\(a\|b\)/X/g' → aaa b (안 바뀜)
printf 'aaa b\n' | sed -E 's/(a|b)/X/g' → XXX X
\+, \s, \b 는 이 맥의 BRE에서는 그 뜻이 아니고(GNU sed에서의 동작은 시험하지 못했다), -E 로 바꿔도 \s 와 \b 는 안 맞는다. 이 맥에서 통한 길은 세 가지였고 고치는 범위가 서로 다르다. 확장 정규식 -E 는 \+ 표기를 + 로, 교대를 | 로 다시 써야 동작하고 \s, \b 표기는 -E 만으로는 안 맞는다(-E 's/a\+/X/' 도 안 바뀐다). [[:space:]] 는 POSIX 표기이고 공백을 고친다. [[:<:]] 는 단어 시작을 고치지만 이 맥의 man re_format 이 “not specified by IEEE Std 1003.2” 라고 적는 확장이라 이식용으로 믿으면 안 된다. -H 를 주어 이 맥의 man sed 가 말하는 “enhanced features” 를 켜면 \s, \b, \+ 가 모두 동작했고 \(a\|b\) 교대와 \w\+ 도 됐다. -E -H 를 함께 주면 \s, \b 는 동작했지만 \+ 는 안 맞았다. 이 셋과 별개로 perl -pe 's/\bb/B/' 는 aaa B 로 \b 표기가 그대로 통했다. 치환 쪽의 \U& 는 대문자 변환이 아니라 글자 U 로 나왔다(Uaaa b). 반대로 \t 매칭과 치환의 \n 은 이 맥 sed(26.6.2)에서 한 입력으로 시험했을 때 동작했다. 다른 macOS 버전은 시험하지 않았다.
그래도 안 되면 이것부터
- 오류 문구가
"파일이름 ...": ...모양이면-i바로 뒤 인자를 먼저 본다. 거기에 스크립트나 옵션이 있으면 접미사로 읽힌 것이다. - 심볼릭 링크에는
sed -i ''가in-place editing only works for regular files로 실패했고 링크도 대상도 바뀌지 않았다. 링크가 아니라 실제 파일 경로를 준다. - 파일 여러 개 중 하나가 없으면
-i에서는 종료 코드 1로 끝나는데, 그 파일의 앞 파일은 이미 바뀌었고 뒤 파일은 처리되지 않았다(없는 파일이 첫 번째면 아무것도 안 바뀌었다).-i없이 같은 목록을 주면 없는 파일만 건너뛰고 나머지를 출력했고 종료 코드는 1이었다. 파일을 하나씩 나눠 돌리면 어디까지 바뀌었는지 알기 쉽다. - 정규식이 안 맞는데 오류도 없으면
sed대신perl -pe로 같은 표현을 먼저 시험해 보고 차이가 나면 위 표를 본다.
맥에서 sed -i 's/a/b/' 파일 이 실패하는 건 sed가 이상해서가 아니라, 이 글의 해석으로는 ‘s/a/b/’ 를 백업 접미사로 읽고 파일 이름을 스크립트로 읽기 때문이다.
세 줄 요약
- 이 맥의 sed는 -i 바로 뒤 단어를 백업 접미사로 읽고 파일 이름을 스크립트로 읽어 이름 앞부분에 따라 오류가 나며, w 가 명령이 되면 기존 파일이 비워질 수 있다.
- -i 뒤에 -e 나 -n 을 하나 두면 오류 없이 c.txt-e 백업이 생기고 -n ‘1p’ 는 첫 줄이 복제되므로 sed -i.bak … && rm 으로 쓴다(같은 이름의 .bak 이 없을 때).
- BRE의 + \s \b 는 오류 없이 안 맞는다. -E 는 + 를 + 로 써야 하고 \s \b 표기는 안 맞는다. -H 는 표기 그대로 고치지만 -E 와 함께면 + 는 안 맞는다.