รูปแบบการออกแบบหรือแนวปฏิบัติที่ดีที่สุดสำหรับเชลล์สคริปต์ [ปิด]


167

ไม่มีใครรู้เกี่ยวกับทรัพยากรใด ๆ ที่พูดถึงแนวปฏิบัติที่ดีที่สุดหรือรูปแบบการออกแบบสำหรับเชลล์สคริปต์ (sh, bash เป็นต้น)


2
ฉันเพิ่งเขียนบทความเล็กน้อยเกี่ยวกับรูปแบบแม่แบบใน BASHเมื่อคืนนี้ ดูว่าคุณคิดอย่างไร
quickshiftin

คำตอบ:


222

ฉันเขียนเชลล์สคริปต์ที่ค่อนข้างซับซ้อนและคำแนะนำแรกของฉันคือ "ไม่" เหตุผลก็คือค่อนข้างง่ายที่จะทำผิดพลาดเล็กน้อยที่ขัดขวางสคริปต์ของคุณหรือทำให้เป็นอันตราย

ที่กล่าวว่าฉันไม่มีทรัพยากรอื่น ๆ ที่จะผ่านคุณ แต่ประสบการณ์ส่วนตัวของฉัน นี่คือสิ่งที่ฉันทำตามปกติซึ่งเป็น overkill แต่มีแนวโน้มที่จะแข็งแม้จะมากอย่างละเอียด

การภาวนา

ทำให้สคริปต์ของคุณยอมรับตัวเลือกแบบยาวและแบบสั้น ระวังเพราะมีสองคำสั่งในการแยกวิเคราะห์ตัวเลือก getopt และ getopts ใช้ getopt เมื่อคุณประสบปัญหาน้อยลง

CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""

getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "$@"`

if test $? != 0
then
    echo "unrecognized option"
    exit 1
fi

eval set -- "$getopt_results"

while true
do
    case "$1" in
        --config_file)
            CommandLineOptions__config_file="$2";
            shift 2;
            ;;
        --debug_level)
            CommandLineOptions__debug_level="$2";
            shift 2;
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "$0: unparseable option $1"
            EXCEPTION=$Main__ParameterException
            EXCEPTION_MSG="unparseable option $1"
            exit 1
            ;;
    esac
done

if test "x$CommandLineOptions__config_file" == "x"
then
    echo "$0: missing config_file parameter"
    EXCEPTION=$Main__ParameterException
    EXCEPTION_MSG="missing config_file parameter"
    exit 1
fi

อีกจุดสำคัญคือโปรแกรมควรกลับศูนย์ถ้าเสร็จสมบูรณ์ไม่เป็นศูนย์ถ้ามีอะไรผิดพลาด

ฟังก์ชั่นการโทร

คุณสามารถเรียกฟังก์ชั่นด้วยการทุบตีเพียงจำไว้เพื่อกำหนดพวกเขาก่อนที่จะโทร ฟังก์ชั่นเป็นเหมือนสคริปต์พวกเขาสามารถคืนค่าตัวเลขเท่านั้น ซึ่งหมายความว่าคุณต้องคิดค้นกลยุทธ์ที่แตกต่างเพื่อส่งคืนค่าสตริง กลยุทธ์ของฉันคือใช้ตัวแปรที่เรียกว่า RESULT เพื่อเก็บผลลัพธ์และส่งคืนค่า 0 หากฟังก์ชันเสร็จสมบูรณ์หมดจด นอกจากนี้คุณสามารถเพิ่มข้อยกเว้นหากคุณส่งคืนค่าที่แตกต่างจากศูนย์จากนั้นตั้งค่า "ตัวแปรตัวแปร" สองรายการ (ของฉัน: EXCEPTION และ EXCEPTION_MSG) ซึ่งเป็นประเภทแรกที่มีประเภทข้อยกเว้นและข้อความที่มนุษย์อ่านได้

เมื่อคุณเรียกใช้ฟังก์ชั่นพารามิเตอร์ของฟังก์ชั่นจะถูกกำหนดให้กับ vars พิเศษ $ 0, $ 1 และอื่น ๆ ฉันขอแนะนำให้คุณใส่ชื่อที่มีความหมายมากกว่านี้ ประกาศตัวแปรภายในฟังก์ชันเป็น local:

function foo {
   local bar="$0"
}

เกิดข้อผิดพลาดได้ง่าย

ใน bash ยกเว้นว่าคุณจะประกาศเป็นอย่างอื่นตัวแปร unset จะถูกใช้เป็นสตริงว่าง นี่เป็นสิ่งที่อันตรายมากในกรณีที่พิมพ์ผิดเนื่องจากจะไม่มีการรายงานตัวแปรที่พิมพ์ไม่ดีและจะถูกประเมินว่าว่างเปล่า ใช้

set -o nounset

เพื่อป้องกันไม่ให้สิ่งนี้เกิดขึ้น ระวังเพราะถ้าคุณทำเช่นนี้โปรแกรมจะยกเลิกทุกครั้งที่คุณประเมินตัวแปรที่ไม่ได้กำหนด ด้วยเหตุผลนี้วิธีเดียวที่จะตรวจสอบว่าตัวแปรไม่ได้ถูกกำหนดไว้ดังต่อไปนี้:

if test "x${foo:-notset}" == "xnotset"
then
    echo "foo not set"
fi

คุณสามารถประกาศตัวแปรแบบอ่านอย่างเดียว:

readonly readonly_var="foo"

modularization

คุณสามารถใช้การทำให้เป็นโมดูลแบบ "python like" ได้ถ้าคุณใช้รหัสต่อไปนี้:

set -o nounset
function getScriptAbsoluteDir {
    # @description used to get the script path
    # @param $1 the script $0 parameter
    local script_invoke_path="$1"
    local cwd=`pwd`

    # absolute path ? if so, the first character is a /
    if test "x${script_invoke_path:0:1}" = 'x/'
    then
        RESULT=`dirname "$script_invoke_path"`
    else
        RESULT=`dirname "$cwd/$script_invoke_path"`
    fi
}

script_invoke_path="$0"
script_name=`basename "$0"`
getScriptAbsoluteDir "$script_invoke_path"
script_absolute_dir=$RESULT

function import() { 
    # @description importer routine to get external functionality.
    # @description the first location searched is the script directory.
    # @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable
    # @param $1 the .shinc file to import, without .shinc extension
    module=$1

    if test "x$module" == "x"
    then
        echo "$script_name : Unable to import unspecified module. Dying."
        exit 1
    fi

    if test "x${script_absolute_dir:-notset}" == "xnotset"
    then
        echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying."
        exit 1
    fi

    if test "x$script_absolute_dir" == "x"
    then
        echo "$script_name : empty script path. Dying."
        exit 1
    fi

    if test -e "$script_absolute_dir/$module.shinc"
    then
        # import from script directory
        . "$script_absolute_dir/$module.shinc"
    elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset"
    then
        # import from the shell script library path
        # save the separator and use the ':' instead
        local saved_IFS="$IFS"
        IFS=':'
        for path in $SHELL_LIBRARY_PATH
        do
            if test -e "$path/$module.shinc"
            then
                . "$path/$module.shinc"
                return
            fi
        done
        # restore the standard separator
        IFS="$saved_IFS"
    fi
    echo "$script_name : Unable to find module $module."
    exit 1
} 

จากนั้นคุณสามารถนำเข้าไฟล์ที่มีนามสกุล. shinc ด้วยไวยากรณ์ต่อไปนี้

นำเข้า "AModule / ModuleFile"

ซึ่งจะค้นหาใน SHELL_LIBRARY_PATH ในขณะที่คุณนำเข้าในเนมสเปซส่วนกลางอย่าลืมนำหน้าฟังก์ชั่นและตัวแปรทั้งหมดด้วยคำนำหน้าที่เหมาะสมมิฉะนั้นคุณอาจเสี่ยงต่อการปะทะกันของชื่อ ฉันใช้เครื่องหมายขีดล่างคู่เป็นจุดหลาม

นอกจากนี้ให้ใส่สิ่งนี้เป็นสิ่งแรกในโมดูลของคุณ

# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
    return 0
fi
BashInclude__imported=1

การเขียนโปรแกรมเชิงวัตถุ

ใน bash คุณไม่สามารถเขียนโปรแกรมเชิงวัตถุได้เว้นแต่คุณจะสร้างระบบการจัดสรรวัตถุที่ค่อนข้างซับซ้อน (ฉันคิดว่ามันเป็นไปได้ แต่เป็นบ้า) ในทางปฏิบัติคุณสามารถทำ "Singleton oriented programming": คุณมีหนึ่งอินสแตนซ์ของแต่ละวัตถุและมีเพียงอันเดียว

สิ่งที่ฉันทำคือ: ฉันกำหนดวัตถุลงในโมดูล (ดูรายการการทำให้เป็นโมดูล) จากนั้นฉันจะกำหนด vars ที่ว่างเปล่า (คล้ายกับตัวแปรสมาชิก) ฟังก์ชั่น init (ตัวสร้าง) และฟังก์ชั่นสมาชิกเช่นในรหัสตัวอย่างนี้

# avoid double inclusion
if test "${Table__imported+defined}" == "defined"
then
    return 0
fi
Table__imported=1

readonly Table__NoException=""
readonly Table__ParameterException="Table__ParameterException"
readonly Table__MySqlException="Table__MySqlException"
readonly Table__NotInitializedException="Table__NotInitializedException"
readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException"

# an example for module enum constants, used in the mysql table, in this case
readonly Table__GENDER_MALE="GENDER_MALE"
readonly Table__GENDER_FEMALE="GENDER_FEMALE"

# private: prefixed with p_ (a bash variable cannot start with _)
p_Table__mysql_exec="" # will contain the executed mysql command 

p_Table__initialized=0

function Table__init {
    # @description init the module with the database parameters
    # @param $1 the mysql config file
    # @exception Table__NoException, Table__ParameterException

    EXCEPTION=""
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    RESULT=""

    if test $p_Table__initialized -ne 0
    then
        EXCEPTION=$Table__AlreadyInitializedException   
        EXCEPTION_MSG="module already initialized"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
    fi


    local config_file="$1"

      # yes, I am aware that I could put default parameters and other niceties, but I am lazy today
      if test "x$config_file" = "x"; then
          EXCEPTION=$Table__ParameterException
          EXCEPTION_MSG="missing parameter config file"
          EXCEPTION_FUNC="$FUNCNAME"
          return 1
      fi


    p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e "

    # mark the module as initialized
    p_Table__initialized=1

    EXCEPTION=$Table__NoException
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    return 0

}

function Table__getName() {
    # @description gets the name of the person 
    # @param $1 the row identifier
    # @result the name

    EXCEPTION=""
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    RESULT=""

    if test $p_Table__initialized -eq 0
    then
        EXCEPTION=$Table__NotInitializedException
        EXCEPTION_MSG="module not initialized"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
    fi

    id=$1

      if test "x$id" = "x"; then
          EXCEPTION=$Table__ParameterException
          EXCEPTION_MSG="missing parameter identifier"
          EXCEPTION_FUNC="$FUNCNAME"
          return 1
      fi

    local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = '$id'"`
      if test $? != 0 ; then
        EXCEPTION=$Table__MySqlException
        EXCEPTION_MSG="unable to perform select"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
      fi

    RESULT=$name
    EXCEPTION=$Table__NoException
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    return 0
}

