Skip to content

Commit

Permalink
Added short version for most commonly used command line options (#17486)
Browse files Browse the repository at this point in the history
Fixes #11644
Fixes #17485

Summary of the issue:
Most common command line options need a short version.
Command line options is not robust, leading to undesirable effects when restarting NVDA
Description of user facing changes
Short versions of two command line flags have been added: -d for --disable-addons and -n for --lang
Using incomplete command line flags or duplicated command line options should not produce unexpected effects when restarting NVDA.
Description of development approach
Added new flags
Check parsed app args rather than raw sys.argv to know the options that NVDA has actually taken into account.
Removed languageHandler.getLanguageCliArgs since we do not use this function anymore and it was not correctly working as expected for some corner case (duplicated flag, incomplete flag)
  • Loading branch information
CyrilleB79 authored Dec 13, 2024
1 parent 6d02759 commit db6458a
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 284 deletions.
272 changes: 272 additions & 0 deletions source/argsParsing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
# -*- coding: UTF-8 -*-
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2024 NV Access Limited, Cyrille Bougot
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import argparse
import sys
import winUser

from typing import IO


class _WideParserHelpFormatter(argparse.RawTextHelpFormatter):
def __init__(self, prog: str, indent_increment: int = 2, max_help_position: int = 50, width: int = 1000):
"""
A custom formatter for argparse help messages that uses a wider width.
:param prog: The program name.
:param indent_increment: The number of spaces to indent for each level of nesting.
:param max_help_position: The maximum starting column of the help text.
:param width: The width of the help text.
"""

super().__init__(prog, indent_increment, max_help_position, width)


class NoConsoleOptionParser(argparse.ArgumentParser):
"""
A commandline option parser that shows its messages using dialogs,
as this pyw file has no dos console window associated with it.
"""

def print_help(self, file: IO[str] | None = None):
"""Shows help in a standard Windows message dialog"""
winUser.MessageBox(0, self.format_help(), "Help", 0)

def error(self, message: str):
"""Shows an error in a standard Windows message dialog, and then exits NVDA"""
out = ""
out = self.format_usage()
out += f"\nerror: {message}"
winUser.MessageBox(0, out, "Command-line Argument Error", winUser.MB_ICONERROR)
sys.exit(2)


def stringToBool(string):
"""Wrapper for configobj.validate.is_boolean to raise the proper exception for wrong values."""
from configobj.validate import is_boolean, ValidateError

try:
return is_boolean(string)
except ValidateError as e:
raise argparse.ArgumentTypeError(e.message)


def stringToLang(value: str) -> str:
"""Perform basic case normalization for ease of use."""
import languageHandler

if value.casefold() == "Windows".casefold():
normalizedLang = "Windows"
else:
normalizedLang = languageHandler.normalizeLanguage(value)
possibleLangNames = languageHandler.listNVDALocales()
if normalizedLang is not None and normalizedLang in possibleLangNames:
return normalizedLang
raise argparse.ArgumentTypeError(f"Language code should be one of:\n{', '.join(possibleLangNames)}.")


_parser: NoConsoleOptionParser | None = None
"""The arguments parser used by NVDA.
"""


def _createNVDAArgParser() -> NoConsoleOptionParser:
"""Create a parser to process NVDA option arguments."""

parser = NoConsoleOptionParser(formatter_class=_WideParserHelpFormatter, allow_abbrev=False)
quitGroup = parser.add_mutually_exclusive_group()
quitGroup.add_argument(
"-q",
"--quit",
action="store_true",
dest="quit",
default=False,
help="Quit already running copy of NVDA",
)
parser.add_argument(
"-k",
"--check-running",
action="store_true",
dest="check_running",
default=False,
help="Report whether NVDA is running via the exit code; 0 if running, 1 if not running",
)
parser.add_argument(
"-f",
"--log-file",
dest="logFileName",
type=str,
help="The file to which log messages should be written.\n"
'Default destination is "%%TEMP%%\\nvda.log".\n'
"Logging is always disabled if secure mode is enabled.\n",
)
parser.add_argument(
"-l",
"--log-level",
dest="logLevel",
type=int,
default=0, # 0 means unspecified in command line.
choices=[10, 12, 15, 20, 100],
help="The lowest level of message logged (debug 10, input/output 12, debugwarning 15, info 20, off 100).\n"
"Default value is 20 (info) or the user configured setting.\n"
"Logging is always disabled if secure mode is enabled.\n",
)
parser.add_argument(
"-c",
"--config-path",
dest="configPath",
default=None,
type=str,
help="The path where all settings for NVDA are stored.\n"
"The default value is forced if secure mode is enabled.\n",
)
parser.add_argument(
"-n",
"--lang",
dest="language",
default=None,
type=stringToLang,
help=(
"Override the configured NVDA language.\n"
'Set to "Windows" for current user default, "en" for English, etc.'
),
)
parser.add_argument(
"-m",
"--minimal",
action="store_true",
dest="minimal",
default=False,
help="No sounds, no interface, no start message etc",
)
# --secure is used to force secure mode.
# Documented in the userGuide in #SecureMode.
parser.add_argument(
"-s",
"--secure",
action="store_true",
dest="secure",
default=False,
help="Starts NVDA in secure mode",
)
parser.add_argument(
"-d",
"--disable-addons",
action="store_true",
dest="disableAddons",
default=False,
help="Disable all add-ons",
)
parser.add_argument(
"--debug-logging",
action="store_true",
dest="debugLogging",
default=False,
help="Enable debug level logging just for this run.\n"
"This setting will override any other log level (--loglevel, -l) argument given, "
"as well as no logging option.",
)
parser.add_argument(
"--no-logging",
action="store_true",
dest="noLogging",
default=False,
help="Disable logging completely for this run.\n"
"This setting can be overwritten with other log level (--loglevel, -l) "
"switch or if debug logging is specified.",
)
parser.add_argument(
"--no-sr-flag",
action="store_false",
dest="changeScreenReaderFlag",
default=True,
help="Don't change the global system screen reader flag",
)
installGroup = parser.add_mutually_exclusive_group()
installGroup.add_argument(
"--install",
action="store_true",
dest="install",
default=False,
help="Installs NVDA (starting the new copy after installation)",
)
installGroup.add_argument(
"--install-silent",
action="store_true",
dest="installSilent",
default=False,
help="Installs NVDA silently (does not start the new copy after installation).",
)
installGroup.add_argument(
"--create-portable",
action="store_true",
dest="createPortable",
default=False,
help="Creates a portable copy of NVDA (and starts the new copy).\n"
"Requires `--portable-path` to be specified.\n",
)
installGroup.add_argument(
"--create-portable-silent",
action="store_true",
dest="createPortableSilent",
default=False,
help="Creates a portable copy of NVDA (without starting the new copy).\n"
"This option suppresses warnings when writing to non-empty directories "
"and may overwrite files without warning.\n"
"Requires --portable-path to be specified.\n",
)
parser.add_argument(
"--portable-path",
dest="portablePath",
default=None,
type=str,
help="The path where a portable copy will be created",
)
parser.add_argument(
"--launcher",
action="store_true",
dest="launcher",
default=False,
help="Started from the launcher",
)
parser.add_argument(
"--enable-start-on-logon",
metavar="True|False",
type=stringToBool,
dest="enableStartOnLogon",
default=None,
help="When installing, enable NVDA's start on the logon screen",
)
parser.add_argument(
"--copy-portable-config",
action="store_true",
dest="copyPortableConfig",
default=False,
help=(
"When installing, copy the portable configuration "
"from the provided path (--config-path, -c) to the current user account"
),
)
# This option is passed by Ease of Access so that if someone downgrades without uninstalling
# (despite our discouragement), the downgraded copy won't be started in non-secure mode on secure desktops.
# (Older versions always required the --secure option to start in secure mode.)
# If this occurs, the user will see an obscure error,
# but that's far better than a major security hazzard.
# If this option is provided, NVDA will not replace an already running instance (#10179)
parser.add_argument(
"--ease-of-access",
action="store_true",
dest="easeOfAccess",
default=False,
help="Started by Windows Ease of Access",
)
return parser


def getParser() -> NoConsoleOptionParser:
global _parser
if not _parser:
_parser = _createNVDAArgParser()
return _parser
42 changes: 31 additions & 11 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import logHandler
import languageHandler
import globalVars
import argsParsing
from logHandler import log
import addonHandler
import extensionPoints
Expand Down Expand Up @@ -214,6 +215,28 @@ class NewNVDAInstance:
directory: Optional[str] = None


def computeRestartCLIArgs(removeArgsList: list[str] | None = None) -> list[str]:
"""Generate an equivalent list of CLI arguments from the values in globalVars.appArgs.
:param removeArgsList: A list of values to ignore when looking in globalVars.appArgs.
"""

parser = argsParsing.getParser()
if not removeArgsList:
removeArgsList = []
args = []
for arg, val in globalVars.appArgs._get_kwargs():
if val == parser.get_default(arg):
continue
if arg in removeArgsList:
continue
flag = [a.option_strings[0] for a in parser._actions if a.dest == arg][0]
args.append(flag)
if isinstance(val, bool):
continue
args.append(f"{val}")
return args


def restartUnsafely():
"""Start a new copy of NVDA immediately.
Used as a last resort, in the event of a serious error to immediately restart NVDA without running any
Expand All @@ -239,13 +262,16 @@ def restartUnsafely():
sys.argv.remove(paramToRemove)
except ValueError:
pass
restartCLIArgs = computeRestartCLIArgs(
removeArgsList=["easeOfAccess"],
)
options = []
if NVDAState.isRunningAsSource():
options.append(os.path.basename(sys.argv[0]))
_startNewInstance(
NewNVDAInstance(
sys.executable,
subprocess.list2cmdline(options + sys.argv[1:]),
subprocess.list2cmdline(options + restartCLIArgs),
globalVars.appDir,
),
)
Expand All @@ -260,15 +286,9 @@ def restart(disableAddons=False, debugLogging=False):
return
import subprocess

for paramToRemove in (
"--disable-addons",
"--debug-logging",
"--ease-of-access",
) + languageHandler.getLanguageCliArgs():
try:
sys.argv.remove(paramToRemove)
except ValueError:
pass
restartCLIArgs = computeRestartCLIArgs(
removeArgsList=["disableAddons", "debugLogging", "language", "easeOfAccess"],
)
options = []
if NVDAState.isRunningAsSource():
options.append(os.path.basename(sys.argv[0]))
Expand All @@ -280,7 +300,7 @@ def restart(disableAddons=False, debugLogging=False):
if not triggerNVDAExit(
NewNVDAInstance(
sys.executable,
subprocess.list2cmdline(options + sys.argv[1:]),
subprocess.list2cmdline(options + restartCLIArgs),
globalVars.appDir,
),
):
Expand Down
4 changes: 2 additions & 2 deletions source/globalVars.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2022 NV Access Limited, Łukasz Golonka, Leonard de Ruijter, Babbage B.V.,
# Copyright (C) 2006-2024 NV Access Limited, Łukasz Golonka, Leonard de Ruijter, Babbage B.V.,
# Aleksey Sadovoy, Peter Vágner
# This file may be used under the terms of the GNU General Public License, version 2 or later.
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
Expand Down Expand Up @@ -41,7 +41,7 @@ class DefaultAppArgs(argparse.Namespace):
logFileName: Optional[os.PathLike] = ""
logLevel: int = 0
configPath: Optional[os.PathLike] = None
language: str = "en"
language: str | None = None
minimal: bool = False
secure: bool = False
"""
Expand Down
13 changes: 3 additions & 10 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,16 +800,9 @@ def makeSettings(self, settingsSizer):
self.languageNames = languageHandler.getAvailableLanguages(presentational=True)
languageChoices = [x[1] for x in self.languageNames]
if languageHandler.isLanguageForced():
try:
cmdLangDescription = next(
ld for code, ld in self.languageNames if code == globalVars.appArgs.language
)
except StopIteration:
# In case --lang=Windows is passed to the command line, globalVars.appArgs.language is the current
# Windows language,, which may not be in the list of NVDA supported languages, e.g. Windows language may
# be 'fr_FR' but NVDA only supports 'fr'.
# In this situation, only use language code as a description.
cmdLangDescription = globalVars.appArgs.language
cmdLangDescription = next(
ld for code, ld in self.languageNames if code == globalVars.appArgs.language
)
languageChoices.append(
# Translators: Shown for a language which has been provided from the command line
# 'langDesc' would be replaced with description of the given locale.
Expand Down
Loading

0 comments on commit db6458a

Please sign in to comment.