LINUX.ORG.RU

Решено | Конвертировать file.txt в формат file.json через Bash

 , , ,


0

1

Hello there,

есть необходимость (тестовое задание) написать скрипт, который примет как аргумент текстовый файл, а на выходе создаст новый файл на основе старого в JSON формате.

К примеру сам input.txt выглядит так:

[ Asserts Samples ], 1..2 tests

-----------------------------------------------------------------------------------
not ok 1 expecting command finishes successfully (bash way),
7ms

ok 2 expecting command prints some message (the same as above, bats way), 10ms

-----------------------------------------------------------------------------------
1 (of 2) tests passed, 1 tests failed, rated as 50%, spent 17ms

А по итогу должно новый файл должен выглядеть так:

{
 "testName": "Asserts Samples",
 "tests": [
  {
   "name": "expecting command finishes successfully (bash way)",
   "status": false,
   "duration": "7ms"
  },
  {
   "name": "expecting command prints some message (the same as above, bats way)",
   "status": true,
   "duration": "10ms"
  }
 ],
 "summary": {
  "success": 1,
  "failed": 1,
  "rating": 50,
  "duration": "17ms"
 }
}

Т.е. как видно из примера есть:

  1. Первая строка, где нас интересует только название в квадратных скобках
  2. Две разделительные строки, которые никому вообще не сдались
  3. n-ое количество строк, где важен результат (true/false), самое название (от номер до запятой) и потраченное время.
  4. Самая последняя строка, где и будет результат (сколько success, сколько failed, rating и общее duration).

И в общем в теории в input.txt может быть и другая инфа (больше тестов, а значит больше строк и другие данные в summary), однако структура одна и та же: название, разделители, тесты, результат.

Считаю себя достойным newbie и поэтому сам пока додумался только до идеи, но не до реализации. Пробовал через jq, однако проще головой удариться о собственный локоть.

Update. По итогу решил таким образом

#!/bin/bash

#create path to redirect output.json to same directory as output.txt
path=$(dirname $1)

#create variables for name line, test lines and result line
firstline=$(cat $1 | head -n +1 | cut -d "[" -f2 | cut -d "]" -f1)
testname=$(echo $firstline)

tests=$(cat $1 | tail -n +3 | head -n -2)

results=$(cat $1 | tail -n1)

#create main JSON variable
json=$(jq -n --arg tn "$testname" '{testname:$tn,tests:[],summary:{}}')

#test's names, status, duration and updating JSON variable
IFS=$'\n'
for i in $tests
do
    if [[ $i == not* ]]
    then
	    stat=false
    else
	    stat=true
    fi

    if [[ $i =~ expecting(.+?)[0-9] ]]
    then
	    var=${BASH_REMATCH[0]}
	    name=${var%,*}
    fi

    if [[ $i =~ [0-9]*ms ]]
    then
	    test_duration=${BASH_REMATCH[0]}
    fi

    json=$(echo $json | jq \
	    --arg na "$name" \
	    --arg st "$stat" \
	    --arg dur "$test_duration" \
	    '.tests += [{name:$na,status:$st|test("true"),duration:$dur}]')
done

#final success, failed, rating, duration and finishing JSON variable

IFS=$'\n'
for l in $results
do
    if [[ $l =~ [0-9]+ ]]
    then
	    success=${BASH_REMATCH[0]}
    fi

    if [[ $l =~ ,.[0-9]+ ]]
    then
	    v=${BASH_REMATCH[0]}
	    failed=${v:2}
    fi

    if [[ $l =~ [0-9]+.[0-9]+% ]] || [[ $l =~ [0-9]+% ]]
    then
	    va=${BASH_REMATCH[0]}
	    rating=${va%%%}
    fi

    if [[ $l =~ [0-9]*ms ]]
    then
	    duration=${BASH_REMATCH[0]}
    fi

    json=$(echo $json | jq \
                --arg suc "$success" \
                --arg fa "$failed" \
                --arg rat "$rating" \
	        --arg dur "$duration" \
	        '.summary += {success:$suc|tonumber,failed:$fa|tonumber,rating:$rat|tonumber,duration:$dur}')
