Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unix one-liner to swap/transpose two lines in multiple text files?

I wish to swap or transpose pairs of lines according to their line-numbers (e.g., switching the positions of lines 10 and 15) in multiple text files using a UNIX tool such as sed or awk.

For example, I believe this sed command should swap lines 14 and 26 in a single file:

sed -n '14p' infile_name > outfile_name
sed -n '26p' infile_name >> outfile_name

How can this be extended to work on multiple files? Any one-liner solutions welcome.

like image 766
Roger Avatar asked Apr 24 '15 17:04

Roger


People also ask

How do you switch lines in Linux?

Press ⌥⇧↑ (macOS), or Alt+Shift+Up Arrow (Windows/Linux), to move a line up. To move a line down use ⌥⇧↓ (macOS), or Alt+Shift+Down Arrow (Windows/Linux).

How do you add a line at the end of a file using sed?

There are different ways to insert a new line in a file using sed, such as using the “a” command, the “i” command, or the substitution command, “s“. sed's “a” command and “i” command are pretty similar.

How do you put a space between lines in shell script?

The G command appends a newline and the hold space to the end of the pattern space. Since, by default, the hold space is empty, this has the effect of just adding an extra newline at the end of each line. The prefix $! tells sed to do this on all lines except the last one.


3 Answers

If you want to edit a file, you can use ed, the standard editor. Your task is rather easy in ed:

printf '%s\n' 14m26 26-m14- w q | ed -s file

How does it work?

  • 14m26 tells ed to take line #14 and move it after line #26
  • 26-m14- tells ed to take the line before line #26 (which is your original line #26) and move it after line preceding line #14 (which is where your line #14 originally was)
  • w tells ed to write the file
  • q tells ed to quit.

If your numbers are in a variable, you can do:

linea=14
lineb=26
{
    printf '%dm%d\n' "$linea" "$lineb"
    printf '%d-m%d-\n' "$lineb" "$linea"
    printf '%s\n' w q
} | ed -s file

or something similar. Make sure that linea<lineb.

like image 130
gniourf_gniourf Avatar answered Oct 21 '22 17:10

gniourf_gniourf


  • If you want robust in-place updating of your input files, use gniourf_gniourf's excellent ed-based answer

  • If you have GNU sed and want to in-place updating with multiple files at once, use
    @potong's excellent GNU sed-based answer (see below for a portable alternative, and the bottom for an explanation)

Note: ed truly updates the existing file, whereas sed's -i option creates a temporary file behind the scenes, which then replaces the original - while typically not an issue, this can have undesired side effects, most notably, replacing a symlink with a regular file (by contrast, file permissions are correctly preserved).

Below are POSIX-compliant shell functions that wrap both answers.


Stdin/stdout processing, based on @potong's excellent answer:

  • POSIX sed doesn't support -i for in-place updating.
  • It also doesn't support using \n inside a character class, so [^\n] must be replaced with a cumbersome workaround that positively defines all character except \n that can occur on a line - this is a achieved with a character class combining printable characters with all (ASCII) control characters other than \n included as literals (via a command substitution using printf).
  • Also note the need to split the sed script into two -e options, because POSIX sed requires that a branching command (b, in this case) be terminated with either an actual newline or continuation in a separate -e option.
# SYNOPSIS
#   swapLines lineNum1 lineNum2
swapLines() {
  [ "$1" -ge 1 ] || { printf "ARGUMENT ERROR: Line numbers must be decimal integers >= 1.\n" >&2; return 2; }
  [ "$1" -le "$2" ] || { printf "ARGUMENT ERROR: The first line number ($1) must be <= the second ($2).\n" >&2; return 2; }
  sed -e "$1"','"$2"'!b' -e ''"$1"'h;'"$1"'!H;'"$2"'!d;x;s/^\([[:print:]'"$(printf '\001\002\003\004\005\006\007\010\011\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037\177')"']*\)\(.*\n\)\(.*\)/\3\2\1/'
}

Example:

$ printf 'line 1\nline 2\nline 3\n' | swapLines 1 3 
line 3
line 2
line 1

In-place updating, based on gniourf_gniourf's excellent answer:

Small caveats:

  • While ed is a POSIX utility, it doesn't come preinstalled on all platforms, notably not on Debian and the Cygwin and MSYS Unix-emulation environments for Windows.
  • ed always reads the input file as a whole into memory.
# SYNOPSIS
#   swapFileLines lineNum1 lineNum2 file
swapFileLines() {
  [ "$1" -ge 1 ] || { printf "ARGUMENT ERROR: Line numbers must be decimal integers >= 1.\n" >&2; return 2; }
  [ "$1" -le "$2" ] || { printf "ARGUMENT ERROR: The first line number ($1) must be <= the second ($2).\n" >&2; return 2; }
  ed -s "$3" <<EOF
H
$1m$2
$2-m$1-
w
EOF
}

Example:

$ printf 'line 1\nline 2\nline 3\n' > file
$ swapFileLines 1 3 file
$ cat file
line 3
line 2
line 1

An explanation of @potong's GNU sed-based answer:

His command swaps lines 10 and 15:

sed -ri '10,15!b;10h;10!H;15!d;x;s/^([^\n]*)(.*\n)(.*)/\3\2\1/' f1 f2 fn
  • -r activates support for extended regular expressions; here, notably, it allows use of unescaped parentheses to form capture groups.
  • -i specifies that the files specified as operands (f1, f2, fn) be updated in place, without backup, since no optional suffix for a backup file is adjoined to the -i option.

  • 10,15!b means that all lines that do not (!) fall into the range of lines 10 through 15 should branch (b) implicitly to the end of the script (given that no target-label name follows b), which means that the following commands are skipped for these lines. Effectively, they are simply printed as is.

  • 10h copies (h) line number 10 (the start of the range) to the so-called hold space, which is an auxiliary buffer.
  • 10!H appends (H) every line that is not line 10 - which in this case implies lines 11 through 15 - to the hold space.
  • 15!d deletes (d) every line that is not line 15 (here, lines 10 through 14) and branches to the end of the script (skips remaining commands). By deleting these lines, they are not printed.
  • x, which is executed only for line 15 (the end of the range), replaces the so-called pattern space with the contents of the hold space, which at that point holds all lines in the range (10 through 15); the pattern space is the buffer on which sed commands operate, and whose contents are printed by default (unless -n was specified).
  • s/^([^\n]*)(.*\n)(.*)/\3\2\1/ then uses capture groups (parenthesized subexpressions of the regular expression that forms the first argument passed to function s) to partition the contents of the pattern space into the 1st line (^([^\n]*)), the middle lines ((.*\n)), and the last line ((.*)), and then, in the replacement string (the second argument passed to function s), uses backreferences to place the last line (\3) before the middle lines (\2), followed by the first line (\1), effectively swapping the first and last lines in the range. Finally, the modified pattern space is printed.

As you can see, only the range of lines spanning the two lines to swap is held in memory, whereas all other lines are passed through individually, which makes this approach memory-efficient.

like image 20
mklement0 Avatar answered Oct 21 '22 18:10

mklement0


This might work for you (GNU sed):

sed -ri '10,15!b;10h;10!H;15!d;x;s/^([^\n]*)(.*\n)(.*)/\3\2\1/' f1 f2 fn

This stores a range of lines in the hold space and then swaps the first and last lines following the completion of the range.

The i flag edits each file (f1,f2 ... fn) in place.

like image 22
potong Avatar answered Oct 21 '22 17:10

potong