Skip to content

Instantly share code, notes, and snippets.

@McUsr
Last active July 12, 2023 11:16
Show Gist options
  • Save McUsr/74e277ff8bd4707f151cf73eae8e20c4 to your computer and use it in GitHub Desktop.
Save McUsr/74e277ff8bd4707f151cf73eae8e20c4 to your computer and use it in GitHub Desktop.
A utility to create a numbered copy of a file into the current directory
/**
* @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;
}
@McUsr
Copy link
Author

McUsr commented Jul 7, 2023

Updated indenting, removed an '\n' from a perror().

@McUsr
Copy link
Author

McUsr commented Jul 7, 2023

Manual indenting.

@McUsr
Copy link
Author

McUsr commented Jul 7, 2023

Improved buffering and documentation, concerning usage scenearios.

@McUsr
Copy link
Author

McUsr commented Jul 7, 2023

Final revision.

@McUsr
Copy link
Author

McUsr commented Jul 7, 2023

Corrected the description, it makes the numbered copy in the same directory as the original file, not in the current directory.

@McUsr
Copy link
Author

McUsr commented Jul 7, 2023

Implemented fread and fwrite and saves 2secs/Gb on my machine, in a thread-safe way.

@McUsr
Copy link
Author

McUsr commented Jul 10, 2023

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.

@McUsr
Copy link
Author

McUsr commented Jul 12, 2023

Started the numbering scheme at 1 instead of zero, which is the more natural place to start, leaving the original as version zero.

@McUsr
Copy link
Author

McUsr commented Jul 12, 2023

Cleaned up the code some more, removing unused variables, and refactoring local variables into one per function, instead of per block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment