17

I am trying to understand the grub config files. So, during this process I came across with file /etc/grub.d/40_custom. My file contains the following lines:

#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.
menuentry "Windows 10" --class windows --class os {
insmod part_msdos
savedefault
insmod ntfs
insmod ntldr
set root='(hd0,msdos1)'
ntldr ($root)/bootmgr
}

as my system is dual boot and apparently this is the boot loader for windows 10.

My question though is this part exec tail -n +3 $0.
If I am deciphering it correctly this just means print the last lines starting from the 3rd line (+3) of the file $0. $0 of course in this case is the actual file /etc/grub.d/40_custom.

So, why do we use this command in 40_custom file? As I get it the output would be the same if ιt was omitted altogether. The only different I might think of is the 1st line which identifies the interpreter:

#!/bin/sh

But then again it is executed since exec tail -n +3 $0 follows it. So, is this just a (useless) convention?

3 Answers 3

16

The trick is what exec does:

$ help exec
exec: exec [-cl] [-a name] [command [arguments ...]] [redirection ...]
    Replace the shell with the given command.

    Execute COMMAND, replacing this shell with the specified program.
    ARGUMENTS become the arguments to COMMAND.  If COMMAND is not specified,
    any redirections take effect in the current shell.

This means it will replace the shell with whatever is given to exec, in this case tail. Here's an example of it in action:

$ cat ~/foo.sh
#!/bin/sh
exec tail -n +3 "$0"
echo foo

$ ./foo.sh
echo foo

So the echo command isn't executed since we have changed the shell and are using tail instead. If we remove the exec tail:

$ cat ~/foo.sh
#!/bin/sh
echo foo
$ ./foo.sh
foo

So, this is a neat trick that lets you write a script whose only job is to output its own contents. Presumably, whatever calls 40_custom expects its contents as output. Of course, this begs the question of why not just running tail -n +3 /etc/grub.d/40_custom directly.

I am guessing the answer is because grub uses its own scripting language and this makes it need this workaround.

7
  • Nice answer! But what if we'll write #!/bin/tail -n +2 as a shellbang? Will it print rest of the file? Jan 7, 2019 at 11:24
  • @val try it and see :) Turns out it doesn't work perfectly as a shebang, the -n +2 seems to be interpreted as -n 2 and only the last two lines are printed. That's probably worth its own question though.
    – terdon
    Jan 7, 2019 at 11:35
  • 3
    @val ...shebang behavior is a lot less standardized and portable than you may expect. See the "interpretation of the command arguments" section of en.wikipedia.org/wiki/Shebang_(Unix)#Portability Jan 7, 2019 at 12:43
  • @val That works in Linux if you remove the space between the n and the +. In Linux at least, after the executable path and a space, the following characters are treated as a single argument. So with the space, tail sees it and probably decides to remove whatever invalid prefix -n's argument has (in this case, a space and a +) until it gets to the number. However, like Charles Duffy said, the current way they did this is probably more portable to other Unices.
    – JoL
    Jan 7, 2019 at 18:59
  • 1
    @fluffy They can use here doc. See the link from Fedora documentation in my answer. They use exactly cat <<EOF ...EOF there Jan 8, 2019 at 3:39
9

The directory /etc/grub.d/ contains many executables (typically shell scripts, but any other executable type is also possible). Whenever grub-mkconfig gets executed (e.g. if you run update-grub, but also when you install an updated kernel package, which usually has a post-install hook that tells the package manager to update grub.cfg), they are all executed in alphabetical order. Their outputs all get concatenated, and end up in the file /boot/grub/grub.cfg, with neat section headers that show which part comes from which /etc/grub.d/ file.

This one particular file 40_custom is designed to allow you to easily add entries/lines into grub.cfg by simply typing/pasting them into this file. Other scripts in the same directory do more complex tasks like looking for kernels or non-linux operating systems and creating menu entries for them.

In order to allow grub-mkconfig to treat all those files in the same way (execute and take the output), 40_custom is a script and uses this exec tail -n +3 $0 mechanism to output its contents (minus the "header"). If it weren't an executable, update-grub would need a special hard-coded exception to take this file's literal text content instead of executing it like all the others. But then what if you (or the makers of another linux distribution) want to give this file a different name? Or what if you didn't know about the exception and created a shell script named 40_custom?

You can read more about grub-mkconfig and /etc/grub.d/* in the GNU GRUB Manual (although it talks mostly about options you can set in /etc/default/grub), and there should also be a file /etc/grub.d/README that states that these files get executed to form grub.cfg.

2
  • 1
    While terdon's answer explains how the line works, this one gives a more plausible design reason as for why GRUB does things this way.
    – JoL
    Jan 7, 2019 at 19:10
  • Great answer. To your point about special exceptions - a "simpler" exception could have been having two directories e.g. /etc/grub.d/exec for exectuable files/scripts, and /etc/grub.d/static for plain text files, or some other indicator to distinguish these. However, this was the design decision that they went with.
    – Stobor
    Jan 8, 2019 at 2:19
3

TL;DR: it's a trick to simplify adding new entries to the file

The whole point is described in one of the Ubuntu's Wiki pages on grub:

  1. Only executable files generate output to grub.cfg during execution of update-grub.

Output of scripts in /etc/grub.d/ becomes the contents of grub.cfg file.

Now, what does exec do ? It can either re-wire output for entire script or if a command is provided - the mentioned command overtakes and replaces the script process. What was once shell script with PID 1234 now is tail command with PID 1234.

Now you already know that tail -n +3 $0 prints everything after the 3rd line in the script itself. So why do we need to do this ? If grub only cares about the output we could just as well do

cat <<EOF
    menuentry {
    ...
    }
EOF

In fact, you will find cat <<EOF example in Fedora documentation, albeit for a different purpose. The whole point is in the comments - ease of use for the users:

# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.

With exec trick, you don't have to know what does cat <<EOF do (spoiler, that's called here-doc), nor you have to remember to add the EOF on the last line. Just add menuentry to the file and be done with it. Plus, if you're scripting adding a menuentry, you can simply append via >> in shell to this file.

See also:

3
  • But why not have grub read the files directly? Do you have any idea why they chose to make them executable instead? It would be much simpler to have them as simple text files which are read by whatever process generates the menu, but instead they chose to make them executable and added this (neat) but complex workaround. Is it because grub has its own scripting language as I posit in my answer?
    – terdon
    Jan 7, 2019 at 10:21
  • @terdon I don't think this has to do anything with the grub language itself. You wouldn't need #!/bin/sh in that case. I suspect this is simply historical reason (carried over from PUPA codebase ) and a design decision inspired by SysV type of scripting. Jan 7, 2019 at 10:36
  • @terdon This actually inspired a question: unix.stackexchange.com/q/492966/85039 Jan 7, 2019 at 10:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .