At head of script file:

(CODE-1)
trap 'on_error "${FUNCNAME[@]} ${BASH_LINENO[@]} $LINENO"' ERR
trap 'on_exit' EXIT

Someone may wonder why don't write code like below because it is easier to handle arguments.

(CODE-2)
trap 'on_error "$LINENO ${FUNCNAME[@]} ${BASH_LINENO[@]}"' ERR
trap 'on_exit' EXIT

I don't know there is any documented information related with this. But, results of my experiments are saying that (CODE-2) doens't work as expected.
In case of (CODE-2), my test shows that only latest function-stack information is passed to 'on_error' trap function.
I don't have any idea about the reason. More investigation is required for this.
But anyway, (CODE-1) works well.
So, you can use those arguments to print function call stack at bash.
You may need to use 'BASH_SOURCE' array too, if your bash uses other files, too.



And there is one interesting case. See follow code.

<< Test environment >>
bash: GNU bash, version 4.3.46(1)-release (x86_64-pc-linux-gnu)
OS: Linux XXXX 4.4.0-64-generic #85-Ubuntu SMP Mon Feb 20 11:50:30 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

-------------------------- TEST-1 -----------------------------
<< a.sh >>
function on_err() {
    echo "error"
}

function on_exit() {
    echo "exit"
}

trap on_err ERR
trap on_exit EXIT

myval=$(echo u | grep p)  # global variable


$ bash -eE a.sh
error
exit

-------------------------- TEST-2 -----------------------------

<< a.sh >>
function on_err() {
    echo "error"
}

function on_exit() {
    echo "exit"
}

trap on_err ERR
trap on_exit EXIT

function f0() {
    local myval=$(echo u | grep p)  # local variable in function
}

f0

$ bash -eE a.sh
exit

---------------------------------------------------------------

Even if errtrace is enabled(-E option), 'error' is NOT printed at TEST-2.
Then, is this means 'ERR' is NOT trapped at TEST-2? That is, does 'on_err' not executed?
Let's have a look following code.

-------------------------- TEST-3 -----------------------------

<< a.sh >>
function on_err() {
    echo "error"
}

function on_exit() {
    echo "exit"
}

trap on_err ERR
trap on_exit EXIT

function f0() {
    myval=$(echo u | grep p)  # NOT local variable anymore.
}

f0

$ bash -eE a.sh
error
exit

-------------------------- TEST-4 -----------------------------

<< a.sh >>
function on_err() {
    echo "error" 1>&2  # echo to standard error.
}

function on_exit() {
    echo "exit"
}

trap on_err ERR
trap on_exit EXIT

function f0() {
    local myval=$(echo u | grep p)  # local variable
}

f0

$ bash -eE a.sh
error
exit

-------------------------- TEST-5 -----------------------------


function on_err() {
    echo "error" 1>&2  # echo to stderr
}

function on_exit() {
    echo "exit"
}

trap on_err ERR
trap on_exit EXIT

myval=$(echo u | grep p)  # assign to global variable.


$ bash -eE a.sh
error
error
exit


-------------------------- TEST-6 -----------------------------


function on_err() {
    true  # There is no echo
}

function on_exit() {
    echo "exit"
}

trap on_err ERR
trap on_exit EXIT

myval=$(echo u | grep p)  # asign to global variable


$ bash -eE a.sh
exit

---------------------------------------------------------------

It's very interesting, isn't it?
Further investigation will be continued for this issue.



'Language > Bash' 카테고리의 다른 글

[Bash] 'set -e' inheritance...???  (0) 2017.05.19

Code refactoring의 어려운 점은 이미 잘 알려진 바와 같다.
하지만, 그 문제의 복잡성 측면에서 보면, 작은 규모의 refactoring - file 혹은 class 단위 변경 - 은, 대규모 - 특히 코드의 구조 혹은 설계를 바꾸는 정도 - refactoring 에 비할 바가 못된다.

legacy 코드의 구조가 한계에 부딫혀, 재설계->재구현 을 고민할 정도의 상황을 가정해 보자.
이때, 가장 많이 고민하는 것은 아마도 "SW 재작성" vs "대규모 refactoring"일 것이다.
그리고, 어떠한 이유에서든, "refactoring"을 하기로 결정한 상황이라면 어떨까?
(실제, SW를 처음부터 새로 작성하는 것에 대한 Risk는 많이 언급되어 지고 있다.)

필자의 경험에 따르면, 이때 가장 중요한 것은
- refactoring의 단위를 적당히 작은 단계(step)로 분류하고,
- 각 step에서는 그 목적에 해당하는 refactoring만 수행
하는 것이다.

정말로 간단하고, 쉬워보이지 않는가?
하지만, 이게 정말 쉽지 않다.

Refactoring을 주제로 하는 많은 이야기들은, '좋은 구조', 'refactoring 시점' 등등 기술적인 측면을 다루고 있다.
그런데, 막상 필자가 실제로 heavy하게 사용되고 있는 SW를 refactoring하는 경험을 해보니, 정작 문제는 앞서 언급했던 두 가지에 있었다.

보통, 대규모 refactoring은 아래의 단계를 거쳐서 진행될 것이다.
- 현재 SW의 문제점 논의
- Refactoring의 범위 결정
- 새로운 SW구조에 대한 설계 철학 공유
- SW의 최종 형태 공유.
- 각 주제별로 refactring 시작.

