scripts: runner: use arguments, not environment vars

The various runners (flash/debug scripts) use environment variables to
take arguments. This is legacy behavior which is not desirable.

Use command line arguments instead.

Note: this leaves more general environment variables with publicly
      documented behavior in place for now, for compatibility, e.g.:

      ZEPHYR_FLASH_OVER_DFU, OPENSDA_FW, ESP_IDF_PATH, PYOCD_DAPARG

For example, when using dfu-util to flash arduino_101, instead of
setting DFUUTIL_PID, DFUUTIL_ALT, and DFUUTIL_IMG environment
variables, have the script invocation look like this:

  python3 .../zephyr_flash_debug.py dfu-util flash \
          [common arguments omitted] \
          --pid=8087:0aba --alt=x86_app \
          --img=.../build/zephyr/zephyr.bin

Make similar changes for other runners (openocd, etc.) and
targets (debug, debugserver).

To implement this in the scripts:

  - have the individual scripts/support/runner/some-runner.py files
    register their own command line arguments

  - teach them to construct instances from arguments, not the
    environment

  - have zephyr_flash_debug.py request runners to register command
    line argument parsers, and handle arguments

In the build system:

  - add a new board_runner_args() extension function that board.cmake
    files can use to add to the zephyr_flash_debug.py command line

  - adjust cmake/flash/CMakeLists.txt to invoke with arguments

  - add new helper include files for each runner (like
    boards/common/dfu-util.board.cmake, etc.), which add default
    options as needed and then add on overrides from
    board_runner_args() calls

  - update board.cmake files to use the new includes and extension

This implied some tweaking when using openocd to make the CMake string
escaping and unescaping work properly.

Signed-off-by: Marti Bolivar <marti@opensourcefoundries.com>
diff --git a/boards/arc/arduino_101_sss/board.cmake b/boards/arc/arduino_101_sss/board.cmake
index b6b06e8..59a632d 100644
--- a/boards/arc/arduino_101_sss/board.cmake
+++ b/boards/arc/arduino_101_sss/board.cmake
@@ -1,29 +1,14 @@
 if(DEFINED ENV{ZEPHYR_FLASH_OVER_DFU})
   set(BOARD_FLASH_RUNNER dfu-util)
-
-  set(DFUUTIL_PID 8087:0aba)
-  set(DFUUTIL_ALT sensor_core)
-  set(DFUUTIL_IMG ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME})
-
-  set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-    DFUUTIL_PID
-    DFUUTIL_ALT
-    DFUUTIL_IMG
-    )
 else()
   set(BOARD_FLASH_RUNNER openocd)
 endif()
 
 set(BOARD_DEBUG_RUNNER openocd)
 
-set(OPENOCD_PRE_CMD "targets 1")
-set(OPENOCD_LOAD_CMD "load_image     ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-set(OPENOCD_VERIFY_CMD "verify_image ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-set(GDB_PORT 3334)
+board_runner_args(dfu-util "--pid=8087:0aba" "--alt=sensor_core")
+set(PRE_LOAD targets 1)
+board_runner_args(openocd "--cmd-pre-load=\"${PRE_LOAD}\"" "--gdb-port=3334")
 
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_PRE_CMD
-  OPENOCD_LOAD_CMD
-  OPENOCD_VERIFY_CMD
-  GDB_PORT
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/dfu-util.board.cmake)
+include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
diff --git a/boards/arc/em_starterkit/board.cmake b/boards/arc/em_starterkit/board.cmake
index 8af72db..02525f2 100644
--- a/boards/arc/em_starterkit/board.cmake
+++ b/boards/arc/em_starterkit/board.cmake
@@ -1,10 +1,4 @@
+# TODO: can this board just use the usual openocd runner?
 set(BOARD_FLASH_RUNNER em-starterkit)
 set(BOARD_DEBUG_RUNNER em-starterkit)
-
-set(OPENOCD_LOAD_CMD "load_image     ${PROJECT_BINARY_DIR}/${KERNEL_ELF_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-set(OPENOCD_VERIFY_CMD "verify_image ${PROJECT_BINARY_DIR}/${KERNEL_ELF_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_LOAD_CMD
-  OPENOCD_VERIFY_CMD
-  )
+board_finalize_runner_args(em-starterkit)
diff --git a/boards/arc/panther_ss/board.cmake b/boards/arc/panther_ss/board.cmake
index 09a364c..6575ce1 100644
--- a/boards/arc/panther_ss/board.cmake
+++ b/boards/arc/panther_ss/board.cmake
@@ -1,7 +1,4 @@
+set(PRE_LOAD targets 1)
+board_runner_args(openocd "--cmd-pre-load=\"${PRE_LOAD}\"")
+set(OPENOCD_USE_LOAD_IMAGE NO)
 include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
-
-set(OPENOCD_PRE_CMD "targets 1")
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_PRE_CMD
-  )
diff --git a/boards/arc/quark_se_c1000_ss_devboard/board.cmake b/boards/arc/quark_se_c1000_ss_devboard/board.cmake
index 64a3fc1..65010c0 100644
--- a/boards/arc/quark_se_c1000_ss_devboard/board.cmake
+++ b/boards/arc/quark_se_c1000_ss_devboard/board.cmake
@@ -1,8 +1,3 @@
+set(PRE_LOAD targets 1)
+board_runner_args(openocd "--cmd-pre-load=\"${PRE_LOAD}\"")
 include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
-
-set(OPENOCD_PRE_CMD "targets 1")
-set(OPENOCD_LOAD_CMD "load_image     ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_PRE_CMD
-  )
diff --git a/boards/arm/96b_carbon/board.cmake b/boards/arm/96b_carbon/board.cmake
index 055fbc9..ca9346e 100644
--- a/boards/arm/96b_carbon/board.cmake
+++ b/boards/arm/96b_carbon/board.cmake
@@ -1,13 +1,4 @@
-set(BOARD_FLASH_RUNNER dfu-util)
+board_runner_args(dfu-util "--pid=0483:df11" "--alt=0")
+board_runner_args(dfu-util "--dfuse-addr=${CONFIG_FLASH_BASE_ADDRESS}")
 
-set(DFUUTIL_PID 0483:df11)
-set(DFUUTIL_ALT 0)
-set(DFUUTIL_IMG ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME})
-set(DFUUTIL_DFUSE_ADDR ${CONFIG_FLASH_BASE_ADDRESS})
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  DFUUTIL_PID
-  DFUUTIL_ALT
-  DFUUTIL_IMG
-  DFUUTIL_DFUSE_ADDR
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/dfu-util.board.cmake)
diff --git a/boards/arm/96b_nitrogen/board.cmake b/boards/arm/96b_nitrogen/board.cmake
index 419d19a..5c39635 100644
--- a/boards/arm/96b_nitrogen/board.cmake
+++ b/boards/arm/96b_nitrogen/board.cmake
@@ -1,8 +1,2 @@
-set(BOARD_FLASH_RUNNER pyocd)
-set(BOARD_DEBUG_RUNNER pyocd)
-
-set(PYOCD_TARGET nrf52)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  PYOCD_TARGET
-  )
+board_runner_args(pyocd "--target=nrf52")
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
diff --git a/boards/arm/arduino_101_ble/board.cmake b/boards/arm/arduino_101_ble/board.cmake
index 73aef2b..3c4ba4a 100644
--- a/boards/arm/arduino_101_ble/board.cmake
+++ b/boards/arm/arduino_101_ble/board.cmake
@@ -1,13 +1,2 @@
-if(DEFINED ENV{ZEPHYR_FLASH_OVER_DFU})
-  set(BOARD_FLASH_RUNNER dfu-util)
-
-  set(DFUUTIL_PID 8087:0aba)
-  set(DFUUTIL_ALT ble_core)
-  set(DFUUTIL_IMG ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME})
-
-  set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-    DFUUTIL_PID
-    DFUUTIL_ALT
-    DFUUTIL_IMG
-    )
-endif()
+board_runner_args(dfu-util "--pid=8087:0aba" "--alt=ble_core")
+include($ENV{ZEPHYR_BASE}/boards/common/dfu-util.board.cmake)
diff --git a/boards/arm/arduino_due/board.cmake b/boards/arm/arduino_due/board.cmake
index 264e10d..8e7c172 100644
--- a/boards/arm/arduino_due/board.cmake
+++ b/boards/arm/arduino_due/board.cmake
@@ -1 +1 @@
-set(BOARD_FLASH_RUNNER bossac)
+include($ENV{ZEPHYR_BASE}/boards/common/bossac.board.cmake)
diff --git a/boards/arm/bbc_microbit/board.cmake b/boards/arm/bbc_microbit/board.cmake
index 15a2fb0..eeb7c32 100644
--- a/boards/arm/bbc_microbit/board.cmake
+++ b/boards/arm/bbc_microbit/board.cmake
@@ -1,8 +1,2 @@
-set(BOARD_FLASH_RUNNER pyocd)
-set(BOARD_DEBUG_RUNNER pyocd)
-
-set(PYOCD_TARGET nrf51)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  PYOCD_TARGET
-  )
+board_runner_args(pyocd "--target=nrf51")
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
diff --git a/boards/arm/frdm_k64f/board.cmake b/boards/arm/frdm_k64f/board.cmake
index bde2ef4..ac4f05e 100644
--- a/boards/arm/frdm_k64f/board.cmake
+++ b/boards/arm/frdm_k64f/board.cmake
@@ -7,14 +7,9 @@
   set_ifndef(BOARD_FLASH_RUNNER pyocd)
 endif()
 
-set(JLINK_DEVICE MK64FN1M0xxx12)
-set(PYOCD_TARGET k64f)
-set(OPENOCD_LOAD_CMD "flash write_image erase ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-set(OPENOCD_VERIFY_CMD "verify_image          ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
+board_runner_args(jlink "--device=MK64FN1M0xxx12")
+board_runner_args(pyocd "--target=k64f")
 
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  JLINK_DEVICE
-  PYOCD_TARGET
-  OPENOCD_LOAD_CMD
-  OPENOCD_VERIFY_CMD
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
+include($ENV{ZEPHYR_BASE}/boards/common/jlink.board.cmake)
+include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
diff --git a/boards/arm/frdm_kl25z/board.cmake b/boards/arm/frdm_kl25z/board.cmake
index e794ba4..a3fe31a 100644
--- a/boards/arm/frdm_kl25z/board.cmake
+++ b/boards/arm/frdm_kl25z/board.cmake
@@ -7,10 +7,8 @@
   set_ifndef(BOARD_FLASH_RUNNER pyocd)
 endif()
 
-set(JLINK_DEVICE MKL25Z128xxx4)
-set(PYOCD_TARGET kl25z)
+board_runner_args(jlink "--device=MKL25Z128xxx4")
+board_runner_args(pyocd "--target=kl25z")
 
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  JLINK_DEVICE
-  PYOCD_TARGET
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
+include($ENV{ZEPHYR_BASE}/boards/common/jlink.board.cmake)
diff --git a/boards/arm/frdm_kw41z/board.cmake b/boards/arm/frdm_kw41z/board.cmake
index 0d8876c..19e83a6 100644
--- a/boards/arm/frdm_kw41z/board.cmake
+++ b/boards/arm/frdm_kw41z/board.cmake
@@ -7,10 +7,8 @@
   set_ifndef(BOARD_FLASH_RUNNER pyocd)
 endif()
 
-set(JLINK_DEVICE MKW41Z512xxx4)
-set(PYOCD_TARGET kw41z4)
+board_runner_args(jlink "--device=MKW41Z512xxx4")
+board_runner_args(pyocd "--target=kw41z4")
 
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  JLINK_DEVICE
-  PYOCD_TARGET
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/jlink.board.cmake)
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
diff --git a/boards/arm/hexiwear_k64/board.cmake b/boards/arm/hexiwear_k64/board.cmake
index 2d4b0d2..7e56df6 100644
--- a/boards/arm/hexiwear_k64/board.cmake
+++ b/boards/arm/hexiwear_k64/board.cmake
@@ -7,10 +7,8 @@
   set_ifndef(BOARD_FLASH_RUNNER pyocd)
 endif()
 
