Tôi vừa viết một trình phân tích cú pháp mà tôi gọi là Yay! ( Yaml không phải là Yamlesque! ) Phân tích Yamlesque , một tập hợp nhỏ của YAML. Vì vậy, nếu bạn đang tìm kiếm một trình phân tích cú pháp YAML tuân thủ 100% cho Bash thì đây không phải là nó. Tuy nhiên, để trích dẫn OP, nếu bạn muốn một tệp cấu hình có cấu trúc dễ dàng nhất có thể cho người dùng không có kỹ thuật chỉnh sửa giống như YAML, điều này có thể được quan tâm.
Nó được đưa ra bởi câu trả lời trước đó nhưng viết các mảng kết hợp ( vâng, nó yêu cầu Bash 4.x ) thay vì các biến cơ bản. Nó làm như vậy theo cách cho phép dữ liệu được phân tích cú pháp mà không cần biết trước về các khóa để có thể viết mã điều khiển dữ liệu.
Cũng như các thành phần mảng khóa / giá trị, mỗi mảng có một keys
mảng chứa danh sách các tên khóa, một children
mảng chứa tên của mảng con và một parent
khóa tham chiếu đến mẹ của nó.
Đây là một ví dụ về Yamlesque:
root_key1: this is value one
root_key2: "this is value two"
drink:
state: liquid
coffee:
best_served: hot
colour: brown
orange_juice:
best_served: cold
colour: orange
food:
state: solid
apple_pie:
best_served: warm
root_key_3: this is value three
Dưới đây là một ví dụ cho thấy cách sử dụng nó:
#!/bin/bash
# An example showing how to use Yay
. /usr/lib/yay
# helper to get array value at key
value() { eval echo \${$1[$2]}; }
# print a data collection
print_collection() {
for k in $(value $1 keys)
do
echo "$2$k = $(value $1 $k)"
done
for c in $(value $1 children)
do
echo -e "$2$c\n$2{"
print_collection $c " $2"
echo "$2}"
done
}
yay example
print_collection example
đầu ra nào:
root_key1 = this is value one
root_key2 = this is value two
root_key_3 = this is value three
example_drink
{
state = liquid
example_coffee
{
best_served = hot
colour = brown
}
example_orange_juice
{
best_served = cold
colour = orange
}
}
example_food
{
state = solid
example_apple_pie
{
best_served = warm
}
}
Và đây là trình phân tích cú pháp:
yay_parse() {
# find input file
for f in "$1" "$1.yay" "$1.yml"
do
[[ -f "$f" ]] && input="$f" && break
done
[[ -z "$input" ]] && exit 1
# use given dataset prefix or imply from file name
[[ -n "$2" ]] && local prefix="$2" || {
local prefix=$(basename "$input"); prefix=${prefix%.*}
}
echo "declare -g -A $prefix;"
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
sed -n -e "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$input" |
awk -F$fs '{
indent = length($1)/2;
key = $2;
value = $3;
# No prefix or parent for the top level (indent zero)
root_prefix = "'$prefix'_";
if (indent ==0 ) {
prefix = ""; parent_key = "'$prefix'";
} else {
prefix = root_prefix; parent_key = keys[indent-1];
}
keys[indent] = key;
# remove keys left behind if prior row was indented more than this row
for (i in keys) {if (i > indent) {delete keys[i]}}
if (length(value) > 0) {
# value
printf("%s%s[%s]=\"%s\";\n", prefix, parent_key , key, value);
printf("%s%s[keys]+=\" %s\";\n", prefix, parent_key , key);
} else {
# collection
printf("%s%s[children]+=\" %s%s\";\n", prefix, parent_key , root_prefix, key);
printf("declare -g -A %s%s;\n", root_prefix, key);
printf("%s%s[parent]=\"%s%s\";\n", root_prefix, key, prefix, parent_key);
}
}'
}
# helper to load yay data file
yay() { eval $(yay_parse "$@"); }
Có một số tài liệu trong tệp nguồn được liên kết và dưới đây là một lời giải thích ngắn về những gì mã làm.
Trước yay_parse
tiên, hàm định vị input
tệp hoặc thoát với trạng thái thoát là 1. Tiếp theo, nó xác định tập dữ liệu prefix
, được chỉ định rõ ràng hoặc xuất phát từ tên tệp.
Nó ghi bash
các lệnh hợp lệ vào đầu ra tiêu chuẩn của nó, nếu được thực thi, xác định các mảng biểu thị nội dung của tệp dữ liệu đầu vào. Đầu tiên trong số này xác định mảng cấp cao nhất:
echo "declare -g -A $prefix;"
Lưu ý rằng các khai báo mảng là kết hợp ( -A
) là một tính năng của Bash phiên bản 4. Các khai báo cũng là toàn cục ( -g
) để chúng có thể được thực thi trong một hàm nhưng có sẵn cho phạm vi toàn cầu như trình yay
trợ giúp:
yay() { eval $(yay_parse "$@"); }
Dữ liệu đầu vào được xử lý ban đầu với sed
. Nó bỏ các dòng không khớp với đặc tả định dạng Yamlesque trước khi phân định các trường Yamlesque hợp lệ bằng ký tự phân tách tệp ASCII và xóa bất kỳ dấu ngoặc kép nào xung quanh trường giá trị.
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
sed -n -e "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$input" |
Hai biểu thức tương tự nhau; chúng chỉ khác nhau bởi vì cái đầu tiên chọn ra các giá trị được trích dẫn trong khi cái thứ hai chọn ra các giá trị không được trích dẫn.
Các tập tin Separator (28 / hex 12 / bát phân 034) được sử dụng bởi vì, như một nhân vật không thể in được, nó dường như không có trong dữ liệu đầu vào.
Kết quả được dẫn vào awk
trong đó xử lý một dòng đầu vào của nó tại một thời điểm. Nó sử dụng ký tự FS để gán từng trường cho một biến:
indent = length($1)/2;
key = $2;
value = $3;
Tất cả các dòng có một thụt lề (có thể bằng 0) và một khóa nhưng chúng không có giá trị. Nó tính toán một mức thụt lề cho dòng chia chiều dài của trường đầu tiên, chứa khoảng trắng hàng đầu, bằng hai. Các mục cấp cao nhất mà không có bất kỳ thụt lề nào ở mức không thụt lề.
Tiếp theo, nó tìm ra những gì prefix
để sử dụng cho các mục hiện tại. Đây là những gì được thêm vào một tên khóa để tạo một tên mảng. Có một root_prefix
mảng cấp cao nhất được xác định là tên tập dữ liệu và dấu gạch dưới:
root_prefix = "'$prefix'_";
if (indent ==0 ) {
prefix = ""; parent_key = "'$prefix'";
} else {
prefix = root_prefix; parent_key = keys[indent-1];
}
Khóa parent_key
là mức thụt ở trên mức thụt dòng của dòng hiện tại và thể hiện bộ sưu tập mà dòng hiện tại là một phần của. Các cặp khóa / giá trị của bộ sưu tập sẽ được lưu trữ trong một mảng với tên được định nghĩa là nối của prefix
và parent_key
.
Đối với cấp cao nhất (mức thụt lề 0), tiền tố của tập dữ liệu được sử dụng làm khóa chính để nó không có tiền tố (được đặt thành ""
). Tất cả các mảng khác đều có tiền tố gốc.
Tiếp theo, khóa hiện tại được chèn vào một mảng (awk-Internal) có chứa các khóa. Mảng này tồn tại trong suốt toàn bộ phiên awk và do đó chứa các khóa được chèn bởi các dòng trước. Khóa được chèn vào mảng bằng cách sử dụng thụt lề làm chỉ mục mảng.
keys[indent] = key;
Vì mảng này chứa các khóa từ các dòng trước đó, bất kỳ khóa nào có mức độ thụt lề so với mức thụt dòng của dòng hiện tại sẽ bị xóa:
for (i in keys) {if (i > indent) {delete keys[i]}}
Điều này để lại mảng khóa chứa chuỗi khóa từ gốc ở mức thụt 0 đến dòng hiện tại. Nó loại bỏ các khóa cũ vẫn còn khi dòng trước được thụt sâu hơn dòng hiện tại.
Phần cuối cùng xuất ra các bash
lệnh: một dòng đầu vào không có giá trị bắt đầu một mức thụt lề mới (một bộ sưu tập theo cách nói YAML) và một dòng đầu vào có giá trị thêm một khóa vào bộ sưu tập hiện tại.
Tên của bộ sưu tập là sự ghép nối của dòng hiện tại prefix
và parent_key
.
Khi một khóa có một giá trị, một khóa có giá trị đó được gán cho bộ sưu tập hiện tại như thế này:
printf("%s%s[%s]=\"%s\";\n", prefix, parent_key , key, value);
printf("%s%s[keys]+=\" %s\";\n", prefix, parent_key , key);
Câu lệnh đầu tiên xuất ra lệnh để gán giá trị cho một phần tử mảng kết hợp được đặt tên theo khóa và câu lệnh thứ hai xuất lệnh để thêm khóa vào keys
danh sách được phân cách bằng dấu cách của bộ sưu tập :
<current_collection>[<key>]="<value>";
<current_collection>[keys]+=" <key>";
Khi khóa không có giá trị, bộ sưu tập mới sẽ được bắt đầu như sau:
printf("%s%s[children]+=\" %s%s\";\n", prefix, parent_key , root_prefix, key);
printf("declare -g -A %s%s;\n", root_prefix, key);
Câu lệnh đầu tiên đưa ra lệnh để thêm bộ sưu tập mới vào children
danh sách giới hạn không gian của bộ sưu tập hiện tại và câu lệnh thứ hai xuất ra lệnh để khai báo một mảng kết hợp mới cho bộ sưu tập mới:
<current_collection>[children]+=" <new_collection>"
declare -g -A <new_collection>;
Tất cả các đầu ra từ yay_parse
có thể được phân tích cú pháp dưới dạng các lệnh bash bằng các lệnh bash eval
hoặc tích source
hợp.