mkdir 명령어 사용법: 디렉터리 만들기의 기본기

터미널을 열고 새 프로젝트를 시작할 때 가장 먼저 치는 명령어가 뭘까요? 아마 mkdir일 겁니다. 이름 그대로 make directory, 뒤에 이름만 붙이면 끝이라 딱히 배울 게 없어 보이는데요.
그런데 막상 쓰다 보면 mkdir: project/src: No such file or directory 같은 메시지에 한 번쯤 막힙니다. 만들라고 시켰는데 없다니, 무슨 소리인가 싶죠. 🤔 셸 스크립트에 mkdir을 넣었다가 두 번째 실행부터 에러가 터지면서 스크립트가 죽는 경험도 흔합니다.
이 글에서는 mkdir의 기본 사용법에서 출발해, 옵션 한두 개로 이런 문제가 어떻게 사라지는지 살펴보겠습니다. 명령어 자체는 간단하지만 왜 이렇게 동작하는지를 알아두면 터미널에서 보내는 시간이 한결 편해집니다.
디렉터리 하나 만들기
가장 기본적인 형태는 mkdir 뒤에 만들고 싶은 이름을 적는 것입니다.
$ mkdir blog
실행해도 아무 반응이 없습니다. 유닉스 명령어의 오랜 전통인데요. 성공하면 조용히 있고 문제가 생겼을 때만 입을 엽니다. 그러니 아무 말이 없다면 잘된 겁니다.
정말 만들어졌는지는 ls 쉘 커맨드로 확인합니다. -F 옵션을 붙이면 디렉터리 이름 뒤에 /가 붙어서 파일과 한눈에 구분됩니다.
$ ls -F
blog/
여러 개를 한 번에 만들기
mkdir은 인자를 여러 개 받습니다. 공백으로 구분해서 죽 나열하면 그만큼 만들어집니다.
$ mkdir src tests docs
$ ls -F
blog/ docs/ src/ tests/
여기서 주의할 점이 하나 있는데요. 이름에 공백이 들어가야 한다면 반드시 따옴표로 감싸야 합니다.
$ mkdir "my folder"
따옴표 없이 mkdir my folder라고 치면 셸이 공백을 기준으로 인자를 쪼개기 때문에 my와 folder라는 디렉터리가 따로 생겨버립니다. 공백을 인자 구분자로 보는 건 mkdir이 아니라 셸이 하는 일이라서, 따옴표로 “이건 하나의 덩어리”라고 알려줘야 합니다.
두 가지 흔한 에러 메시지
mkdir이 말을 걸어오는 경우는 대체로 둘 중 하나입니다. 첫 번째는 이미 같은 이름이 있을 때입니다.
$ mkdir blog
mkdir: blog: File exists
디렉터리든 파일이든 그 이름이 이미 쓰이고 있으면 거절합니다. 기존 걸 덮어쓰거나 비우지 않고 그냥 멈춘다는 점이 중요한데요. 덕분에 실수로 작업물을 날릴 일은 없습니다.
두 번째는 중간 경로가 없을 때입니다.
$ mkdir project/src/components
mkdir: project/src: No such file or directory
에러 메시지를 자세히 보면 힌트가 있습니다. 만들라고 시킨 건 project/src/components인데 없다고 지목한 건 project/src죠. mkdir은 기본적으로 디렉터리를 딱 하나만 만듭니다. project/src/components를 만들려면 그 부모인 project/src가 이미 존재해야 하는데, project조차 없으니 첫 삽부터 뜰 수가 없는 상황입니다.
그렇다고 mkdir project, mkdir project/src, mkdir project/src/components를 차례로 칠 수는 없는 노릇입니다. Unix/Linux 디렉토리 구조 이해하기에서 본 것처럼 경로가 깊어지면 이런 반복은 금방 고통스러워집니다.
중간 경로까지 한 번에 만드는 -p
그래서 있는 게 -p 옵션입니다. parents, 즉 부모 디렉터리도 필요하면 알아서 만들어달라는 뜻입니다.
$ mkdir -p project/src/components
$ ls -F project/src
components/
한 번에 project, project/src, project/src/components 세 개가 모두 생겼습니다. 경로를 훑어 내려가면서 없는 것만 채워 넣는 방식이라, 몇 단계가 비어 있든 상관없습니다.
무엇이 만들어졌는지 눈으로 확인하고 싶다면 -v(verbose) 옵션을 같이 붙입니다.
$ mkdir -pv project/src/components
project
project/src
project/src/components
새로 만든 것만 출력된다는 점이 유용한데요. 이미 있던 디렉터리는 목록에 나오지 않으니, 내가 예상한 만큼 실제로 생성됐는지 대조해볼 수 있습니다. 참고로 위 출력은 macOS 기준이고, 리눅스에 주로 들어 있는 GNU 버전은 mkdir: created directory ... 같은 좀 더 수다스러운 문장으로 알려줍니다.
-p의 진짜 매력은 멱등성입니다
-p를 그저 “깊은 경로 만들 때 쓰는 옵션”으로만 알고 계셨다면, 실은 절반만 아신 겁니다. -p에는 겉으로 잘 드러나지 않는 두 번째 성질이 있는데요. 이미 있는 디렉터리를 대상으로 실행해도 에러를 내지 않습니다.
$ mkdir -p project/src/components
$ mkdir -p project/src/components
$ echo $?
0
방금 만든 걸 그대로 한 번 더 실행했는데 아무 일도 일어나지 않았고, 종료 코드도 성공을 뜻하는 0입니다. 아까 mkdir blog를 두 번 쳤을 때 File exists로 거절당한 것과 대조적이죠.
이 차이가 왜 중요할까요? 셸 스크립트를 떠올려 보면 답이 나옵니다.
#!/bin/sh
set -e
mkdir build
cp -r src/* build/
set -e는 명령어 하나라도 실패하면 스크립트를 즉시 중단시키는 설정인데요. 이 스크립트는 처음 실행할 때는 멀쩡히 돌아가지만, 두 번째부터는 build가 이미 있다는 이유로 첫 줄에서 죽어버립니다. 정작 하려던 복사는 시작도 못 하고요. 😅
#!/bin/sh
set -e
mkdir -p build
cp -r src/* build/
-p 하나 붙였을 뿐인데 몇 번을 실행하든 결과가 같아집니다. 이렇게 여러 번 실행해도 결과가 달라지지 않는 성질을 멱등성(idempotency)이라고 부르는데요. 빌드 스크립트나 배포 스크립트처럼 반복 실행이 당연한 곳에서는 거의 필수적인 성질입니다. 스크립트 안의 mkdir에 습관적으로 -p를 붙이는 사람들이 많은 이유가 여기에 있습니다.
다만 이 편리함에는 뒷면도 있습니다. -p는 “이미 있어도 그냥 넘어가라”는 뜻이므로, 디렉터리가 이미 존재하는지 확인하는 용도로는 쓸 수 없습니다. 남이 만든 디렉터리를 내가 방금 만든 것으로 착각해도 mkdir -p는 알려주지 않습니다. 존재 여부를 꼭 알아야 한다면 -p 없이 실행해서 성공/실패로 판단하거나 [ -d dir ]로 따로 검사해야 합니다.
중괄호 확장으로 프로젝트 뼈대 세우기
새 프로젝트를 시작하면 보통 여러 디렉터리를 한꺼번에 만들게 되는데요. 이때 중괄호 확장(brace expansion)을 쓰면 한 줄로 끝납니다.
$ mkdir -p myapp/{src,tests,docs}
$ ls -F myapp
docs/ src/ tests/
중괄호는 중첩도 됩니다. 꽤 복잡한 구조도 한 줄에 담을 수 있습니다.
$ mkdir -p site/{src/{components,pages,styles},public}
$ find site -type d
site
site/public
site/src
site/src/components
site/src/pages
site/src/styles
여기서 짚고 넘어갈 게 있습니다. 중괄호 확장은 mkdir의 기능이 아닙니다. mkdir은 중괄호가 뭔지도 모릅니다. 셸이 명령어를 실행하기 전에 미리 펼쳐서 넘겨주는 것뿐인데요. echo를 앞에 붙여보면 무슨 일이 벌어지는지 그대로 보입니다.
$ echo mkdir -p myapp/{src,tests,docs}
mkdir -p myapp/src myapp/tests myapp/docs
mkdir 입장에서는 그냥 인자 세 개를 받은 것뿐입니다. 앞서 “여러 개를 한 번에 만들기”에서 본 것과 완전히 같은 상황이죠. 이 사실을 알면 따옴표를 잘못 썼을 때 벌어지는 일도 이해가 됩니다.
$ mkdir -p "test/{a,b}"
$ find test
test
test/{a,b}
따옴표로 감싸버리면 셸이 손대지 않고 그대로 넘기기 때문에, {a,b}라는 이름의 디렉터리가 진짜로 만들어집니다. 공백이 있을 땐 따옴표가 필요하지만 중괄호를 쓸 땐 씌우면 안 되는 정반대의 규칙입니다.
한 가지 더, 중괄호 확장은 bash와 zsh의 기능이지 모든 셸이 지원하는 표준은 아닙니다. #!/bin/sh로 시작하는 스크립트에 썼다가 환경에 따라 동작이 갈릴 수 있으니, 이식성이 중요한 스크립트라면 경로를 하나씩 나열하는 편이 안전합니다. 참고로 *나 ? 같은 Glob 패턴은 이미 있는 파일 이름에 매칭되는 반면, 중괄호 확장은 존재하지 않는 이름도 그냥 펼쳐준다는 점에서 다릅니다. 아직 만들지도 않은 디렉터리를 만드는 데 중괄호를 쓸 수 있는 이유입니다.
권한을 지정하는 -m 옵션
mkdir로 디렉터리를 만들면 권한이 자동으로 정해집니다. 보통은 소유자만 쓸 수 있고 나머지는 읽고 들어갈 수만 있는 상태인데요.
$ mkdir normal
$ ls -ld normal
drwxr-xr-x@ 2 dale wheel 64 Jul 17 08:10 normal
755 권한이 붙었습니다. 이 값은 mkdir이 정한 게 아니라 umask라는 설정에서 나옵니다. umask는 새로 만드는 파일과 디렉터리에서 빼낼 권한을 지정하는 값입니다.
$ umask
022
디렉터리의 기본값 777에서 umask 022를 걷어내면 755가 됩니다. 남에게 쓰기 권한을 주지 않겠다는 뜻이죠.
이 기본값 대신 원하는 권한을 직접 지정하려면 -m(mode) 옵션을 씁니다.
$ mkdir -m 700 secret
$ ls -ld secret
drwx------@ 2 dale wheel 64 Jul 17 08:10 secret
소유자만 접근할 수 있는 디렉터리가 만들어졌습니다. 재미있는 건 -m으로 지정한 값에는 umask가 적용되지 않는다는 점인데요.
$ umask
022
$ mkdir -m 777 open
$ ls -ld open
drwxrwxrwx@ 2 dale wheel 64 Jul 17 08:10 open
umask가 022인데도 755가 아니라 777이 그대로 들어갔습니다. umask는 어디까지나 “명시하지 않았을 때의 기본값”을 다듬는 장치라서, 대놓고 지정한 값 앞에서는 물러섭니다.
그런데 -m과 -p를 함께 쓸 때는 함정이 하나 있습니다.
$ mkdir -pm 700 a/b/c
$ ls -ld a a/b a/b/c
drwxr-xr-x@ 3 dale wheel 96 Jul 17 08:10 a
drwxr-xr-x@ 3 dale wheel 96 Jul 17 08:10 a/b
drwx------@ 2 dale wheel 64 Jul 17 08:10 a/b/c
700이 붙은 건 맨 끝의 a/b/c뿐이고, 가는 길에 만들어진 a와 a/b는 평범한 755입니다. -m은 인자로 준 그 경로에만 적용되고, -p가 알아서 채워 넣은 중간 디렉터리는 umask 기본값을 따르기 때문인데요. 비밀 디렉터리를 만든다고 mkdir -pm 700 secrets/keys를 쳤는데 정작 secrets는 누구나 들여다볼 수 있는 상태로 남는 셈입니다. 경로 전체를 잠가야 한다면 -m만 믿지 말고 만든 뒤에 chmod로 확인하는 편이 좋습니다.
만들자마자 그 안으로 들어가기
디렉터리를 만들면 십중팔구 바로 그 안으로 이동하게 되는데요. 경로를 두 번 타이핑하는 게 은근히 번거롭습니다.
$ mkdir -p deep/nested/dir
$ cd deep/nested/dir
이럴 때 $_를 쓰면 앞 명령어의 마지막 인자를 그대로 재활용할 수 있습니다.
$ mkdir -p deep/nested/dir && cd $_
$ pwd
/Users/dale/demo/deep/nested/dir
&&로 이어두면 mkdir이 성공했을 때만 cd가 실행되니, 생성에 실패했는데 엉뚱한 곳으로 이동하는 사고도 막아줍니다. 참고로 $_는 bash와 zsh의 기능이고, 위 예시에서는 -p가 옵션이라 마지막 인자인 deep/nested/dir이 잡힌 겁니다. 디렉터리를 여러 개 만드는 명령 뒤에 붙이면 맨 마지막 것만 잡히니 주의하세요.
잘못 만들었다면 rmdir
만드는 게 있으면 지우는 것도 있어야겠죠. mkdir의 짝은 rmdir입니다.
$ rmdir blog
다만 rmdir은 비어 있는 디렉터리만 지웁니다.
$ rmdir full
rmdir: full: Directory not empty
답답해 보이지만 실은 안전장치입니다. 안에 뭐가 들었는지 모르는 채로 통째로 날리는 일을 막아주니까요. 내용물까지 확실히 지울 생각이라면 rm -r을 써야 하는데, 이쪽은 되돌릴 수 없으니 경로를 두 번 확인하고 엔터를 치는 습관을 들이는 게 좋습니다. -p로 만든 깊은 경로를 통째로 되돌리고 싶다면 rmdir -p a/b/c처럼 쓰면 되는데, 이때도 각 단계가 비어 있어야만 지워집니다.
마치며
지금까지 mkdir을 살펴봤습니다. 이름만 붙이면 끝나는 단순한 명령어인데, 옵션 하나가 성격을 꽤 바꿔놓죠.
그중에서도 -p는 꼭 챙기시면 좋겠습니다. 중간 경로를 알아서 채워준다는 것보다, 이미 있어도 조용히 넘어가준다는 쪽이 실전에서는 더 요긴하거든요. 스크립트에 mkdir을 넣을 일이 생기면 거의 반사적으로 붙이게 될 겁니다. 중괄호 확장은 mkdir이 해주는 일이 아니라는 것만 기억해두시면 되고요. 셸이 미리 펼쳐서 넘겨주는 것뿐이라 친절하게 따옴표를 씌우면 오히려 망가집니다.
다음 단계로는 mkdir이 만들어낸 구조를 다루는 쪽으로 넘어가면 좋습니다. cp, mv, rm과 친해지고 나면 터미널에서 손으로 하던 작업을 스크립트로 옮기는 재미가 붙기 시작합니다.
더 자세한 옵션과 표준 동작이 궁금하시다면 POSIX mkdir 명세를 참고하세요.
This work is licensed under CC BY 4.0