-set(JLINK_DEVICE MK64FN1M0xxx12)
-set(PYOCD_TARGET k64f)
+board_runner_args(pyocd "--target=k64f")
+board_runner_args(jlink "--device=MK64FN1M0xxx12")
 
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  JLINK_DEVICE
-  PYOCD_TARGET
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
+include($ENV{ZEPHYR_BASE}/boards/common/jlink.board.cmake)
diff --git a/boards/arm/hexiwear_kw40z/board.cmake b/boards/arm/hexiwear_kw40z/board.cmake
index bff8044..1981474 100644
--- a/boards/arm/hexiwear_kw40z/board.cmake
+++ b/boards/arm/hexiwear_kw40z/board.cmake
@@ -7,10 +7,8 @@
   set_ifndef(BOARD_FLASH_RUNNER pyocd)
 endif()
 
-set(JLINK_DEVICE MKW40Z160xxx4)
-set(PYOCD_TARGET kw40z4)
+board_runner_args(jlink "--device=MKW40Z160xxx4")
+board_runner_args(pyocd "--target=kw40z4")
 
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  JLINK_DEVICE
-  PYOCD_TARGET
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/jlink.board.cmake)
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
diff --git a/boards/arm/mimxrt1050_evk/board.cmake b/boards/arm/mimxrt1050_evk/board.cmake
index 8e6f095..ae020d5 100644
--- a/boards/arm/mimxrt1050_evk/board.cmake
+++ b/boards/arm/mimxrt1050_evk/board.cmake
@@ -4,14 +4,5 @@
 # SPDX-License-Identifier: Apache-2.0
 #
 
-set_ifndef(OPENSDA_FW jlink)
-
-if(OPENSDA_FW STREQUAL jlink)
-  set_ifndef(BOARD_DEBUG_RUNNER jlink)
-endif()
-
-set(JLINK_DEVICE Cortex-M7)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  JLINK_DEVICE
-  )
+board_runner_args(jlink "--device=Cortex-M7")
+include($ENV{ZEPHYR_BASE}/boards/common/jlink.board.cmake)
diff --git a/boards/arm/nrf51_pca10028/board.cmake b/boards/arm/nrf51_pca10028/board.cmake
index 76f17e0..c360db6 100644
--- a/boards/arm/nrf51_pca10028/board.cmake
+++ b/boards/arm/nrf51_pca10028/board.cmake
@@ -1,7 +1,2 @@
-set(BOARD_FLASH_RUNNER nrfjprog)
-
-set(NRF_FAMILY NRF51)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  NRF_FAMILY
-  )
+board_runner_args(nrfjprog "--nrf-family=NRF51")
+include($ENV{ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
diff --git a/boards/arm/nrf51_vbluno51/board.cmake b/boards/arm/nrf51_vbluno51/board.cmake
index 15a2fb0..eeb7c32 100644
--- a/boards/arm/nrf51_vbluno51/board.cmake
+++ b/boards/arm/nrf51_vbluno51/board.cmake
@@ -1,8 +1,2 @@
-set(BOARD_FLASH_RUNNER pyocd)
-set(BOARD_DEBUG_RUNNER pyocd)
-
-set(PYOCD_TARGET nrf51)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  PYOCD_TARGET
-  )
+board_runner_args(pyocd "--target=nrf51")
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
diff --git a/boards/arm/nrf52840_pca10056/board.cmake b/boards/arm/nrf52840_pca10056/board.cmake
index 623dacf..a494475 100644
--- a/boards/arm/nrf52840_pca10056/board.cmake
+++ b/boards/arm/nrf52840_pca10056/board.cmake
@@ -1,6 +1,2 @@
-set(BOARD_FLASH_RUNNER nrfjprog)
-set(NRF_FAMILY NRF52)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  NRF_FAMILY
-  )
+board_runner_args(nrfjprog "--nrf-family=NRF52")
+include($ENV{ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
diff --git a/boards/arm/nrf52_blenano2/board.cmake b/boards/arm/nrf52_blenano2/board.cmake
index 419d19a..5c39635 100644
--- a/boards/arm/nrf52_blenano2/board.cmake
+++ b/boards/arm/nrf52_blenano2/board.cmake
@@ -1,8 +1,2 @@
-set(BOARD_FLASH_RUNNER pyocd)
-set(BOARD_DEBUG_RUNNER pyocd)
-
-set(PYOCD_TARGET nrf52)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  PYOCD_TARGET
-  )
+board_runner_args(pyocd "--target=nrf52")
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
diff --git a/boards/arm/nrf52_pca10040/board.cmake b/boards/arm/nrf52_pca10040/board.cmake
index 0130656..a494475 100644
--- a/boards/arm/nrf52_pca10040/board.cmake
+++ b/boards/arm/nrf52_pca10040/board.cmake
@@ -1,7 +1,2 @@
-set(BOARD_FLASH_RUNNER nrfjprog)
-
-set(NRF_FAMILY NRF52)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  NRF_FAMILY
-  )
+board_runner_args(nrfjprog "--nrf-family=NRF52")
+include($ENV{ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
diff --git a/boards/arm/nrf52_vbluno52/board.cmake b/boards/arm/nrf52_vbluno52/board.cmake
index 419d19a..5c39635 100644
--- a/boards/arm/nrf52_vbluno52/board.cmake
+++ b/boards/arm/nrf52_vbluno52/board.cmake
@@ -1,8 +1,2 @@
-set(BOARD_FLASH_RUNNER pyocd)
-set(BOARD_DEBUG_RUNNER pyocd)
-
-set(PYOCD_TARGET nrf52)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  PYOCD_TARGET
-  )
+board_runner_args(pyocd "--target=nrf52")
+include($ENV{ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
diff --git a/boards/arm/sam_e70_xplained/board.cmake b/boards/arm/sam_e70_xplained/board.cmake
index a888467..fb4b381 100644
--- a/boards/arm/sam_e70_xplained/board.cmake
+++ b/boards/arm/sam_e70_xplained/board.cmake
@@ -1,7 +1,3 @@
+set(POST_VERIFY atsamv gpnvm set 1)
+board_runner_args(openocd "--cmd-post-verify=\"${POST_VERIFY}\"")
 include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
-
-set(OPENOCD_POST_CMD "atsamv gpnvm set 1")
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_POST_CMD
-  )
diff --git a/boards/arm/usb_kw24d512/board.cmake b/boards/arm/usb_kw24d512/board.cmake
index bf1424e..fe85ec4 100644
--- a/boards/arm/usb_kw24d512/board.cmake
+++ b/boards/arm/usb_kw24d512/board.cmake
@@ -1,7 +1,3 @@
-set(BOARD_DEBUG_RUNNER jlink)
+board_runner_args(jlink "--device=MKW24D512xxx5")
 
-set(JLINK_DEVICE MKW24D512xxx5)
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  JLINK_DEVICE
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/jlink.board.cmake)
diff --git a/boards/common/bossac.board.cmake b/boards/common/bossac.board.cmake
new file mode 100644
index 0000000..6c4cad0
--- /dev/null
+++ b/boards/common/bossac.board.cmake
@@ -0,0 +1,2 @@
+set_ifndef(BOARD_FLASH_RUNNER bossac)
+board_finalize_runner_args(bossac) # No default arguments to provide.
diff --git a/boards/common/dfu-util.board.cmake b/boards/common/dfu-util.board.cmake
new file mode 100644
index 0000000..f41cf3a
--- /dev/null
+++ b/boards/common/dfu-util.board.cmake
@@ -0,0 +1,2 @@
+set_ifndef(BOARD_FLASH_RUNNER dfu-util)
+board_finalize_runner_args(dfu-util) # No default arguments to provide.
diff --git a/boards/common/esp32.board.cmake b/boards/common/esp32.board.cmake
new file mode 100644
index 0000000..81b2e81
--- /dev/null
+++ b/boards/common/esp32.board.cmake
@@ -0,0 +1,12 @@
+set(BOARD_FLASH_RUNNER esp32)
+
+if(NOT DEFINED ESP_IDF_PATH)
+  if($ENV{ESP_IDF_PATH})
+    message(WARNING "Setting ESP_IDF_PATH in the environment is deprecated. Use cmake -DESP_IDF_PATH=... instead.")
+    set(ESP_IDF_PATH $ENV{ESP_IDF_PATH})
+  endif()
+endif()
+
+assert(ESP_IDF_PATH "ESP_IDF_PATH is not set")
+
+board_finalize_runner_args(esp32 "--esp-idf-path=${ESP_IDF_PATH}")
diff --git a/boards/common/jlink.board.cmake b/boards/common/jlink.board.cmake
new file mode 100644
index 0000000..cb3f5ff
--- /dev/null
+++ b/boards/common/jlink.board.cmake
@@ -0,0 +1,2 @@
+set_ifndef(BOARD_DEBUG_RUNNER jlink)
+board_finalize_runner_args(jlink) # No default arguments to provide.
diff --git a/boards/common/nios2.board.cmake b/boards/common/nios2.board.cmake
new file mode 100644
index 0000000..dec5ee8
--- /dev/null
+++ b/boards/common/nios2.board.cmake
@@ -0,0 +1,7 @@
+set_ifndef(BOARD_FLASH_RUNNER nios2)
+set_ifndef(BOARD_DEBUG_RUNNER nios2)
+
+board_finalize_runner_args(nios2
+  # TODO: merge this script into nios2.py
+  "--quartus-flash=$ENV{ZEPHYR_BASE}/scripts/support/quartus-flash.py"
+  )
diff --git a/boards/common/nrfjprog.board.cmake b/boards/common/nrfjprog.board.cmake
new file mode 100644
index 0000000..f686896
--- /dev/null
+++ b/boards/common/nrfjprog.board.cmake
@@ -0,0 +1,2 @@
+set_ifndef(BOARD_FLASH_RUNNER nrfjprog)
+board_finalize_runner_args(nrfjprog) # No default arguments to provide.
diff --git a/boards/common/openocd.board.cmake b/boards/common/openocd.board.cmake
index b2a31b2..6ea17fb 100644
--- a/boards/common/openocd.board.cmake
+++ b/boards/common/openocd.board.cmake
@@ -1,10 +1,31 @@
-set(BOARD_FLASH_RUNNER openocd)
-set(BOARD_DEBUG_RUNNER openocd)
+set_ifndef(BOARD_FLASH_RUNNER openocd)
+set_ifndef(BOARD_DEBUG_RUNNER openocd)
 
-set(OPENOCD_LOAD_CMD   "flash write_image erase ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-set(OPENOCD_VERIFY_CMD "verify_image            ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
+# "load_image" or "flash write_image erase"?
+if(CONFIG_X86 OR CONFIG_ARC)
+  set_ifndef(OPENOCD_USE_LOAD_IMAGE YES)
+endif()
+if(OPENOCD_USE_LOAD_IMAGE)
+  set_ifndef(OPENOCD_FLASH load_image)
+else()
+  set_ifndef(OPENOCD_FLASH flash write_image erase)
+endif()
 
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_LOAD_CMD
-  OPENOCD_VERIFY_CMD
+# zephyr.bin, or something else?
+set_ifndef(OPENOCD_IMAGE "${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME}")
+
+# CONFIG_FLASH_BASE_ADDRESS, or something else?
+if(NOT DEFINED OPENOCD_ADDRESS)
+  # This can't use set_ifndef() because CONFIG_FLASH_BASE_ADDRESS is
+  # the empty string on some targets, which causes set_ifndef() to
+  # choke.
+  set(OPENOCD_ADDRESS "${CONFIG_FLASH_BASE_ADDRESS}")
+endif()
+
+set(OPENOCD_CMD_LOAD_DEFAULT ${OPENOCD_FLASH} ${OPENOCD_IMAGE} ${OPENOCD_ADDRESS})
+set(OPENOCD_CMD_VERIFY_DEFAULT verify_image ${OPENOCD_IMAGE} ${OPENOCD_ADDRESS})
+
+board_finalize_runner_args(openocd
+  "--cmd-load=\"${OPENOCD_CMD_LOAD_DEFAULT}\""
+  "--cmd-verify=\"${OPENOCD_CMD_VERIFY_DEFAULT}\""
   )
diff --git a/boards/common/pyocd.board.cmake b/boards/common/pyocd.board.cmake
new file mode 100644
index 0000000..9a2f45f
--- /dev/null
+++ b/boards/common/pyocd.board.cmake
@@ -0,0 +1,3 @@
+set_ifndef(BOARD_FLASH_RUNNER pyocd)
+set_ifndef(BOARD_DEBUG_RUNNER pyocd)
+board_finalize_runner_args(pyocd) # No default arguments to provide.
diff --git a/boards/nios2/altera_max10/board.cmake b/boards/nios2/altera_max10/board.cmake
index ad4d1d6..266bc0d 100644
--- a/boards/nios2/altera_max10/board.cmake
+++ b/boards/nios2/altera_max10/board.cmake
@@ -1,4 +1,2 @@
-set(BOARD_FLASH_RUNNER nios2)
-set(BOARD_DEBUG_RUNNER nios2)
-set(NIOS2_CPU_SOF $ENV{ZEPHYR_BASE}/arch/nios2/soc/nios2f-zephyr/cpu/ghrd_10m50da.sof)
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS NIOS2_CPU_SOF)
+board_runner_args(nios2 "--cpu-sof=$ENV{ZEPHYR_BASE}/arch/nios2/soc/nios2f-zephyr/cpu/ghrd_10m50da.sof")
+include($ENV{ZEPHYR_BASE}/boards/common/nios2.board.cmake)
diff --git a/boards/x86/arduino_101/board.cmake b/boards/x86/arduino_101/board.cmake
index 98ace6b..6f3ddab 100644
--- a/boards/x86/arduino_101/board.cmake
+++ b/boards/x86/arduino_101/board.cmake
@@ -1,27 +1,12 @@
 if(DEFINED ENV{ZEPHYR_FLASH_OVER_DFU})
   set(BOARD_FLASH_RUNNER dfu-util)
-
-  set(DFUUTIL_PID 8087:0aba)
-  set(DFUUTIL_ALT x86_app)
-  set(DFUUTIL_IMG ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME})
-
-  set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-    DFUUTIL_PID
-    DFUUTIL_ALT
-    DFUUTIL_IMG
-    )
-else()
-  set(BOARD_FLASH_RUNNER openocd)
 endif()
 
 set(BOARD_DEBUG_RUNNER openocd)
 
