<p>Does anyone know of any resources that talk about best practices or design patterns for shell scripts (sh, bash etc.)?</p>

[{'answer_id': 78509, 'author': 'Community', 'author_id': -1, 'author_profile': 'https://Stackoverflow.com/users/-1', 'pm_score': 3, 'selected': False, 'text': "<p>Easy:\nuse python instead of shell scripts.\nYou get a near 100 fold increase in readablility, without having to complicate anything you don't need, and preserving the ability to evolve parts of your script into functions, objects, persistent objects (zodb), distributed objects (pyro) nearly without any extra code.</p>\n"}, {'answer_id': 78526, 'author': 'user10392', 'author_id': 10392, 'author_profile': 'https://Stackoverflow.com/users/10392', 'pm_score': 3, 'selected': False, 'text': "<p>use set -e so you don't plow forward after errors. Try making it sh compatible without relying on bash if you want it to run on not-linux.</p>\n"}, {'answer_id': 79400, 'author': 'jtimberman', 'author_id': 7672, 'author_profile': 'https://Stackoverflow.com/users/7672', 'pm_score': 5, 'selected': False, 'text': '<p>Take a look at the <a href="http://www.tldp.org/LDP/abs/html/index.html" rel="noreferrer">Advanced Bash-Scripting Guide</a> for a lot of wisdom on shell scripting - not just Bash, either. </p>\n\n<p>Don\'t listen to people telling you to look at other, arguably more complex languages. If shell scripting meets your needs, use that. You want functionality, not fanciness. New languages provide valuable new skills for your resume, but that doesn\'t help if you have work that needs to be done and you already know shell.</p>\n\n<p>As stated, there aren\'t a lot of "best practices" or "design patterns" for shell scripting. Different uses have different guidelines and bias - like any other programming language. </p>\n'}, {'answer_id': 80140, 'author': 'Fhoxh', 'author_id': 14785, 'author_profile': 'https://Stackoverflow.com/users/14785', 'pm_score': 4, 'selected': False, 'text': '<p>There was a great session at OSCON this year (2008) on just this topic: <a href="http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf" rel="noreferrer">http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf</a></p>\n'}, {'answer_id': 86712, 'author': 'Paweł Hajdan', 'author_id': 9403, 'author_profile': 'https://Stackoverflow.com/users/9403', 'pm_score': 4, 'selected': False, 'text': "<p><strong>Know when to use it.</strong> For quick and dirty gluing commands together it's okay. If you need to make any more than few non-trivial decisions, loops, anything, go for Python, Perl, and <strong>modularize</strong>.</p>\n\n<p>The biggest problem with shell is often that end result just looks like a big ball of mud, 4000 lines of bash and growing... and you can't get rid of it because now your whole project depends on it. Of course, <strong>it started at 40 lines</strong> of beautiful bash.</p>\n"}, {'answer_id': 87339, 'author': 'Willem', 'author_id': 15447, 'author_profile': 'https://Stackoverflow.com/users/15447', 'pm_score': 3, 'selected': False, 'text': '<p>To find some "best practices", look how Linux distro\'s (e.g. Debian) write their init-scripts (usually found in /etc/init.d)</p>\n\n<p>Most of them are without "bash-isms" and have a good separation of configuration settings, library-files and source formatting.</p>\n\n<p>My personal style is to write a master-shellscript which defines some default variables, and then tries to load ("source") a configuration file which may contain new values.</p>\n\n<p>I try to avoid functions since they tend to make the script more complicated. (Perl was created for that purpose.)</p>\n\n<p>To make sure the script is portable, test not only with #!/bin/sh, but also use #!/bin/ash, #!/bin/dash, etc. You\'ll spot the Bash specific code soon enough.</p>\n'}, {'answer_id': 88790, 'author': 'pixelbeat', 'author_id': 4421, 'author_profile': 'https://Stackoverflow.com/users/4421', 'pm_score': 4, 'selected': False, 'text': '<p>shell script is a language designed to manipulate files and processes.\nWhile it\'s great for that, it\'s not a general purpose language,\nso always try to glue logic from existing utilities rather than\nrecreating new logic in shell script.</p>\n\n<p>Other than that general principle I\'ve collected some <a href="http://www.pixelbeat.org/programming/shell_script_mistakes.html" rel="noreferrer" title="common shell script mistakes">common shell script mistakes</a>.</p>\n'}, {'answer_id': 739034, 'author': 'Stefano Borini', 'author_id': 78374, 'author_profile': 'https://Stackoverflow.com/users/78374', 'pm_score': 9, 'selected': True, 'text': '<p>I wrote quite complex shell scripts and my first suggestion is "don\'t". The reason is that is fairly easy to make a small mistake that hinders your script, or even make it dangerous.</p>\n\n<p>That said, I don\'t have other resources to pass you but my personal experience. \nHere is what I normally do, which is overkill, but tends to be solid, although <em>very</em> verbose.</p>\n\n<p><strong>Invocation</strong></p>\n\n<p>make your script accept long and short options. be careful because there are two commands to parse options, getopt and getopts. Use getopt as you face less trouble.</p>\n\n<pre><code>CommandLineOptions__config_file=""\nCommandLineOptions__debug_level=""\n\ngetopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "$@"`\n\nif test $? != 0\nthen\n    echo "unrecognized option"\n    exit 1\nfi\n\neval set -- "$getopt_results"\n\nwhile true\ndo\n    case "$1" in\n        --config_file)\n            CommandLineOptions__config_file="$2";\n            shift 2;\n            ;;\n        --debug_level)\n            CommandLineOptions__debug_level="$2";\n            shift 2;\n            ;;\n        --)\n            shift\n            break\n            ;;\n        *)\n            echo "$0: unparseable option $1"\n            EXCEPTION=$Main__ParameterException\n            EXCEPTION_MSG="unparseable option $1"\n            exit 1\n            ;;\n    esac\ndone\n\nif test "x$CommandLineOptions__config_file" == "x"\nthen\n    echo "$0: missing config_file parameter"\n    EXCEPTION=$Main__ParameterException\n    EXCEPTION_MSG="missing config_file parameter"\n    exit 1\nfi\n</code></pre>\n\n<p>Another important point is that a program should always return zero if completes successfully, non-zero if something went wrong.</p>\n\n<p><strong>Function calls</strong></p>\n\n<p>You can call functions in bash, just remember to define them before the call. Functions are like scripts, they can only return numeric values. This means that you have to invent a different strategy to return string values. My strategy is to use a variable called RESULT to store the result, and returning 0 if the function completed cleanly. \nAlso, you can raise exceptions if you are returning a value different from zero, and then set two "exception variables" (mine: EXCEPTION and EXCEPTION_MSG), the first containing the exception type and the second a human readable message.</p>\n\n<p>When you call a function, the parameters of the function are assigned to the special vars $0, $1 etc. I suggest you to put them into more meaningful names. declare the variables inside the function as local:</p>\n\n<pre><code>function foo {\n   local bar="$0"\n}\n</code></pre>\n\n<p><strong>Error prone situations</strong></p>\n\n<p>In bash, unless you declare otherwise, an unset variable is used as an empty string. This is very dangerous in case of typo, as the badly typed variable will not be reported, and it will be evaluated as empty. use</p>\n\n<pre><code>set -o nounset\n</code></pre>\n\n<p>to prevent this to happen. Be careful though, because if you do this, the program will abort every time you evaluate an undefined variable. For this reason, the only way to check if a variable is not defined is the following:</p>\n\n<pre><code>if test "x${foo:-notset}" == "xnotset"\nthen\n    echo "foo not set"\nfi\n</code></pre>\n\n<p>You can declare variables as readonly:</p>\n\n<pre><code>readonly readonly_var="foo"\n</code></pre>\n\n<p><strong>Modularization</strong></p>\n\n<p>You can achieve "python like" modularization if you use the following code:</p>\n\n<pre><code>set -o nounset\nfunction getScriptAbsoluteDir {\n    # @description used to get the script path\n    # @param $1 the script $0 parameter\n    local script_invoke_path="$1"\n    local cwd=`pwd`\n\n    # absolute path ? if so, the first character is a /\n    if test "x${script_invoke_path:0:1}" = \'x/\'\n    then\n        RESULT=`dirname "$script_invoke_path"`\n    else\n        RESULT=`dirname "$cwd/$script_invoke_path"`\n    fi\n}\n\nscript_invoke_path="$0"\nscript_name=`basename "$0"`\ngetScriptAbsoluteDir "$script_invoke_path"\nscript_absolute_dir=$RESULT\n\nfunction import() { \n    # @description importer routine to get external functionality.\n    # @description the first location searched is the script directory.\n    # @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable\n    # @param $1 the .shinc file to import, without .shinc extension\n    module=$1\n\n    if test "x$module" == "x"\n    then\n        echo "$script_name : Unable to import unspecified module. Dying."\n        exit 1\n    fi\n\n    if test "x${script_absolute_dir:-notset}" == "xnotset"\n    then\n        echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying."\n        exit 1\n    fi\n\n    if test "x$script_absolute_dir" == "x"\n    then\n        echo "$script_name : empty script path. Dying."\n        exit 1\n    fi\n\n    if test -e "$script_absolute_dir/$module.shinc"\n    then\n        # import from script directory\n        . "$script_absolute_dir/$module.shinc"\n    elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset"\n    then\n        # import from the shell script library path\n        # save the separator and use the \':\' instead\n        local saved_IFS="$IFS"\n        IFS=\':\'\n        for path in $SHELL_LIBRARY_PATH\n        do\n            if test -e "$path/$module.shinc"\n            then\n                . "$path/$module.shinc"\n                return\n            fi\n        done\n        # restore the standard separator\n        IFS="$saved_IFS"\n    fi\n    echo "$script_name : Unable to find module $module."\n    exit 1\n} \n</code></pre>\n\n<p>you can then import files with the extension .shinc with the following syntax</p>\n\n<p>import "AModule/ModuleFile"</p>\n\n<p>Which will be searched in SHELL_LIBRARY_PATH. As you always import in the global namespace, remember to prefix all your functions and variables with a proper prefix, otherwise you risk name clashes. I use double underscore as the python dot.</p>\n\n<p>Also, put this as first thing in your module</p>\n\n<pre><code># avoid double inclusion\nif test "${BashInclude__imported+defined}" == "defined"\nthen\n    return 0\nfi\nBashInclude__imported=1\n</code></pre>\n\n<p><strong>Object oriented programming</strong></p>\n\n<p>In bash, you cannot do object oriented programming, unless you build a quite complex system of allocation of objects (I thought about that. it\'s feasible, but insane).\nIn practice, you can however do "Singleton oriented programming": you have one instance of each object, and only one.</p>\n\n<p>What I do is: i define an object into a module (see the modularization entry). Then I define empty vars (analogous to member variables) an init function (constructor) and member functions, like in this example code</p>\n\n<pre><code># avoid double inclusion\nif test "${Table__imported+defined}" == "defined"\nthen\n    return 0\nfi\nTable__imported=1\n\nreadonly Table__NoException=""\nreadonly Table__ParameterException="Table__ParameterException"\nreadonly Table__MySqlException="Table__MySqlException"\nreadonly Table__NotInitializedException="Table__NotInitializedException"\nreadonly Table__AlreadyInitializedException="Table__AlreadyInitializedException"\n\n# an example for module enum constants, used in the mysql table, in this case\nreadonly Table__GENDER_MALE="GENDER_MALE"\nreadonly Table__GENDER_FEMALE="GENDER_FEMALE"\n\n# private: prefixed with p_ (a bash variable cannot start with _)\np_Table__mysql_exec="" # will contain the executed mysql command \n\np_Table__initialized=0\n\nfunction Table__init {\n    # @description init the module with the database parameters\n    # @param $1 the mysql config file\n    # @exception Table__NoException, Table__ParameterException\n\n    EXCEPTION=""\n    EXCEPTION_MSG=""\n    EXCEPTION_FUNC=""\n    RESULT=""\n\n    if test $p_Table__initialized -ne 0\n    then\n        EXCEPTION=$Table__AlreadyInitializedException   \n        EXCEPTION_MSG="module already initialized"\n        EXCEPTION_FUNC="$FUNCNAME"\n        return 1\n    fi\n\n\n    local config_file="$1"\n\n      # yes, I am aware that I could put default parameters and other niceties, but I am lazy today\n      if test "x$config_file" = "x"; then\n          EXCEPTION=$Table__ParameterException\n          EXCEPTION_MSG="missing parameter config file"\n          EXCEPTION_FUNC="$FUNCNAME"\n          return 1\n      fi\n\n\n    p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e "\n\n    # mark the module as initialized\n    p_Table__initialized=1\n\n    EXCEPTION=$Table__NoException\n    EXCEPTION_MSG=""\n    EXCEPTION_FUNC=""\n    return 0\n\n}\n\nfunction Table__getName() {\n    # @description gets the name of the person \n    # @param $1 the row identifier\n    # @result the name\n\n    EXCEPTION=""\n    EXCEPTION_MSG=""\n    EXCEPTION_FUNC=""\n    RESULT=""\n\n    if test $p_Table__initialized -eq 0\n    then\n        EXCEPTION=$Table__NotInitializedException\n        EXCEPTION_MSG="module not initialized"\n        EXCEPTION_FUNC="$FUNCNAME"\n        return 1\n    fi\n\n    id=$1\n\n      if test "x$id" = "x"; then\n          EXCEPTION=$Table__ParameterException\n          EXCEPTION_MSG="missing parameter identifier"\n          EXCEPTION_FUNC="$FUNCNAME"\n          return 1\n      fi\n\n    local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = \'$id\'"`\n      if test $? != 0 ; then\n        EXCEPTION=$Table__MySqlException\n        EXCEPTION_MSG="unable to perform select"\n        EXCEPTION_FUNC="$FUNCNAME"\n        return 1\n      fi\n\n    RESULT=$name\n    EXCEPTION=$Table__NoException\n    EXCEPTION_MSG=""\n    EXCEPTION_FUNC=""\n    return 0\n}\n</code></pre>\n\n<p><strong>Trapping and handling signals</strong></p>\n\n<p>I found this useful to catch and handle exceptions.</p>\n\n<pre><code>function Main__interruptHandler() {\n    # @description signal handler for SIGINT\n    echo "SIGINT caught"\n    exit\n} \nfunction Main__terminationHandler() { \n    # @description signal handler for SIGTERM\n    echo "SIGTERM caught"\n    exit\n} \nfunction Main__exitHandler() { \n    # @description signal handler for end of the program (clean or unclean). \n    # probably redundant call, we already call the cleanup in main.\n    exit\n} \n\ntrap Main__interruptHandler INT\ntrap Main__terminationHandler TERM\ntrap Main__exitHandler EXIT\n\nfunction Main__main() {\n    # body\n}\n\n# catch signals and exit\ntrap exit INT TERM EXIT\n\nMain__main "$@"\n</code></pre>\n\n<p><strong>Hints and tips</strong></p>\n\n<p>If something does not work for some reason, try to reorder the code. Order is important and not always intuitive.</p>\n\n<p>do not even consider working with tcsh. it does not support functions, and it\'s horrible in general. </p>\n\n<p>Hope it helps, although please note. If you have to use the kind of things I wrote here, it means that your problem is too complex to be solved with shell. use another language. I had to use it due to human factors and legacy.</p>\n'}]
['https://Stackoverflow.com/questions/78497', 'https://Stackoverflow.com', 'https://Stackoverflow.com/users/14437/']