Zoom out and consider...
You've shown good appreciation of C's ability to concatenate literal strings (the multi-line "help text").
Your method is perfectly valid.
An alternative could be to store the address of that text into a pointer variable, eliminate the function that encloses it, then, change the two invocations of that ex-function to
printf( myHelpVerbiage );
,
or
puts( myHelpVerbiage );
to neutralise any embedded single %
characters that may be in the verbiage. This is one of C's nefarious 'traps for the careless' that is easily fallen-into (even by pro's).
Going BIG, a naïve or malicious user running your executable with command line redirection to a file or a pipe would, if using for instance "-x"
on the command line, feed that text to the pipe or the file. "Help text" should go to stderr
to be read by a human, or whatever logging is done of error/misfire invocation. Read up on fputs()
, too, and consider its use here.
Avoid too quickly presuming you've got all the usage scenarios covered...
A comment from @DavidConrad (thank you David!) highlights a subtle distinction the code should accommodate. There's "help text" and there's "usage text".
The former should go to stdout
: fputs( myVerbiage, stdout );
User forgot usage syntax.
The latter should go to stderr
: fputs( myVerbiage, stderr );
A CLI error detected.
There are myriad ways of dealing with problematic real world inputs and data. Endless pursuit; plug-up one crack only to notice the next one, ad infinitum. (Great for job security if you play your cards right!)
And, YES! Protecting the executable from "user abuse" is one of the most difficult aspects of writing code! Foolproof?? NEVER underestimate the ingenuity of a fool! If there's a way to make your program go off the rails, one day a 'fool' will find how to trigger that trainwreck. All you can do is do YOUR best to make THEM work for it.
Missing
Usage option examples do not include an example when program is used as a Unix filter, only "command line parameter" examples shown. Easy to fix.
atoi() vs strtol()
In the gentler, more trusting days of C's inception, atoi()
was sufficient. There were no (or fewer) bad actors around. In 2025, code needs to be more robust. Read up on strtol()
and appreciate how it can be used to not only work with different bases, but its 3rd parameter can be used to detect the difference between "123" and "123FrogsAndToads". This WILL be important as you write defensive code for more intricate problems.
No fault to you, but atoi()
is a sure sign of a beginner/amateur coder. It remains in C's standard library for compilers to be backward compatible for code written in a time when smoking in theatres was allowed.
Renovating legacy code (replacing atoi()
with strtol()
) can be like playing "Whack a Mole". Yes, one thing has been 'improved', but a previously 'dormant' bug may be awoken by the altered machine code. Related concept: trying to replace one Jenga piece late in the game. Sometimes it's wiser to leave well enough alone. Otherwise, a 10-minute fix may become a week-long misery.
Flow
Examine the if/else
cascade in shiftChar()
. Think about whether-or-not else
is necessary in the else if
and else
lines. Just to say, return;
is pretty absolute about what happens next. Code simplification while maintaining correct operation is highly desirable.
Try to form a practice of walking away for a few minutes, then returning to see if you can improve the 'flow' of the logic so it reads like a poem - easy to swallow without having to chew too much...
malloc() & realloc()
The executable likely has at least 1Mb of scratchpad "stack" available.
Consider reading into a fixed-size "stack string buffer", measuring what's been read, then starting or growing the dynamic buffer being assembled as required, with the tiny expense of having to copy (append) a bunch of bytes from stack buffer to dynamic memory buffer.
Left as an exercise is to really examine the doco for realloc()
and eliminate the anticipatory malloc()
you've written.
And, most important, always type free()
whenever you type any of the names of the alloc-brothers. Never forget that what's borrowed must be returned. Some programs may be expected to run continuously, borrowing and releasing limited system resources so as not to deplete them to zero.
Sometimes you will be in the zone, conceiving and typing code, keystrokes sounding like rain on a rooftop. If you habitually type free();
on the line after malloc()
or a sibling, it's unlikely the compiler will let you forget to move that operation to a suitable place. Your 'zone' is not disturbed, and you can continue to pour out all that's racing through your mind until you're exhausted. When the pace has slowed and tentative compiling starts, free()
won't have been accidentally forgotten. Many IDE editors insert {}
when {
alone is typed. Think of malloc()
and free()
similarly. "You can't have one without the other".
Kitchen sinks are for kitchens, not code
I've suggested you eliminate the one "worker function" in this code. In its place, I suggest you see the body of main()
as three distinct phases: set-up, do-stuff, and report (plus the missing tear-down of the absent free()
). Each of these 'phases' really wants to be isolated into its own much simpler function. This is "refactoring" to follow The KISS Principle. Computers don't care, but human readers 'grok' things better as small chunks, one at a time. Further, it's easier to write/modify capsules of code and diagnose a bug when its range is restricted.
Don't throw "even-the-kitchen-sink" into an overly-long function. Break things down as much as you see fit. (One can overdo this; experience will establish your own comfort zone of factoring code into fragments.)
Reversable?
If a sender uses 5
to encipher a message, wouldn't it be easier for the receiver to use 5
(instead of 21
) to decipher the ciphertext?
Consider the very minor tweak needed to provide this convenience.
Some are not so great at summing, or visualising inverted relationships.
A first-time user trying to decipher may wonder whether they can use -d
OR skip it and enter -c -5
... I hope you see what I mean...
Maybe consider simplify the UI to -e 5
and -d 5
only. One means "encipher", the other "decipher", and the required next parameter is always a positive magnitude...
Keep or get rid of "defaults to 13". Ask your friends what they'd prefer if they were to be users of this program. I'd prefer no defaults, myself. It's no big ask of any user to be explicit to use your program. (Becomes a non-optional parameter to the program! If default of 0
has not been changed, then terminate telling user -e #
or -d #
is a required parameter.)
Conclusion
All in all, very good code for a neophyte. Some hiccups, but nothing too major. A minor niggle over the final two for()
's and braces (one has, one has not). A minor inconsistency that could be improved upon.
Otherwise... Welcome to the club! Hope you come to love C as much as so many have before you.
Ya know...
That final printing for()
loop could be replaced harnessing the power of printf()
.
printf( "%.*s\n", length, rot );
If you read, and read again, and again, the doco for printf()
you'll discover it has quite astounding capabilities! The format string used here informs printf()
to only try to output length
characters of the char []
at address rot
. No '\0'
to make a C string is required in this case.
This is but one "power user" technique you'll be master of by keeping a copy of the doco for the C Standard Library at your bedside. Read a bit before turning off the light every night. Most of the basic functionalities you'll ever need are already available. With its libraries, C is not as primitive as its reputation has people thinking.
And, now that we've save a few lines of code, we could consider how to handle being asked to encipher nothing (an empty file or ""
as a parameter string). Should we report the number of characters processed to stderr
, or may terminate with a FAILED message and error code??
Decisions, decisions!
To consider: If the user starts your program for it to encode from the standard input, your program will sit listening for any/all text provided until the user types whatever EOF key combo is appropriate for their OS. You may get a call at 2AM from a user complaining "your program just goes off into an endless loop." Perhaps, if stdin
is the source of the data, your program could print a prompt to stderr
that contains the key combo to signal EOF. Maybe even, "You may commence entry now. >>"
Maybe add this extra user advisory of EOF key combo to the help text that's displayed for "-h"
...
Overtime
With a few minor fix-ups or adjustments, there are no catastrophic problems with your code. Kudos.
However, thinking in a KISS frame of mind, there's a lot of complexity to this involving spooling-up its source data, 'batch' processing of the data, then spooling-out the results. This may have been the crux of your taking this approach.
Consider stripping out a lot of the guts to leave behind only the enciphering that deals with input one character at a time, writing each cipher-char to its output. Easy-peasy. This is how many Unix 'filters' are written; imposing no preset (or even dynamic) limits on the size of source data they can process. stdin
is buffered, as is (normally) stdout
. The application does not necessarily need to supply its own buffers to function.
KISSing is always good!