Command History
터미널을 열었을때 실행되는 interactive shell 에서는 명령 history 기능을 사용할 수 있습니다. 명령 history 는 이전에 한번 사용했던 명령을 다시 타입 할 필요 없이 재사용할 수 있게 해줍니다. 터미널 별로 history list 가 생성되며 사용한 명령들이 목록에 추가됩니다. 터미널 종료 시에는 shopt -s histappend
옵션 설정에 따라 $HISTFILE 에 현재 history list 가 저장됩니다.
history 확장에 사용되는 문자는 !
인데
명령 행상 어느 위치에서든지 !
문자에 이어 공백 없이 다른 문자나 숫자가 오면
history 확장이 됩니다.
심지어 double quotes 안에서도 확장이 일어나므로 주의해야 합니다.
한가지 예외적인 경우는 !=
인데 [
명령에서 연산자로 사용되므로 history 확장에서 제외됩니다.
!
문자가 명령이름의 위치에 오고 뒤이어 공백이 올경우는 shell keyword 로 인식되어 logical NOT 의 기능을 합니다.
$ find ! -size 0 # ! 문자 뒤에 공백이 오므로 OK
$ find !-size 0 # 이것은 history 확장 대상으로 명령이 정상적으로 실행되지 않는다.
bash: !-size: event not found
$ ! test -s emptyfile # logical NOT 쉘 키워드 (! 문자가 명령 위치에 오고 이어 공백이 오므로)
Command history 확장은 shell prompt 상에서만 작동하는 기능입니다.
그러므로 Non-interactive shell 인 스크립트 실행시에는 기본적으로 disable 됩니다.
명령 라인을 찾는 방법
프롬프트에서 history
명령을 실행하면 현재 history list 에 있는 목록들이 번호와 함께 표시됩니다. 이 번호는 해당 명령 라인을 지정할때 사용됩니다.
!n
n 번 명령을 리턴합니다.
$ !2145
lsb_release -d
Description: Ubuntu 15.04
$ echo command history : !2145
command history : lsb_release -d
!!
바로 이전 명령을 나타냅니다.
$ history
...
3 ps f
4 cat README.md
5 find * -name '*.log' -size +10M -exec rm -f {} \;
$ !!
find * -name '*.log' -size +10M -exec rm -f {} \;
!-n
이전 n 번째 명령을 나타냅니다.
$ history
...
3 date
4 ps f
5 find * -name '*.log'
$ !-1
find * -name '*.log'
$ !-2
ps f
$ !-3
date
!string
명령 이름이 string 으로 시작하는 가장 최근 명령을 찾습니다.
$ history
...
3 find * -name '*.log' -size +10M -exec rm -f {} \;
4 find * -name '*.log'
5 ps f
$ !fi
find * -name '*.log'
!?string[?]
이건 명령이름을 검색하는게 아니고 전체 명령라인 중에 string 이 포함돼 있는지를 찾습니다. 가장 최근에 매칭이 되는 라인을 리턴합니다.
$ history
...
3 find * -name "*.tmp" -o -name "*.old"
4 find * -name "*.tmp"
5 find * -name "*.log"
$ !?tmp
find * -name "*.tmp"
# 뒤에 '?' 를 붙이면 연이어 명령을 작성할 수 있다.
$ !?tmp? -exec rm -f {} \;
find * -name "*.tmp" -exec rm -f {} \;
# 뒤에 '?' 를 안붙일 경우 오류
$ !?tmp -exec rm -f {} \;
bash: !?tmp -exec rm -f {} \;: event not found
^old^new
이전 명령에서 처음에 매칭되는 하나만 변경됩니다. !!:s/string1/string2/
와 같습니다.
$ mkdir -p test/exp/scenario/
$ ^exp^lab
$ mkdir -p test/lab/scenario/
!#
이것은 이전 명령이 아니라 현재 프롬프트 상에서 작성 중인 명령을 나타냅니다.
$ echo 111 222 333 !#:1 # 첫번째 인수
$ echo 111 222 333 111
$ echo 111 222 333 !#:2 # 두번째 인수
$ echo 111 222 333 222
$ mv long/path/name/oldname !#$ # 마지막 인수
$ mv long/path/name/oldname long/path/name/oldname
찾은 명령 라인에서 원하는 인수를 지정하는 방법
명령 라인을 지정한 후에 :
문자를 붙인 후 원하는 인수들을 지정할 수 있습니다.
0 번은 명령을 나타내고 이후 인수들은 1, 2, 3 ... 번으로 지정할 수 있습니다.
$ touch home foo bar tmp log
$ !tou:0 # 0 번은 명령
touch
$ !tou:1 # 1 번은 첫번째 인수
home
$ !tou:2 # 2 번은 두번째 인수
foo
$ !tou:2-4 # '-' 를 이용해 범위를 지정
foo bar tmp
$ !tou:* # '*' 는 모든 인수를 나타냅니다.
home foo bar tmp log
$ !tou:3* # '3*' 은 3 번째 인수부터 끝까지
bar tmp log
$ !tou:^ # '^' 는 첫번째 인수를 나타내며 !tou:1 와 같습니다.
home
$ !tou:$ # '$' 는 마지막 인수를 나타냅니다.
log
명령라인 지정을 생략하면 바로 이전 명령이 사용됩니다.
$ echo 11 22 33 44
11 22 33 44
$ !:0
echo
$ !:1
11
$ !:2-4
22 33 44
$ !*
11 22 33 44
$ !^
11
$ !$
44
지정한 라인, 인수에 modifiers 적용하기
라인을 지정한 뒤, 또는 인수들을 지정한 뒤에 :
문자를 붙인 후 modifiers 를 적용시킬 수 있습니다.
s/old/new/
지정한 명령라인에서 old 에 해당하는 스트링을 new 로 변경합니다. new 부분에 &
문자가 오면 old 로 대체됩니다. 기본적으로 처음에 매칭되는 하나만 적용되며 모두에 적용하려면 g
옵션을 추가합니다.
$ mv foo.htm foo.html
# 처음 하나만 변경된다
$ !:s/foo/bar
mv bar.htm foo.html
# 'g' 옵션을 추가하면 모두 변경된다.
$ !:gs/foo/bar
mv bar.htm bar.html
파일 경로명에서 파일명 분류하기
$ cat /home/foo/readme.txt
$ !:h # 'h' 는 head 를 의미
cat /home/foo # cat 명령이 함께 포함됨
$ !:t # 't' 는 tail 을 의미
readme.txt
$ !:h/AA # 연이어 명령을 작성할 수 있다.
cat /home/foo/AA
$ !:1:h # ':1' 인수에서 head 를 구함
/home/foo
파일 경로명에서 확장자 분류하기
$ cat /home/foo/readme.txt
$ !:r # 확장자를 remove
cat /home/foo/readme
$ !:r.old
cat /home/foo/readme.old
$ !:e # 확장자 (extension) 만 구함.
.txt
결과물 quoting 하기
$ echo foo bar tmp
$ !:2-3:q
'bar tmp'
$ !:q # 전체 라인이 quote 된다.
'echo foo bar tmp'
$ !:*:x # space 로 분리되어 각각 quote 된다.
'foo' 'bar' 'tmp'
실행 금지하기
history 확장이 되면 바로 결과물이 실행되는데 p
옵션을 붙이면 결과만 표시하고 실행을 금지할 수 있습니다.
$ mv foo bar
$ !mv:s/foo/boo/:p # 결과만 프린트되고 실행은 되지 않는다
mv boo bar
Double quotes 과 history 확장
History 확장은 double quotes 내에서도 일어나므로 주의해야 합니다.
$ lsb_relase -d
Description: Ubuntu 15.04
$ echo command history : !lsb
command history : lsb_release -d
$ echo "hello!lsb world" # double quotes 에서도 history 확장이 된다.
hellolsb_relase -d world"
$ echo "hello!516world"
hellolsb_release -dworld
$ echo 'hello!516world' # single quotes 에서는 확장이 안된다.
hello!516world
Double quotes 사용시 다음과 같이 history 확장을 회피할 수 있습니다.
$ echo "hello"\!"lsb world"
hello!lsb world
$ edho "hello"'!'"516world"
hello!516world
# sed 명령으로 "2016 12-10" 와 매칭되지 않는 라인을 삭제하려고 하지만 !d 에서
# history 확장이 되어 정상적으로 실행되지 않는다.
$ search="2016 12-10"
$ sed "/$search/!d" file.txt
ERR
$ sed "/$search/"'!d' file.txt
History 관련 환경 변수
HISTIGNORE
history 리스트에 저장할때 제외시킬 명령패턴을
:
로 분리하여 등록합니다.HISTIGNORE='ls:ls -al:cd:bg:fg:history'
HISTFILESIZE
history 파일에 저장될 최대 라인수를 나타냅니다.
HISTSIZE
history 리스트에 기억될 최대 명령수를 나타냅니다. 디폴트 값은 500 입니다.
HISTFILE
history 를 저장할 파일을 지정합니다.
HISTCONTROL
명령 history 의 작동방식을
:
로 분리하여 설정할수 있습니다.- ignorespace : space 로 시작하는 명령라인을 history 에 저장하지 않습니다.
- ignoredups : 이전 history 명령과 중복될경우 저장하지 않습니다.
- ignoreboth : ignorespace:ignoredups 와 같습니다.
- erasedups : 이전 모든 history 라인을 비교하여 중복된 history 를 제거한후 저장합니다.
HISTTIMEFORMAT
history 번호에 이어 timestamp 를 붙일 수 있습니다. history file 에도 저장됩니다.
예) export HISTTIMEFORMAT="%F %T "
History 관련 옵션
Set
history
명령 history 기능을 enable, disable 할 수 있습니다.
-H | histexpand
!
문자를 이용한 history 확장 기능을 제공합니다.set -o history
이 설정돼있어야 사용할 수 있습니다.
Shopt
cmdhist
multiple-line 명령을 작성할 경우 명령 줄들이 각각 다른 history 번호로 할당돼서 다음에 재사용하기가 어려운데 이 옵션을 사용하면 newline 을
;
로 치환해서 저장해 줍니다.lithist
옵션과 같이 사용하면 newline 도 그대로 유지됩니다.
lithist
multiple-line 명령을 작성할 경우 newline 을 유지해 줍니다.
histreedit
history 확장이 실패할 경우 입력했던 내용이 없어지지 않고 다시 수정할수 있는 기회를 줍니다.
histverify
history 확장된 명령을 바로 실행하지 않고 필요시 수정할 수 있게 enter 를 입력할 기회를 줍니다.
histappend
shell 을 exit 할때
HISTFILE
변수에 설정돼 있는 파일에 현재 history list 를 append 합니다. off 이면 overwrite 합니다.터미널이 비정상적으로 종료할 경우 history list 가 저장되지 않습니다. 그럴 경우를 위해
PROMPT_COMMAND='history -a'
를 설정해 사용할 수 있습니다.
History builtin 명령
history [-c] [-d offset] [n] or history -anrw [filename] or history -ps arg [arg...]
현재 세션의 history list 를 관리하며 history file 을 read 하거나 write 해서 여러 터미널 세션 간에 history 를 동기화할 수 있습니다.
옵션 | 설명 |
---|---|
-c | 현재 세션의 history list 를 모두 삭제합니다. |
-d offset | offset 위치의 항목을 삭제합니다. |
-r | history file 을 읽어들이고 내용을 현재 세션의 history list 에 append 합니다 |
-n | history file 에서 아직 읽어 들이지 않은 항목이 있으면 모두 읽어 들입니다. |
-a | 현재 세션의 history list 를 history file 에 append 합니다. |
-w | 현재 세션의 history list 를 history file 에 write 합니다. |
Quiz
피보나치 수열을 D[x] = D[x - 1] + D[x - 2]
점화식을 이용해 구할때 x
값이 45 를 넘어서면
속도가 많이 느려지는데 왜 그런지 wc
와 sort
, uniq
명령을 이용해 알아보고
dynamic programming 을 이용해서 문제를 해결하는것 입니다.
다이나믹 프로그래밍1: https://blog.naver.com/ndb796/221233570962
다이나믹 프로그래밍2: https://galid1.tistory.com/507
$ cat fibo.c
#include <stdio.h>
#include <stdlib.h>
long fibo(int x)
{
if (x == 1) return 1;
if (x == 2) return 1;
return fibo(x - 1) + fibo(x - 2);
}
int main(int argc, char *argv[])
{
printf("%ld\n", fibo(atoi(argv[1])));
}
$ gcc fibo.c
$ time ./a.out 10 $ time ./a.out 45 # x 값이 45 이면
55 1134903170
real 0m0.003s real 0m7.808s # 7 초가 걸린다!
user 0m0.000s user 0m7.802s
sys 0m0.003s sys 0m0.005s
이번에는 fibo 함수에 다음과 같이 printf
함수를 추가해서 출력값을 wc -l
명령으로
카운트 해보면 fibo 함수 호출이 총 1664080 번이나 발생한것을 알 수 있습니다.
또한 sort 명령으로 정렬해서 uniq 명령으로 각 x 값 별로 출현횟수를 출력해보면
./a.out 30
실행에서 x 값이 2 일때 fibo 함수 호출이 514229 번 발생한것을 볼 수 있습니다.
이것은 다시 말해서 이미 연산이 완료된 값이 514228 번 다시 계산된다는 뜻이므로
속도가 느려질 수밖에 없습니다.
long fibo(int x)
{
printf("%d\n", x);
if (x == 1) return 1;
if (x == 2) return 1;
return fibo(x - 1) + fibo(x - 2);
}
$ gcc fibo.c
$ ./a.out 30 | wc -l # fibo 함수 호출이 총 1664080 번 발생
1664080
$ ./a.out 30 | sort -n | uniq -c
317811 1
514229 2 # x 값이 2 일때 fibo 함수 호출이 514229 번 발생
317811 3
196418 4 # x 값이 4 일때 fibo 함수 호출이 196418 번 발생
121393 5
75025 6 # sort 명령에서 -n 옵션은 입력값을 숫자로 취급해서 정렬합니다.
. . . # uniq 명령에서 -c 옵션은 같은값이 연이어 나타날경우 출현횟수를
13 24 # 왼쪽에 표시해 줍니다.
8 25
5 26
3 27
2 28
1 29
1 30
1 832040
이번에는 dynamic programming 을 이용해서 연산 결과를 res 배열에 저장해서 사용하면 ( memoization ) 전체 fibo 함수 호출이 58 회로 줄어들고 각 x 값 별로 fibo 함수가 호출된 횟수도 2 회를 넘지 않는것을 볼 수가 있습니다.
#include <stdio.h>
#include <stdlib.h>
long res[100] = {0}; # res 배열을 0 으로 초기화
long fibo(int x)
{
printf("%d\n", x);
if (x == 1) return 1;
if (x == 2) return 1;
if (res[x] != 0) return res[x]; # res 배열에 저장값이 있을경우 반환
res[x] = fibo(x - 1) + fibo(x - 2); # 그렇지 않으면 res 배열에 연산결과를 저장
return res[x];
}
int main(int argc, char *argv[])
{
printf("%ld\n", fibo(atoi(argv[1])));
}
----------------------------------------
$ gcc fibo.c
$ ./a.out 30 | wc -l # fibo 함수 호출이 총 58 번 발생
58
$ ./a.out 30 | sort -n | uniq -c
1 1
2 2
2 3 # 각 x 값 별로 fibo 함수가 호출된 횟수도 2 회를 넘지 않는다.
2 4
. . .
2 27
2 28
1 29
1 30
1 832040
다음은 gcc 에서 제공하는 __int128
타입을 이용해 128 bits 값까지 출력해 봅니다.
$ cat fibo64.c $ cat fibo128.c
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
long res[200] = {0}; __int128 res[200] = {0};
long fibo(int x) void print(__int128 x)
{ {
if (x == 1) return 1; char num[50] = {0};
if (x == 2) return 1; int i = 0;
if (res[x] != 0) return res[x]; void sub(__int128 x) {
res[x] = fibo(x - 1) + fibo(x - 2); if (x < 0) {
printf("%ld\n", res[x]); num[i++] = '-';
return res[x]; x = -x;
} }
if (x > 9) sub(x / 10);
int main(int argc, char *argv[]) num[i++] = x % 10 + '0';
{ }
fibo(atoi(argv[1])); sub(x); puts(num);
} }
__int128 fibo(int x)
{
if (x == 1) return 1;
if (x == 2) return 1;
if (res[x] != 0) return res[x];
res[x] = fibo(x - 1) + fibo(x - 2);
print(res[x]);
return res[x];
}
int main(int argc, char *argv[])
{
fibo(atoi(argv[1]));
}
----------------------------------------------------------------------------------------
# 64bits 는 마지막 출력값들이 정확하지 않다.
$ ./a.out64 100 $ ./a.out128 100
. . . . . .
2880067194370816120 2880067194370816120
4660046610375530309 4660046610375530309
7540113804746346429 7540113804746346429
-6246583658587674878 12200160415121876738
1293530146158671551 19740274219868223167
-4953053512429003327 31940434634990099905
-3659523366270331776 51680708854858323072
-8612576878699335103 83621143489848422977
6174643828739884737 135301852344706746049
-2437933049959450366 218922995834555169026
3736710778780434371 354224848179261915075
fibo128.c 파일에서
void sub(__int128 x)
nested function 은 gcc 에서만 가능합니다.
2 .
recursion 을 이용해 코드를 작성하면 자동으로 stack 을 사용하는 것과 같게 되기 때문에( 함수 호출로 인해 ) 코드가 간결해지는 장점이 있지만 처리해야될 데이터가 많을 경우 stack overflow 가 발생할 수 있습니다. 이때는 recursion 대신에 반복문을 이용해 작성을 해야 하는데요 ( recursion 을 이용한 코드는 모두 반복문으로 만들 수 있습니다 ). 위의 fibo 함수를 반복문을 이용해 다시 작성해 보는 것입니다.
#include <stdio.h>
#include <stdlib.h>
long fibo(int arg)
{
if (arg == 1)
return 1;
else {
long num1 = 0, num2 = 1, res = 0;
for (int i = 2; i <= arg; i++) {
res = num1 + num2;
num1 = num2;
num2 = res;
}
return res;
}
}
int main(int argc, char *argv[])
{
printf("%ld\n", fibo(atoi(argv[1])));
}
다음은 trampoline style 로 작성한 코드 입니다.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
long num1, num2, res;
int cnt, arg;
} param_t;
typedef struct data {
void (*callback)(struct data*);
void *param;
} data_t;
void trampoline(data_t *data)
{
while (data->callback != NULL)
data->callback(data);
}
void thunk(data_t *data)
{
param_t *param = data->param;
if (param->cnt > param->arg) {
data->callback = NULL;
}
else {
param->res = param->num1 + param->num2;
param->num1 = param->num2;
param->num2 = param->res;
param->cnt++;
}
}
int main(int argc, char *argv[])
{
int arg = atoi(argv[1]);
param_t params = { .num1 = 0, .num2 = 1, .res = 0, .cnt = 2, .arg = arg };
data_t data = { &thunk, ¶ms };
trampoline( &data );
printf("%ld\n", arg == 1 ? 1 : params.res);
return 0;
}
3 .
프로그래밍 언어중 최초로 recursion 이 가능했던 언어는 무었일까요?
거의 동시대에 LISP 과 ALGOL 언어가 있습니다. LISP 은 언어 자체의 design goals 중에 하나가 recursion 을 제공하는 것이었고 ALGOL 은 절차형 언어로는 최초로 recursion 이 가능한 언어였다고 합니다.