검색 엔진의 방문이 늘어나고 있군...

Posted
Filed under 쉘 스크립트
참조 원문 : Unix tip: Using Bash's regular expressions

  버전 3(2004년)부터 bash는 =~ 로 표기하는 내장형 정규 표현식 비교 연산자를 갖추었습니다. 그래서 사실 스크립트에서 grep이나 sed로 하는 작업의 상당수는 이 연산자로 처리할 수 있습니다.

  다른 비교 연산자(-lt, == 등)와 마찬가지로 비교 결과가 참이면 0을 리턴합니다. 아래 예제는 $digit의 값이 하나의 숫자인지를 판별하는 것을 보여주고 있습니다.
if [[ $digit =~ [0-9] ]]; then
    echo '$digit is a digit'
else
    echo "oops"
fi

  아래는 프롬프트에 입력한 값이 숫자만으로 구성되어 있는지를 판별하는 예제입니다.
echo -n "Your answer> "
read REPLY
if [[ $REPLY =~ ^[0-9]+$ ]]; then
    echo Numeric
else
    echo Non-numeric
fi

  Bash의 정규 표현식도 상당히 복잡하게 응용해서 사용할 수 있습니다. $email 변수의 값이 이메일 주소 형식인지를 판별하는 예제입니다. 첫 번째 표현식(계정명)은 문자, 숫자, 일부 특수 문자를 받습니다. 첫 번째 ]의 오른쪽에 있는 +는 앞의 표현식에 해당하는 문자를 몇 개든지 받는다는 뜻입니다. 그 뒤에는 계정명과 이메일 도메인 사이에 @ 문자가 있는지 확인합니다. 그리고 도메인 명의 앞부분과 뒷부분 사이에 점(\.)이 있는지 확인합니다.
if [[ "$email" =~ "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$" ]]
then
    echo "This email address looks fine: $email"
else
    echo "This email address is flawed: $email"
fi

  아래는 변수의 값이 IP 주소 형태인지를 판별하는 예제입니다.
#!/bin/bash

