[PATCH] Add a "-h" flag to mv

From: John Baldwin <jhb_at_freebsd.org>
Date: Tue, 28 Aug 2012 10:58:09 -0400
I have a use case at work where I need to be able to update a symlink that 
points to a directory atomically (so that it points to a new directory).  To 
give a conrete example, suppose I have two directories 'foo' and 'bar', and a 
symlink 'a' that I wish to atomically flip from 'foo' to 'bar'.

Using 'ln -shf bar a' is not atomic as it uses separate unlink() and symlink() 
system calls, so there is a race where another thread may encounter ENOENT 
while traversing 'a'.

The approach we used was to create a new symbolic link 'a.new' (e.g. via
'ln -s bar a.new') and then use rename() to rename 'a.new' on top of 'a'.
Normally to do an atomic rename from userland one would use 'mv', but
'mv a.new a' doesn't do that.  Instead, it moves 'a.new' into the directory
referenced by the 'a' symlink.  At work we have resorted to invoking python's
os.rename() in a one-liner to handle this.

While rehashing this discussion today it occurred to me that a -h flag to
mv would allow it to work in this case (and is very similar to how ln treats
its -h flag).  To that end, I have a patch to add a new -h flag to mv that
allows one to atomically update a symlink that points to a directory.  I
could not find any other mv commands that have adopted a -h (or a different
flag that accomplishes the same task).  Given that it functions identically
to the -h flag for ln, -h seemed the "logical" choice.  Any objections?

Index: mv.1
===================================================================
--- mv.1	(revision 239731)
+++ mv.1	(working copy)
_at__at_ -32,7 +32,7 _at__at_
 .\"	_at_(#)mv.1	8.1 (Berkeley) 5/31/93
 .\" $FreeBSD$
 .\"
-.Dd May 12, 2007
+.Dd August 28, 2012
 .Dt MV 1
 .Os
 .Sh NAME
_at__at_ -41,7 +41,7 _at__at_
 .Sh SYNOPSIS
 .Nm
 .Op Fl f | i | n
-.Op Fl v
+.Op Fl hv
 .Ar source target
 .Nm
 .Op Fl f | i | n
_at__at_ -81,6 +81,21 _at__at_
 or
 .Fl n
 options.)
+.It Fl h
+If the
+.Ar target
+operand is a symbolic link to a directory,
+do not follow it.
+This causes the
+.Nm
+utility to rename the file
+.Ar source
+to the destination path
+.Ar target
+rather than moving
+.Ar source
+into the directory referenced by
+.Ar target .
 .It Fl i
 Cause
 .Nm
_at__at_ -142,7 +157,8 _at__at_
 .Ex -std
 .Sh COMPATIBILITY
 The
-.Fl n
+.Fl h ,
+.Fl n ,
 and
 .Fl v
 options are non-standard and their use in scripts is not recommended.
Index: mv.c
===================================================================
--- mv.c	(revision 239731)
+++ mv.c	(working copy)
_at__at_ -68,7 +68,7 _at__at_
 /* Exit code for a failed exec. */
 #define EXEC_FAILED 127
 
-static int	fflg, iflg, nflg, vflg;
+static int	fflg, hflg, iflg, nflg, vflg;
 
 static int	copy(const char *, const char *);
 static int	do_move(const char *, const char *);
_at__at_ -87,8 +87,11 _at__at_
 	int ch;
 	char path[PATH_MAX];
 
-	while ((ch = getopt(argc, argv, "finv")) != -1)
+	while ((ch = getopt(argc, argv, "fhinv")) != -1)
 		switch (ch) {
+		case 'h':
+			hflg = 1;
+			break;
 		case 'i':
 			iflg = 1;
 			fflg = nflg = 0;
_at__at_ -123,6 +126,17 _at__at_
 		exit(do_move(argv[0], argv[1]));
 	}
 
+	/*
+	 * If -h was specified, treat the target as a symlink instead of
+	 * directory.
+	 */
+	if (hflg) {
+		if (argc > 2)
+			usage();
+		if (lstat(argv[1], &sb) == 0 && S_ISLNK(sb.st_mode))
+			exit(do_move(argv[0], argv[1]));
+	}
+
 	/* It's a directory, move each file into it. */
 	if (strlen(argv[argc - 1]) > sizeof(path) - 1)
 		errx(1, "%s: destination pathname too long", *argv);
_at__at_ -483,7 +497,7 _at__at_
 {
 
 	(void)fprintf(stderr, "%s\n%s\n",
-		      "usage: mv [-f | -i | -n] [-v] source target",
+		      "usage: mv [-f | -i | -n] [-hv] source target",
 		      "       mv [-f | -i | -n] [-v] source ... directory");
 	exit(EX_USAGE);
 }

-- 
John Baldwin
Received on Tue Aug 28 2012 - 12:58:11 UTC

This archive was generated by hypermail 2.4.0 : Wed May 19 2021 - 11:40:30 UTC