I need to create a config file for my own script:
Here is an example:


source /home/myuser/test/config
echo "Name=$nam" >&2
echo "Surname=$sur" >&2

Content of /home/myuser/test/config:


that works!

My question: is this the correct way to do this or there're other ways?

  • The variables should be at the top. I'm surprised it works. Anyway, why do you need a config file? Are you planning to use these variables somewhere else? Dec 23 '14 at 17:01
  • Faheem, I need the variables because my script has many options: using a config file will semplify the script. Thanks
    – Pol Hallen
    Dec 23 '14 at 17:05
  • 6
    IMHO its fine. I would do this way. Dec 23 '14 at 17:11
  • abcde also does it this way and that is a quite big program (for a shell script). You can have a look at it here.
    – Lucas
    Dec 23 '14 at 21:04

10 Answers 10


source is not secure as it will execute arbitrary code. This may not be a concern for you, but if file permissions are incorrect, it may be possible for an attacker with filesystem access to execute code as a privileged user by injecting code into a config file loaded by an otherwise-secured script such as an init script.

So far, the best solution I've been able to identify is the clumsy reinventing-the-wheel solution:


echo rm -rf /
PROMPT_COMMAND='echo "Sending your last command $(history 1) to my email"'
hostname=localhost; echo rm -rf /

Using source, this would run echo rm -rf / twice, as well as change the running user's $PROMPT_COMMAND. Instead, do this:

myscript.sh (Bash 4)