ดักจับและจัดการสัญญาณ

ฉันพบว่ามีประโยชน์ในการจับและจัดการข้อยกเว้น

function Main__interruptHandler() {
    # @description signal handler for SIGINT
    echo "SIGINT caught"
    exit
} 
function Main__terminationHandler() { 
    # @description signal handler for SIGTERM
    echo "SIGTERM caught"
    exit
} 
function Main__exitHandler() { 
    # @description signal handler for end of the program (clean or unclean). 
    # probably redundant call, we already call the cleanup in main.
    exit
} 

trap Main__interruptHandler INT
trap Main__terminationHandler TERM
trap Main__exitHandler EXIT

function Main__main() {
    # body
}

# catch signals and exit
trap exit INT TERM EXIT

Main__main "$@"

คำแนะนำและเคล็ดลับ

หากบางสิ่งบางอย่างใช้งานไม่ได้ด้วยเหตุผลบางประการลองสั่งรหัสใหม่ การสั่งซื้อเป็นสิ่งสำคัญและไม่ง่ายเสมอไป

อย่าพิจารณาทำงานกับ tcsh มันไม่รองรับฟังก์ชั่นและมันก็น่ากลัวโดยทั่วไป

หวังว่าจะช่วยได้โปรดทราบ หากคุณต้องใช้สิ่งที่ฉันเขียนที่นี่หมายความว่าปัญหาของคุณซับซ้อนเกินกว่าจะแก้ไขด้วยเชลล์ได้ ใช้ภาษาอื่น ฉันต้องใช้มันเนื่องจากปัจจัยมนุษย์และมรดก


7
ว้าวและฉันคิดว่าฉันจะใช้จ่ายมากเกินไปในการทุบตี ... ฉันมักจะใช้ฟังก์ชั่นที่แยกได้และการใช้ subshells ในทางที่ผิด ไม่มีตัวแปรทั่วโลกทั้งในและนอก (เพื่อรักษาซากของสติ) ผลตอบแทนทั้งหมดผ่าน stdout หรือไฟล์ที่ส่งออก set -u / set -e (ชุดที่แย่เกินไป - e กลายเป็นไร้ประโยชน์ทันทีที่แรกถ้าและรหัสของฉันส่วนใหญ่มักจะอยู่ในนั้น) ฟังก์ชันขัดแย้งกับ [local something = "$ 1"; shift] (อนุญาตให้ทำการเรียงลำดับใหม่ได้ง่ายเมื่อทำการเปลี่ยนโครงสร้างใหม่) หลังจากหนึ่ง 3000 เส้นทุบตีสคริปต์ผมมักจะเขียนสคริปต์แม้มีขนาดเล็กที่สุดในแฟชั่นนี้ ...
ยู

การแก้ไขเล็กน้อยสำหรับการทำให้เป็นโมดูล: 1 คุณต้องการผลตอบแทนหลังจาก "$ script_absolute_dir / $ module.shinc" เพื่อหลีกเลี่ยงคำเตือนที่ขาดหายไป 2 คุณต้องตั้งค่า IFS = "$ save_IFS" ก่อนที่คุณจะกลับมาหาโมดูลใน $ SHELL_LIBRARY_PATH
Duff

"ปัจจัยมนุษย์" เป็นปัจจัยที่เลวร้ายที่สุด เครื่องจักรไม่ต่อสู้กับคุณเมื่อคุณมอบสิ่งที่ดีกว่าให้กับพวกเขา
jeremyjjbrown

1
ทำไมgetoptVS getopts? getoptsสามารถพกพาได้มากกว่าและใช้งานได้ใน POSIX เชลล์ โดยเฉพาะอย่างยิ่งเนื่องจากคำถามคือวิธีปฏิบัติที่ดีที่สุดของเชลล์แทนที่จะเป็นวิธีปฏิบัติที่ดีที่สุดสำหรับทุบตีฉันจะสนับสนุนการปฏิบัติตาม POSIX เพื่อสนับสนุนเชลล์จำนวนมากเมื่อเป็นไปได้
Wimateeka

1
ขอบคุณที่เสนอคำแนะนำทั้งหมดสำหรับเชลล์สคริปต์แม้ว่าคุณจะซื่อสัตย์: "หวังว่าจะช่วยได้โปรดทราบหากคุณต้องใช้สิ่งที่ฉันเขียนที่นี่หมายความว่าปัญหาของคุณซับซ้อนเกินไปที่จะแก้ไขด้วย เชลล์ใช้ภาษาอื่นฉันต้องใช้มันเนื่องจากปัจจัยมนุษย์และมรดก "
dieHellste

