Chapter 27.3 — Interpreter Scripts

Chapter 27.3 — Interpreter Scripts
How #! (Shebang) Works · Script Execution via exec() · EmbeddedPathashala
📌 Topic
Scripts & #!
🧠 Level
Beginner
💻 Examples
3 Programs
❓ Q&A
7 Questions

What is an Interpreter Script?

When you write a shell script, Python script, or awk script — these are just text files. The CPU can’t run text directly. So the Linux kernel uses the #! (shebang) line at the top of the file to figure out which program (the interpreter) should actually execute the text.

When you exec() a script file, the kernel reads the first line. If it starts with #!, it automatically re-runs the execution using the specified interpreter.

The #! Line Format

#! interpreter-path [ optional-arg ]

Examples:
#!/bin/sh              # Use Bourne shell
#!/bin/bash            # Use Bash
#!/usr/bin/python3     # Use Python 3
#!/usr/bin/awk -f      # Use awk (with -f flag)
#!/usr/bin/perl        # Use Perl
Rules for the #! line:

  • Must be the very first two characters of the file: #!
  • PATH is NOT used — you must give an absolute path to the interpreter
  • Only ONE optional argument is allowed (treated as a single word, even with spaces)
  • Linux limits the #! line to 127 characters

What the Kernel Does When It Sees #!

Step Action Example
1 You call execve(“myscript.sh”, argv, envp) argv = {“myscript.sh”, “arg1”, NULL}
2 Kernel reads first 2 bytes → sees “#!” First line: #!/bin/bash
3 Kernel extracts interpreter path and optional arg interpreter = /bin/bash, optional-arg = (none)
4 Kernel re-does exec with new argument list /bin/bash myscript.sh arg1
5 Interpreter starts and reads the script bash reads and executes myscript.sh

How Arguments Are Assembled for the Interpreter

argv[0] argv[1] argv[2] argv[3]…
interpreter-path
/bin/awk
optional-arg
-f
script-path
myscript.awk
remaining args from original execve()
(excluding argv[0])

Example 1: exec() a Shell Script from C

/* example1_exec_script.c
 * gcc -o ex1 example1_exec_script.c
 *
 * Step 1: Create the script file
 * Step 2: Make it executable: chmod +x myscript.sh
 * Step 3: Run: ./ex1
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

/* Create a simple shell script file */
void create_script(const char *filename)
{
    FILE *f = fopen(filename, "w");
    if (!f) { perror("fopen"); exit(1); }

    fprintf(f, "#!/bin/sh\n");
    fprintf(f, "echo \"Script argv[0] = $0\"\n");
    fprintf(f, "echo \"Script argv[1] = $1\"\n");
    fprintf(f, "echo \"Script argv[2] = $2\"\n");
    fprintf(f, "echo \"HOME = $HOME\"\n");

    fclose(f);

    /* Make it executable */
    chmod(filename, 0755);
    printf("Created script: %s\n", filename);
}

int main(void)
{
    pid_t child;
    int status;
    const char *script = "/tmp/test_script.sh";

    create_script(script);

    child = fork();
    if (child == -1) { perror("fork"); exit(1); }

    if (child == 0) {
        /* Child: exec the script */
        char *argv[] = { (char *)script, "first_arg", "second_arg", NULL };
        char *envp[] = { "HOME=/embedded", "PATH=/bin:/usr/bin", NULL };

        printf("Child: execve(%s)\n", script);
        execve(script, argv, envp);

        fprintf(stderr, "execve failed: %s\n", strerror(errno));
        exit(127);
    }

    waitpid(child, &status, 0);
    if (WIFEXITED(status))
        printf("Parent: script exited with %d\n", WEXITSTATUS(status));
    return 0;
}

/* Expected output:
 * Created script: /tmp/test_script.sh
 * Child: execve(/tmp/test_script.sh)
 * Script argv[0] = /tmp/test_script.sh
 * Script argv[1] = first_arg
 * Script argv[2] = second_arg
 * HOME = /embedded
 * Parent: script exited with 0
 */

Example 2: Using Optional Arg in #! Line (awk -f)

Without the -f option, awk treats its first argument as the script text. With -f it treats it as a script file. The #! optional-arg solves this.

/* example2_awk_script.c
 * gcc -o ex2 example2_awk_script.c && ./ex2
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

void create_awk_script(const char *filename)
{
    FILE *f = fopen(filename, "w");
    if (!f) { perror("fopen"); exit(1); }

    /* The -f flag in #! line tells awk: filename is a script file */
    fprintf(f, "#!/usr/bin/awk -f\n");
    fprintf(f, "{ print NR \": \" $0 }  # print line number: line\n");
    fprintf(f, "END { print \"Total lines:\", NR }\n");

    fclose(f);
    chmod(filename, 0755);
}

void create_input_file(const char *filename)
{
    FILE *f = fopen(filename, "w");
    if (!f) { perror("fopen"); exit(1); }
    fprintf(f, "BLE Audio\nLC3 Codec\nIsochronous Streams\n");
    fclose(f);
}

