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 를 넘어서면 속도가 많이 느려지는데 왜 그런지 wcsort, 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, &params };
    trampoline( &data );
    printf("%ld\n", arg == 1 ? 1 : params.res);
    return 0;
}

3 .

프로그래밍 언어중 최초로 recursion 이 가능했던 언어는 무었일까요?

거의 동시대에 LISP 과 ALGOL 언어가 있습니다. LISP 은 언어 자체의 design goals 중에 하나가 recursion 을 제공하는 것이었고 ALGOL 은 절차형 언어로는 최초로 recursion 이 가능한 언어였다고 합니다.