SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-09

Bash 히스토리 확장 !! 및 !$ 사용법 정리

Bash에서 !!로 이전 명령을 반복하고 !$로 마지막 인자를 불러오는 방법을 설명합니다. 히스토리 확장을 활용하는 6가지 핵심 명령어와 실행 전 결과를 미리 확인하는 :p 지정자 활용법을 상세히 다룹니다.

Bash 히스토리 확장의 기능

Bash 히스토리 확장은 셸 히스토리에 기록된 이전 명령줄을 Bash가 실행하기 전에 다시 구성합니다. !!는 이전 명령을 반복합니다. !$은 이전 명령의 마지막 인자를 삽입합니다. 이 치환은 텍스트 기반으로 이루어지며, Bash가 명령줄을 단어 단위로 분리하기 전에 가장 먼저 수행됩니다. 따라서 결과물은 처음에 입력했던 문자와 정확히 일치합니다.

서버에서 수행하는 거의 모든 작업은 다음 6가지 형태로 처리할 수 있습니다.

  • !!은 이전 줄 전체를 반복하며, sudo !!은 이를 root 권한으로 다시 실행합니다.
  • !$는 이전 줄의 마지막 인자를 의미합니다.
  • !*은 이전 줄의 모든 인자를 의미합니다.
  • !n은 히스토리 번호 n에 해당하는 명령을 실행하며, !-n는 현재 위치에서 역순으로 번호를 셉니다.
  • !stringstring로 시작하는 가장 최근 명령을 실행합니다.
  • ^old^new는 이전 줄에서 첫 번째 oldnew로 치환하여 다시 실행합니다.

아래의 모든 내용은 서버의 대화형 프롬프트에서 직접 입력하는 기준입니다. 스크립트에서는 히스토리 확장이 비활성화되며, 이에 대해서는 마지막 섹션에서 설명합니다.

한 번만 입력하는 긴 경로

이 사례는 그 자체로 충분한 가치가 있습니다. 릴리스 디렉터리를 준비할 때 경로가 너무 길면 두 번 입력하는 과정에서 오타가 발생하기 쉽습니다.

sudo mkdir -p /srv/www/app/releases/2026-08-07
sudo chown -R deploy:deploy !$
ls -ld !$
sudo -u deploy nano !$/config.env

각 줄을 실행하기 전에 bash는 확장이 생성한 줄을 출력합니다.

sudo chown -R deploy:deploy /srv/www/app/releases/2026-08-07

이 echo는 확인을 위한 것입니다. 명령어의 출력을 읽기 전에 먼저 확인하십시오. bash가 !$을 무엇으로 해석했는지 알 수 있는 유일한 기회이기 때문입니다.

이 체인이 유지되는 이유는 bash가 사용자가 입력한 !$가 아니라 확장된 줄을 히스토리에 저장하기 때문입니다. 따라서 3번 줄은 확장된 2번 줄에서 마지막 인수를 가져오고, 4번 줄은 3번 줄에서 가져옵니다. 4번 줄은 또한 지정자 뒤에 텍스트가 올 수 있음을 보여줍니다. !$/config.env은 경로 끝에 /config.env이 붙은 형태가 되는데, 이는 단어 지정자가 /에서 멈추기 때문입니다.

같은 세션에서 하나 더 살펴보겠습니다. 파일을 편집한 후 해당 파일이 포함된 디렉터리를 나열하고 싶다면 다음과 같이 합니다.

ls -l !$:h

:h은 head 수정자입니다. 이는 경로에서 마지막 구성 요소를 제거하며, dirname와 동일한 작업을 수행합니다. :t는 마지막 구성 요소만 남기고, :r은 확장자를 제거하며, :e은 확장자만 남깁니다.

!! 및 sudo !!를 사용하여 마지막 명령 반복하기

sudo를 잊어버리면 서비스 관리자가 다음과 같이 알립니다:

systemctl restart nginx
Failed to restart nginx.service: Interactive authentication required.
sudo !!

Bash는 !!를 이전 줄의 텍스트로 대체하므로 셸은 sudo systemctl restart nginx을 실행합니다.

