Script to swap the names of two files
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
Solution 1:
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
Solution 2:
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 theIFS
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 variableREPLY
.
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.