可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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 ...