부록 A. 여러분들이 보내준 스크립트들(Contributed Scripts)

이 스크립트들은 이 문서의 주제에 딱 들어맞지는 않지만 쉘 프로그래밍 테크닉의 재밌는 부분을 보여주고 또한 쓸만합니다. 이 스크립트들을 분석해 보고 실행해 보면서 재미를 느껴보기 바랍니다.

예 A-1. manview: 포맷된 맨 페이지를 보는 스크립트

#!/bin/bash
# manview.sh: 맨페이지 소스를 보기 위해 형식화하기.

# 맨페이지 소스를 작성하는 도중에 
# 즉시 작업 결과를 보고 싶을 때 쓰면 아주 좋습니다.

E_WRONGARGS=65

if [ -z "$1" ]
then
  echo "사용법: `basename $0` [filename]"
  exit $E_WRONGARGS
fi

groff -Tascii -man $1 | less
# 맨페이지를 groff 로 보기.

# 맨페이지에 테이블이나 방정식이 들어 있다면, 위 코드는 에러가 납니다.
# 이렇게 하면 그런 상황을 처리할 수 있습니다.
#
#   gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man
#
#   Thanks, S.C.

exit 0

예 A-2. mailformat: 이메일 메세지를 포맷해서 보기

#!/bin/bash
# mail-format.sh: 이메일 메세지를 포맷.

# 캐럿(>)이나, 탭, 과도한 들여쓰기가 된 줄을 삭제.

ARGS=1
E_BADARGS=65
E_NOFILE=66

if [ $# -ne $ARGS ]  # 원하는 숫자의 인자가 넘어왔는지.
then
  echo "사용법: `basename $0` filename"
  exit $E_BADARGS
fi

if [ -f "$1" ]       # 파일이 존재하는지 확인.
then
    file_name=$1
else
    echo "\"$1\" 이란 파일은 없습니다."
    exit $E_NOFILE
fi

MAXWIDTH=70          # 긴 줄을 접을 넓이.

sed '
s/^>//
s/^  *>//
s/^  *//
s/		*//
' $1 | fold -s --width=$MAXWIDTH
          # "fold"의 -s 옵션은 가능하다면 공백문자에서 줄을 잘라줍니다.

#  이 스크립트는 아주 유명한 잡지에 기사로 소개되어 격찬을 받은 
#+ 164K 짜리 윈도우즈용 유틸리티에서 영감을 얻어 작성되었습니다.

exit 0

예 A-3. rn: 간단한 파일이름 변경 유틸리티

이 스크립트는 예 12-15의 변경판입니다.

#! /bin/bash
#
# "lowercase.sh"에 기초해 작성된 아주 간단한 파일이름 "rename" 유틸리티.
#
#  Vladimir Lanin(lanin@csd2.nyu.edu)이 만든 "ren" 유틸리티가 
#+ 이것보다 훨씬 더 좋습니다.


ARGS=2
E_BADARGS=65
ONE=1                     # 단수, 복수를 문법에 맞게 쓰려고(아래를 보세요).

if [ $# -ne "$ARGS" ]
then
  echo "사용법: `basename $0` old-pattern new-pattern"
  #  예를 들어 "rn gif jpg" 라고 하면 현재 디렉토리의 모든 gif 파일의 이름을 
  #+ jpg 파일로 바꿔줍니다.
  exit $E_BADARGS
fi

number=0                  # 실제로 얼마나 많은 파일이름이 바뀌었는지를 담고 있을 변수.


for filename in *$1*      # 디렉토리에서 일치하는 모든 파일을 탐색.
do
   if [ -f "$filename" ]  # 찾았다면...
   then
     fname=`basename $filename`            # 경로를 떼어내고,
     n=`echo $fname | sed -e "s/$1/$2/"`   # 새 이름으로 바꾼 다음,
     mv $fname $n                          # 이름 바꿈.
     let "number += 1"
   fi
done   

if [ "$number" -eq "$ONE" ]                # 문법에 맞게 하려고
then
 echo "$number 개의 파일이름이 바뀌었습니다."
else 
 echo "$number 개의 파일이름들이 바뀌었습니다."
fi 

exit 0


# 독자들을 위한 연습문제:
# 어떤 종류의 파일일 경우에 이 스크립트가 제대로 동작하지 않을까요?
# 그런 파일을 처리하려면 어떻게 고치죠?

예 A-4. encryptedpw: 로컬에 암호화 되어 있는 비밀번호로 ftp 사이트에 파일을 업로드하는 스크립트

#!/bin/bash

# "ex72.sh" 예제를 암호화된 비밀번호를 사용하도록 수정함.

#  하지만 복호화된 비밀번호가 그대로 전송되기 때문에 
#+ 아직도 안전하지는 않습니다. 주의하세요.
# 이 점이 걱정된다면 "ssh" 같은 것을 써보세요.

E_BADARGS=65

if [ -z "$1" ]
then
  echo "사용법: `basename $0` filename"
  exit $E_BADARGS
fi  

Username=bozo           # 알맞게 고치세요.
pword=/home/bozo/secret/password_encrypted.file
# 암호화된 비밀번호가 들어있는 파일.

Filename=`basename $1`  # 파일이름에서 경로이름을 떼어 냅니다.

Server="XXX"
Directory="YYY"         # 실제 서버이름과 디렉토리로 바꾸세요.


Password=`cruft <$pword`          # 비밀번호 복호화.
#  저자가 만든 고전적인 1회용 암호표(onetime pad) 알고리즘에 기반한
#+ "cruft" 파일 암호화 패키지를 씁니다.
#+ "cruft"는 다음에서 얻을 수 있습니다.
#+ Primary-site:   ftp://metalab.unc.edu /pub/Linux/utils/file
#+                 cruft-0.2.tar.gz [16k]


ftp -n $Server <<End-Of-Session
user $Username $Password
binary
bell
cd $Directory
put $Filename
bye
End-Of-Session
# "ftp"의 -n 옵션은 자동 로그인을 막아줍니다.
# "bell"은 파일 전송이 일어날 때마다 '벨'을 울려줍니다.

exit 0

예 A-5. copy-cd: 데이타 CD를 복사하는 스크립트

#!/bin/bash
# copy-cd.sh: 데이타 CD 복사

CDROM=/dev/cdrom                           # CDROM 디바이스
OF=/home/bozo/projects/cdimage.iso         # 출력 파일
#       /xxxx/xxxxxxx/                     여러분 시스템에 맞게 고치세요.
BLOCKSIZE=2048
SPEED=2                                    # 더 빠른 CDROM 이면 거기에 맞는 속도를 쓰세요.

echo; echo "원본 CD 를 넣고, 마운트는 하지 \"마세요.\""
echo "준비가 되면 엔터를 누르세요. "
read ready                                 # 입력 대기, $ready 는 안 쓰임.

echo; echo "원본 CD 를 $OF 로 복사합니다."
echo "시간이 걸릴 수도 있으니 기다리기 바랍니다."

dd if=$CDROM of=$OF bs=$BLOCKSIZE          # 디바이스를 물리적 그대로(raw) 복사.


echo; echo "데이타 CD 를 꺼내세요."
echo "공 CD 를 넣으세요."
echo "준비가 되면 엔터를 누르세요. "
read ready                                 # 입력 대기, $ready 는 안 쓰임.

echo "$OF 를 CDR 로 복사합니다."

cdrecord -v -isosize speed=$SPEED dev=0,0 $OF
# Joerg Schilling 의 "cdrecord" 패키지 씀(해당 문서 참고).
# http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html


echo; echo "$OF 를 $CDROM 디바이스로 복사 완료했습니다."

echo "이미지 파일을 지우고 싶으세요(y/n)? "  # 아마 꽤 큰 파일이겠죠.
read answer

case "$answer" in
[yY]) rm -f $OF
      echo "$OF 를 지웠습니다."
      ;;
*)    echo "$OF 를 안 지웠습니다.";;
esac

echo

# 독자들을 위한 연습문제:
# 위의 "case" 문을 "yes"와 "Yes"도 받아들이도록 고쳐보세요.

exit 0

예 A-6. days-between: 두 날짜 사이의 차이를 계산해 주는 스크립트

#!/bin/bash
# days-between.sh:    두 날짜 사이의 날 수.
# 사용법: ./days-between.sh YYYY/[M]M/[D]D YYYY/[M]M/[D]D

ARGS=2                # 명령어줄 매개변수는 두 개 필요.
E_PARAM_ERR=65        # 매개변수 에러.

REFYR=1600            # 참조 년(Reference year).
CENTURY=100
DIY=365
ADJ_DIY=367           # 윤년 + fraction 을 조절하기 위해서.
MIY=12
DIM=31
LEAPCYCLE=4

MAXRETVAL=256         # 함수에서 리턴 가능한 최대 정수

diff=		      # 날짜 간격을 위한 전역 변수 선언.
value=                # 절대값을 위한 전역 변수 선언.
day=                  # 년, 월, 일을 위한 전역 변수 선언.
month=
year=


Param_Error ()        # 틀린 명령어줄 매개변수.
{
  echo "사용법: `basename $0` YYYY/[M]M/[D]D YYYY/[M]M/[D]D"
  echo "       (1600/1/3 이후의 날짜여야 합니다)"
  exit $E_PARAM_ERR
}  


Parse_Date ()                 # 명령어줄 매개변수로 넘어온 날짜를 파싱.
{
#  month=${1%%/**}
#  dm=${1%/**}                 # Day and month.
#  day=${dm#*/}
# 옮긴이: YYYY/MM/DD 형태로 바꿈.
  year=${1%%/**}
  ym=${1%/**}                 # 년, 월.
  month=${ym#*/}
  let "day = `basename $1`"   # 파일이름은 아니지만 똑같이 동작합니다.
}  


check_date ()                 # 날짜 범위가 맞는지 확인.
{
  [ "$day" -gt "$DIM" ] || [ "$month" -gt "$MIY" ] || [ "$year" -lt "$REFYR" ] && Param_Error
  # 제대로 된 날짜가 아닐 경우에 스크립트를 종료.
  # "or-list / and-list" 를 썼죠.
  # 독자들을 위한 연습문제: 날짜를 좀 더 정확하게 확인하도록 구현해 보세요.
}


