is there a way to check if a bash script is comple

2019-03-23 04:13发布

问题:

I'm trying to implement a REPL (read-eval-print loop) in bash. If such a thing already exists, please ignore the following and answer this question with a pointer to it.

Let's use this script as an example (name it test.sh):

if true
then
  echo a
else
  echo b
fi
echo c

What I want to do is to read this script line by line, check if what I have read so far is a complete bash expression; if it is complete, eval it; otherwise keep on reading the next line. The script below illustrates my idea hopefully (it does not quite work, though).

x=""
while read -r line
do
  x=$x$'\n'$line  # concatenate by \n
  # the line below is certainly a bad way to go
  if eval $x 2>/dev/null; then
    eval $x  # code seems to be working, so eval it
    x=""  # empty x, and start collecting code again
  else
    echo 'incomplete expression'
  fi
done < test.sh

Motivation

For a bash script, I want to parse it into syntactically complete expressions, evaluate each expression, capture the output, and finally mark up the source code and output (say, using Markdown/HTML/LaTeX/...). For example, for a script

echo a
echo b

What I want to achieve is the output like this:

```bash
echo a
```

```
a
```

```bash
echo b
```

```
b
```

instead of evaluating the whole script and capture all the output:

```bash
echo a
echo b
```

```
a
b
```

回答1:

bash -n -c "$command_text"

...will determine whether your $command_text is a syntactically valid script without actually executing it.


Note that there's a huge breadth of space between "syntactically valid" and "correct". Consider adopting something like http://shellcheck.net/ if you want to properly parse the language.



回答2:

The following scripts should generate the Markdown output you expect.

eval "set -n; $x" is used to verify if the command is complete, by checking for syntax errors in the command. Only a command that has no syntax errors will be considered complete, executed, and shown in the output Markdown.

Please note that the input script that is to be processed is executed in a sub-shell and therefore will not interfere with the processing script itself (i.e. the input script can use the same variable names as the processing script and cannot change the values of variables in the processing script). The only exception are the special variables called ___internal__variable___.

There are two approaches to how to achieve that, which I present below. In Version 1, whenever a new complete command is processed, all the statements before it are executed to create a "context" for the command. This effectively runs the input script multiple times.

In Version 2, the environment of the sub-shell is stored in a variable after each complete command is executed. Then, before the next command is executed, the previous environment is restored in the sub-shell.

Version 1

#!/bin/bash

x=""  # Current
y=""  # Context
while IFS= read -r line  # Keep indentation
do
    [ -z "$line" ] && continue  # Skip empty lines

    x=$x$'\n'$line  # Build a complete command
    # Check current command for syntax errors
    if (eval "set -n; $x" 2> /dev/null)
    then
        # Run the input script up to the current command
        # Run context first and ignore the output
        ___internal_variable___="$x"
        out=$(eval "$y" &>/dev/null; eval "$___internal_variable___")
        # Generate command markdown
        echo "=================="
        echo
        echo "\`\`\`bash$x"
        echo "\`\`\`"
        echo
        # Generate output markdown
        if [ -n "$out" ]
        then
            echo "Output:"
            echo
            echo "\`\`\`"
            echo "$out"
            echo "\`\`\`"
            echo
        fi
        y=$y$'\n'$line  # Build context
        x=""  # Clear command
    fi
done < input.sh

Version 2

#!/bin/bash

x=""  # Current command
y="true"  # Saved environment
while IFS= read -r line  # Keep indentation
do
    [ -z "$line" ] && continue  # Skip empty lines

    x=$x$'\n'$line  # Build a complete command
    # Check current command for syntax errors
    if (eval "set -n; $x" 2> /dev/null)
    then
        # Run the current command in the previously saved environment
        # Then store the output of the command as well as the new environment
        ___internal_variable_1___="$x"  # The current command
        ___internal_variable_2___="$y"  # Previously saved environment
        out=$(bash -c "${___internal_variable_2___}; printf '<<<BEGIN>>>'; ${___internal_variable_1___}; printf '<<<END>>>'; declare -p" 2>&1)
        # Separate the environment description from the command output
        y="${out#*<<<END>>>}"
        out="${out%%<<<END>>>*}"
        out="${out#*<<<BEGIN>>>}"

        # Generate command markdown
        echo "=================="
        echo
        echo "\`\`\`bash$x"
        echo "\`\`\`"
        echo
        # Generate output markdown
        if [ -n "$out" ]
        then
            echo "Output:"
            echo
            echo "\`\`\`"
            echo "$out"
            echo "\`\`\`"
            echo
        fi
        x=""  # Clear command
    fi
