flybywire
flybywire

Reputation: 273582

Shortest way to swap two files in bash

Can two files be swapped in bash?

Or, can they be swapped in a shorter way than this:

cp old tmp
cp curr old
cp tmp curr
rm tmp

Upvotes: 74

Views: 49475

Answers (18)

rmixarelli
rmixarelli

Reputation: 1

If your $(which mv) version supports -b then the code below is ok, otherwise edit the function with alternative temporary file strategy.

# define alias fswap

alias fswap='function _fswap () { mv -b ${1} ${2} && mv ${2}~ ${1}; }; _fswap'

# usage

fswap file1 file2

Upvotes: 0

For people looking at this nowadays: mv --exchange old curr

The flag has probably been introduced after this post was created and answered.

Upvotes: 3

sehe
sehe

Reputation: 393114

I use qmv (from rename_utils package in Debian). It it way more powerful, because it allows you to bulk rename batches of files using a text editor, but specifically it detects and handles cyclical renames.

Example:

enter image description here

Of course it's overpowered which is why I was looking here, but since nobody even mentioned this before, I thought I'd add.

Upvotes: 0

Can Bal
Can Bal

Reputation: 1583

$ mv old tmp && mv curr old && mv tmp curr

is slightly more efficient!

Wrapped into reusable shell function:

function swap()         
{
    local TMPFILE=tmp.$$
    mv "$1" $TMPFILE && mv "$2" "$1" && mv $TMPFILE "$2"
}

Upvotes: 93

ideasman42
ideasman42

Reputation: 48058

Here is a swap script with paranoid error checking to avoid unlikely case of a failure.

  • if any of the operations fail it's reported.
  • the path of the first argument is used for the temp path (to avoid moving between file-systems).
  • in the unlikely case the second move fails, the first is restored.

Script:

#!/bin/sh

if [ -z "$1" ] || [ -z "$2" ]; then
    echo "Expected 2 file arguments, abort!"
    exit 1
fi

if [ ! -z "$3" ]; then
    echo "Expected 2 file arguments but found a 3rd, abort!"
    exit 1
fi

if [ ! -f "$1" ]; then
    echo "File '$1' not found, abort!"
    exit 1
fi

if [ ! -f "$2" ]; then
    echo "File '$2' not found, abort!"
    exit 1
fi

# avoid moving between drives
tmp=$(mktemp --tmpdir="$(dirname '$1')")
if [ $? -ne 0 ]; then
    echo "Failed to create temp file, abort!"
    exit 1
fi

# Exit on error,
mv "$1" "$tmp"
if [ $? -ne 0 ]; then
    echo "Failed to to first file '$1', abort!"
    rm "$tmp"
    exit 1
fi

mv "$2" "$1"
if [ $? -ne 0 ]; then
    echo "Failed to move first file '$2', abort!"
    # restore state
    mv "$tmp" "$1"
    if [ $? -ne 0 ]; then
        echo "Failed to move file: (unable to restore) '$1' has been left at '$tmp'!"
    fi
    exit 1
fi

mv "$tmp" "$2"
if [ $? -ne 0 ]; then
    # this is very unlikely!
    echo "Failed to move file: (unable to restore) '$1' has been left at '$tmp', '$2' as '$1'!"
    exit 1
fi

Upvotes: 2

Hardy
Hardy

Reputation: 19119

Add this to your .bashrc:

function swap()         
{
    local TMPFILE=tmp.$$
    mv "$1" $TMPFILE
    mv "$2" "$1"
    mv $TMPFILE "$2"
}

If you want to handle potential failure of intermediate mv operations, check Can Bal's answer.

Please note that neither this, nor other answers provide an atomic solution, because it's impossible to implement such using Linux syscalls and/or popular Linux filesystems. For Darwin kernel, check exchangedata syscall.

Upvotes: 52

Richard
Richard

Reputation: 61289

This is what I use as a command on my system ($HOME/bin/swapfiles). I think it is relatively resilient to badness.

#!/bin/bash

if [ "$#" -ne 2 ]; then
  me=`basename $0`
  echo "Syntax: $me <FILE 1> <FILE 2>"
  exit -1
fi

if [ ! -f $1 ]; then
  echo "File '$1' does not exist!"
fi
if [ ! -f $2 ]; then
  echo "File '$2' does not exist!"
fi
if [[ ! -f $1 || ! -f $2 ]]; then
  exit -1
fi

tmpfile=$(mktemp $(dirname "$1")/XXXXXX)
if [ ! -f $tmpfile ]; then
  echo "Could not create temporary intermediate file!"
  exit -1
fi

# move files taking into account if mv fails
mv "$1" "$tmpfile" && mv "$2" "$1" && mv "$tmpfile" "$2"

Upvotes: 6

Bonaparte
Bonaparte

Reputation: 469

I have this in a working script I delivered. It's written as a function, but you would invoke it

d_swap lfile rfile

The GNU mv has the -b and the -T switch. You can deal with directories using the -T switch.

The quotes are for filenames with spaces.

It's a bit verbose, but I've used it many times with both files and directories. There might be cases where you would want to rename a file with the name a directory had, but that isn't handled by this function.

This isn't very efficient if all you want to do is rename the files (leaving them where they are), that is better done with a shell variable.