typeset -A config # init array
config=( # set default values in config array

while read line
    if echo $line | grep -F = &>/dev/null
        varname=$(echo "$line" | cut -d '=' -f 1)
        config[$varname]=$(echo "$line" | cut -d '=' -f 2-)
done < myscript.conf

echo ${config[username]} # should be loaded from defaults
echo ${config[password]} # should be loaded from config file
echo ${config[hostname]} # includes the "injected" code, but it's fine here
echo ${config[PROMPT_COMMAND]} # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

myscript.sh (Mac/Bash 3-compatible)

config() {
    val=$(grep -E "^$1=" myscript.conf 2>/dev/null || echo "$1=__DEFAULT__" | head -n 1 | cut -d '=' -f 2-)

    if [[ $val == __DEFAULT__ ]]
        case $1 in
                echo -n "root"
                echo -n ""
                echo -n "localhost"
        echo -n $val

echo $(config username) # should be loaded from defaults
echo $(config password) # should be loaded from config file
echo $(config hostname) # includes the "injected" code, but it's fine here
echo $(config PROMPT_COMMAND) # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

Please reply if you find a security exploit in my code.

  • 1
    FYI this is a Bash version 4.0 solution which sadly is subject to insane licensing issues imposed by Apple and is not available by default on Macs
    – Sukima
    Dec 6 '16 at 16:16
  • @Sukima Good point. I've added a version that is compatible with Bash 3. Its weakness is that it will not handle * in inputs properly, but then what in Bash handles that character well?
    – Mikkel
    Dec 6 '16 at 20:16
  • The first script fails if the password contains a backslash.
    – Kusalananda
    Feb 27 '17 at 20:06
  • @Kusalananda What if the backslash is escaped? my\\password
    – Mikkel
    Feb 27 '17 at 23:38
  • This version takes several seconds to process my config file, I found the solution from gw0 to be much faster.
    – scharette
    Mar 4 '20 at 18:42

Here is a clean and portable version which is compatible with Bash 3 and up, on both Mac and Linux.

It specifies all defaults in a separate file, to avoid the need for a huge, cluttered, duplicated "defaults" config function in all of your shell scripts. And it lets you choose between reading with or without default fallbacks:


myvar=Hello World


myvar=Default Value
othervar=Another Variable

config.shlib (this is a library, so there is no shebang-line):

config_read_file() {
    (grep -E "^${2}=" -m 1 "${1}" 2>/dev/null || echo "VAR=__UNDEFINED__") | head -n 1 | cut -d '=' -f 2-;

config_get() {
    val="$(config_read_file config.cfg "${1}")";
    if [ "${val}" = "__UNDEFINED__" ]; then
        val="$(config_read_file config.cfg.defaults "${1}")";
    printf -- "%s" "${val}";

test.sh (or any scripts where you want to read config values):

#!/usr/bin/env bash
source config.shlib; # load the config library functions
echo "$(config_get myvar)"; # will be found in user-cfg
printf -- "%s\n" "$(config_get myvar)"; # safer way of echoing!
myvar="$(config_get myvar)"; # how to just read a value without echoing
echo "$(config_get othervar)"; # will fall back to defaults
echo "$(config_get bleh)"; # "__UNDEFINED__" since it isn't set anywhere

Explanation of the test script:

  • Note that all usages of config_get in test.sh are wrapped in double quotes. By wrapping every config_get in double quotes, we ensure that text in the variable value will never be misinterpreted as flags. And it ensures that we preserve whitespace properly, such as multiple spaces in a row in the config value.
  • And what's that printf line? Well, it's something you should be aware of: echo is a bad command for printing text that you have no control over. Even if you use double quotes, it will interpret flags. Try setting myvar (in config.cfg) to -e and you will see an empty line, because echo will think that it's a flag. But printf doesn't have that problem. The printf -- says "print this, and don't interpret anything as flags", and the "%s\n" says "format the output as a string with a trailing newline, and lastly the final parameter is the value for printf to format.
  • If you aren't going to be echoing values to the screen, then you'd simply assign them normally, like myvar="$(config_get myvar)";. If you're going to print them to the screen, I suggest using printf to be totally safe against any echo-incompatible strings that may be in the user config. But echo is fine if the user-provided variable isn't the first character of the string you are echoing, since that's the only situation where "flags" could be interpreted, so something like echo "foo: $(config_get myvar)"; is safe, since the "foo" doesn't begin with a dash and therefore tells echo that the rest of the string isn't flags for it either. :-)
  • @user2993656 Thanks for spotting that my original code still had my private config filename (environment.cfg) in it instead of the correct one. As for the "echo -n" edit you did, that depends on the shell used. On Mac/Linux Bash, "echo -n" means "echo without trailing newline", which I did to avoid trailing newlines. But it seems to work exactly the same without it, so thanks for the edits!
    – gw0
    Feb 27 '17 at 19:12
  • Actually, I just went through and rewrote it to use printf instead of echo, which ensures that we'll get rid of the risk of echo misinterpreting "flags" in the config values.
    – gw0
    Feb 27 '17 at 19:49
  • 1
    I really like this version. I dropped the config.cfg.defaults in lieu of defining them at the time of calling $(config_get var_name "default_value"). tritarget.org/static/…
    – Sukima
    Oct 22 '17 at 4:12
  • Likewise - this is great.
    – Simeon
    Apr 4 '20 at 10:06

Parse the configuration file, don't execute it.

I'm currently writing an application at work that uses an extremely simple XML configuration:


In the shell script (the "application"), this is what I do to get at the username (more or less, I've put it in a shell function):

username=$( xmlstarlet sel -t -v '/config/username' "$config_file" )

The xmlstarlet command is XMLStarlet, which is available for most Unices. On some systems, it's installed as xml.

I'm using XML since other parts of the application also deals with data encoded in XML files, so it was easiest.

If you prefer JSON, there's jq which is an easy to use shell JSON parser.

My configuration file would look something like this in JSON:

  "username": "username-or-email",
  "password": "the-password"      

And then I'd be getting the username in the script:

username=$( jq -r .username "$config_file" )

There is also TOML ("Tom's Obvious Minimal Language"), with parsers for several languages. My personal favorite parser, tomlq, is part of the yq distribution from https://kislyuk.github.io/yq/ (uses jq behind the scenes).

The TOML version of your configuration file would look like

username = "username-or-email"
password = "the-password"

(the strings would be JSON-encoded)... and getting data out from it would be as simple as in the JSON case (since tomlq is built on top of jq):

username=$( tomlq -r .username "$config_file" )
  • Executing the script has a number of advantages and disadvantages. The main disadvantages are security, if someone can alter the config file then they can execute code, and it is harder to make it idiot proof. The advantages are speed, on a simple test it is more than 10,000 times faster to source a config file than to run pq, and flexibility, anyone who likes monkey patching python will appreciate this.
    – icarus
    Jan 4 '17 at 21:33
  • @icarus How big configuration files do you usually encounter, and how often do you need to parse them in one session? Notice too that several values may be had out of XML or JSON in one go.
    – Kusalananda
    Jan 4 '17 at 21:36
  • Usually only a few (1 to 3) values. If you are using eval to set multiple values then you are executing selected parts of the config file :-).
    – icarus
    Jan 4 '17 at 22:39
  • 1
    @icarus I was thinking arrays... No need to eval anything. The performance hit of using a standard format with an existing parser (even though it's an external utility) is negligible in comparison to the robustness, the amount of code, the ease of use, and maintainability.
    – Kusalananda
    Jan 4 '17 at 23:32
  • 2
    +1 for "parse the config file, don't execute it"
    – Iiridayn
    Jul 3 '17 at 22:56

The most common, efficient and correct way is to use source, or . as a shorthand form. For example:

source /home/myuser/test/config


. /home/myuser/test/config

Something to consider, however, is the security issues that using an additional externally-sourced configuration file can raise, given that additional code can be inserted. For more information, including on how to detect and resolve this issue, I would recommend taking a look at the 'Secure it' section of http://wiki.bash-hackers.org/howto/conffile#secure_it

  • 8
    I had high hopes for that article (came up in my search results too), but the author's suggestion of attempting to use regex to filter out malicious code is an exercise in futility.
    – Mikkel
    May 28 '15 at 23:02
  • The procedure with dot, requires an absolute path? With the relative one it doesn't work
    – Davide
    Oct 23 '18 at 11:50

I use this in my scripts:

sed_escape() {
  sed -e 's/[]\/$*.^[]/\\&/g'

cfg_write() { # path, key, value
  cfg_delete "$1" "$2"
  echo "$2=$3" >> "$1"

cfg_read() { # path, key -> value
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" | sed "s/^$(echo "$2" | sed_escape)=//" | tail -1

cfg_delete() { # path, key
  test -f "$1" && sed -i "/^$(echo $2 | sed_escape).*$/d" "$1"

cfg_haskey() { # path, key
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" > /dev/null

Should support every character combination, except keys can't have = in them, since that's the separator. Anything else works.

% cfg_write test.conf mykey myvalue
% cfg_read test.conf mykey
% cfg_delete test.conf mykey
% cfg_haskey test.conf mykey || echo "It's not here anymore"
It's not here anymore

Also, this is completely safe since it doesn't use source or eval.

  • Isn't this supposed to first create the config file if it doesn't exist? It doesn't, but touch -a "${path}" will of course ensure it exists, also without frivolously updating its mtime.
    – Asclepius
    Oct 25 '20 at 0:48

This is succinct and secure:

# Read common vars from common.vars
# the incantation here ensures (by env) that only key=value pairs are present
# then declare-ing the result puts those vars in our environment 
declare $(env -i `cat common.vars`)

The -i ensures you only get the variables from common.vars

Update: An illustration of the security is that

env -i 'touch evil1 foo=omg boo=$(touch evil2)'

Will not produce any touched files. Tested on mac with bash, i.e. using bsd env.

  • Just see how evil1 and evil2 files are created if you put this to common.vars ``` touch evil1 foo=omg boo=$(touch evil2) ```
    – pihentagy
    Mar 3 '20 at 13:12
  • 1
    @pihentagy For me the following produces no touched files env -i 'touch evil1 foo=omg boo=$(touch evil2)'. Running on mac.
    – Marcin
    Mar 3 '20 at 16:08
  • 1
    indeed, but cannot access foo. I've tried env -i ... myscript.sh and inside that script foo is not defined. However, if you remove "garbage", it will work. So thanks for explaining. :+1:
    – pihentagy
    Mar 5 '20 at 9:03

Most users, although not many containers, already have the git binary. Why not therefore use git config for application configuration management using a dedicated non-conflicting configuration file as in the examples below?

# Set
$ git config -f ~/.myapp core.mykey myval

# Get
$ git config -f ~/.myapp core.mykey

# Get invalid
$ git config -f ~/.myapp core.mykey
$ echo $?

# List
git config -f ~/.myapp -l

# View
$ cat ~/.myapp 
    mykey = myval

For additional commands, see its man page. It is wise to first ensure the configuration file exists:

touch -a ~/.myapp

This one seems safe and short. Feel free to crack this ruthlessly. I'd like to know of a better way.


while read LINE; do declare "$LINE"; done < evil.conf

I'm using bash 4.3.48.

It's also compliant with bash --posix. See bottom for test.

But sh doesn't support it because of declare.

Basic test for those who want proof

Create file evil.conf

echo > evil.conf '
C=$(echo hello)

# Could produce side-effect
D=`touch evil`
E=$(ping -n 3)
echo hello

# Could produce visible side-effect
touch evil2
ping -n 3

Load the config with the snippet

while read LINE; do declare "$LINE"; done < evil.conf

Output (see sanitizer in action)

bash: declare: `': not a valid identifier
bash: declare: `': not a valid identifier
bash: declare: `# Could produce side-effect': not a valid identifier
bash: declare: `echo hello': not a valid identifier
bash: declare: `': not a valid identifier
bash: declare: `# Could produce visible side-effect': not a valid identifier
bash: declare: `touch evil2': not a valid identifier
bash: declare: `ping -n 3': not a valid identifier

Let's check values now

for V in A B C D E F; do declare -p $V; done

declare -- A="1"
declare -- B="2"
declare -- C="\$((1+2))"
declare -- D="\`touch evil\`"
declare -- E="\$(ping -n 3)"
declare -- F="ok"

Check side effects (no side effects):

ls evil evil2

ls: cannot access 'evil': No such file or directory
ls: cannot access 'evil2': No such file or directory

Appendix. Test bash --posix

bash -c 'while read LINE; do declare "$LINE"; done < evil.conf; for V in A B C D E F; do declare -p $V; done' --posix

For my scenario, source or . was fine, but I wanted to support local environment variables (ie, FOO=bar myscript.sh) taking precedence over configured variables. I also wanted the config file to be user editable and comfortable to someone used to sourced config files, and to keep it as small/simple as possible, to not distract from the main thrust of my very small script.

This is what I came up with:

if [ ! -f $CONF ]; then
    cat > $CONF << CONF
VAR1="default value"
. <(sed 's/^\([^=]\+\) *= *\(.*\)$/\1=${\1:-\2}/' < $CONF)

Essentially - it checks for variable definitions (without being very flexible about whitespace) and rewrites those lines so that the value is converted to a default for that variable, and the variable is unmodified if found, like the XDG_CONFIG_HOME variable above. It sources this altered version of the config file and continues on.

Future work could make the sed script more robust, filter out lines that look weird or aren't definitions, etc, not break on end of line comments - but this is good enough for me for now.


You can do it:

cat > /home/myuser/test/config << EOF

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.