4

I have some text files, and I'd like to be able to move an arbitrary line in any of the files up or down one line (lines at the beginning or end of the file would stay where they are). I have some working code but it seems kludgy and I'm not convinced I have all the edge cases covered, so I'm wondering if there's some tool or paradigm that does this better (e.g. easier to understand the code (for other readers or me in 6 months), easier to debug, and easier to maintain; "more efficient" isn't very important).

move_up() {
  # fetch line with head -<line number> | tail -1
  # insert that one line higher
  # delete the old line
  sed -i -e "$((line_number-1))i$(head -$line_number $file | tail -1)" -e "${line_number}d" "$file"
}

move_down() {
  file_length=$(wc -l < "$file")
  if [[ "$line_number" -ge $((file_length - 1)) ]]; then
    # sed can't insert past the end of the file, so append the line
    # then delete the old line
    echo $(head -$line_number "$file" | tail -1) >> "$file"
    sed -i "${line_number}d" "$file"
  else
    # get the line, and insert it after the next line, and delete the original
    sed -i -e "$((line_number+2))i$(head -$line_number $file | tail -1)" -e "${line_number}d" "$file"
  fi
}

I can do error checking of inputs inside or outside these functions, but bonus points if bad input (like non-integers, non-existent files, or line numbers greater than the length of the file) are handled sanely.

I want it to run in a Bash script on modern Debian/Ubuntu systems. I don't always have root access but can expect "standard" tools to be installed (think a shared web server), and may be able to request installation of other tools if I can justify the request (though fewer external dependencies is always better).

Example:

$ cat b
1
2
3
4
$ file=b line_number=3 move_up
$ cat b
1
3
2
4
$ file=b line_number=3 move_down
$ cat b
1
3
4
2
$ 
don_crissti
  • 82,805

3 Answers3

14

Similar to Archemar's suggestion, you could script this with ed:

printf %s\\n ${linenr}m${addr} w q | ed -s infile

i.e.

linenr                      #  is the line number
m                           #  command that moves the line
addr=$(( linenr + 1 ))      #  if you move the line down
addr=$(( linenr - 2 ))      #  if you move the line up
w                           #  write changes to file
q                           #  quit editor

e.g. to move line no. 21 one line up:

printf %s\\n 21m19 w q | ed -s infile

to move line no. 21 one line down:

printf %s\\n 21m22 w q | ed -s infile

But since you only need to move a certain line up or down by one line, you could also say that you practically want to swap two consecutive lines. Meet sed:

sed -i -n 'addr{h;n;G};p' infile

i.e.

addr=${linenr}           # if you move the line down
addr=$(( linenr - 1 ))   # if you move the line up
h                        # replace content of the hold  buffer with a copy of the pattern space
n                        # read a new line replacing the current line in the pattern space  
G                        # append the content of the hold buffer to the pattern space
p                        # print the entire pattern space

e.g. to move line no. 21 one line up:

sed -i -n '20{h;n;G};p' infile

to move line no. 21 one line down:

sed -i -n '21{h;n;G};p' infile

I used gnu sed syntax above. If portability is a concern:

sed -n 'addr{
h
n
G
}
p' infile

Other than that, the usual checks: file exists and is writable; file_length > 2; line_no. > 1; line_no. < file_length;

don_crissti
  • 82,805
3

there is a vi command called move m

you can use vi in text mode : ex

  $line_number=7
  $line_up=$(($line_number + 1 ))
  (echo ${line_number}m${line_up} ; echo wq ) | ex foo

where

  • foo is your file
Archemar
  • 31,554
0

With vims (use vim in sed mode): https://github.com/MilesCranmer/vim-stream

You can:

cat file.txt | vims "$NUMBERm.-1"

To move the line down by one.

Miles
  • 111