0

A number of scripts i've written accept user-parameters set within a separate .config file and sourced in:

source "./Example.config" || exit

In the interest of clarity and robustness, i'd like to allow users to include spaces within the .config file variables, such as:

############################################################
## Example
############################################################
VAR1 = 25
VAR2 = Y

Rather than:

VAR1=25
VAR2=Y

Is there a simple way to allow for this within the bash script? If so, how?

AnnaSchumann
  • 1,261
  • 10
  • 22
  • See this answer: http://stackoverflow.com/a/20815951/1314743 – Martin Nyolt Sep 16 '16 at 11:41
  • 4
    There's a security risk here, which you may be aware of but future readers may not be. Your design executes any commands that your user puts into the config file, with the permissions of the calling process. It may not matter in your use case, but other developers should consider parsing and verifying the structure of the input rather than just handing it to the shell. – Eric Sep 16 '16 at 12:03

2 Answers2

5

There's no need for external tools such as sed at all, or for the security risks involved in using source (being, as it is, a moral equivalent to eval). The following is a best-practices approach for bash 4.0 or newer:

shopt -s extglob # enable +()

declare -A config=( )
while IFS='=' read -r k v; do
  [[ $v ]] || continue        # skip lines that aren't assignments
  [[ $k = '#'* ]] && continue # skip comments
  k=${k%+([[:space:]])}       # trim trailing whitespace from keys
  v=${v#+([[:space:]])}       # trim leading whitespace from values
  config[$k]=$v
done <Example.config

This lets you refer to configuration-file data as ${config[foo]}, keeping it in a separate namespace from variables such as $foo. This is actually a better practice for security: It ensures that variables such as LD_PRELOAD or PATH that can impact system security can't be modified behind your back.

It also makes your configuration format more flexible, letting you use configuration option names that aren't valid shell variables (containing dashes, spaces, etc). For instance, you could allow:

# in your Example.config
server name = hello

...and...

# in your code
echo "Using server name: ${config['server name']}"
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • so given the O.P.s sample config file, can the original code continue to use `$VAR1`, or will it have to be converted to `${config[VAR1]}` , etc.? – shellter Sep 16 '16 at 15:47
  • 2
    @shellter, yes, it would need to be converted, but I'm arguing that it *should* be converted -- it'll be both more explicit and more secure when internal variable names can't be inadvertently overwritten by configuration. – Charles Duffy Sep 16 '16 at 15:48
  • Just wanted to be sure, I thought I might be missing something, but I haven't worked on projects where we had users writing config files. As always, I learn a great deal from reading your answer. Good luck to all. – shellter Sep 16 '16 at 15:54
  • @CharlesDuffy is correct that the use of a specific namespace for imported configuration is safer than allowing the configuration to override arbitrary variables in the caller. – Eric Sep 16 '16 at 21:19
  • @CharlesDuffy Great solution. Small typo - it should be \*# and not #\*. – AnnaSchumann Sep 19 '16 at 13:16
  • @AnnaSchumann, could you use backticks to code-format your comment? I'm presuming there are some asterisks in there, but can't tell where. Or if you want to propose an edit yourself, I'll take a look and approve it. – Charles Duffy Sep 19 '16 at 13:17
  • @CharlesDuffy Updated comment. When using #\* it throws errors and basically comments out the rest of the line. – AnnaSchumann Sep 19 '16 at 13:22
  • @AnnaSchumann, ahh. `[[ $k = "#"* ]]` does what I was looking for there -- `*#` is a quite different behavior (checking if the last character of the line is a `#`). – Charles Duffy Sep 19 '16 at 22:06
4

This bash snippet allows assignments with any amount of whitespace around the first equals sign without also exposing a command injection risk by using source to read variables.

exec 5< <(sed 's/ *= */=/' Example.config)
while IFS== read -u 5 n v; do
  printf -v "$n" "%s" "$v";
done
exec 5<&-

Here's a version that allows # comments and checks for legal variable names:

exec 5< <(sed  -e 's/ *= */=/' -e 's/#.*$//' Example.config)
while IFS== read -u 5 n v; do
  [[ "$n" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] && printf -v "$n" "%s" "$v"
done
exec 5<&-

EDIT: Added "%s" as suggested by Charles. He's completely right.

Eric
  • 1,431
  • 13
  • 14
  • 1
    I wouldn't bother with `sed`. Just do `read -r n eq v`, and skip lines where `$eq != =`. – chepner Sep 16 '16 at 12:59
  • @chepner, ...though in that case `foo=bar` is broken. – Charles Duffy Sep 16 '16 at 15:51
  • 1
    `printf -v "$n" %s "$v"` would be the better practice -- right now, this treats the name as a format string, not a literal value. – Charles Duffy Sep 16 '16 at 16:15
  • Just curious, by the way -- any particular reason for the `exec 5< <(...)` and `exec 5<&-`, rather than putting the `5< <(...)` immediately after the `done`, and thus scoping it that way? This code is every bit as correct, but not a stylistic choice I see often. – Charles Duffy Sep 16 '16 at 20:24
  • I didn't want to asume that the script wouldn't have need of STDIN inside the loop. I could imagine a tool prompting a user to fix invalid values, etc. while reading configuration. It was probably over-caution on my part. – Eric Sep 16 '16 at 21:17
  • I understand not using `while ... done < <(...)`; I was asking if there was a reason you didn't use `while ... done 5< <(...)`. – Charles Duffy Sep 16 '16 at 21:36
  • (now, there are *very* good reasons to do it with two explicit `exec`s if you're using bash 4.1's automatic FD allocation, since there exist versions of bash in which FDs opened that way aren't automatically closed, but that's neither here nor there). – Charles Duffy Sep 16 '16 at 21:37
  • Thanks @CharlesDuffy. I didn't know that `5< ...` would work. I'd initially written this to use the auto FD feature, but that doesn't work on OS X's ancient bash, so I adapted it. – Eric Sep 16 '16 at 23:17
  • *nod*. These days, I mostly tend to advise folks on OS X to install a modern bash through macports and use a `#!/usr/bin/env bash` shebang. – Charles Duffy Sep 16 '16 at 23:38