!!은 이전 줄이 무엇이었든 정확히 그 이전 줄을 의미합니다. 여기서 흔한 실수가 발생합니다. 마지막 명령이 실패한 명령이라고 생각하지만, 그 사이에 cdhistory을 실행했거나 실패한 명령이 두 줄 전일 수 있습니다. 이 경우 sudo !!는 루트 권한으로 잘못된 명령을 실행하게 됩니다. 확실하지 않을 때는 먼저 출력해 보십시오:

sudo !!:p

:p는 확장된 줄을 출력만 하고 실행하지는 않습니다. 출력된 줄은 히스토리에 추가되므로, 내용이 올바르다면 다음 줄에서 !!을 입력하여 실행하면 됩니다.

!$를 사용하여 마지막 인자 재사용하기

!$은 이전 줄의 마지막 단어입니다. 마지막 단어는 보통 경로명이나 서비스명처럼 작업 중인 대상인 경우가 많으므로 가장 자주 사용하게 될 형태입니다.

sudo systemctl status nginx
sudo systemctl reload !$

이 기능에서 사람들이 놀라는 점은 두 가지입니다.

첫째, !$은 사용자가 의도한 마지막 인자가 아니라 마지막 단어를 가져옵니다. 이전 줄이 리다이렉션으로 끝났다면 마지막 단어는 리다이렉션 대상이 됩니다.

sudo nginx -T > /tmp/nginx-dump.conf
less !$

위의 경우는 잘 작동합니다. 하지만 journalctl -u nginx > /tmp/log.txt 뒤에 !$을 입력하면 /tmp/log.txt이 되며 nginx가 되지 않습니다. 화면에 출력된 줄을 확인하십시오.

둘째, 이 확장은 텍스트 기반이므로 변수는 확장되지 않은 상태로 돌아옵니다. ls $HOME/backups 이후에 !$를 입력하면 $HOME/backups라는 문자가 나타나며, bash는 이를 다시 일반 매개변수로 확장합니다. 이 순서를 기억하십시오. 히스토리 확장은 매개변수 확장 및 $( )를 사용한 명령 치환보다 먼저 실행되므로, 값이 아닌 텍스트만 인식합니다.

관련된 다른 형태들도 알아두면 유용합니다. !^은 첫 번째 인자, !:2은 두 번째 인자, !:2-4은 인자 범위를 의미합니다. !!:$!$을 길게 작성하는 방식입니다.

명령을 실행하기 전에 텍스트를 먼저 확인하고 싶다면 Alt-.(또는 Esc를 누른 뒤 .)을 누르십시오. Readline이 이전 명령의 마지막 인자를 프롬프트에 즉시 삽입하며, 여기서 내용을 수정할 수 있습니다. 다시 누르면 그 이전 명령의 마지막 인자로 이동합니다. Enter를 누르기 전까지는 아무것도 실행되지 않습니다.

!*를 사용하여 모든 인자를 전달하기

!*는 첫 번째 단어를 제외한 이전 줄의 모든 단어를 의미합니다.

stat /srv/www/app/shared/config.env /srv/www/app/shared/secrets.env
sudo chmod 600 !*

"첫 번째 단어를 제외한"이라는 문구는 실제 작업을 수행합니다. !*는 0번째 단어만 삭제하므로 옵션이 경로와 함께 전달됩니다. ls -l file1 file2 이후에 !*-l file1 file2이 되며, 따라서 chmod-l을 인자로 받기 때문에 sudo chmod 600 !*는 실패합니다. 단어에 0부터 번호를 매기고 슬라이스를 사용하십시오. !!:2*는 2번째 단어부터 끝까지를 의미합니다.

sudo에서도 동일한 함정이 발생합니다. sudo chown deploy:deploy /srv/www/app 이후에 0번째 단어는 sudo이고 1번째 단어는 chown이므로, !*은 다음 명령에 chown deploy:deploy /srv/www/app을 전달합니다. 이는 의도한 결과가 아닐 가능성이 큽니다.

!n 및 !-n을 사용하여 위치로 명령 선택하기

history는 모든 항목 앞에 번호를 붙여 목록을 출력합니다.

history 5
  512  sudo nginx -t
  513  sudo systemctl reload nginx
  514  ss -tulpn
  515  sudo tail -f /var/log/nginx/error.log
  516  history 5

