7

my python file has these 2 variables:

week_date = "01/03/16-01/09/16"
cust_id = "12345"

how can i read this into a shell script that takes in these 2 variables?

my current shell script requires manual editing of "dt" and "id". I want to read the python variables into the shell script so i can just edit my python parameter file and not so many files.

shell file:

#!/bin/sh

dt="01/03/16-01/09/16" 
cust_id="12345"

In a new python file i could just import the parameter python file.

3
  • They're set where? At module level? Commented Jan 12, 2016 at 19:13
  • it is at module level Commented Jan 12, 2016 at 19:14
  • 2
    BTW, using the #!/bin/sh shebang or invoking it with sh scriptname makes your script a POSIX sh script, not a bash script. Since your question is tagged bash, I'm assuming that that was a mistake and that you meant to use bash. Commented Jan 12, 2016 at 21:15

4 Answers 4

12

Consider something akin to the following:

#!/bin/bash
#      ^^^^ NOT /bin/sh, which doesn't have process substitution available.

python_script='
import sys
d = {}                                 # create a dict to store values in
exec(open(sys.argv[1], "r").read(), d) # exec code with that dict as namespace
for k in sys.argv[2:]:
  print("%s\0" % str(d[k]).split("\0")[0]) # extract values
'

read_python_vars() {
  local python_file=$1; shift
  local varname
  for varname; do
    IFS= read -r -d '' "${varname#*:}"
  done < <(python -c "$python_script" "$python_file" "${@%%:*}")
}

You might then use this as:

read_python_vars config.py week_date:dt cust_id:id
echo "Customer id is $id; date range is $dt"

...or, if you didn't want to rename the variables as they were read, simply:

read_python_vars config.py week_date cust_id
echo "Customer id is $cust_id; date range is $week_date"

Advantages:

  • Unlike a naive regex-based solution (which would have trouble with some of the details of Python parsing -- try teaching sed to handle both raw and regular strings, and both single and triple quotes without making it into a hairball!) or a similar approach that used newline-delimited output from the Python subprocess, this will correctly handle any object for which str() gives a representation with no NUL characters that your shell script can use.

  • Running content through the Python interpreter also means you can determine values programmatically -- for instance, you could have some Python code that asks your version control system for the last-change-date of relevant content.

    Think about scenarios such as this one:

    start_date = '01/03/16'
    end_date = '01/09/16'
    week_date = '%s-%s' % (start_date, end_date)
    

    ...using a Python interpreter to parse Python means you aren't restricting how people can update/modify your Python config file in the future.

Now, let's talk caveats:

  • If your Python code has side effects, those side effects will obviously take effect (just as they would if you chose to import the file as a module in Python). Don't use this to extract configuration from a file whose contents you don't trust.
  • Python strings are Pascal-style: They can contain literal NULs. Strings in shell languages are C-style: They're terminated by the first NUL character. Thus, some variables can exist in Python than cannot be represented in shell without nonliteral escaping. To prevent an object whose str() representation contains NULs from spilling forward into other assignments, this code terminates strings at their first NUL.