int main(void)
{
    pid_t child;
    int status;
    const char *awk_script = "/tmp/count_lines.awk";
    const char *input_file = "/tmp/topics.txt";

    create_awk_script(awk_script);
    create_input_file(input_file);
    printf("Running awk script via execve...\n\n");

    child = fork();
    if (child == 0) {
        /* Kernel sees #!/usr/bin/awk -f and transforms args:
         * /usr/bin/awk  -f  /tmp/count_lines.awk  /tmp/topics.txt */
        char *argv[] = { awk_script, (char *)input_file, NULL };
        char *envp[] = { NULL };

        execve(awk_script, argv, envp);
        fprintf(stderr, "execve: %s\n", strerror(errno));
        exit(127);
    }

    waitpid(child, &status, 0);
    return 0;
}

/* Expected output:
 * Running awk script via execve...
 *
 * 1: BLE Audio
 * 2: LC3 Codec
 * 3: Isochronous Streams
 * Total lines: 3
 */

Example 3: Scripts Without #! — execlp() Special Behavior

When a script has no #! line and you use execlp() or execvp(), they automatically try to run it with /bin/sh. Regular execve() would just fail with ENOEXEC.

/* example3_no_shebang.c
 * gcc -o ex3 example3_no_shebang.c && ./ex3
 *
 * Demonstrates: execlp() handles missing #! by running /bin/sh
 *               execve() fails with ENOEXEC for same script
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

void create_plain_script(const char *path)
{
    FILE *f = fopen(path, "w");
    if (!f) { perror("fopen"); exit(1); }

    /* No #! line! Just plain shell commands */
    fprintf(f, "echo 'Running without shebang!'\n");
    fprintf(f, "echo \"My PID is $$\"\n");

    fclose(f);
    chmod(path, 0755);
    printf("Created plain script (no #!): %s\n\n", path);
}

void try_execve(const char *script)
{
    pid_t child = fork();
    if (child == 0) {
        char *argv[] = { (char *)script, NULL };
        char *envp[] = { "PATH=/bin:/usr/bin", NULL };
        printf("Trying execve()...\n");
        execve(script, argv, envp);
        printf("execve FAILED: %s (errno %d = ENOEXEC expected)\n",
               strerror(errno), errno);
        exit(1);
    }
    int st; waitpid(child, &st, 0);
    printf("\n");
}

void try_execlp(const char *script)
{
    pid_t child = fork();
    if (child == 0) {
        printf("Trying execlp()...\n");
        /* execlp: if file has execute bit but no #!, run via /bin/sh */
        execlp(script, script, (char *)NULL);
        printf("execlp FAILED: %s\n", strerror(errno));
        exit(1);
    }
    int st; waitpid(child, &st, 0);
}

int main(void)
{
    const char *script = "/tmp/plain_script.sh";
    create_plain_script(script);

    try_execve(script);   /* Will fail */
    try_execlp(script);   /* Will succeed via /bin/sh */

    return 0;
}

/* Expected output:
 * Created plain script (no #!): /tmp/plain_script.sh
 *
 * Trying execve()...
 * execve FAILED: Exec format error (errno 8 = ENOEXEC expected)
 *
 * Trying execlp()...
 * Running without shebang!
 * My PID is 12345
 */

❓ Interview Questions — Interpreter Scripts

Q1. What does the #! line do, and who processes it?
Answer: The #! (shebang) line specifies which interpreter should run the script. It is processed by the Linux kernel during exec(), not by the shell. When execve() detects #! at the start of a file, it re-does the exec using the interpreter path from that line.
Q2. Why should the interpreter path in #! be absolute (e.g., /bin/bash not just bash)?
Answer: The kernel does NOT use the PATH environment variable when processing the #! line. It uses the path literally. If you write just “bash”, the kernel looks for a file literally named “bash” in the root directory and fails.
Q3. What is argv[0] when a script is exec’d?
Answer: argv[0] is the interpreter path (e.g., /bin/bash). The script path becomes argv[1], any optional arg from the #! line becomes an argument too. The original argv[0] given to execve() is discarded.
Q4. What happens if you exec() a script file with no #! line using execve()?
Answer: execve() fails with ENOEXEC (“Exec format error”). The file has execute permission but the kernel cannot recognize its format. However, execlp() and execvp() handle this specially — they fall back to running the script with /bin/sh.
Q5. Why does Linux treat the optional-arg as a single word (even with spaces)?
Answer: The kernel’s #! parser reads from the space after the interpreter path to the end of the line as one token. It does not split on spaces like a shell would. So “#!/usr/bin/awk -f -v FS=,” passes “-f -v FS=,” as a single argument, not three. This is Linux-specific behavior.
Q6. Why is #!/usr/bin/env python3 commonly used instead of #!/usr/bin/python3?
Answer: The path to python3 varies across systems (/usr/bin, /usr/local/bin, etc.). Using /usr/bin/env python3 works because env is almost always at /usr/bin/env, and env searches PATH for python3 — making the script portable across different Linux distributions.
Q7. How many optional arguments can the #! line have on Linux?
Answer: Only one — the entire text after the interpreter path (and its leading space) to end-of-line is treated as a single argument string. You cannot pass multiple space-separated arguments on the #! line on Linux (unlike some older BSDs).

Next: File Descriptors and exec()

Learn how open files survive across exec() and how to control this

→ Part 5: File Descriptors & exec() 🏠 All Tutorials

Leave a Reply

Your email address will not be published. Required fields are marked *