My Blog

Latest blog


What is the Shell and Shell Scripts?

 쉘 shell의 사전적 의미는 다음과 같습니다.
"the hard outer covering of some creatures"

Shell 'image by pixabay'

 컴퓨터에서 쉘도 비슷한 역할을 합니다. 운영체제의 커널(운영체제의 일부로서 컴퓨터의 메모리에 항상 떠있는 프로그램)을 감싸고 있습니다. 하지만 단순히 껍데기에 지나지 않고 유저와 커널을 연결시켜주는 다리 역할까지 합니다. 쉘은 대화창 형태의 인퍼테이스를 제공하며 사용자는 명령어를 사용하여 원하는 명령을 내릴 수 있습니다. 쉘은 이를 운영체제에 전달하고, 운영체제는 하드웨어가 이해할 수 있는 언어로 번역합니다. 하드웨어의 처리결과를 다시 사용자가 알아볼 수 있는 형태로 변경하여 화면으로 전달해 줍니다.

 쉘 스크립트 shell script의 기본 컨셉은 사용자의 명령어를 실행 순서대로 나열해 놓은 리스트 입니다.


Shell Types

 유닉스 Unix는 두 가지 타입의 쉘이 있습니다.
  1. 1. Bourne shell: '$'을 기본 프롬프트 기호로 사용함.
  2. 1. Bourne Again shell(bash): 1989년 브라이언 폭스 Brian Fox가 GNU 프로젝트를 위해 개발. 현재 리눅스의 표준 쉘.
  3. 2. Bourne shell(sh): 1977년 AT&T사의 벨 연구소에서 스티브 본이 Stephen Bourne 개발. 최초의 bourne shell.
  4. 3. Korn shell(ksh): 1983년 AT&T사의 벨 연구소에서 데이비드 콘 David Korn이 개발. sh를 기반으로 C 쉘의 많은 기능들을 추가함.
  5. 4. POSIX shell(sh)
  6. 2. C shell: C 언어 구문과 유사한 쉘. '%'를 기본 프롬프트 기호로 사용함.
  7. 1. C shell(csh): 1978년 버클리 대학의 빌 조이 Bill Joy가 개발.
  8. 2. TENEX/TOPS C shell(tcsh): 1983년 카네기 멜런 대학교의 켄 그리어 Ken Greer가 개발.


Google Shell Style Guide

 많은 구글러들에 의해 작성되고 수정되며 유지되는 가이드입니다. 원문은 이 곳에서 확인할 수 있습니다.


Background: Which Shell to Use

 유일하게 사용이 허용된 쉘 스크립트 언어는 bash입니다.

 실행파일은 반드시 '#!/bin/bash'로 시작해야 하며 최소한의 플래그 flag만 지녀야 합니다. 'set'으로 쉘 옵션을 설정하여 'bash [script_name]'과 같은 형식으로 스크립트를 호출해도 문제없이 실행되도록 합니다.

 실행 가능한 쉘 스크립트를 bash로 제한하는 것은 이미 모든 컴퓨터에 설치된 쉘 스크립트 언어이기 때문이며 이는 일관성을 갖도록 만들어 줍니다.

 단, 예외사항은 특정 패키지 사용을 위해 강제받을 때 다른 쉘 스크립트 언어를 사용할 수 있습니다. 예를 들어 'Solaris SVR4 packages'는 본 쉘의 사용이 필요합니다.


