У меня есть большой файл ascii, который я хочу переформатировать. Формат ввода:

a ; 1 ; 2 
b ; 2 ; 3 
c ; 4 ; 5 
d ; 6 ; 7 
e ; 8 ; 9 
f ; 10 ; 11

Он имеет N = 4 строки. Формат вывода должен быть

a ; 1 ; 2; c ; 4 ; 5; e ; 8 ; 9
b ; 2 ; 3; d ; 6 ; 7; f ; 10 ; 11

Поэтому я хочу вырезать n = 2 последовательных строки и потенциально вставить их по горизонтали в новый файл результатов.

Как я могу это сделать с помощью bash?

-1
mcExchange 6 Май 2021 в 14:04

2 ответа

Лучший ответ

Для этого можно использовать awk.

Awk обрабатывает свои входные файлы построчно и позволяет каждый раз выполнять широкий спектр операций. Он поддерживает массивы, поэтому для решения данной проблемы мы можем использовать массив, в котором мы подготавливаем данные в правильном формате для окончательного вывода.

Изначально массив пуст. Для первых строк n все, что нам нужно сделать, это сохранить строку в новой строке двумерного массива. Для вашего примера это дает нам:

| a ; 1 ; 2 |
| b ; 2 ; 3 |

Как мы это построили в awk? Для удобства awk предоставляет специальную переменную NR, которая всегда сохраняет номер текущей строки из входного файла. Таким образом, мы можем просто использовать эту переменную для индексации первого измерения нашего массива, за исключением того, что NR отсчитывается от 1, поэтому нам нужно вычесть 1 для индексирования с отсчетом от 0:

a[NR-1] = $0

Здесь $0 содержит содержимое текущей строки в awk.

После первых строк n мы хотим объединить каждую новую строку с тем, что уже хранится в массиве, всегда начиная с вершины. Итак, нам нужно позаботиться о двух вещах:

  1. Вычисление правильного индекса для массива a
  2. Выполнение операции конкатенации

Следующая строка делает и то, и другое:

a[(NR-1)%n] = a[(NR-1)%n] "; " $0

Обратите внимание, что вычисление индекса массива теперь больше не NR-1, а (NR-1)%n, в котором используется оператор mod %. Конкатенация тривиальна: мы просто записываем три части для последовательного объединения: 1) предыдущую запись массива 2) разделительную строку ; и 3) снова текущую строку.

Тем не менее, мы наблюдаем кое-что интересное: благодаря тому, как awk обрабатывает неинициализированные переменные, мы могли почти использовать указанное выше выражение и для первых строк n, поскольку mod не изменяет эти значения, а { {X2}} при первом использовании будет просто пустой строкой. Единственная проблема - это разделительная строка: мы не хотим, чтобы она была в начале строки.

Но есть простой способ обойти это: мы можем просто не печатать его в конце. Тогда все, что осталось сделать, это следующее:

  • Для каждой строки: a[(NR-1)%n] = a[(NR-1)%n] "; " $0
  • В конце: распечатать содержимое a, но убрать первые два символа.

В основном это то, что делает следующий скрипт, за исключением того, что он также удаляет пробелы в начале и конце каждой строки (используя gsub) и добавляет немного удобства за счет использования аргументов командной строки:

#!/bin/sh

if [ $# -lt 2 ]; then
    echo "USAGE: $(basename "${0}") <n> <file>+"
    exit 1
fi

n=${1}
shift

awk -v "n=${n}" '
{ gsub(/^[ \t]+|[ \t]+$/, "", $0); a[(NR-1)%n] = a[(NR-1)%n] "; " $0 }
END { 
  for(i=0; i<n; i++) {
    print substr(a[i],3)
  }
}' ${@}
1
Thomas 6 Май 2021 в 15:50

Вот мое «не элегантное решение», использующее части ответа @Thomas:

#!/bin/bash

in_file=data.txt

# start at line 2
n=2
begin=$n
num_iterations=2

# first paste 2 starting lines to result file
head -n $n  $in_file > result.txt 

# then paste remaining 
for i in $(seq $num_iterations); do

    # increment starting line by 2
    let "begin+=$n"  

    # cut 2 lines and paste to temporary file
    head -n $begin  $in_file | tail -n $n | sed 's/ *# */ ; /' > tmp.txt  
    
    # concat results horizontally
    paste -d '; ' result.txt tmp.txt > result_tmp.txt;  
    
    # update result file
    mv result_tmp.txt result.txt  

done

Лучше попробуйте понять необъяснимое решение @oguz ismail

awk -v n=2 '++i>n{i=1} {r[i]=r[i]s[i]$0;s[i]="; "} END{for(i=1;i<=n;i++)print r[i]}' file
0
mcExchange 6 Май 2021 в 12:51