done

#redirect variable's output to file
echo $json | jq "." > $path"/output.json"

написать на баше рапознавалку структуры текстового файла.
потом на том же баше написать генерилку необходимого json.
по месту допилить напильником
… profit

pfg ★★★★★ ()

Предложи им помощь в доработке программы, которая это генерирует, чтобы она умела выводить сразу в JSON :)

anonymous ()
Ответ на: комментарий от anonymous

Тогда пожелаем им всем удачи в этом нелёгком страдании.

anonymous ()
jo testName="Asserts Samples"  | jq

----------------------------------------------
{
  "testName": "Asserts Samples"
}



пробуй считывать содержимое файла в переменные
скармливать их jo
а потом просто передавать в jq

smilessss ★★★★★ ()
Последнее исправление: smilessss (всего исправлений: 1)
Ответ на: комментарий от smilessss

Однако таким образом можно и просто написать что-то вроде echo ‘{«testname»:«Asserts sample»,«tests»:[{«name»:«expecting smth»,«status»:«false»,«speed»:«7ms»},{«name»:«expecting smth»,«status»:«false»,«speed»:«7ms»}],«summary»:{«good»:«yes»}}’ | jq «.»

BlackMllk ()
Ответ на: комментарий от BlackMllk

попробуй так ответить на своё тз

а лучше всё же наделай перменных
и пробуй велосипедить

smilessss ★★★★★ ()
Ответ на: комментарий от BlackMllk

ну я бы просто взял babashka и в условный десяток строк кода написал бы решение, не мучая ни себя ни окружающих

anonymous ()

Пробегаешься по строкам, регулярками выдёргиваешь значения, выводишь результат.
В один цикл можно всё завернуть.
jo/jq имхо тут не нужны, т.к. json простой получается.

ashot ★★★★ ()
Ответ на: комментарий от ashot

Вот я об этом подумал, что сначала нужно выдернуть нужные значения в переменные. Мол главное зафиксировать название из первой строки, несколько данных из последней и цикл по строкам тестов. И после соединив это сделать json. Но как без jq?

BlackMllk ()
Ответ на: комментарий от BlackMllk

Но как без jq?

Да просто печатаешь построчно значения из переменных/массивов(куда ты там сохранять будешь), добавляя скобки кавычки.
echo/printf в помощь. У тебя json простой как 3 копейки. Я бы не стал с посторонними утилитами заморачиваться. А ты сделай как тебе хочется.))

ashot ★★★★ ()
Ответ на: комментарий от ashot

Ну построчно то кайфы, но надо так, чтобы он автоматически это делал в зависимости от того, если к примеру строк с тестами будет не 7, а 2, или 10, или 4.

т.е. я смог разобрать всё на переменные по названиям заголовков в json (вроде testname, name, duration, rating итд) и по факту запилить к примеру первую строку самую или последнюю не так сложно. но как сделать цикл для части с tests? Для начала я подумал, что эти переменные вообще должны обновляться, т.к. они не статичны от строки к строке, поэтому они в цикле с условиями

BlackMllk ()
Ответ на: комментарий от BlackMllk

Как-то так можно:

#!/bin/bash
func1(){
	# обрабатываем первую строку
}
func2(){
	# обрабатываем строки
}
func3(){
	# обработка последней строки
}