if [ $# != 1 ]; then
    echo "Usage: $0 address"
    exit 1
else
    ip=$1
fi

if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
    echo "Looks like an IPv4 IP address"
elif [[ $ip =~ ^[A-Fa-f0-9:]+$ ]]; then
    echo "Could be an IPv6 IP address"
else
    echo "oops"
fi

  또한 bash는 간소화된 루프문을 제공합니다. 100번 루프하고 싶다면 아래의 문법으로 해결할 수 있습니다.
for n in {1..100}
do
    echo $n
done

  아래와 같은 표현식으로 문자나 숫자를 범위로 지정하여 루프를 처리할 수도 있습니다. 꼭 1이나 a부터 시작할 필요는 없으며, 역순으로 처리하는 것도 가능합니다.
{a..z}
{z..a}
{c..f}
{5..25}
{10..-10}

  쉘에서 아래처럼 간단히 확인해볼 수도 있습니다.
$ echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z
$ echo {5..-1}
5 4 3 2 1 0 -1
2013/07/15 12:42 2013/07/15 12:42
Posted
Filed under 쉘 스크립트
참조 원문 : Ten things I wish I knew earlier about the Linux command line

1. Ctrl+U와 Ctrl+Y(bash 한정)
  긴 명령어를 치던 도중 다른 명령어를 먼저 실행해야 하는 상황을 겪어본 적이 있는가? 이럴 때 좋은 방법이 있다. 커서가 현재 명령줄의 끝에 오게 한 후(단축키: Ctrl+E) Ctrl+U를 누르면 줄이 비워진다. 먼저 실행해야 할 다른 명령어를 실행한 후 Ctrl+Y를 누르면 아까 쳤던 명령어가 그대로 붙여진다.

2. screen 명령어
  하나의 터미널 세션 속에서 다수의 터미널 세션을 실행하는 명령어다. 세션은 단축키로 관리한다. 기본으로 설치되지 않을 수 있으니 필요하다면 설치해야 한다. 실행하면 도움말 같은 것이 나온 후 빈 터미널 창이 나온다. 여기서 sleep 9999를 실행해서 터미널을 일시적으로 사용하지 못하는 상태로 만들자. 그리고 나서 Ctrl+A를 누른 후 c를 누르면 새로운 창이 열린다. sleep 명령어를 실행하던 터미널은 계속해서 작동하고 있다. 두 창 사이를 옮겨 다니는 단축키는 Ctrl+A n(Next)와 Ctrl+A p(Previous)다. 명령어를 실행 중인 상태에서 실행에 중단 없이 screen을 종료하려면 Ctrl+A d(Detach)를 누른다. 이렇게 종료했던 screen 터미널로 돌아가려면 screen -R을 실행한다.

3. xargs 명령어
  다수의 파일이나 어떤 파일의 여러 줄을 참고하여 명령어를 실행할 때 xargs는 큰 도움이 된다. 원래 시스템이 처리할 수 있는 커맨드 라인 인자의 양과 크기에는 한계가 있지만 xargs를 사용하면 모든 인자를 다 처리하면서도 명령어 사용 횟수는 최소화한다. 아래는 사용 예다.
find . -iname '*.php' -print0 | xargs -0 svn add
  svn 같은 버전 제어 시스템을 다뤄 본 사람이라면 코드 파일을 새로 만들 때마다 귀찮게 svn add를 해줘야 한다는 것을 알것이다. 위 명령어는 이 일을 한 번에 해결한다. 원리는 아래와 같다.
  1. "find . -iname '*.php' -print0"로 현재 디렉토리(".") 이하에 있으면서 파일명이 .php로 끝나는(대소문자 미구분) 파일명을 출력하고 이때 각 출력 사이를 널 문자("-print0")로 구분한다. 널 문자는 파일명에 절대 사용되지 않지만 줄 바꿈 문자는 사용될 수 있기 때문에 널 문자로 나누는 것이 더 안전하다.
  2. "xargs -0 svn add"는 파이프("|")를 통해 표준 입력으로 앞선 명령어의 출력을 받아 널 문자("-0")로 나눠진 각 인자를 인식하고 svn의 "add" 인자 뒤에 파일명을 인자로 넣는다.
  find에서 -exec 옵션을 쓰는 것과 비교했을 때 xargs를 사용하는 것이 더 안전하고 활용성도 높다.

4. bash를 간단한 계산기로 사용
echo $((3*37+12)) # 출력 123
echo $((2**16-1)) # 2의 16제곱 빼기 1; 출력 65535
echo $((103/10)) # 출력 10, 모든 연산은 정수로 처리한다.
echo $((103%10)) # 출력 3, 103을 10으로 나눴을 때의 나머지
  비트 연산도 지원하며 캐럿("^")으로 할 수 있는데 그래서 일반적인 계산기 프로그램과 달리 "^"가 아닌 "**"가 제곱을 위한 문자로 쓰인다. 아래처럼 16진수나 8진수를 10진수로 변환할 수도 있다.
echo $((0xdeadbeef)) # 출력 3735928559
echo $((0127)) # 출력 87
  더 자세한 것은 man bash에서 "arithmetic evaluation"을 검색하면 볼 수 있다. 계산에 소수점을 사용하고 싶다면 아래처럼 bc를 사용하는 것이 좋다.
echo 'scale=12; 2.5*2.5' | bc # 출력 6.25
echo 'scale=12; sqrt(14)' | bc # 출력 3.741657386773
  bc를 사용할 때 당신이 원하는 정확도로 소수점 연산을 할 수 있다. scale 변수는 소수점 이하의 자릿수를 제어한다.

5. 작은 따옴표
  작은 따옴표는 그 안에 있는 문자를 쉘이 해석하지 않고 그대로 명령어에 넘기는 역할을 한다.
find . -iname '*.conf'
  *.conf를 둘러싸고 있는 작은 따옴표를 보자. 만약 이걸 빼면 bash는 *.conf를 glob expression으로 해석하여 현재 디렉토리에서 파일명이 .conf로 끝나는 모든 파일로 대체하기 때문에 서브 디렉토리는 검색하지 않는다는 결과를 초래한다. 그러므로 특수 문자를 포함한 것에는 항상 작은 따옴표를 붙이는 것이 좋다.

6. For 루프문
  xargs를 사용하면 다수의 파일에 한 명령어에 실행할 수 있다. 하지만 경우에 따라 그보다 더 강력한 기능이 필요할 때가 있다. 예를 들어 아래를 보자.
# 현재 디렉토리에 있는 각 php 파일에 while 루프문이 몇 개 있는지 조사
for file in ./*.php; do echo -n "$file":\ ; grep 'while' "$file" | wc -l; done
  1. "for file in ./*.php": 현재 디렉토리에서 파일명이 ".php"로 끝나는 모든 파일의 목록을 만들고 각 파일마다 그 이름을 "$file"이라는 변수에 넣고 아래의 코드들을 실행한다.
  2. "do ... done": 루프문 안에 있는 코드의 시작과 끝을 나타낸다. 원래는 둘 다 줄의 시작에 있어야 하며 그것이 각각의 앞에 세미콜론이 있는 이유다.
  3. "echo -n "$file":\ ": 파일명, 콜론, 공백 문자를 출력하며 줄을 바꾸지 않는다.("-n" 옵션을 사용하면 줄을 바꾸지 않는다.) 백슬래쉬는 공백 문자 1개를 bash가 먹지 않고 그대로 출력하기 위해 사용했다.
  4. "grep 'while' "$file" | wc -l": 파일을 읽어 "while"이라는 문자열이 있는 줄만 파이프를 통해 "wc -l" 명령어로 보내 총 몇 줄인지 센다.

7. 문자열 조작
  파일명이 DSC로 시작하는 다수의 사진 파일이 있다고 가정하자. 이 DSC를 Vacation2011로 바꾸는 방법은 아래와 같다.
for file in DSC*; do mv "$file" Vacation2011"${file#DSC}"; done
  여기서 중요한 부분은 "${file#DSC}"이다. #은 그 뒤에 있는 문자열을 그 앞에 있는 변수의 내용의 앞부분에서 지워야 한다는 것을 뜻한다. 문자열의 뒤에서 지우는 연산자도 있다.(파일 확장자를 없앨 때 유용하다.) 문자열을 찾아 치환하는 것에 대한 자세한 것은 이곳을 참조하자.

8. 프로세스 대체(Process Substitution)
  두 명령어의 출력 결과에서 차이점을 빠르게 찾고 싶은가? 물론 아래처럼 두 결과를 임시 파일로 리다이렉트해서 비교하는 방법도 있다.
find /etc | sort > local_etc_files
find /mnt/remote/etc | sort > remote_etc_files
diff local_etc_files remote_etc_files
rm local_etc_files remote_etc_files
  위 작업을 통해 로컬 컴퓨터와 원격 컴퓨터의 /etc 디렉토리가 각각 어떤 파일을 가지고 있는지 비교할 수 있다. 하지만 4줄이나 소모했다. 프로세스 대체를 사용하면 한 줄로 할 수 있다.
diff <(find /etc | sort) <(find /mnt/remote/etc | sort)
  <(...) 문법은 "안에 있는 명령어를 실행하고 출력을 임시 파이프 파일에 연결시킨 후 그것을 인자로서 사용하라"는 뜻을 담고 있다. 좀 더 완벽한 이해를 위해 아래를 실행해보자.
echo <(echo test)
  "test"라는 문자열을 출력하는 대신 "/dev/fd/63" 같은 문자열을 출력할 것이다. 이것을 통해 <(...) 부분이 파일로 교체된다는 것을 알 수 있다. 이 파일은 <(...) 안에 있는 명령어의 출력으로 인한 스트림으로서 아래처럼 읽는 것이 가능하다.
cat <(echo test)
  Bash는 "echo test"의 출력을 /dev/fd/<파일디스크립션번호>로 리다이렉트하고 그 파일의 경로명을 cat에게 넘기며, cat은 그 파일로부터 echo의 출력을 읽는다. 이 기술은 임시 파일이 필요할 때 쓸 수 있지만 제한사항이 있다. 이 임시 파일은 사라지기 전에 한 번만 읽을 수 있다. 임시 파일의 이름을 보관했다가 나중에 사용하는 것은 불가능하다는 것이다. 프로그램의 출력을 여러 번 참조해야 한다면 그냥 임시 파일을 만들거나 파이프를 써야 한다.


2013/07/04 11:29 2013/07/04 11:29
Posted
Filed under 쉘 스크립트
참조 원문 : Log Parser
#!/bin/bash

script=$(basename $0)_errors
log1=/var/log/messages
log2=/var/log/secure
log3=/var/log/dmesg

mydate=$(date +%b\ %d)

for log in $log{1,2,3}
do
if [ -e $log ] && [ -s $log ]
then
echo
echo BEGIN $log
grep -E "$mydate" $log | grep -E 'Device|fail'  2> $script
echo END $log
echo
fi
done
  필요한대로 수정한 후 cron으로 매일 23시 55분에 실행하면 적당할 것 같군요. 변수로 지정한 로그 파일들에서 당일 생성된 항목 중 지정한 키워드(위의 경우 Device나 fail)가 검출되면 해당 라인을 미리 정한 파일에 집어넣습니다. 간단해서 설명할 것도 없군요. 결과는 아래처럼 나오겠지요.
BEGIN /var/log/messages
Aug 31 01:21:43 mail kernel: pnp: Device 00:05 does not support disabling.
Aug 31 01:21:53 mail smartd[2046]: Device: /dev/hda, opened
Aug 31 01:21:53 mail smartd[2046]: Device: /dev/hda, not found in smartd database.
Aug 31 01:21:53 mail smartd[2046]: Device: /dev/hda, lacks SMART capability
Aug 31 01:21:53 mail smartd[2046]: Device: /dev/hda, to proceed anyway, use '-T permissive' Directive.
Aug 31 01:21:53 mail smartd[2046]: Device: /dev/hdc, opened
Aug 31 01:21:53 mail smartd[2046]: Device: /dev/hdc, packet devices [this device CD/DVD] not SMART capable
END /var/log/messages

BEGIN /var/log/secure
Aug 31 01:21:48 mail sshd[1872]: error: Bind to port 22 on 0.0.0.0 failed: Address already in use.
END /var/log/secure

BEGIN /var/log/dmesg
END /var/log/dmesg
  근데 많이 부실하긴 하네요. 나중에 좀 더 견고한 걸 올려야겠습니다.


2013/06/20 17:33 2013/06/20 17:33
Posted
Filed under 쉘 스크립트
참조 원문 : 8 More Bash Tips for Working Faster With the Shell

  날이면 날마다 오는 배시 관련 포스팅입니다.

앨리어스 등록
  일반적으로 앨리어스를 등록할 때는 ~./bashrc 파일에 아래와 같은 형식으로 원하는 내용을 집어넣습니다.
alias name='command'
  그냥 이렇게 평범하게 등록하고 쓰는 방법도 있지만 앨리어스 전용 파일을 별도로 만들고 그 파일을 읽는다면 관리가 더 깔끔하겠죠? 이를 위해 .bash_aliases 파일을 만들어 앨리어스들을 넣은 후 ~./bashrc 파일에서 아래와 같이 파일을 읽어들입니다.
if [ -f ~/.bash_aliases ]; then
    . ~/.bash_aliases
fi

히스토리를 탐색할 때 페이지 업/다운 키 사용
  ~/.inputrc라는 파일을 만들고 아래의 두 줄을 집어넣으면 PageUp 키와 PageDown 키로 히스토리를 탐색할 수 있습니다. 파일을 만든 후 다시 로그인을 해야 적용됩니다.
"\e[5~": history-search-backward
"\e[6~": history-search-forward

이맥스 스타일의 단축키들
  화살표까지 손을 옮기기 귀찮을 때 사용하면 편합니다.
  • ^A – go to the start of line
  • ^E – go to the end of line
  • ^H – erase one character to the left
  • ^D – erase one character to the right, it also exits the shell by default if there is no character to delete
  • ^U – erase everything from the cursor to start
  • ^K – erase everything from the cursor to end
  • ^P – bring the previous command in history
  • ^N – bring the next command in history
  • ^C – interrupt character, sends SIGTERM to the current application

매뉴얼 페이지에 색깔 적용
  아래의 내용을 ~./bashrc 파일에 넣습니다.
export LESS_TERMCAP_mb=$'\E[01;31m' # begin blinking
export LESS_TERMCAP_md=$'\E[01;38;5;74m' # begin bold
export LESS_TERMCAP_me=$'\E[0m' # end mode
export LESS_TERMCAP_se=$'\E[0m' # end standout-mode
export LESS_TERMCAP_so=$'\E[38;5;246m' # begin standout-mode - info box
export LESS_TERMCAP_ue=$'\E[0m' # end underline
export LESS_TERMCAP_us=$'\E[04;38;5;146m' # begin underline
  그리고 .bashrc 파일을 다시 읽어들입니다.
. ~/.bashrc


파일 이름의 뒷부분에 원하는 문자열 추가
  만약 'filename'이란 이름의 파일이 있을 때 이 파일을 백업 파일로 만들 때는 'filename.backup' 같은 식으로 이름을 바꾸기 마련입니다. 이때 보통 아래와 같은 방법으로 이름을 바꿉니다.
mv filename filename.backup
  이때 아래의 형식으로 명령을 내리면 같은 효과를 볼 수 있습니다.
mv filename{,.backup}
2010/12/29 13:44 2010/12/29 13:44
Posted
Filed under 쉘 스크립트
참조 원문 : HowTo: Use Bash Parameter Substitution Like A Pro

  달러($) 문자는 파라미터 확장과 명령어 대입에 사용됩니다. 이걸 이용하면 sed나 awk 같은 외부 명령어 없이 필요에 따라 변수를 조작하거나 확장할 수 있습니다.


more..


more..


more..


more..


more..


more..


more..



요약: 문자열 조작과 확장 변수
  • ${parameter:-defaultValue} : 디폴트 쉘 변수 값을 사용
  • ${parameter:=defaultValue} : 디폴트 쉘 변수 값을 설정
  • ${parameter:?"Error Message"} : 파라미터가 설정되어 있지 않았을 경우 에러 메시지 출력
  • ${#var} : 문자열의 길이 계산
  • ${var%pattern} : 오른쪽에서 패턴과 일치하는 부분 제거
  • ${var%%pattern} : 패턴과 일치하는 부분들 중 가장 왼쪽 부분부터 끝까지 제거
  • ${var:num1:num2} : 문자열 추출
  • ${var#pattern} : 왼쪽에서 패턴과 일치하는 부분 제거
  • ${var##pattern} : 패턴과 일치하는 부분들 중 가장 오른쪽 부분부터 처음까지 제거
  • ${var/pattern/string} : 검색 및 치환(1개)
  • ${var//pattern/string} : 검색 및 치환(모두)
2010/12/21 09:53 2010/12/21 09:53
Posted
Filed under 프로그램과 명령어/커맨드 라인 트릭
참조 원문 : My top ten shell/bash tricks!


1. sudo를 사용하는 것을 잊었을 때 !! 활용
  루트 권한이 필요한 명령어를 사용할 때 sudo를 사용하는 것을 잊어서 처음부터 다시 타이핑을 하거나 이전 명령어를 불러 앞 부분에 sudo를 붙이는 경우가 있는데 더 간편하게 하는 방법이 있습니다. 바로 이전에 실행한 명령어로 대체되는 '!!' 문자열을 사용하는 겁니다. 배시 쉘에서 사용이 가능합니다.
$ ethtool eth0
Settings for eth0:
Cannot get device settings: Operation not permitted
Cannot get wake-on-lan settings: Operation not permitted
    Current message level: 0x00000000 (0)
Cannot get link status: Operation not permitted
$ sudo !!
sudo ethtool eth0
[sudo] password for mirashi:


2. Ctrl+R로 이전에 사용한 명령어 찾기
  역시 배시 쉘에서 사용이 가능한 기능으로 Ctrl+R을 누른 후 이전에 사용한 명령어에 포함된 문자나 문자열을 입력하여 이전에 사용한 명령어를 검색할 수 있습니다. 검색은 가장 최근 실행한 것부터 역순으로 검색되며 Ctrl+R을 누를 때마다 검색을 계속 진행합니다. 그냥 Ctrl+R만 계속 누를 경우 이전에 실행한 명령어들을 차례대로 보여줍니다. 역시 배시 쉘에서 가능합니다.

  이전에 실행한 명령어를 검색하는 또 다른 방법으로는 'history | grep 검색할_명령어'를 실행하는 방법이 있습니다.


3. 가장 많이 실행했던 명령어 10개 보기
$ history | tr '\011' ' ' | tr -s " "| cut -d' ' -f3 | sort | uniq -c | sort -nbr | head -n10
    212 sudo
     38 man
     26 ls
     24 cd
     23 echo
     15 vi
     15 awk
     13 dig
     12 lsof
      9 htpasswd


4. 한방에 시스템을 무너트리는 명령어
  아래의 명령어로 시스템에 DoS 공격을 할 수 있습니다. fork bomb이라고 하는 공격이며 프로세스 복제로 프로세스 테이블을 포화상태로 만듭니다.
:(){ :|:& };:


5. &&로 연쇄 반응 일으키기
  '&&'는 명령어를 순차적으로 실행하게 해줍니다. 만약 '명령어1 && 명령어2'이라는 형식으로 실행하면 '명령어1'이 실행되어 무사히 종료됐을 경우에만 '명령어2'가 실행됩니다. 여기서 중요한 사실은 만약 '명령어1'의 종료 값이 0이 아닐 경우(=비정상 종료했을 경우) '명령어2'가 실행되지 않는다는 겁니다. 아래는 데비안 계열의 배포판에서 이 기법을 활용한 예로 패키지 목록을 업데이트한 후 패키지 업데이트를 실행하는 명령어입니다.
sudo apt-get update && sudo apt-get upgrade


6. screen 명령어로 화면 분할
  먼저 'screen' 명령어를 실행합니다. 그리고 엔터를 눌러 쉘 화면으로 빠져나옵니다. 이제 Ctrl+a를 누른 후 S(대문자)를 누르면 화면이 분할됩니다. 이제 Ctrl+a를 누른 후 탭을 누르면 다음 창으로 넘어갈 수 있습니다. 분할 후 새로 생긴 창에는 아무것도 없는데 커서가 그런 창에 있는 상태에서 Ctrl+a를 누른 후 c(소문자)를 누르면 쉘이 실행됩니다. 현재 커서가 있는 창을 닫고 싶을 때는 Ctrl+a를 누른 후 X(대문자)를 누르면 됩니다.

2010/12/14 17:05 2010/12/14 17:05
Posted
Filed under 쉘 스크립트
참조 원문 : Single vs. Double Quotes in Bash

  배시 쉘을 쓰다보면 백 쿼트(`), 싱글 쿼트('), 더블 쿼트(")의 기능이 햇갈리기 마련입니다. 특히 싱글 쿼트와 더블 쿼트는 똑같이 문자열을 처리하기 위해 존재하지만 둘 사이에는 작은 차이가 있고 그 작은 차이도 파고들면 복잡해집니다.

  백 쿼트(`)는 그 안에 있는 명령어의 실행 결과(정확히는 표준 출력-stdout-)를 다시 입력으로 바꾸는 기능을 가지고 있습니다. 주로 변수에 어떤 명령어의 출력 결과를 집어넣을 때 사용합니다. 말로 설명하면 어렵지만 아래의 예로 쉽게 이해할 수 있으리라 생각합니다.
mirashi@myservlab:~$ echo Today is `date +%A`
Today is Tuesday

mirashi@myservlab:~$ uname
Linux
mirashi@myservlab:~$ UNAME_RET=`uname`
mirashi@myservlab:~$ echo $UNAME_RET
Linux

  이제 싱글 쿼트(')에 대해 알아보겠습니다. 두 싱글 쿼트 사이에 있는 문자열은 어떤 해석도 거치지 않고 문자 그대로 인식합니다. 아래의 예를 봅시다.
mirashi@myservlab:~$ name="Juliet Kemp"
mirashi@myservlab:~$ echo "$name"
Juliet Kemp
mirashi@myservlab:~$ echo '$name'
$name
  이렇듯 싱글 쿼트는 변환이고 해석이고 뭐고 가차 없이 문자열을 있는 그대로 받아들입니다. 참 솔직한 녀석이 아닐 수 없습니다. 그러므로 문자열에 특수 문자가 많을 때 유용합니다. 물론 같은 이유로 변수를 다루는 부분에서 쓰면 영 좋지 않겠죠?


  더블 쿼트(")는 좀 더 유연한 녀석입니다. 안에 있는 문자열에서 몇 가지 특수 문자를 재해석합니다. 재해석하는 문자로는 공식적으로 달러($), 백쿼트(`), 그리고 이스케이프(\ 로 시작) 문자가 있습니다만...디폴트 상태의 배시 쉘이라면 여기에 느낌표(!)가 추가됩니다. 여기에 대한 이야기는 조금 있다가 하도록 하겠습니다.

  더블 쿼트는 문자열 중간에 공백이 있을 때, 변수와 함께 사용할 때, 명령어의 인수로 사용할 문자열에 공백 문자가 포함되어 하나의 인수가 둘로 인식되는 것을 막을 때 유용합니다. 셋 중 앞의 2개는 바로 위에 있는 싱글 쿼트 예제의 첫 번째 줄과 두 번째 줄에서 차례대로 볼 수 있습니다. 아래는 세 번째 내용을 설명하기 위한 예제입니다.
mirashi@myservlab:~$ mkdir my directory
mirashi@myservlab:~$ ls -l
drwxr-xr-x 2 mirashi mirashi 4096 2010-10-21 19:38 directory
drwxr-xr-x 2 mirashi mirashi 4096 2010-10-21 19:38 my
mirashi@myservlab:~$ mkdir "my directory"
mirashi@myservlab:~$ ls -l
drwxr-xr-x 2 mirashi mirashi 4096 2010-10-21 19:38 directory
drwxr-xr-x 2 mirashi mirashi 4096 2010-10-21 19:38 my
drwxr-xr-x 2 mirashi mirashi 4096 2010-10-21 19:38 my directory
  전자의 경우 my와 directory라는 2개의 디렉토리가 만들어졌지만 후자는 공백 문자가 포함된 my directory라는 디렉토리가 만들어졌습니다. 그렇다면 이번에는 변수와 함께 사용할 때 어떻게 유용한지 볼까요?
mirashi@myservlab:~$ NameToRemove="my directory"
mirashi@myservlab:~$ rmdir "$NameToRemove"
mirashi@myservlab:~$ ls -l
drwxr-xr-x 2 mirashi mirashi 4096 2010-10-21 19:38 directory
drwxr-xr-x 2 mirashi mirashi 4096 2010-10-21 19:38 my
mirashi@myservlab:~$ rmdir $NameToRemove
mirashi@myservlab:~$ ls -l
mirashi@myservlab:~$
  둘의 차이가 보이십니까? 변수를 인수로 사용할 때 변수명을 더블 쿼트로 감싸면 변수의 내용 전체가 하나의 인수로 인식되는 것을 볼 수 있습니다. 변수의 내용을 변수명 대신 그대로 대입해보면 위 내용을 쉽게 이해할 수 있으리라 생각합니다. 또 다른 예로 아래와 같은 경우가 있습니다.
mirashi@myservlab:~$ ls
abcdef
mirashi@myservlab:~$ dpkg-query -l abc*
No packages found matching abcdef.
mirashi@myservlab:~$ dpkg-query -l "abc*"
No packages found matching abc*.
  사용자가 있는 디렉토리에는 abcdef라는 이름의 파일이 있습니다. 이 상태에서 abc라는 이름으로 시작하는 패키지를 검색하기 위해 dpkg-query -l abc* 라고 명령어를 실행했습니다. 그랬더니 abc*가 확장되어 abcdef로 변한 다음 dpkg-query 명령어가 실행되는 것을 볼 수 있습니다. 그래서 abc*를 더블 쿼트로 묶어 실행했더니 의도대로 명령어가 실행되는 것을 볼 수 있습니다.


  아까 더블 쿼트에서 달러($), 백쿼트(`), 그리고 이스케이프 문자 외에 느낌표(!)도 재해석되는 문자에 포함된다고 했는데 사실 엄밀히 따지자면 더블 쿼트와는 관계가 없습니다. 배시 쉘에서 느낌표는 '히스토리 익스팬션(History Expansion)'이라는 것의 디폴트 문자로 쓰이는데 이것은 배시 쉘의 히스토리 기능을 이용한 일종의 명령어 단축 실행 기능이라고 할 수 있습니다. 대충 아래와 같은 기능입니다.
mirashi@myservlab:~$ echo "history expansion"
history expansion
mirashi@myservlab:~$ !e
echo "history expansion"
history expansion
  문제는 이 히스토리 익스팬션 문자라는 것은 백슬래시(\)와 싱글 쿼트(')로만 문자 자체로서 표시(Quote)할 수 있다는 겁니다. 그럼 혹자는 '느낌표 앞에 백슬래시를 쓰면 되겠네?'라고 생각하겠지만 아래처럼 그 방법은 안 통합니다.
mirashi@myservlab:~$ echo "hi\!"
hi\!
  느낌표가 히스토리 익스팬션 문자로 쓰이는 것은 막았으나 더블 쿼트에서 재해석되는 문자는 달러($), 백쿼트(`), 이스케이프 문자 밖에 없기 때문에 백슬래시도 그대로 출력되어 버립니다. 그러면 느낌표를 어떻게 해야 출력할 수 있을까요? 혹자는 아래와 같은 해결 방안을 제시할 수도 있습니다.
mirashi@myservlab:~$ echo hi!
hi!
  네. 되긴 됩니다. 근데 쿼트를 아예 안 쓰고 억지고 해결했다는 문제는 제끼더라도 저게 과연 제대로 작동하고 있는 걸까요? 아래를 봅시다.
mirashi@myservlab:~$ echo hi!yo
-bash: !yo: event not found
  결국 눈가리고 아웅이라는 겁니다. 출력은 됐을지언정 그 출력된 느낌표는 여전히 히스토리 익스팬션 문자로서 역할을 하고 있는 것이죠. 이 문제를 해결하는 방법은 2가지가 있는데 하나는 싱글 쿼트를 사용하는 겁니다. 이 포스트에서 가장 먼저 설명했듯이 싱글 쿼트는 다음 싱글 쿼트가 나오기 전까지의 모든 문자를 문자 그대로 받아들입니다. 조금 전에도 히스토리 익스팬션 문자는 백슬래시(\)와 싱글 쿼트(')로만 문자 자체로서 표시할 수 있다고 했었지요.
mirashi@myservlab:~$ echo 'hi!'
hi!
  다른 방법은 히스토리 익스팬션 문자를 다른 것으로 바꾸거나 아예 없애버리는 겁니다. 히스토리 익스팬션 문자는 'histchars'이라는 이름의 쉘 변수를 통해 지정할 수 있습니다. 참고로 오타 아니고 정말 소문자 맞습니다. 아래처럼 저 변수에 빈 값을 넣으면 자연스럽게 이 기능을 사용하지 않게 되는 것이죠.
mirashi@myservlab:~$ histchars=
mirashi@myservlab:~$ echo "hi!"
hi!
  그런데 배시 쉘은 디폴트로 느낌표를 히스토리 익스팬션 문자로 사용하기 때문에 이 방법은 별로 좋은 방법이라고 할 수 없겠습니다. 자기 호스트에서 저렇게 설정하고 저걸 기준으로 스크립트를 짰다가 다른 호스트에 복사해서 그대로 사용했다간...별로 좋지 못하겠죠. 히스토리 익스팬션은 정말 뻘스러운 기능이 아닌가 싶습니다. 마지막으로 원문의 리플에 달린 뻘스럽지만 그래서 좋은 예제를 보며 포스트를 마치겠습니다.
bash$ cost=2.79
bash$ echo "Alice's sister said, \"Gas costs \$$cost/gal"'!'"\""
Alice's sister said, "Gas costs $2.79/gal!"
bash$ echo 'Bob'"'"'s reply was, "$'"$cost"' is too much!"'
Bob's reply was, "$2.79 is too much!"


2010/10/21 18:00 2010/10/21 18:00