-
-
Save McUsr/74e277ff8bd4707f151cf73eae8e20c4 to your computer and use it in GitHub Desktop.
/** | |
* @brief nrcp : makes a numbered copy of a file. | |
* | |
* It finds the ascending number that isn't used | |
* as a suffix for the file, and creates a copy | |
* of the file with that number appended as a | |
* suffix -- in the same directory as the original | |
* file! | |
* | |
* @author McUsr | |
* | |
* Thanks to: u/N-R-K, u/oh5nxo and u/teksnl | |
* | |
* Copyright (C) 2023 - McUsr License: LGPL2.0 | |
* | |
* This program is free software; you can redistribute it | |
* and/or modify it under the terms of the GNU General | |
* Public License as published by the Free Software | |
* Foundation; either version 2 of the License, or (at your | |
* option) any later version. | |
* | |
* This program is distributed in the hope that it will be | |
* useful, but WITHOUT ANY WARRANTY; without even the | |
* implied warranty of MERCHANTABILITY or FITNESS FOR A | |
* PARTICULAR PURPOSE. See the GNU General Public License | |
* for more details. | |
* | |
* You should have received a copy of the GNU General Public | |
* License along with this program; if not, write to the | |
* Free Software Foundation, Inc., 59 Temple Place - Suite | |
* 330, Boston, MA 02111-1307, USA. | |
* | |
* | |
* Intended usage: | |
* | |
* Is to for instance have a way of making "cheap versions" | |
* while tweaking some configuration, so it is easy/possible | |
* to back track and get that modification back without | |
* trashing the editors undo tree, and so on. | |
* | |
* You can even add the -k option, which keeps the permissions | |
* for an executable shell script and also will keep the file | |
* extension, so that test.c becomes test.0.c,and so on.. | |
* | |
* It is also intended to be run in directories you own, or | |
* has full rights to, as user "you". Not intended to be used | |
* as a "setuid", but it will work when executed as root. | |
* | |
* It is not Windows compatible due to 'sys/stat' that has a | |
* different layout on Windows machines. | |
* | |
* To compile: `cc -std=c99 -o nrcp nrcp.c | |
* | |
* | |
*/ | |
#define _XOPEN_SOURCE 700 | |
#include<stdio.h> | |
#include<sys/types.h> | |
#include<sys/stat.h> | |
#include<unistd.h> | |
#include<stdbool.h> | |
#include<errno.h> | |
#include<stdlib.h> | |
#include<string.h> | |
extern int errno; | |
#define PNAME "nrcp" | |
static bool fileexists( const char *fname ); | |
static char *fnwithnumber( char *fname ); | |
static char *fnwithnrembedded( char *fname ); | |
static void anumberedcopy( char *fntobackup, | |
bool preserve ); | |
static void usage( ); | |
static char *geterrstr( char const *funcname, | |
int theerrno ); | |
int main( int argc, char *argv[] ) | |
{ | |
bool keep = false; | |
if ( argc >= 2 ) { | |
if ( !strcmp( argv[1], "-k" ) ) { | |
keep = true; | |
argc--; | |
argv++; | |
} else if ( !strcmp( argv[1], "-h" ) ) { | |
usage( ); | |
exit( EXIT_SUCCESS ); | |
} | |
} | |
if ( argc < 2 ) { /* check for file names */ | |
fprintf( stderr, | |
PNAME | |
": Missing the file to make a numbered " | |
"copy of.\n" ); | |
usage( ); | |
exit( EXIT_FAILURE ); | |
} | |
if ( fileexists( argv[1] ) == true ) { | |
anumberedcopy( argv[1], keep ); | |
} else { | |
fprintf( stderr, PNAME | |
": The file %s, don't exist!\n", argv[1] ); | |
exit( 1 ); | |
} | |
return 0; | |
} | |
static void usage( ) | |
{ | |
printf( "\n" PNAME | |
": Makes a numbered copy of a file.\n\n" | |
"An ascending nr. is appended or embedded at the end of" | |
" the copy.\n\nUSAGE: " PNAME " -h -k FILE\n\n" | |
" Options:\n\n -h - help: Show this help and die." | |
"\n\n -k - keep: Keep the original file extension " | |
"and file mode.\n\n" ); | |
} | |
static blksize_t bfsize( const char *fname ); | |
/** | |
* @brief | |
* creates a numbered copy of a file with | |
* first free number appended to it in the | |
* current directory | |
* | |
* @param fntobackup | |
* The file to back up. | |
*/ | |
static void anumberedcopy( char *fntobackup, bool preserve ) | |
{ | |
FILE *in, *out; | |
blksize_t bsize;; | |
char *buf; | |
int thiserr; | |
struct stat fs; | |
int res; | |
char *errstr; | |
bool backupsuccess; | |
char *fntobackupto; | |
if ( fntobackup == NULL ) { | |
fprintf( stderr, PNAME | |
": anumberedcopy: fntobackup == NULL\n" ); | |
exit( EXIT_FAILURE ); | |
} | |
in = fopen( fntobackup, "rb" ); /* open for read */ | |
if ( in == NULL ) { | |
thiserr = errno; | |
errstr = | |
geterrstr( "anumberedcopy", thiserr ); | |
fprintf( stderr, | |
"%s: Couldn't open file to" | |
"backup: '%s'.\n%s\n", | |
PNAME,fntobackup, errstr ); | |
free( errstr ); | |
errstr=NULL; | |
exit( EXIT_FAILURE ); | |
} else { | |
if ( preserve == true ) { | |
res = stat( fntobackup, &fs ); | |
if ( res == -1 ) { | |
thiserr = errno; | |
errstr = | |
geterrstr( "anumberedcopy", thiserr ); | |
fprintf( stderr, | |
PNAME ": Error reading file-" | |
"type and mode for'%s'\n%s\n", | |
fntobackup, errstr ); | |
free( errstr ); | |
exit( EXIT_FAILURE ); | |
} | |
fntobackupto = fnwithnrembedded( fntobackup ); | |
} else { | |
fntobackupto = fnwithnumber( fntobackup ); | |
} | |
} | |
out = fopen( fntobackupto, "wb" ); /* open for write */ | |
if ( out == NULL ) { | |
thiserr = errno; | |
errstr = | |
geterrstr( "anumberedcopy", thiserr ); | |
fprintf( stderr, | |
"%s: Couldn't create file to" | |
"backup to: '%s'.\n%s\n", | |
PNAME,fntobackupto, errstr ); | |
free( errstr ); | |
errstr=NULL; | |
free( fntobackupto ); | |
fntobackupto=NULL; | |
exit( EXIT_FAILURE ); | |
} | |
bsize = bfsize( fntobackup ); | |
buf = malloc( sizeof( char ) * bsize ); | |
if ( buf == NULL ) { | |
thiserr = errno; | |
errstr = | |
geterrstr( "anumberedcopy", thiserr ); | |
fprintf( stderr, | |
"%s: Couldn't allocate meory for buffer" | |
"when backing up to: '%s'.\n%s\n", | |
PNAME,fntobackupto, errstr ); | |
free( errstr ); | |
errstr=NULL; | |
free( fntobackupto ); | |
fntobackupto=NULL; | |
exit( EXIT_FAILURE ); | |
} | |
/** assign large buffers for files | |
* _IOFBF == fully buffered */ | |
for ( ;; ) { | |
size_t len = fread( buf, 1, sizeof( buf ), in ); | |
if ( len == 0 ) { | |
break; | |
} | |
fwrite( buf, 1, len, out ); | |
} | |
backupsuccess = true; | |
if ( ferror( in ) ) { | |
thiserr = errno; | |
errstr = | |
geterrstr( "anumberedcopy", thiserr ); | |
fprintf( stderr, | |
"%s: There were errors reading the input" | |
"stream :'%s'.\n%s\n", | |
PNAME,fntobackup, errstr ); | |
free( errstr ); | |
errstr=NULL; | |
clearerr( in ); | |
backupsuccess = false; | |
} | |
fclose( in ); | |
if ( ferror( out ) ) { | |
thiserr = errno; | |
errstr = | |
geterrstr( "anumberedcopy", thiserr ); | |
fprintf( stderr, | |
"%s: There were errors writing the output" | |
"stream :'%s'.\n%s\n", | |
PNAME,fntobackupto, errstr ); | |
free( errstr ); | |
clearerr( in ); | |
backupsuccess = false; | |
clearerr( out ); | |
} | |
fclose( out ); | |
if ( preserve == true ) { | |
res = chmod( fntobackupto, fs.st_mode ); | |
if ( res != 0 ) { | |
thiserr = errno; | |
errstr = geterrstr( "anumberedcopy", thiserr ); | |
fprintf( stderr, | |
PNAME ": Unable to change file-" | |
"type and mode for %s\n%s\n", | |
fntobackupto, errstr ); | |
free( errstr ); | |
backupsuccess = false; | |
} | |
} | |
if ( backupsuccess == true ) { | |
fprintf( stderr, PNAME ": copied %s to %s\n", | |
fntobackup, fntobackupto ); | |
} else { | |
fprintf( stderr, | |
PNAME | |
": There were errors copying %s to %s\n", | |
fntobackup, fntobackupto ); | |
if ( unlink( fntobackupto ) == -1 ) { | |
thiserr = errno; | |
errstr = | |
geterrstr( "anumberedcopy", thiserr ); | |
fprintf( stderr, | |
"%s: Error deleting bad backup file" | |
":'%s'.\n%s\n", PNAME,fntobackupto, | |
errstr ); | |
free( errstr ); | |
} | |
} | |
free( buf ); | |
free( fntobackupto ); | |
if ( backupsuccess == false ) { | |
exit( EXIT_FAILURE ); | |
} | |
} | |
/** | |
* @brief | |
* Creates a file name with an append number that doesn't | |
* exist. | |
* | |
* The numbers are probed in ascending order, much like on | |
* an Apple machine. | |
* | |
* @param fname | |
* | |
* @return | |
* The new file name. | |
*/ | |
static char *fnwithnumber( char *fname ) | |
{ | |
static char *newfname; | |
char *endptr; | |
size_t flen = strlen( fname ); | |
newfname = malloc( flen + 5 ); | |
if ( newfname == NULL ) { | |
perror( PNAME | |
": fnwithnumber: couldn't allocate mem" | |
" for new filename \n" ); | |
exit( 1 ); | |
} | |
char *p = memcpy( newfname, fname, flen ); | |
memcpy( p + flen, ".1", sizeof ".1" ); | |
endptr = newfname + flen; /* points to last '.' */ | |
if ( fileexists( newfname ) == true ) { | |
int ctr = 1; | |
do { | |
ctr++; | |
if ( ctr < 10 ) { | |
endptr[1] = ctr + '0'; | |
} else if ( ctr < 100 ) { | |
endptr[1] = ( ctr / 10 ) + '0'; | |
endptr[2] = ( ctr % 10 ) + '0'; | |
} else if ( ctr < 1000 ) { | |
endptr[1] = ( ctr / 100 ) + '0'; | |
endptr[2] = ( ( ctr % 100 ) / 10 ) + '0'; | |
endptr[3] = ( ctr % 10 ) + '0'; | |
} | |
} while ( fileexists( newfname ) == true && | |
ctr < 999 ); | |
if ( ctr == 999 ) { | |
fprintf( stderr, | |
PNAME ": fnwithnumber: There are " | |
"1000 backup files from the original, " | |
"please change file name\n to something" | |
" other than %s\n", fname ); | |
exit( 1 ); | |
} | |
} | |
p = NULL; | |
return newfname; | |
} | |
/** | |
* @brief | |
* Creates a file name with a number embedded before file | |
* extension that doesn't exist. | |
* | |
* The numbers are probed in ascending order, much like on | |
* an Apple machine. | |
* | |
* @param fname | |
* | |
* @return | |
* The new file name. | |
*/ | |
static char *fnwithnrembedded( char *fname ) | |
{ | |
static char *newfname; | |
char *tempfname; | |
char *extension; | |
size_t extlen; | |
char *embdptr; | |
char *endptr; | |
endptr = strrchr( fname, '.' ); | |
if ( endptr == NULL ) { | |
return fnwithnumber( fname ); | |
} else { | |
extension = strdup( endptr ); | |
extlen = strlen( extension ); | |
} | |
size_t flen = strlen( fname ) - extlen; | |
tempfname = malloc( flen + 5 ); | |
if ( tempfname == NULL ) { | |
perror( PNAME | |
": fnwithnrembedded: couldn't allocate mem" | |
" for temp filename \n" ); | |
exit( EXIT_FAILURE ); | |
} | |
newfname = malloc( flen + 5 + extlen ); | |
if ( newfname == NULL ) { | |
perror( PNAME | |
": fnwithnrembedded: couldn't allocate mem" | |
" for new filename \n" ); | |
exit( EXIT_FAILURE ); | |
} | |
char *t = memcpy( tempfname, fname, flen ); | |
memcpy( t + flen, ".1", sizeof ".1" ); | |
embdptr = tempfname + flen; /* points to last '.' */ | |
memcpy( newfname, tempfname, flen + 5 ); | |
strcat( newfname, extension ); | |
if ( fileexists( newfname ) == true ) { | |
int ctr = 1; | |
do { | |
ctr++; | |
if ( ctr < 10 ) { | |
embdptr[1] = ctr + '0'; | |
} else if ( ctr < 100 ) { | |
embdptr[1] = ( ctr / 10 ) + '0'; | |
embdptr[2] = ( ctr % 10 ) + '0'; | |
} else if ( ctr < 1000 ) { | |
embdptr[1] = ( ctr / 100 ) + '0'; | |
embdptr[2] = ( ( ctr % 100 ) / 10 ) + '0'; | |
embdptr[3] = ( ctr % 10 ) + '0'; | |
} | |
/* Need to update some meta-data here */ | |
memcpy( newfname, tempfname, flen + 5 ); | |
strcat( newfname, extension ); | |
} while ( fileexists( newfname ) == true && | |
ctr < 999 ); | |
if ( ctr == 999 ) { | |
fprintf( stderr, | |
PNAME ": fnwithnumber: There are " | |
"1000 backup files from the original, " | |
"please change file name\n to something" | |
" other than %s\n", fname ); | |
exit( 1 ); | |
} | |
} | |
free( tempfname ); | |
tempfname = NULL; | |
free( extension ); | |
extension = NULL; | |
return newfname; | |
} | |
/** | |
* @brief | |
* Checks if a file can be opened for reading. (exists) | |
* | |
* @param fname | |
* | |
* @return | |
* true if file exists. | |
*/ | |
static bool fileexists( const char *fname ) | |
{ | |
if ( access( fname, F_OK ) == 0 ) { | |
return true; | |
} else { | |
return false; | |
} | |
} | |
/** | |
* @brief Find the preferred blocksize for a file. | |
* | |
* @param fname | |
* | |
* @return | |
* The number | |
*/ | |
static blksize_t bfsize( const char *fname ) | |
{ | |
struct stat statbuf; | |
if ( stat( fname, &statbuf ) == -1 ) { | |
perror( PNAME ": stat" ); | |
exit( EXIT_FAILURE ); | |
} | |
return statbuf.st_blksize; | |
} | |
/** | |
@brief returns a formattedd string with the function name | |
that causeed the error, the error message, and the error | |
number. | |
*/ | |
static char *geterrstr( char const *funcname, int theerrno ) | |
{ | |
static char *buffer; | |
size_t needed = | |
snprintf( NULL, 0, "%s: %s (%d)", funcname, | |
strerror( theerrno ), | |
theerrno ) + 1; | |
buffer = malloc( needed ); | |
sprintf( buffer, "%s: %s (%d)", funcname, | |
strerror( theerrno ), theerrno ); | |
return buffer; | |
} |
Manual indenting.
Improved buffering and documentation, concerning usage scenearios.
Final revision.
Corrected the description, it makes the numbered copy in the same directory as the original file, not in the current directory.
Implemented fread and fwrite and saves 2secs/Gb on my machine, in a thread-safe way.
Final version, added a small help -h option, and a -k option, that will embed the number inside of the file extension, and preserve the original mode of the file, like the executable bit, so that it makes for even easier workflows.
Started the numbering scheme at 1 instead of zero, which is the more natural place to start, leaving the original as version zero.
Cleaned up the code some more, removing unused variables, and refactoring local variables into one per function, instead of per block.
Updated indenting, removed an '\n' from a perror().