{
while read str; do
    if [[ $str =~ ^\[.*$ ]]; then
	func1 "$str"
    elif [[ $str =~ ^not.*$ ]]; then
	func2 "$str"
    elif [[ $str =~ ^ok.*$ ]]; then
	func2 "$str"
    elif [[ $str =~ ^[0-9].*$ ]]; then
	func3 "$str"
    fi
done
} < input.txt
ashot ★★★★ ()
Ответ на: комментарий от ashot

По итогу сделал 2 цикла для того, чтобы разобрать строки между разделителями и чтобы разобрать последнюю строку, а потом через jq. Через Printf тоже получилось, но было решено, что это грязнющее решение :D

BlackMllk ()
Ответ на: комментарий от BlackMllk

А чистого решения для такой тупейшей задачи и быть не может. Либо для входящего формата пишется полноценный парсер либо делается на коленке лижбы работало, либо первоначальный вариант переписывается для вывода в виде жсона. Что они от тебя хотели увидеть лексер полноценный для рандомного выхлопа рандомной утилиты? Плюнь им в лицо и уходи.

LINUX-ORG-RU ★★★★★ ()

Bash

А питончик не подойдёт? Ля как красиво получается:

import sys, re, json

input_text = open(sys.argv[1]).read()

regex = {
    'split_parts':        r'\s*-----+\n',
    'name':               r'\[ (?P<testName>.*?) ]',

    'test_status_ok':     r'^ok',

    'test_name_dur':      r'\d+ (?P<name>.*?),'            \
                          r'\s+(?P<duration>\d+\s*m?s)',

    'summary':            r'(?P<success>\d+) .*?passed, '  \
                          r'(?P<failed>\d+) .*?failed, '   \
                          r'rated as (?P<rating>\d+)%, '   \
                          r'spent (?P<duration>\d+\s*m?s)'
}

test_sep = '\n\n'


def my_search(what_re, where):
    mo = re.search(what_re, where)
    return mo.groupdict() if mo else {'MATCH FAILED': where}


NAME, TESTS, SUMMARY = \
    re.split(regex['split_parts'], input_text)


J = {
  ** my_search(regex['name'], NAME),
  'tests': [
    {
      ** my_search(regex['test_name_dur'], test),
      'status': bool(re.match(regex['test_status_ok'], test))
    }
      for test in TESTS.split(test_sep)
  ],
  'summary': {
    ** my_search(regex['summary'], SUMMARY)
  }
}


print(json.dumps(J, indent=1))
$ python j.py input.txt
{
 "testName": "Asserts Samples",
 "tests": [
  {
   "name": "expecting command finishes successfully (bash way)",
   "duration": "7ms",
   "status": false
  },
  {
   "name": "expecting command prints some message (the same as above, bats way)",
   "duration": "10ms",
   "status": true
  }
 ],
 "summary": {
  "success": "1",
  "failed": "1",
  "rating": "50",
  "duration": "17ms"
 }
}

:)

anonymous ()
Ответ на: комментарий от LINUX-ORG-RU

А чистого решения для такой тупейшей задачи и быть не может

Что такое «чистое решение»?

anonymous ()

Сколько заплатишь?

rupert ★★★★★ ()
Ответ на: комментарий от BlackMllk

Ну значит тестовое ты завалил. Попробуй в тестомесы, в Сосновоборске должно быть местечко.

anonymous ()
Ответ на: комментарий от LINUX-ORG-RU

Наверное ты прав. Мне сложно судить из-за своей неопытности. Но это простое «школьное» тестовое задание, которое обычно и включает в себя несуразные условия, просто чтобы было. Иначе это элементарно бы надо было сделать в том же питоне, где для этого есть более удобные модули без танца с бубнами.

BlackMllk ()
Ответ на: комментарий от BlackMllk

Но это простое «школьное» тестовое задание, которое обычно и включает в себя несуразные условия, просто чтобы было.

Твоё задание звучит так: «распарсить непонятную хню.»

Все решения сведуться к одному и тому же, вне зависимости от выбранных инструментов.

ashot ★★★★ ()
Ответ на: комментарий от ashot

Необходимость распарсить непонятную хню от этого не пропала :D У меня получилось лично так. Тут наверное много чего можно сделать лучше и стилёвее (хотя бы те же циклы с регулярками), но на данный момент я додумался до этого.Само собой всё это месиво предполагает, что формат текста на вбросе каждый раз будет одним и тем же, поэтому даже это уже не так идеально.

#!/bin/bash