Now, let's talk about implementation details.

  • ${@%%:*} is an expansion of $@ which trims all content after and including the first : in each argument, thus passing only the Python variable names to the interpreter. Similarly, ${varname#*:} is an expansion which trims everything up to and including the first : from the variable name passed to read. See the bash-hackers page on parameter expansion.

  • Using <(python ...) is process substitution syntax: The <(...) expression evaluates to a filename which, when read, will provide output of that command. Using < <(...) redirects output from that file, and thus that command (the first < is a redirection, whereas the second is part of the <( token that starts a process substitution). Using this form to get output into a while read loop avoids the bug mentioned in BashFAQ #24 ("I set variables in a loop that's in a pipeline. Why do they disappear after the loop terminates? Or, why can't I pipe data to read?").

  • The IFS= read -r -d '' construct has a series of components, each of which makes the behavior of read more true to the original content:

    • Clearing IFS for the duration of the command prevents whitespace from being trimmed from the end of the variable's content.
    • Using -r prevents literal backslashes from being consumed by read itself rather than represented in the output.
    • Using -d '' sets the first character of the empty string '' to be the record delimiter. Since C strings are NUL-terminated and the shell uses C strings, that character is a NUL. This ensures that variables' content can contain any non-NUL value, including literal newlines.

    See BashFAQ #001 ("How can I read a file (data stream, variable) line-by-line (and/or field-by-field)?") for more on the process of reading record-oriented data from a string in bash.

Sign up to request clarification or add additional context in comments.

7 Comments

if i keep variable name "dt" in the shell script, will it just be dt=$week_date after the code above?
should there be a space after the second < to be < < (python -c "$python_script" filename.py week_date id)
@jxn, see edits; I've made variable renaming in the process explicit, and tried to address your questions about <() syntax.
@jxn, ...btw, if you're getting an error about that syntax, it probably means your code is running with /bin/sh rather than with bash.
@EdMorton, thank you for the nudge to update this for Python 3.
|
4

Other answers give a way to do exactly what you ask for, but I think the idea is a bit crazy. There's a simpler way to satisfy both scripts - move those variables into a config file. You can even preserve the simple assignment format.

Create the config itself: (ini-style)

dt="01/03/16-01/09/16"
cust_id="12345"

In python:

config_vars = {}
with open('the/file/path', 'r') as f:
    for line in f:
        if '=' in line:
            k,v = line.split('=', 1)
            config_vars[k] = v
week_date = config_vars['dt']
cust_id = config_vars['cust_id']

In bash:

source "the/file/path"

And you don't need to do crazy source parsing anymore. Alternatively you can just use json for the config file and then use json module in python and jq in shell for parsing.

6 Comments

The only thing I might change about this -- if you're going to have Python code parsing something written in shell, consider using shlex to actually get shell-compatible (well, POSIX-sh-compatible; it doesn't support bash extensions such as $'') lexing; that way you get support for multi-line assignments and the like.
So I was thinking of config being ini-style, not bash style (otherwise we're back at the crazy "parse other language" problem). If you need escaping, multiline or anything else, just go with json instead...
Something like: import shlex; config_vars = dict([item.split('=', 1) for item in shlex.split(open('the/file/path').read()) if '=' in item]); that way you get the escaping, multiline, &c. support and don't have to write the parser yourself. :)
...otherwise you get bugs where source "file" -- by virtue of running through a real shell parser that interpolates expansions inside double quotes, treats foo=bar baz as an invocation of the command baz with the environment variable foo transiently set, etc -- doesn't do the exact same thing as your Python code does. Well, you have those bugs regardless, but more of them without shlex.
...which is to say: If you really want to parse an INI-style file correctly in bash, even one with no comments or section headers, it's more complex than just calling source input.ini. Not necessarily much more -- could look like this: while IFS="=" read -r var value; do printf -v "$var" '%s' "$value"; done <input.ini, and if the shell were modern bash (printf -v isn't available in baseline POSIX sh), that would suffice.
|
0

I would do something like this. You may want to modify it little bit for minor changes to include/exclude quotes as I didn't really tested it for your scenario:

#!/bin/sh
exec <$python_filename
while read line
do
        match=`echo $line|grep "week_date ="`
        if [ $? -eq 0 ]; then
                dt=`echo $line|cut -d '"' -f 2`
        fi

        match=`echo $line|grep "cust_id ="`
        if [ $? -eq 0 ]; then
                cust_id=`echo $line|cut -d '"' -f 2`
        fi
done

11 Comments

Needs more quotes. Look at what echo $line does if line='hello * world'.
...and, of course, you've got all the usual problems that come with the naive string-parsing approach. What if it's week_date = 'foo' instead of week_date = "foo"? Or week_date = r'foo'? Or week_date = get_current_datespan()?
Also, there's no point to checking $? separately; just put your grep inside your if statement: if printf '%s\n' "$line" | grep -q 'week_date ='; then
...using printf '%s\n' there because it's more reliable than echo; see pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html -- you'll see that echo's output if the input string contains any backslash characters at all is undefined by the standard; it's allowed to interpolate backslash-escape sequences without any -e or -E; it's allowed to behave in an undefined way if -n is passed (note, undefined -- suppressing a trailing newline is not standards-defined behavior); and it's otherwise error-prone in general.
also, running a separate grep command for every line in your input file is wildly inefficient. Even if you wanted to stick to POSIX syntax, I'd suggest a case statement to do a pattern match instead. That is: case $line in "week_date ="*) ... ;; "cust_id ="*) ... ;; esac.
|
0

An alternative is to use awk

#!/usr/bin/env bash

WEEKDATE=$(awk -F \" '/^week_date =/ {print $2 }' config.py)
echo "$WEEKDATE"

Or with grep and cut

#!/usr/bin/env bash

WEEKDATE=$(grep '^week_date =' config.py | cut -d \" -f 2 -)
echo "$WEEKDATE"

These variants use the python file config.py and only work for single line strings.

Most likely you use some convention about what is a string.
Python strings have 4 possible delimiters: ", ', """ and '''.
Adjust the delimiter for the used string.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.