How to force newline at end of files and why you should do it

Alexey Inkin
6 min readJul 18, 2022

--

git blame

This file appears to have 3 lines of code:

When you add it to git, you see the message complaining about no newline:

diff --git a/lib/file.dart b/lib/file.dart
new file mode 100644
index 0000000..7fc9f98
--- /dev/null
+++ b/lib/file.dart
@@ -0,0 +1,3 @@
+class Foo {
+ const Foo();
+}
\ No newline at end of file

Why does it matter?

Let’s add another class:

We can see that the editor shows the new lines in green, but git has another opinion on this:

diff --git a/lib/file.dart b/lib/file.dart
index 7fc9f98..87a89ce 100644
--- a/lib/file.dart
+++ b/lib/file.dart
@@ -1,3 +1,5 @@
class Foo {
const Foo();
-}
\ No newline at end of file
+}
+
+class Bar {}
\ No newline at end of file

It considers the line with } deleted and a new line of } added. This is because technically the newline character is a part of the line it terminates.

This is how a line is defined in POSIX standard:

A sequence of zero or more non- <newline> characters plus a terminating <newline> character.

I guess it is just to have the simplest definition of a line.

So in our file, a single-character pseudo-line of } was replaced with a two-character line of } and newline.

When showing the lines’ history, the editor (Android Studio in my case) is forgiving enough to show the 3rd line as unchanged:

What it does is running git blame with -w option that ignores all whitespace changes including newline characters.

But this virtue is not granted. Many tools will show what really happened:

$ git show

$ git blame file.dart
f02f999d (Alexey Inkin 2022-07-14 10:25:25 +0400 1) class Foo {
f02f999d (Alexey Inkin 2022-07-14 10:25:25 +0400 2) const Foo();
d0cb21dd (Alexey Inkin 2022-07-14 10:31:18 +0400 3) }
d0cb21dd (Alexey Inkin 2022-07-14 10:31:18 +0400 4)
d0cb21dd (Alexey Inkin 2022-07-14 10:31:18 +0400 5) class Bar {}

wc -l

wc -l command is the most common way to count lines in your code. Following the definition, it just counts the number of newlines in a file.

So if you don’t end a file with a newline, most UNIX tools will show you have one less line.

This answer goes deeper on inconvenient scenarios without newline.

The Solution

The solution is to always end all of your text files with a newline:

When committing, you don’t get a no-newline warning:

diff --git a/lib/file_with_newline.dart b/lib/file_with_newline.dart
new file mode 100644
index 0000000..c57d1e3
--- /dev/null
+++ b/lib/file_with_newline.dart
@@ -0,0 +1,3 @@
+class Foo {
+ const Foo();
+}

Your diff is pristine:

Your blame looks the same with or without -w option:

60c0a7c0 (Alexey Inkin 2022-07-14 11:04:03 +0400 1) class Foo {
60c0a7c0 (Alexey Inkin 2022-07-14 11:04:03 +0400 2) const Foo();
60c0a7c0 (Alexey Inkin 2022-07-14 11:04:03 +0400 3) }
f496ee14 (Alexey Inkin 2022-07-14 11:05:51 +0400 4)
f496ee14 (Alexey Inkin 2022-07-14 11:05:51 +0400 5) class Bar {}

And you get paid for every line you author:

$ wc -l *
4 file.dart
5 file_with_newline.dart

Linter

In many languages there is a linter rule to flag unterminated lines. In Dart, use eol_at_end_of_file lint.

Read this for linter configuration.

Command

If your language does not support such lint, or you want to check non-code files, use a custom command to find files not ending with newline (credit):

find . -type f | xargs -L1 bash -c 'test "$(tail -c1 "$0")" && echo "No newline at end of $0"'

It works like this:

  • find prints all file names recursively.
  • . is where to start from, the current directory in this case. You can also add your code directory here. You can add multiple directories separated by space.
  • -type f limits the results to ordinary files and not directories.
  • | takes the output of the previous command and directs it as input to the following command instead of printing. This is called piping.
  • xargs takes lines from input and runs a given command on them.
  • -L1 means to run the command on each input line separately (otherwise it glues them).
  • bash -c 'test "$(tail -c1 "$0")" && echo "No newline at end of $0"' is what xargs runs on each input line (file path). So effectively this is ran for each file:
bash -c 'test "$(tail -c1 "$0")" && echo "No newline at end of $0"' file_path 
  • bash -c string means to run string as a command and make everything after it available to that command as positional arguments. Here string is
    'test "$(tail -c1 "$0")" && echo "No new line at end of $0"'
    After the string comes file_path, so it is available to that command as the positional argument of $0. So bash will effectively run this:
test "$(tail -c1 "file_path")" && echo "No newline at end of file_path"
  • test with a string after it checks if that string is not empty. It exits with a non-zero code (error) if it is empty and a zero code (OK) otherwise. && only continues to the next command if the previous one exited with zero code. It means that
    echo "No newline at and of file_path" will only be printed if the string passed to test is not empty.
  • "$(something)" means that something will be executed as a command, and its output inserted between the quotes. It means to execute
    tail -c1 "file_path" and to pass its result as an argument to test.
  • tail -c1 "file_path" takes the last 1 byte from the given file. So it is either newline character or some other byte. And this byte is what is passed to test as an argument. A string beginning with a newline character is treated by test as empty, no matter how long it is. So for newline character test exits with a non-zero code. Any other character means a non-empty string, it makes test exit with zero code and to continue to echo.

GitHub workflow

You can make GitHub to run this check for you on each commit. For this, add this to your workflow:

If you are new to workflows, read the end of this article.

And never end your Medium stories with newline. It is a byte-level unit. All rich documents have many sorts of wrappers for their content units and never deal with bytes including newline

--

--

Alexey Inkin

Google Developer Expert in Flutter. PHP, SQL, TS, Java, C++, professionally since 2003. Open for consulting & dev with my team. Telegram channel: @ainkin_com