-set(OPENOCD_PRE_CMD "targets 1")
-set(OPENOCD_LOAD_CMD "load_image     ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-set(OPENOCD_VERIFY_CMD "verify_image ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
+board_runner_args(dfu-util "--pid=8087:0aba" "--alt=x86_app")
+set(PRE_LOAD targets 1)
+board_runner_args(openocd "--cmd-pre-load=\"${PRE_LOAD}\"")
 
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_PRE_CMD
-  OPENOCD_LOAD_CMD
-  OPENOCD_VERIFY_CMD
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
+include($ENV{ZEPHYR_BASE}/boards/common/dfu-util.board.cmake)
diff --git a/boards/x86/panther/board.cmake b/boards/x86/panther/board.cmake
index e21d41e..1496ef4 100644
--- a/boards/x86/panther/board.cmake
+++ b/boards/x86/panther/board.cmake
@@ -1,12 +1,4 @@
-set(BOARD_FLASH_RUNNER openocd)
-set(BOARD_DEBUG_RUNNER openocd)
-
-set(OPENOCD_PRE_CMD "targets 1")
-set(OPENOCD_LOAD_CMD "load_image     ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_PHYS_LOAD_ADDR}")
-set(OPENOCD_VERIFY_CMD "verify_image ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_PHYS_LOAD_ADDR}")
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_PRE_CMD
-  OPENOCD_LOAD_CMD
-  OPENOCD_VERIFY_CMD
-  )
+set(PRE_LOAD targets 1)
+board_runner_args(openocd "--cmd-pre-load=\"${PRE_LOAD}\"")
+set(OPENOCD_ADDRESS ${CONFIG_PHYS_LOAD_ADDR})
+include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
diff --git a/boards/x86/quark_d2000_crb/board.cmake b/boards/x86/quark_d2000_crb/board.cmake
index 682155a..2b06911 100644
--- a/boards/x86/quark_d2000_crb/board.cmake
+++ b/boards/x86/quark_d2000_crb/board.cmake
@@ -1,7 +1 @@
 include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
-
-set(OPENOCD_LOAD_CMD "load_image     ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_LOAD_CMD
-  )
diff --git a/boards/x86/quark_se_c1000_devboard/board.cmake b/boards/x86/quark_se_c1000_devboard/board.cmake
index 729f163..65010c0 100644
--- a/boards/x86/quark_se_c1000_devboard/board.cmake
+++ b/boards/x86/quark_se_c1000_devboard/board.cmake
@@ -1,9 +1,3 @@
+set(PRE_LOAD targets 1)
+board_runner_args(openocd "--cmd-pre-load=\"${PRE_LOAD}\"")
 include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
-
-set(OPENOCD_PRE_CMD "targets 1")
-set(OPENOCD_LOAD_CMD "load_image     ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_PRE_CMD
-  OPENOCD_LOAD_CMD
-  )
diff --git a/boards/x86/tinytile/board.cmake b/boards/x86/tinytile/board.cmake
index 98ace6b..6f3ddab 100644
--- a/boards/x86/tinytile/board.cmake
+++ b/boards/x86/tinytile/board.cmake
@@ -1,27 +1,12 @@
 if(DEFINED ENV{ZEPHYR_FLASH_OVER_DFU})
   set(BOARD_FLASH_RUNNER dfu-util)
-
-  set(DFUUTIL_PID 8087:0aba)
-  set(DFUUTIL_ALT x86_app)
-  set(DFUUTIL_IMG ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME})
-
-  set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-    DFUUTIL_PID
-    DFUUTIL_ALT
-    DFUUTIL_IMG
-    )
-else()
-  set(BOARD_FLASH_RUNNER openocd)
 endif()
 
 set(BOARD_DEBUG_RUNNER openocd)
 
-set(OPENOCD_PRE_CMD "targets 1")
-set(OPENOCD_LOAD_CMD "load_image     ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
-set(OPENOCD_VERIFY_CMD "verify_image ${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME} ${CONFIG_FLASH_BASE_ADDRESS}")
+board_runner_args(dfu-util "--pid=8087:0aba" "--alt=x86_app")
+set(PRE_LOAD targets 1)
+board_runner_args(openocd "--cmd-pre-load=\"${PRE_LOAD}\"")
 
-set_property(GLOBAL APPEND PROPERTY FLASH_SCRIPT_ENV_VARS
-  OPENOCD_PRE_CMD
-  OPENOCD_LOAD_CMD
-  OPENOCD_VERIFY_CMD
-  )
+include($ENV{ZEPHYR_BASE}/boards/common/openocd.board.cmake)
+include($ENV{ZEPHYR_BASE}/boards/common/dfu-util.board.cmake)
diff --git a/boards/xtensa/esp32/board.cmake b/boards/xtensa/esp32/board.cmake
index 52d3449..71595bf 100644
--- a/boards/xtensa/esp32/board.cmake
+++ b/boards/xtensa/esp32/board.cmake
@@ -1 +1 @@
-set(BOARD_FLASH_RUNNER esp32)
+include($ENV{ZEPHYR_BASE}/boards/common/esp32.board.cmake)
diff --git a/cmake/app/boilerplate.cmake b/cmake/app/boilerplate.cmake
index 484c211..dffda55 100644
--- a/cmake/app/boilerplate.cmake
+++ b/cmake/app/boilerplate.cmake
@@ -244,9 +244,3 @@
 May include isr_tables.c etc."
   )
 set_property(GLOBAL PROPERTY GENERATED_KERNEL_SOURCE_FILES "")
-
-define_property(GLOBAL PROPERTY FLASH_SCRIPT_ENV_VARS
-  BRIEF_DOCS "Environment variables to pass to the flash/debug script"
-  FULL_DOCS  "Environment variables to pass to the flash/debug script"
-  )
-set_property(GLOBAL PROPERTY GENERATED_KERNEL_SOURCE_FILES "")
diff --git a/cmake/extensions.cmake b/cmake/extensions.cmake
index 179b2e9..a1a1219 100644
--- a/cmake/extensions.cmake
+++ b/cmake/extensions.cmake
@@ -8,6 +8,7 @@
 # 1.1. zephyr_*
 # 1.2. zephyr_library_*
 # 1.3. generate_inc_*
+# 1.4. board_*
 # 2. Kconfig-aware extensions
 # 2.1 *_if_kconfig
 # 2.2 Misc
@@ -435,6 +436,61 @@
   set_property(GLOBAL APPEND PROPERTY ZEPHYR_LIBS ${library})
 endfunction()
 
+# 1.4. board_*
+#
+# This section is for extensions which control Zephyr's board runners
+# from the build system. The Zephyr build system has targets for
+# flashing and debugging supported boards. These are wrappers around a
+# "runner" Python package that is part of Zephyr. This section
+# provides glue between CMake and the runner invocation script,
+# zephyr_flash_debug.py.
+
+# This function is intended for board.cmake files.
+#
+# Usage:
+#   board_runner_args(runner "--some-arg=val1" "--another-arg=val2")
+#
+# Will ensure the command line to zephyr_flash_debug.py contains:
+#   --some-arg=val1 --another-arg=val2
+#
+# in the flash, debug, and debugserver target recipes, as
+# appropriate. These settings will override any defaults provided by
+# the build system.
+function(board_runner_args runner)
+  string(MAKE_C_IDENTIFIER ${runner} runner_id)
+  # Note the "_EXPLICIT_" here, and see below.
+  set_property(GLOBAL APPEND PROPERTY BOARD_RUNNER_ARGS_EXPLICIT_${runner_id} ${ARGN})
+endfunction()
+
+# This function is intended for internal use by
+# boards/common/runner.board.cmake files.
+#
+# Basic usage:
+#   board_finalize_runner_args(runner)
+#
+# This ensures the build system captures all arguments added in any
+# board_runner_args() calls.
+#
+# Extended usage:
+#   board_runner_args(runner "--some-arg=default-value")
+#
+# This provides common or default values for arguments. These are
+# placed before board_runner_args() calls, so they generally take
+# precedence, except for arguments which can be given multiple times
+# (use these with caution).
+function(board_finalize_runner_args runner)
+  string(MAKE_C_IDENTIFIER ${runner} runner_id)
+  get_property(explicit GLOBAL PROPERTY "BOARD_RUNNER_ARGS_EXPLICIT_${runner_id}")
+  # Note no _EXPLICIT_ here. This property contains the final list.
+  set_property(GLOBAL APPEND PROPERTY BOARD_RUNNER_ARGS_${runner_id}
+    # Default arguments from the common runner file come first.
+    ${ARGN}
+    # Arguments explicitly given with board_runner_args() come
+    # last, so they take precedence.
+    ${explicit}
+    )
+endfunction()
+
 ########################################################
 # 2. Kconfig-aware extensions
 ########################################################