!513은 513번 항목을 다시 실행합니다. !-2은 현재 입력 중인 줄에서 두 줄 이전의 항목을 실행하며, 따라서 !-1!!은 같은 의미입니다.

번호를 사용하기 전에 반드시 확인하십시오. !-2는 명령을 실행할 때마다 새로운 위치를 가리키며, 여기에는 무심코 실행한 ls도 포함됩니다. 절대 번호는 한 세션 내에서는 고정되어 있지만, 같은 서버의 다른 세션에서는 번호가 다를 수 있으며, 다음 로그인 시 히스토리 파일을 다시 읽어 들인 후에도 번호가 달라집니다. 어제 기억해 둔 번호는 오늘 다른 명령을 가리키게 됩니다.

문자열 접두사로 명령 다시 실행하기

!stringstring으로 시작하는 가장 최근 명령을 실행합니다.

!ss

위 목록에서 ss -tulpn을 다시 실행하며, 이는 Linux 서버에서 열린 포트 확인하기에서 설명한 리스닝 소켓 확인 명령입니다. !?string?는 명령의 시작 부분이 아닌 줄 내 어디든 일치하는 항목을 찾으므로, 명령 이름은 기억나지 않지만 인자는 기억날 때 유용합니다.

접두사는 길게 유지하십시오. !s은 가장 최근에 실행된 ss, sudo, systemctl 또는 shutdown 중 하나와 일치하게 되며, 실행되기 전까지는 무엇이 실행될지 알 수 없습니다. !string:p는 명령을 실행하지 않고 일치하는 내용을 출력만 합니다. 일치하는 항목이 없으면 bash는 bash: !ss: event not found을 출력하고 아무것도 실행하지 않으므로 안전합니다.

^old^new를 사용하여 오타 하나 수정하기

sudo systemctl status ngnix
Unit ngnix.service could not be found.
^ngnix^nginx

셸은 이전 명령줄을 다시 실행하면서 첫 번째 ngnixnginx로 교체합니다. 첫 번째 일치 항목만 변경됩니다. 모든 일치 항목을 변경하려면 긴 형식인 !!:gs/ngnix/nginx/를 사용하십시오. 여기서 s은 치환을 의미하며 g은 줄 전체에 걸쳐 수행하라는 의미입니다.

실행 전 확장 결과 확인하기

확장 기능이 의도치 않은 명령을 실행하는 사고를 방지하는 두 가지 습관이 있습니다.

첫 번째는 이미 살펴본 :p입니다. 확장 명령 뒤에 이를 추가하면 bash는 명령을 실행하는 대신 결과를 출력합니다. 예: !!:p 또는 !systemctl:p. 출력된 줄은 히스토리에 저장되므로, 이후 !!를 입력하면 방금 확인한 명령이 실행됩니다.

두 번째 방법은 더 강력합니다. 별도의 기억 없이도 모든 확장에 적용되기 때문입니다.

shopt -s histverify

이 줄을 ~/.bashrc에 추가하십시오. histverify이 설정되면 Enter 키를 눌러도 확장이 즉시 실행되지 않습니다. Bash는 확장된 줄을 프롬프트에 다시 작성하므로 내용을 읽고 편집할 수 있으며, 두 번째로 Enter를 누르면 명령이 실행됩니다. 키 입력이 한 번 더 필요하지만, 이로써 모든 종류의 사고를 예방할 수 있습니다. 이 기능은 readline을 사용하므로 대화형 프롬프트에서만 작동합니다.

Ctrl-R은 또 다른 작업 방식이며, 어떤 확장도 수행하지 않습니다. Ctrl-R을 누르고 명령의 일부를 입력하십시오. 프롬프트가 reverse-i-search 프롬프트로 바뀌며 입력하는 동안 가장 최근에 일치하는 명령을 보여줍니다. Ctrl-R을 다시 누르면 더 오래된 기록으로 이동합니다. Enter를 누르면 표시된 줄이 실행됩니다. Ctrl-G를 누르면 검색이 취소되고 원래 프롬프트로 돌아갑니다. 왼쪽 화살표 키를 누르면 검색이 종료되고 일치하는 줄이 편집 가능한 상태로 프롬프트에 남습니다.

명령을 먼저 확인하고 싶을 때는 Ctrl-R을 사용하십시오. 명령이 바로 이전 줄에 있어 화면에서 읽을 수 있는 상태라면 !!!$을 사용하십시오.

왜 echo "done!"은 event not found 오류를 발생시키는가

echo "deploy done!"
bash: !": event not found

히스토리 확장은 따옴표 해석보다 먼저 수행되며, 큰따옴표는 ! 문자를 보호하지 못합니다. 작은따옴표나 백슬래시만 이를 보호할 수 있습니다. 따라서 echo 'deploy done!'는 예상한 대로 출력됩니다. 백슬래시도 확장을 막을 수 있지만, 큰따옴표 안에서는 bash가 백슬래시를 결과물에 그대로 남기기 때문에 작은따옴표를 사용하는 것이 깔끔한 해결책입니다.

이 문제는 비밀번호를 다룰 때 가장 치명적입니다. 강력한 비밀번호에는 종종 !이 포함되기 때문입니다. mysql -u app -p"S3cret!pass"와 같은 명령은 event-not-found 오류로 실패하거나, 운 좋게 히스토리 항목과 일치하는 경우 의도치 않은 문자열을 조용히 전달하게 됩니다. 작은따옴표를 사용하거나, 더 나아가 도구가 직접 입력을 요청하게 하여 비밀번호가 명령줄에 노출되지 않도록 하십시오. 이러한 습관은 계정 보안 관리의 기본입니다. 키와 비밀번호를 새로 생성한 서버에서 어떻게 다루어야 하는지는 새로운 VPS에서의 첫 10분을 참조하십시오.

! 문자가 포함된 텍스트 블록을 붙여넣어야 한다면, set +H을 사용하여 해당 세션의 확장을 끄고, 작업 후 set -H로 다시 켜십시오.

HISTSIZE, HISTFILESIZE 및 히스토리 저장 위치

두 변수의 이름이 비슷하여 혼동하는 경우가 많습니다.

  • HISTSIZE은 실행 중인 셸이 메모리에 유지할 명령어의 개수입니다.
  • HISTFILESIZE는 디스크 파일에 유지할 줄 수이며, HISTFILE이 별도로 지정되지 않는 한 ~/.bash_history이 적용됩니다.

셸이 종료될 때 파일이 기록되며, 이때 HISTFILESIZE 줄로 잘립니다(truncate). Ubuntu의 기본값인 ~/.bashrc은 각각 1000과 2000으로 설정되어 있습니다. 현재 설정값을 확인하려면 다음을 실행하십시오.

grep HIST ~/.bashrc
echo "$HISTSIZE $HISTFILESIZE $HISTFILE"

!string 검색에서 실행했던 명령어가 계속 누락된다면 두 값을 모두 높이십시오. 10000과 20000 정도의 값은 일반 텍스트 파일로 저장되므로 시스템 자원을 거의 소모하지 않습니다. 음수 값을 설정하면 제한이 완전히 사라집니다.

일주일 만에 서버에 다시 접속했을 때 타임스탬프가 유용하게 쓰입니다.

export HISTTIMEFORMAT='%F %T '

history를 설정하면 각 항목 앞에 날짜와 시간이 표시됩니다. bash가 파일 내의 각 명령어 앞에 에포크 시간(epoch seconds)을 담은 주석 줄을 기록하기 때문입니다.

여러 터미널을 사용할 때 히스토리가 사라진다는 불만이 흔히 제기됩니다. 각 셸은 메모리에 고유한 목록을 유지하다가 종료 시점에 파일에 기록하는데, histappend 설정이 없으면 마지막에 종료된 셸이 다른 셸이 저장한 내용을 덮어쓰기 때문입니다. 다음 두 설정으로 이 문제를 해결할 수 있습니다.

shopt -s histappend
export PROMPT_COMMAND='history -a'