25

ลองดูที่คู่มือการใช้สคริปต์การทุบตีขั้นสูงเพื่อความรู้อย่างมากเกี่ยวกับการเขียนสคริปต์เปลือกไม่ใช่เพียงแค่การทุบตี

อย่าฟังคนที่บอกให้คุณดูภาษาอื่นที่ซับซ้อนกว่านี้ หากการเขียนสคริปต์เชลล์ตรงตามความต้องการของคุณให้ใช้สิ่งนั้น คุณต้องการฟังก์ชั่นไม่ใช่เรื่องเพ้อฝัน ภาษาใหม่ให้ทักษะใหม่ที่มีค่าสำหรับเรซูเม่ของคุณ แต่นั่นไม่ได้ช่วยอะไรถ้าคุณมีงานที่ต้องทำและคุณรู้จักเชลล์แล้ว

ตามที่ระบุไว้ไม่มี "แนวปฏิบัติที่ดีที่สุด" หรือ "รูปแบบการออกแบบ" จำนวนมากสำหรับการสร้างสคริปต์เชลล์ การใช้งานที่แตกต่างกันมีแนวทางและอคติที่แตกต่างกันเช่นภาษาการเขียนโปรแกรมอื่น ๆ


9
โปรดทราบว่าสำหรับสคริปต์ที่มีความซับซ้อนเพียงเล็กน้อยนั่นไม่ใช่วิธีปฏิบัติที่ดีที่สุด การเข้ารหัสไม่ได้เกี่ยวกับการทำให้บางสิ่งทำงาน มันเกี่ยวกับการสร้างอย่างรวดเร็วง่ายดายและมีความน่าเชื่อถือนำมาใช้ใหม่และง่ายต่อการอ่านและบำรุงรักษา (โดยเฉพาะอย่างยิ่งสำหรับผู้อื่น) เชลล์สคริปไม่ได้ขยายไปในทุกระดับ ภาษาที่มีประสิทธิภาพมากขึ้นนั้นง่ายกว่ามากสำหรับโครงการที่มีตรรกะ
ผู้เยี่ยมชม

20

เชลล์สคริปต์เป็นภาษาที่ออกแบบมาเพื่อจัดการไฟล์และกระบวนการ แม้ว่ามันจะยอดเยี่ยมสำหรับมัน แต่มันไม่ใช่ภาษาที่ใช้งานทั่วไปดังนั้นให้พยายามติดกาวตรรกะจากยูทิลิตี้ที่มีอยู่แทนที่จะสร้างตรรกะใหม่ในเชลล์สคริปต์

อื่น ๆ กว่าที่หลักการทั่วไปผมได้รวบรวมบางข้อผิดพลาดที่พบบ่อยเชลล์สคริปต์



11

รู้ว่าควรใช้เมื่อไร สำหรับคำสั่งการติดกาวที่รวดเร็วและสกปรกเข้าด้วยกันมันก็โอเค หากคุณต้องการที่จะทำให้อะไรมากไปกว่าการตัดสินใจที่ไม่น่ารำคาญไม่กี่ห่วงอะไรไปสำหรับ Python, Perl และmodularize

ปัญหาที่ใหญ่ที่สุดของเชลล์คือผลลัพธ์ที่ออกมาดูเหมือนโคลนก้อนใหญ่ทุบตี 4,000 เส้นและเติบโต ... และคุณไม่สามารถกำจัดมันได้เพราะตอนนี้โครงการทั้งหมดของคุณขึ้นอยู่กับมันแล้ว แน่นอนมันเริ่มต้นด้วย 40 bash ที่สวยงาม


9

ง่าย: ใช้ python แทนเชลล์สคริปต์ คุณสามารถเพิ่มความสามารถในการอ่านได้เพิ่มขึ้นเกือบ 100 เท่าโดยไม่ต้องยุ่งยากอะไรอีกต่อไปและรักษาความสามารถในการพัฒนาส่วนของสคริปต์ของคุณให้เป็นฟังก์ชันวัตถุวัตถุถาวร (zodb) วัตถุกระจาย (pyro) เกือบไม่มี รหัสพิเศษ