strip_leading_zero () # 월이나 일 앞에 0 이 있을 경우에는 
{                     # Bash 가 8 진수로 해석하기 때문에(POSIX.2, 2.9.2.1 절) 
  val=${1#0}          # 그 0 을 잘라낼 필요가 있습니다.
  return $val
}


day_index ()          # 가우스 공식(Gauss' Formula):
{                     # 1600년 1월 3일부터 매개변수로 넘어온 날짜까지 계산.

  day=$1
  month=$2
  year=$3

  let "month = $month - 2"
  if [ "$month" -le 0 ]
  then
    let "month += 12"
    let "year -= 1"
  fi  

  let "year -= $REFYR"
  let "indexyr = $year / $CENTURY"


  let "Days = $DIY*$year + $year/$LEAPCYCLE - $indexyr + $indexyr/$LEAPCYCLE + $ADJ_DIY*$month/$MIY + $day - $DIM"
  # 이 알고리즘에 대한 자세한 설명은 다음을 참고하세요.
  # http://home.t-online.de/home/berndt.schwerdtfeger/cal.htm


  if [ "$Days" -gt "$MAXRETVAL" ]  # 함수에서 256 보다 큰 수를 리턴하기 위해서
  then                             # 결과가 256 보다 크다면 음수로 바꿈.
    let "dindex = 0 - $Days"     
  else let "dindex = $Days"
  fi

  return $dindex

}  


calculate_difference ()            # 두 날짜간 차이나는 날 수를 계산.
{
  let "diff = $1 - $2"             # 전역 변수.
}  


abs ()                             # 절대값
{                                  # "value" 전역 변수 사용.
  if [ "$1" -lt 0 ]                # If 음수
  then                             # then
    let "value = 0 - $1"           # 부호 바꾸기,
  else                             # else
    let "value = $1"               # 그대로 둠.
  fi
}



if [ $# -ne "$ARGS" ]              # 명령어줄 매개변수가 두 개 필요.
then
  Param_Error
fi  

Parse_Date $1
check_date $day $month $year      # 날짜 형식이 맞는지 확인.

strip_leading_zero $day           # 일이나 월 앞에 붙은 0 을 제거.
day=$?                          
strip_leading_zero $month
month=$?

day_index $day $month $year
date1=$?

abs $date1                         # 절대값을 구해 확실히 양수로 만듦
date1=$value                     

Parse_Date $2
check_date $day $month $year

strip_leading_zero $day
day=$?
strip_leading_zero $month
month=$?

day_index $day $month $year
date2=$?

abs $date2                         # 양수로 만듦.
date2=$value

calculate_difference $date1 $date2

abs $diff                          # 양수로 만듦.
diff=$value

echo $diff

exit 0
# http://buschencrew.hypermart.net/software/datedif
# 에 있는 가우스 공식의 C 구현과 이 스크립트를 비교해 보세요.

+

다음 두 스크립트는 토론토 대학의 Mark Moraes가 작성했습니다. 허가 사항과 제한 사항에 대해서는 이 문서와 같이 배포되는 "Moraes-COPYRIGHT" 파일을 참고하기 바랍니다.

예 A-7. behead: 메일과 뉴스 메세지 헤더를 제거해 주는 스크립트

#! /bin/sh
# 메일이나 뉴스 메세지에서 첫번째 빈 줄이 나올 때까지의 
# 헤더를 떼어 내 주는 스크립트.
# 토론토 대학의 Mark Moraes 작성

# --> 이 주석은 HOWTO 저자가 붙인 것입니다.

if [ $# -eq 0 ]; then
# --> 명령어 줄 인자가 없다면 표준입력에서 재지향되어 들어오는 파일에 대해서 동작함.
	sed -e '1,/^$/d' -e '/^[ 	]*$/d'
	# --> 공백 문자로 시작하는 첫번째 줄이 나올 때까지
	# --> 빈 줄과 모든 줄을 지웁니다.
else
# --> 명령어 줄 인자가 있다면 그 주어진 파일에 대해서 동작함
	for i do
		sed -e '1,/^$/d' -e '/^[ 	]*$/d' $i
		# --> 위와 같습니다.
	done
fi

# --> 독자를 위한 연습문제: 에러 체크와 다른 옵션을 추가해 보세요.
# -->
# --> 작은 sed 스크립트가 인자를 넘기는 것만 빼고 반복되는것을 유심히 살펴 보세요.
# --> 이 부분을 함수로 빼는게 이치에 맞을까요? 그 대답에 대해서 설명해 보세요.

예 A-8. ftpget: ftp에서 파일을 다운로드 해 주는 스크립트

#! /bin/sh 
# $Id: ftpget,v 1.2 91/05/07 21:15:43 moraes Exp $ 
# 익명 ftp에 배치 작업을 실행하는 스크립트. 기본적으로 명령어 라인 인자들을
# ftp의 입력으로 바꿔주는 일을 합니다.
# 간단하고 빠릅니다 - ftplist와 한 쌍이 되도록 작성됐습니다.
# -h 는 접속할 호스트를 나타냅니다(기본값은 prep.ai.mit.edu)
# -d 는 접속후 cd 로 옮겨갈 디렉토리를 나타냅니다. -d 를 여러번 쓸 수도 
# 있는데, 이렇게 하면 주어진 순서대로 디렉토리를 옮겨갈 것입니다.
# 만약에 해당 디렉토리가 상대 경로라면 순서를 잘 매겨야 합니다. 
# 요즘엔 너무 많은 심볼릭 링크가 존재하기 때문에 아주 조심해서 사용해야 합니다.
# (기본값은 ftp 로그인 디렉토리)
# -v 는 ftp의 verbose모드를 켜서, ftp 서버의 모든 응답을 보여줍니다.
# -f remotefile[:localfile] 은 remote 파일을 local 파일로 이름을 바꿔 
# 가져옵니다.
# -m pattern 은 주어진 패턴에 해당하는 파일들을 mget으로 가져옵니다.
# 쉘 문자들을 인용(quote)해야 하는 것을 기억하세요.
# -c 는 현재 자신의 시스템에서 주어진 디렉토리로 cd 를 실행합니다.
# 예를 들면,
# 	ftpget -h expo.lcs.mit.edu -d contrib -f xplaces.shar:xplaces.sh \
#		-d ../pub/R3/fixes -c ~/fixes -m 'fix*' 
# 는 expo.lcs.mit.edu 의 ~ftp/contrib 에서 xplaces.shar 를 현재 디렉토리에
# xplaces.sh 로 가져오고, ~ftp/pub/R3/fixes 에서 모든 수정 파일('fix*')들을
# 자기 시스템의 ~/fixes 디렉토리로 가져옵니다.
# ftp 에서 해당 명령어가 주어진 순서대로 실행되기 때문에 옵션 순서가 
# 중요하다는 것은 아주 확실합니다.
#
# Mark Moraes (moraes@csri.toronto.edu), Feb 1, 1989 
# ==> Docbook에서 처리할 수 있도록 부등호 괄호를 소괄호로 바꾸었습니다.
#

# ==> 이런 주석은 HOWTO 저자가 덧붙인 주석입니다.

# PATH=/local/bin:/usr/ucb:/usr/bin:/bin
# export PATH
# ==> 원래 스크립트에 있던 위 두 줄은 쓸데없어 보입니다.

TMPFILE=/tmp/ftp.$$
# ==> 스크립트의 프로세스 ID($$)로 임시 파일을 만듦.

SITE=`domainname`.toronto.edu
# ==> 'domainname'은 'hostname'과 비슷합니다.
# ==> 좀 더 일반적으로 쓰려면 매개변수로 처리하도록 재작성 할 수도 있습니다.

usage="사용법: $0 [-h remotehost] [-d remotedirectory]... [-f remfile:localfile]... \
		[-c localdirectory] [-m filepattern] [-v]"
ftpflags="-i -n"
verbflag=
set -f 		# -m 옵션에서 globbing을 쓰기 위해서
set x `getopt vh:d:c:m:f: $*`
if [ $? != 0 ]; then
	echo $usage
	exit 65
fi
shift
trap 'rm -f ${TMPFILE} ; exit' 0 1 2 3 15
echo "user anonymous ${USER-gnu}@${SITE} > ${TMPFILE}"
# ==> 쿼우트 추가(복잡한 echo 문에서는 이렇게 하기 바랍니다).
echo binary >> ${TMPFILE}
for i in $*   # ==> 명령어줄 인자를 파싱.
do
	case $i in
	-v) verbflag=-v; echo hash >> ${TMPFILE}; shift;;
	-h) remhost=$2; shift 2;;
	-d) echo cd $2 >> ${TMPFILE}; 
	    if [ x${verbflag} != x ]; then
	        echo pwd >> ${TMPFILE};
	    fi;
	    shift 2;;
	-c) echo lcd $2 >> ${TMPFILE}; shift 2;;
	-m) echo mget "$2" >> ${TMPFILE}; shift 2;;
	-f) f1=`expr "$2" : "\([^:]*\).*"`; f2=`expr "$2" : "[^:]*:\(.*\)"`;
	    echo get ${f1} ${f2} >> ${TMPFILE}; shift 2;;
	--) shift; break;;
	esac
done
if [ $# -ne 0 ]; then
	echo $usage
	exit 65   # ==> 표준을 따르기 위해 "exit 2"였던 것을 수정.
fi
if [ x${verbflag} != x ]; then
	ftpflags="${ftpflags} -v"
fi
if [ x${remhost} = x ]; then
	remhost=prep.ai.mit.edu
	# ==> 여러분이 좋아하는 ftp 싸이트로 바꾸세요.
fi
echo quit >> ${TMPFILE}
# ==> 모든 명령어는 임시 파일로 저장됩니다.

ftp ${ftpflags} ${remhost} < ${TMPFILE}
# ==> 이제 임시 파일에 저장됐던 명령어들이 ftp 에 의해 한 번에 처리됩니다.

rm -f ${TMPFILE}
# ==> 끝으로, 임시 파일 삭제(지우지 않고 로그 파일로 복사할 수도 있습니다).


# ==> 독자들을 위한 연습문제:
# ==> 1) 에러 체크를 추가하세요.
# ==> 2) 다른 편리한 기능들(bells & whistles)을 넣어보세요.

+

다음 스크립트는 Antek Sawicki가 보내주었는데 9.3절에서 논의된 매개변수 치환 연산자의 깔끔한 사용법을 보여줍니다.

예 A-9. password: 8 글자짜리 랜덤한 비밀번호 생성 스크립트

#!/bin/bash
# 오래된 시스템에서는 #!/bin/bash2 라고 바꿔야 될지도 모릅니다.
#
# 이 Bash 2.x 용 무작위 비밀번호 생성기는 Antek Sawicki <tenox@tenox.tc>
# 의 관대한 허락하에 싣습니다.
#
# ==> 본 문서의 저자가 추가한 주석 ==>


MATRIX="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
LENGTH="8"
# ==> 더 긴 비밀번호를 원한다면 'LENGTH'를 늘리세요.


while [ "${n:=1}" -le "$LENGTH" ]
# ==> := 가 "기본값 치환"(default substitution) 연산자였던거 기억나시죠?
# ==> 그래서 만약에 'n'이 초기화 되지 않았다면 1로 세트됩니다.
do
	PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
	# ==> 아주 훌륭한데 약간 헷갈리네요.

	# ==> 제일 안쪽부터 살펴 봅시다...
	# ==> ${#MATRIX} 는 MATRIX 배열의 길이를 리턴합니다.

	# ==> $RANDOM%${#MATRIX} 은 1 부터 MATRIX의 길이 - 1 에서 
	# ==> 무작위 숫자를 리턴합니다.

	# ==> ${MATRIX:$(($RANDOM%${#MATRIX})):1}
	# ==> 은 MATRIX 에서 그 무작위 숫자의 위치에 있는 
	# ==> 길이 1 짜리 문자열로 확장된 값을 리턴합니다.
	# ==> 3.3.1 절의 {var:pos:len} 매개변수 치환과 해당 예제들을 참고하세요.

	# ==> PASS=... 이 결과를 바로 전 PASS 에 붙입니다(연결).

	# ==> PASS 가 만들어지는 과정을 좀 더 확실하게 보고 싶다면, 
	# ==> 다음 줄의 주석을 푸세요.
	# ==>             echo "$PASS"
	# ==> 루프를 한 번 돌때마다 PASS 가 한 글자씩 만들어 집니다.

	let n+=1
	# ==> 다음 단계를 위해 'n'을 증가.
done

echo "$PASS"      # ==> 필요하다면 파일로 재지향.

exit 0

+

James R. Van Zandt 가 보내준 다음 스크립트는 네임드 파이프를 사용하는데 그의 말에 따르면, "정말로 쿼우팅과 이스케이핑" 연습용이었답니다.

예 A-10. fifo: 네임드 파이프를 써서 매일 백업해 주는 스크립트

#!/bin/bash
# ==> James R. Van Zandt 가 작성한 스크립트로 그의 허락하에 이곳에 싣습니다.

# ==> 이 문서의 저자가 추가한 주석.

  
  HERE=`uname -n`    # ==> 호스트이름
  THERE=bilbo
  echo "`date +%r`에 $THERE 로 백업을 시작합니다"
  # ==> `date +%r` 은 시간을 12 시간 포맷("08:08:34 PM")으로 리턴합니다.
  
  # /pipe 가 보통 파일이 아니고 진짜 파이프인지 확인.
  rm -rf /pipe
  mkfifo /pipe       # ==> "/pipe"란 "네임드 파이프"(named pipd) 만들기.
  
  # ==> 'su xyz' 는 명령어를 "xyz"란 사용자로 실행시킵니다.
  # ==> 'ssh' 는 secure shell을 띄웁니다(원격지 로그인 클라이언트).
  su xyz -c "ssh $THERE \"cat >/home/xyz/backup/${HERE}-daily.tar.gz\" < /pipe"&
  cd /
  tar -czf - bin boot dev etc home info lib man root sbin share usr var >/pipe
  # ==> 네임드 파이프(/pipe)를 써서 프로세스끼리 통신을 함
  # ==> 'tar/gzip' 은 /pipe로 쓰고, 'ssh'은 /pipe에서 읽음

  # ==> 최종 결과는 / 이하의 메인 디렉토리들이 백업 됩니다.

  # ==> 지금 같은 상황에서 "익명의 파이프"인 | 대신 그 반대인 
  # ==> "네임드 파이프"를 쓴 것이 어떤 이점이 있을까요?
  # ==> 익명 파이프도 제대로 동작할까요?


  exit 0

+

다음은 Stephane Chazelas가 보내준, 배열없이 소수(prime number)를 생성해 주는 스크립트입니다.

예 A-11. 나머지 연산자로 소수 생성하기

#!/bin/bash
# primes.sh: 배열을 쓰지 않고 소수(prime number)를 생성.

#  이 스크립트는 고전적인 "에라토스테네스의 체" 알고리즘을 쓰지 *않고*
#+ "%" 나머지 연산자를 써서 소수를 구하는 좀 더 직관적인 방법을 사용합니다.
#
# Stephane Chazelas 가 보내준 스크립트.


LIMIT=1000                    # 2 - 1000 의 소수

Primes()
{
 (( n = $1 + 1 ))             # 다음 정수.
 shift                        # 목록의 다음 매개변수.
#  echo "_n=$n i=$i_"
 
 if (( n == LIMIT ))
 then echo $*
 return
 fi

 for i; do                    # "i"는 $n 의 이전값인 "@"로 세트됨.
#   echo "-n=$n i=$i-"
   (( i * i > n )) && break   # 최적화.
   (( n % i )) && continue    # 나머지 연산자로 소수가 아닌 수를 걸러냄.
   Primes $n $@               # 루프안에서 재귀 호출.
   return
   done

   Primes $n $@ $n            # 루프밖에서 재귀 호출.
                              # 위치 매개변수로 누적(accumulate) 성공.
			      			  # "$@" 는 현재 찾은 소수의 목록입니다.
}

Primes 1

exit 0

# 무슨 일이 일어나는지 알고 싶으면 16줄과 24줄의 주석을 풀어보세요.

# 이 알고리즘과 에라토스테네스의 체 알고리즘(ex68.sh)의 속도를 비교해 보세요.

# 연습문제: 더 빠르게 실행시키기 위해서 재귀 호출을 쓰지 말고 다시 작성해 보세요.

+

Jordi Sanfeliu가 이 근사한 tree 스크립트를 쓰도록 허락해 주었습니다.

예 A-12. tree: 디렉토리 구조를 트리 형태로 보여주는 스크립트

#!/bin/sh
#         @(#) tree      1.1  30/11/95       by Jordi Sanfeliu
#                                         email: mikaku@arrakis.es
#
#         Initial version:  1.0  30/11/95
#         Next version   :  1.1  24/02/97   심볼릭 링크도 지원
#         Patch by       :  Ian Kjos, 찾을 수 없는 디렉토리 지원
#                           email: beth13@mail.utexas.edu
#
#         Tree 는 디렉토리 트리구조를 확실히(:-) ) 보여줍니다.
#

# ==> 'Tree' 스크립트는 원자자인 Jordi Sanfeliu 의 허락하에 여기에 싣습니다.
# ==> 이 문서의 저자가 추가한 주석.
# ==> 인자 쿼우팅 추가.


search () {
   for dir in `echo *`
   # ==> `echo *` 는 현재 디렉토리의 모든 파일을 한 줄에 표시해 줌.
   # ==> for dir in *   라고 하는 것과 비슷함.
   # ==> 하지만 "dir in `echo *`" 은 이름에 빈 칸이 들어있는 파일은 처리 못함.
   do
      if [ -d "$dir" ] ; then   # ==> 디렉토리라면(-d)...
         zz=0   # ==> 디렉토리 깊이를 갖고 있을 임시 변수.
         while [ $zz != $deep ]    # 안쪽 루프인지 계속 확인.
         do
            echo -n "|   "    # ==> 수직 연결자를 표시.
	                      # ==> 들여쓰기(indent)를 위해 라인피드 없는 두 개의 빈 칸 
            zz=`expr $zz + 1` # ==> zz 증가.
         done
         if [ -L "$dir" ] ; then   # ==> 디렉토리가 심볼릭 링크라면...
            echo "+---$dir" `ls -l $dir | sed 's/^.*'$dir' //'`
	    # ==> 수평 연결자와 긴 목록 표시(long linting)에서 날짜/시간 부분을 
		# ==> 제외한 디렉토리 이릉을 표시.
         else
            echo "+---$dir"      # ==> 수평 연결자와 디렉토리 이름 표시.
            if cd "$dir" ; then  # ==> 하위 디렉토리로 들어갈 수 있다면...
               deep=`expr $deep + 1`   # ==> 깊이 증가.
               search     # 재귀적으로 호출 ;-)
	                  # ==> 자기 자신을 부르는 함수.
               numdirs=`expr $numdirs + 1`   # ==> 디렉토리 숫자를 증가.
            fi
         fi
      fi
   done
   cd ..   # ==> 부모 디렉토리로 올라감.
   if [ "$deep" ] ; then  # ==> depth = 0 이면(TRUE을 리턴)...
      swfi=1              # ==> 탐색이 끝났음을 알리는 플래그 세트.
   fi
   deep=`expr $deep - 1`  # ==> 깊이를 감소.
}

# - 메인 -
if [ $# = 0 ] ; then
   cd `pwd`    # ==> 인자없이 불리면 현재 디렉토리에서 시작.
else
   cd $1       # ==> 아니면 주어진 디렉토리로 이동.
fi
echo "시작 디렉토리 = `pwd`"
swfi=0      # ==> 탐색 종료 플래그.
deep=0      # ==> 보여줄 깊이.
numdirs=0
zz=0

while [ "$swfi" != 1 ]   # 플래그가 세트 안 된 동안...
do
   search   # ==> 변수를 초기화하고 함수를 부름.
done
echo "전체 디렉토리 수 = $numdirs"

exit 0
# ==> 독자들에게 도전: 이 스크립트가 정확히 어떻게 동작하는지 알아내 보세요.

Noah Friedman 의 허락하에 문자열 조작 C 라이브러리 함수 몇 개의 쉘 스크립트 버전을 보여 줍니다.

예 A-13. 문자열 함수들: C 형태의 문자열 함수

#!/bin/bash

# string.bash --- string(3) 라이브러리의 bash 에뮬레이션 버전
# 저자: Noah Friedman <friedman@prep.ai.mit.edu>
# ==>     저자의 허락하에 이 문서에 게재함.
# Created: 1992-07-01
# Last modified: 1993-09-29
# Public domain

# Chet Ramey 가 bash 버전 2 문법을 쓰도록 변환

# Commentary:
# Code:

#:docstring strcat:
# Usage: strcat s1 s2
#
# Strcat 은 s2 를 s1 으로 덧붙임.
#
# 예제:
#    a="foo"
#    b="bar"
#    strcat a b
#    echo $a
#    => foobar
#
#:end docstring:

###;;;autoload   ==> Autoloading of function commented out.
function strcat ()
{
    local s1_val s2_val

    s1_val=${!1}                        # 변수 간접참조 확장
    s2_val=${!2}
    eval "$1"=\'"${s1_val}${s2_val}"\'
    # ==> 두 변수중 하나라도 작은 따옴표가 들어 있을 경우에는,
    # ==> eval $1='${s1_val}${s2_val}' 이라고 하면 됩니다.
}

#:docstring strncat:
# 사용법: strncat s1 s2 $n
# 
# strcat 과 비슷하지만 s2 에서 최대 n 문자만큼만 덧붙입니다. s2 가 n 보다
# 짧다면 그냥 s2 만 복사합니다. Echoes result on stdout.
#
# 예제:
#    a=foo
#    b=barbaz
#    strncat a b 3
#    echo $a
#    => foobar
#
#:end docstring:

###;;;autoload
function strncat ()
{
    local s1="$1"
    local s2="$2"
    local -i n="$3"
    local s1_val s2_val

    s1_val=${!s1}                       # ==> 변수 간접 참조 확장
    s2_val=${!s2}

    if [ ${#s2_val} -gt ${n} ]; then
       s2_val=${s2_val:0:$n}            # ==> 문자열조각(substring) 추출
    fi

    eval "$s1"=\'"${s1_val}${s2_val}"\'
    # ==> 두 변수중 하나라도 작은 따옴표가 들어 있을 경우에는,
    # ==> eval $1='${s1_val}${s2_val}' 이라고 하면 됩니다.
}

#:docstring strcmp:
# 사용법: strcmp $s1 $s2
#
# Strcmp 는 두 인자를 사전상의 순서대로 비교해서 s1 이 s2 보다 작으면 음수,
# 같으면 0, 크면 양수를 리턴합니다.
#:end docstring:

###;;;autoload
function strcmp ()
{
    [ "$1" = "$2" ] && return 0

    [ "${1}" '<' "${2}" ] > /dev/null && return -1

    return 1
}

#:docstring strncmp:
# 사용법: strncmp $s1 $s2 $n
# 
# strcmp 와 비슷하나 최대 n 문자만큼만 비교합니다( n 이 0 이하면 0 을 리턴).
#:end docstring:

###;;;autoload
function strncmp ()
{
    if [ -z "${3}" -o "${3}" -le "0" ]; then
       return 0
    fi
   
    if [ ${3} -ge ${#1} -a ${3} -ge ${#2} ]; then
       strcmp "$1" "$2"
       return $?
    else
       s1=${1:0:$3}
       s2=${2:0:$3}
       strcmp $s1 $s2
       return $?
    fi
}

#:docstring strlen:
# 사용법: strlen s
#
# Strlen 은 s 의 길이를 리턴합니다.
#:end docstring:

###;;;autoload
function strlen ()
{
    eval echo "\${#${1}}"
    # ==> 인자로 넘어온 변수값의 길이를 리턴
}

#:docstring strspn:
# 사용법: strspn $s1 $s2
# 
# Strspn 은 s2 전체를 포함하는 s1 의 최대 초기 세그먼트 길이를 리턴합니다.
#:end docstring:

###;;;autoload
function strspn ()
{
    # IFS 를 언셋해 놓으면 공백문자도 일반 문자처럼 처리할 수 있습니다.
    local IFS=
    local result="${1%%[!${2}]*}"
 
    echo ${#result}
}

#:docstring strcspn:
# 사용법: strcspn $s1 $s2
#
# Strcspn 은 s2 전체가 포함되지 않는 s1 의 최대 초기 세그먼트 길이를 리턴합니다.
#:end docstring:

###;;;autoload
function strcspn ()
{
    # IFS 를 언셋해 놓으면 공백문자도 일반 문자처럼 처리할 수 있습니다.
    local IFS=
    local result="${1%%[${2}]*}"
 
    echo ${#result}
}

#:docstring strstr:
# 사용법: strstr s1 s2
# 
# Strstr 은 s1 에서 s2 가 처음 발견되는 문자열조각(substring)을 에코해 주고,
# s1 에서 s2 가 발견되지 않는다면 그냥 무시합니다. 
# 만약에 s2 가 길이가 0 인 문자열을 가르킨다면 s1 을 리턴합니다.
#:end docstring:

###;;;autoload
function strstr ()
{
    # s2 가 가르키는 문자열의 길이가 0 이라면 s1 을 에코
    [ ${#2} -eq 0 ] && { echo "$1" ; return 0; }

    # s1 에서 s2 가 발견되지 않으면 그냥 리턴
    case "$1" in
    *$2*) ;;
    *) return 1;;
    esac

    # 일치하는 부분부터 끝까지를 떼어 내기 위해 패턴 매칭 사용
    first=${1/$2*/}

    # 그 다음에는 일치하지 첫번째 부분을 떼어냄
    echo "${1##$first}"
}

#:docstring strtok:
# 사용법: strtok s1 s2
#
# Strtok 은 s1 이 구분자인 s2 에 들어 있는 하나 이상의 문자열에 의해 
# 0 개 이상으로 이루어져 있다고 가정합니다. 
# strtok 이 처음 불리면(비어 있지 않은 s1 을 지정해서) 첫번째 토큰을 포함한
# 문자열을 표준출력으로 에코해 줍니다. 이 함수는 첫번째 인자가 빈 문자열로 
# 계속 호출되도록 해서 바로 다음에 따라나오는 토큰을 처리하도록 하기 위해서
# 호출시마다 s1 에서의 자신의 위치를 계속 추적합니다.
# 이런 식으로 해서 그 다음 호출은 더 이상의 토큰이 없을 때까지 s1 을 처리합니다.
# 구분 문자열인 s2 는 호출시마다 다를 수 있습니다. s1 에 처리할 토큰이 
# 남아 있지 않다면, 빈 값이 표준출력으로 에코됩니다.

###;;;autoload
function strtok ()
{
 :
}

#:docstring strtrunc:
# 사용법: strtrunc $n $s1 {$s2} {$...}
# strncmp 처럼 여러 함수에서 쓰이는데, 비교를 위해서 인자를 잘라냅니다. 
# 각각의 s1, s2 ... 에서 처음 n 개의 문자를 표준출력으로 에코해 줍니다.
#:end docstring:

###;;;autoload
function strtrunc ()
{
    n=$1 ; shift
    for z; do
        echo "${z:0:$n}"
    done
}

# provide string

# string.bash 는 여기까지


# ========================================================================== #
# ==> 여기부터는 본 문서의 저자가 추가한 부분입니다.

# ==> 이 스크립트를 여러분 스크립트에서 쓰려면 여기부터 끝까지 지운다음 
# ==> 여러분 스크립트에서 이 스크립트를 "source" 하면됩니다.

# strcat
string0=one
string1=two
echo
echo "\"strcat\" 함수 테스트:"
echo "원래의 \"string0\" = $string0"
echo "\"string1\" = $string1"
strcat string0 string1
echo "새 \"string0\" = $string0"
echo

# strlen
echo
echo "\"strlen\" 함수 테스트:"
str=123456789
echo "\"str\" = $str"
echo -n "\"str\" 의 길이 = "
strlen str
echo



# 독자들을 위한 연습문제:
# 여기서 소개한 문자열 함수를 모두 테스트하는 코드를 작성해 보세요.


exit 0

Stephane Chazelas 는 Bash 스크립트의 객체 지향적 구현을 보여줬습니다.

예 A-14. 객체 지향 데이타 베이스

#!/bin/bash
# obj-oriented.sh: 쉘 스크립트에서 객체 지향적 프로그래밍 하기.
# Stephane Chazelas 작성.


person.new()        # C++ 의 클래스 선언처럼 보입니다.
{
  local obj_name=$1 name=$2 firstname=$3 birthdate=$4

  eval "$obj_name.set_name() {
          eval \"$obj_name.get_name() {
                   echo \$1
                 }\"
        }"

  eval "$obj_name.set_firstname() {
          eval \"$obj_name.get_firstname() {
                   echo \$1
                 }\"
        }"

  eval "$obj_name.set_birthdate() {
          eval \"$obj_name.get_birthdate() {
            echo \$1
          }\"
          eval \"$obj_name.show_birthdate() {
            echo \$(date -d \"1/1/1970 0:0:\$1 GMT\")
          }\"
          eval \"$obj_name.get_age() {
            echo \$(( (\$(date +%s) - \$1) / 3600 / 24 / 365 ))
          }\"
        }"

  $obj_name.set_name $name
  $obj_name.set_firstname $firstname
  $obj_name.set_birthdate $birthdate
}

echo

person.new self Bozeman Bozo 101272413
# "person.new" 인스턴스 생성(실제로는 인자를 함수로 넘기는 것임).

self.get_firstname       #   Bozo
self.get_name            #   Bozeman
self.get_age             #   28
self.get_birthdate       #   101272413
self.show_birthdate      #   Sat Mar 17 20:13:33 MST 1973

echo

# 생성된 함수를 보려면 
# typeset -f
# 라고 해 보세요(화면이 주루룩 올라가니까 조심하세요).

exit 0