How to check your grammar and spelling on Linux using LanguageTool, which is some kind of European technology.

An AI image. You aren't missing anything.
An AI image depicting a European monk checking your bad grammar.

Of all the spelling and grammar checkers available on Linux, LanguageTool is one of them. It is an alternative to Grammarly, I suppose. There is a free component of the software that has reduced functionality. It runs as a webservice on the local machine. Follow the setup instructions in the code listing and you will be rewarded with a simple command that checks your spelling and grammar, like this:

Usage: check-grammar FILENAME

  Where the filetype of FILENAME is one of:
    txt,html,md,markdown,rst,ipynb,json,xliff

  If the input file does not have a file extension,
  plain text will (possibly erroneously) be assumed.

This requires a Linux system with Python, and Docker (docker.io). The instructions have been tested on Ubuntu 22.04 . Save this code as the file "check-grammar" and follow the setup instructions. The file is also available on Github as a Gist: check-grammar.sh, MIT License.

#!/bin/bash
# Check grammar using LanguageTool


# Copyright (c) 2025 Vic Dellarocco
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons
# to whom the Software is furnished to do so, subject to the
# following conditions:
#
# The above copyright notice and this permission notice shall
# be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


# https://languagetool.org/
# https://github.com/languagetool-org/languagetool
# https://pypi.org/project/pyLanguagetool

# FIRST TIME SETUP:
# I am not scripting this part because it requires downloading
# some big files. You will have to do this manually.

# Make a place for this program. I used ~/opt/languagetool .
 # mkdir ~/opt/languagetool
 # cd ~/opt/languagetool
 # mkdir ngrams
# Save THIS file in the ~/opt/languagetool directory.

# I am using languagetool version 6.5 because the instructions
# from languagetool say to:
#   "Use ngrams-xx-2015* files for LanguageTool <= 6.5,
#    ngrams-xx-2024* files for LanguageTool >= 6.6."
# but the ngrams-xx-2024* files aren't available for download!

# Download and extract the ngrams file: trust me, you need them.
# WARNING: big file: 8961853713 bytes.
 # wget https://languagetool.org/download/ngram-data/ngrams-en-20150817.zip
 # unzip ngrams-en-20150817.zip -d ngrams

# Erikvl87 has the best docker image for this:
# https://github.com/Erikvl87/docker-languagetool
# https://hub.docker.com/r/erikvl87/languagetool
# Get the docker image: 
 # docker pull erikvl87/languagetool:6.5

# Create the container:
 # docker run \
 # --name languagetool \
 # -p 127.0.0.1:8010:8010 \
 # -v `pwd`/ngrams:/ngrams:ro \
 # erikvl87/languagetool:6.5
# If that worked, then the container should be installed and
# running. You can stop it if you want like this:
#  docker stop languagetool

# If that all worked, then you can use THIS file to check your
# grammar as if it were a real normal command line command:
#  chmod+x check-grammar
#  ./check-grammar filetocheck
# You can put a symbolic link in your ~/bin/ dir to make it even
# easier to use:
#  cd ~/bin
#  ln -s ~/opt/languagetool/check-grammar

# A quick reference of selected docker commands:
#  docker stop    languagetool # stops the container.
#  docker start   languagetool # start if not already running
#  docker restart languagtool  # start if stopped else restart
#  docker rm      languagetool # remove the container.
#  docker ps                   # list running containers.
#  docker ps -a                # list all containers.

USAGE='Usage: check-grammar FILENAME

  Where the filetype of FILENAME is one of:
    txt,html,md,markdown,rst,ipynb,json,xliff

  If the input file does not have a file extension,
  plain text will (possibly erroneously) be assumed.
'

# Function to determine file type based on extension
determine_file_type() {
 local file="$1"
 local extension="${file##*.}"

 # Convert extension to lowercase
 extension="${extension,,}"

 case "$extension" in
  txt)
   DOCTYPE="txt"
   return 0
   ;;
  html)
   DOCTYPE="html"
   return 0
   ;;
  md|markdown)
   DOCTYPE="md"
   return 0
   ;;
  rst)
   DOCTYPE="rst"
   return 0
   ;;
  ipynb)
   DOCTYPE="ipynb"
   return 0
   ;;
  json)
   DOCTYPE="json"
   return 0
   ;;
  xliff)
   DOCTYPE="xliff"
   return 0
   ;;
  *)
   DOCTYPE="txt"  # Use "txt" for unrecognized types
   return 1
   ;;
 esac
 }