done < input.sh

Example

For input script input.sh:

x=10
echo "$x"
y=$(($x+1))
echo "$y"


while [ "$y" -gt "0" ]
do
    echo $y
    y=$(($y-1))
done

The output will be:

==================

```bash
x=10
```

==================

```bash
echo "$x"
```

Output:

```
10
```

==================

```bash
y=$(($x+1))
```

==================

```bash
echo "$y"
```

Output:

```
11
```

==================

```bash
while [ "$y" -gt "0" ]
do
    echo $y
    y=$(($y-1))
done
```

Output:

```
11
10
9
8
7
6
5
4
3
2
1
```


回答3:

Assume your test commands are stored in a file called "example". That is, using same commands than in previous answer:

$ cat example
x=3
echo "$x"
y=$(($x+1))
echo "$y"


while [ "$y" -gt "0" ]
do
    echo $y
    y=$(($y-1))
done

the command:

$ (echo 'PS1=; PROMPT_COMMAND="echo -n =====; echo"'; cat example2 ) | bash -i

produces:

=====
x=3
=====
echo "$x"
3
=====
y=$(($x+1))
=====
echo "$y"
4
=====

=====

=====
while [ "$y" -gt "0" ]
> do
>     echo $y
>     y=$(($y-1))
> done
4
3
2
1
=====
exit

if you are interested also in the intermediate results of a loop, the command:

$ ( echo 'trap '"'"'echo; echo command: $BASH_COMMAND; echo answer:'"'"' DEBUG'; cat example ) | bash

results in:

command: x=3
answer:

command: echo "$x"
answer:
3

command: y=$(($x+1))
answer:

command: echo "$y"
answer:
4

command: [ "$y" -gt "0" ]
answer:

command: echo $y
answer:
4

command: y=$(($y-1))
answer:

command: [ "$y" -gt "0" ]
answer:

command: echo $y
answer:
3

command: y=$(($y-1))
answer:

command: [ "$y" -gt "0" ]
answer:

command: echo $y
answer:
2

command: y=$(($y-1))
answer:

command: [ "$y" -gt "0" ]
answer:

command: echo $y
answer:
1

command: y=$(($y-1))
answer:

command: [ "$y" -gt "0" ]
answer:

Addendum 1

It is not difficult to change the previous results to some other format. By example, this small perl script:

$ cat formatter.pl 
#!/usr/bin/perl
#

$state=4; # 0: answer, 1: first line command, 2: more command, 4: unknown

while(<>) {
#  print $state;

  if( /^===COMMAND===/ ) {
    print "===\n";
    $state=1;
    next;
  }

  if( $state == 1 ) {
    print;
    $state=2;
    next;
  }

  if( $state == 2 && /^>+ (.*)/ ) {
    print "$1\n";
    next;
  }

  if( $state == 2 ) {
    print "---\n";
    $state=0;
    redo;
  }

  if( $state == 0 ) {
    print;
    next;
  }
}

when used in command:

( echo 'PS1="===COMMAND===\n"'; cat example ) | bash -i 2>&1 | ./formatter.pl

gives this result:

===
x=3
===
echo "$x"
---
3
===
y=$(($x+1))
===
echo "$y"
---
4
===

===

===
while [ "$y" -gt "0" ]
do
    echo $y
    y=$(($y-1))
done
---
4
3
2
1
===
exit


回答4:

In lieu of pidfiles, as long as your script has a uniquely identifiable name you can do something like this:

  #!/bin/bash
COMMAND=$0
# exit if I am already running
RUNNING=`ps --no-headers -C${COMMAND} | wc -l`
if [ ${RUNNING} -gt 1 ]; then
  echo "Previous ${COMMAND} is still running."
  exit 1
fi
... rest of script ...