[Python] sh 쉘 스크립트 디버깅 Develop Tip

리눅스에서 쉘 스크립트를 이용하여 많은 작업을 하고는 합니다.
갑자기 이런 쉘 스크립트가 복잡하고 굉장히 크다고 가정하고
디버깅을 어떻게 하면 좋을까 여러번 생각해 본 적이 있습니다.

그런데 보통 다른 최신의 프로그래밍 IDE (예, PyCharm, Visual Studio 등)와 같은
에서의 디버깅과 같은 것을 쉘에서 지원해주면 얼마나 좋을까 하는 생각이 
들었지만 지원해 주지는 않습니다.

물론 '-x' 옵션을 sh (bash 등)에 주어 해당 내용을 에코시켜
디버깅아닌 디버깅이라 부르고는 하지만 이것은 일반적인
디버깅이라 할 수 없지요.

암튼 필요에 따라 고민을 하다, 파이썬으로 접근을 해 보았습니다.

우선 다음과 같은 세 개의 Shell Script가 있습니다.

const.sh
============================================
#!/bin/sh

MY_CONST1=1
MY_CONST2="abc"


funcs.sh
============================================
#!/bin/sh

. ./const.sh

my_func1()
{
  echo "MY_CONST1=${MY_CONST1}"
  return ${MY_CONST1}
}

my_func2()
{
  echo "MY_CONST2=${MY_CONST2}"
  return 0
}


main.sh
============================================
#!/bin/sh

. ./funcs.sh

my_func1
RC=$?
echo $RC

if [ $RC -ne 0 ];then
  my_func2
  echo $?
fi

이제 main.sh 를 돌리면,

$ ./main.sh 
MY_CONST1=1
1
MY_CONST2=abc
0

와 같이 결과가 나옵니다.

간단히 const.sh에는 상수 변수를 넣었고,
funcs.sh 에는 쉘 함수를 정의하였고,
main.sh 에는 이를 이용하는 것으로                                                                                                           간단한 쉘 테스트 프로그램 입니다.

여기에서 main.sh가 크다고 가정하고
이를 디버깅하고자 했을 때 파이썬을 이용한다고 
생각하고 어떻게 할 수 있을 까 고민을 좀 했습니다.

결론은, 다음과 같은 파이썬 프로그램을 만들었습니다.

pysh.py
============================================
#! /usr/bin/env python

################################################################################
import os
import pexpect


################################################################################
class PyShRun(object):
    # ==========================================================================
    PS = '___@@@>>>> '

    # ==========================================================================
    def __init__(self, init_cmd='/bin/sh', env=None, exit_cmd='exit',
                 is_echo=True, logger=None):
        self.init_cmd = init_cmd
        if not (env and isinstance(env, dict)):
            env = {}
        self.env = env
        self.env['PS1'] = self.PS
        self.exit_cmd = exit_cmd
        self.is_echo = is_echo
        self.logger = logger
        # for internal
        self.exp = None
        self.is_opened = False
        self.echo_str = None

    # ==========================================================================
    def __enter__(self):
        self.open()
        return self

    # ==========================================================================
    def __exit__(self, *args):
        self.close()

    # ==========================================================================
    def open(self):
        self.close()
        self.exp = pexpect.spawn(self.init_cmd, env=self.env)
        self.exp.expect(self.PS)
        self.is_opened = True

    # ==========================================================================
    def close(self):
        if self.is_opened and self.exp is not None:
            self.exp.sendline(self.exit_cmd)
            self.exp.wait()
            self.exp = None

    # ==========================================================================
    def _stdout(self):
        sto = self.exp.before.decode()
        # exclude first line as command
        nlf = sto.find('\r\n')
        if nlf > 0:
            sto = sto[nlf+1:]
        sto = sto.lstrip('\n')
        if sto[-2:] == '\r\n':
            sto = sto[:-2] + '\n'
        if sto:
            self.echo_str = sto
            if self.is_echo:
                print(sto, end='')
        else:
            self.echo_str = ''
        return self.echo_str

    # ==========================================================================
    def run(self, cmd, is_expect=True):
        if not (self.is_opened and self.exp is not None):
            raise RuntimeError('Please open first')
        self.exp.sendline(cmd)
        if is_expect:
            # if then or do command's prompt is "^> $"
            self.exp.expect([f'{self.PS}$', '> $'])
        return self._stdout()

    # ==========================================================================
    def run_script(self, script_file, encoding='utf-8'):
        if not os.path.exists(script_file):
            raise IOError(f'Cannot open script file "{script_file}"')
        with open(script_file, encoding=encoding) as ifp:
            for line in ifp:
                line = line.rstrip()
                if not line:
                    continue
                self.run(line)

    # ==========================================================================
    def exit(self):
        return self.run(self.exit_cmd, is_expect=False)


################################################################################
if __name__ == '__main__':
    with PyShRun() as psr:
        psr.run_script('main.sh')


이 파이썬 프로그램의 역할은 pexpect 를 이용하여 해당 /bin/sh (디폴트는 sh 인데 bash로 대치할 수도
있겠네요) 로 엽니다. 내부적으로 PS1 프롬프트를 바꾸고 해당 프롬프트가 나타나거나,
if ...; then 구문이나 do ... 등의 구문이 나올 때 "> " 와 같은 프롬프트를 기다리고
shell 명령을 하나씩 실행하는 것입니다.
이를 다시 run_script 에서는 shell 스크립트를 읽어 한 줄씩 실행하도록 하는 것이구요.

물론 실행 속도는 일반 shell로 돌렸을 때보다 더 느려지기는 하지만,
하줄 씩 실행해보고 멈추고 하는 것은 가능해 보입니다.

좀 더 활용하면 쉘 더버거도 만들 수 있지 않을까 싶네요..

 
어느 분께는 도움이 되셨기를..


덧글

댓글 입력 영역

구글애드텍스트