Tips

대입 연산에는 변수나 명령치환을 quote 하지 않아도 된다.

보통 변수나 명령치환을 명령문에서 quote 하지않고 사용하게 되면 단어분리, globbing 이 일어나고 공백과 개행이 유지되지 않습니다. 그런데 예외가 있는데요 바로 대입 연산을 할 때입니다. 대입 연산에서는 quote 을 하지 않아도 단어분리, globbing 이 일어나지 않고 포멧을 그대로 유지해 줍니다.

이것은 다시 말하면 대입 연산에서는 자동으로 double quotes 이 붙는것과 같고, 공백이나 개행을 무시할 방법이 없는 것과 같습니다.

$ AA="I
> like
> winter     and     snow"

$ echo $AA       # 변수를 quote 하지 않으면 공백과 개행이 유지되지 않는다.
I like winter and snow

$ BB=$AA         # 대입연산 에서는 변수를 quote 하지 않아도 된다. (BB="$AA" 와 같다)

$ echo "$BB"     # 공백과 개행이 유지 된다.
I
like
winter     and     snow

------------------------

$ AA="foo          "
$ BB="bar"
$ CC=$AA$BB              # quote 을 하지 않아도 된다.
$ echo "$CC"             # 공백이 유지 된다.
foo          bar

$ DD=/home/test/$AA$BB
$ echo "$DD"
/home/test/foo          bar

$ EE=$( echo -e "foo       bar\tzoo" | sed 's/\t/\n/' )     # 명령 치환
$ echo "$EE"
foo       bar
zoo

-------------------------------------

$ ARR=(11 22 "33     44" 55)

$ AA=${ARR[@]}    # array 원소들이 공백으로 분리돼 있지만 정상적으로 대입된다.

$ echo "$AA"
11 22 33     44 55

# BB=${ARR[*]} 는 결과적으로 BB="${ARR[*]}" 와 같다. (echo "${ARR[*]}")
$ ( IFS=: BB=${ARR[*]}; IFS=$' \t\n' echo "$BB"  )
11:22:33     44:55

-------------------------------------

$ touch AA=100
$ AA=*           # 대입연산 에서는 globbing 이 일어나지 않는다. (AA="*" 와 같다)
$ echo "$AA"
*

# 다음과 같은 경우 SORTED_FILES 대입연산 에서는 globbing 이 일어나지 않으므로
# 값이 myfile.split.*.sorted 가 되지만 rm 명령에서는 globbing 이 일어나므로 
# 매칭 되는 파일들이 모두 삭제됩니다.

FILE_PREFIX=myfile.split
SORTED_FILES=$FILE_PREFIX.*.sorted

rm -f $SORTED_FILES

만약에 대입 연산에서 공백과 개행을 무시하고 싶으면 다음과 같이 할 수 있습니다.

$ AA="I
> like
> winter     and     snow"

$ BB=$( set -f; echo $AA )      # 변수 $AA 를 quote 하지 않는다.

$ echo "$BB"    
I like winter and snow
---------------------------

