7

I am new to Bash scripting. I have been trying to make a script that will swap the file names of the two files passed to it by the user.

Here's a picture of the two versions of my script so far

Here is the script in text format with mv:

#! /bin/bash
file1=$file1
file2=$file2

echo "write file name :"
read file1 file2

if [[ $file1 = $file2 ]]
then 
  cp $file1 $file1
  mv $file1 $file2 
fi

if [[ $file2 = $file1 ]]
then   
  mv $file2 $file1
fi

But my question is if I can make a script that let the user write down 2 filenames first, then the script will swap the 2 file names

The basic of swapping file names what I have read is this

cp $file1 temporaryfile
mv $file1 $file2
mv $file2 temporyfile
3
  • 2
    What's the point of file1=$file1? Did you mean file1=$1?
    – Barmar
    Dec 5, 2017 at 10:28
  • 2
    cp $file1 $file1 -- you're copying a file to itself.
    – Barmar
    Dec 5, 2017 at 10:31
  • 1
    You have single quotes around a string with variables in it. Don't you know that variables are only expanded inside double quotes?
    – Barmar
    Dec 5, 2017 at 10:32

2 Answers 2

11

One possible way to do it

A while back I created a function specifically for that purpose which I keep in my .bashrc, and can be adapted into a script. You should be taking advantage of positional parameters so that users can put filenames on command-line. Here's my original function:

swap_files() {
    if [ $# -ne 2 ]
    then
        echo "Usage: swap_files file1 file2"
    else
        local TMPFILE=$(mktemp) 
        mv -- "$1" "$TMPFILE"
        mv -- "$2" "$1"
        mv -- "$TMPFILE" "$2"
    fi
}

You can get rid of swap_files(){ declaration, local keyword, and closing }, and turn it into a script - just add #!/bin/bash at the top. Granted there's tons of things that can be improved, but at the very basic level that's about as simple as swapping gets (which by the way is frequently taught in C to swap array items, but that's just a tangent topic).

#!/bin/bash
if [ $# -ne 2 ]
then
    printf "Usage: swap_files file1 file2\n" > /dev/stderr
else
    TMPFILE=$(mktemp)
    mv -- "$1" "$TMPFILE"
    mv -- "$2" "$1"
    mv -- "$TMPFILE" "$2"
fi

Of course, remember to quote the positional parameters if filenames contain spaces. Like so:

swap_files 'file 1' 'file 2'

Note the use of -- to avoid issues with filenames that have leading - in them. A better way would be to get into habit of referencing files in current working directory with ./, especially if you are using globstar * ( globstar is not relevant in this question, but it's worth mentioning if we're talking about filenames with leading -). Besides,./ way is more portable, since some versions of mv such as on FreeBSD don't have the -- option.


As suggested by terdon in the comments, we can also create temp file in first file's parent folder to avoid moving files across filesystems.

#!/bin/bash
if [ $# -ne 2 ]
then
    printf "Usage: swap_files file1 file2\n" > /dev/stderr
else
    file1_dir=${1%/*}
    # just in case there were no slashes removed, assume cwd
    if [ "$file1_dir" = "$1" ]; then
        file1_dir="."
    fi
    tmpfile=$(mktemp -p "$file1_dir" )
    mv -- "$1" "$tmpfile"
    mv -- "$2" "$1"
    mv -- "$tmpfile" "$2"
fi

Your script and things to improve

1. Redundant variable assignment

file1=$file1
file2=$file2

This portion assigns $file1 variable to ... file1 variable; there's two problems with this - assigning variable to itself is redundant, and it doesn't exist to begin with, there's no declaration of that variable earlier in the script.

2. Beware of word splitting with read

Here's what's going to happen if your user tries to put in even quoted items into your read command:

$ read file1 file2
'one potato' 'two potato'

$ echo "$file1"
'one

$ echo "$file2"
potato' 'two potato'

In accordance with shell behavior, the shell splits everything that is read on stdin and tries to fit in each word into corresponding variables, and if words exceed number of variables - it tries to push everything into the last variable. I'd recommend you read in each file , one at a time.

3. Copying to itself is an error

You're doing

cp $file1 $file1;

That'll produce an error

$ cp input.txt input.txt
cp: 'input.txt' and 'input.txt' are the same file

Perhaps you wanted to do

cp "$file1" "$file1".tmp

Or just make use of mktemp command as I did. Also note the quoting of variables to prevent word splitting.


Other fun ways to do it

Did you know that you can cat any file with redirection to make a copy? So using mv or cp isn't the only way. Something like this:

$ cat ./wallpaper.jpg > wallpaper.jpg.tmp
$ cat ./screenshot.jpg > wallpaper.jpg
$ cat ./wallpaper.jpg.tmp > ./screenshot.jpg
3
  • 4
    You're not keeping the temp file, you're mving it. Also, for one more improvement, you can use dir1="${file1%/*}"; tmpfile="$(mktemp -p "$dir1")" so that the tempfile is always created on the same filesystem as the original file, to avoid running mv across filesystem boundaries.
    – terdon
    Dec 5, 2017 at 8:56
  • 1
    Just a note: Tempfiles created by mktemp never contain whitespaces 'n stuff, so quoting $TMPFILE isn't necessary – on the other hand it's good practice to just quote any path and filename occuring in a script.
    – dessert
    Dec 5, 2017 at 9:17
  • @dessert Good point. Although there's more to quoting than just whitespaces. You might be interested in unix.stackexchange.com/q/171346/85039 Dec 5, 2017 at 9:27
6

You can use parameter expansion for the task if getting your two filenames or you can read them in in the script. my below example script uses parameter expansion. And you might want to use a temporary directory for your move options, because if the filename used in the script already exists, this file will silently be overwritten.

#!/bin/bash

# creating a temporary directory to prevent overwrites if the file called 'tmpfile' exists
TMP=$(mktemp -d)

# copying the filenames into variables for later use
file1="$1"
file2="$2"

# swapping the files
# first move and rename file1 to $TMP/tempfile
mv "$1" $TMP/tempfile
# renaming file2 to file1 name
mv "$2" "$file1"
# now renaming and moving the tempfile to file2 
mv $TMP/tempfile "$file2"
# removing the temporary folder if tempfile is not existent anymore
[ -e $TMP/tempfile ] && echo "Error!" || rm -r $TMP

From Bash-Manual #Shell-Parameters:

A parameter is an entity that stores values. It can be a name, a number, or one of the special characters listed below. A variable is a parameter denoted by a name. A variable has a value and zero or more attributes. Attributes are assigned using the declare builtin command (see the description of the declare builtin in Bash Builtins).

A parameter is set if it has been assigned a value. The null string is a valid value. Once a variable is set, it may be unset only by using the unset builtin command.

A variable may be assigned to by a statement of the form

name=[value]

And if you want to read in the filenames from an interactive dialog:

#!/bin/bash

# creating a temporary directory to prevent overwrites if the file called 'tmpfile' exists
TMP=$(mktemp -d)

# reading the filenames from user input into variables for later use
read -rp "Enter first filename: " file1
read -rp "Enter second filename: " file2

# swapping the files
# first move and rename file1 to $TMP/tempfile
mv "$file1" $TMP/tempfile
# renaming file2 to file1 name
mv "$file2" "$file1"
# now renaming and moving the tempfile to file2 
mv $TMP/tempfile "$file2"
# removing the temporary folder if tempfile is not existent anymore
[ -e $TMP/tempfile ] && echo "Error!" || rm -r $TMP

From Bash-Manual #Bash-Builtins:

read [-ers] [-a aname] [-d delim] [-i text] [-n nchars]
    [-N nchars] [-p prompt] [-t timeout] [-u fd] [name …]

One line is read from the standard input, or from the file descriptor fd supplied as an argument to the -u option, split into words as described above in [Word Splitting][6], and the first word is assigned to the first name, the second word to the second name, and so on. If there are more words than names, the remaining words and their intervening delimiters are assigned to the last name. If there are fewer words read from the input stream than names, the remaining names are assigned empty values. The characters in the value of the IFS variable are used to split the line into words using the same rules the shell uses for expansion (described above in [Word Splitting][6]). The backslash character ‘\’ may be used to remove any special meaning for the next character read and for line continuation. If no names are supplied, the line read is assigned to the variable REPLY.

read accepts several options. In this case, two are most relevant, since you want to ask the user a question and get input for them. Those options are:

  • -r → If this option is given, backslash does not act as an escape character. The backslash is considered to be part of the line. In particular, a backslash-newline pair may not be used as a line continuation.
  • -p prompt → Display prompt, without a trailing newline, before attempting to read any input. The prompt is displayed only if input is coming from a terminal.

While this is not the worst situation to forget -r, you almost always want to include it to prevent \ from acting as an escape character. -p shows the user a prompt.

3
  • 1
    Blanks are allowed after #! actually, see unix.stackexchange.com/q/276751/85039 Otherwise good answer Dec 5, 2017 at 8:09
  • mv $TMP/tempfile "$file2" && would prevent $TMP from being removed in case mv could finish successfully (imagine a full disk) – or how about [ -e $TMP/tempfile ] && rm -r $TMP || echo "Error!"?
    – dessert
    Dec 5, 2017 at 9:48
  • 1
    There's still $1 and $2 in your second script… Btw, what's the use of $file1 and $file2 in the first one?
    – dessert
    Dec 5, 2017 at 9:50

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .