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

后端 未结 8 1772
谎友^
谎友^ 2021-01-06 03:22

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 a

8条回答
  •  清酒与你
    2021-01-06 04:02

    The use of the following helper script allows using the power of find ... -exec ./script '{}' l1 l2 \; to locate the target files and to swap lines l1 & l2 in each file in place. (it requires that there are no identical duplicate lines within the file that fall within the search range) The script uses sed to read the two swap lines from each file into an indexed array and passes the lines to sed to complete the swap by matching. The sed call uses its "matched first address" state to limit the second expression swap to the first occurrence. An example use of the helper script below to swap lines 5 & 15 in all matching files is:

    find . -maxdepth 1 -type f -name "lnum*" -exec ../swaplines.sh '{}' 5 15 \;
    

    For example, the find call above found files lnumorig.txt and lnumfile.txt in the present directory originally containing:

    $ head -n20 lnumfile.txt.bak
     1  A simple line of test in a text file.
     2  A simple line of test in a text file.
     3  A simple line of test in a text file.
     4  A simple line of test in a text file.
     5  A simple line of test in a text file.
     6  A simple line of test in a text file.
    
    14  A simple line of test in a text file.
    15  A simple line of test in a text file.
    16  A simple line of test in a text file.
    17  A simple line of test in a text file.
    18  A simple line of test in a text file.
    19  A simple line of test in a text file.
    20  A simple line of test in a text file.
    

    And swapped the lines 5 & 15 as intended:

    $ head -n20 lnumfile.txt
     1  A simple line of test in a text file.
     2  A simple line of test in a text file.
     3  A simple line of test in a text file.
     4  A simple line of test in a text file.
    15  A simple line of test in a text file.
     6  A simple line of test in a text file.
    
    14  A simple line of test in a text file.
     5  A simple line of test in a text file.
    16  A simple line of test in a text file.
    17  A simple line of test in a text file.
    18  A simple line of test in a text file.
    19  A simple line of test in a text file.
    20  A simple line of test in a text file.
    

    The helper script itself is:

    #!/bin/bash
    
    [ -z $1 ] && {              # validate requierd input (defaults set below)
        printf "error: insufficient input calling '%s'. usage: file [line1 line2]\n" "${0//*\//}" 1>&2
        exit 1
    }
    
    l1=${2:-10}                 # default/initialize line numbers to swap
    l2=${3:-15}
    
    while IFS=$'\n' read -r line; do  # read lines to swap into indexed array
        a+=( "$line" ); 
    done <<<"$(sed -n $((l1))p "$1" && sed -n $((l2))p "$1")"
    
    ((${#a[@]} < 2)) && {       # validate 2 lines read
        printf "error: requested lines '%d & %d' not found in file '%s'\n" $l1 $l2 "$1"
        exit 1
    }
    
                                # swap lines in place with sed (remove .bak for no backups)
    sed -i.bak -e "s/${a[1]}/${a[0]}/" -e "0,/${a[0]}/s/${a[0]}/${a[1]}/" "$1"
    
    exit 0
    

    Even though I didn't manage to get it all done in a one-liner I decided it was worth posting in case you can make some use of it or take ideas from it. Note: if you do make use of it, test to your satisfaction before turning it loose on your system. The script currently uses sed -i.bak ... to create backups of the files changed for testing purposes. You can remove the .bak when you are satisfied it meets your needs.

    If you have no use for setting default lines to swap in the helper script itself, then I would change the first validation check to [ -z $1 -o -z $2 -o $3 ] to insure all required arguments are given when the script is called.

    While it does identify the lines to be swapped by number, it relies on the direct match of each line to accomplish the swap. This means that any identical duplicate lines up to the end of the swap range will cause an unintended match and failue to swap the intended lines. This is part of the limitation imposed by not storing each line within the range of lines to be swapped as discussed in the comments. It's a tradeoff. There are many, many ways to approach this, all will have their benefits and drawbacks. Let me know if you have any questions.


    Brute Force Method

    Per your comment, I revised the helper script to use the brute forth copy/swap method that would eliminate the problem of any duplicate lines in the search range. This helper obtains the lines via sed as in the original, but then reads all lines from file to tmpfile swapping the appropriately numbered lines when encountered. After the tmpfile is filled, it is copied to the original file and tmpfile is removed.

    #!/bin/bash
    
    [ -z $1 ] && {              # validate requierd input (defaults set below)
        printf "error: insufficient input calling '%s'. usage: file [line1 line2]\n" "${0//*\//}" 1>&2
        exit 1
    }
    
    l1=${2:-10}                 # default/initialize line numbers to swap
    l2=${3:-15}
    
    while IFS=$'\n' read -r line; do  # read lines to swap into indexed array
        a+=( "$line" ); 
    done <<<"$(sed -n $((l1))p "$1" && sed -n $((l2))p "$1")"
    
    ((${#a[@]} < 2)) && {       # validate 2 lines read
        printf "error: requested lines '%d & %d' not found in file '%s'\n" $l1 $l2 "$1"
        exit 1
    }
    
                                # create tmpfile, set trap, truncate
    fn="$1"
    rmtemp () { cp "$tmpfn" "$fn"; rm -f "$tmpfn"; }
    trap rmtemp SIGTERM SIGINT EXIT
    
    declare -i n=1
    tmpfn="$(mktemp swap_XXX)"
    :> "$tmpfn"
    
                                # swap lines in place with a tmpfile
    while IFS=$'\n' read -r line; do
    
        if ((n == l1)); then
            printf "%s\n" "${a[1]}" >> "$tmpfn"
        elif ((n == l2)); then
            printf "%s\n" "${a[0]}" >> "$tmpfn"
        else
            printf "%s\n" "$line" >> "$tmpfn"
        fi
        ((n++))
    
    done < "$fn"
    
    exit 0
    

提交回复
热议问题