Search code examples
c-preprocessoravrprogmem

How to use #define for get filename without file extension and store it to PROGMEM


I want to get the name of the compiled file without extension as a constant in PROGMEM. Partially the name can be cleared like this:

#define __FILENAME__ strrchr(__FILE__, '\\') + 1

Okay, it's easy. Next I need to delete the file extension, I can do this with the code:

char *dot = strrchr(__FILENAME__, '.'); *dot = '\0'

But I can't get all-in-one inside #define because of step-by-step actions sequence and an redundant variable.

Is there a pretty solution? Length of the filename is unknown.

The option of clearing the name in the program body is an extra expense of code and memory.


Solution

  • How to use #define for get filename without file extension

    It is not possible to use C preprocessor to get a filename with file extension - C preprocessor has no capability of parsing strings.

    #define __FILENAME__
    

    Defining identifiers starting with double __ is invalid. They are reserved, you can't define such your own identifiers.

    Your code will fail miserably when the __FILE__ does not contain a slash or a dot.

    Next I need to delete the file extension, I can do this with the code:

    The code you presented is invalid. You can't modify __FILE__, it is a constant. If you are using runtime, you have to malloc the required memory or allocate the memory on stack.

    Potentially, you could execute a function on program startup to fix the value, or just call the function right after main. The happy assumption here is that filename without extension will always be smaller than __FILE__, so we can overallocate memory.

    static char file_we[] = __FILE__;
    
    __attribute__((__constructor__))
    void _fix_file_we_() {
        // modify file_we
        char *const slash = strrchr(__FILE__, '\\');
        if (slash) {
            strmove(file_we, slash + 1);
        }
        chat *const dot = strrchr(file_we, '.');
        if (dot) {
             *dot = '\0';
        }
    }
    

    With GCC, there are__attribute__((__constructor__)) and __FILE_NAME__ extension you might be interested in.

    and store it to PROGMEM

    It is impossible to get a string literal of a filename without extension in C programming language. You have to use an external program or an extension.

    With the above approach and using GCC, you can just instead compute only the length of file_we at runtime and store __FILE_NAME__ in a constant static variable. You will lose 2 bytes for .c, but that is really not much.

    #include <stdio.h>
    #include <string.h>
    static const char file_name[] = __FILE_NAME__;
    int main() {
       printf("%.*s\n", (int)strcspn(file_name, "."), file_name);
    }
    

    In this case, we might precompute the dot position with a constant expression, given the string is not long. The compilation time with longer strings when writing such long ternary expressions might increase.

    #include <stdio.h>
    #include <limits.h>
    
    #define FORRANGE_0(f, ...)  f(0, __VA_ARGS__)
    #define FORRANGE_1(f, ...)  FORRANGE_0(f, __VA_ARGS__) f(1, __VA_ARGS__)
    #define FORRANGE_2(f, ...)  FORRANGE_1(f, __VA_ARGS__) f(2, __VA_ARGS__)
    #define FORRANGE_3(f, ...)  FORRANGE_2(f, __VA_ARGS__) f(3, __VA_ARGS__)
    #define FORRANGE_4(f, ...)  FORRANGE_3(f, __VA_ARGS__) f(4, __VA_ARGS__)
    #define FORRANGE_5(f, ...)  FORRANGE_4(f, __VA_ARGS__) f(5, __VA_ARGS__)
    #define FORRANGE_6(f, ...)  FORRANGE_5(f, __VA_ARGS__) f(6, __VA_ARGS__)
    #define FORRANGE_7(f, ...)  FORRANGE_6(f, __VA_ARGS__) f(7, __VA_ARGS__)
    #define FORRANGE_8(f, ...)  FORRANGE_7(f, __VA_ARGS__) f(8, __VA_ARGS__)
    #define FORRANGE_9(f, ...)  FORRANGE_8(f, __VA_ARGS__) f(9, __VA_ARGS__)
    #define FORRANGE(f, n, ...) FORRANGE_##n(f, __VA_ARGS__)
    
    #define DOT_POS_CASE(n, f)  f[n]=='.' ? n :
    #define DOT_POS(f)  ( \
        !f[0] ? INT_MAX : \
        !f[1] ? FORRANGE(DOT_POS_CASE, 0, f) INT_MAX : \
        !f[2] ? FORRANGE(DOT_POS_CASE, 1, f) INT_MAX : \
        !f[3] ? FORRANGE(DOT_POS_CASE, 2, f) INT_MAX : \
        !f[4] ? FORRANGE(DOT_POS_CASE, 3, f) INT_MAX : \
        !f[5] ? FORRANGE(DOT_POS_CASE, 4, f) INT_MAX : \
        !f[6] ? FORRANGE(DOT_POS_CASE, 5, f) INT_MAX : \
        !f[7] ? FORRANGE(DOT_POS_CASE, 6, f) INT_MAX : \
        !f[8] ? FORRANGE(DOT_POS_CASE, 7, f) INT_MAX : \
        !f[9] ? FORRANGE(DOT_POS_CASE, 8, f) INT_MAX : \
        /* TODO: add more to handle longer strings */ \
        INT_MAX)
                
    static const char file_name[] = __FILE_NAME__;
    static int file_name_dot_pos = DOT_POS(__FILE_NAME__);
    
    int main() {
        printf("%.*s\n", file_name_dot_pos, file_name);
    }
    

    In real life, typically, the build system is configured to precompute the value and pass it using compiler options for each file separately, like -DFILE_WE="something".

    For example, the following CMake script:

    cmake_minimum_required(VERSION 3.11)
    project(test)
    add_executable(tgt main.c)
    
    get_property(srcs TARGET tgt PROPERTY SOURCES)
    foreach(src IN LISTS srcs)
      get_filename_component(file_we ${src} NAME_WE)
      set_property(
        SOURCE ${src}
        APPEND PROPERTY COMPILE_DEFINITIONS
        "FILE_WE=\"${file_we}\""
      )
    endforeach()
    
    include(CTest)
    add_test(NAME tgt COMMAND tgt)
    set_tests_properties(tgt PROPERTIES PASS_REGULAR_EXPRESSION main)
    

    Compiles a program:

    #include <stdio.h>
    int main() { printf("%s\n", FILE_WE); }
    

    With:

     /usr/bin/cc -DFILE_WE=\"main\"  .....main.c
    

    Or you can use a better preprocessor like m4 or jinja2 or php.