aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--NEWS1
-rw-r--r--doc/man/serd-pipe.126
-rw-r--r--include/serd/error.h45
-rw-r--r--include/serd/log.h209
-rw-r--r--include/serd/serd.h2
-rw-r--r--include/serd/world.h12
-rw-r--r--meson.build7
-rw-r--r--meson/suppressions/meson.build1
-rw-r--r--src/.clang-tidy3
-rw-r--r--src/log.c325
-rw-r--r--src/log.h21
-rw-r--r--src/reader.c7
-rw-r--r--src/reader.h3
-rw-r--r--src/serd_config.h13
-rw-r--r--src/world.c66
-rw-r--r--src/world.h26
-rw-r--r--src/writer.c24
-rw-r--r--test/meson.build29
-rw-r--r--test/test_log.c121
-rw-r--r--tools/console.c11
-rw-r--r--tools/console.h5
-rw-r--r--tools/serd-pipe.c14
22 files changed, 798 insertions, 173 deletions
diff --git a/NEWS b/NEWS
index 9dc3d414..21a842f6 100644
--- a/NEWS
+++ b/NEWS
@@ -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,