Background: When to use Shell

 쉘은 작은 유틸리티나 간단한 랩퍼 스크립트 wrapper scripts에만 사용해야 합니다.

 쉘 스크립트가 개발언어는 아니지만, 구글 전체적으로 다양한 유틸리티 스크립트를 작성하는데 사용됩니다. 이 스타일 가이드는 쉘 스크립트 언어가 범용적으로 사용되도록 어떻게 사용해야 하는지 그 방법을 제안하기 보다는 이것의 사용법을 인지시키는데 더 큰 목적이 있습니다.

 몇 가지 가이드라인을 우선 안내합니다.:
  • ⊙ 만약 당신이 대부분 다른 유틸리티를 호출하거나, 연관된 작은 데이터들을 다루는 작업을 한다면 쉘을 사용하는 것이 알맞습니다.
  • ⊙ 퍼포먼스가 중요하다면 쉘 말고 다른 것을 사용하세요.
  • ⊙ 만약 100 라인 이상 긴 스크립트를 사용하거나 직관적이지 않은 제어흐름 논리를 사용한다면, 좀 더 체계적인 언어를 사용해서 다시 작성해야 합니다. 스크립트의 길이는 계속 증가한다는 사실을 명심하세요. 하루라도 빨리 다른 언어를 사용하여 스크립트를 새로 작성하는 것이 나중에 더 많은 시간을 들여야 하는 불필요한 수고를 막는 길입니다.
  • ⊙ 코드의 복잡성을 평가할 때(다른 언어로 바꿀지 말지 결정하기 위해), 그 코드가 작성자가 아닌 다른 사람들이 쉽게 유지보수 할 수 있는지 따져봐야 합니다.


Shell Files and Interpreter Invocation: File Extensions

 실행파일은 확장자를 가지지 않거나(강력히 추천) '.sh' 확장자를 가져야 합니다. 라이브러리는 반드시 '.sh' 확장자를 가져야하며 그 자체로 실행할 수 없어야 합니다.

 프로그램이 실행될 때 어떤 언어로 쓰였는지 알 필요가 없고 쉘은 확장자를 필요로 하지 않으므로, 우리는 실행파일에 확장자를 사용하지 않는 것을 선호합니다.

 그러나 라이브러리는 어떤 언어로 쓰였는지 아는 것이 중요하고 때로는 다른 언어로 작성된 비슷한 라이브러리가 필요하기도 합니다. 따라서 동일한 목적을 가진, 그러나 다른 언어로 작성된 라이브러리 파일을 언어에 따른 접미사를 제외하고 동일한 파일명으로 정할 수 있습니다.


Shell Files and Interpreter Invocation: SUID/SGID

 SUID와 SGID는 쉘 스크립트에서 금지합니다. (*SUID, SGID는 root가 아닌 일반 user가 일시적으로 root의 권한을 사용할 수 있도록 설정하는 기능입니다.)

 쉘은 너무나도 많은 보안이슈들이 존재하므로 SUID/SGID를 허용하는 것이 거의 불가능합니다. 이런 이유로 bash는 SUID를 실행하기 어려우나 여전히 몇몇 플랫폼에서는 SUID를 사용하고 있으므로 이것의 사용을 명시적으로 금지합니다.

 필요한 경우 접근 권한을 제공하기 위해 'sudo'를 사용하세요.


Environment: STDOUT vs STDERR

 모든 에러 메시지는 'STDERR'로 보내야 합니다.

 이 방식은 실제 이슈로부터 일반적인 상태를 쉽게 분리할 수 있습니다.

 다른 상태 정보와 함께 에러 메시지를 보여주는 기능이 권장됩니다.

STDERR


Comments: File Header

 각 파일의 시작은 스크립트 내용에 대한 설명으로 시작합니다.

 모든 파일은 최상단에 스크립트 내용에 대한 간략한 요약 코멘트를 반드시 포함해야 합니다. 저작권 고지와 작성자 정보는 선택사항입니다.

File header


Comments: Function Comments

 명확하지 않으면서 길이가 짧은 함수는 모두 코멘트를 작성해야 합니다. 라이브러리에 포함되는 모든 함수는 길이나 복잡도에 상관없이 반드시 코멘트를 작성해야 합니다.

 코드를 모두 읽어보지 않고 코멘트(그리고 만약 제공된다면 도움말)를 읽는 것만으로도 누구든지 당신의 프로그램을 어떻게 사용하는지 배울 수 있거나 라이브러리 내의 함수를 사용할 수 있도록 만들 수 있어야 합니다.

 모든 함수 코멘트는 아래 요소들을 사용하여 API의 의도된 동작을 설명해야 합니다.

  • ⊙ Description of the function
  • ⊙ Globals: 사용되거나 한정된 전역변수 리스트
  • ⊙ Arguments: 인자
  • ⊙ Outputs: STDOUT이나 STDERR로의 출력
  • ⊙ Returns: 기본 종료상태 외 반환되는 값

Function comments


Comments: Implementation Comments

 코드에서 까다롭고 명확하지 않거나 흥미로운, 혹은 중요한 파트에 코멘트를 작성해야 합니다.

 일반적인 구글 코딩 코멘트 방식을 준수합니다. 모든 것에 코멘트를 작성하지 마세요. 만약 복잡한 알고리즘이나 평범하지 않은 무엇인가를 하고 있다면 짧은 코멘트를 작성합니다.


Comments: TODO Comments

 임시적인, 단기간 솔루션, 혹은 충분히 좋지만 완벽하지 않은 코드에 대해서 TODO 코멘트를 작성합니다.

 이것은 C++ Guide의 컨벤션과 일치합니다.

 TODO는 모두 대문자로 써야하며, TODO에 의해 참조된 문제에 대한 최상의 컨텍스트를 가진 사람의 이름, 이메일 주소, 혹은 다른 식별자를 붙여줘야 합니다. 이런 방식의 주요 목적은 일관성을 유지함으로써 이런 요청에 대해 더 자세한 내용들을 어떻게 찾아볼 수 있는지 검색 가능하도록 하기 위함입니다. TODO는 이것에 의해 참조된 사람이 꼭 그 문제를 해결해야 하는 것은 아닙니다. 그러므로 TODO를 사용할 때, 당신의 이름을 적어줘야 합니다.

TODO Comments


Formatting: Indentation

 수정중인 코드는 이미 적용되어 있는 스타일을 따라야 하지만, 새로운 코드에는 다음과 같은 내용을 적용합니다.

 들여쓰기는 스페이스 2개를 사용합니다. 탭 tab은 사용하지 않습니다.

 가독성을 높이기 위해 블럭 사이에는 빈 라인을 사용합니다. 들여쓰기는 스페이스 2개를 사용합니다. 탭 tab은 사용하지 않습니다. 이미 작성된 코드는 그 곳에서 사용하는 들여쓰기 방식을 따라야 합니다.


Formatting: Line Length and Long Strings

 한 개 라인의 최대 길이는 80자 입니다.

 만약 80자 이상 써야한다면 가능한 here 문서 또는 embedded newline을 사용합니다. 자연스럽게 분리할 수 없고 80자 이상 긴 리터럴 문자열 literal strings이라면 어쩔 수 없지만, 더 짧은 길이로 만드는 방법을 강력히 권장합니다.

Line Length and Long Strings


Formatting: Pipelines

 복수개의 파이프라인은 가능하면 모두 하나의 라인에 넣을 수 있도록 합니다.

 만약 불가능한 경우 2개의 공백 들여쓰기와 함께 파이프라인을 새로운 라인에 사용합니다. 연속적인 명령문인 경우 '|'를 사용하고 논리적인 단위인 경우 '||'와 '&&' 기호를 사용합니다.

Pipelines


Formatting: Loops

 '; do'와 '; then'을 'while', 'for', 'if'와 동일한 라인에 사용합니다.

 쉘에서 루프 loop는 약간 다르지만, 함수를 선언할 때 중괄호에 적용하는 것과 동일한 원칙을 따릅니다. 즉, '; do'와 '; then'은 if/for/while과 동일한 라인에 위치해야 합니다. 'else'와 닫는 구문 closing statements은 새로운 라인에 위치해야 하며 여는 구문과 수직으로 동일선상에 위치해야 합니다.

Loops


Formatting: Case statement

  • ⊙ 들여쓰기는 2개의 공백을 사용합니다.
  • ⊙ 하나의 라인에 모두 넣을 수 없을 때, 패턴 pattern의 닫는 괄호 다음과 ';;' 기호 이전에 1개의 공백을 사용합니다.
  • ⊙ 길이가 길거나 다중명령어인 경우 패턴 pattern, 동작 actions, 그리고 ';;' 기호를 각각 다른 라인에 위치 시킵니다.
 패턴 표현식은 'case'와 esac'에서 한 단계 들여쓰기 합니다. 다중라인 동작 Multiline actions은 한 단계 더 들여쓰기 합니다. 일반적으로 패턴 표현식에 인용부호는 필요하지 않고 여는 괄호도 사용하지 않습니다. ';&'와 ';;&' 표기법은 피하세요.

Case statement
 간단한 명령문은 가독성이 허용하는 한 패턴 pattern과 ';;' 기호를 동일라인에 사용합니다. 이런 방식은 종종 단일문자 옵션을 사용할 때 적합합니다. 동작 actions이 동일라인에 적합하지 않은 경우 모두 분리된 라인에 위치시키며, 동일라인에 사용하는 경우 패턴 pattern의 닫는 괄호 다음과 ';;' 기호 이전에 각각 1개의 공백을 사용합니다.

Case statement of Simple commands


Formatting: Variable expansion

 우선순위: 일관성을 유지하세요; 변수에 인용부호를 사용하세요; "$var"보다는 "${var}"가 선호되는 방식입니다.

 다음 가이드라인은 강력히 권장하지만 필수 제약사항은 아닙니다. 가능하면 권장하는 방식을 사용하세요.

 우선순위 순서대로 정리해보면,
  • ⊙ 이미 코드에 규칙이 존재하는 경우 일관성을 지키세요.
  • ⊙ 변수에 인용부호를 사용하세요.
  • ⊙ 강력히 필요하거나 막심한 혼동을 피하기 위한 목적이 아닌 이상 단일문자 쉘 스페셜 single character shell specials/위치 매개변수 positional parameters에는 중괄호를 사용하지 않습니다. 다른 모든 변수에는 중괄호 사용이 선호됩니다.

Variable expansion


Formatting: Quoting

 인용부호를 제외한 확장이 필요하지 않거나 쉘 내부 정수가 아닌 경우라면 변수, 대체 명령어, 공백, 또는 쉘 메타 문자를 포함한 문자열은 항상 인용부호를 사용합니다.

 명령어 라인 플래그와 같은 리스트 요소의 안전한 인용부호 적용을 위해서 배열을 사용합니다.

 정수로 정의되는 쉘 내부 읽기전용 특수 변수('$?', '$#', '$$', '$!')는 선택적으로 인용부호를 사용합니다.

 (명령어 옵션이나 경로명과 달리) "단어"인 문자열에 인용부호 사용을 선호합니다.

 Literal integers에는 절대로 인용부호를 사용하지 않습니다.

 [[ ... ]] 패턴 매치 규칙에 인용부호를 사용할 때는 주의하세요.

 메시지나 로그의 문자열에 단순히 인자들을 추가할 때와 같이 '$*'를 사용하는 특별한 이유가 없는 한 '$@'를 사용합니다.


Quoting


Features and Bugs - ShellCheck

 ShellCheck project는 여러분이 작성한 쉘 스크립트의 일반적인 버그와 경고메시지를 확인합니다. 쉘 스크립트 길이와 상관없이 모두 이 과정을 거치도록 권장합니다.


Features and Bugs - Command Substitution

 Backticks(`) 대신에 $(command)를 사용하세요.

 중첩된 backticks를 사용하는 경우 escaping을 위해서 내부 backticks에 '\' 기호를 사용합니다. 하지만 $(command) 포맷은 중첩 되더라도 있는 그대로 사용할 수 있으며 가독성 또한 좋습니다.


Command Substitution


Features and Bugs - Test, [ ... ], and [[ ... ]]

 '[ ... ]', test, '/usr/bin/[' 방식보다 '[[ ... ]]' 방식을 선호합니다.

 '[[ ... ]]' 방식은 경로명 확장자가 존재하지 않는 에러나 '[['와 ']]' 사이에서 일어나는 단어 분리와 같은 에러를 줄여줍니다. 또한 '[ ... ]' 방식은 정규표현식이 허용되지 않는 반면 '[[ ... ]]' 방식은 허용됩니다.


'[[ ... ]]'


Features and Bugs - Testing Strings

 가능하면 문자열 필터 대신 인용부호를 사용하세요.

 Bash는 테스트에서 빈 문자열을 다루기 충분할 정도로 똑똑합니다. 따라서 코드를 테스트를 할 때 문자열 필터 대신 빈 문자열/비어있지 않은 문자열을 사용하세요. 이것은 코드 가독성 또한 높여줍니다.


Testing strings 1

 테스트 대상에 대한 혼동을 피하기 위해서 명시적으로 '-z'와 '-n' 옵션을 사용하세요.

 동일하다는 의미를 표현할 때 '='와 '==' 두 가지 방식 모두 작동하지만, 명확성을 위해 '=='를 사용하세요. 그러나 '[[ ... ]]' 안에서 '<' 기호와 '>' 기호를 사용할 때 주의하세요. 대신 '(( ... ))' 방식이나 '-lt', '-gt'와 같은 방식을 사용합니다.


Testing strings 2


Features and Bugs - Wildcard Expansion of Filenames

 파일명 확장자에 와일드카드를 사용할 때 분명한 경로를 사용하세요.

 파일명이 '-' 기호로 시작될 수 있는 경우 와일드카드 확장에 './*'를 사용하는 것이 '*'를 사용하는 것보다 훨씬 안전합니다.


Wildcard expansion of filenames

Features and Bugs - Eval

 'eval'은 피해야 합니다.

 변수에 input을 할당할 때 'eval'을 사용하면 input을 흐트려 뜨립니다. 또한 그 변수가 과거에 어떤 상태였는지 확인과정을 거치지 않고서도 설정할 수 있도록 만들 수 있습니다.


Eval


Features and Bugs - Arrays

 인용부호 관련 문제를 피하기 위해서 리스트 요소들을 저장할 때 bash 배열을 사용합니다. 이 방식은 특히 인자 리스트에 적용해야 합니다. 보다 복잡한 데이터 구조를 쉽게 만들기 위해 배열을 사용해서는 안됩니다.


Arrays

 배열의 장점
 배열을 사용하면 인용부호 의미의 혼동 없이 문자열 내용들을 나열할 수 있습니다. 반대로 배열을 사용하지 않으면 문자열 안에서 중첩된 인용부호 사용과 같은 잘못된 시도로 이어질 수 있습니다.
 배열을 사용하면 공백을 포함한 문자열, 임의의 문자열에 대한 시퀀스/리스트를 안전하게 저장할 수 있습니다.

 배열의 단점
 배열을 사용하면 스크립트의 복잡도가 증가할 수 있습니다.

 배열의 결정
 리스트를 안전하게 생성하고 전달하기 위해서 배열을 사용해야 합니다. 특히 명령어 인수들을 작성할 때 혼동되는 인용부호 문제를 피하기 위해 배열을 사용해야 합니다. 배열에 접근하기 위해서는 인용부호 확장- "${array[@]}" -을 사용하세요. 그러나 더욱 숙련된 데이터 처리가 요구된다면 쉘 스크립팅을 사용하지 말아야 합니다.


Features and Bugs - Pipes to While

 'while'에 파이프를 사용하는 것보다 대체 프로세스를 사용하거나 builtin(bash4+) readarray를 사용하세요. 파이프는 서브쉘을 만드는데 파이프라인 안에서 일어나는 그 어떤 변수의 수정작업도 부모쉘에 전파되지 않습니다.

 파이프 안 'while'에 대한 서브쉘은 추적하기 어려운 미묘한 버그들을 초래할 수 있습니다.

 대체 프로세스를 사용하는 방법도 서브쉘을 만듭니다. 하지만 서브쉘 내부에 'while' (혹은 다른 어떤 명령어)을 사용하지 않고서도 서브쉘로부터 'while'로 리다이렉션을 허용합니다.

 또는 파일을 읽고 배열로 만든 뒤 배열 안 내용들을 반복하며 도는 builtin readarray를 사용하세요.


Pipes to while


Features and Bugs - Arithmetic

 'let', '$[ ... ]', 'expr'보다 '(( ... ))'나 '$(( ... ))' 방식을 사용합니다.

 '$[ ... ]'나 'expr', 'let'은 절대 사용하지 않습니다.

 '<'나 '>' 기호는 '[[ ... ]]' 표현식 안에서 수치비교 기능을 할 수 없습니다. 모든 숫자 비교 작업에서는 '[[ ... ]]' 방식을 사용하지 말고 '(( ... ))' 방식을 사용합니다.


Arithmetic


Naming Conventions - Function Names

 함수명은 소문자로 이루어지고 단어 사이 밑줄로 구분합니다. 라이브러리는 '::' 기호로 분리합니다. 함수명 다음에 괄호가 필요합니다. 키워드 'function'은 선택사항이지만 프로젝트 전반에 걸쳐 일관성을 가져야 합니다.

 만약 여러분이 하나의 함수를 작성한다면, 소문자를 사용하고 단어 사이 밑줄을 사용해서 분리하세요. 만약 패키지를 작성한다면 '::' 기호와 함께 패키지 이름을 분리하세요. 중괄호는 반드시 함수명과 동일 라인에 위치해야 하며 함수명과 괄호 사이에 공백은 넣지 않습니다.


Function names


Naming Conventions - Variable Names

 변수명도 함수명과 동일합니다.

 루프를 돌기 위한 변수명은 루프 대상 변수와 비슷하게 만들어야 합니다.


Variable names


Naming Conventions - Constants and Environment Variable Names

 상수 및 환경변수명은 모두 대문자로 이루어지고 단어 사이 밑줄로 구분하며 파일 상단에 선언합니다.


Constant and environment variable names


Naming Conventions - Source Filenames

 소스파일명은 소문자를 사용하고 단어 사이 밑줄로 구분합니다.

 예를들어, maketemplate나 make_template를 사용하되 make-template는 사용하지 않습니다.


Naming Conventions - Read-only Variables

 대상 변수들이 '읽기전용'이라는 사실을 확실히 하기 위해서 'readonly'나 'decalre -r'을 사용합니다.

 전역변수는 쉘 내부에서 광범위하게 사용되므로 에러를 잡아내는 것이 중요합니다. 만약 당신이 read-only 변수를 선언했다면 명확하게 표현하세요.


Read-only Variables


Naming Conventions - Use Local Variables

 특정 함수와 관련된 변수를 선언할 때 'local'을 사용하세요. 선언과 할당은 서로 다른 라인에 위치해야 합니다.

 지역변수를 선언할 때는 'local'을 사용하여 오직 함수 안에서, 그리고 그 하위 항목에서만 보이도록 합니다. 이런 방식은 전역변수 공간을 오염시키는 것을 방지하고 함수 밖에서 중요할지도 모르는 변수를 무심결에 설정하는 것을 방지합니다.

 대체 명령어에 의해 값이 할당될 때 선언과 할당은 각각 분리된 라인에 위치해야 합니다; Builtin local은 대체 명령어로부터 종료 코드를 전파하지 않습니다.

Use local variables


Naming Conventions - Function Location

 파일 내 모든 함수는 상수 바로 아래에 함께 위치시킵니다. 함수 사이에 실행 가능한 코드를 숨기지 않습니다. 그런 방식은 코드를 파악하기 어렵게 만들며 그 결과 디버깅 할 때 나쁜 결과를 초래합니다.


Naming Conventions - main

 스크립트가 1개 이상의 함수를 포함할 정도로 길이가 길면 'main' 함수가 필요합니다.

 프로그램의 시작점을 쉽게 찾도록 하기 위해서 'main' 함수를 가장 아래에 넣습니다. 코멘트가 없는 파일의 가장 마지막 라인은 'main' 함수를 호출합니다.


Calling Commands - Checking Return Values

 항상 리턴값을 체크합니다. 그리고 리턴값은 유용한 정보를 제공해야 합니다.

 파이프를 사용하지 않은 명령문에서 '$?'를 사용하거나 'if'문을 사용하여 간단히 리턴값을 직접 확인합니다.


Checking return values


Conclusion

 상식적인 선에서 일관성을 유지하세요.


Opinion

 파이썬 스타일 가이드(PEP8)에 이어 이번에는 구글에서 사용하는 쉘 스크립트 스타일 가이드를 살펴봤습니다. 그동안 쉘 스크립트는 리눅스에서 NGS data 분석을 위한 프로그램 실행과 자동화 코드를 생성하는데 사용했습니다. 별다른 컨벤션 없이 필요한 기능을 검색하며 규칙없이 사용했는데 앞으로는 구글 스타일 가이드에 따라 코드를 만들도록 노력해야 겠습니다.