Есть ли способ проверить, завершен ли bash script или нет?
Я пытаюсь реализовать REPL (цикл чтения-eval-print) в bash. Если такая вещь уже существует, пожалуйста, проигнорируйте следующее и ответьте на этот вопрос указателем на нее.
Позвольте использовать этот script в качестве примера (name it test.sh
):
if true
then
echo a
else
echo b
fi
echo c
Что я хочу сделать, так это прочитать эту script строку за строкой, проверить, что то, что я прочитал до сих пор, является полным выражением bash; если он завершен, eval
it; в противном случае продолжайте читать следующую строку. script ниже иллюстрирует мою идею, надеюсь (это не совсем работает, хотя).
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
Мотивация
Для bash script, я хочу разобрать его в синтаксически полные выражения, оценить каждое выражение, захватить вывод и, наконец, пометить исходный код и выводить (скажем, используя Markdown/HTML/LaTeX/...). Например, для script
echo a
echo b
То, что я хочу достичь, - это результат следующим образом:
```bash
echo a
```
```
a
```
```bash
echo b
```
```
b
```
вместо оценки всего script и захватить весь вывод:
```bash
echo a
echo b
```
```
a
b
```
Ответы
Ответ 1
bash -n -c "$command_text"
... определит, является ли ваш $command_text
синтаксически допустимым script, фактически не выполнив его.
Обратите внимание, что существует огромная ширина пространства между "синтаксически действительными" и "правильными". Подумайте о том, чтобы принять что-то вроде http://shellcheck.net/, если вы хотите правильно разобрать язык.
Ответ 2
Следующие сценарии должны генерировать вывод Markdown, который вы ожидаете.
eval "set -n; $x"
используется для проверки завершения команды, путем проверки наличия синтаксических ошибок в команде. Только команда, которая не имеет синтаксических ошибок, будет считаться полной, выполнена и показана на выходе Markdown.
Обратите внимание, что вход script, который должен быть обработан, выполняется в под-оболочке и поэтому не будет мешать самой обработке script (т.е. вход script может использовать те же имена переменных, что и script и не может изменять значения переменных в обработке script). Единственным исключением являются специальные переменные, называемые ___internal__variable___
.
Существует два подхода к тому, как достичь этого, что представлено ниже. В Версия 1, всякий раз, когда обрабатывается новая полная команда, все выполняемые перед ней операторы выполняются для создания "контекста" для команды. Это эффективно запускает вход script несколько раз.
В Версия 2 среда под-оболочки хранится в переменной после выполнения каждой полной команды. Затем, перед выполнением следующей команды, предыдущая среда восстанавливается в под-оболочке.
Версия 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
Версия 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
Пример
Для ввода script input.sh
:
x=10
echo "$x"
y=$(($x+1))
echo "$y"
while [ "$y" -gt "0" ]
do
echo $y
y=$(($y-1))
done
Выход будет:
==================
```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
Предположим, что ваши тестовые команды хранятся в файле с именем "example". То есть, используя те же команды, что и в предыдущем ответе:
$ cat example
x=3
echo "$x"
y=$(($x+1))
echo "$y"
while [ "$y" -gt "0" ]
do
echo $y
y=$(($y-1))
done
команда:
$ (echo 'PS1=; PROMPT_COMMAND="echo -n =====; echo"'; cat example2 ) | bash -i
дает:
=====
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
если вам также интересны промежуточные результаты цикла, команда:
$ ( echo 'trap '"'"'echo; echo command: $BASH_COMMAND; echo answer:'"'"' DEBUG'; cat example ) | bash
приводит к:
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:
Добавление 1
Нетрудно изменить предыдущие результаты в другом формате. Например, этот маленький 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;
}
}
при использовании в команде:
( echo 'PS1="===COMMAND===\n"'; cat example ) | bash -i 2>&1 | ./formatter.pl
дает следующий результат:
===
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
Вместо pidfiles, если ваш script имеет уникально идентифицируемое имя, вы можете сделать что-то вроде этого:
#!/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 ...