Finished printing...

This commit is contained in:
Ognjen Milan Robovic 2023-11-13 15:07:28 -05:00
parent 17df8ad502
commit 663fc649ff
2 changed files with 87 additions and 71 deletions

View File

@ -54,77 +54,82 @@ static void echo_one_by_one (int number) {
You can find situations in which fall-through cases are good, for example, they can be very useful when encoding some CPU instructions into machine code, but guess what? The
compiler will think you've made some kind of mistake, like that you forgot to break from those cases, and it'll warn you about it. I like to clean all compiler warnings (and some
linter warnings, if they're not totally brain-dead), so I just don't use them. I know, sounds stupid, but there's usually some other way to do it, and to get the same solution.
linter warnings, if they're not totally brain-dead), so I just don't use them. I know, sounds stupid, but there's usually some other way to do it, to get the same solution.
Since we have several methods for printing text, they use standard output (terminal), file descriptor and a string respectively, we could implement them in separate functions, use
function pointers or simply copy+paste bunch of code into lot of functions, and form a "function family". I'm using the simplest solution here, we have one general function, and
explicitly declaring where I want my data to be printed. Simple as that. Lets call it 'printing_crossroad' for fun.
function pointers or simply copy+paste bunch of code into lot of functions, and form a "function family". Lets do something very simple and straight forward. We'll end up with
repeated code, but sometimes the simplicity can benefit us more than some smart solutions that are harder to understand. I'll explain as we progress...
*/
static void printing_crossroad (char * text, int size, int file, char * string) { // We pass a lot of arguments here, it makes things simpler below.
if (file > 0) { // We don't want to print to standard input, which is file descriptor of value 0.
file_write (file, text, size); // And we simply "output" our text of certain size to wanted location.
} else if (string != NULL) {
string_copy_limit (string, text, size);
} else {
out (text, size);
}
}
static void to_output (char * data, int size, int file, char * string) { (void) file; (void) string; out ( data, size); }
static void to_file (char * data, int size, int file, char * string) { (void) string; file_write (file, data, size); }
static void to_string (char * data, int size, int file, char * string) { (void) file; string_concatenate_limit (string, data, size); }
/*
Lets break down what's going on here, since it might be confusing for beginners. We've defined 3 internal functions, that'll only be used in this file and no other. They look
similar, and they ignore some of their arguments by casting them to 'void', that's how you silence the compiler warnings about unused function agruments. But why are we passing
those arguments if we won't use them? Because we can safely use one function pointer to any of those 3 functions.
Internal variable 'printing' is a function pointer to one of those 3 functions, and the default value for it is the memory address of function 'to_output'. So, if we just use it,
by default it'll print to standard output. If we call functions below, they'll change the value of 'printing' to coresponding function memory address. I chose to use concatenation
for 'print_string', but if you wanted, you could use something else, or just reinitialize it to null string, and think about memory before using it. Unlike 'printf' function from
<stdio.h> header file, my print functions don't allocate memory or make buffers.
*/
static void (* printing) (char * data, int size, int file, char * string) = to_output;
static void print_colour (char colour_id, int file, char * string) {
switch (colour_id) { // We use "special" character '/' to use terminal colours.
case '/': printing_crossroad ("/", 1, file, string); break; // If we have literally typed "//" in our 'format' string, it'll just output "/".
case '0': printing_crossroad ("\033[1;30m", 7, file, string); break; // Notice that we couldn't use function 'terminal_colour', it only uses standard output.
case '1': printing_crossroad ("\033[1;31m", 7, file, string); break; // Since we want to support file descriptors and strings, we use 'printing_crossroad'.
case '2': printing_crossroad ("\033[1;32m", 7, file, string); break;
case '3': printing_crossroad ("\033[1;33m", 7, file, string); break;
case '4': printing_crossroad ("\033[1;34m", 7, file, string); break;
case '5': printing_crossroad ("\033[1;35m", 7, file, string); break;
case '6': printing_crossroad ("\033[1;36m", 7, file, string); break;
case '7': printing_crossroad ("\033[1;37m", 7, file, string); break;
case '-': printing_crossroad ("\033[0m", 4, file, string); break;
default: printing_crossroad ("?", 1, file, string); break; // Now, if we provided some other character after "/", I like to make an intentional mismatch.
} // It's not such a big mistake to abort the program, since it's obvious that results are bad.
switch (colour_id) { // We use "special" character '/' to use terminal colours.
case '/': (* printing) ("/", 1, file, string); break; // If we have literally typed "//" in our 'format' string, it'll just output "/".
case '0': (* printing) ("\033[1;30m", 7, file, string); break; // Notice that we couldn't use function 'terminal_colour', it only uses standard output.
case '1': (* printing) ("\033[1;31m", 7, file, string); break; // Since we want to support file descriptors and strings, we use 'printing'.
case '2': (* printing) ("\033[1;32m", 7, file, string); break; // Also, we're dereferencing 'printing' pointer, which essentially calls one of the 3 functions.
case '3': (* printing) ("\033[1;33m", 7, file, string); break;
case '4': (* printing) ("\033[1;34m", 7, file, string); break;
case '5': (* printing) ("\033[1;35m", 7, file, string); break;
case '6': (* printing) ("\033[1;36m", 7, file, string); break;
case '7': (* printing) ("\033[1;37m", 7, file, string); break;
case '-': (* printing) ("\033[0m", 4, file, string); break;
default: (* printing) ("?", 1, file, string); break; // Now, if we provided some other character after "/", I like to make an intentional mismatch.
} // It's not such a big mistake to abort the program, since it's obvious that results are bad.
}
static void print_format (char format_id, va_list argument_list, int file, char * string) {
switch (format_id) { // We use character '%' this time, same as 'printf' function does, lets see...
static void print_format (char format_id, va_list list, int file, char * string) {
switch (format_id) { // We use character '%' this time, same as 'printf' function does, lets see...
case '%': {
printing_crossroad ("%", 1, file, string);
(* printing) ("%", 1, file, string);
} break;
case 'i': {
int integer; // Leave these local variables uninitialized (don't assign a value to them).
char * format;
int length;
integer = va_arg (argument_list, int); // Macro 'va_arg' will move argument from the list into our local variable, with the provided type.
length = string_length (format = number_to_string (integer));
printing_crossroad (format, length, file, string); // You might get the feeling that this isn't type safe, and you'd be totally right.
int integer; // Leave these local variables uninitialized (don't assign a value to them).
char * format; // We'll use format here and below in order to shorten the length of lines.
integer = va_arg (list, int); // Macro 'va_arg' will pop an argument from the list, with the provided type.
format = number_to_string (integer);
(* printing) (format, string_length (format), file, string); // You might get the feeling that this isn't type safe, and you're totally right.
} break;
case 'f': {
double ieee754; // Because we used curly braces, we can declare local variables in these blocks of code.
ieee754 = va_arg (argument_list, double); // I intentionally call this IEEE754 because I hate to use 'float' and 'double'.
double ieee754; // Because we used curly braces, we can declare local variables in these blocks of code.
char * format;
// And we're printing to terminal our (rounded) number.
format = number_to_string ((int) ieee754);
printing_crossroad (format, string_length (format), file, string);
ieee754 = va_arg (list, double); // I intentionally call this IEEE754 because I hate to use 'float' and 'double'.
format = number_to_string ((int) ieee754); // And we're printing to terminal our (rounded) number.
(* printing) (format, string_length (format), file, string);
} break;
case 's': {
char * literal; // Really simple stuff, but needs some time getting used to it, we're writing in an old language.
literal = va_arg (argument_list, char *); // In my opinion, this should be the part of the C language itself, not some macro black magic.
// You can write a bunch of variadic functions yourself if you want, to easy your workflow in some cases.
printing_crossroad (literal, string_length (literal), file, string);
char * format; // Really simple stuff, but needs some time getting used to it.
format = va_arg (list, char *); // In my opinion, this should be the part of the C language itself, but oh well...
(* printing) (format, string_length (format), file, string); // This could be written even shorter, but it'd require more mental-overhead.
} break;
default: {
printing_crossroad ("?", 1, file, string); // Again, I think it's best to print a bad thing, see it and fix it, then to abort the program in this case...
(* printing) ("?", 1, file, string); // Lets not abort the program in this case...
} break;
}
// This entire switch statement can be written more shortly (in terms of lines of code) like this:
// switch (format_id) {
// case '%': { out ("%", 1); } break;
// case 'i': { int integer; integer = va_arg (argument_list, int); echo (number_to_string (integer)); } break;
// case 'f': { double ieee754; ieee754 = va_arg (argument_list, double); echo (number_to_string ((int) ieee754)); } break;
// case 's': { char * string; string = va_arg (argument_list, char *); echo (string); } break;
// default: { out ("?", 1); } break;
// case '%': { ... } break;
// case 'i': { ... } break;
// case 'f': { ... } break;
// case 's': { ... } break;
// default: { ... } break;
// }
// Choose your own preference with switch statement (and any other one), and stay consistent with how you format it.
// Some people prefer more shorter lines of code, others prefer less longer lines of code (like I do).
@ -138,42 +143,51 @@ print ("My integer is %i.\n", 404); // Prints "My integer is 404." with new
print ("Heyo %s %s", "world", "#6!\n"); // Prints "Heyo world #6!" with new line.
print ("/1Cyaa world!/-\n"); // Prints red "Cyaa world!" with new line.
@
Now, since we're dealing with unsafe macros, no type-checking, and in general code that I don't like to write, we have that one general printing function, which splits into 3
functions, for standard output, file descriptor and string. Our general function is 'print_select', and it'll handle colouring and formating special characters for us, calling
other functions we defined previously, 'print_colour' and 'print_format'. Since those 2 functions are used only once, we could just place (inline) them where they're called, but
that way we'd end up with very indented code, which I'm not a fan of. I don't follow any indentation rules, I just don't indent a lot.
*/
void print (char * format, ...) {
va_list argument_list; // This is mandatory local variable if we'll use variadic arguments in this function.
static void print_select (char * format, va_list list, int file, char * string) {
int offset, length;
length = string_length (format);
va_start (argument_list, format); // Every variadic function needs to start with this (before using 'va_arg').
for (offset = 0; offset != length; ++offset) { // We start iterating through our 'format' string, and looking for special characters below.
if (format [offset] == '/') { // Colouring special character is '/', and colours are specified from '0'...'7', and '-' to cancel them.
for (offset = 0; offset != length; ++offset) { // We start iterating through our 'format' string, and looking for special characters below.
if (format [offset] == '/') { // Colouring special character is '/', and colours are specified from '0'...'7', and '-' to cancel them.
++offset;
print_colour (format [offset], 0, NULL); // We're calling function that will colour our printed text, this one is simple.
} else if (format [offset] == '%') { // And formatting special character is '%', it'll use variadic arguments!
print_colour (format [offset], file, string); // We're calling function that will colour our printed text, this one is simple.
} else if (format [offset] == '%') { // And formatting special character is '%', it'll use variadic arguments!
++offset;
print_format (format [offset], argument_list, 0, NULL); // We're calling function that will format our agruments, so we pass variable 'argument_list'.
print_format (format [offset], list, file, string); // We're calling function that will format our agruments, so we pass variable 'list'.
} else {
out (& format [offset], 1); // Not a special character? Okay, we'll just print them one by one.
(* printing) (& format [offset], 1, file, string); // Not a special character? Okay, we'll just print them one by one.
}
}
va_end (argument_list); // And every variadic function needs to end with this. Puns intended.
}
void file_print (int file, char * format, ...) {
(void) file;
(void) format;
return;
}
/*
Now, lets break down what those last 3 very similar functions do. Everything I say about 'print', is same for the other two, but with exception to their output.
void string_print (char * string, char * format, ...) {
(void) string;
(void) format;
return;
@C
void print (char * format, ...) {
va_list list; // Every variadic function needs to have this list, and sometimes you need to pass it to other functions.
printing = to_output; // We're selecting different output method, notice the difference between 3 functions below, it's minimal, and this is part of it.
va_start (list, format); // Every variadic function needs to start with this macro (or function depending on the implementation in <stdarg.h>).
print_select (format, list, 0, NULL); // And we just call our general super smart function to handle everything for us.
va_end (list); // Every variadic function needs to end with this macro... Pun intended.
}
@
*/
void print ( char * format, ...) { va_list list; printing = to_output; va_start (list, format); print_select (format, list, 0, NULL); va_end (list); }
void file_print (int file, char * format, ...) { va_list list; printing = to_file; va_start (list, format); print_select (format, list, file, NULL); va_end (list); }
void string_print (char * string, char * format, ...) { va_list list; printing = to_string; va_start (list, format); print_select (format, list, 0, string); va_end (list); }
#endif

View File

@ -23,4 +23,6 @@ splint -weak -warnposix -retvalother -syntax -type chapters/chapter_3.h
splint -weak -warnposix -retvalother -syntax -type chapters/chapter_3.c
splint -weak -warnposix -retvalother -syntax -type xhartae.c
valgrind --show-leak-kinds=all --leak-check=full ./xhartae
exit