histappend은 셸이 파일을 교체하는 대신 파일 끝에 목록을 추가하도록 만듭니다. history -a은 프롬프트가 뜰 때마다 새로운 줄을 추가하므로, 정상적으로 종료되지 않고 강제 종료된 세션의 명령어라도 파일에 남게 됩니다. Ubuntu의 기본값인 ~/.bashrc는 이미 histappend을 설정하고 있습니다. 히스토리는 사용자별, 호스트별로 저장되므로 관리하는 서버가 많을수록 이 설정이 중요해집니다. 한 워크스테이션에서 여러 서버를 관리할 때 찾으려는 !$이 다른 호스트에 있을 수 있기 때문입니다.

bash 기록에서 비밀 정보 제외하기

HISTCONTROL는 어떤 줄을 기록할지 결정합니다.

  • ignorespace은 공백으로 시작하는 모든 줄을 기록하지 않습니다.
  • ignoredups는 바로 이전 줄과 동일한 줄을 기록하지 않습니다.
  • ignoreboth는 위 두 가지 기능을 모두 수행합니다.
  • erasedups은 새로 저장하려는 줄과 동일한 이전 기록을 모두 삭제합니다.
export HISTCONTROL=ignoreboth

ignorespace이 활성화된 상태에서 명령 앞에 공백을 하나 추가하면 해당 명령은 메모리 목록에 저장되지 않으며, 결과적으로 파일에도 기록되지 않습니다. 이 기능을 사용하기 전에 echo "$HISTCONTROL" 명령으로 현재 설정값을 확인하십시오. Ubuntu의 기본 사용자 .bashrcignoreboth을 설정하지만, root 셸이나 최소화된 이미지에서는 이 설정이 되어 있지 않을 수 있습니다. 이 경우 공백을 추가해도 명령이 그대로 기록됩니다.

이 기능이 무엇을 보호하는지 정확히 이해해야 합니다. 이 방식은 ~/.bash_history에 기록되는 것을 막을 뿐입니다. 프로세스가 실행되는 동안에는 ps 출력에 명령어가 그대로 노출되므로, 시스템의 다른 사용자가 이를 확인할 수 있습니다. 또한 sudo은 실행된 명령을 시스템 로그에 기록합니다. 기록 파일은 일반 텍스트 형식이므로 파일 권한을 600으로 유지해야 하며, 홈 디렉터리에 접근할 수 있는 사용자는 누구나 최근 수천 개의 명령어를 읽을 수 있다는 점을 명심하십시오.

이미 민감한 정보가 기록되어 있다면 해당 항목을 삭제하고 파일을 다시 작성하십시오:

history
history -d 517
history -w

history -d는 메모리에서 해당 항목을 제거하며, history -w는 현재 메모리 목록을 파일에 덮어씁니다. history -c은 전체 목록을 삭제합니다. HISTIGNORE은 비밀 정보가 아닌 불필요한 기록을 관리하는 설정입니다. HISTIGNORE='ls:pwd:history:clear'을 사용하면 특정 줄을 목록에서 제외하여 검색 결과의 정확도를 높일 수 있습니다.

셸 스크립트에서 히스토리 확장이 작동하지 않는 이유

히스토리 확장은 대화형 셸에 속하는 기능입니다. 스크립트는 비대화형 셸에서 실행되며, 이 환경에서는 히스토리 목록이 활성화되지 않고 확장 기능도 꺼져 있습니다. 따라서 !!!$은 일반 텍스트로 그대로 남게 됩니다. 스크립트 내부의 sudo !!은 sudo에게 !!라는 이름의 명령어를 그대로 실행하라고 요청하는 꼴이 되며, 결과적으로 실패합니다.

현재 사용 중인 셸의 설정을 확인하십시오:

echo $-

출력 결과는 현재 설정된 옵션 플래그들의 집합이며, himBHs과 같은 형태입니다. i는 셸이 대화형임을 의미하고, H는 히스토리 확장이 활성화되어 있음을 의미합니다. 스크립트 내부에서 동일한 명령어를 실행하면 두 문자 모두 나타나지 않습니다.

이것이 셸 작업의 두 영역을 구분하는 경계입니다. 프롬프트에서는 !$과 Ctrl-R을 사용하여 눈에 보이는 명령어의 키 입력을 줄일 수 있습니다. 파일 내에서는 대신 이름을 지정해야 합니다. 경로를 변수에 넣거나 명령어 치환을 사용하여 출력을 캡처하십시오. 개인의 히스토리에 의존하는 스크립트는 다음 사용자가 실행할 때마다 다른 결과를 내놓게 되는데, 이는 스크립트의 본래 목적과 정반대되는 상황입니다.

이러한 이유로, 이 페이지의 모든 예제는 실제 프롬프트에 직접 입력하는 것을 전제로 합니다. 이 예제들은 .sh 파일에 붙여넣었을 때 동일하게 동작하지 않습니다.

FAQ

bash에서 !!는 어떤 역할을 합니까?

!!은 이전 명령줄 전체 텍스트로 확장됩니다. 따라서 sudo !!를 입력하면 마지막 명령을 root 권한으로 다시 실행합니다. 이 확장은 텍스트 기반으로 bash가 명령줄을 해석하기 전에 수행되며, bash는 실행 직전에 완성된 명령줄을 출력합니다. 이전 명령이 무엇인지 확실하지 않다면 먼저 sudo !!:p을 입력하십시오. :p은 명령을 실행하지 않고 확장된 결과만 출력하며, 해당 줄을 히스토리에 추가하므로 이어서 !!를 입력하면 실행할 수 있습니다.

이전 명령의 마지막 인자를 재사용하려면 어떻게 합니까?

!$을 사용하십시오. sudo mkdir -p /srv/www/app/releases를 입력한 뒤 ls -ld !$ls -ld /srv/www/app/releases으로 바뀝니다. 이는 명령줄의 마지막 단어를 가져오므로, 끝에 위치한 리다이렉션 대상도 마지막 단어로 간주됩니다. 여기에 텍스트를 덧붙일 수도 있습니다. !$/config.env은 경로 뒤에 내용을 추가합니다. 단어 지정자가 /에서 멈추기 때문입니다. 대화형 방식으로는 Alt-.를 사용할 수 있으며, 이는 프롬프트에 동일한 텍스트를 삽입하여 Enter를 누르기 전에 확인할 수 있게 해줍니다.

텍스트에 느낌표가 포함되어 있는데 왜 bash는 "event not found"라고 출력합니까?

큰따옴표는 !이 히스토리 확장을 수행하는 것을 막지 못합니다. 따라서 echo "done!"을 입력하면 bash는 히스토리 이벤트를 찾으려 시도하고 bash: !": event not found를 출력합니다. 작은따옴표는 이를 방지하므로 echo 'done!'과 같이 작성하십시오. 백슬래시를 사용해도 확장을 멈출 수 있지만, 큰따옴표 안에서는 백슬래시가 출력 결과에 그대로 남습니다. !가 포함된 긴 블록을 붙여넣으려면 set +H를 사용하여 해당 세션의 확장을 끄십시오.

왜 셸 스크립트에서는 !!와 !$가 작동하지 않습니까?

히스토리 확장은 대화형 셸에서만 활성화됩니다. 스크립트는 비대화형으로 실행되므로 셸이 히스토리 목록을 생성하지 않으며, !!은 일반 텍스트로 남게 됩니다. echo $-을 실행하여 현재 셸 상태를 확인하십시오. 대화형 셸은 iH를 포함한 플래그를 출력하지만, 스크립트는 아무것도 출력하지 않습니다. 스크립트에서는 변수나 명령 치환을 대신 사용하십시오.

bash 히스토리에 비밀번호가 남지 않게 하려면 어떻게 합니까?

~/.bashrcHISTCONTROL=ignorespace 또는 HISTCONTROL=ignoreboth을 설정한 뒤, 명령 앞에 공백을 하나 추가하여 시작하면 히스토리에 저장되지 않습니다. 설정값이 비어 있으면 앞의 공백이 아무런 역할을 하지 못하므로, 먼저 echo "$HISTCONTROL"으로 값을 확인하십시오. 이 방법은 ~/.bash_history에 줄이 기록되는 것만 방지합니다. 명령이 실행되는 동안 ps에는 여전히 보이며, sudo은 실행된 내용을 기록합니다. 만약 비밀 정보가 이미 저장되었다면 history로 번호를 찾은 뒤, history -d <number>을 실행하고 history -w를 사용하여 파일을 다시 작성하십시오.