7
คุณโต้แย้งตัวเองด้วยการพูดว่า "โดยไม่ต้องยุ่งยาก" จากนั้นจึงแสดงรายการความซับซ้อนต่าง ๆ ที่คุณคิดว่าเป็นการเพิ่มคุณค่าในขณะที่ในกรณีส่วนใหญ่จะถูกทารุณกรรมเป็นสัตว์ประหลาดที่น่าเกลียดแทนที่จะใช้
Evgeny

3
นี่หมายถึงข้อเสียเปรียบที่ดีสคริปต์ของคุณจะไม่สามารถพกพาได้ในระบบที่ไม่มีไพ
ธ อน

1
ฉันรู้ว่าสิ่งนี้ได้รับคำตอบใน '08 (ตอนนี้เป็นเวลาสองวันก่อนหน้า '12) อย่างไรก็ตามสำหรับผู้ที่ดูในปีนี้ในภายหลังฉันจะเตือนผู้ใช้ไม่ให้หันกลับมาใช้ภาษาเช่น Python หรือ Ruby เนื่องจากมีแนวโน้มว่าจะพร้อมใช้งานและหากไม่เป็นคำสั่ง (หรือคลิกสองครั้ง) จากการติดตั้ง . หากคุณต้องการความสะดวกในการพกพาลองนึกถึงการเขียนโปรแกรมของคุณใน Java เพราะคุณจะกดยากเพื่อค้นหาเครื่องที่ไม่มี JVM ให้ใช้งาน
Wil Moore III

@astropanic สวยมากทุกพอร์ต Linux ด้วย Python ทุกวันนี้
Pithikos

@Pithikos แน่นอนและซอกับความยุ่งยากของ python2 เทียบกับ python3 ทุกวันนี้ฉันเขียนเครื่องมือทั้งหมดของฉันด้วยการไปและไม่สามารถมีความสุขได้
astropanic

9

ใช้ set -e เพื่อไม่ให้คุณไปข้างหน้าหลังจากเกิดข้อผิดพลาด ลองทำให้มันเข้ากันได้โดยไม่ต้องพึ่งพาทุบตีถ้าคุณต้องการที่จะทำงานบนไม่ได้ลินุกซ์


7

หากต้องการค้นหา "แนวทางปฏิบัติที่ดีที่สุด" ให้ดูว่า Linux distro's (เช่น Debian) เขียนสคริปต์เริ่มต้นของพวกเขาได้อย่างไร (มักพบใน /etc/init.d)

ส่วนใหญ่ไม่มี "bash-isms" และมีการแยกการตั้งค่าการกำหนดค่าที่ดีไลบรารีไฟล์และการจัดรูปแบบต้นฉบับ

สไตล์ส่วนตัวของฉันคือการเขียน master-shellscript ซึ่งกำหนดตัวแปรเริ่มต้นบางอย่างแล้วพยายามโหลด ("แหล่งที่มา") ไฟล์กำหนดค่าซึ่งอาจมีค่าใหม่

ฉันพยายามหลีกเลี่ยงฟังก์ชั่นเนื่องจากพวกเขามักจะทำให้สคริปต์ซับซ้อนขึ้น (Perl ถูกสร้างขึ้นเพื่อวัตถุประสงค์ดังกล่าว)

เพื่อให้แน่ใจว่าสคริปต์เป็นแบบพกพาให้ทดสอบไม่เพียงกับ #! / bin / sh แต่ยังใช้ #! / bin / ash, #! / bin / dash ฯลฯ คุณจะเห็นรหัสเฉพาะ Bash เร็ว ๆ นี้


-1

หรือข้อความเก่าที่คล้ายกับสิ่งที่ Joao กล่าวไว้:

"ใช้ Perl คุณจะต้องการรู้การทุบตี แต่ไม่ได้ใช้"

น่าเศร้าที่ฉันลืมผู้ที่พูดไป

และใช่วันนี้ฉันจะแนะนำ python มากกว่า perl

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.