반응형

    정보

    • 업무명     : 쉘 스크립트 (bash) 개발자가 빠지기 쉬운 함정 (1)

    • 작성자     : 박진만

    • 작성일     : 2020-03-21

    • 설   명      :

    • 수정이력 :

     

     요약

    [특징]

    • 쉘 스크립트 개발자가 빠지기 쉬운 함정을 소개.

     

    [활용 자료]

    • 없음

     

    [자료 처리 방안 및 활용 분석 기법]

    • 없음

     

    [사용 언어]

    • Bash Script

     

     내용

    • 이 페이지는 Bash 프로그래머가 흔히 발생하는 오류에 대해 요약하였습니다.

    • 아래의 모든 코드의 예시는 어떤 결함을 가지고 있습니다.

    • 들어가기에 앞서 기본적으로 따옴표 (“)를 항상 사용하고, 또 절대로 단어 분할을 사용하지 않는다면 대부분의 함정에 빠지지 않을 수 있습니다.

    • 따옴표를 사용하지 않았을 때의 단어 분할이 되는 이유는 기본적으로 켜져 있는, Bourne쉘에서 상속받은 레거시 코드의 설계 오류 때문입니다.

    • 따라서 하단에서 소개할 결함의 대부분은 이 따옴표와 단어 분할에 관련된 문제가 대부분입니다.

     

    1. for i in $(ls *.mp3)

    • BASH 프로그래머들이 루프를 작성할 때 가장 범한 쉬운 일반적인 실수는 아래와 같은 코드들을 들 수 있습니다.

    for i in $(ls *.mp3); do    # 잘못된 코드
        some command $i     # 잘못된 코드
    done
    
    for i in $(ls)              # 잘못된 코드
    for i in `ls`               # 잘못된 코드
    
    for i in $(find . -type f)  # 잘못된 코드
    for i in `find . -type f`   # 잘못된 코드
    
    files=($(find . -type f))   # 잘못된 코드
    for i in ${files[@]}        # 잘못된 코드

     

    • 즉 명령 치환의 경우 어떤 종류의 것도 따옴표없이 사용해서는 안됩니다.

    • 여기에는 두 가지 문제가 있습니다.

      • 첫 번째는 따옴표 없이 변수의 출력을 인수로 split하여 사용하는 것,

      • 그리고 ls 출력을 해석하는 방법입니다.

      • 위의 방식대로 ls를 출력하면 결코 출력 결과가 나오지 않을 것입니다.

    • 왜냐하면 이는 파일 이름에 공백이 포함되어있기 때문에 나타나는 이름의 분리 때문입니다.

    • 또 다른 이유로는 $(ls *.mp3)명령 치환의 출력 결과물이 분리되기 때문입니다.

      • 가령 “01 - Don't Eat the Yellow Snow.mp3” 라는 파일이 현재 디렉토리에 있다고 가정해봅시다.

      • for 루프는 파일 이름을 구분 한 결과 각 단어를 01, -, Don't, Eat... 와 같이 반복하게 될 것입니다.

    • 있을 수 있는 더욱 더욱 나쁜 상황은 이전 단어를 split 해 나가고 얻어진 문자열로 인하여 경로명 확장이 발생할 때입니다.

    • 예를 들어, ls가 *포함 문자 출력을 발생시키고, 그것을 포함한 캐릭터가 패턴으로 인식되고, 그것과 일치하는 모든 파일 이름이 목록으로 대체되는 경우입니다.

    • 또한 큰 따옴표 (")를 대체 하는 것 역시 불가능합니다.

    for i in "$(ls *.mp3)"; do # Wrong!

     

    • 위의 코드는 ls전체 출력을 한 단어로 취급하게 되어 버립니다.

    • 각 파일의 이름을 반복하는 대신 루프는 모든 파일 이름을 붙인 문자열 i로 단 한 번만 실행될 것입니다.

    • 상기한 ls 명령의 사용법이 적절하지 않다는 것을 분명해 보입니다.

    • 다시말해, 이 외부 명령 (역자주 : ls명령)의 출력은 사람이 읽을 것을 특히 상정하고 있으며, 특별히 스크립트에서 해석하는 방법을 상정하고 있는 것 같지 않아 보입니다. 그렇다면 올바른 사용법은 무엇일까요?

    for i in *.mp3; do    # Better! and...
        some command "$i" # ...always double-quote expansions!
    done
    

     

    • Bash 같은 쉘은 기본적으로 glob 기능을 사용할 수 있으며, 즉 쉘 자체에서 파일 이름 매칭 목록에 패턴을 전개하는 것을 가능하게하기 위한 방법이 이미 구현되어 있습니다.

    • 즉 외부 명령어의 결과를 해석 할 필요가 없습니다.

    • 대신 쉘의 기본 기능인 glob를 이용하여 *.mp3를 직접 적어주는 것만으로 패턴 매치가 제대로 작동되며, 이는 따옴표 없이도 단어 분할의 영향을 받지 않게 됩니다.

    • 만약 파일을 재귀적으로 처리 할 필요가 있다면, find 를 사용하는 것도 하나의 방법입니다.

    • 여기서 문제가 있습니다.

    • 만약 *.mp3-files이 현재 디렉토리에 존재하지 않는다면 어떻게 될까요? 그 경우 i="*.mp3"라는 것이 i에 삽입 된 상태에서 for루프는 한번만 실행되게 됩니다.즉 일치하는 파일이 없다는 빈 공백만이 한번만 출력될 것입니다.

    # POSIX
    for i in *.mp3; do
        [ -e "$i" ] || continue
        some command "$i"
    done

     

    • 단순히 따옴표를 사용하고, 단어분할을 사용하지 않으면 이러한 일반적인 실수를 방지 할 수 있을 것입니다.

    • 즉 오류의 거의 대부분은 따옴표 없이 코드를 작성하는 것과 관련이 있고, 이어서 단어 분할, 그리고 glob 옵션과 관련이 있습니다.

    • 위 loop 본문 $i주위의 따옴표에 주목하십시오. 이것은 두 번째 함정과 밀접한 관련이 있습니다.

     

    2. cp $file $target

    • 이 위에 표시되는 명령 중 무엇이 잘못일까요?

    • 당신은 $file 그리고 $target이 확장 혹은 와일드 카드를 모두 가지고 있지 않다는 사실을 먼저 주목하여야 합니다.

    • 그러나 이러한 경우에도 불구하고 파일 이름의 확장 결과는 분할될 것이며 이는 경로명이 확장되는 결과를 낳게 될 것입니다.

    • 그러나 따옴표가 처리된 매개 변수는 반드시 파일 전체의 이름을 포함하게 될 것입니다.

    cp -- "$file" "$target"

     

    • 만약 큰 따옴표 없이 cp 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb 같은 명령을 수행하게 되는 경우 아래와 같은 에러 메세지가 반환 될 것입니다.

    cannot stat `01': No such file or directory

     

    • $file이 그 안에 와일드 카드 ( *, ?, [) 를 포함하고 있으며, 거기에 그것들과 일치하는 파일이 있었다면 이것들은 확장 될 것입니다.

    • 즉 - 부터 시작하는 $file 내용이 있는 경우에는 큰 따옴표가 있어도 당신이 cp 명령 줄 옵션을 주었다고 생각하게 됩니다 (아래의 함정 3을 참고)

    • 특히 변수가 여러 파일 이름을 포함하는 경우에는 위와 같은 예시가 매개 변수 확장의 인용에서 평범한 모범 사례라 할 수 있을 것입니다.

    • 설령 어떤 일반적이지 않은 상황에서도 변수의 내용이 문제가 없음을 보장 할 수 있습니다.

    • 결론적으로 경험을 쌓은 스크립트 작성자는 코드의 문맥 상 분명히 파라미터의 내용이 안전한 것이 보증되어있는 몇 안되는 경우를 제외하고는 항상 따옴표를 사용합니다.

    • 상급자의 경우 제목의 cp명령이 잘못되었음을 바로 간파 할 수 있을 것입니다.

     

    3. 대시 (-) 로 시작하는 파일 이름

    • 대시 (-)로 시작되는 파일명은 다양한 문제를 일으킵니다.

    • *.mp3 등의 와일드카드로 확장되어 파일 이름이 정렬되는경우, 대시 (-)는 많은 로케일에서 문자 앞 부분에 정렬됩니다.

    • 즉 이와 같은 경우 -filename이라는 것을 옵션으로 잘못 해석 하게 될 수 있습니다. 이에 대해서는 두 가지 해결책이 있습니다.

    • 첫 번째 해결책은 --명령 ( cp같은)을 인수 사이에 넣는 것입니다. 이는 단순하지만 좋은 방법입니다.

    cp -- "$file" "$target"

     

    • 그러나 이 방법은 잠재적인 문제가 있습니다.

      • 옵션으로 해석 될 수 있는 상황에서 모든 파라미터의 사용 시작에 --가 삽입 된 것을 매번 확인해야 할 필요가 있고, --를 남용하게 되는 경우 명령 자체가 매우 복잡해지게 될 가능성이 있기 때문입니다.

    • 대부분의 자주 쓰는 옵션을 파싱하는 라이브러리는 이를 이해하는 동시에 프로그램은 이러한 기능을 제대로 취급 무료로 이 기능을 계승하고 있는 것입니다.

    • 그러나 이 옵션의 끝을 인식하는 것은 결국 애플리케이션에 의존하게 됩니다.

    •  표준 유틸리티에 대해서도 POSIX에 의해 기술 된 몇 가지 예외가 있을 것입니다. echo가 단적인 하나의 예입니다.

    • 다른 방법은 상대경로나 절대경로를 사용하여 파일 이름이 디렉토리로부터 시작되었는지 확인하는 방법입니다.

    for i in ./*.mp3; do
        cp "$i" /target
        ...
    done
    • 이 경우 파일 이름이 -로 시작하더라도 해당 정렬의 경우 변수의 값이 ./-foo.mp3 를 포함하고 있는지 확인하게 됩니다. 이것은 cp명령과 관련된 것에서는 완벽하게 안전하다고 볼 수 있습니다.

    • 결국 모든 결과가 같은 접두사를 가지도록 만들게 된다면, loop본문에서 몇 번 변수를 사용해서 간단하게 접두어를 결합 할 수 있을 것입니다.

    • 이는 생성 된 각 단어에 대한 몇 가지 여분의 문자를 저장할 때 이론적으로 코드를 절약 할 수 있습니다.

    for i in *.mp3; do
        cp "./$i" /target
        ...
    done

     

    4. [ $foo = "bar" ] 

    • 이것은 함정 # 2와 매우 비슷한 경우입니다. 그러나 매우 중요한 것이므로 반복합니다.

    • 위의 예는 따옴표가 잘못된 위치에 있습니다.

      • 즉 Bash에서는 문자열에 따옴표를 사용할 필요가 없습니다. (메타 문자 혹은 패턴 문자를 포함하지 않는다면). 하지만 확장 혹은 와일드카드를 포함 할 수 있는지 없는지 확실하지 않을 때는 따옴표를 사용해야 할 것입니다.

    • 그리고 또한 위의 코드는 여러 가지 이유로 잘못될 수 있습니다.

    • 만약 [] 속의 변수가 존재하지 않는 경우 또는 공백인 경우, [] 명령은 다음과 같이 될 수 있습니다.

    [ = "bar" ] # 잘못된 코드

     

    • 그리고 다음과 같은 오류가 발생할 것입니다.

    unary operator expected. 
    (The = operator is binary, not unary, 
    so the [ command is rather shocked to see it there.)

     

    • 만약 변수가 내부에 공백을 포함하고 있다면 다음과 같이 [ ]내부의 $foo가 두 단어 이상으로 분할될 것입니다.

    [ multiple words here = "bar" ]

     

    • 이것은 언뜻 보았을 때 괜찮은 것처럼 보일지도 모르겠지만 [ 에 관한 한 이것은 구문 오류입니다. 올바른 작성법은 다음과 같습니다.

    # POSIX
    [ "$foo" = bar ] # Right!

     

    • POSIX의 []는 전달 된 인수의 수에 따라 그 작용을 결정하기 때문에 $foo이 -로 시작하더라도 POSIX 호환 구현에서 제대로 작동하게 됩니다. 만약 매우 오래된 레거시 코드와 같은 경우 이에 따른 문제가 있을지도 모르지만, 당신이 새로운 코드를 작성하는 경우에는 이에 대해 전혀 걱정할 필요가 없습니다.

    • Bash 나 다른 ksh와 같은 쉘의 경우 [[를 사용하는 새로운 대안이 있습니다.

    # Bash / Ksh
    [[ $foo == bar ]] # 올바른 방법

     

    • 이 경우 [[ ]]안에있는 =의 왼쪽의 변수에 따옴표를 쓸 필요가 없습니다.

    • 또한 공백의 변수도 제대로 처리됩니다.

    • 한편, 이 경우에도 따옴표를 사용하는 것 역시 가능합니다.

    • 그리고 [[ ]] 를 사용하는 경우 ==를 이용할 수 있습니다. 그러나 [[ 을 사용한 비교는 단순한 문자열 비교가 아닌 오른쪽의 문자열에 대해 패턴 매칭을 실시하게 될 것입니다.

    • 오른쪽을 단일 문자열로 하려면 파일 이름에서 패턴 매치의 문맥에서 특별한 의미를 가진 문자열이 사용되는 경우에는 반드시 따옴표를 사용해야 하는 것을 기억하십시오.

    # Bash / Ksh
    match=b*r
    [[ $foo == "$match" ]] # 좋은 코드

     

    • 그리고 당신은 아래와 같은 코드를 본 적이 있을 것입니다.

    # POSIX / Bourne
    [ x"$foo" = xbar ] # 나쁘지 않지만 불필요한 코드

     

    • x"$foo" 와 같이 x가 붙은 코드는 매우 원시적인 코드이지만 몇몇 아주 오래된 쉘에서 실행하는 데 필요한 경우가 있습니다.

    • 이 방법이 필요한 쉘은 POSIX에 따르지 않는 것으로 기억하시면 됩니다.

      • 그러나 Heirloom Bourne Shell 같은 매우 오래된 쉘조차 이 위의 방법이 필요하지 않습니다. 이러한 과도한 명시성은 매우 드문 요구 사항이며 일반적으로 불필요합니다.

     

    5. cd $ (dirname "$ f")

    • 이것도 또 다른 인용 오류입니다. 변수를 확장하고, 명령 치환의 결과가 분할되어 경로명으로 확장될 것입니다. 따라서 이것에 대해서는 다음과 같이 따옴표를 수행해야합니다.

    cd -P -- "$(dirname -- "$f")"

     

    • 여기에서 명확하지 않은 것은 어떻게 따옴표를 중첩하는가에 대한 것입니다.

    • C 프로그래머는 이것을 첫 번째와 두 번째 큰 따옴표를 한 그룹으로 보고 3 번째와 4 번째를 또 다른 그룹으로 보게 될 것입니다.

    • 그러나 Bash에서는 그러하지 않습니다. Bash는 큰 따옴표를 하나의 쌍으로 명령 치환에서 취급하게 되며 외부쌍과 내부 쌍으로 나눠지게 됩니다.

    • 즉 다른 방법으로 이를 표현하면 parser는 명령 대체를 내부 레벨로 취급하고, 내부의 인용은 외부의 인용과는 별개로 분리되는 것입니다.

     

    6. [ "$foo" = bar && "$bar" = foo ] 

    • &&는 [ ]명령에서 사용할 수 없습니다. Bash는 [[ ]]또는 (( )) 외부의 &&에 대해 &&전후에서 명령을 두 개의 명령으로 분할하게 됩니다.

    [ bar = "$foo" ] && [ foo = "$bar" ] # Right! (POSIX)
    [[ $foo = bar && $bar = foo ]]       # Also right! (Bash / Ksh)

     

    • 함정 # 4에서 소개 한 전통적인 이유로 [ ]안의 변수와 상수를 반대하고 있습니다. 또한 [[ ]]의 경우도 마찬가지로 반대하고 있지만, 패턴으로 해석하는 것을 방지하기 위해 전개시 따옴표가 필요합니다.

    • 또한 || 역시 마찬가지입니다. 결론적으로 두 경우 모두 대신에, [[ ]] 또는 두 개의 [ ] 명령을 사용하십시오.

    • 만약 이를 방지하려면 다음과 같이 할 수도 있습니다.

    [ bar = "$foo" -a foo = "$bar" ] # Not portable.

     

    • -a, -o 등의 바이너리 연산자는 POSIX 표준에 XSI 확장입니다. 이 모든 것은 POSIX-2008 obusolute 로 표시됩니다.

    • 이들은 새로운 코드에서 사용하는 것을 권장하지 않습니다.

      • [ A = B -a C = D ]또는 -o를 사용할 때 실질적인 문제 중 하나는 POSIX가 4 개 이상의 인수와 test또는 [ ] 명령의 결과를 명시하지 않을 수 있습니다.

    • 따라서 이는 아마도 대부분의 쉘에서 작동하겠지만 더이상 권장하지는 않습니다.

    • 다시 반복하지만 POSIX 쉘에 대한 코드를 작성해야하는 경우 [[ ]]또는 두 개의 [ ]명령을 사용할 것이 권장됩니다.

     

    7. [[ $foo > 7 ]]

    • 여기에서는 여러 문제가 있습니다. 먼저 [[ ]]명령은 산술식을 평가하기 위해서만 사용하여서는 안됩니다.

    • 대신 지원되는 테스트 연산자 중 하나 이상을 포함하는 경우에 해당 식을 사용 하여야 합니다.

    • 기술적으로 보았을 때 당신이 [[ ]]연산자를 이용하여 계산을 하는 것은 가능하지만, 단지 수치 비교 (또는 다른 쉘의 산술 연산)를 하고 싶은 뿐인 경우에는 (( ))를 사용하는 것이 훨씬 낫습니다.

    # Bash / Ksh
    ((foo > 7))     # Right!
    [[ foo -gt 7 ]] # Works, but is pointless. Most will consider it wrong. Use ((...)) or let instead.

     

    • 만약 당신이 [[ ]]내부에서 >연산자를 사용한다면, 그것은 문자열 비교로 처리됩니다. (즉 로케일의 순서로 조합하게 됩니다.)

    • 이는 사실상 정수의 비교가 아니며, 실제로는 작동하는 것처럼 보이지만 당신의 기대한 방식으로 작동 하는 것은 아닙니다.

    • 또한 >연산자를 [ ]속에서 사용하는 케이스는 더욱 나쁩니다. 즉 출력 자체가 방향 전환이 됩니다. 디렉터리 내부의 7이라는 파일을 수신하게 되고 $foo가 비어 있지 않으면 성공하게 되는 구조로 작동하게 됩니다.

    • 만약 엄격한 POSIX 호환이 필요하고, (( )) 를 사용할 수 없는 경우, 기존 방식의 [ ]사용이 올바른 대안입니다.

    # POSIX
    [ "$foo" -gt 7 ]       # Also right!
    [ $((foo > 7)) -ne 0 ] # POSIX-compatible equivalent to ((, for more general math operations.

     

    • 만약 산술 식에 입력 ( ((또는 let포함) 또는 수치 비교에 관한 [ ] 테스트 표현을 확인할 수 없다면, 반드시 항상 평가 표현 전에 입력을 검증 해야합니다.

    # POSIX
    case $foo in
        *[^[:digit:]]*)
            printf '$foo expanded to a non-digit: %s\n' "$foo" >&2
            exit 1
            ;;
        *)
            [ $foo -gt 7 ]
    esac

     

    8. grep foo bar | while read -r; do ((count++)); done

    • 위의 코드는 언뜻 보기에 문제가 없어보입니다.

    • 단지 grep -c 등으로 구현할 수도 있지만, 이것은 확장성이 제한되기 때문입니다. 상기 제목과 같은 코드의 경우 다른 SubShell 에서 각각의 명령을 병렬로 실행할되므로 count를 바꾸는 것은 while루프의 외부에 이양되어 있지 않습니다.

    • 이 동작은 여러면에서 거의 모든 Bash 초보자를 놀라게합니다.

    • POSIX는 subshell에서 평가 된 병렬의 마지막 요소를 지정하지 않습니다.

    • POSIX는 파이프 라인의 마지막 요소는 서브 쉘로 평가되고 있는지를 지정하지 않습니다.

    • ksh 버전 93 또는 Bash 버전 4.2 이상 shopt -s lastpipe을 실행하는 경우 while루프를 이 예와 같이 원래의 쉘 프로세스에서 실행하고 아무런 부작용 없이 사용할 수 있습니다.

    • 따라서 휴대용 스크립트는 하나의 동작에 의존하지 않는 방식으로 설명해야합니다.

     

    9. if [grep foo myfile]

    • 많은 초보자들이 하는 실수 중 하나는 [ ]또는 [[ ]] 바로 앞에 if 키워드가 있는 매우 일반적인 패턴을 보고 if문이 잘못 되었다고 판단하는 것입니다.

    • 그러나 이것은 실제로 그렇지 않습니다. 위의 코드는 실제로 if명령을 가지고 있습니다. [ ] 명령은 if 명령 구문 마커가 없습니다. 이것은 결과적으로 아래의 명령과 동일합니다.

    # POSIX
    if [ false ]; then echo "HELP"; fi
    
    if test false; then echo "HELP"; fi

     

    • 위의 두 명령은 동일합니다. 모두 인수 false가 비어 있지 않은지 확인합니다. 두 경우 모두 HELP가 항상 표시됩니다. 쉘 구문을 다른 프로그래밍 언어에서 유추 한 프로그래머는 놀랄 것입니다.

    • if문 구문은 다음과 같습니다.

    if COMMANDS
    then <COMMANDS>
    elif <COMMANDS> # optional
    then <COMMANDS>
    else <COMMANDS> # optional
    fi # required

     

    • 다른 보통의 간단한 명령뿐만 아니라 위의 코드는 인수를 취하고 있습니다. 그리고 if 다른 명령을 포함하여 구성 명령, [ ] 구문 자체가 없습니다!

    • 제로 이상의 옵션 elif의 섹션 1 옵션의 else마디가 있을지도 모릅니다.

    • if구성 명령은 두 가지 혹은 그 이상의 명령의 목록을 포함하고 then, elif또는 else에 의해 각각 구분 된 fi키워드에 의해 종료됩니다.

    • 첫 번째 섹션의 마지막 명령의 종료 상태와 각각의 다음 elif항목이 각각 어떤 then마디가 평가되는지를 결정합니다.

    • 그리고 elif은 then절 중 하나가 실행되기 전에 평가됩니다. 만약 평가되는 then부분이 없다면 else절을 가지고, 또는 else이 주어지지 않으면 if블록은 완료하고 if명령 전체 0 (true)을 반환합니다.

    • 만약 grep명령의 출력에 의거 뭔가를 확인하고 싶다면, 괄호와 대괄호, 백틱이나 다른 어떤 구문에서도 쓸 필요가 없습니다! 아래 처럼 그냥 if뒤에 grep명령으로 사용 하면 됩니다.

    if grep -q fooregex myfile; then
    ...
    fi

     

    • 만약 grep이 myfile행에 매치한다면, 종료 코드 0 (true)이되고, then마디가 실행됩니다. 그렇지 않은 경우, 즉 일치하는 것이 없는 경우 grep는 0이 아닌 코드를 반환하고 if전체 명령은 0이 됩니다.

     

    10. if [bar="$foo"]; then ...

    [bar="$foo"] # 잘못된 코드
    
    [ bar="$foo" ] # 이것 역시 잘못된 코드

     

    • 앞의 예에서 설명했듯이, [ ] 명령입니다, 다른 간단한 명령처럼 Bash는 명령에 공백이 따르기 때문에 첫 번째 인수 이후 다른 확장 등을 기대할 수 있습니다. 즉 공백이 없이 모든 것을 함께 수행 할 수 없습니다! 아래에 올바른 방법이 있습니다.

    if [ bar = "$foo" ]; then ...

     

    • bar, =, "$foo"의 전개 결과 ][ 명령에 있어서 각각 다른 인수입니다. 각각의 인수 쌍 사이에 공백이 없이 할 수 없으며, 이에 따라 Shell은 각각의 인수의 시작과 끝이 어디인지 알 수 있습니다.

     

     

    [Shell Script] 쉘 스크립트 (bash) 개발자가 빠지기 쉬운 함정 (2)

    정보 업무명 : 쉘 스크립트 (bash) 개발자가 빠지기 쉬운 함정 (2) 작성자 : 박진만 작성일 : 2020-03-22 설 명 : 수정이력 : 요약 [특징] 쉘 스크립트 개발자가 빠지기 쉬운 함정을 소개. [활용 자료] 없음 [자료..

    shlee1990.tistory.com

     

     참고 문헌

    [논문]

    • 없음

    [보고서]

    • 없음

    [URL]

    • 없음

     

     문의사항

    [기상학/프로그래밍 언어]

    • sangho.lee.1990@gmail.com

    [해양학/천문학/빅데이터]

    • saimang0804@gmail.com

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    본 블로그는 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음
    반응형
    • 네이버 블러그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기