#create path to redirect output.json to same directory as output.txt
path=$(dirname $1)

#create variables for name line, test lines and result line
firstline=$(cat $1 | head -n +1 | cut -d "[" -f2 | cut -d "]" -f1)
testname=$(echo $firstline)

tests=$(cat $1 | tail -n +3 | head -n -2)

results=$(cat $1 | tail -n1)

#create main JSON variable
json=$(jq -n --arg tn "$testname" '{testname:$tn,tests:[],summary:{}}')

#test's names, status, duration and updating JSON variable
IFS=$'\n'
for i in $tests
do
    if [[ $i == not* ]]
    then
	    stat=false
    else
	    stat=true
    fi

    if [[ $i =~ expecting(.+?)[0-9] ]]
    then
	    var=${BASH_REMATCH[0]}
	    name=${var%,*}
    fi

    if [[ $i =~ [0-9]*ms ]]
    then
	    test_duration=${BASH_REMATCH[0]}
    fi

    json=$(echo $json | jq \
	    --arg na "$name" \
	    --arg st "$stat" \
	    --arg dur "$test_duration" \
	    '.tests += [{name:$na,status:$st|test("true"),duration:$dur}]')
done

#final success, failed, rating, duration and finishing JSON variable

IFS=$'\n'
for l in $results
do
    if [[ $l =~ [0-9]+ ]]
    then
	    success=${BASH_REMATCH[0]}
    fi

    if [[ $l =~ ,.[0-9]+ ]]
    then
	    v=${BASH_REMATCH[0]}
	    failed=${v:2}
    fi

    if [[ $l =~ [0-9]+.[0-9]+% ]] || [[ $l =~ [0-9]+% ]]
    then
	    va=${BASH_REMATCH[0]}
	    rating=${va%%%}
    fi

    if [[ $l =~ [0-9]*ms ]]
    then
	    duration=${BASH_REMATCH[0]}
    fi

    json=$(echo $json | jq \
                --arg suc "$success" \
                --arg fa "$failed" \
                --arg rat "$rating" \
	        --arg dur "$duration" \
	        '.summary += {success:$suc|tonumber,failed:$fa|tonumber,rating:$rat|tonumber,duration:$dur}')
done

#redirect variable's output to file
echo $json | jq "." > $path"/output.json"
BlackMllk ()
Ответ на: комментарий от ashot

Ну, наконец-то! А то я уже беспокоиться начал.

xD

anonymous ()
Ответ на: комментарий от BlackMllk

Ай как сочно.

Спасибо :)

На чистом баше заголяться с джейсоном, конечно, та ещё задачка. Царствия небесного её автору.

anonymous ()
Ответ на: комментарий от BlackMllk

Тут наверное много чего можно сделать лучше и стилёвее

В сферическом идеале, в плоскасти.

У тебя задача изначально дебильная. Повторюсь: «распарсить непонятною хню» – это уже любое решение будет не идеальным, а решения будут схожими, вне зависимости от инструмента.

ashot ★★★★ ()
Ответ на: комментарий от anonymous

На чистом баше заголяться с джейсоном, конечно, та ещё задачка. Царствия небесного её автору.

Чем тут баш-то провинился? В этой задаче. Тут джейсун – с боку припёка. А решения – регэкспы во фсе поля.

ashot ★★★★ ()
Ответ на: комментарий от anonymous

Потому, что не баш тебе ближе.)) Не на нём ты пишешь.

ashot ★★★★ ()
Ответ на: комментарий от BlackMllk

которое обычно и включает в себя несуразные условия

Ну только если в этом была их намеренная цель, тогда ладно.

LINUX-ORG-RU ★★★★★ ()
Ответ на: комментарий от ashot

Я закинул в самый первый пост правильным (вроде бы) образом. Комментарии тут нельзя редактировать и удалять насколько я понял, или можно?

BlackMllk ()
Ответ на: комментарий от BlackMllk

Если на них не ответили. Но ты всегда можешь заново запостить..

ashot ★★★★ ()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.