diff --git a/cmake/flash/CMakeLists.txt b/cmake/flash/CMakeLists.txt
index 90e067f..153a1ea 100644
--- a/cmake/flash/CMakeLists.txt
+++ b/cmake/flash/CMakeLists.txt
@@ -1,36 +1,31 @@
 assert_not(FLASH_SCRIPT "FLASH_SCRIPT has been removed; use BOARD_FLASH_RUNNER")
 assert_not(DEBUG_SCRIPT "DEBUG_SCRIPT has been removed; use BOARD_DEBUG_RUNNER")
 
-get_property(ENV_VARS GLOBAL PROPERTY FLASH_SCRIPT_ENV_VARS)
-
-set(ENV_VARS_FORMATTED "")
-foreach(env_var ${ENV_VARS})
-  list(APPEND ENV_VARS_FORMATTED
-    ${env_var}=${${env_var}}
-    )
-endforeach()
-
-list(APPEND ENV_VARS_FORMATTED
-                O=${PROJECT_BINARY_DIR}
-  KERNEL_ELF_NAME=${KERNEL_ELF_NAME}
-  KERNEL_HEX_NAME=${KERNEL_HEX_NAME}
-  KERNEL_BIN_NAME=${KERNEL_BIN_NAME}
-        BOARD_DIR=${BOARD_DIR}
-              GDB=${CMAKE_GDB}
-          OPENOCD_DEFAULT_PATH=${OPENOCD_DEFAULT_PATH}
-          OPENOCD=${OPENOCD}
+set(RUNNER_ARGS_COMMON
+  # Required:
+  "--board-dir=${BOARD_DIR}"
+  "--kernel-elf=${PROJECT_BINARY_DIR}/${KERNEL_ELF_NAME}"
+  "--kernel-hex=${PROJECT_BINARY_DIR}/${KERNEL_HEX_NAME}"
+  "--kernel-bin=${PROJECT_BINARY_DIR}/${KERNEL_BIN_NAME}"
+  # Optional, but so often needed that they're provided by default:
+  # (TODO: revisit whether we really want these here)
+  "--gdb=${CMAKE_GDB}"
+  "--openocd=${OPENOCD}"
+  "--openocd-search=${OPENOCD_DEFAULT_PATH}"
   )
 
 foreach(target flash debug debugserver)
+  string(TOUPPER "${target}" target_upper)
+
   if(target STREQUAL flash)
     set(comment "Flashing ${BOARD}")
-    set(runner ${BOARD_FLASH_RUNNER})
+    set(runner "${BOARD_FLASH_RUNNER}")
   elseif(target STREQUAL debug)
     set(comment "Debugging ${BOARD}")
-    set(runner ${BOARD_DEBUG_RUNNER})
+    set(runner "${BOARD_DEBUG_RUNNER}")
   elseif(target STREQUAL debugserver)
     set(comment "Debugging ${BOARD}")
-    set(runner ${BOARD_DEBUG_RUNNER})
+    set(runner "${BOARD_DEBUG_RUNNER}")
     if(EMU_PLATFORM)
       # cmake/qemu/CMakeLists.txt will add a debugserver target for
       # emulation platforms, so we don't add one here
@@ -39,13 +34,18 @@
   endif()
 
   if(runner)
+    # E.g. runner_ident = dfu_util (note the underscore), etc.
+    string(MAKE_C_IDENTIFIER "${runner}" runner_ident)
+    # E.g. args = BOARD_RUNNER_ARGS_openocd, BOARD_RUNNER_ARGS_dfu_util, etc.
+    get_property(args GLOBAL PROPERTY "BOARD_RUNNER_ARGS_${runner_ident}")
     set(cmd
       ${CMAKE_COMMAND} -E env
-      ${ENV_VARS_FORMATTED}
       ${PYTHON_EXECUTABLE}
       $ENV{ZEPHYR_BASE}/scripts/support/zephyr_flash_debug.py
       ${runner}
       ${target}
+      ${RUNNER_ARGS_COMMON}
+      ${args}
       DEPENDS ${logical_target_for_zephyr_elf}
       WORKING_DIRECTORY ${APPLICATION_BINARY_DIR}
       )
diff --git a/scripts/support/runner/arc.py b/scripts/support/runner/arc.py
index 9f5e5bf..4b0a8be 100644
--- a/scripts/support/runner/arc.py
+++ b/scripts/support/runner/arc.py
@@ -1,14 +1,13 @@
 # Copyright (c) 2017 Linaro Limited.
+# Copyright (c) 2017 Open Source Foundries Limited.
 #
 # SPDX-License-Identifier: Apache-2.0
 
 '''ARC architecture-specific runners.'''
 
 from os import path
-import os
-import shlex
 
-from .core import ZephyrBinaryRunner, get_env_or_bail
+from .core import ZephyrBinaryRunner
 
 DEFAULT_ARC_TCL_PORT = 6333
 DEFAULT_ARC_TELNET_PORT = 4444
@@ -26,22 +25,19 @@
     #
     # TODO: exit immediately when flashing is done, leaving Zephyr running.
 
-    def __init__(self, elf, zephyr_base, board_dir,
-                 gdb, openocd='openocd', extra_init=None, default_path=None,
-                 tui=None, tcl_port=DEFAULT_ARC_TCL_PORT,
+    def __init__(self, board_dir, elf, gdb,
+                 openocd='openocd', search=None,
+                 tui=False, tcl_port=DEFAULT_ARC_TCL_PORT,
                  telnet_port=DEFAULT_ARC_TELNET_PORT,
                  gdb_port=DEFAULT_ARC_GDB_PORT, debug=False):
         super(EmStarterKitBinaryRunner, self).__init__(debug=debug)
-        self.elf = elf
-        self.zephyr_base = zephyr_base
         self.board_dir = board_dir
-        self.gdb = gdb
+        self.elf = elf
+        self.gdb_cmd = [gdb] + (['-tui'] if tui else [])
         search_args = []
-        if default_path is not None:
-            search_args = ['-s', default_path]
+        if search is not None:
+            search_args = ['-s', search]
         self.openocd_cmd = [openocd] + search_args
-        self.extra_init = extra_init if extra_init is not None else []
-        self.tui = tui
         self.tcl_port = tcl_port
         self.telnet_port = telnet_port
         self.gdb_port = gdb_port
@@ -50,57 +46,29 @@
     def name(cls):
         return 'em-starterkit'
 
-    def create_from_env(command, debug):
-        '''Create runner from environment.
+    @classmethod
+    def do_add_parser(cls, parser):
+        parser.add_argument('--tui', default=False, action='store_true',
+                            help='if given, GDB uses -tui')
+        parser.add_argument('--tcl-port', default=DEFAULT_ARC_TCL_PORT,
+                            help='openocd TCL port, defaults to 6333')
+        parser.add_argument('--telnet-port', default=DEFAULT_ARC_TELNET_PORT,
+                            help='openocd telnet port, defaults to 4444')
+        parser.add_argument('--gdb-port', default=DEFAULT_ARC_GDB_PORT,
+                            help='openocd gdb port, defaults to 3333')
 
-        Required:
-
-        - O: build output directory
-        - KERNEL_ELF_NAME: zephyr kernel binary in ELF format
-        - ZEPHYR_BASE: zephyr Git repository base directory
-        - BOARD_DIR: board directory
-        - GDB: gdb executable
-
-        Optional:
-
-        - OPENOCD: path to openocd, defaults to openocd
-        - OPENOCD_EXTRA_INIT: initialization command for GDB server
-        - OPENOCD_DEFAULT_PATH: openocd search path to use
-        - TUI: if present, passed to gdb server used to flash
-        - TCL_PORT: openocd TCL port, defaults to 6333
-        - TELNET_PORT: openocd telnet port, defaults to 4444
-        - GDB_PORT: openocd gdb port, defaults to 3333
-        '''
-        elf = path.join(get_env_or_bail('O'),
-                        get_env_or_bail('KERNEL_ELF_NAME'))
-        zephyr_base = get_env_or_bail('ZEPHYR_BASE')
-        board_dir = get_env_or_bail('BOARD_DIR')
-        gdb = get_env_or_bail('GDB')
-
-        openocd = os.environ.get('OPENOCD', 'openocd')
-        extra_init = os.environ.get('OPENOCD_EXTRA_INIT', None)
-        if extra_init is not None:
-            extra_init = shlex.split(extra_init)
-        default_path = os.environ.get('OPENOCD_DEFAULT_PATH', None)
-        tui = os.environ.get('TUI', None)
-        tcl_port = int(os.environ.get('TCL_PORT',
-                                      str(DEFAULT_ARC_TCL_PORT)))
-        telnet_port = int(os.environ.get('TELNET_PORT',
-                                         str(DEFAULT_ARC_TELNET_PORT)))
-        gdb_port = int(os.environ.get('GDB_PORT',
-                                      str(DEFAULT_ARC_GDB_PORT)))
+    @classmethod
+    def create_from_args(cls, args):
+        if args.gdb is None:
+            raise ValueError('--gdb not provided at command line')
 
         return EmStarterKitBinaryRunner(
-            elf, zephyr_base, board_dir,
-            gdb, openocd=openocd, extra_init=extra_init,
-            default_path=default_path, tui=tui,
-            tcl_port=tcl_port, telnet_port=telnet_port,
-            gdb_port=gdb_port, debug=debug)
+            args.board_dir, args.kernel_elf, args.gdb,
+            openocd=args.openocd, search=args.openocd_search,
+            tui=args.tui, tcl_port=args.tcl_port, telnet_port=args.telnet_port,
+            gdb_port=args.gdb_port, debug=args.verbose)
 
     def do_run(self, command, **kwargs):
-        if command not in {'flash', 'debug', 'debugserver'}:
-            raise ValueError('{} is not supported'.format(command))
-
         kwargs['openocd-cfg'] = path.join(self.board_dir, 'support',
                                           'openocd.cfg')
 
@@ -114,7 +82,6 @@
 
         server_cmd = (self.openocd_cmd +
                       ['-f', config] +
-                      self.extra_init +
                       ['-c', 'tcl_port {}'.format(self.tcl_port),
                        '-c', 'telnet_port {}'.format(self.telnet_port),
                        '-c', 'gdb_port {}'.format(self.gdb_port),
@@ -122,16 +89,11 @@
                        '-c', 'targets',
                        '-c', 'halt'])
 
-        tui_arg = []
-        if self.tui is not None:
-            tui_arg = [self.tui]
-
         continue_arg = []
         if command == 'flash':
             continue_arg = ['-ex', 'c']
 
-        gdb_cmd = ([self.gdb] +
-                   tui_arg +
+        gdb_cmd = (self.gdb_cmd +
                    ['-ex', 'target remote :{}'.format(self.gdb_port),
                     '-ex', 'load'] +
                    continue_arg +
diff --git a/scripts/support/runner/bossac.py b/scripts/support/runner/bossac.py
index e22b0dc..a34be44 100644
--- a/scripts/support/runner/bossac.py
+++ b/scripts/support/runner/bossac.py
@@ -4,11 +4,9 @@
 
 '''bossac-specific runner (flash only) for Atmel SAM microcontrollers.'''
 
-from os import path
-import os
 import platform
 
-from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps
 
 DEFAULT_BOSSAC_PORT = '/dev/ttyACM0'
 
@@ -31,25 +29,17 @@
     def capabilities(cls):
         return RunnerCaps(commands={'flash'})
 
-    def create_from_env(command, debug):
-        '''Create flasher from environment.
+    @classmethod
+    def do_add_parser(cls, parser):
+        parser.add_argument('--bossac', default='bossac',
+                            help='path to bossac, default is bossac')
+        parser.add_argument('--bossac-port', default='/dev/ttyACM0',
+                            help='serial port to use, default is /dev/ttyACM0')
 
-        Required:
-
-        - O: build output directory
-        - KERNEL_BIN_NAME: name of kernel binary
-
-        Optional:
-
-        - BOSSAC: path to bossac, default is bossac
-        - BOSSAC_PORT: serial port to use, default is /dev/ttyACM0
-        '''
-        bin_name = path.join(get_env_or_bail('O'),
-                             get_env_or_bail('KERNEL_BIN_NAME'))
-        bossac = os.environ.get('BOSSAC', 'bossac')
-        port = os.environ.get('BOSSAC_PORT', DEFAULT_BOSSAC_PORT)
-        return BossacBinaryRunner(bin_name, bossac=bossac, port=port,
-                                  debug=debug)
+    @classmethod
+    def create_from_args(command, args):
+        return BossacBinaryRunner(args.kernel_bin, bossac=args.bossac,
+                                  port=args.bossac_port, debug=args.verbose)
 
     def do_run(self, command, **kwargs):
         if platform.system() != 'Linux':
diff --git a/scripts/support/runner/core.py b/scripts/support/runner/core.py
index cfb4036..1aafc23 100644
--- a/scripts/support/runner/core.py
+++ b/scripts/support/runner/core.py
@@ -14,54 +14,9 @@
 import abc
 import os
 import platform
-import pprint
 import shlex
 import signal
 import subprocess
-import sys
-
-
-def get_env_or_bail(env_var):
-    '''Get an environment variable, or raise an error.
-
-    In case of KeyError, an error message is printed, along with the
-    environment, and the exception is re-raised.
-    '''
-    try:
-        return os.environ[env_var]
-    except KeyError:
-        print('Variable {} not in environment:'.format(
-                  env_var), file=sys.stderr)
-        pprint.pprint(dict(os.environ), stream=sys.stderr)
-        raise
-
-
-def get_env_bool_or(env_var, default_value):
-    '''Get an environment variable as a boolean, or return a default value.
-
-    Get an environment variable, interpret it as a base ten
-    integer, and convert that to a boolean.
-
-    In case the environment variable is not defined, return default_value.
-    '''
-    try:
-        return bool(int(os.environ[env_var]))
-    except KeyError:
-        return default_value
-
-
-def get_env_strip_or(env_var, to_strip, default_value):
-    '''Get and clean up an environment variable, or return a default value.
-
-    Get the value of env_var from the environment. If it is
-    defined, return that value with to_strip stripped off. If it
-    is undefined, return default_value (without any stripping).
-    '''
-    value = os.environ.get(env_var, None)
-    if value is not None:
-        return value.strip(to_strip)
-    else:
-        return default_value
 
 
 def quote_sh_list(cmd):
@@ -227,10 +182,10 @@
       binary.
 
     This class provides an API for these commands. Every runner has a
-    name, and declares commands it can handle. Zephyr boards declare
-    compatible runner(s) by name to the build system, which can then
-    call into the create_runner() method below to make a concrete
-    runner instance for use executing a command.
+    name (like 'pyocd'), and declares commands it can handle (like
+    'flash'). Zephyr boards (like 'nrf52_pca10040') declare compatible
+    runner(s) by name to the build system, which makes concrete runner
+    instances to execute commands via this class.
 
     If your board can use an existing runner, all you have to do is
     give its name to the build system. How to do that is out of the
@@ -240,58 +195,34 @@
     If you want to define and use your own runner:
 
     1. Define a ZephyrBinaryRunner subclass, and implement its
-       abstract methods. Override any methods you need to, especially
-       capabilities().
+       abstract methods. You may need to override capabilities().
 
     2. Make sure the Python module defining your runner class is
-       imported by this package's __init__.py (otherwise,
-       create_runner() won't work).
+       imported, e.g. by editing this package's __init__.py (otherwise,
+       get_runners() won't work).
 
     3. Give your runner's name to the Zephyr build system in your
        board's build files.
 
+    For command-line invocation from the Zephyr build system, runners
+    define their own argparse-based interface through the common
+    add_parser() (and runner-specific do_add_parser() it delegates
+    to), and provide a way to create instances of themselves from
+    parsed arguments via create_from_args().
+
     Runners use a variety of target-specific tools and configuration
     values, the user interface to which is abstracted by this
     class. Each runner subclass should take any values it needs to
     execute one of these commands in its constructor.  The actual
-    command execution is handled in the run() method.
-
-    At present, the Zephyr build system uses environment variables to
-    control runner behavior.  To support this, a create_runner()
-    method is defined below.  This method takes a runner name, and
-    iterates over defined ZephyrBinaryRunner subclasses to find the
-    runner class. It then checks that it supports the command, then
-    instantiates and returns a runner with configuration determined by
-    the environment.
-
-    To support this, subclasses currently must define a static method:
-    create_from_env(). This is called by create_runner() to create a
-    concrete runner instance.
-
-    The environment-based factories are for legacy use *only*; the
-    build system is moving away from use of environment variables. The
-    user must be able to construct and use a runner using only the
-    constructor and run() method.
-
-    '''
+    command execution is handled in the run() method.'''
 
     def __init__(self, debug=False):
         self.debug = debug
 
     @staticmethod
-    def create_runner(runner_name, command, debug):
-        for cls in ZephyrBinaryRunner.__subclasses__():
-            if cls.name() == runner_name:
-                break
-        else:
-            raise ValueError('no runner named {} is known'.format(runner_name))
-
-        caps = cls.capabilities()
-        if command not in caps.commands:
-            raise ValueError('runner {} does not implement command {}'.format(
-                runner_name, command))
-
-        return cls.create_from_env(command, debug)
+    def get_runners():
+        '''Get a list of all currently defined runner classes.'''
+        return ZephyrBinaryRunner.__subclasses__()
 
     @classmethod
     @abc.abstractmethod
@@ -314,10 +245,77 @@
         Subclasses should override appropriately if needed.'''
         return RunnerCaps()
 
-    @staticmethod
+    @classmethod
+    def add_parser(cls, parser):
+        '''Adds a sub-command parser for this runner.
+
+        The given object, parser, is a sub-command parser from the
+        argparse module. For more details, refer to the documentation
+        for argparse.ArgumentParser.add_subparsers().
+
+        The standard (required) arguments are:
+
+        * --board-dir
+        * --kernel-elf, --kernel-hex, --kernel-bin
+
+        The standard optional arguments are:
+
+        * --gdb
+        * --openocd, --openocd-search
+
+        Runner-specific options are added through the do_add_parser()
+        hook.
+
+        The single positional argument is "command". This is currently
+        restricted to values 'flash', 'debug', and 'debugserver'.'''
+        # Required options.
+        parser.add_argument('--board-dir', required=True,
+                            help='Zephyr board directory')
+        parser.add_argument('--kernel-elf', required=True,
+                            help='path to kernel binary in .elf format')
+        parser.add_argument('--kernel-hex', required=True,
+                            help='path to kernel binary in .hex format')
+        parser.add_argument('--kernel-bin', required=True,
+                            help='path to kernel binary in .bin format')
+
+        # Optional options.
+        parser.add_argument('--gdb', default=None,
+                            help='GDB compatible with the target')
+        parser.add_argument('--openocd', default='openocd',
+                            help='OpenOCD to use')
+        parser.add_argument('--openocd-search', default=None,
+                            help='directory to add to OpenOCD search path')
+
+        # Runner-specific options.
+        cls.do_add_parser(parser)
+
+        # The lone positional argument. Note that argparse can't cope
+        # with adding options after the first positional argument, so
+        # this must come last.
+        parser.add_argument('command',
+                            choices=['flash', 'debug', 'debugserver'],
+                            help='command to run (flash, debug, debugserver)')
+
+    @classmethod
     @abc.abstractmethod
-    def create_from_env(command, debug):
-        '''Create new runner instance from environment variables.'''
+    def do_add_parser(cls, parser):
+        '''Hook for adding runner-specific options.
+
+        Subclasses **must not** add positional arguments. That is, when
+        calling parser.add_argument(), make sure to begin the argument
+        with '-' so it is interpreted as an option, rather than a
+        positional argument.
+
+        * OK: parser.add_argument('--my-option')
+        * Not OK: parser.add_argument('my-argument').'''
+
+    @classmethod
+    @abc.abstractmethod
+    def create_from_args(cls, args):
+        '''Create an instance from command-line arguments.
+
+        These will have been parsed from the command line according to
+        the specification defined by add_parser().'''
 
     def run(self, command, **kwargs):
         '''Runs command ('flash', 'debug', 'debugserver').
diff --git a/scripts/support/runner/dfu.py b/scripts/support/runner/dfu.py
index a0703ce..e7303d5 100644
--- a/scripts/support/runner/dfu.py
+++ b/scripts/support/runner/dfu.py
@@ -4,11 +4,10 @@
 
 '''Runner for flashing with dfu-util.'''
 
-import os
 import sys
 import time
 
-from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps
 
 
 class DfuUtilBinaryRunner(ZephyrBinaryRunner):
@@ -33,29 +32,30 @@
     def capabilities(cls):
         return RunnerCaps(commands={'flash'})
 
-    def create_from_env(command, debug):
-        '''Create flasher from environment.
+    @classmethod
+    def do_add_parser(cls, parser):
+        # Required:
+        parser.add_argument("--pid", required=True,
+                            help="USB VID:PID of the board")
+        parser.add_argument("--alt", required=True,
+                            help="interface alternate setting number or name")
 
-        Required:
+        # Optional:
+        parser.add_argument("--img",
+                            help="binary to flash, default is --kernel-bin")
+        parser.add_argument("--dfuse-addr", default=None,
+                            help='''target address if the board is a DfuSe
+                            device; ignored it not present''')
+        parser.add_argument('--dfu-util', default='dfu-util',
+                            help='dfu-util executable; defaults to "dfu-util"')
 
-        - DFUUTIL_PID: USB VID:PID of the board
-        - DFUUTIL_ALT: interface alternate setting number or name
-        - DFUUTIL_IMG: binary to flash
-
-        Optional:
-
-        - DFUUTIL_DFUSE_ADDR: target address if the board is a
-          DfuSe device. Ignored if not present.
-        - DFUUTIL: dfu-util executable, defaults to dfu-util.
-        '''
-        pid = get_env_or_bail('DFUUTIL_PID')
-        alt = get_env_or_bail('DFUUTIL_ALT')
-        img = get_env_or_bail('DFUUTIL_IMG')
-        dfuse = os.environ.get('DFUUTIL_DFUSE_ADDR', None)
-        exe = os.environ.get('DFUUTIL', 'dfu-util')
-
-        return DfuUtilBinaryRunner(pid, alt, img, dfuse=dfuse, exe=exe,
-                                   debug=debug)
+    @classmethod
+    def create_from_args(cls, args):
+        if args.img is None:
+            args.img = args.kernel_bin
+        return DfuUtilBinaryRunner(args.pid, args.alt, args.img,
+                                   dfuse=args.dfuse_addr, exe=args.dfu_util,
+                                   debug=args.verbose)
 
     def find_device(self):
         cmd = list(self.cmd) + ['-l']
diff --git a/scripts/support/runner/esp32.py b/scripts/support/runner/esp32.py
index a7836b5..868152e 100644
--- a/scripts/support/runner/esp32.py
+++ b/scripts/support/runner/esp32.py
@@ -5,9 +5,8 @@
 '''Runner for flashing ESP32 devices with esptool/espidf.'''
 
 from os import path
-import os
 
-from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps
 
 
 class Esp32BinaryRunner(ZephyrBinaryRunner):
@@ -33,44 +32,41 @@
     def capabilities(cls):
         return RunnerCaps(commands={'flash'})
 
-    def create_from_env(command, debug):
-        '''Create flasher from environment.
+    @classmethod
+    def do_add_parser(cls, parser):
+        # Required
+        parser.add_argument('--esp-idf-path', required=True,
+                            help='path to ESP-IDF')
 
-        Required:
+        # Optional
+        parser.add_argument('--esp-device', default='/dev/ttyUSB0',
+                            help='serial port to flash, default /dev/ttyUSB0')
+        parser.add_argument('--esp-baud-rate', default='921600',
+                            help='serial baud rate, default 921600')
+        parser.add_argument('--esp-flash-size', default='detect',
+                            help='flash size, default "detect"')
+        parser.add_argument('--esp-flash-freq', default='40m',
+                            help='flash frequency, default "40m"')
+        parser.add_argument('--esp-flash-mode', default='dio',
+                            help='flash mode, default "dio"')
+        parser.add_argument(
+            '--esp-tool',
+            help='''if given, complete path to espidf. default is to search for
+            it in [ESP_IDF_PATH]/components/esptool_py/esptool/esptool.py''')
 
-        - O: build output directory
-        - KERNEL_ELF_NAME: name of kernel binary in ELF format
+    @classmethod
+    def create_from_args(command, args):
+        if args.esp_tool:
+            espidf = args.esp_tool
+        else:
+            espidf = path.join(args.esp_idf_path, 'components', 'esptool_py',
+                               'esptool', 'esptool.py')
 
-        Optional:
-
-        - ESP_DEVICE: serial port to flash, default /dev/ttyUSB0
-        - ESP_BAUD_RATE: serial baud rate, default 921600
-        - ESP_FLASH_SIZE: flash size, default 'detect'
-        - ESP_FLASH_FREQ: flash frequency, default '40m'
-        - ESP_FLASH_MODE: flash mode, default 'dio'
-        - ESP_TOOL: complete path to espidf, or set to 'espidf' to look for it
-          in $ESP_IDF_PATH/components/esptool_py/esptool/esptool.py
-        '''
-        elf = path.join(get_env_or_bail('O'),
-                        get_env_or_bail('KERNEL_ELF_NAME'))
-
-        # TODO add sane device defaults on other platforms than Linux.
-        device = os.environ.get('ESP_DEVICE', '/dev/ttyUSB0')
-        baud = os.environ.get('ESP_BAUD_RATE', '921600')
-        flash_size = os.environ.get('ESP_FLASH_SIZE', 'detect')
-        flash_freq = os.environ.get('ESP_FLASH_FREQ', '40m')
-        flash_mode = os.environ.get('ESP_FLASH_MODE', 'dio')
-        espidf = os.environ.get('ESP_TOOL', 'espidf')
-
-        if espidf == 'espidf':
-            idf_path = get_env_or_bail('ESP_IDF_PATH')
-            espidf = path.join(idf_path, 'components', 'esptool_py', 'esptool',
-                               'esptool.py')
-
-        return Esp32BinaryRunner(elf, device, baud=baud,
-                                 flash_size=flash_size, flash_freq=flash_freq,
-                                 flash_mode=flash_mode, espidf=espidf,
-                                 debug=debug)
+        return Esp32BinaryRunner(
+            args.kernel_elf, args.esp_device, baud=args.esp_baud_rate,
+            flash_size=args.esp_flash_size, flash_freq=args.esp_flash_freq,
+            flash_mode=args.esp_flash_mode, espidf=espidf,
+            debug=args.verbose)
 
     def do_run(self, command, **kwargs):
         bin_name = path.splitext(self.elf)[0] + path.extsep + 'bin'
diff --git a/scripts/support/runner/jlink.py b/scripts/support/runner/jlink.py
index 37a0c44..f956955 100644
--- a/scripts/support/runner/jlink.py
+++ b/scripts/support/runner/jlink.py
@@ -4,10 +4,7 @@
 
 '''Runner for debugging with JLink.'''
 
-from os import path
-import os
-
-from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps
 
 DEFAULT_JLINK_GDB_PORT = 2331
 
@@ -17,7 +14,7 @@
 
     def __init__(self, device,
                  gdbserver='JLinkGDBServer', iface='swd', elf_name=None,
-                 gdb=None, gdb_port=DEFAULT_JLINK_GDB_PORT, tui=None,
+                 gdb=None, gdb_port=DEFAULT_JLINK_GDB_PORT, tui=False,
                  debug=False):
         super(JLinkBinaryRunner, self).__init__(debug=debug)
         self.device = device
@@ -26,7 +23,7 @@
         self.elf_name = elf_name
         self.gdb_cmd = [gdb] if gdb is not None else None
         self.gdb_port = gdb_port
-        self.tui_arg = [tui] if tui is not None else []
+        self.tui_arg = ['-tui'] if tui else []
 
     @classmethod
     def name(cls):
@@ -36,49 +33,28 @@
     def capabilities(cls):
         return RunnerCaps(commands={'debug', 'debugserver'})
 
-    def create_from_env(command, debug):
-        '''Create runner from environment.
+    @classmethod
+    def do_add_parser(cls, parser):
+        # Required:
+        parser.add_argument('--device', required=True, help='device name')
 
-        Required:
+        # Optional:
+        parser.add_argument('--iface', default='swd',
+                            help='interface to use, default is swd')
+        parser.add_argument('--tui', default=False, action='store_true',
+                            help='if given, GDB uses -tui')
+        parser.add_argument('--gdbserver', default='JLinkGDBServer',
+                            help='GDB server, default is JLinkGDBServer')
+        parser.add_argument('--gdb-port', default=DEFAULT_JLINK_GDB_PORT,
+                            help='pyocd gdb port, defaults to {}'.format(
+                                DEFAULT_JLINK_GDB_PORT))
 
-        - JLINK_DEVICE: device name
-
-        Required for 'debug':
-
-        - GDB: gdb to use
-        - O: build output directory
-        - KERNEL_ELF_NAME: zephyr kernel binary in ELF format
-
-        Optional for 'debug':
-
-        - TUI: if present, passed to gdb server used to flash
-
-        Optional for 'debug', 'debugserver':
-
-        - JLINK_GDBSERVER: default is JLinkGDBServer
-        - GDB_PORT: default is 2331
-        - JLINK_IF: default is swd
-        '''
-        device = get_env_or_bail('JLINK_DEVICE')
-
-        gdb = os.environ.get('GDB', None)
-        o = os.environ.get('O', None)
-        elf = os.environ.get('KERNEL_ELF_NAME', None)
-        elf_name = None
-        if o is not None:
-            if elf is not None:
-                elf_name = path.join(o, elf)
-        tui = os.environ.get('TUI', None)
-
-        gdbserver = os.environ.get('JLINK_GDBSERVER', 'JLinkGDBServer')
-        gdb_port = int(os.environ.get('GDB_PORT',
-                                      str(DEFAULT_JLINK_GDB_PORT)))
-        iface = os.environ.get('JLINK_IF', 'swd')
-
-        return JLinkBinaryRunner(device, gdbserver=gdbserver,
-                                 iface=iface, elf_name=elf_name,
-                                 gdb=gdb, gdb_port=gdb_port, tui=tui,
-                                 debug=debug)
+    @classmethod
+    def create_from_args(cls, args):
+        return JLinkBinaryRunner(args.device, gdbserver=args.gdbserver,
+                                 iface=args.iface, elf_name=args.kernel_elf,
+                                 gdb=args.gdb, gdb_port=args.gdb_port,
+                                 tui=args.tui, debug=args.verbose)
 
     def print_gdbserver_message(self):
         print('JLink GDB server running on port {}'.format(self.gdb_port))
diff --git a/scripts/support/runner/nios2.py b/scripts/support/runner/nios2.py
index 2916360..c172e86 100644
--- a/scripts/support/runner/nios2.py
+++ b/scripts/support/runner/nios2.py
@@ -4,9 +4,6 @@
 
 '''Runner for NIOS II, based on quartus-flash.py and GDB.'''
 
-from os import path
-import os
-
 from .core import ZephyrBinaryRunner, NetworkPortHelper
 
 
@@ -20,61 +17,36 @@
     #      and CONFIG_INCLUDE_RESET_VECTOR must be disabled."
 
     def __init__(self, hex_name=None, elf_name=None, cpu_sof=None,
-                 zephyr_base=None, gdb=None, tui=None, debug=False):
+                 quartus_py=None, gdb=None, tui=False, debug=False):
         super(Nios2BinaryRunner, self).__init__(debug=debug)
         self.hex_name = hex_name
         self.elf_name = elf_name
         self.cpu_sof = cpu_sof
-        self.zephyr_base = zephyr_base
+        self.quartus_py = quartus_py
         self.gdb_cmd = [gdb] if gdb is not None else None
-        self.tui_arg = [tui] if tui is not None else []
+        self.tui_arg = ['-tui'] if tui else []
 
     @classmethod
     def name(cls):
         return 'nios2'
 
-    def create_from_env(command, debug):
-        '''Create runner from environment.
+    @classmethod
+    def do_add_parser(cls, parser):
+        # TODO merge quartus-flash.py script into this file.
+        parser.add_argument('--quartus-flash', required=True)
+        parser.add_argument('--cpu-sof', required=True,
+                            help='path to the the CPU .sof data')
+        parser.add_argument('--tui', default=False, action='store_true',
+                            help='if given, GDB uses -tui')
 
-        Required for 'flash', 'debug':
-
-        - O: build output directory
-
-        Required for 'flash':
-
-        - KERNEL_HEX_NAME: name of kernel binary in HEX format
-        - NIOS2_CPU_SOF: location of the CPU .sof data
-        - ZEPHYR_BASE: zephyr Git repository base directory
-
-        Required for 'debug':
-
-        - KERNEL_ELF_NAME: name of kernel binary in ELF format
-        - GDB: GDB executable
-
-        Optional for 'debug':
-
-        - TUI: one additional argument to GDB (e.g. -tui)
-        '''
-        cpu_sof = os.environ.get('NIOS2_CPU_SOF', None)
-        zephyr_base = os.environ.get('ZEPHYR_BASE', None)
-
-        o = os.environ.get('O', None)
-        hex_ = os.environ.get('KERNEL_HEX_NAME', None)
-        elf = os.environ.get('KERNEL_ELF_NAME', None)
-        hex_name = None
-        elf_name = None
-        if o is not None:
-            if hex_ is not None:
-                hex_name = path.join(o, hex_)
-            if elf is not None:
-                elf_name = path.join(o, elf)
-
-        gdb = os.environ.get('GDB', None)
-        tui = os.environ.get('TUI', None)
-
-        return Nios2BinaryRunner(hex_name=hex_name, elf_name=elf_name,
-                                 cpu_sof=cpu_sof, zephyr_base=zephyr_base,
-                                 gdb=gdb, tui=tui, debug=debug)
+    @classmethod
+    def create_from_args(command, args):
+        return Nios2BinaryRunner(hex_name=args.kernel_hex,
+                                 elf_name=args.kernel_elf,
+                                 cpu_sof=args.cpu_sof,
+                                 quartus_py=args.quartus_flash,
+                                 gdb=args.gdb, tui=args.tui,
+                                 debug=args.verbose)
 
     def do_run(self, command, **kwargs):
         if command == 'flash':
@@ -83,19 +55,12 @@
             self.debug_debugserver(command, **kwargs)
 
     def flash(self, **kwargs):
-        sof_msg = (
-            'Cannot flash; '
-            'Please set NIOS2_CPU_SOF variable to location of CPU .sof data')
-
-        if self.zephyr_base is None:
-            raise ValueError('Cannot flash; ZEPHYR_BASE is missing.')
+        if self.quartus_py is None:
+            raise ValueError('Cannot flash; --quartus-flash not given.')
         if self.cpu_sof is None:
-            raise ValueError(sof_msg)
-        if self.hex_name is None:
-            raise ValueError('Cannot flash; .hex binary name is missing')
+            raise ValueError('Cannot flash; --cpu-sof not given.')
 
-        cmd = [path.join(self.zephyr_base, 'scripts', 'support',
-                         'quartus-flash.py'),
+        cmd = [self.quartus_py,
                '--sof', self.cpu_sof,
                '--kernel', self.hex_name]
 
diff --git a/scripts/support/runner/nrfjprog.py b/scripts/support/runner/nrfjprog.py
index 8b92b33..7a7c063 100644
--- a/scripts/support/runner/nrfjprog.py
+++ b/scripts/support/runner/nrfjprog.py
@@ -4,10 +4,9 @@
 
 '''Runner for flashing with nrfjprog.'''
 
-from os import path
 import sys
 
-from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps
 
 
 class NrfJprogBinaryRunner(ZephyrBinaryRunner):
@@ -26,20 +25,16 @@
     def capabilities(cls):
         return RunnerCaps(commands={'flash'})
 
-    def create_from_env(command, debug):
-        '''Create flasher from environment.
+    @classmethod
+    def do_add_parser(cls, parser):
+        parser.add_argument('--nrf-family', required=True,
+                            choices=['NRF51', 'NRF52'],
+                            help='family of nRF MCU')
 
-        Required:
-
-        - O: build output directory
-        - KERNEL_HEX_NAME: name of kernel binary in ELF format
-        - NRF_FAMILY: e.g. NRF51 or NRF52
-        '''
-        hex_ = path.join(get_env_or_bail('O'),
-                         get_env_or_bail('KERNEL_HEX_NAME'))
-        family = get_env_or_bail('NRF_FAMILY')
-
-        return NrfJprogBinaryRunner(hex_, family, debug=debug)
+    @classmethod
+    def create_from_args(cls, args):
+        return NrfJprogBinaryRunner(args.kernel_hex, args.nrf_family,
+                                    debug=args.verbose)
 
     def get_board_snr_from_user(self):
         snrs = self.check_output(['nrfjprog', '--ids'])
diff --git a/scripts/support/runner/openocd.py b/scripts/support/runner/openocd.py
index 85dcaf1..cd55b16 100644
--- a/scripts/support/runner/openocd.py
+++ b/scripts/support/runner/openocd.py
@@ -5,10 +5,8 @@
 '''Runner for openocd.'''
 
 from os import path
-import os
-import shlex
 
-from .core import ZephyrBinaryRunner, get_env_or_bail, get_env_strip_or
+from .core import ZephyrBinaryRunner
 
 DEFAULT_OPENOCD_TCL_PORT = 6333
 DEFAULT_OPENOCD_TELNET_PORT = 4444
@@ -19,10 +17,9 @@
     '''Runner front-end for openocd.'''
 
     def __init__(self, openocd_config,
-                 openocd='openocd', default_path=None,
-                 bin_name=None, elf_name=None,
-                 load_cmd=None, verify_cmd=None, pre_cmd=None, post_cmd=None,
-                 extra_init=None,
+                 openocd='openocd', search=None,
+                 elf_name=None,
+                 pre_cmd=None, load_cmd=None, verify_cmd=None, post_cmd=None,
                  tcl_port=DEFAULT_OPENOCD_TCL_PORT,
                  telnet_port=DEFAULT_OPENOCD_TELNET_PORT,
                  gdb_port=DEFAULT_OPENOCD_GDB_PORT,
@@ -31,108 +28,62 @@
         self.openocd_config = openocd_config
 
         search_args = []
-        if default_path is not None:
-            search_args = ['-s', default_path]
+        if search is not None:
+            search_args = ['-s', search]
         self.openocd_cmd = [openocd] + search_args
-        self.bin_name = bin_name
         self.elf_name = elf_name
         self.load_cmd = load_cmd
         self.verify_cmd = verify_cmd
         self.pre_cmd = pre_cmd
         self.post_cmd = post_cmd
-        self.extra_init = extra_init if extra_init is not None else []
         self.tcl_port = tcl_port
         self.telnet_port = telnet_port
         self.gdb_port = gdb_port
         self.gdb_cmd = [gdb] if gdb is not None else None
-        self.tui_arg = [tui] if tui is not None else []
+        self.tui_arg = ['-tui'] if tui else []
 
     @classmethod
     def name(cls):
         return 'openocd'
 
-    def create_from_env(command, debug):
-        '''Create runner from environment.
+    @classmethod
+    def do_add_parser(cls, parser):
+        # Options for flashing:
+        parser.add_argument('--cmd-pre-load',
+                            help='Command to run before flashing')
+        parser.add_argument('--cmd-load',
+                            help='''Command to load/flash binary
+                            (required when flashing)''')
+        parser.add_argument('--cmd-verify',
+                            help='''Command to verify flashed binary''')
+        parser.add_argument('--cmd-post-verify',
+                            help='Command to run after verification')
 
-        Required:
+        # Options for debugging:
+        parser.add_argument('--tui', default=False, action='store_true',
+                            help='if given, GDB uses -tui')
+        parser.add_argument('--tcl-port', default=DEFAULT_OPENOCD_TCL_PORT,
+                            help='openocd TCL port, defaults to 6333')
+        parser.add_argument('--telnet-port',
+                            default=DEFAULT_OPENOCD_TELNET_PORT,
+                            help='openocd telnet port, defaults to 4444')
+        parser.add_argument('--gdb-port', default=DEFAULT_OPENOCD_GDB_PORT,
+                            help='openocd gdb port, defaults to 3333')
 
-        - ZEPHYR_BASE: zephyr Git repository base directory
-        - BOARD_DIR: directory of board definition
+    @classmethod
+    def create_from_args(cls, args):
+        openocd_config = path.join(args.board_dir, 'support', 'openocd.cfg')
 
-        Optional:
-
-        - OPENOCD: path to openocd, defaults to openocd
-        - OPENOCD_DEFAULT_PATH: openocd search path to use
-
-        Required for 'flash':
-
-        - O: build output directory
-        - KERNEL_BIN_NAME: zephyr kernel binary
-        - OPENOCD_LOAD_CMD: command to load binary into flash
-        - OPENOCD_VERIFY_CMD: command to verify flash executed correctly
-
-        Optional for 'flash':
-
-        - OPENOCD_PRE_CMD: command to run before any others
-        - OPENOCD_POST_CMD: command to run after verifying flash write
-
-        Required for 'debug':
-
-        - GDB: GDB executable
-        - O: build output directory
-        - KERNEL_ELF_NAME: zephyr kernel binary, ELF format
-
-        Optional for 'debug':
-
-        - TUI: one additional argument to GDB (e.g. -tui)
-        - OPENOCD_EXTRA_INIT: additional arguments to pass to openocd
-        - TCL_PORT: openocd TCL port, defaults to 6333
-        - TELNET_PORT: openocd telnet port, defaults to 4444
-        - GDB_PORT: openocd gdb port, defaults to 3333
-        '''
-        zephyr_base = get_env_or_bail('ZEPHYR_BASE')
-        board_dir = get_env_or_bail('BOARD_DIR')
-        openocd_config = path.join(board_dir, 'support', 'openocd.cfg')
-
-        openocd = os.environ.get('OPENOCD', 'openocd')
-        default_path = os.environ.get('OPENOCD_DEFAULT_PATH', None)
-
-        o = os.environ.get('O', None)
-        bin_ = os.environ.get('KERNEL_BIN_NAME', None)
-        elf = os.environ.get('KERNEL_ELF_NAME', None)
-        bin_name = None
-        elf_name = None
-        if o is not None:
-            if bin_ is not None:
-                bin_name = path.join(o, bin_)
-            if elf is not None:
-                elf_name = path.join(o, elf)
-
-        load_cmd = get_env_strip_or('OPENOCD_LOAD_CMD', '"', None)
-        verify_cmd = get_env_strip_or('OPENOCD_VERIFY_CMD', '"', None)
-        pre_cmd = get_env_strip_or('OPENOCD_PRE_CMD', '"', None)
-        post_cmd = get_env_strip_or('OPENOCD_POST_CMD', '"', None)
-
-        gdb = os.environ.get('GDB', None)
-        tui = os.environ.get('TUI', None)
-        extra_init = os.environ.get('OPENOCD_EXTRA_INIT', None)
-        if extra_init is not None:
-            extra_init = shlex.split(extra_init)
-        tcl_port = int(os.environ.get('TCL_PORT',
-                                      str(DEFAULT_OPENOCD_TCL_PORT)))
-        telnet_port = int(os.environ.get('TELNET_PORT',
-                                         str(DEFAULT_OPENOCD_TELNET_PORT)))
-        gdb_port = int(os.environ.get('GDB_PORT',
-                                      str(DEFAULT_OPENOCD_GDB_PORT)))
-
-        return OpenOcdBinaryRunner(openocd_config,
-                                   openocd=openocd, default_path=default_path,
-                                   bin_name=bin_name, elf_name=elf_name,
-                                   load_cmd=load_cmd, verify_cmd=verify_cmd,
-                                   pre_cmd=pre_cmd, post_cmd=post_cmd,
-                                   extra_init=extra_init, tcl_port=tcl_port,
-                                   telnet_port=telnet_port, gdb_port=gdb_port,
-                                   gdb=gdb, tui=tui, debug=debug)
+        return OpenOcdBinaryRunner(
+            openocd_config,
+            openocd=args.openocd, search=args.openocd_search,
+            elf_name=args.kernel_elf,
+            pre_cmd=args.cmd_pre_load,
+            load_cmd=args.cmd_load, verify_cmd=args.cmd_verify,
+            post_cmd=args.cmd_post_verify,
+            tcl_port=args.tcl_port, telnet_port=args.telnet_port,
+            gdb_port=args.gdb_port, gdb=args.gdb, tui=args.tui,
+            debug=args.verbose)
 
     def do_run(self, command, **kwargs):
         if command == 'flash':
@@ -143,8 +94,6 @@
             self.do_debugserver(**kwargs)
 
     def do_flash(self, **kwargs):
-        if self.bin_name is None:
-            raise ValueError('Cannot flash; binary name is missing')
         if self.load_cmd is None:
             raise ValueError('Cannot flash; load command is missing')
         if self.verify_cmd is None:
@@ -179,9 +128,8 @@
             raise ValueError('Cannot debug; no .elf specified')
 
         server_cmd = (self.openocd_cmd +
-                      ['-f', self.openocd_config] +
-                      self.extra_init +
-                      ['-c', 'tcl_port {}'.format(self.tcl_port),
+                      ['-f', self.openocd_config,
+                       '-c', 'tcl_port {}'.format(self.tcl_port),
                        '-c', 'telnet_port {}'.format(self.telnet_port),
                        '-c', 'gdb_port {}'.format(self.gdb_port),
                        '-c', 'init',
diff --git a/scripts/support/runner/pyocd.py b/scripts/support/runner/pyocd.py
index 2e98f1c..cf17168 100644
--- a/scripts/support/runner/pyocd.py
+++ b/scripts/support/runner/pyocd.py
@@ -4,10 +4,10 @@
 
 '''Runner for pyOCD .'''
 
-from os import path
 import os
+import sys
 
-from .core import ZephyrBinaryRunner, get_env_or_bail
+from .core import ZephyrBinaryRunner
 
 DEFAULT_PYOCD_GDB_PORT = 3333
 
@@ -17,7 +17,7 @@
 
     def __init__(self, target, flashtool='pyocd-flashtool',
                  gdb=None, gdbserver='pyocd-gdbserver',
-                 gdb_port=DEFAULT_PYOCD_GDB_PORT, tui=None,
+                 gdb_port=DEFAULT_PYOCD_GDB_PORT, tui=False,
                  bin_name=None, elf_name=None,
                  board_id=None, daparg=None, debug=False):
         super(PyOcdBinaryRunner, self).__init__(debug=debug)
@@ -27,7 +27,7 @@
         self.gdb_cmd = [gdb] if gdb is not None else None
         self.gdbserver = gdbserver
         self.gdb_port = gdb_port
-        self.tui_args = [tui] if tui is not None else []
+        self.tui_args = ['-tui'] if tui else []
         self.bin_name = bin_name
         self.elf_name = elf_name
 
@@ -45,68 +45,47 @@
     def name(cls):
         return 'pyocd'
 
+    @classmethod
+    def do_add_parser(cls, parser):
+        parser.add_argument('--target', required=True,
+                            help='target override')
+
+        parser.add_argument('--daparg',
+                            help='Additional arguments to pyocd tool')
+        parser.add_argument('--flashtool', default='pyocd-flashtool',
+                            help='flash tool path, default is pyocd-flashtool')
+        parser.add_argument('--gdbserver', default='pyocd-gdbserver',
+                            help='GDB server, default is pyocd-gdbserver')
+        parser.add_argument('--gdb-port', default=DEFAULT_PYOCD_GDB_PORT,
+                            help='pyocd gdb port, defaults to {}'.format(
+                                DEFAULT_PYOCD_GDB_PORT))
+        parser.add_argument('--tui', default=False, action='store_true',
+                            help='if given, GDB uses -tui')
+        parser.add_argument('--board-id',
+                            help='ID of board to flash, default is to prompt')
+
+    @classmethod
+    def create_from_args(cls, args):
+        daparg = os.environ.get('PYOCD_DAPARG')
+        if daparg:
+            print('Warning: setting PYOCD_DAPARG in the environment is',
+                  'deprecated; use the --daparg option instead.',
+                  file=sys.stderr)
+            if args.daparg is None:
+                print('Missing --daparg set to {} from environment'.format(
+                          daparg),
+                      file=sys.stderr)
+                args.daparg = daparg
+
+        return PyOcdBinaryRunner(
+            args.target, flashtool=args.flashtool, gdb=args.gdb,
+            gdbserver=args.gdbserver, gdb_port=args.gdb_port, tui=args.tui,
+            bin_name=args.kernel_bin, elf_name=args.kernel_elf,
+            board_id=args.board_id, daparg=args.daparg, debug=args.verbose)
+
     def port_args(self):
         return ['-p', str(self.gdb_port)]
 
-    def create_from_env(command, debug):
-        '''Create runner from environment.
-
-        Required:
-
-        - PYOCD_TARGET: target override
-
-        Optional:
-
-        - PYOCD_DAPARG: arguments to pass to pyocd tool, default is none
-        - PYOCD_BOARD_ID: ID of board to flash, default is to prompt
-
-        Required for 'flash':
-
-        - O: build output directory
-        - KERNEL_BIN_NAME: name of kernel binary
-
-        Optional for 'flash':
-
-        - PYOCD_FLASHTOOL: flash tool path, defaults to pyocd-flashtool
-
-        Required for 'debug':
-
-        - O: build output directory
-        - KERNEL_ELF_NAME
-        - GDB: gdb executable
-
-        Optional for 'debug', 'debugserver':
-
-        - TUI: one additional argument to GDB (e.g. -tui)
-        - GDB_PORT: pyocd gdb port, defaults to 3333
-        - PYOCD_GDBSERVER: gdb server executable, defaults to pyocd-gdbserver
-        '''
-        target = get_env_or_bail('PYOCD_TARGET')
-
-        o = os.environ.get('O', None)
-        bin_ = os.environ.get('KERNEL_BIN_NAME', None)
-        elf = os.environ.get('KERNEL_ELF_NAME', None)
-        bin_name = None
-        elf_name = None
-        if o is not None:
-            if bin_ is not None:
-                bin_name = path.join(o, bin_)
-            if elf is not None:
-                elf_name = path.join(o, elf)
-
-        flashtool = os.environ.get('PYOCD_FLASHTOOL', 'pyocd-flashtool')
-        board_id = os.environ.get('PYOCD_BOARD_ID', None)
-        daparg = os.environ.get('PYOCD_DAPARG', None)
-        gdb = os.environ.get('GDB', None)
-        gdbserver = os.environ.get('PYOCD_GDBSERVER', 'pyocd-gdbserver')
-        gdb_port = os.environ.get('GDB_PORT', DEFAULT_PYOCD_GDB_PORT)
-        tui = os.environ.get('TUI', None)
-
-        return PyOcdBinaryRunner(target, flashtool=flashtool, gdb=gdb,
-                                 gdbserver=gdbserver, gdb_port=gdb_port,
-                                 tui=tui, bin_name=bin_name, elf_name=elf_name,
-                                 board_id=board_id, daparg=daparg, debug=debug)
-
     def do_run(self, command, **kwargs):
         if command == 'flash':
             self.flash(**kwargs)
diff --git a/scripts/support/runner/qemu.py b/scripts/support/runner/qemu.py
index 8bea47e..4ad79da 100644
--- a/scripts/support/runner/qemu.py
+++ b/scripts/support/runner/qemu.py
@@ -17,9 +17,13 @@
     def name(cls):
         return 'qemu'
 
-    def create_from_env(command, debug):
-        '''Create runner. No environment dependencies.'''
-        return QemuBinaryRunner()
+    @classmethod
+    def do_add_parser(cls, parser):
+        pass                    # Nothing to do.
+
+    @classmethod
+    def create_from_args(command, args):
+        return QemuBinaryRunner(debug=args.verbose)
 
     def do_run(self, command, **kwargs):
         if command == 'debugserver':
diff --git a/scripts/support/runner/xtensa.py b/scripts/support/runner/xtensa.py
index 3098e6b..ad0897e 100644
--- a/scripts/support/runner/xtensa.py
+++ b/scripts/support/runner/xtensa.py
@@ -6,7 +6,7 @@
 
 from os import path
 
-from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps
 
 
 class XtensaBinaryRunner(ZephyrBinaryRunner):
@@ -25,20 +25,15 @@
     def capabilities(cls):
         return RunnerCaps(commands={'debug'})
 
-    def create_from_env(command, debug):
-        '''Create runner from environment.
+    @classmethod
+    def do_add_parser(cls, parser):
+        parser.add_argument('--xcc-tools', required=True,
+                            help='path to XTensa tools')
 
-        Required:
-
-        - XCC_TOOLS: path to Xtensa tools
-        - O: build output directory
-        - KERNEL_ELF_NAME: zephyr kernel binary in ELF format
-        '''
-        xt_gdb = path.join(get_env_or_bail('XCC_TOOLS'), 'bin', 'xt-gdb')
-        elf_name = path.join(get_env_or_bail('O'),
-                             get_env_or_bail('KERNEL_ELF_NAME'))
-
-        return XtensaBinaryRunner(xt_gdb, elf_name)
+    @classmethod
+    def create_from_args(command, args):
+        xt_gdb = path.join(args.xcc_tools, 'bin', 'xt-gdb')
+        return XtensaBinaryRunner(xt_gdb, args.kernel_elf, args.verbose)
 
     def do_run(self, command, **kwargs):
         gdb_cmd = (self.gdb_cmd + [self.elf_name])
diff --git a/scripts/support/zephyr_flash_debug.py b/scripts/support/zephyr_flash_debug.py
index a600465..12acea2 100755
--- a/scripts/support/zephyr_flash_debug.py
+++ b/scripts/support/zephyr_flash_debug.py
@@ -14,45 +14,68 @@
 such as OpenOCD, pyOCD, etc.
 """
 
+import argparse
+import functools
 import sys
 
-from runner.core import ZephyrBinaryRunner, get_env_bool_or
+from runner.core import ZephyrBinaryRunner
 
 
-# TODO: Stop using environment variables.
-#
-# Migrate the build system so we can use an argparse.ArgumentParser and
-# per-flasher subparsers, so invoking the script becomes something like:
-#
-#   python zephyr_flash_debug.py openocd --openocd-bin=/openocd/path ...
-#
-# For now, maintain compatibility.
-def run(runner_name, command, debug):
+def runner_handler(cls, args):
+    runner = cls.create_from_args(args)
+    # This relies on ZephyrBinaryRunner.add_parser() having command as
+    # its single positional argument; see its docstring for details.
+    runner.run(args.command)
+
+
+def main():
+    # Argument handling is split into a two-level structure, with
+    # common options to the script first, then a sub-command (i.e.  a
+    # runner name), then options and arguments for that sub-command
+    # (like 'flash --some-option=value').
+    #
+    # For top-level help (including a list of runners), run:
+    #
+    # $ZEPHYR_BASE/.../SCRIPT.py -h
+    #
+    # For help on a particular RUNNER (like 'pyocd'), run:
+    #
+    # $ZEPHYR_BASE/.../SCRIPT.py RUNNER -h
+    #
+    # For verbose output, use:
+    #
+    # $ZEPHYR_BASE/.../SCRIPT.py [-v|--verbose] RUNNER [--runner-options]
+    #
+    # Note that --verbose comes *before* RUNNER, not after!
+    top_parser = argparse.ArgumentParser()
+    top_parser.add_argument('-v', '--verbose',
+                            default=False, action='store_true',
+                            help='If set, enable verbose output.')
+    sub_parsers = top_parser.add_subparsers(dest='runner')
+
+    # Add a sub-command for each runner. (It's a bit hackish for runners
+    # to know about argparse, but it's good enough for now.)
+    handlers = {}
+    for cls in ZephyrBinaryRunner.get_runners():
+        if cls.name() in handlers:
+            print('Runner {} name is already a sub-command'.format(cls.name()),
+                  file=sys.sterr)
+            sys.exit(1)
+        sub_parser = sub_parsers.add_parser(cls.name())
+        cls.add_parser(sub_parser)
+        handlers[cls.name()] = functools.partial(runner_handler, cls)
+
+    args = top_parser.parse_args()
+    if args.runner is None:
+        runners = ', '.join(handlers.keys())
+        print('Missing runner; choices: {}'.format(runners), file=sys.stderr)
+        sys.exit(1)
     try:
-        runner = ZephyrBinaryRunner.create_runner(runner_name, command, debug)
-    except ValueError:
-        print('runner {} is not available or does not support {}'.format(
-                  runner_name, command),
-              file=sys.stderr)
+        handlers[args.runner](args)
+    except Exception as e:
+        print('Error: {}'.format(e), file=sys.stderr)
         raise
 
-    runner.run(command)
-
 
 if __name__ == '__main__':
-    commands = {'flash', 'debug', 'debugserver'}
-    debug = True
-    try:
-        debug = get_env_bool_or('VERBOSE', False)
-        if len(sys.argv) != 3 or sys.argv[2] not in commands:
-            raise ValueError('usage: {} <runner-name> <{}>'.format(
-                sys.argv[0], '|'.join(commands)))
-        run(sys.argv[1], sys.argv[2], debug)
-    except Exception as e:
-        if debug:
-            raise
-        else:
-            print('Error: {}'.format(e), file=sys.stderr)
-            print('Re-run with VERBOSE=1 for a stack trace.',
-                  file=sys.stderr)
-            sys.exit(1)
+    main()