$ BB=${AA//+([$IFS])/ }         # +( ) 확장패턴과 매개변수 확장을 이용해
# 또는                           # $IFS 에 해당하는 값을 공백으로 치환
$ BB=${AA//+([$' \t\n'])/ }

$ echo "$BB"
I like winter and snow

대입 연산에서 변수를 quote 했을때 결과가 달라지는 경우는 다음과 같이 매개변수 확장에서 입니다.

$ arr=(11 22 33)

$ AA=${xxx-'${arr[@]}'}        # xxx 는 존재하지 않는 변수

$ echo "$AA"                   # 결과적으로 AA='${arr[@]}' 와 같다.
${arr[@]}

$ AA="${xxx-'${arr[@]}'}"      # '${arr[@]}' 가 double quotes 안에 있으므로

$ echo "$AA"                   # 결과적으로 AA="'${arr[@]}'" 와 같게된다.
'11 22 33'

----------------------------

$ AA=$( echo '${arr[@]}' )     # 명령치환 에서는 차이가 없다.
$ echo "$AA"
${arr[@]}

$ AA="$( echo '${arr[@]}' )"
$ echo "$AA"
${arr[@]}

명령에 선행하는 대입 연산

명령에 선행한 대입 연산은 명령이 실행되기 위해 fork-exec 과정을 거칠때 export 한 변수와 함께 환경변수 형태로 전달됩니다. 따라서 해당 변수는 해당 명령 이하에만 적용이 되고 명령이 종료된 후에는 사라집니다. 이것은 결과적으로 env 명령으로 변수를 설정해 외부 명령을 실행하는 것과 같은 것인데 env 명령의 단점은 shell builtin 명령은 실행할 수 없습니다.

read 명령은 IFS 값을 이용해 읽어들인 라인의 필드를 분리하는데 다음과 같이 명령 앞에서 변경해 사용하면 종료후에 IFS 값을 복구하는 과정을 거치지 않아도 됩니다.

$ echo -n "$IFS" | od -a       # read 명령 사용전 IFS 값
0000000  sp  ht  nl

# IFS 값을 read 명령에 한해 일시적으로 ':' 로 사용
$ IFS=: read -ra arr <<< "Arch Linux:Ubuntu Linux:Suse Linux"

$ echo "${arr[1]}"             # 정상적으로 필드가 분리 되었다.
Ubuntu Linux

$ echo -n "$IFS" | od -a       # read 명령 사용후 IFS 값 
0000000  sp  ht  nl            # 명령 사용 후 값이 변하지 않았다.
..........................................................

$ AA=aaa:bbb:ccc

$ IFS=: eval set -- '$AA'

$ echo $2 : $#
bbb : 3

$ echo -n "$IFS" | od -a
0000000  sp  ht  nl
..........................................................

$ echo $LC_CTYPE
C.UTF-8

$ grep -P '\xC0' <<< $'Testing this: \xC0 byte'

$ LC_CTYPE=C grep -P '\xC0' <<< $'Testing this: \xC0 byte'
Testing this: � byte

$ echo $LC_CTYPE
C.UTF-8

대입한 값은 명령이 실행돼야 적용된다.

다음은 대입 연산이 적용되지 않는데요. 이유는 echo 명령이 시작하기 전에 인수 부분에서 $A $B $C 변수확장이 일어나기 때문입니다.

$ A=1 B=2 C=3
$ A=4 B=5 C=6 echo $A $B $C       # 대입한 값이 적용되지 않는다.
1 2 3

다음과 같이 eval 명령을 사용하면 해결할 수 있습니다.

$ A=1 B=2 C=3

$ A=4 B=5 C=6 eval echo '$A $B $C'
4 5 6
$ echo $A $B $C                    # 명령 종료 후에는 원래 값으로 복귀
1 2 3                             

--------------------------------

$ A=4 B=5 C=6 sh -c 'echo $A $B $C'
4 5 6
$ echo $A $B $C
1 2 3

활용하여 array 값을 다룰때도 사용할 수 있습니다.

$ cat data
111:222:333
aaa:bbb:ccc:ddd
xxx:yyy

$ cat test.sh
#!/bin/bash 
set -f; r_num=1
while read -r record; do
    IFS=: eval 'fields=( $record )'        # ':' 를 구분자로 필드를 분리해 array 생성
    IFS=@ eval 'echo ">>> ${fields[*]}"'   # 필드 구분자를 '@' 로해서 출력
    echo record $r_num has ${#fields[*]} fields
    let r_num++
done < data
set +f
----------------------------------------------

$ ./test.sh 
>>> 111@222@333
record 1 has 3 fields
>>> aaa@bbb@ccc@ddd
record 2 has 4 fields
>>> xxx@yyy
record 3 has 2 fields

uniq, sort -u 차이

파일 내용 중에서 중복되어 존재하는 라인들을 하나의 라인만 남기고 출력하고자 할 때는 uniq 명령을 사용해서는 안되고 sort -u 명령을 사용해야 합니다. 왜냐하면 파일 전체에서 중복되어 존재하는 라인을 하나로 만들려면 먼저 sort 를 해야 하는데 uniq 명령은 입력되는 라인을 차례로 처리하기 때문입니다.

$ cat file
333
111
222   # 222
444   # 444 연이어지는 중복 라인
444
444
222   # 222
555

# uniq 명령은 파일을 sort 하지 않고 입력되는 라인을 차례로 처리한다.
$ uniq file
333
111
222   # 222
444   # 444 연이어지는 중복 라인만 uniq 라인으로 됨
222   # 222 는 그대로 남아 있다.
555
--------------------------------------------------------

# 파일 전체가 먼저 sort 된후에 처리되므로 222, 444 와 같은 모든 중복 라인들이 uniq 라인으로 됩니다.
$ sort -u file
111
222
333
444
555

세로 출력을 가로 출력으로, 가로 출력을 세로 출력으로

세로 출력을 가로 출력으로

# 세로 출력을                 # 가로 출력으로
$ seq 10                    $ echo $(seq 10)
1                           1 2 3 4 5 6 7 8 9 10
2
3
4                           $ seq 10 | xargs
5                           1 2 3 4 5 6 7 8 9 10
6 
7
8                           $ seq 10 | paste -s -d ' '
9                           1 2 3 4 5 6 7 8 9 10
10

가로 출력을 세로 출력으로

# 가로 출력을                                    # 세로 출력으로
$ echo $(seq 10)                               $ printf "%s\n" $(seq 10)
1 2 3 4 5 6 7 8 9 10                           1
                                               2
                                               3
                                               ...
$ echo $(seq 10) | tr '[[:blank:]]' '\n'       $ echo $(seq 10) | sed -E 's/[ \t]+/\n/g'
1                                              1
2                                              2
3                                              3
...                                            ...
$ echo $(seq 10) | xargs -n1                   $ echo $(seq 10) | xargs -n2
1                                              1 2
2                                              3 4
3                                              5 6
...                                            7 8
                                               9 10

ls *.txtecho *.txt 두 명령을 프롬프트 상에서 실행해 보면 동일하게 가로 출력이 되는데요. 하지만 터미널이 아닌 파이프나 파일로 결과가 전달될 때는 ls 명령은 newline 에 의해 분리되는 세로 출력이 됩니다.

$ ls *.txt 
aaa.txt  bbb.txt  ccc.txt

$ echo *.txt
aaa.txt bbb.txt ccc.txt

$ ls *.txt | wc -l
3

$ echo *.txt | wc -l
1
----------------------------

# nl 은 newline 을 의미
$ ls *.txt | od -a
0000000   a   a   a   .   t   x   t  nl   b   b   b   .   t   x   t  nl
0000020   c   c   c   .   t   x   t  nl
0000030

# sp 는 space 를 의미
$ echo *.txt | od -a
0000000   a   a   a   .   t   x   t  sp   b   b   b   .   t   x   t  sp
0000020   c   c   c   .   t   x   t  nl
0000030

Non-printing 문자가 포함되는지 알아보기

cat 명령을 이용하면 tab 문자 같은 non-printing 문자가 포함되는지 알아볼 수 있습니다.
이외에도 cat 명령은 각 라인의 끝을 표시하거나, 라인 넘버를 붙일 수도 있고 연이어지는 공백 라인을 하나로 만들 수 있습니다.

# tab 은 ^I, \a 는 ^G, \r 은 ^M 으로 표시되는걸 볼 수 있습니다.
$ echo -e "AAA \t BBB \a CCC \r DDD" | cat -t
AAA ^I BBB ^G CCC ^M DDD

$ objdump -d /bin/ls | cat -t 
. . .
. . .
    3658:^I48 83 ec 08          ^Isub    $0x8,%rsp
    365c:^I48 8b 05 7d b9 21 00 ^Imov    0x21b97d(%rip),%rax
    3663:^I48 85 c0             ^Itest   %rax,%rax
    3666:^I74 02                ^Ije     366a <_init@@Base+0x12>
    3668:^Iff d0                ^Icallq  *%rax
    366a:^I48 83 c4 08          ^Iadd    $0x8,%rsp
    366e:^Ic3                   ^Iretq   
. . .
. . .
--------------------------------------------------

# 연이어지는 공백 라인을 하나로 
$ echo -e "AAA\nBBB\nCCC\n\n\n\n\n\nDDD" | cat -s
AAA
BBB
CCC

DDD

sudo 명령 사용 시 미리 패스워드 인증하기

스크립트 내에서 sudo 명령을 사용할 경우 시작 전에 미리 패스워드 인증을 하는 것이 좋습니다. sudo 명령은 처음에 한번 패스워드 인증을 하면 결과를 cache 하므로 다음부터는 인증을 하지 않습니다.

#!/bin/sh

# 스크립트 처음에 -v 옵션으로 미리 패스워드 인증을 합니다.
sudo -v -p "\
root privilege required.
[sudo] password for $USER: " || exit

.....
.....

# 이후에 사용되는 sudo 명령에서는 패스워드 인증을 하지 않게 됩니다.
sudo id

특정 디렉토리, 파일 모니터링하기

inotify-tools 를 이용하면 특정 디렉토리( 또는 이하 디렉토리 ), 파일을 event 별로 모니터링할 수 있습니다.

# 먼저 inotify-tools 패키지 설치
# inotifywait 와 inotifywatch 두 개의 실행파일이 생성됩니다.
$ sudo apt install inotify-tools

# 1. /tmp 디렉토리 이하를 모니터링하기 위해 inotifywait 명령 실행
# 2. 테스트를 위해 firefox 브라우저 실행
$ inotifywait -r -m -e create,delete,access,modify /tmp
Setting up watches.  Beware: since -r was given, this may take a while!
Watches established.
/tmp/ CREATE,ISDIR firefox_mug896
/tmp/ ACCESS,ISDIR firefox_mug896
/tmp/firefox_mug896/ CREATE lock
/tmp/firefox_mug896/ DELETE lock
/tmp/firefox_mug896/ DELETE .parentlock
/tmp/ DELETE,ISDIR firefox_mug896
^C

숫자에 단위를 붙여서 프린트

numfmt 명령을 이용하면 숫자에 K,M,G 단위를 붙여 출력하거나 반대로 단위가 붙은 숫자를 풀어서 숫자로 출력할 수 있습니다. 사용되는 단위에는 1000 을 1K 로 보는 si 단위와 2^10 값인 1024 를 1K 로 보는 iec 단위가 있습니다. iec-i 단위는 iec 단위에 i 문자가 붙는 것입니다.

SI 는 프랑스어인 Systeme Internationale 의 약자로 International System of Units 을 의미하고 IEC 는 International Electrotechnical Commission 을 의미합니다.

$ numfmt --from=si 1M       # 1M 단위 숫자를 풀어서 출력
1000000
$ numfmt --from=iec 1M      # iec 단위는 1M 가 2^20 이 됩니다.
1048576

$ numfmt --to=si 1000000    # 반대로 단위를 붙여서 출력
1.0M
$ numfmt --to=iec 1048567
1.0M
$ numfmt --to=iec 1000000   # 1000000 을 iec 단위로 출력하면 977K 가 됩니다.
977K

해당 명령에서 단위 출력을 지원하는 경우도 있지만 그렇지 않을 경우 다음과 같이 numfmt 명령을 사용하여 출력할 수 있습니다.

# df 명령은 단위 출력을 지원하지만 예제를 위해서 다음과 같은 출력값을 이용하겠습니다.
$ df -B1 
Filesystem        1B-blocks        Used    Available Use% Mounted on
udev             8338116608           0   8338116608   0% /dev
tmpfs            1672515584     1650688   1670864896   1% /run
/dev/sda1      250966470656 92287070208 145859670016  39% /
tmpfs            8362565632   223703040   8138862592   3% /dev/shm
tmpfs               5242880        4096      5238784   1% /run/lock
tmpfs            8362565632           0   8362565632   0% /sys/fs/cgroup
tmpfs            1672511488      110592   1672400896   1% /run/user/1000

# --header=1 옵션은 상단 1 개의 라인을 처리에서 제외합니다.
# --field=2-4 옵션으로 2 ~ 4 번째 컬럼을 지정합니다.
# --field=3 또는 --field=3,5 와 같이 사용할 수 있습니다.
$ df -B1 | numfmt --header=1 --field=2-4 --to=iec 
Filesystem        1B-blocks        Used    Available Use% Mounted on
udev                   7.8G           0         7.8G   0% /dev
tmpfs                  1.6G        1.6M         1.6G   1% /run
/dev/sda1              234G         86G         136G  39% /
tmpfs                  7.8G        214M         7.6G   3% /dev/shm
tmpfs                  5.0M        4.0K         5.0M   1% /run/lock
tmpfs                  7.8G           0         7.8G   0% /sys/fs/cgroup
tmpfs                  1.6G        108K         1.6G   1% /run/user/1000

# iec-i 단위를 이용하면 Ki,Mi,Gi 와같이 뒤에 i 문자가 붙습니다.
# --suffix=B 옵션을 사용하여 KiB,MiB,GiB 로 만들 수 있습니다.
$ df -B1 | numfmt --header=1 --field=2-4 --to=iec-i --suffix=B
Filesystem        1B-blocks        Used    Available Use% Mounted on
udev                 7.8GiB          0B       7.8GiB   0% /dev
tmpfs                1.6GiB      1.6MiB       1.6GiB   1% /run
/dev/sda1            234GiB       86GiB       136GiB  39% /
tmpfs                7.8GiB      214MiB       7.6GiB   3% /dev/shm
tmpfs                5.0MiB      4.0KiB       5.0MiB   1% /run/lock
tmpfs                7.8GiB          0B       7.8GiB   0% /sys/fs/cgroup
tmpfs                1.6GiB      108KiB       1.6GiB   1% /run/user/1000

# 결과값에 특정 값을 곱하기 해서 출력해야 될 경우는 '--from-unit=' 을 사용하고
# 특정 값을 나누기해서 출력해야 될 경우는 '--to-unit=' 을 사용하면 됩니다.

다음 스크립트는 단위 suffix 가 붙은 숫자들을 더하기 하여 합계를 출력합니다.

$ cat numfmt.sh        
#!/bin/bash
#
#    -f : field
#    -d : delimiter
#    -h : skip header lines
#
errArg () { echo "$0: invalid option argument -- $1"; exit 1 ;} >&2

while getopts "f:d:h:" opt; do
    case $opt in
        f ) [[ $OPTARG =~ ^[0-9]+$ ]] && field=$OPTARG  || errArg "$OPTARG" ;;
        h ) [[ $OPTARG =~ ^[0-9]+$ ]] && header=$OPTARG || errArg "$OPTARG" ;;
        d ) delimiter=$OPTARG ;;
    esac
    # 유효하지 않은 옵션을 사용하거나, 옵션 인수 값을 빼먹으면 종료
    [ "$opt" = "?" ] && exit 1
    [[ $OPTARG =~ ^- ]] && errArg "$OPTARG"