FULL_PATH_TO_SCRIPT="$(realpath "$0")"
SCRIPT_DIRECTORY="$(dirname "$FULL_PATH_TO_SCRIPT")"

# VENV detection and activation.
 function detect-venv   () {
  python -c 'if 1:#for indentation
  import sys
  ss=sys.prefix != sys.base_prefix
  # print("%s\n%s\n%s" % (sys.prefix,sys.base_prefix,ss) )
  if ss:
   sys.exit(0)
  sys.exit(1)
  '
  return $?
  }
 function activate-venv () {
  # shellcheck disable=SC1091
  source "$SCRIPT_DIRECTORY"/venv/bin/activate || true;
  detect-venv
  VENV=$?
  if [ $VENV = 1 ]; then
   echo "No venv available."
   echo "Need a venv and these python dependencies:"
   echo ""
   # If you have additional dependencies that are not
   # in the requirements.txt file, let the user know.
   sed 's/^/  /' "$SCRIPT_DIRECTORY"/requirements.txt
   echo ""
   echo ""
   read -rp "Do you want to create a python venv and install python dependencies (y/n) " response
   case ${response:0:1} in
    y|Y )
     echo "Creating venv..."
     python -m venv "$SCRIPT_DIRECTORY"/venv
     echo "Activating venv."
     # shellcheck disable=SC1091
     source "$SCRIPT_DIRECTORY"/venv/bin/activate
     # Sanity check, then Install python dependencies:
     detect-venv
     VENV=$?
     if [ $VENV = 1 ]; then
      echo "No active venv. Refusing to procede."
      return 1
      fi
     # You can put install instructions here
     # for additional dependencies that are not
     # in the requirements.txt file, like this:
     # "&& pip install pyLanguagetool \"
     : \
     && echo 'Installing python libraries.' \
     && python -m pip install -r "$SCRIPT_DIRECTORY"/requirements.txt \
     ;
     ;;
    n|N )
     echo "Ok, no action taken."
     ;;
    * )
     echo "Ok, no action taken."
     ;;
   esac
   unset response
   fi
  unset VENV
  }
 function run-app       () {
  # shellcheck disable=SC2078
  if [ "Run your program:" ]; then
   # Sanity check:
   detect-venv
   VENV=$?
   if [ $VENV = 1 ]; then
    echo "No active venv. Refusing to procede."
    return 1
    fi
   # Put your script logic here:

   # Check for -h or --help in the arguments
   for arg in "$@"; do
    if [[ "$arg" == "-h" || "$arg" == "--help" ]]; then
     echo "$USAGE"
     exit 0
    fi
   done
   
   # Initialize an empty array to hold the filtered arguments
   filtered_args=()

   # Loop through each argument passed to the script
   for arg in "$@"; do
    # Check if the argument does not start with a dash
    if [[ "$arg" != -* ]]; then
      # Add the argument to the filtered_args array
      filtered_args+=("$arg")
     fi
    done

   # Sets the DOCTYPE variable:
   determine_file_type "${filtered_args[0]}"
   # shellcheck disable=SC2181
   if [ $? -eq 0 ]; then
    echo "File type: $DOCTYPE"
   else
    echo "The input file either has no file extension"
    echo "or the file extension is unsupported."
    echo "Plain text will (possibly erroneously) be assumed."
    echo "Supported file types are:"
    echo "  txt,html,md,markdown,rst,ipynb,json,xliff"
    # Or you could refuse to proceed:
    # echo "Unknown file type: ${filtered_args[0]} "
    # return 1
   fi

   # start the container if it is not already running:
   docker start languagetool 
   # Note: the language type is hard-coded here:
   pylanguagetool -a http://localhost:8010/v2/ -l en-US -t "$DOCTYPE" "$@"
   fi
  }

# Call the functions:
activate-venv
run-app "$@"

So, yeah, I did this and it changed my life.