Shell – `read` command not working in a Makefile

makeshellvariable

I have a make script to perform 3 tasks:

  1. Import a MySQL database
  2. Move a configuration file
  3. Configure the configuration file

For these tasks, the script requires 3 inputs:

  1. MySQL Host
  2. MySQL Username
  3. MySQL Password

For some reason, whenever I read an input, it saves to the right variable, but the content is the variable name that I save it to without the first letter. I can't seem to find out why it does that.

Here's the Makefile:

SHELL := /bin/bash

default:
        @echo "Welcome!";\
        echo -n "Please enter the MySQL host (default: localhost):";\
        read host;\
        host=${host:-localhost};\
        echo -n "Please enter the MySQL username:";\
        read username;\
        echo -n "Please enter the MySQL password:";\
        read -s password;\
        mv includes/config.php.example includes/config.php 2>/dev/null;true;\
        sed 's/"USER", ""/"USER", "$(username)"/g' includes/config.php > includes/config.php;\
        sed 's/"PASSWORD", ""/"PASSWORD", "$(password)"/g' includes/config.php > includes/conf$
        echo $username;\
        echo $password;\
        mysql -u "$username" -p"$password" codeday-team < ./codeday-team.sql;\
        echo "Configuration complete. For further configuration options, check the config file$
        exit 0;

The output is:

Welcome!
Please enter the MySQL host (default: localhost):
Please enter the MySQL username:<snip>    
Please enter the MySQL password:sername
assword
ERROR 1045 (28000): Access denied for user 'sername'@'localhost' (using password: YES)
Configuration complete. For further configuration options, check the config file includes/config.php

As you can see, it outputs sername and assword. For the life of me I can't fix this!

While the purpose of this post is to solve the read bug, I would appreciate any advice, bugs reports, or suggestions. I may award a bounty for them.

Thank you for helping!

Best Answer

You're mixing shell and make variables in there. Both make and the shell use $ for their variables.

In Makefile, variables are $(var) or $v for single-letter variables, and $var or ${var} in shells.

But if you write $var in a Makefile, make will understand it as $(v)ar. If you want to pass a literal $ to the shell, you need to enter it as $$, as in $$var or $${var} so that it becomes $var or ${var} for the shell.

Also, make runs sh, not bash to interpret that code (Edit, sorry missed your SHELL := /bin/bash above, note that many systems don't have bash in /bin if they have bash at all and := is GNU specific), so you need to use sh syntax there. echo -n, read -s are zsh/bash syntax, not sh syntax.

Best here would be to add a zsh/bash script to do that (and add a build dependency on bash). Something like:

#! /usr/bin/env bash
printf 'Welcome\nPlease enter the MySQL host (default: localhost): '
read host || exit
host=${host:-localhost}
printf "Please enter the MySQL username: "
read username || exit
printf "Please enter the MySQL password:"
IFS= read -rs password || exit
printf '\n'
mv includes/config.php.example includes/config.php 2>/dev/null
repl=${password//\\/\\\\}
repl=${repl//&/\\&}
repl=${repl//:/\\:}
{ rm includes/config.php && 
  sed 's/"USER", ""/"USER", "'"$username"'"/g
       s:"PASSWORD", "":"PASSWORD", "'"$repl"'"/g' > includes/config.php
} < includes/config.php || exit
mysql -u "$username" -p"$password" codeday-team < ./codeday-team.sql || exit

echo "Configuration complete. For further configuration options, check the config file"

It addresses a few more issues:

  • you need -r when reading the password if you want to allow the user to use backslashes in their password.
  • you need IFS= if you want to allow the user to have a password that starts or ends in blanks
  • same would apply for the username, but here we're making the assumption that the user won't use anything silly for the username, and the blank stripping and backslash handling could be seen as a /feature/.
  • your ;true after mv doesn't do what you think it does. It doesn't cancel the effect of the errexit option (for make implementations that call the shell with -e). You'd need ||true instead. Here, we're not using errexit but doing the error handling ourselves with || exit where needed.
  • You need to escape backslash, & and the s:pattern:repl: separator (here :) or it won't work (and could have nasty side effects).
  • You can't do sed ... < file > file, as file would be truncated before sed is even started. Some sed implementations support a -i or -i '' option for that. Alternatively you could use perl -pi. Here we're doing the equivalent of perl -pi manually (delete and recreate the input file after it has been open for reading) but without taking care of the file's metadata.

    Here, it would be better to use the example one as input and the final one as output.

It still doesn't address a few more issues:

  • the new config.php is created with permissions derived from the current umask which is likely to be world readable and owned by the user running make. You may need to adapt the umask and/or change ownership if the includes dir is not otherwise restricted as that file contains sensitive information.
  • if the password contains " characters, that will likely break (this time for php). You'd want to either forbid those (by returning an error) or add another layer of escaping for them in the right syntax for that php file. You're likely to have similar problems with backslash and you may want to exclude non-ascii or control characters as well.
  • Passing the password on the command-line of mysql is generally a bad idea as that means it shows in the output of ps -f. It would be better to use:

     mysql --defaults-file=<(
       printf '[client]\nuser=%s\npassword="%s"\n' "$username" "$password") ...
    

    printf being built-in, it wouldn't show up in ps output.

  • the $host variable is not used.

  • prompting the user like that means that your script can not easily be automated. You could take the input from arguments or (better for the password) with environment variables instead.
Related Question