d_swap() {
 test $# -eq 2 || return 2

 test -e "$1" || return 3
 test -e "$2" || return 3

 if [ -f "$1" -a -f "$2" ]
 then
    mv -b "$1" "$2" && mv "$2"~ "$1"
    return 0
 fi

 if [ -d "$1" -a -d "$2" ]
 then
    mv -T -b "$1" "$2" && mv -T "$2"~ "$1"
    return 0
 fi

 return 4
}

This function will rename files. It uses a temp name (it puts a dot '.' in front of the name) just in case the files/directories are in the same directory, which is usually the case.

d_swapnames() {
    test $# -eq 2 || return 2

    test -e "$1" || return 3
    test -e "$2" || return 3

    local lname="$(basename "$1")"
    local rname="$(basename "$2")"

    ( cd "$(dirname "$1")" && mv -T "$lname" ".${rname}" ) && \
    ( cd "$(dirname "$2")" && mv -T "$rname" "$lname" ) && \
    ( cd "$(dirname "$1")" && mv -T ".${rname}" "$rname" )
}

That is a lot faster (there's no copying, just renaming). It is even uglier. And it will rename anything: files, directories, pipes, devices.

Upvotes: 0

cayhorstmann
cayhorstmann

Reputation: 3371

A somewhat hardened version that works for both files and directories:

function swap()
{
  if [ ! -z "$2" ] && [ -e "$1" ] && [ -e "$2" ] && ! [ "$1" -ef "$2" ] && (([ -f "$1" ] && [ -f "$2" ]) || ([ -d "$1" ] && [ -d "$2" ])) ; then
    tmp=$(mktemp -d $(dirname "$1")/XXXXXX)
    mv "$1" "$tmp" && mv "$2" "$1" &&  mv "$tmp"/"$1" "$2"
    rmdir "$tmp"
  else
    echo "Usage: swap file1 file2 or swap dir1 dir2"
  fi
}

This works on Linux. Not sure about OS X.

Upvotes: 4

adam_0
adam_0

Reputation: 7200

One problem I had when using any of the solutions provided here: your file names will get switched up.

I incorporated the use of basename and dirname to keep the file names intact*.

swap() {
    if (( $# == 2 )); then
        mv "$1" /tmp/
        mv "$2" "`dirname $1`"
        mv "/tmp/`basename $1`" "`dirname $2`"
    else
        echo "Usage: swap <file1> <file2>"
        return 1
    fi
}

I've tested this in bash and zsh.


*So to clarify how this is better:

If you start out with:

dir1/file2: this is file2
dir2/file1: this is file1

The other solutions would end up with:

dir1/file2: this is file1
dir2/file1: this is file2

The contents are swapped but the file names stayed. My solution makes it:

dir1/file1: this is file1
dir2/file2: this is file2

The contents and names are swapped.

Upvotes: 2

tsoomo
tsoomo

Reputation: 95

Hardy's idea was good enough for me. So I've tried my following two files to swap "sendsms.properties", "sendsms.properties.swap". But once I called this function as same argument "sendsms.properties" then this file deleted. Avoiding to this kind of FAIL I added some line for me :-)

function swp2file()
{   if [ $1 != $2 ] ; then
    local TMPFILE=tmp.$$
    mv "$1" $TMPFILE
    mv "$2" "$1"
    mv $TMPFILE "$2"
    else
    echo "swap requires 2 different filename"
    fi
}

Thanks again Hardy ;-)

Upvotes: 3

Leslie Viljoen
Leslie Viljoen

Reputation: 513

Combining the best answers, I put this in my ~/.bashrc:

function swap()
{
  tmpfile=$(mktemp $(dirname "$1")/XXXXXX)
  mv "$1" "$tmpfile" && mv "$2" "$1" &&  mv "$tmpfile" "$2"
}

Upvotes: 16

Łukasz Rysiak
Łukasz Rysiak

Reputation: 3078

do you actually want to swap them? i think its worth to mention that you can automatically backup overwritten file with mv:

mv new old -b

you'll get:

old and old~

if you'd like to have

old and old.old

you can use -S to change ~ to your custom suffix

mv new old -b -S .old
ls
old old.old

using this approach you can actually swap them faster, using only 2 mv:

mv new old -b && mv old~ new

Upvotes: 37

cube
cube

Reputation: 3948

tmpfile=$(mktemp $(dirname "$file1")/XXXXXX)
mv "$file1" "$tmpfile"
mv "$file2" "$file1"
mv "$tmpfile" "$file2"

Upvotes: 36

djna
djna

Reputation: 55907

using mv means you have one fewer operations, no need for the final rm, also mv is only changing directory entries so you are not using extra disk space for the copy.

Temptationh then is to implementat a shell function swap() or some such. If you do be extremly careful to check error codes. Could be horribly destructive. Also need to check for pre-existing tmp file.

Upvotes: 2

Yvo
Yvo

Reputation: 19263

You could simply move them, instead of making a copy.

#!/bin/sh
# Created by Wojtek Jamrozy (www.wojtekrj.net)
mv $1 cop_$1
mv $2 $1
mv cop_$1 $2

http://www.wojtekrj.net/2008/08/bash-script-to-swap-contents-of-files/

Upvotes: 6

arturh
arturh

Reputation: 6106

mv old tmp
mv curr old
mv tmp curr

Upvotes: 0

Alastair
Alastair

Reputation: 4523

Surely mv instead of cp?

Upvotes: 0

Related Questions