From 6713e29e56d6ed9f266b141f64d505dccfa9b1ed Mon Sep 17 00:00:00 2001 From: vitali87 Date: Fri, 27 Mar 2026 21:57:37 +0100 Subject: [PATCH 1/2] feat: add json-to-csv conversion via jq --- lib/convert.sh | 23 ++++++++++++-- test.sh | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/lib/convert.sh b/lib/convert.sh index b069e72..458cd6e 100644 --- a/lib/convert.sh +++ b/lib/convert.sh @@ -166,15 +166,24 @@ _u7_convert() { json) local input="$1" if [[ "$2" != "to" ]]; then - echo "Usage: u7 cv json to yaml [yield ]" + echo "Usage: u7 cv json to [yield ]" return 1 fi local to_fmt="$3" local output="${input%.*}.$to_fmt" if [[ "$4" == "yield" ]]; then + if [[ -z "$5" ]]; then + echo "Usage: u7 cv json to [yield ]" + return 1 + fi output="$5" fi + if [[ ! -f "$input" ]]; then + echo "File not found: $input" + return 1 + fi + case "$to_fmt" in yaml|yml) if [[ "$_U7_DRY_RUN" == "1" ]]; then @@ -183,6 +192,16 @@ _u7_convert() { yq -P < "$input" > "$output" fi ;; + csv) + _u7_require jq || return 1 + if [[ "$_U7_DRY_RUN" == "1" ]]; then + echo "[dry-run] jq -r '...' < \"$input\" > \"$output\"" + else + local tmpfile + tmpfile=$(mktemp "${output}.XXXXXX") || { echo "Error: Failed to create temp file"; return 1; } + jq -r '(.[0] | keys_unsorted) as $keys | $keys, map([.[ $keys[] ]])[] | @csv' < "$input" > "$tmpfile" && mv "$tmpfile" "$output" || { rm -f "$tmpfile"; return 1; } + fi + ;; *) echo "Unsupported conversion: json to $to_fmt" ; return 1 ;; esac ;; @@ -316,7 +335,7 @@ Entities: files to archive yield Create archive image to [yield ] Convert image (png/jpg/webp/gif/etc) video to [yield ] Convert video - json to yaml [yield ] Convert JSON to YAML + json to [yield ] Convert JSON to YAML or CSV yaml to json [yield ] Convert YAML to JSON csv to json [yield ] Convert CSV to JSON case upper to lower on Rename to lowercase diff --git a/test.sh b/test.sh index d1358ef..6ba2807 100755 --- a/test.sh +++ b/test.sh @@ -988,6 +988,89 @@ else ((PASSED++)) fi +# ===== sh log tests ===== + +# Setup: create a temp log file +LOG_FILE="$TEST_DIR/test.log" +for i in $(seq 1 30); do + echo "line $i: log entry" >> "$LOG_FILE" +done + +# Test: sh log default shows last 20 lines +result=$(u7 sh log "$LOG_FILE" 2>&1) +line_count=$(echo "$result" | wc -l | tr -d ' ') +assert_equals "sh log default shows 20 lines" "20" "$line_count" +assert_contains "sh log default starts at line 11" "line 11" "$result" + +# Test: sh log with limit +result=$(u7 sh log "$LOG_FILE" limit 5 2>&1) +line_count=$(echo "$result" | wc -l | tr -d ' ') +assert_equals "sh log limit 5 shows 5 lines" "5" "$line_count" +assert_contains "sh log limit 5 starts at line 26" "line 26" "$result" + +# Test: sh log with match +echo "ERROR something broke" >> "$LOG_FILE" +echo "INFO all good" >> "$LOG_FILE" +echo "ERROR another failure" >> "$LOG_FILE" +result=$(u7 sh log "$LOG_FILE" match ERROR 2>&1) +line_count=$(echo "$result" | wc -l | tr -d ' ') +assert_equals "sh log match finds 2 ERROR lines" "2" "$line_count" + +# Test: sh log missing file +result=$(u7 sh log nonexistent.log 2>&1) +assert_contains "sh log reports missing file" "File not found" "$result" + +# Test: sh log no arguments +result=$(u7 sh log 2>&1) +assert_contains "sh log no args shows usage" "Usage" "$result" + +# Test: sh log invalid limit +result=$(u7 sh log "$LOG_FILE" limit abc 2>&1) +assert_contains "sh log rejects non-integer limit" "limit must be a positive integer" "$result" + +# Test: sh log match without pattern +result=$(u7 sh log "$LOG_FILE" match 2>&1) +assert_contains "sh log match without pattern shows usage" "Usage" "$result" + +# Test: sh log unknown subcommand +result=$(u7 sh log "$LOG_FILE" badarg 2>&1) +assert_contains "sh log unknown subcommand shows usage" "Usage" "$result" + +# ===== cv json to csv tests ===== + +# Test: Convert JSON to CSV +echo '[{"name":"Alice","age":30},{"name":"Bob","age":25}]' > test_cv.json +if command -v jq &>/dev/null; then + u7 cv json test_cv.json to csv yield test_cv.csv >/dev/null 2>&1 + if [[ -f "test_cv.csv" ]]; then + result=$(cat test_cv.csv) + assert_contains "cv json to csv works" "Alice" "$result" + else + echo -e "${RED}✗${NC} cv json to csv (file not created)" + ((FAILED++)) + fi +else + echo -e "${GREEN}✓${NC} cv json to csv (skipped - jq not installed)" + ((PASSED++)) +fi + +# Test: Convert JSON to CSV missing file +result=$(u7 cv json nonexistent.json to csv 2>&1) +assert_contains "cv json to csv reports missing file" "not found" "$result" + +# Test: Convert JSON to unsupported format +result=$(u7 cv json test_cv.json to xml 2>&1) +assert_contains "cv json rejects unsupported format" "Unsupported" "$result" + +# Test: Convert JSON to CSV dry-run +if command -v jq &>/dev/null; then + result=$(u7 -n cv json test_cv.json to csv yield dryrun.csv 2>&1) + assert_contains "cv json to csv dry-run" "dry-run" "$result" +else + echo -e "${GREEN}✓${NC} cv json to csv dry-run (skipped - jq not installed)" + ((PASSED++)) +fi + # Cleanup cd / rm -rf "$TEST_DIR" From c3ca6e1249ba718a8330c423c7431a4cffbd166b Mon Sep 17 00:00:00 2001 From: vitali87 Date: Fri, 27 Mar 2026 22:06:56 +0100 Subject: [PATCH 2/2] fix: handle empty JSON arrays in cv json to csv and remove unrelated sh log tests --- lib/convert.sh | 2 +- test.sh | 48 ------------------------------------------------ 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/lib/convert.sh b/lib/convert.sh index 458cd6e..62dcd0c 100644 --- a/lib/convert.sh +++ b/lib/convert.sh @@ -199,7 +199,7 @@ _u7_convert() { else local tmpfile tmpfile=$(mktemp "${output}.XXXXXX") || { echo "Error: Failed to create temp file"; return 1; } - jq -r '(.[0] | keys_unsorted) as $keys | $keys, map([.[ $keys[] ]])[] | @csv' < "$input" > "$tmpfile" && mv "$tmpfile" "$output" || { rm -f "$tmpfile"; return 1; } + jq -r 'if length == 0 then empty else (.[0] | keys_unsorted) as $keys | $keys, map([.[ $keys[] ]])[] | @csv end' < "$input" > "$tmpfile" && mv "$tmpfile" "$output" || { rm -f "$tmpfile"; return 1; } fi ;; *) echo "Unsupported conversion: json to $to_fmt" ; return 1 ;; diff --git a/test.sh b/test.sh index 6ba2807..38c1b98 100755 --- a/test.sh +++ b/test.sh @@ -988,54 +988,6 @@ else ((PASSED++)) fi -# ===== sh log tests ===== - -# Setup: create a temp log file -LOG_FILE="$TEST_DIR/test.log" -for i in $(seq 1 30); do - echo "line $i: log entry" >> "$LOG_FILE" -done - -# Test: sh log default shows last 20 lines -result=$(u7 sh log "$LOG_FILE" 2>&1) -line_count=$(echo "$result" | wc -l | tr -d ' ') -assert_equals "sh log default shows 20 lines" "20" "$line_count" -assert_contains "sh log default starts at line 11" "line 11" "$result" - -# Test: sh log with limit -result=$(u7 sh log "$LOG_FILE" limit 5 2>&1) -line_count=$(echo "$result" | wc -l | tr -d ' ') -assert_equals "sh log limit 5 shows 5 lines" "5" "$line_count" -assert_contains "sh log limit 5 starts at line 26" "line 26" "$result" - -# Test: sh log with match -echo "ERROR something broke" >> "$LOG_FILE" -echo "INFO all good" >> "$LOG_FILE" -echo "ERROR another failure" >> "$LOG_FILE" -result=$(u7 sh log "$LOG_FILE" match ERROR 2>&1) -line_count=$(echo "$result" | wc -l | tr -d ' ') -assert_equals "sh log match finds 2 ERROR lines" "2" "$line_count" - -# Test: sh log missing file -result=$(u7 sh log nonexistent.log 2>&1) -assert_contains "sh log reports missing file" "File not found" "$result" - -# Test: sh log no arguments -result=$(u7 sh log 2>&1) -assert_contains "sh log no args shows usage" "Usage" "$result" - -# Test: sh log invalid limit -result=$(u7 sh log "$LOG_FILE" limit abc 2>&1) -assert_contains "sh log rejects non-integer limit" "limit must be a positive integer" "$result" - -# Test: sh log match without pattern -result=$(u7 sh log "$LOG_FILE" match 2>&1) -assert_contains "sh log match without pattern shows usage" "Usage" "$result" - -# Test: sh log unknown subcommand -result=$(u7 sh log "$LOG_FILE" badarg 2>&1) -assert_contains "sh log unknown subcommand shows usage" "Usage" "$result" - # ===== cv json to csv tests ===== # Test: Convert JSON to CSV