diff options
-rw-r--r-- | NEWS | 1 | ||||
-rw-r--r-- | doc/man/serd-pipe.1 | 26 | ||||
-rw-r--r-- | include/serd/error.h | 45 | ||||
-rw-r--r-- | include/serd/log.h | 209 | ||||
-rw-r--r-- | include/serd/serd.h | 2 | ||||
-rw-r--r-- | include/serd/world.h | 12 | ||||
-rw-r--r-- | meson.build | 7 | ||||
-rw-r--r-- | meson/suppressions/meson.build | 1 | ||||
-rw-r--r-- | src/.clang-tidy | 3 | ||||
-rw-r--r-- | src/log.c | 325 | ||||
-rw-r--r-- | src/log.h | 21 | ||||
-rw-r--r-- | src/reader.c | 7 | ||||
-rw-r--r-- | src/reader.h | 3 | ||||
-rw-r--r-- | src/serd_config.h | 13 | ||||
-rw-r--r-- | src/world.c | 66 | ||||
-rw-r--r-- | src/world.h | 26 | ||||
-rw-r--r-- | src/writer.c | 24 | ||||
-rw-r--r-- | test/meson.build | 29 | ||||
-rw-r--r-- | test/test_log.c | 121 | ||||
-rw-r--r-- | tools/console.c | 11 | ||||
-rw-r--r-- | tools/console.h | 5 | ||||
-rw-r--r-- | tools/serd-pipe.c | 14 |
22 files changed, 798 insertions, 173 deletions
@@ -2,6 +2,7 @@ serd (1.1.1) unstable; urgency=medium * Add SerdBuffer for mutable buffers to keep SerdChunk const-correct * Add SerdWorld for shared library state + * Add extensible logging API * Add support for parsing variables * Add support for writing terse output with minimal newlines * Add support for xsd:float and xsd:double literals diff --git a/doc/man/serd-pipe.1 b/doc/man/serd-pipe.1 index f5908c1d..ae3e6620 100644 --- a/doc/man/serd-pipe.1 +++ b/doc/man/serd-pipe.1 @@ -165,6 +165,18 @@ Variables can be written in SPARQL style, for example or .Dq $var . .El +.Sh ENVIRONMENT +Errors and warnings are printed in color by default if the output is a terminal. +This can be overridden with environment variables: +.Pp +.Bl -tag -compact -width 14n +.It Ev NO_COLOR +If present (regardless of value), color is disabled. +.It Ev CLICOLOR +If set to 0, color is disabled. +.It Ev CLICOLOR_FORCE +If set to anything other than 0, color is forced on. +.El .Sh EXIT STATUS .Nm exits with a status of 0, or non-zero if an error occurred. @@ -218,6 +230,20 @@ exits with a status of 0, or non-zero if an error occurred. .%T RDF 1.1 Turtle .Re .Lk https://www.w3.org/TR/turtle/ +.It +.Rs +.%A Jan Niklas Hasse +.%T CLICOLOR +.%D April 2015 +.Re +.Lk https://bixense.com/clicolors/ +.It +.Rs +.%A Joshua Stein +.%T NO_COLOR +.%D August 2017 +.Re +.Lk http://no-color.org/ .El .Sh AUTHORS .Nm diff --git a/include/serd/error.h b/include/serd/error.h deleted file mode 100644 index 7051bd4f..00000000 --- a/include/serd/error.h +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2011-2022 David Robillard <d@drobilla.net> -// SPDX-License-Identifier: ISC - -#ifndef SERD_ERROR_H -#define SERD_ERROR_H - -#include "serd/attributes.h" -#include "serd/caret.h" -#include "serd/status.h" -#include "zix/attributes.h" - -#include <stdarg.h> - -SERD_BEGIN_DECLS - -/** - @defgroup serd_error Error reporting - @ingroup serd_errors - @{ -*/ - -/// An error description -typedef struct { - SerdStatus status; ///< Error code - const SerdCaret* ZIX_NULLABLE caret; ///< File origin of error - const char* ZIX_NONNULL fmt; ///< Printf-style format string - va_list* ZIX_NONNULL args; ///< Arguments for fmt -} SerdError; - -/** - Callback function to log errors. - - @param handle Handle for user data. - @param error Error description. -*/ -typedef SerdStatus (*SerdLogFunc)(void* ZIX_NULLABLE handle, - const SerdError* ZIX_NONNULL error); - -/** - @} -*/ - -SERD_END_DECLS - -#endif // SERD_ERROR_H diff --git a/include/serd/log.h b/include/serd/log.h new file mode 100644 index 00000000..a9261131 --- /dev/null +++ b/include/serd/log.h @@ -0,0 +1,209 @@ +// Copyright 2011-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef SERD_LOG_H +#define SERD_LOG_H + +#include "serd/attributes.h" +#include "serd/caret.h" +#include "serd/status.h" +#include "serd/string_view.h" +#include "serd/world.h" +#include "zix/attributes.h" + +#include <stdarg.h> +#include <stddef.h> + +SERD_BEGIN_DECLS + +/** + @defgroup serd_logging Logging + @ingroup serd_errors + @{ +*/ + +/// Log entry level, compatible with syslog +typedef enum { + SERD_LOG_LEVEL_EMERGENCY, ///< Emergency, system is unusable + SERD_LOG_LEVEL_ALERT, ///< Action must be taken immediately + SERD_LOG_LEVEL_CRITICAL, ///< Critical condition + SERD_LOG_LEVEL_ERROR, ///< Error + SERD_LOG_LEVEL_WARNING, ///< Warning + SERD_LOG_LEVEL_NOTICE, ///< Normal but significant condition + SERD_LOG_LEVEL_INFO, ///< Informational message + SERD_LOG_LEVEL_DEBUG, ///< Debug message +} SerdLogLevel; + +/** + A structured log field. + + Fields are used to add metadata to log messages. Syslog-compatible keys + should be used where possible, otherwise, keys should be namespaced to + prevent clashes. + + Serd itself uses the following keys: + + - ERRNO - The `errno` of the original system error if any (decimal string) + - SERD_COL - The 1-based column number in the file (decimal string) + - SERD_FILE - The file which caused this message (string) + - SERD_LINE - The 1-based line number in the file (decimal string) + - SERD_CHECK - The check/warning/etc that triggered this message (string) +*/ +typedef struct { + const char* ZIX_NONNULL key; ///< Field name + const char* ZIX_NONNULL value; ///< Field value +} SerdLogField; + +/** + Function for handling log messages. + + By default, the log is printed to `stderr`, but this can be overridden to + instead send log messages to a user function of this type. + + @param handle Pointer to opaque user data. + @param level Log level. + @param n_fields Number of entries in `fields`. + @param fields An array of `n_fields` extra log fields. + @param message Log message. +*/ +typedef SerdStatus (*SerdLogFunc)(void* ZIX_NULLABLE handle, + SerdLogLevel level, + size_t n_fields, + const SerdLogField* ZIX_NULLABLE fields, + SerdStringView message); + +/// A #SerdLogFunc that does nothing (for suppressing log output) +SERD_CONST_API SerdStatus +serd_quiet_log_func(void* ZIX_NULLABLE handle, + SerdLogLevel level, + size_t n_fields, + const SerdLogField* ZIX_NULLABLE fields, + SerdStringView message); + +/** + Set a function to be called with log messages (typically errors). + + If no custom logging function is set, then messages are printed to stderr. + + @param world World that will send log entries to the given function. + + @param log_func Log function to call for every log message. Each call to + this function represents a complete log message with an implicit trailing + newline. + + @param handle Opaque handle that will be passed to every invocation of + `log_func`. +*/ +SERD_API void +serd_set_log_func(SerdWorld* ZIX_NONNULL world, + SerdLogFunc ZIX_NULLABLE log_func, + void* ZIX_NULLABLE handle); + +/** + Write a message to the log with a `va_list`. + + This is the fundamental and most powerful function for writing entries to + the log, the others are convenience wrappers that ultimately call this. + + This writes a single complete entry to the log, and so may not be used to + print parts of a line like a more general printf-like function. There + should be no trailing newline in `fmt`. Arguments following `fmt` should + correspond to conversion specifiers in the format string as in printf from + the standard C library. + + @param world World to log to. + @param level Log level. + @param n_fields Number of entries in `fields`. + @param fields An array of `n_fields` extra log fields. + @param fmt Format string. + @param args Arguments for `fmt`. + + @return A status code, which is always #SERD_SUCCESS with the default log + function. If a custom log function is set with serd_set_log_func() and it + returns an error, then that error is returned here. +*/ +SERD_LOG_FUNC(5, 0) +SERD_API SerdStatus +serd_vxlogf(const SerdWorld* ZIX_NONNULL world, + SerdLogLevel level, + size_t n_fields, + const SerdLogField* ZIX_NULLABLE fields, + const char* ZIX_NONNULL fmt, + va_list args); + +/** + Write a message to the log with extra fields. + + This is a convenience wrapper for serd_vxlogf() that takes the format + arguments directly. +*/ +SERD_LOG_FUNC(5, 6) +SERD_API SerdStatus +serd_xlogf(const SerdWorld* ZIX_NONNULL world, + SerdLogLevel level, + size_t n_fields, + const SerdLogField* ZIX_NULLABLE fields, + const char* ZIX_NONNULL fmt, + ...); + +/** + Write a simple message to the log. + + This is a convenience wrapper for serd_vxlogf() which sets no extra fields. +*/ +SERD_LOG_FUNC(3, 0) +SERD_API SerdStatus +serd_vlogf(const SerdWorld* ZIX_NONNULL world, + SerdLogLevel level, + const char* ZIX_NONNULL fmt, + va_list args); + +/** + Write a simple message to the log. + + This is a convenience wrapper for serd_vlogf() that takes the format + arguments directly. +*/ +SERD_LOG_FUNC(3, 4) +SERD_API SerdStatus +serd_logf(const SerdWorld* ZIX_NONNULL world, + SerdLogLevel level, + const char* ZIX_NONNULL fmt, + ...); + +/** + Write a message to the log with a caret position. + + This is a convenience wrapper for serd_vxlogf() which sets `SERD_FILE`, + `SERD_LINE`, and `SERD_COL` to the position of the given caret. Entries are + typically printed with a GCC-style prefix like "file.ttl:16:4". +*/ +SERD_LOG_FUNC(4, 0) +SERD_API SerdStatus +serd_vlogf_at(const SerdWorld* ZIX_NONNULL world, + SerdLogLevel level, + const SerdCaret* ZIX_NULLABLE caret, + const char* ZIX_NONNULL fmt, + va_list args); + +/** + Write a message to the log with a caret position. + + This is a convenience wrapper for serd_vlogf_at() that takes the format + arguments directly. +*/ +SERD_LOG_FUNC(4, 5) +SERD_API SerdStatus +serd_logf_at(const SerdWorld* ZIX_NONNULL world, + SerdLogLevel level, + const SerdCaret* ZIX_NULLABLE caret, + const char* ZIX_NONNULL fmt, + ...); + +/** + @} +*/ + +SERD_END_DECLS + +#endif // SERD_LOG_H diff --git a/include/serd/serd.h b/include/serd/serd.h index 2b09eff2..6103c543 100644 --- a/include/serd/serd.h +++ b/include/serd/serd.h @@ -28,7 +28,7 @@ @{ */ -#include "serd/error.h" +#include "serd/log.h" #include "serd/status.h" /** diff --git a/include/serd/world.h b/include/serd/world.h index a950a1aa..9440e6eb 100644 --- a/include/serd/world.h +++ b/include/serd/world.h @@ -5,7 +5,6 @@ #define SERD_WORLD_H #include "serd/attributes.h" -#include "serd/error.h" #include "serd/node.h" #include "serd/status.h" #include "zix/attributes.h" @@ -73,17 +72,6 @@ SERD_API const SerdNode* ZIX_NONNULL serd_world_get_blank(SerdWorld* ZIX_NONNULL world); /** - Set a function to be called when errors occur. - - The `error_func` will be called with `handle` as its first argument. If - no error function is set, errors are printed to stderr. -*/ -SERD_API void -serd_world_set_error_func(SerdWorld* ZIX_NONNULL world, - SerdLogFunc ZIX_NULLABLE error_func, - void* ZIX_NULLABLE handle); - -/** @} */ diff --git a/meson.build b/meson.build index 642da5a9..4cf4e8d6 100644 --- a/meson.build +++ b/meson.build @@ -80,6 +80,10 @@ else 'stdio.h', 'return fileno(stdin);', ], + 'isatty': [ + 'unistd.h', + 'return isatty(0);', + ], 'posix_fadvise': [ 'fcntl.h', 'posix_fadvise(0, 0, 4096, POSIX_FADV_SEQUENTIAL);', @@ -131,9 +135,9 @@ c_headers = files( 'include/serd/buffer.h', 'include/serd/caret.h', 'include/serd/env.h', - 'include/serd/error.h', 'include/serd/event.h', 'include/serd/input_stream.h', + 'include/serd/log.h', 'include/serd/memory.h', 'include/serd/node.h', 'include/serd/output_stream.h', @@ -160,6 +164,7 @@ sources = files( 'src/caret.c', 'src/env.c', 'src/input_stream.c', + 'src/log.c', 'src/node.c', 'src/output_stream.c', 'src/read_nquads.c', diff --git a/meson/suppressions/meson.build b/meson/suppressions/meson.build index 8bb9a27f..dba5c46c 100644 --- a/meson/suppressions/meson.build +++ b/meson/suppressions/meson.build @@ -85,6 +85,7 @@ if is_variable('cc') '/wd4514', # unreferenced inline function has been removed '/wd4710', # function not inlined '/wd4711', # function selected for automatic inline expansion + '/wd4774', # format string is not a string literal '/wd4800', # implicit conversion from int to bool '/wd4820', # padding added after construct '/wd5045', # will insert Spectre mitigation for memory load diff --git a/src/.clang-tidy b/src/.clang-tidy index 77b43fc1..53834f98 100644 --- a/src/.clang-tidy +++ b/src/.clang-tidy @@ -1,11 +1,10 @@ -# Copyright 2020-2022 David Robillard <d@drobilla.net> +# Copyright 2020-2023 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC Checks: > -*-magic-numbers, -bugprone-easily-swappable-parameters, -cert-err33-c, - -clang-analyzer-valist.Uninitialized, -clang-diagnostic-unused-function, -hicpp-multiway-paths-covered, -hicpp-signed-bitwise, diff --git a/src/log.c b/src/log.c new file mode 100644 index 00000000..6ba885c6 --- /dev/null +++ b/src/log.c @@ -0,0 +1,325 @@ +// Copyright 2011-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "log.h" +#include "serd_config.h" +#include "world.h" + +#include "serd/attributes.h" +#include "serd/caret.h" +#include "serd/log.h" +#include "serd/node.h" +#include "serd/status.h" +#include "serd/string_view.h" +#include "serd/world.h" +#include "zix/attributes.h" + +#if USE_ISATTY +# include <unistd.h> +#endif + +#include <assert.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static const char* const log_level_strings[] = {"emergency", + "alert", + "critical", + "error", + "warning", + "note", + "info", + "debug"}; + +static int +level_color(const SerdLogLevel level) +{ + switch (level) { + case SERD_LOG_LEVEL_EMERGENCY: + case SERD_LOG_LEVEL_ALERT: + case SERD_LOG_LEVEL_CRITICAL: + case SERD_LOG_LEVEL_ERROR: + return 31; // Red + case SERD_LOG_LEVEL_WARNING: + return 33; // Yellow + case SERD_LOG_LEVEL_NOTICE: + case SERD_LOG_LEVEL_INFO: + case SERD_LOG_LEVEL_DEBUG: + break; + } + + return 1; // White +} + +static void +serd_ansi_start(const bool enabled, + FILE* const stream, + const int color, + const bool bold) +{ + if (enabled) { + fprintf(stream, bold ? "\033[0;%d;1m" : "\033[0;%dm", color); + } +} + +static void +serd_ansi_reset(const bool enabled, FILE* const stream) +{ + if (enabled) { + fprintf(stream, "\033[0m"); + fflush(stream); + } +} + +static bool +terminal_supports_color(const int fd) +{ + // https://no-color.org/ + // NOLINTNEXTLINE(concurrency-mt-unsafe) + if (getenv("NO_COLOR")) { + return false; + } + + // https://bixense.com/clicolors/ + // NOLINTNEXTLINE(concurrency-mt-unsafe) + const char* const clicolor_force = getenv("CLICOLOR_FORCE"); + if (clicolor_force && !!strcmp(clicolor_force, "0")) { + return true; + } + + // https://bixense.com/clicolors/ + // NOLINTNEXTLINE(concurrency-mt-unsafe) + const char* const clicolor = getenv("CLICOLOR"); + if (clicolor && !strcmp(clicolor, "0")) { + return false; + } + +#if USE_ISATTY + // Assume support if stream is a TTY (blissfully ignoring termcap nightmares) + return isatty(fd); +#else + (void)stream; + return false; +#endif +} + +SerdStatus +serd_log_init(SerdLog* const log) +{ + log->func = NULL; + log->handle = NULL; + log->stderr_color = terminal_supports_color(1); + return SERD_SUCCESS; +} + +SerdStatus +serd_quiet_log_func(void* const handle, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const SerdStringView message) +{ + (void)handle; + (void)level; + (void)n_fields; + (void)fields; + (void)message; + return SERD_SUCCESS; +} + +static const char* +get_log_field(const size_t n_fields, + const SerdLogField* const fields, + const char* const key) +{ + for (size_t i = 0; i < n_fields; ++i) { + if (!strcmp(fields[i].key, key)) { + return fields[i].value; + } + } + + return NULL; +} + +void +serd_set_log_func(SerdWorld* const world, + const SerdLogFunc log_func, + void* const handle) +{ + assert(world); + + world->log.func = log_func; + world->log.handle = handle; +} + +SERD_LOG_FUNC(5, 0) +static SerdStatus +serd_default_vxlogf(const bool stderr_color, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const char* const fmt, + va_list args) +{ + assert(fmt); + + // Print input file and position prefix if available + const char* const file = get_log_field(n_fields, fields, "SERD_FILE"); + const char* const line = get_log_field(n_fields, fields, "SERD_LINE"); + const char* const col = get_log_field(n_fields, fields, "SERD_COL"); + if (file) { + serd_ansi_start(stderr_color, stderr, 1, true); + if (line && col) { + fprintf(stderr, "%s:%s:%s: ", file, line, col); + } else { + fprintf(stderr, "%s: ", file); + } + serd_ansi_reset(stderr_color, stderr); + } + + // Print GCC-style level prefix (error, warning, etc) + serd_ansi_start(stderr_color, stderr, level_color(level), true); + fprintf(stderr, "%s: ", log_level_strings[level]); + serd_ansi_reset(stderr_color, stderr); + + // Format and print the message itself + vfprintf(stderr, fmt, args); + + // Print clang-tidy style check suffix + const char* const check = get_log_field(n_fields, fields, "SERD_CHECK"); + if (check) { + fprintf(stderr, " [%s]", check); + } + + fprintf(stderr, "\n"); + return SERD_SUCCESS; +} + +SerdStatus +serd_vxlogf(const SerdWorld* const world, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const char* const fmt, + va_list args) +{ + assert(world); + assert(fmt); + + if (world->log.func) { + char message[512] = {0}; + const int r = vsnprintf(message, sizeof(message), fmt, args); + + return (r <= 0 || (size_t)r >= sizeof(message)) + ? SERD_BAD_ARG + : world->log.func(world->log.handle, + level, + n_fields, + fields, + serd_substring(message, (size_t)r)); + } + + return serd_default_vxlogf( + world->log.stderr_color, level, n_fields, fields, fmt, args); +} + +SerdStatus +serd_xlogf(const SerdWorld* const world, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const char* const fmt, + ...) +{ + assert(world); + assert(fmt); + + va_list args; // NOLINT(cppcoreguidelines-init-variables) + va_start(args, fmt); + + const SerdStatus st = serd_vxlogf(world, level, n_fields, fields, fmt, args); + + va_end(args); + return st; +} + +SerdStatus +serd_vlogf(const SerdWorld* const world, + const SerdLogLevel level, + const char* const fmt, + va_list args) +{ + assert(world); + assert(fmt); + + return serd_vxlogf(world, level, 0U, NULL, fmt, args); +} + +SerdStatus +serd_logf(const SerdWorld* const world, + const SerdLogLevel level, + const char* const fmt, + ...) +{ + assert(world); + assert(fmt); + + va_list args; // NOLINT(cppcoreguidelines-init-variables) + va_start(args, fmt); + + const SerdStatus st = serd_vxlogf(world, level, 0U, NULL, fmt, args); + + va_end(args); + return st; +} + +SerdStatus +serd_vlogf_at(const SerdWorld* ZIX_NONNULL world, + SerdLogLevel level, + const SerdCaret* caret, + const char* ZIX_NONNULL fmt, + va_list args) +{ + assert(world); + assert(fmt); + + const SerdNode* const document = caret ? serd_caret_document(caret) : NULL; + if (!document) { + return serd_vxlogf(world, level, 0U, NULL, fmt, args); + } + + char line[24]; + char col[24]; + snprintf(line, sizeof(line), "%u", serd_caret_line(caret)); + snprintf(col, sizeof(col), "%u", serd_caret_column(caret)); + + const SerdLogField fields[] = { + {"SERD_FILE", serd_node_string(document)}, + {"SERD_LINE", line}, + {"SERD_COL", col}, + }; + + return serd_vxlogf(world, level, 3, fields, fmt, args); +} + +SerdStatus +serd_logf_at(const SerdWorld* ZIX_NONNULL world, + SerdLogLevel level, + const SerdCaret* caret, + const char* ZIX_NONNULL fmt, + ...) +{ + assert(world); + assert(fmt); + + va_list args; // NOLINT(cppcoreguidelines-init-variables) + va_start(args, fmt); + + const SerdStatus st = serd_vlogf_at(world, level, caret, fmt, args); + + va_end(args); + return st; +} diff --git a/src/log.h b/src/log.h new file mode 100644 index 00000000..ebb7dd60 --- /dev/null +++ b/src/log.h @@ -0,0 +1,21 @@ +// Copyright 2011-2023 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef SERD_SRC_LOG_H +#define SERD_SRC_LOG_H + +#include "serd/log.h" +#include "serd/status.h" + +#include <stdbool.h> + +typedef struct { + SerdLogFunc func; + void* handle; + bool stderr_color; +} SerdLog; + +SerdStatus +serd_log_init(SerdLog* log); + +#endif // SERD_SRC_LOG_H diff --git a/src/reader.c b/src/reader.c index 6f5ccce6..f34f7974 100644 --- a/src/reader.c +++ b/src/reader.c @@ -17,6 +17,7 @@ #include "world.h" #include "serd/input_stream.h" +#include "serd/log.h" #include <assert.h> #include <stdarg.h> @@ -32,8 +33,10 @@ r_err(SerdReader* const reader, const SerdStatus st, const char* const fmt, ...) { va_list args; // NOLINT(cppcoreguidelines-init-variables) va_start(args, fmt); - const SerdError e = {st, &reader->source->caret, fmt, &args}; - serd_world_error(reader->world, &e); + + serd_vlogf_at( + reader->world, SERD_LOG_LEVEL_ERROR, &reader->source->caret, fmt, args); + va_end(args); return st; } diff --git a/src/reader.h b/src/reader.h index 01ad5890..601453b0 100644 --- a/src/reader.h +++ b/src/reader.h @@ -10,7 +10,6 @@ #include "try.h" #include "serd/attributes.h" -#include "serd/error.h" #include "serd/node.h" #include "serd/reader.h" #include "serd/sink.h" @@ -36,8 +35,6 @@ typedef struct { struct SerdReaderImpl { SerdWorld* world; const SerdSink* sink; - SerdLogFunc error_func; - void* error_handle; SerdNode* rdf_first; SerdNode* rdf_rest; SerdNode* rdf_nil; diff --git a/src/serd_config.h b/src/serd_config.h index 90c4d31e..0bd9b5ac 100644 --- a/src/serd_config.h +++ b/src/serd_config.h @@ -60,6 +60,13 @@ # endif # endif +// POSIX.1-2001: isatty() +# ifndef HAVE_ISATTY +# if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L +# define HAVE_ISATTY +# endif +# endif + // POSIX.1-2001: posix_fadvise() # ifndef HAVE_POSIX_FADVISE # if SERD__POSIX_VERSION >= 200112L && !defined(__APPLE__) @@ -97,6 +104,12 @@ # define USE_FILENO 0 #endif +#if defined(HAVE_ISATTY) && HAVE_ISATTY +# define USE_ISATTY 1 +#else +# define USE_ISATTY 0 +#endif + #if defined(HAVE_POSIX_FADVISE) && HAVE_POSIX_FADVISE # define USE_POSIX_FADVISE 1 #else diff --git a/src/world.c b/src/world.c index 9f10de49..f5ab2f19 100644 --- a/src/world.c +++ b/src/world.c @@ -3,68 +3,19 @@ #include "world.h" -#include "caret.h" +#include "log.h" #include "node.h" #include "serd/node.h" - +#include "serd/status.h" #include "serd/string_view.h" +#include "serd/world.h" #include <assert.h> -#include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -SerdStatus -serd_world_error(const SerdWorld* const world, const SerdError* const e) -{ - if (world->error_func) { - world->error_func(world->error_handle, e); - } else { - fprintf(stderr, "error: "); - if (e->caret) { - fprintf(stderr, - "%s:%u:%u: ", - serd_node_string(e->caret->document), - e->caret->line, - e->caret->col); - } - vfprintf(stderr, e->fmt, *e->args); - fprintf(stderr, "\n"); - } - return e->status; -} - -SerdStatus -serd_world_verrorf(const SerdWorld* const world, - const SerdStatus st, - const char* const fmt, - va_list args) -{ - va_list args_copy; - va_copy(args_copy, args); - - const SerdError e = {st, NULL, fmt, &args_copy}; - serd_world_error(world, &e); - va_end(args_copy); - return st; -} - -SerdStatus -serd_world_errorf(const SerdWorld* const world, - const SerdStatus st, - const char* const fmt, - ...) -{ - va_list args; // NOLINT(cppcoreguidelines-init-variables) - va_start(args, fmt); - const SerdError e = {st, NULL, fmt, &args}; - serd_world_error(world, &e); - va_end(args); - return st; -} - SerdWorld* serd_world_new(void) { @@ -81,6 +32,8 @@ serd_world_new(void) world->limits.writer_max_depth = 128U; world->blank_node = blank_node; + serd_log_init(&world->log); + return world; } @@ -125,12 +78,3 @@ serd_world_get_blank(SerdWorld* const world) #undef BLANK_CHARS } - -void -serd_world_set_error_func(SerdWorld* world, - SerdLogFunc error_func, - void* handle) -{ - world->error_func = error_func; - world->error_handle = handle; -} diff --git a/src/world.h b/src/world.h index 56db46cc..af6281d4 100644 --- a/src/world.h +++ b/src/world.h @@ -4,32 +4,18 @@ #ifndef SERD_SRC_WORLD_H #define SERD_SRC_WORLD_H -#include "serd/error.h" +#include "log.h" + #include "serd/node.h" -#include "serd/status.h" #include "serd/world.h" -#include <stdarg.h> #include <stdint.h> struct SerdWorldImpl { - SerdLimits limits; - SerdLogFunc error_func; - void* error_handle; - uint32_t next_blank_id; - SerdNode* blank_node; + SerdLimits limits; + SerdLog log; + uint32_t next_blank_id; + SerdNode* blank_node; }; -SerdStatus -serd_world_error(const SerdWorld* world, const SerdError* e); - -SerdStatus -serd_world_errorf(const SerdWorld* world, SerdStatus st, const char* fmt, ...); - -SerdStatus -serd_world_verrorf(const SerdWorld* world, - SerdStatus st, - const char* fmt, - va_list args); - #endif // SERD_SRC_WORLD_H diff --git a/src/writer.c b/src/writer.c index 940b22a0..94c75625 100644 --- a/src/writer.c +++ b/src/writer.c @@ -16,6 +16,7 @@ #include "serd/attributes.h" #include "serd/env.h" #include "serd/event.h" +#include "serd/log.h" #include "serd/node.h" #include "serd/output_stream.h" #include "serd/sink.h" @@ -161,7 +162,7 @@ write_node(SerdWriter* writer, SerdField field, SerdStatementFlags flags); -SERD_NODISCARD static bool +static bool supports_abbrev(const SerdWriter* writer) { return writer->syntax == SERD_TURTLE || writer->syntax == SERD_TRIG; @@ -198,7 +199,7 @@ w_err(SerdWriter* writer, SerdStatus st, const char* fmt, ...) va_list args; // NOLINT(cppcoreguidelines-init-variables) va_start(args, fmt); - serd_world_verrorf(writer->world, st, fmt, args); + serd_vlogf(writer->world, SERD_LOG_LEVEL_ERROR, fmt, args); va_end(args); return st; @@ -264,9 +265,13 @@ sink(const void* buf, size_t len, SerdWriter* writer) char message[1024] = {0}; serd_system_strerror(errno, message, sizeof(message)); - w_err(writer, SERD_BAD_WRITE, "write error (%s)\n", message); + w_err(writer, SERD_BAD_WRITE, "write error (%s)", message); } else { - w_err(writer, SERD_BAD_WRITE, "write error\n"); + w_err(writer, + SERD_BAD_WRITE, + "unknown write error, %zu / %zu bytes written", + written, + len); } } @@ -291,7 +296,7 @@ write_character(SerdWriter* const writer, const uint32_t c = parse_utf8_char(utf8, size); switch (*size) { case 0: - *st = w_err(writer, SERD_BAD_TEXT, "invalid UTF-8 start: %X\n", utf8[0]); + *st = w_err(writer, SERD_BAD_TEXT, "invalid UTF-8 start: %X", utf8[0]); return 0; case 1: snprintf(escape, sizeof(escape), "\\u%04X", utf8[0]); @@ -864,7 +869,7 @@ write_uri_node(SerdWriter* const writer, !serd_env_base_uri(writer->env)) { return w_err(writer, SERD_BAD_ARG, - "syntax does not support URI reference <%s>\n", + "URI reference <%s> in unsupported syntax", node_str); } @@ -886,7 +891,7 @@ write_curie(SerdWriter* const writer, const SerdNode* const node) if (!supports_abbrev(writer) || !fast) { const SerdStringView curie = serd_node_string_view(node); if ((st = serd_env_expand_in_place(writer->env, curie, &prefix, &suffix))) { - return w_err(writer, st, "undefined namespace prefix '%s'\n", node_str); + return w_err(writer, st, "undefined namespace prefix '%s'", node_str); } } @@ -1307,7 +1312,10 @@ serd_writer_end_anon(SerdWriter* writer, const SerdNode* node) } if (!writer->anon_stack_size) { - return w_err(writer, SERD_BAD_EVENT, "unexpected end of anonymous node\n"); + return w_err(writer, + SERD_BAD_EVENT, + "unexpected end of anonymous node '%s'", + serd_node_string(node)); } // Write the end separator ']' and pop the context diff --git a/test/meson.build b/test/meson.build index a2436b22..01f75b5a 100644 --- a/test/meson.build +++ b/test/meson.build @@ -127,6 +127,7 @@ unit_tests = [ 'caret', 'env', 'free_null', + 'log', 'node', 'overflow', 'reader', @@ -210,7 +211,7 @@ simple_command_tests = { if is_variable('serd_pipe') pipe_script_args = common_script_args + ['--tool', serd_pipe] serd_ttl = files('../serd.ttl')[0] - bad_input_file = files('extra/bad/bad-base.ttl') + bad_input_file = files('extra/bad/bad-lang.ttl') test('serd_ttl', serd_pipe, args: [serd_ttl], env: test_env, suite: 'data') @@ -250,7 +251,31 @@ if is_variable('serd_pipe') files('test_quiet.py'), args: pipe_script_args + [bad_input_file], env: test_env, - suite: cmd_suite, + suite: 'log', + ) + test( + 'CLICOLOR_FORCE', + serd_pipe, + args: bad_input_file, + env: test_env + ['CLICOLOR_FORCE=1'], + should_fail: true, + suite: 'log', + ) + test( + 'CLICOLOR', + serd_pipe, + args: bad_input_file, + env: test_env + ['CLICOLOR=0'], + should_fail: true, + suite: 'log', + ) + test( + 'NO_COLOR', + serd_pipe, + args: bad_input_file, + env: test_env + ['NO_COLOR=1'], + should_fail: true, + suite: 'log', ) # Inputs diff --git a/test/test_log.c b/test/test_log.c new file mode 100644 index 00000000..3ab2008f --- /dev/null +++ b/test/test_log.c @@ -0,0 +1,121 @@ +// Copyright 2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#undef NDEBUG + +#include "serd/caret.h" +#include "serd/log.h" +#include "serd/node.h" +#include "serd/status.h" +#include "serd/string_view.h" +#include "serd/world.h" + +#include <assert.h> +#include <stdbool.h> +#include <string.h> + +static SerdStatus +custom_log_func(void* const handle, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const SerdStringView message) +{ + (void)message; + + bool* const called = (bool*)handle; + + assert(level == SERD_LOG_LEVEL_NOTICE); + assert(n_fields == 1); + assert(!strcmp(fields[0].key, "TEST_KEY")); + assert(!strcmp(fields[0].value, "TEST VALUE")); + assert(!strcmp(message.data, "test message 42")); + assert(message.length == strlen("test message 42")); + + *called = true; + return SERD_SUCCESS; +} + +static void +test_bad_arg(void) +{ + SerdWorld* const world = serd_world_new(); + bool called = false; + + serd_set_log_func(world, custom_log_func, &called); + + assert(serd_logf(world, SERD_LOG_LEVEL_ERROR, "%s", "") == SERD_BAD_ARG); + + serd_world_free(world); +} + +static void +test_default_log(void) +{ + SerdWorld* const world = serd_world_new(); + + for (unsigned i = 0; i <= SERD_LOG_LEVEL_DEBUG; ++i) { + const SerdLogLevel level = (SerdLogLevel)i; + + assert(!serd_logf(world, level, "test")); + } + + serd_world_free(world); +} + +static void +test_custom_log(void) +{ + SerdWorld* const world = serd_world_new(); + bool called = false; + + serd_set_log_func(world, custom_log_func, &called); + + const SerdLogField fields[1] = {{"TEST_KEY", "TEST VALUE"}}; + assert(!serd_xlogf( + world, SERD_LOG_LEVEL_NOTICE, 1, fields, "test message %d", 42)); + + assert(called); + serd_world_free(world); +} + +static void +test_caret(void) +{ + SerdWorld* const world = serd_world_new(); + SerdNode* const name = serd_new_string(serd_string("filename")); + SerdCaret* const caret = serd_caret_new(name, 46, 2); + + serd_logf_at(world, SERD_LOG_LEVEL_NOTICE, caret, "is just ahead of me"); + serd_logf_at(world, SERD_LOG_LEVEL_NOTICE, NULL, "isn't"); + + serd_caret_free(caret); + serd_node_free(name); + serd_world_free(world); +} + +static void +test_filename_only(void) +{ + SerdWorld* const world = serd_world_new(); + + const SerdLogField fields[3] = {{"TEST_KEY", "TEST VALUE"}, + {"SERD_FILE", "somename"}, + {"SERD_CHECK", "somecheck"}}; + + assert(!serd_xlogf(world, SERD_LOG_LEVEL_INFO, 3, fields, "no numbers here")); + + serd_world_free(world); +} + +int +main(void) +{ + test_bad_arg(); + test_default_log(); + test_custom_log(); + test_caret(); + test_filename_only(); + + return 0; +} diff --git a/tools/console.c b/tools/console.c index d0e6aaef..f7de779f 100644 --- a/tools/console.c +++ b/tools/console.c @@ -64,7 +64,9 @@ serd_set_base_uri_from_path(SerdEnv* const env, const char* const path) } SerdSyntax -serd_choose_syntax(const SerdSyntax requested, const char* const filename) +serd_choose_syntax(SerdWorld* const world, + const SerdSyntax requested, + const char* const filename) { if (requested) { return requested; @@ -75,9 +77,10 @@ serd_choose_syntax(const SerdSyntax requested, const char* const filename) return guessed; } - fprintf(stderr, - "warning: unable to determine syntax of \"%s\", trying TriG\n", - filename); + serd_logf(world, + SERD_LOG_LEVEL_WARNING, + "unable to determine syntax of \"%s\", trying TriG", + filename); return SERD_TRIG; } diff --git a/tools/console.h b/tools/console.h index 273b40c9..29b0a7df 100644 --- a/tools/console.h +++ b/tools/console.h @@ -9,6 +9,7 @@ #include "serd/output_stream.h" #include "serd/status.h" #include "serd/syntax.h" +#include "serd/world.h" #include <stdio.h> @@ -22,7 +23,9 @@ SerdStatus serd_set_base_uri_from_path(SerdEnv* env, const char* path); SerdSyntax -serd_choose_syntax(SerdSyntax requested, const char* filename); +serd_choose_syntax(SerdWorld* world, + SerdSyntax requested, + const char* filename); SerdInputStream serd_open_tool_input(const char* filename); diff --git a/tools/serd-pipe.c b/tools/serd-pipe.c index b09c12e1..734d02f4 100644 --- a/tools/serd-pipe.c +++ b/tools/serd-pipe.c @@ -4,8 +4,8 @@ #include "console.h" #include "serd/env.h" -#include "serd/error.h" #include "serd/input_stream.h" +#include "serd/log.h" #include "serd/node.h" #include "serd/output_stream.h" #include "serd/reader.h" @@ -69,14 +69,6 @@ missing_arg(const char* const name, const char opt) } static SerdStatus -quiet_error_func(void* const handle, const SerdError* const e) -{ - (void)handle; - (void)e; - return SERD_SUCCESS; -} - -static SerdStatus read_file(SerdWorld* const world, const SerdSyntax syntax, const SerdReaderFlags flags, @@ -304,7 +296,7 @@ main(int argc, char** argv) serd_writer_new(world, output_syntax, writer_flags, env, &out, block_size); if (quiet) { - serd_world_set_error_func(world, quiet_error_func, NULL); + serd_set_log_func(world, serd_quiet_log_func, NULL); } if (root_uri) { @@ -355,7 +347,7 @@ main(int argc, char** argv) } if ((st = read_file(world, - serd_choose_syntax(input_syntax, inputs[i]), + serd_choose_syntax(world, input_syntax, inputs[i]), reader_flags, serd_writer_sink(writer), stack_size, |