done

test -z "$field"     && field=1
test -n "$header"    && TAIL="tail -n +$(( header + 1)) | "
test -n "$delimiter" && SEP="-F '$delimiter'"

eval "$( cat <<EOF
$TAIL
awk $SEP '{print \$$field}' |
numfmt --from=iec |
awk -M '{ sum += \$1 } END{ print sum }' |
numfmt --to=iec
EOF
)"

##############  실행 결과  ##############

$ df -h | numfmt.sh -f3 -h1    # 3 번째 field 값, header 는 1 라인을 제외함
106G
$ df -h | numfmt.sh -f4 -h1
145G
$ df -B1 | numfmt.sh -f3 -h1
105G
$ df -B1 | numfmt.sh -f4 -h1
144G

Quiz

cat 명령의 반대는 무엇일까요?

알파벳을 거꾸로 하면 됩니다.

$ seq 5 | cat
1
2
3
4
5

$ seq 5 | tac
5
4
3
2
1
------------------------------------------------------

# 다음은 deb 패키지가 시스템에 install, remove, upgrade 되는 기록을
# /var/log/dpkg.log 로그 파일과 tac 명령을 이용해 최근 순으로 보여줍니다.
# case 문에 ;;& (next-matching) 을 사용하였으므로 -i, -ir, -rui 와 같이 사용할 수 있습니다.
$ cat dpkg-history.sh
#!/bin/bash

FILE=/var/log/dpkg.log
GREP="grep -E --color=always"
export LESS="-FRSXi"

case $1 in
    -*i* )
        str+=" install "
        ;;&                         # ;;& (next-matching)
    -*u* )
        [ -n "$str" ] && str+="|"; str+=" upgrade "
        ;;&
    -*r* )
        [ -n "$str" ] && str+="|"; str+=" remove "
        ;;&
    -*p* )
        [ -n "$str" ] && str+="|"; str+=" purge "
        ;;
esac

if [ -z "$str" ]; then 
    str=" install | upgrade | remove | purge "
fi

tac "$FILE" | $GREP "$str" | less