Bash parameter quotes and eval

2020-07-09 07:00发布

I've written a bash logging library to be implemented with some complex scripts that my company is currently using. I've been deadset on providing the script filename (${BASH_SOURCE}) and the line number (${LINENO}) of the calling script when making the log calls. However, I didn't want to have to rely on the user or implementing script to pass in these two variables as parameters. If this were C/C++, I would just create a macro that prepends "__FILE__" and "__LINE__" to the parameter list.

I was finally able to get this part working. Here are some much-simplified abstracts as a proof of concept:

Here's the logging library:

# log.sh

LOG="eval _log \${BASH_SOURCE} \${LINENO}"

_log () {
    _BASH_SOURCE=`basename "${1}"` && shift
    _LINENO=${1} && shift

    echo "(${_BASH_SOURCE}:${_LINENO}) $@"
}

And an implementing test script:

# MyTest.sh

. ./log.sh

${LOG} "This is a log message"
# (test.sh:5) This is a log message

This works pretty well (and, I was thrilled to get it working at first). However, this has one glaring problem: the interaction between quotes and eval. If I make the call:

${LOG} "I'm thrilled that I got this working"
# ./test.sh: eval: line 5: unexected EOF while looking for matching `''
# ./test.sh: eval: line 6: syntax error: unexpected end of file

Now, I believe that I understand why this is happening. The quoted parameters are kept intact as they're passed to eval, but at that point, the content is placed as-is into the resulting command string. I know that I can fix this by doing some escaping; however, I REALLY do not want to force the implementing scripts to have to do this. Before I implemented this "eval macro" capability, I had users make calls directly to "_log" and allowed them to optionally pass in "${LINENO}." With this implementation, The failing call above (with only a quoted sentence) worked just fine.

At the most basic level, all I really want is for a script to be able to call [log function/macro] "String to log with special characers" and have the resulting log message contain the filename and line number of the calling script, followed by the log message. If it's possible, I would assume that I'm very close, but if there's something I'm overlooking that would require a different approach, I'm open to that as well. I can't force the users to escape all of their messages, as that will likely cause them to not use this library. This is such a problem that if I can't find a solution to this, I'll likely revert to the old capability (which required ${LINENO} to be passed as a function parameter - this was much less intrusive).

TLDR: Is there any way to get eval to respect special characters within a quoted parameter, without having to escape them?

3条回答
女痞
2楼-- · 2020-07-09 07:04

I recommend avoiding eval if possible. For your logging use case, you could take a look at the shell builtin caller. If you need more information, you can use the variables BASH_SOURCE, BASH_LINENO and FUNCNAME. Note that all of these variables are arrays and contain the full call stack. See the following example:

#! /bin/bash       

function log() {
    echo "[$( caller )] $*" >&2
    echo "BASH_SOURCE: ${BASH_SOURCE[*]}"
    echo "BASH_LINENO: ${BASH_LINENO[*]}"
    echo "FUNCNAME: ${FUNCNAME[*]}"
}

function foobar() {
    log "failed:" "$@"
}

foobar "$@"
查看更多
Ridiculous、
3楼-- · 2020-07-09 07:12

(Note: this solves the immediate problem of quoting, but @nosid's answer about accessing the call stack is much better)

Change your definition of _log slightly, to read from standard input instead of taking the log message from positional parameters:

_log () {
    # Set up _BASH_SOURCE and _LINENO the same way

    cat <(echo -n "$(_BASH_SOURCE:$_LINENO) ") -
}

Then pass your log message via standard input using a here doc or a here string:

${LOG} <<<"This is a log message"
${LOG} <<<"I'm thrilled this works, too!"
${LOG} <<HERE
Even this long
message works as
intended!
HERE
查看更多
闹够了就滚
4楼-- · 2020-07-09 07:28

For your specific case of logging, you may want to look at this function which prints the caller's context's file function and line number.

Reusable quoting function

This function will do correct quoting for you:

function token_quote {
  local quoted=()
  for token; do
    quoted+=( "$(printf '%q' "$token")" )
  done
  printf '%s\n' "${quoted[*]}"
}

Example usage:

Example usage:

$ token_quote token 'single token' token
token single\ token token

Above, note the single token's space is quoted as \.

$ set $(token_quote token 'single token' token)
$ eval printf '%s\\n' "$@"
token
single token
token
$

This shows that the spaces in tokens are preserved.

查看更多
登录 后发表回答