4

The following example C++ project has two files: hello_world.sh and hello_world.cpp.

I want to embed the "hello_world.sh" as resource file when building the project with cmake, after building the project, an executable called "helloworld" is generated.

Here is src/hello_world.sh

#!/bin/bash
echo "Hello World in hello_world.sh"

Here is src/hello_world.cpp

#include <iostream>

int main()
{
    std::cout << "Hello World in hello_world.cpp" << std::endl;
    
    /** How to call hello_world.sh in the main() function? */
    
    /** Dummy Code **/
    
    // Use /bin/bash to run the hello_world.sh 
    // which is embedded into the "helloworld" executable when building with cmake
    
    /bin/bash /path/to/resource_inside_the_helloworld_executable/hello_world.sh
}

Build the project, and embed the "hello_world.sh" into the generated executable file "helloworld":

# ... I don't know how to build this project to include "hello_world.sh" as resource file ...

Here is the expected output when running "helloworld" from terminal:

# helloworld
Hello World in hello_world.cpp
Hello World in hello_world.sh

I know I can install the "hello_world.sh" into the /home/test/hello_world.sh and then run it with "/bin/bash /home/test/hello_world.sh". But I just want to embed the "hello_world.sh" into the executable "helloworld" as a resource and call "hello_world.sh" internally from "helloworld".

Is it possible? If yes, how to do it?

6
  • 1
    If you want to call a script from c++ code, you can use std::system Commented May 31 at 8:25
  • 1
    I know I can call the system command with std::system("/bin/bash /home/test/hello_world.sh"), but the problem is: How to embed the hello_world.sh as resource and run the hello_world.sh? How to tell std:system to get the bash script from resource std::system("/bin/bash /not/a/real/path/which/is/embedded/into/the/executable/as/resource/hello_world.sh")? Commented May 31 at 8:28
  • 2
    You could add it as constant text in your source, and then write it to a temporary file to execute it at run time. On Windows you can use resource types inside the executable, but I am not sure if Linux has the same feature. Commented May 31 at 8:34
  • Sorry, missed the point. I guess that embedding the script into the binary is possible (as an array of characters for instance generated by some cmake target), but I can't see a way to call it from the c++ code. Note that you could still write this array in a temporary file and then use std::system, but it is seems quite tedious. Commented May 31 at 8:34
  • 2
    See the answer at https://stackoverflow.com/questions/1997172/is-there-a-linux-equivalent-of-windows-resource-files. Commented May 31 at 8:57

2 Answers 2

8

Maybe stream your bash script to the bash command like this :

#include <cstdio>
#include <string>
#include <iostream>
#include <sstream>
#include <vector>

std::string shellEscape(const std::string &arg) {
    if (arg.empty())
        return "''";
    std::string out = "'";
    for (char c: arg) {
        if (c == '\'')
            out += "'\\''";
        else
            out += c;
    }
    out += "'";
    return out;
}

int main(int argc, char *argv[]) {
    const std::vector<std::string> args(argv + 1, argv + argc);

    const std::string bashScript = R"BASH(
#!/usr/bin/env bash

printArgs() {
    if [ "$#" -ne 0 ]; then
        printf 'Arguments: %d\n' "$#"
        i=1
        for arg; do
            printf 'Argument %d: %s\n' "$i" "$arg"
            i=$((i + 1))
        done
    fi
}

printArgs "$@"
printf 'Hello from embedded Bash script!\n'
printf 'Current directory: %s\n' "$PWD"
printf 'System info: %s\n' "$(uname -a)"
)BASH";

    // Build command with escaped arguments
    std::ostringstream command;
    command << "/usr/bin/env bash -s --";
    for (const auto &arg: args) {
        command << ' ' << shellEscape(arg);
    }

    FILE *pipe = popen(command.str().c_str(), "w");
    if (!pipe) {
        perror("popen failed");
        return 1;
    }

    std::istringstream iss(bashScript);
    std::string line;
    while (std::getline(iss, line)) {
        fwrite(line.c_str(), 1, line.size(), pipe);
        fputc('\n', pipe);
    }

    if (const int exitStatus = pclose(pipe); exitStatus != 0) {
        std::cerr << "Bash execution failed with code "
                << exitStatus << std::endl;
        return 1;
    }

    return 0;
}

Each line of the Bash script is handled with Unix LF line endings. It sends it to the pipe for the bash command.

Bash does not make a difference if the script comes from a file or from a pipe stream. In both cases, bash loads the entire script in memory then start executing the script.

To answer @stackbiz's question: This make absolutely 0 difference if the embedded script has functions in it.

It also supports passing arguments safely to the embedded bash script.

Example execution:

./bashEmbed foo\\ bar\" 'baz composed'

Output:

Arguments: 3
Argument 1: foo\
Argument 2: bar"
Argument 3: baz composed
Hello from embedded Bash script!
Current directory: /home/lea/CLionProjects/BashEmbed
System info: Linux marvin 6.11.0-26-generic #26~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Apr 17 19:20:47 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
Sign up to request clarification or add additional context in comments.

2 Comments

What if the base script contains function? A function block inside the bash script will have multiple lines. It is not only a single line.
@stackbiz if the bash script has functions it doe snot change anything. Bash will load the entire script even when it is provided as input stream. Then it execute it as any other script. This makes absolutely no difference if the script comes from a file on disk or a file stream as in this implementation. The code I wrote feeds the stream line by line to be sure that each line has the correct LF ending. But bash will execute it only when all the lines has been sent to it.
-1

You can write your script into an anonymous (memory-backed) file (create it with memfd_create, then write to it) and execute it with fexecve.

Example (uses some C++17 features for convenience):

#include <array>
#include <cstdio>
#include <string_view>
#include <sys/mman.h>
#include <unistd.h>

// NOTE: the shebang MUST be in the first line
constexpr std::string_view SCRIPT = R"(#!/bin/bash
echo "Hello from bash!"
echo "Arguments: $0 $*"
)";

int main()
{
    // Cannot use MFD_CLOEXEC here (see fexecve(3))
    int memfd = memfd_create("script", 0);
    if (memfd < 0)
    {
        perror("memfd_create");
        return 1;
    }

    for (auto bytesRemaining = SCRIPT; !bytesRemaining.empty(); )
    {
        int result = write(memfd, bytesRemaining.data(), bytesRemaining.size());
        if (result < 0)
        {
            perror("write");
            close(memfd);
            return 1;
        }
        bytesRemaining.remove_prefix(result);
    }

    const char* args[] = { "SCRIPT", "arg1", "arg2", "arg3", nullptr };

    // You may want to fork here first
    fexecve(memfd, const_cast<char**>(args), environ);
    perror("fexecve");
    return 1;
}

Using this approach, the script can still use stdin normally. It's not very portable, although I think FreeBSD may also support it. arg0 also seems to be overridden, I'm not sure why.


Additionally, you can also embed your script using C++26's #embed:

constexpr char SCRIPT_STORAGE[] = {
#embed "script.sh"
};
constexpr std::string_view SCRIPT(std::data(SCRIPT_STORAGE), std::size(SCRIPT_STORAGE));

It seems to work well on GCC 15.1.

2 Comments

Bad idea. Programs run from the script will inherit the memfd and can modify the script by simply writing to it. Try echo uname >&3 (or whatever fd ls -l /proc/self/fd shows as memfd) to see.
@oguzismail Good point. That can be easily fixed by closing the file descriptor from the script, though.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.