/**
* @file columns.c
* @group columns
* @{
*/
/*
* Columns Copyright (C) 1992-2015 by Bruce Korb - all rights reserved
* Columns is free software.
* This file is part of AutoGen.
*
* AutoGen Copyright (C) 1992-2014 by Bruce Korb - all rights reserved
*
* AutoGen 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 3 of the License, or
* (at your option) any later version.
*
* AutoGen 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, see <http://www.gnu.org/licenses/>.
*/
#ifndef NUL
# define NUL '\0'
#endif
struct print_list {
char ** papz;
int index;
};
typedef struct print_list tPrintList, *tpPrintList;
int maxEntryWidth = 0;
int fillColumnCt = 0;
#define LINE_LIMIT 4096
char zLine[ LINE_LIMIT ];
char zFmtLine[ LINE_LIMIT ];
char ** papzLines = (char **)NULL;
char const * pzLinePfx = "";
char const * pzFirstPfx = NULL;
size_t allocCt = 0;
size_t usedCt = 0;
size_t columnCt = 0;
size_t columnSz = 0;
size_t indentSize = 0;
/* = = = START-STATIC-FORWARD = = = */
static void
fserr_die(char const * fmt, ...);
static inline void *
malloc_or_die(size_t sz);
static char const *
construct_first_pfx(char const * f_indent);
static uint32_t
pad_indentation(char const * pzIndentArg, char const ** pfx);
static void
readLines(void);
static void
writeColumns(void);
static void
trim_last_separation(void);
static void
writeRows(void);
static int
emitWord(char const * word, size_t len, int col);
static void
writeFill(void);
static int
compProc(const void * p1, const void * p2);
/* = = = END-STATIC-FORWARD = = = */
int
main(int argc, char ** argv)
{
(void)optionProcess( &columnsOptions, argc, argv );
if (HAVE_OPT( INDENT )) {
indentSize = pad_indentation( OPT_ARG(INDENT), &pzLinePfx);
OPT_VALUE_WIDTH -= (long)indentSize;
pzFirstPfx = HAVE_OPT(FIRST_INDENT)
? construct_first_pfx( OPT_ARG(FIRST_INDENT))
: pzLinePfx;
}
if (HAVE_OPT( LINE_SEPARATION ))
OPT_VALUE_WIDTH -= (long int)strlen( OPT_ARG( LINE_SEPARATION));
if (HAVE_OPT( COL_WIDTH ))
columnSz = (size_t)OPT_VALUE_COL_WIDTH;
if (HAVE_OPT( COLUMNS ))
columnCt = (size_t)OPT_VALUE_COLUMNS;
if (OPT_VALUE_WIDTH <= 16)
OPT_VALUE_WIDTH = 16;
readLines();
if (HAVE_OPT( SORT ))
qsort(VOIDP(papzLines), usedCt, sizeof(char *), &compProc);
if (HAVE_OPT( BY_COLUMNS ))
writeColumns();
else if (HAVE_OPT(FILL))
writeFill();
else
writeRows();
return EXIT_SUCCESS;
}
static void
fserr_die(char const * fmt, ...)
{
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "%s fserr %d (%s): ", columnsOptions.pzProgName,
errno, strerror(errno));
vfprintf(stderr, fmt, ap);
va_end(ap);
exit(EXIT_FAILURE);
}
static inline void *
malloc_or_die(size_t sz)
{
void * res = malloc(sz);
if (res == NULL) {
errno = ENOMEM;
fserr_die("could not allocate %d bytes", sz);
}
return res;
}
static char const *
construct_first_pfx(char const * f_indent)
{
static char const pad_fmt[] = "%%-%ds";
// 24 > log10(0xFFFFFFFFFFFFFFFFULL)
char pfx_buf[24 + sizeof(pad_fmt)];
size_t firstSize = pad_indentation(f_indent, &pzFirstPfx);
size_t len;
char * res;
/*
* If this first line exceeds the indentation size, then we
* need to append a newline and any indentation.
*/
if (firstSize > indentSize) {
size_t sep_len = HAVE_OPT(LINE_SEPARATION)
? strlen( OPT_ARG(LINE_SEPARATION)) : 0;
len = firstSize + sep_len + indentSize + 3;
sprintf(pfx_buf, pad_fmt, (int)firstSize);
} else {
len = indentSize + 3;
sprintf(pfx_buf, pad_fmt, (int)indentSize);
}
res = malloc_or_die(len);
snprintf(res, len, pfx_buf, pzFirstPfx);
pzFirstPfx = res;
if (firstSize > indentSize) {
char * p = res + firstSize;
if (HAVE_OPT( LINE_SEPARATION )) {
len = strlen(OPT_ARG(LINE_SEPARATION));
memcpy(p, OPT_ARG(LINE_SEPARATION), len);
p += len;
}
sprintf(p, "\n%s", pzLinePfx);
}
return res;
}
static uint32_t
pad_indentation(char const * pzIndentArg, char const ** pfx)
{
char * pz;
unsigned int cct;
errno = 0;
cct = (unsigned int)strtoul(pzIndentArg, &pz, 0);
/*
* IF the indent argument is a number
*/
if ((*pz == NUL) && (errno == 0) && (cct < OPT_VALUE_WIDTH)) {
char * p;
/*
* Allocate a string to hold the line prefix
*/
*pfx = p = malloc_or_die( (size_t)cct + 1 );
/*
* Set it to a NUL terminated string of spaces
*/
if (cct > 0)
memset(p, ' ', (size_t)cct);
p[cct] = NUL;
} else {
/*
* Otherwise, set the line prefix to whatever the string is.
* It will not be the empty string because that is handled
* as an indent count of zero and is ignored.
*/
char const * p = pzIndentArg;
*pfx = pzIndentArg;
cct = 0;
for (;;) {
/*
* Compute the length of the last line of the prefix.
*/
switch (*p++) {
case NUL:
goto colsCounted;
case '\t':
cct += (unsigned)OPT_VALUE_TAB_WIDTH;
cct -= (unsigned)(cct % OPT_VALUE_TAB_WIDTH);
break;
case '\n':
case '\f':
case '\r':
cct = 0;
break;
default:
cct++;
break;
case '\a':
break;
}
} colsCounted:;
}
return cct;
}
static void
readLines(void)
{
int sepLen = HAVE_OPT(SEPARATION)
? (int)strlen(OPT_ARG(SEPARATION)) : 0;
/*
* Read the input text, stripping trailing white space
*/
for (;;) {
char * pzL;
char * pzText = fgets(zLine, (int)sizeof(zLine), stdin);
size_t len;
if (pzText == NULL)
break;
/*
* Trim off trailing white space.
*/
len = strlen( pzText );
pzText += len;
while (isspace(pzText[-1])) {
if (--pzText == zLine) {
if (HAVE_OPT(FILL))
break;
goto next_line;
}
len--;
}
*pzText = NUL;
/*
* IF the input lines are to be reformatted,
* THEN the length is the result of the sprintf
* Else, compute the length.
*/
if (HAVE_OPT(FORMAT)) {
pzText = zFmtLine;
len = (size_t)snprintf(
zFmtLine, sizeof(zFmtLine), OPT_ARG(FORMAT), zLine);
} else {
pzText = zLine;
}
/*
* Allocate a string and space in the pointer array.
*/
len += (size_t)sepLen + 1;
pzL = (char *)malloc_or_die( len );
if (++usedCt > allocCt) {
allocCt += 128;
papzLines = (char **)realloc(VOIDP(papzLines),
sizeof(char *) * allocCt );
}
papzLines[ usedCt-1 ] = pzL;
/*
* Copy the text and append the separation character
*/
strcpy( pzL, pzText );
/*
* Initially, all strings have the separator,
* the entries may get reordered.
*/
if (sepLen > 0)
strcat(pzL, OPT_ARG(SEPARATION));
if ((int)len > maxEntryWidth)
maxEntryWidth = (int)len;
next_line:;
}
if (maxEntryWidth == 0) {
fputs( "columns warning: no input text was read\n", stderr );
exit( EXIT_SUCCESS );
}
/*
* Set the line width to the amount of space we have to play with.
*/
if ((OPT_VALUE_WIDTH < maxEntryWidth) && (! HAVE_OPT(FILL)))
OPT_VALUE_WIDTH = maxEntryWidth;
/*
* If we do not have a column size set,
* then figure out what it must be.
*/
if (columnSz == 0) {
/*
* IF the column count has not been set,
* THEN compute it.
*/
if (columnCt == 0)
columnCt = (size_t)(OPT_VALUE_WIDTH / maxEntryWidth);
/*
* IF there are to be multiple columns, ...
*/
if (columnCt > 1) {
size_t spreadwidth = (size_t)(OPT_VALUE_WIDTH - maxEntryWidth);
int sz = (int)(spreadwidth / (size_t)(columnCt-1));
/*
* Either there is room for added space,
* or we must (possibly) reduce the number of columns
*/
if (sz >= maxEntryWidth)
columnSz = (size_t)sz;
else {
columnCt = ((size_t)spreadwidth / (size_t)maxEntryWidth) + 1;
if (columnCt > 1)
columnSz = (size_t)spreadwidth / (size_t)(columnCt - 1);
else columnSz = (size_t)OPT_VALUE_WIDTH;
}
}
}
/*
* Otherwise, the column size has been set. Ensure it is sane.
*/
else {
bool compute_val = (columnCt == 0);
/*
* Increase the column size to the width of the widest entry
*/
if (maxEntryWidth > (int)columnSz)
columnSz = (size_t)maxEntryWidth;
/*
* IF we have not been provided a column count
* *OR* we are set to overfill the output line,
* THEN compute the number of columns.
*/
if (! compute_val) {
long width = (long)maxEntryWidth + (long)(columnSz * (columnCt-1));
compute_val = width > (unsigned)OPT_VALUE_WIDTH;
}
if (compute_val)
columnCt = (size_t)(
(((long)OPT_VALUE_WIDTH - (long)maxEntryWidth) /
(long)columnSz) + 1);
}
/*
* Ensure that any "spread" we added to the column size
* does not exceed the parameterized limit.
*/
if ( HAVE_OPT( SPREAD )
&& ((maxEntryWidth + OPT_VALUE_SPREAD - 1) < (int)columnSz))
columnSz = (size_t)(maxEntryWidth + OPT_VALUE_SPREAD - 1);
}
static void
writeColumns(void)
{
char zFmt[ 12 ];
int colCt, rowCt, col, row;
tpPrintList pPL;
colCt = (int)columnCt;
snprintf(zFmt, sizeof(zFmt), "%%-%ds", (int)columnSz);
if (colCt == 1) {
writeRows();
return;
}
pPL = (tpPrintList)malloc_or_die((unsigned long)colCt * sizeof(tPrintList));
/*
* This "loop" is normally executed half way through and exited.
* IF, however, we would produce an empty final column,
* we will reduce our column count and line width and then
* try the top-of-column pointer computation again.
*
* The problem solved here is that sometimes, when the
* number of entries in a row is greater than the number of rows,
* it is possible that all the entries that would have been
* in the last column are, instead, essentially put on the
* last row. That will leave the final column empty.
* We could regroup at that point and spread the columns some more,
* but, if done, is an exercise for later.
*/
for (;;) {
int rem;
int fsz;
rowCt = ((int)usedCt/colCt) + (((int)usedCt % colCt) ? 1 : 0);
/*
* FOR each column, compute the address of the pointer to
* the string at the top of the column, and the index of
* that entry.
*/
for (col = 0; col < colCt ; col++) {
pPL[col].papz = papzLines + (col * rowCt);
pPL[col].index = col * rowCt;
}
/*
* IF the final column is not empty,
* THEN break out and start printing.
*/
if (pPL[colCt-1].index < (int)usedCt)
break;
/*
* The last column is blank, so we reduce our column count,
* even if the user specified a count!!
*/
colCt--;
rem = (int)OPT_VALUE_WIDTH - (int)(colCt * maxEntryWidth);
if ((rem == 0) || (colCt < 2) || (columnSz > 0))
fsz = maxEntryWidth;
else
fsz = maxEntryWidth + (rem / (colCt-1));
snprintf( zFmt, sizeof(zFmt), "%%-%ds", fsz );
}
/*
* Now, actually print each row...
*/
for ( row = 0 ;; ) {
char * pzL;
char * pzE;
if (pzLinePfx != NULL)
fputs( pzLinePfx, stdout );
/*
* Increment the index of the current entry in the last column.
* IF it goes beyond the end of the entries in use,
* THEN reduce our column count.
*/
if ((pPL[colCt-1].index)++ >= (int)usedCt)
colCt--;
/*
* Get the address of the string in the last column.
*/
pzE = *(pPL[colCt-1].papz++);
col = 0;
/*
* FOR every column except the last,
* print the entry with the width format
*
* No need to worry about referring to a non-existent entry. Only
* the last column might have that problem, and we addressed it above
* where the column count got decremented.
*/
while (++col < colCt) {
pzL = *(pPL[col-1].papz++);
fprintf( stdout, zFmt, pzL );
free( VOIDP(pzL) );
}
/*
* See if we are on the last row. If so, then this is the last entry.
* Strip any separation characters, emit the entry and break out.
*/
if (++row == rowCt) {
/*
* IF we have a separator,
* THEN remove it from the last entry.
*/
if (HAVE_OPT( SEPARATION )) {
char * pz = pzE + strlen( pzE )
- strlen( OPT_ARG(SEPARATION));
*pz = NUL;
}
fputs(pzE, stdout);
if (HAVE_OPT(ENDING))
fputs(OPT_ARG(ENDING), stdout);
putc( '\n', stdout );
break;
}
/*
* Print the last entry on the line, without the width format.
* If we have line separation (which does not apply to the last
* line), then emit those characters, too.
*/
fputs( pzE, stdout );
if (HAVE_OPT( LINE_SEPARATION ))
fputs( OPT_ARG( LINE_SEPARATION ), stdout );
putc( '\n', stdout );
free( VOIDP(pzE) );
}
free(pPL);
}
static void
trim_last_separation(void)
{
char * pz = papzLines[ usedCt-1 ];
pz += strlen(pz) - strlen( OPT_ARG(SEPARATION));
*pz = NUL;
}
static void
writeRows(void)
{
char zFmt[32];
int colCt;
colCt = (int)columnCt;
snprintf(zFmt, sizeof(zFmt), "%%-%ds", (int)columnSz);
if (HAVE_OPT( SEPARATION ))
trim_last_separation();
if (pzFirstPfx != NULL) {
fputs( pzFirstPfx, stdout );
pzFirstPfx = pzLinePfx;
}
{
char ** ppzLL = papzLines;
size_t left = usedCt;
int lnNo = 0;
/*
* FOR every entry we are to emit, ...
*/
for (;;) {
char * pzL = *ppzLL++;
/*
* IF this is the last entry,
* THEN emit it and a new line and break out
*/
if (--left <= 0) {
fputs(pzL, stdout);
if (HAVE_OPT(ENDING))
fputs(OPT_ARG(ENDING), stdout);
putc('\n', stdout);
free(VOIDP(pzL));
break;
}
/*
* IF the count of entries on this line is still less
* than the number of columns,
* THEN emit the padded entry
* ELSE ...
*/
if (++lnNo < colCt)
fprintf( stdout, zFmt, pzL );
else {
lnNo = 0;
/*
* Last entry on the line. Emit the string without padding.
* IF we have a line separation string, emit that too.
*/
fputs( pzL, stdout );
if (HAVE_OPT( LINE_SEPARATION ))
fputs( OPT_ARG( LINE_SEPARATION ), stdout );
putc( '\n', stdout );
/*
* Start the next line with any required indentation
*/
if (pzFirstPfx != NULL) {
fputs( pzFirstPfx, stdout );
pzFirstPfx = pzLinePfx;
}
}
free( VOIDP(pzL) );
}
}
}
static int
emitWord(char const * word, size_t len, int col)
{
static int ended_with_period = 0;
if (col > 0) {
if ((int)len >= (OPT_VALUE_WIDTH - col)) {
putc('\n', stdout);
if (pzLinePfx != NULL)
fputs(pzLinePfx, stdout);
col = 0;
} else {
if (ended_with_period) {
putc(' ', stdout);
col++;
}
putc(' ', stdout);
col++;
}
}
fwrite(word, len, 1, stdout);
col += (int)len;
ended_with_period = (word[len - 1] == '.');
return col;
}
/*
* writeFill -- fill the output. Pack together as much as will fit
* on each line.
*/
static void
writeFill(void)
{
char ** ppzLL = papzLines;
size_t left = usedCt;
int colNo = 0;
if (HAVE_OPT( SEPARATION ))
trim_last_separation();
if (pzFirstPfx != NULL)
fputs(pzFirstPfx, stdout);
/*
* FOR every entry we are to emit, ...
*/
while (left-- > 0) {
char * pzL = *ppzLL;
while (isspace(*pzL)) pzL++;
/*
* Blank lines are magical and trigger a blank line in output.
*/
if (*pzL == NUL) {
if (! HAVE_OPT(SORT)) {
if (colNo > 0) /* guard against multiple blank lines */
putc('\n', stdout);
putc('\n', stdout);
colNo = -2;
}
free(*(ppzLL++));
continue;
}
/*
* We are going to emit some output. Make sure we're indented.
*/
if (colNo < 0) {
if (pzLinePfx != NULL)
fputs(pzLinePfx, stdout);
colNo = 0;
}
do {
size_t tknlen;
for (tknlen = 0; pzL[tknlen] != NUL; tknlen++)
if (isspace(pzL[tknlen]))
break;
colNo = emitWord(pzL, tknlen, colNo);
pzL += tknlen;
while (isspace(*pzL)) pzL++;
} while (*pzL != NUL);
free(*(ppzLL++));
}
if (HAVE_OPT(ENDING) && (left == 0))
fputs(OPT_ARG(ENDING), stdout);
putc('\n', stdout);
}
/*
* Line comparison procedure
*/
static int
compProc(const void * p1, const void * p2)
{
char const * pz1 = *(char * const *)p1;
char const * pz2 = *(char * const *)p2;
return strcmp(pz1, pz2);
}
/** @}
*
* Local Variables:
* mode: C
* c-file-style: "stroustrup"
* indent-tabs-mode: nil
* End:
* end of columns/columns.c */