하지만, 이런식의 진행은 'legacy SW의 상태'와 '최종 SW의 상태' 사이에 큰 차이가 있기 때문에, 그 끝이 좋지 못한 경우가 많다.
많은 양의 변화를 한꺼번에 진행하면, refactoring과정에서 발생한 오류를 찾아내기 너무 힘들어서, 결국 refactoring 코드를 버리거나, 아니면 SW 재작성에 버금가는(혹은 그 이상의) 노력이 들어가게 된다.
이론적으로는 이런 내용들을 대부분의 개발자들이 잘 알고 있지만, 실제로는 어떨까?

예를 들어, class간의 관계를 재 설정하는 refactoring진행 중, code의 context와 맞지 않는 변수 이름을 발견했다면? 혹은 code style이 잘못된 곳을 발견했다면?
대부분의 경우, 아주 작은 수정이므로, 겸사겸사 같이 수정하면서 진행할 것이다.
이것은, 마치 "거실 바닥 정리"라는 과정 중 "벽에 작은 얼룩을 발견"한 경우, 그냥 지나치지 못하고, 잠깐 시간내어서 얼룩을 지우는 것과 같다.
혹은, SW의 "Feature creep"과도 일맥상통해 보인다.
이런 식의 작은 side-refactoring들이 모여서, 한 step의 복잡성을 증가시키고, 결국 해당 step을 포기하고 처음부터 다시 진행하도록 만든다.

따라서, refactroing을 계획할 때는 앞서 언급한 것처럼, 그 단계를 잘게 나누어야 한다.
물론, 각 단계별로, "새로운 구조" + "legacy 구조" 의 형태를 지켜 내기 위한 overhead가 필요하므로, 너무 잘게 분리할 경우, 이 overhead 비용이 너무 커질 수 있으므로 주의해야 한다.


외부에 service를 제공하는 모듈 (예를 들면, library)일 경우, 잘못된 사용을 원천적으로 막는 것은 상당히 어려우면서도 중요하다.(모듈의 사용성과도 밀접한 관계가 있다.)

그런데, module의 instance가 생성되고 나서, 초기화 작업 이후에는 다시 setting될 필요가 없는 값들의 경우, 되도록이면, final (혹은 const)로 선언하는 것이, 코드의 이해도를 높이기도 좋고, 잘못된 사용을 막기도 좋다.


이때, default argument 값을 사용할 수 있는 언어의 경우(C++, Python 등)는 별 문제 없는데, 그렇지 않는 경우(ex, Java)는 사용성을 위해서 module constructor를 overloading해야 한다.(대부분의 경우, default 변수가 사용될 경우, 굳이 매번 이 값들을 argument로 전달해야 하는 것은 상당히 번거롭다.)


예를 들면,  'name', 'numArms'와 'numLegs' 세개의 final 변수를 가지는 'Person' module의 경우


class Person {
    private final String mName;
    private final int mNumArms;
    private final int mNumLegs;

    public Person(String name, int numArms, int numLegs) {
        mName = name;
        mNumArms = numArms;
        mNumLegs = numLegs;
    }
    public Person(String name, int numArms) {
        this(name, numArms, 2);
    }
    public Person(String name) {
        this(name, 2, 2); // 2를 default value로 사용. 대부분의 사람은 팔 다리가 2개... 
    }
}

와 같이 작성된다.

그나마 argument가 3개인 경우가 저정도고, argument가 많아지면, 사용성을 높이기 위해서 overloading되는 생성자도 많아진다.

이 문제를 builder pattern으로 해결할 수 있다.

예를 들면.

class PersonBuilder <T extends PersonBuilder > { // Generic type T 를 사용하는 이유는, 이 Builder를 상속받는 builder를 위함이다.
    private final String mName;  // name은 default가 없으므로 생성시 외부에서 반드시 argument로 받아야 한다.
    private int mNumArms = 2;  // 대부분의 사람은 팔, 다리가 2개...
    private int mNumLegs = 2;

    public PersonBuilder(String name) {
        mName = name;
    }
    public T
    setNumArms(int numArms) {
        mNumArms = numArms;
        return (T)this;
    }
    public T
    setNumLegs(int numLegs) {
        mNumLegs = numLegs;
        return (T)this;
    }

    public Person
    create() {
        return new Person(mName, mNumArms, mNumLegs);
    }
}


그렇지만, 역시 해당 class를 상속받는 경우, 긴 argument를 가진 생성자를 상속해야 하는 불편함은 여전하지만, 이 문제 역시, builder를 상속받는 방법으로 해결할 수 있다.


class OldPerson extends Person { ...}

class OldPersonBuilder<T extends OldPersonBuilder> extends PersonBuilder<T> {
    public OldPersonBuilder
    create() { ...}
}


Generic을 사용했기 때문에, 아래와 같은 용법이 가능하다.

OldPersonBuilder<OldPersonBuilder> bldr = new OldPersonBuilder<OldPersonBuilder>("MyName");
bldr.setNumArms(2).setNumLegs(2);

대략 정리해 보면, 이와 같은 pattern을 사용할때는


Module에서 final. Default값 존재 X => Builder에서도 final

Module에서 final. Default값 존재 O => Builder에서 NOT final.


이정도일려나?


+ Recent posts