<< , <<<
<<
( here document ), <<<
( here string ) 은 실행 중에 임시 파일을 만들어서 stdin 에 연결합니다. 그러므로 echo
명령 대신에 cat
명령을 사용합니다. 아래는 ls -l
명령의 인수로 here docuemnt 를 사용하였는데 stdin 입력으로 sh-thd.Th1cFU 임시 파일이 사용된 후 삭제된 것을 볼수가 있습니다.
bash-5.1 버전 부터는 작성 내용이 pipe buffer size 보다 작을 경우는 pipe 를 이용하고 그보다 클 경우는 이전과 같이 임시파일을 사용합니다.
sh
에서는<<
만 사용할 수 있습니다.
$ stat -L -c "%F" /dev/stdin <<EOF
123
EOF
regular file
$ stat -L -c "%F" /dev/stdin <<< 123
regular file
<<
, <<<
왼쪽에 공백없이 붙여서 file descriptor 를 사용할 수 있습니다.
$ 3<<< hello <&3 cat # <&3 는 0<&3 와 같은 것임
hello
$ 3<<END cat /dev/fd/3
> hello
> END
hello
---------------------------------------------------
$ exec 3<<END
> 111
> 222
> END
$ ls -l /proc/$$/fd/
total 0
lrwx------ 1 mug896 mug896 64 2018-01-05 18:51 0 -> /dev/pts/15
lrwx------ 1 mug896 mug896 64 2018-01-05 18:51 1 -> /dev/pts/15
lrwx------ 1 mug896 mug896 64 2018-01-05 18:51 2 -> /dev/pts/15
lrwx------ 1 mug896 mug896 64 2018-01-05 18:51 255 -> /dev/pts/15
lr-x------ 1 mug896 mug896 64 2018-01-05 18:51 3 -> /tmp/sh-thd.0bYLMF (deleted)
$ cat /dev/fd/3
111
222
$ cat /dev/fd/3
111
222
$ cat <&3
111
222
$ cat <&3
$ # <&3 는 파일 포지션이 이동하므로
$ exec 3>&-
-----------------------------------------
# 파이프로 인해 stdin 이 사용 중일 경우 다음과 같이 하면 됩니다.
$ command ... | sed -f /dev/fd/3 3<<EOF
...
EOF
$ command ... | awk -f /dev/fd/3 3<<\EOF
...
EOF
$ echo ' Robbins , "1234 Street, NE" , MyTown ,USA, USA ' | perl -n /dev/fd/3 3<<\@
chomp; my %seen;
print "<", $_, ">", "\n" foreach # 7. 추출한 값에 '<', '>' 를 추가해 출력
sort { $b cmp $a } # 6. 역순으로 정렬
map { s/([0-9]+)/$1 * 2/e; $_ } # 5. 숫자에는 * 2 를 실행
grep { ! $seen{$_}++ } # 4. 중복 항목은 제거
map { s/^\s+|\s+$//g; $_ } # 3. 항목 앞, 뒤 공백 제거
grep { /\S/ } # 2. empty 항목은 제거
split /,|("[^"]*")/; # 1. ',' or "..." 로 필드를 분리
@
<USA>
<Robbins>
<MyTown>
<"2468 Street, NE">
<< here document
sed, awk 같은 명령을 작성할 때는 quotes 을 사용하므로 동일한 quotes 을 출력에 사용하려면 escape 과정을 거쳐야 하는데요. here document 를 이용하면 파일에서 작성하는 것과 같이 quotes 을 escape 할 필요가 없습니다. 또한 본문 내용 중에 변수를 사용할 수도 있어서 여러모로 편리한 기능입니다.
here document 는 본문의 처음과 끝을 별도의 레이블로 구분하는데 이때 사용되는 구분자 명은 임의로 만들어 사용할 수 있습니다. 한가지 주의할 점은 마지막 줄은 구분자 외에 다른 문자가 와서는 안됩니다 ( 공백도 안됨 ). 구분자를 quote 하거나 escape 하면 변수확장, 명령치환이 일어나지 않습니다.
# quotes 을 escape 하지 않고 사용할 수 있어서 echo 명령 대신에 출력에 사용하기 좋습니다.
# 예) 명령의 help 메시지 작성
$ cat <<EOF
> this is "double quotes"
> this is 'single quotes'
> EOF
this is "double quotes"
this is 'single quotes'
# 기본적으로 본문에서 변수확장, 명령치환이 일어난다.
$ AA=100
$ cat <<ABC # 구분자 ABC
> here $AA
> document $(date +%D)
> ABC
here 100
document 07/23/15
# 구분자를 quote 하거나 escape 하면 변수확장, 명령치환이 발생하지 않습니다.
$ AA=100
$ cat <<'END' # 또는 <<\END
> here $AA
> document $(date +%D)
> END
here $AA
document $(date +%D)
# 마지막 줄은 구분자 외에 다른 문자가 오면 안됩니다.
$ cat <<@ # 구분자 @
> @@@
> @@
> @
@@@
@@
awk, sed 명령 사용예
# here document 는 stdin 에 연결되므로 '-f -' 옵션을 사용 ('-' 는 stdin 을 나타냄)
$ awk -f - <<\EOF
> BEGIN { print "''print single quotes easily''" }
> EOF
''print single quotes easily''
# single, double quotes 을 쉽게 출력할 수 있다.
# ( 파이프 로인해 stdin 이 사용 중이므로 -f /dev/fd/3 를 사용 )
$ echo "print quotes easily" | sed -f /dev/fd/3 3<<EOF
> s/.*/''&""/
> EOF
''print quotes easily""
$ echo "print quotes easily" | sed "$( cat <<EOF
> s/.*/''&""/
> EOF
> )"
''print quotes easily""
Heredoc 을 이용해 shell script 실행하기
Heredoc 으로 shell script 를 작성해 실행할 때 인수 전달이 필요할 경우 -s
옵션을 사용해야 합니다.
$ bash <<\EOF -s foo bar zoo $ bash <<\EOF foo bar zoo
echo $1, $2, $3 echo $1, $2, $3
echo $# echo $#
EOF EOF
foo, bar, zoo bash: foo: No such file or directory
3
-----------------------------
$ alias bashx='bash <<\@ -s'
$ bashx 11 22 33
> echo $0, $1, $2, $3
> echo $#
> @
bash, 11, 22, 33
3
소스코드 파일 없이 heredoc 을 이용해 컴파일 하기
# 명령행 상에서 첫번째 '-' 는 stdout 을( -o 옵션과 ), 두번째 '-' 는 stdin 을 나타냄
# gcc 는 파일 확장자를 통해 c, c++ 파일을 구분하는데 지금은 stdin 으로부터
# 입력을 받으므로 -x 옵션을 이용해 c 파일임을 명시해야 합니다. ( c++ 의 경우는 -xc++ )
$ gcc -S -xc -o - - <<\EOF
int a = 123;
int foo() {
int b = 234;
int c = a + b;
return c;
}
EOF
.file ""
.intel_syntax noprefix
.text
.globl a
. . . .
. . . .
Here document 를 파일로 쓰기
$ cat <<\END > file.conf # <<END 는 0<<END 와 같은 것임
here = 100
document = 200
END
$ <<\END cat > file.conf
here = 100
document = 200
END
$ cat > file.conf <<\END
here = 100
document = 200
END
Here document 를 파이프로 전달하기
$ sqlite3 mydatabase <<\@ | awk -F '\t' \
'
BEGIN { print "<table>" }
{ printf "<tr><td>%s</td><td>%s</td></tr>\n", $1, $2 }
END { print "</table>" }
'
.mode list
.separator \t
.header off
SELECT name,phone FROM person;
@
# 또는
$ sqlite3 mydatabase <<\@ | awk -f /dev/fd/3 3<<\@@ -F '\t'
.mode list
.separator \t
.header off
SELECT name,phone FROM person;
@
BEGIN { print "<table>" }
{ printf "<tr><td>%s</td><td>%s</td></tr>\n", $1, $2 }
END { print "</table>" }
@@
<table>
<tr><td>Jennifer Whalen</td><td>010-8129-4728</td></tr>
<tr><td>Donald OConnell</td><td>010-3512-4623</td></tr>
. . .
. . .
-------------------------------------------------------
$ cat <<\EOF | m4 $ m4 <<\EOF $ <<\EOF m4
define(foo, world)dnl define(foo, world)dnl define(foo, world)dnl
define(foo, m4) define(foo, m4) define(foo, m4)
hello world macro ! hello world macro ! hello world macro !
EOF EOF EOF
hello m4 macro ! hello m4 macro ! hello m4 macro !
&&
연산자로 명령 연결하기
$ ( cd `mktemp -d` && trap "rm -rf '$PWD'" 0 && gcc -xc -static - <<\@ \
&& docker build -f - <<\@@ . -t $$ && docker run --rm $_ && docker rmi $_ )
#include <stdio.h>
int main() {
puts ("\nhello heredoc !!!\n");
return 0;
}
@
FROM scratch
COPY a.out /
ENTRYPOINT ["/a.out"]
@@
############ 실행 결과 ###########
Sending build context to Docker daemon 868.4kB # docker build
Step 1/3 : FROM scratch
--->
Step 2/3 : COPY a.out /
---> bbb48d00ab4b
Step 3/3 : ENTRYPOINT ["/a.out"]
---> Running in 9bd94749f43a
Removing intermediate container 9bd94749f43a
---> a3b234463237
Successfully built a3b234463237
Successfully tagged 23837:latest
hello heredoc !!! # docker run
Untagged: 23837:latest # docker rmi
Deleted: sha256:a3b2344632374c716a5938e51aa6031771879f8f1215280f346b8d23cb2e8a1e
Deleted: sha256:bbb48d00ab4bb42134e47ef459c490c5ef5c02436d68a75220c33378b2700f3c
Deleted: sha256:d6e7c19c556bee448f900f3133a0723cf286282574d1056ed62feae5d0069065
Here document 를 이용한 변수값 설정
$ IFS= read -rd '' AA <<\@ || :
'here'
"document"
@
$ echo "$AA"
'here'
"document"
# 마지막에 newline 이 값에 포함됨
...................................
# 이것은 위 방법과 결과가 동일하지만 한가지 차이점은 명령치환이라 마지막에 newline 이 제거됩니다.
$ AA=$( cat <<EOF
'here'
"document"
EOF # 마지막 줄은 구분자 외에 다른 문자가 오면 안된다. (공백도 안됨)
)
$ echo "$AA"
'here'
"document" # 마지막에 newline 이 값에 포함되지 않음
............................................................
# 명령치환이 일어나야 하므로 END 구분자를 escape 하면 안됩니다.
$ IFS=, read AA BB CC <<END
$( awk 'BEGIN{ print "100,,*" }' ) # 두 번째 값은 empty
END
$ echo "$AA : $BB : $CC"
100 : : *
다음은 html 에서 <head>
태그 아래에 javascript 코드를 추가하기 위한 것인데요.
먼저 here document 에 코드를 붙여넣기 한 후 sed 명령 실행에 필요한 escape 처리를 하여 변수에 대입합니다.
좀 더 자세한 내용은 여기 를 참조하세요
GOOGLE_ANAL=$( cat <<\EOF | sed -z -e 's#\([&\#]\)#\\\1#g' -e 's#\n#\\n#g'
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-1234"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-1234');
</script>
EOF
)
# escape 처리가 완료된 변수값을 이용해 sed 명령 실행
sed -z -i 's#<head>#'"${GOOGLE_ANAL}"'#' *.html
현재 각 cpu core 에 바운드된 스레드 개수를 실시간으로 보기
# ps 명령에서 psr 은 현재 스레드가 바운드된 cpu core 를 말하고
# 뒤에 '=' 는 ps 명령이 타이틀을 표시하지 않게 합니다.
# 출력 결과가 실제 코어 수 보다 많게 나오는 이유는 Hyper-Threading 때문입니다.
$ watch -td -n1 "$( cat <<\EOF
ps axH -o psr= |
awk '{a[$1]++} END {
for (i in a) { printf "cpu%-6d",i }; print "";
for (i in a) { printf "%-9d", a[i] }; print "" }'
EOF
)"
cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7
289 232 269 198 260 214 279 215
sh -c '...'
또는 bash -c '...'
사용시 multi-line 명령 작성법
보통 sh -c '...'
형태로 명령을 실행할 때는 quote 을 하고 single-line 으로 작성하는데요.
here document 를 이용하면 multi-line 으로 명령을 작성할 수 있고
quotes 도 single, double quotes 모두 escape 없이 사용할 수 있습니다.
$ IFS= read -rd '' CMD <<\EOF || : $ bash -c "$( cat <<\EOF
AA=100 AA=100
echo -e "double quotes\n$AA" echo -e "double quotes\n$AA"
echo 'single quotes\n$AA' echo 'single quotes\n$AA'
cat <<\@ cat <<\@
'here' 'here'
"document" "document"
@ @
EOF EOF
)"
$ bash -c "$CMD"
double quotes double quotes
100 100
single quotes\n$AA single quotes\n$AA
'here' 'here'
"document" "document"
$ seq 10 | perl -lne 'BEGIN{ $total = 0; } $total += $_; END{ print "Total : $total"; }'
Total : 55
# shuf -i 1-10 | perl /dev/fd/3 3<<\@ 형식도 가능
$ shuf -i 1-10 | perl -e "$( cat <<\@
use List::Util qw/max min sum/;
push @arr, $_ while(<>);
printf "max : %u, min : %u, sum : %u\n", max(@arr), min(@arr), sum(@arr);
@
)"
max : 10, min : 1, sum : 55
------------------------------------------
# shuf -i 1-10 | python3 /dev/fd/3 3<<\@ 형식도 가능
$ shuf -i 1-10 | python3 -c "$( cat <<\@
arr = []
while 1:
try:
line = input()
arr.append(int(line))
except EOFError:
break
print(f"max : {max(arr)}, min : {min(arr)}, sum : {sum(arr)}")
@
)"
max : 10, min : 1, sum : 55
--------------------------------------------
$ shuf -i 1-10 | node -e "$( cat <<\@
process.stdin.setEncoding('utf8');
let arr = [];
let tmp = "";
process.stdin.on('data', (chunk) => {
const lines = chunk.split("\n");
lines[0] = tmp + lines[0];
tmp = lines.pop();
lines.forEach((line) => arr.push(parseInt(line)));
});
process.stdin.on('end', () => console.log("max : %d, min : %d, sum : %d",
arr.reduce((p, c) => p > c ? p : c),
arr.reduce((p, c) => p > c ? c : p),
arr.reduce((p, c) => p + c)));
@
)"
max : 10, min : 1, sum : 55
while 문의 입력으로 사용하기
$ while read -r line; do
> echo "$line"
> done <<END
> here
> document
> END
here
document
Leading tab 을 이용한 들여쓰기
here document 기호로 <<-
를 이용하면 내용을 작성할 때 leading tab 을 사용하여
들여 쓰기를 할 수 있습니다. 출력시에는 leading tab 이 제거되어 출력됩니다.
명령문 작성시 tab 문자 입력은
Ctrl-v
+tab
으로 할 수 있습니다.
$ if true; then
> cat <<EOF
> here
> document
> with tab
> EOF
> fi
here # 출력에 leading tab 이 포함됨
document
with tab
# '<<' 기호 대신에 '<<-' 를 사용
$ if true; then
> cat <<-EOF
> here
> document
> with tab
> EOF # 종료 구분자도 들여쓰기 가능
> fi
here # 출력은 leading tab 이 제거되어 출력됩니다.
document
with tab
중첩 사용
$ cat <<\EOF1 <<EOF2 $ FOO=$( cat <<\EOF1
'hello' cat <<\EOF2
"world" 'hello'
EOF1 "world"
EOF2
message start EOF1
$( < /dev/stdin ) )
message end
EOF2 $ echo "$FOO"
cat <<\EOF2
###### 실행 결과 ###### 'hello'
"world"
message start EOF2
'hello'
"world" $ eval "$FOO"
message end 'hello'
"world"
<<< here string
here string 은 <<<
우측에 하나의 인수만을 갖습니다.
페이지의 처음 그림에서 볼 수 있듯이 값이 임시 파일 형태로 전달되기 때문에
<<< $(command ...)
는 사실상 < /tmp/file
와 같게되고
이때 file 내용은 $(command ...)
의 확장 결과가 됩니다.
따라서 명령 치환이나 변수가 사용될 경우 따로 quotes 을 하지 않아도
포멧이 유지되고 globbing, 단어분리가 발생하지 않습니다.
# quote 하지 않아도 포멧이 유지된다.
$ cat <<< $( echo -e "hello\nhere string" )
hello
here string
# globbing 이 발생하지 않는다.
$ cat <<< *
*
$ while read -r file; do echo "$file"; done <<< $(find .)
.
./쉘 스크립트 테스팅.txt # 단어분리가 발생하지 않는다.
./2013-03-19 154412.csv
./ReadObject.class
./ReadObject.java
명령의 출력을 read 명령을 이용해 변수값에 설정하려고 할 때 파이프를 사용하면 subshell 로 인해 설정된 변수의 값을 유지할 수가 없습니다. 이때 here string 을 이용하면 현재 shell 에서 실행되어 문제를 해결할 수 있습니다.
# read 명령이 파이프 로인해 subshell 에서 실행되어 값이 표시되지 않는다
$ awk 'BEGIN { print "here string test" }' | read -r v1 v2 v3
$ echo "$v1 : $v2 : $v3"
: :
# '<<<' 는 현재 shell 에서 실행되어 정상적으로 값을출력.
$ read -r v1 v2 v3 <<< $( awk 'BEGIN{ print "here string test" }' )
$ echo "$v1 : $v2 : $v3"
here : string : test
# 변수값 중에 empty 값이 있을 경우 IFS 를 활용
$ IFS=, read -r AA BB CC <<< $( awk 'BEGIN{ print "100,,*" }' )
$ echo "$AA : $BB : $CC"
100 : : *
$ mapfile -t lines <<< `cat --help`
$ echo "${lines[0]}"
Usage: cat [OPTION]... [FILE]...
$ echo "${lines[1]}"
Concatenate FILE(s) to standard output.
만약에 작성중인 스크립트에 다음과 같은 코드가 있다면 here string 으로 바꿀수 있습니다.
$ echo "$SOME" | sed 's/foo/bar/' # 파이프 양쪽에 두개의 프로세스 생성
$ sed 's/foo/bar/' <<< $SOME # sed 하나의 프로세스만 생성
$ echo "$SOME" | head -n 10
$ <<< $SOME head -n 10
here string 은 마지막에 자동으로 newline 을 붙입니다.
$ od -a <<< '123'
0000000 1 2 3 nl
0000004
Quiz
다음 스크립트는 file.cpp 소스파일을 컴파일하면 watch 명령을 이용해 자동으로 objdump 결과를 출력해주는 함수 입니다. 그런데 실행을 해보면 정상적으로 동작하지 않는데요. 어떻게 수정해야 될까요?
$ watchdump () {
watch -t -n1 "$( cat <<\EOF
input=${2//\*/\\*}; input=${input//[/\\[}
objdump -dC "$1" | awk -F '\t' \
'NF == 1 { print; next } NF == 2 { next } { sub(/ +#[^#]*$/, ""); print $3 }' |
sed -n '/^[[:xdigit:]]\+ <\.\?'"$input"'>:$/,/^$/p'
EOF
)" ;}
# 사용방법: watchdump 실행파일|오브젝트파일 function-name
# 함수명은 objdump -dC file.o 출력에서 < > 괄호 안에있는 값.
$ watchdump test.o 'calc()'
오류
---------------------------------------------------------
1. watch 명령에 전달되는 awk, sed 명령문에서 single, double quotes 이 사용되고 있기 때문에
heredoc 을 이용해 작성하는 것은 좋은 방법이지만 구분자에 해당하는 EOF 를 escape 하여
watchdump 명령문에서 전달한 $1, $2 인수 값이 확장되지 않고 있습니다.
따라서 <<\EOF ---> <<EOF 로 변경해야 합니다.
2. <<EOF 로 변경함에 따라 awk 의 print $3 문에서 사용된 $3 스트링이 shell 변수로 인식됩니다.
따라서 print $3 ---> print \$3 로 escape 해야 합니다.
$/ 스트링은 shell 에서 변수로 사용되지 않으므로 escape 할 필요가 없습니다.
최종 수정본
$ watchdump () {
watch -t -n1 "$( cat <<EOF
input=${2//\*/\\*}; input=${input//[/\\[}
objdump -dC "$1" | awk -F '\t' \
'NF == 1 { print; next } NF == 2 { next } { sub(/ +#[^#]*$/, ""); print \$3 }' |
sed -n '/^[[:xdigit:]]\+ <\.\?'"$input"'>:$/,/^$/p'
EOF
)" ;}
다음은 watch 명령 대신에 inotifywait
명령을 이용한 버전입니다.
inotifywait 명령 라인에서 블록 상태에 있다가 인수로 전달한 test.o 파일이
변경되면 이벤트가 발생해서 블록이 해제됩니다.
명령을 사용하려면 inotify-tools 패키지를 설치해야 합니다.
# gcc 는 컴파일 시에 타겟파일을 삭제한 후에 다시 생성합니다.
# 따라서 inotifywait 에서 이벤트를 등록할때 attrib 를 사용해야 합니다.
watchdump () {
test -f "$1" || touch "$1"
local input=${2//\*/\\*}; input=${input//[/\\[}
while true; do
clear
objdump -dC "$1" | awk -F '\t' \
'NF == 1 { print; next } NF == 2 { next } { sub(/ +#[^#]*$/, ""); print $3 }' |
sed -n '/^[[:xdigit:]]\+ <\.\?'"$input"'>:$/,/^$/p'
inotifywait -qq -e attrib,modify,open "$1" || break
done
}
2 .
쉘 스크립트에 바이너리 파일을 저장하여 실행하려면 어떻게 할까요?
base64 를 이용하는 방법
base64 명령을 이용하면 binary 파일이 텍스트로 변환되므로 이 방법은 binary 파일을 전송할 수 없는 환경에서 이용할 수 있습니다.
1 . 먼저 base64 명령을 이용하여 binary 파일을 텍스트로 변환합니다.
$ base64 binaryCommand > test.sh
2 . test.sh 파일을 텍스트 에디터로 열어 보면 base64 로 인코딩된 데이터가 저장되어있습니다. 다음과 같이 수정하여 사용하면 됩니다.
#!/bin/bash
# 먼저 시스템이 바이너리 명령을 실행할 수 있는지 체크합니다.
[ "$(uname -mo)" = "x86_64 GNU/Linux" ] || { echo "binary can't execute"; exit 1 ;}
trap 'rm -f "$tmpFile"' EXIT
echo start...
tmpFile=`mktemp -p .`
base64 -d > "$tmpFile" <<\EOF
f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAsABAAAAAAABAAAAAAAAAAAABAAAAAAAAAAAAAEAAOAAC
AEAABAADAAEAAAAFAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAA2gAAAAAAAADaAAAAAAAAAAAA
IAAAAAAAAQAAAAYAAADaAAAAAAAAANoAYAAAAAAA2gBgAAAAAAAOAAAAAAAAAA4AAAAAAAAAAAAg
AAAAAABIx8ABAAAASMfHAQAAAEjHxtoAYABIx8IOAAAADwVIx8A8AAAASDH/DwVIZWxsbywgd29y
bGQhCgAuc2hzdHJ0YWIALnRleHQALmRhdGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAQAAAAYAAAAAAAAAsABAAAAA
AACwAAAAAAAAACoAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAEQAAAAEAAAADAAAAAAAA
ANoAYAAAAAAA2gAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAADAAAA
AAAAAAAAAAAAAAAAAAAAAOgAAAAAAAAAFwAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAA=
EOF
chmod +x "$tmpFile" && "$tmpFile"
echo end...
base64 명령은 바이너리 파일을 텍스트 파일로 변환해 주는 유틸입니다.
변환할때 6 bits 를 1 문자로 바꾸는데 이때 사용하는 문자셋은A-Z
,a-z
,0-9
,+
,/
총 64 문자 (2^6 = 64) 입니다 ( 이것이 명령 이름에 붙은 64 의미입니다 ). 예를 들어 3 bytes ( 3 * 8 = 24 bits ) 를 변환할 경우 결과적으로 4 개의 문자가 ( 24 / 6 = 4 ) 생겨서 4 bytes 가 되므로 실제 파일 보다 약 33% 정도 크기가 커집니다. base64 를 사용하지 않고 hex 로 바꾼다면 1 byte 당 2 문자가 필요하므로 파일 사이즈가 2 배가 되겠죠.
binary 파일을 직접 append 하는 방법
이 방법은 base64 인코딩을 거치지 않고 binary 파일을 직접 스크립트 파일에 append 합니다. binary 파일은 압축파일, 실행파일, 이미지, 동영상 어떤 종류던 상관없습니다. 인코딩에 의해 파일 사이즈가 커지지 않으므로 쉘 스크립트를 이용해 프로그램을 배포할때 주로 이 방법을 사용합니다.
1 . 먼저 append 할 binary 파일의 사이즈를 구합니다.
$ stat -c %s /usr/bin/date
104960
2 . 그다음 아래와 같은 내용의 스크립트 파일을 먼저 작성합니다.
이번에는 sh
로 작성하였습니다.
base64 를 이용하는 방법에서는 없었던 tail 명령이 사용된 것을 볼 수 있습니다.
이 부분이 스크립트 파일에 append 된 binary 파일을 추출하는 과정입니다.
그리고 마지막에는 반드시 exit 명령을 사용해서 종료해야 합니다.
그렇지 않으면 실행이 append 된 파일까지 넘어갑니다.
$ cat test.sh
#!/bin/sh -e
[ "$(uname -mo)" = "x86_64 GNU/Linux" ] || { echo "binary can't execute"; exit 1 ;}
trap 'exit' HUP INT QUIT TERM
trap 'rm -f "$tmpFile"' EXIT
scriptFile=`readlink -f "$0"`
tmpFile=`mktemp -p "$PWD"`
# tail 명령으로 append 된 binary file 을 추출
tail -c 104960 "$scriptFile" > "$tmpFile"
# 만약에 append 된 파일이 tar 압축 파일이라면
# cd `mktemp -d -p "$PWD"` && tail -c 104960 "$scriptFile" | tar xvz
chmod +x "$tmpFile" && "$tmpFile"
# 마지막은 반드시 exit 명령으로 종료
exit
3 . 스크립트 파일 작성을 완료하였으면 저장하고 나와서 마지막으로 binary 파일을 append 합니다.
$ cat /usr/bin/date >> test.sh
$ ./test.sh # 스크립트를 실행하면 append 한 파일이 실행된다.
Wed Jun 15 00:17:58 KST 2022
한가지 참고할 사항은 파일을 append 하고 나서 스크립트 파일을 다시 수정해야될 경우가 생길수 있는데 이때 vi 에디터로 수정하여 저장하면 파일의 마지막 부분에 자동으로 newline 이 붙어서 문제가 됩니다. 그러므로 파일을 다시 수정할 때나 아니면 이미 파일의 마지막에 newline 이 포함되었다면 다음과 같은 방법으로 종료하여 newline 을 제거할수 있습니다.
# vi 로 스크립트 수정이 완료되었으면 다음과 같이 종료해야 합니다.
: set binary noeol
: wq
Perl 에서의 __DATA__
토큰
Perl 에서는 __DATA__
토큰을 이용하면 스크립트 내에서 <DATA>
스트림을 사용할 수
있기 때문에 파일을 append 해서 사용하는 기능을 쉽게 이용할 수 있습니다.
따로 append 할 파일의 사이즈를 구하거나, tail 명령을 이용할 필요가 없습니다.
다음은 test.pl
스크립트 파일에 /bin/z*
실행파일들을 tar 압축하여 append 한다음
스크립트 실행시 <DATA>
스트림을 이용해 tmpdir 에 압축 해제하는 예입니다.
__DATA__
토큰은 스크립트 파일 마지막에 위치해야 하고 이후에 데이터가 존재하면 안됩니다.
$ cat test.pl
#!/usr/bin/perl
my $tmpdir = `mktemp -d -p "$ENV{PWD}"` or die; # 임시 디렉토리 생성.
chomp $tmpdir;
open my $fh, "| tar xvz -C '$tmpdir'" or die $!; # <DATA> 로부터 데이터를 읽어들여
binmode $fh; # 파이프를 통해 tar 명령으로 전달
print $fh $_ while (<DATA>);
close $fh;
system "ls '$tmpdir/bin/'";
__DATA__
$ tar cz /bin/z* >> test.pl # /bin/z* 파일들을 tar 압축하여 test.pl 파일에 append.
$ ./test.pl # 스크립트를 실행하면 압축 해제되는 것을 볼 수 있다.
. . .
bin/zsh5
bin/zstd
bin/zstdcat
bin/zstdgrep
bin/zstdless
bin/zstdmt
zcat zdump zfgrep zip zipgrep zipsplit zmore zsh zstdcat zstdmt
zcmp zegrep zforce zipcloak zipinfo zjsdecode znew zsh5 zstdgrep
zdiff zenity zgrep zipdetails zipnote zless zrun zstd zstdless