What Are the Rules for Valid Identifiers (E.G. Functions, Vars, etc) in Bash

What are the rules for valid identifiers (e.g. functions, vars, etc) in Bash?

From the manual:

   Shell Function Definitions
...
name () compound-command [redirection]
function name [()] compound-command [redirection]

name is defined elsewhere:

       name   A  word  consisting  only  of alphanumeric characters and under‐
scores, and beginning with an alphabetic character or an under‐
score. Also referred to as an identifier.

So hyphens are not valid. And yet, on my system, they do work...

$ bash --version
GNU bash, version 4.2.25(1)-release (x86_64-pc-linux-gnu)

Bash script - user input 'not an identifier:'

You can't use - in variable names. Only letters, digits, and _ are allowed.

Try:

read -r app_name
npx create-react-app "${app_name}"

How to define completion for function name pattern ?

Use compgen -A function to list all functions; then grep -E '.*_dir$' to filter out functions ending with _dir.

complete -A directory $(compgen -A function | grep -E '.*dir$')

This assumes that all function names ending with _dir are free of special symbols like * and spaces. According to the specification, this should always be the case.

This command will only set up completion for already existing functions. To automatically add completion for functions you will define in an interactive session, add the command to PROMPT_COMMAND so that it will be executed at every prompt.

PROMPT_COMMAND="complete -A directory \$(compgen -A function | grep -E '.*dir$')"

Pass commandline args into another script

You need double quotes around $test_command in buid-and-run-node.sh, otherwise that variable is being split on the white space and appears to contain arguments for buid-and-run.sh. Like this:

$DIR/build-and-run.sh -n $container_name -c "$test_command" -s $src -f $DIR/../dockerfiles/dockerfile_node

Further Info

As the comment below rightly points out, it's good practice to quote all variables in Bash, unless you know you want them off (for example, to enable shell globbing). It's also helpful, at least in cases where the variable name is part of a larger word, to use curly braces to delineate the variable name. This is to prevent later characters from being treated as part of the variable name if they're legal. So a better command call might look like:

"${DIR}/build-and-run.sh" -n "$container_name" -c "$test_command" -s "$src" -f "${DIR}/../dockerfiles/dockerfile_node"

The 'eval' command in Bash and its typical uses

eval takes a string as its argument, and evaluates it as if you'd typed that string on a command line. (If you pass several arguments, they are first joined with spaces between them.)

${$n} is a syntax error in bash. Inside the braces, you can only have a variable name, with some possible prefix and suffixes, but you can't have arbitrary bash syntax and in particular you can't use variable expansion. There is a way of saying “the value of the variable whose name is in this variable”, though:

echo ${!n}
one

$(…) runs the command specified inside the parentheses in a subshell (i.e. in a separate process that inherits all settings such as variable values from the current shell), and gathers its output. So echo $($n) runs $n as a shell command, and displays its output. Since $n evaluates to 1, $($n) attempts to run the command 1, which does not exist.

eval echo \${$n} runs the parameters passed to eval. After expansion, the parameters are echo and ${1}. So eval echo \${$n} runs the command echo ${1}.

Note that most of the time, you must use double quotes around variable substitutions and command substitutions (i.e. anytime there's a $): "$foo", "$(foo)". Always put double quotes around variable and command substitutions, unless you know you need to leave them off. Without the double quotes, the shell performs field splitting (i.e. it splits value of the variable or the output from the command into separate words) and then treats each word as a wildcard pattern. For example:

$ ls
file1 file2 otherfile
$ set -- 'f* *'
$ echo "$1"
f* *
$ echo $1
file1 file2 file1 file2 otherfile
$ n=1
$ eval echo \${$n}
file1 file2 file1 file2 otherfile
$eval echo \"\${$n}\"
f* *
$ echo "${!n}"
f* *

eval is not used very often. In some shells, the most common use is to obtain the value of a variable whose name is not known until runtime. In bash, this is not necessary thanks to the ${!VAR} syntax. eval is still useful when you need to construct a longer command containing operators, reserved words, etc.

How to return a string value from a Bash function

There is no better way I know of. Bash knows only status codes (integers) and strings written to the stdout.

Allowed characters in Linux environment variable names

From The Open Group:

These strings have the form
name=value; names shall not contain
the character '='.
For values to be
portable across systems conforming to
IEEE Std 1003.1-2001, the value shall
be composed of characters from the
portable character set (except NUL
and as indicated below
).

So names may contain any character except = and NUL, but:

Environment variable names used by the utilities in the Shell and
Utilities volume of IEEE Std
1003.1-2001 consist solely of uppercase letters, digits, and the '_'
(underscore)
from the characters
defined in Portable Character Set and
do not begin with a digit
. Other
characters may be permitted by an
implementation; applications shall
tolerate the presence of such names.

So while the names may be valid, your shell might not support anything besides letters, numbers, and underscores.

Exclamation point in bash function name - should I?

Out of burning curiousity, why is it so much more convenient to use the exclamation point in your function name?

Generally, for portability reasons, you may not want to use the bang; just because the interpreter on that particular OS accepts it, if you need to deploy that script elsewhere, other interpreters of slightly different flavors/versions may not be as accepting.

Design patterns or best practices for shell scripts

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.

That said, I don't have other resources to pass you but my personal experience.
Here is what I normally do, which is overkill, but tends to be solid, although very verbose.

Invocation

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.

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

Another important point is that a program should always return zero if completes successfully, non-zero if something went wrong.

Function calls

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.
Also, 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.

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:

function foo {
local bar="$0"
}

Error prone situations

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

set -o nounset

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:

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

You can declare variables as readonly:

readonly readonly_var="foo"

Modularization

You can achieve "python like" modularization if you use the following code:

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
}

you can then import files with the extension .shinc with the following syntax

import "AModule/ModuleFile"

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.

Also, put this as first thing in your module

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

Object oriented programming

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).
In practice, you can however do "Singleton oriented programming": you have one instance of each object, and only one.

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

# 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
}

Trapping and handling signals

I found this useful to catch and handle exceptions.

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 "$@"

Hints and tips

If something does not work for some reason, try to reorder the code. Order is important and not always intuitive.

do not even consider working with tcsh. it does not support functions, and it's horrible in general.

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.



Related Topics



Leave a reply



Submit